Personal fork of prime agent. Mirror of: https://mnemosyne.sigilzero.dev/sigilzero/prime-agent

fix: withdraw background command notices the model already read (#2159)

* feat(coding-agent): keep the system prompt static across refinements and deliver harness state in-context Fixes RES-1318. Applying a refinement no longer rebuilds and swaps agent.state.systemPrompt, so the provider prefix cache survives every refinement. Instead: - _applyRefine appends one durable refinement_notice custom message per applied refinement at the apply seam, labeled [auto-refinement] / [user-refinement] / [self-refinement] by source, with the trigger line and applied edits in the digest's own notation (rollbacks print as rollbacks). Zero-applied-edit refinements emit nothing and the notice never starts a turn. convertToLlm passes the new type through as a user message while the refinement_outcome audit message stays filtered and untouched. - The harness digest injection is removed from the system prompt entirely (buildSystemPrompt loses the harnessState option). The digest now appears at cold context boundaries only, reusing formatHarnessStateForPrompt byte-identically: fresh sessions get a digest custom message as the first context message; compaction attaches a digest snapshot to the compaction entry mechanically (never through the summarizer) so the post-compaction head message renders memories-first before the summary on both the initial and update-merge paths; resumes append a fresh digest at the tail only when the newest digest in live context no longer matches disk state (identity dedupe), which is also the migration path for pre-change sessions. The new compaction entry field and custom message types ride the existing schema-tolerant JSON entry stream (optional field, generic custom messages): backward-compatible, no daemon protocol or schema revision change. * test(coding-agent): adapt message-sequence pins to the session-start harness digest Every session now opens with a harness_digest custom message, so tests that pinned exact message sequences, first-message identity, provider-context text lists, or entry counts see one extra leading custom message. Adds a conversationMessages helper to the suite harness (filters the digest) and updates the affected assertions; provider-context probes now select the last user message instead of the first. * fix(coding-agent): harden harness digest recency and keep digests out of the summarizer Review fixes on #2098: - _latestContextHarnessDigest picked the last digest by array position, but retained pre-compaction messages are presented after the compaction head while being chronologically older, so an old retained digest could defeat resume dedupe. Recency is now the greatest message timestamp among all in-context digest carriers. - The session-start harness digest entry was fed to the first compaction's summarizer as an ordinary custom message. prepareCompaction now excludes harness_digest entries from summarizer input; the digest is only attached mechanically at the new compaction head. * fix(coding-agent): label non-serialized refine.run notices as self-refinement Review fix on #2098: _consumePendingRequestedRefine forwarded agent-callable refine.run requests through refine() without a source, so the notice carried the [user-refinement] label in non-serialized sessions. refine() now accepts an explicit source override and the pending-request consumer passes "self". * test(coding-agent): pin the self source on the pending refine.run consumer * fix(coding-agent): treat the digest as boundary-injected mechanical context everywhere Review round on #2098 (three findings): - Untouched sessions stay empty: the fresh-session digest is now injected lazily at the first committed turn instead of at construction, so abandoned-draft cleanup and raw message-count emptiness checks (daemon-session-list lifecycle/activity, isEmptyDraftContent, branch seedability, hasExistingSession) keep seeing an empty session. Non-empty contexts (resume) still inject at construction with identity dedupe. - Tree navigation is a cold boundary: navigateTree rebuilds the context, so it now runs the same refresh-iff-stale digest check as resume (fork/clone/switch construct a new session and were already covered by the constructor path). - Headless terminal selection skips harness_digest like other non-output customs, so resume-and-print returns the saved final assistant output. Sweep of other message-kind-sensitive consumers: branch summarization now excludes harness_digest from summarizer input (same rule as compaction); estimateContextTokens and side-question seeding intentionally keep the digest (it is live context); _repliedToParentSinceTask and getUserMessagesForForking are kind-filtered already. * chore(coding-agent): trim narrative comments and redundant test asserts on the digest work Comments cut to one-line invariant guards; duplicate digest-notation and content asserts removed where a kept unit pin already covers the same rendering (labels and notation stay pinned in refinement-outcome-message.test, source plumbing stays pinned per path). No behavioral change. * fix(coding-agent): scope the first-turn digest to the turn that delivers it Review fix on #2098 (Bugbot thread 2919fcf3): the first-turn digest was persisted directly at commit, before agent.prompt ran, so cancelling or clearing that first turn stripped the turn's captured messages but left the digest in state and in the session file - resurfacing the emptiness-check class through the cancel path. The digest now rides the turn's next-turn delivery records: it persists at message_end like the rest of the turn, first-turn cancellation strips and restores it with the turn, and the lazy injection re-delivers on the next committed turn. * test(coding-agent): event-order pins include the digest custom pair on the first turn * fix(coding-agent): deliver the first-turn digest on skip-policy turns and re-arm on cancel Two follow-up review findings on the lazy-injection shape: - Custom-triggered turns use the skip next-turn-context policy, so a fresh session whose first turn was sendCustomMessage(triggerTurn) ran without the digest. The pending digest now attaches on every committed first turn; skip policy still withholds ordinary pending next-turn messages. - A cancelled first turn whose digest record was already durable dropped the digest from live context without restoring it (restorable records skip durable ones). Lazy injection now owns digest delivery uniformly: cancelled turns never restore the digest message and instead re-arm the pending flag, so the next committed turn re-delivers a fresh one. * fix(coding-agent): never park the harness digest as pending next-turn context Review fix on #2098 (Bugbot skip-turn-digest-lost-on-failure): a failed commit parked the undelivered digest in _pendingNextTurnMessages, which skip-policy custom-trigger turns never drain, and a parked copy plus a re-armed injection could double-deliver. Parking now filters the digest out and re-arms _harnessDigestPending instead, keeping lazy injection the single owner of digest delivery. * feat(coding-agent): unify synthetic user-channel messages under one bracket grammar Every machine-injected user-channel message now opens with one grammar: `[<kind>(: <qualifier>)( <address>)]`, a blank line, then the payload. - agent_message: `[agent-message from <relationship>:<name>]`; message id and endpoint ids live in details only. Detection keys on customType; the old header parser stays for persisted legacy transcripts (accept-both, emit-new). - heartbeat_prompt: `[heartbeat: <schedule> run#<n>]` instead of the raw prompt masquerading as the user. - async_bash_completion: `[bash-done pid:<pid> exit:<code>]`; the standing BashHandle inspection hint is gone (it duplicated the system prompt). - rlm child notices: `[child-exited: no-reply|cancelled child:<name>]` and `[child-failed child:<name>]`. - goal_context: `[goal: continuation|budget-limit|objective-updated]` replaces the <goal_context> XML wrapper. - ipython_state_restored: `[python-state-restored]` label (customType keeps its string); goal prose says "Python REPL" instead of "ipython". - compactionSummary/branchSummary/harness digest prefixes gain `[compaction-summary]`/`[branch-summary]`/`[harness-digest]` first lines. clearQueuedAgentMessages now matches queued actions by the delivered custom message's customType, with the legacy text parser as fallback. Fixes RES-1321 * fix(coding-agent): sanitize names interpolated into bracket headers One owner (sanitizeMessageHeaderValue) strips brackets, newlines, commas, and ":" from header-interpolated names: an RLM child name can no longer close the [child-failed]/[child-exited] header early, and an unlabeled sender named "parent:root" can no longer render as a relationship label. Addresses the Bugbot/Macroscope review findings on PR 2115. * fix(coding-agent): keep the heartbeat header on one line for whitespace-bearing schedules parseAgentCronSchedule's every/in regexes accept embedded newlines, so the schedule shown in the [heartbeat: ...] header goes through the shared header sanitizer; the exact expression stays in details.schedule. * fix(coding-agent): bring the remaining built-in injectors into the bracket grammar Review sweep of every built-in customType against convertToLlm found three model-visible kinds still emitting unmarked content: - autonomous_status -> `[autonomous-status: on|off]` + usage payload - autonomous continuations -> `[autonomous-continuation]` + prompt, and `[autonomous-continuation: gate-failed]` owned by buildAutonomousGateFailureContinuation (covers the headless injector too) - prime-agent.update_complete -> `[update-complete]` + notice - ipython_state (kernel persisted through compaction) -> `[python-state]` All other custom types are either grammar-compliant or filtered out of LLM context by convertToLlm / entry-only. A table-driven pin now walks every constructor-backed synthetic kind and asserts a grammar-conforming first line. * test(coding-agent): adapt daemon-mode and model-extension pins to the bracket grammar The full CI matrix (first run since the PR retargeted main) caught two expectations outside test/suite still pinning pre-grammar text: the retained- subagent send now asserts the exact `[agent-message from ...]` prompt, and the model_select injected-prompt pin carries the `[heartbeat: ...]` header. * feat(runtime): tell the host when a background command result is read A background command usually exits before the model looks at its handle, so the completion notice is queued while the model is already reading the result in the next cell. Track that read on the handle: reads before the notice is dispatched cancel it outright, and reads after the host accepted it send one bash.consumed request so the host can withdraw the queued notice. Only reads that reach the model count: the running command's buffer, and awaits by consumers that outlive their cell, are not result reads. * feat(coding-agent): withdraw background command notices the model already read A queued completion notice for a command whose result the kernel already read is noise: it re-announces work the model just inspected and starts an extra turn for it. Handle bash.consumed by dropping the matching notice while it is still queued; delivered notices are no longer clearable, so nothing is retracted after the model saw them, and an unread completion is delivered as before. Notices are matched on pid and command because pids are reused across handles. The queue and transcript label becomes "Background command finished", which describes what happened instead of naming the transport. * fix(runtime): only count result reads that a live cell receives The first version marked a finished handle as read on any output/tail/poll call, so a detached watcher polling the handle between turns withdrew the completion notice. An idle session then never learned the command finished: the notice was its only wake-up. Decide it from one rule instead, in one place: a read counts only while a cell is executing, because the value and anything the reader prints ride that cell's turn, and an awaiting reader must also be one that live cell waits for, since a discarded or detached wrapper task awaits a value nobody sees. The live cell comes from the kernel's own active-cell state rather than the cell contextvar, which detached tasks keep long after their cell ended. * fix(coding-agent): withdraw one notice per result read A read belongs to one handle, so it may retire one notice. Cancelling every action that matches the pid and command meant a pid reused for an identical command took a second, unread notice down with the first. Withdraw the earliest match only, which is the notice of the older handle the read belongs to. The RLM docs still taught the old rule (await in the creating cell to suppress) and the old "Shell message" name, so they now describe the withdrawal and which reads count. * fix: drop function duplicated by the main merge * test: trim redundant pins and slim comments


Author Sebastian Müller Committer GitHub Date Commit Parent fb2db8ee
+302 -14
11 changed files
+302 -14
11 changed files