@sema-agent/core 7.3.1 → 7.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/agents/peer-admission.d.ts +18 -3
  3. package/dist/agents/peer-admission.js +79 -4
  4. package/dist/agents/peer-held-queue.d.ts +101 -0
  5. package/dist/agents/peer-held-queue.js +229 -0
  6. package/dist/agents/peer-idle.d.ts +109 -0
  7. package/dist/agents/peer-idle.js +240 -0
  8. package/dist/agents/peer-notice-route.d.ts +33 -0
  9. package/dist/agents/peer-notice-route.js +46 -0
  10. package/dist/agents/peer-notices.d.ts +103 -0
  11. package/dist/agents/peer-notices.js +206 -0
  12. package/dist/agents/peer-session-drain.d.ts +39 -4
  13. package/dist/agents/peer-session-drain.js +248 -42
  14. package/dist/agents/send-message-tool.d.ts +8 -1
  15. package/dist/agents/send-message-tool.js +96 -30
  16. package/dist/agents/subagent.js +1 -0
  17. package/dist/core/auto-mode-defaults.d.ts +11 -0
  18. package/dist/core/auto-mode-defaults.js +2 -0
  19. package/dist/core/auto-mode.d.ts +59 -0
  20. package/dist/core/auto-mode.js +57 -1
  21. package/dist/core/checkpoint-store.js +2 -2
  22. package/dist/core/governance-codes.d.ts +1 -1
  23. package/dist/core/governance-codes.js +8 -0
  24. package/dist/core/hooks.d.ts +30 -0
  25. package/dist/core/hooks.js +43 -8
  26. package/dist/core/mailbox-store.d.ts +33 -1
  27. package/dist/core/mailbox-store.js +42 -2
  28. package/dist/core/runner/assemble-result.d.ts +5 -0
  29. package/dist/core/runner/assemble-result.js +1 -1
  30. package/dist/core/runner/denial-limit-arms.d.ts +149 -0
  31. package/dist/core/runner/denial-limit-arms.js +91 -0
  32. package/dist/core/runner/edited-files-ledger.d.ts +33 -0
  33. package/dist/core/runner/edited-files-ledger.js +14 -0
  34. package/dist/core/runner/prepare-hands-readface.d.ts +5 -0
  35. package/dist/core/runner/prepare-hands-readface.js +1 -0
  36. package/dist/core/runner/prepare-task.d.ts +62 -1
  37. package/dist/core/runner/prepare-task.js +120 -89
  38. package/dist/core/runner/runtask.js +10 -0
  39. package/dist/core/sensitive-path-policy.d.ts +27 -6
  40. package/dist/core/sensitive-path-policy.js +57 -2
  41. package/dist/core/task-notification.d.ts +24 -2
  42. package/dist/core/task-notification.js +6 -1
  43. package/dist/core/tool-policy.d.ts +55 -4
  44. package/dist/core/tool-policy.js +28 -5
  45. package/dist/core/types.d.ts +207 -10
  46. package/dist/index.d.ts +10 -5
  47. package/dist/index.js +8 -3
  48. package/dist/orchestration/workflow.js +7 -3
  49. package/dist/tools/fs/fs-write.d.ts +4 -4
  50. package/dist/tools/fs/fs-write.js +30 -11
  51. package/dist/tools/fs/index.d.ts +7 -1
  52. package/dist/tools/fs/index.js +1 -1
  53. package/dist/tools/fs/safety.d.ts +29 -8
  54. package/dist/tools/fs/safety.js +11 -1
  55. package/package.json +1 -1
  56. package/test/export-surface.snapshot.json +169 -1
@@ -733,6 +733,32 @@ export interface ToolExecuteContext {
733
733
  * the tool runs outside a Runner task.
734
734
  */
735
735
  parentCwd?: string;
736
+ /**
737
+ * The RUNNING task's file-history LINEAGE (Runner-filled, read-only, never a model/tool argument;
738
+ * present iff a `RunnerDeps.fileHistoryStore` is live for the run): the history scope this run
739
+ * records its first-touch edits into and the TREE those records' keys are minted against — the
740
+ * canonical root spelling plus the env's filesystem identity (`fs`: a remote workspace's handle,
741
+ * `"host"` for the control-plane host's filesystem, else a per-env-instance token).
742
+ *
743
+ * WHICH children are threaded it, exactly: the Agent tool's FRESH spawn legs (sync / steer /
744
+ * background / fork) pass it VERBATIM into the child's trusted internals
745
+ * (`RunInternals.fileHistoryLineage`), which is how a same-process delegation tree keeps ONE
746
+ * history: a child on the SAME tree (both coordinates equal) records into the lineage's scope (the
747
+ * root session's), so the root session's `rewindFilesTo` reaches the child's edits; a child on a
748
+ * different tree (worktree isolation, an explicit `cwd`, a fresh per-task sandbox) starts its own
749
+ * lineage, because root-relative keys re-joined against a different tree would name files that run
750
+ * never edited. Three delegated legs deliberately carry NO lineage and keep their own scope: the
751
+ * Agent tool's REVIVE arm (a revival's lineage would be the REVIVER's tree, which says nothing
752
+ * about the revived row's), and the workflow / team orchestration spawn legs (`run_workflow`,
753
+ * `runTeamDiscussion` — they build their children's internals without this seat). Edits made by
754
+ * those children are therefore NOT reachable from the root session's rewind, and a restore request
755
+ * on one of them is not refused either — it converges that child's own scope.
756
+ */
757
+ fileHistoryLineage?: {
758
+ scope: string;
759
+ root: string;
760
+ fs: string;
761
+ };
736
762
  /**
737
763
  * design/319 (A ticket) — the RUNNING task's reminder provenance mark, Runner-filled, read-only,
738
764
  * NEVER a model/tool argument. The Agent tool's FORK route threads it into the forked child's
@@ -1299,6 +1325,35 @@ export type TrackEditResult = {
1299
1325
  refusal: string;
1300
1326
  };
1301
1327
  export type TrackFileEditHook = (req: TrackEditRequest) => Promise<TrackEditResult>;
1328
+ /**
1329
+ * The hands band's MUTATION-EDITED observation seat — the counterpart of {@link TrackEditRequest},
1330
+ * fired at the other end of the same lane. Called once per Write/Edit/NotebookEdit call whose FINAL
1331
+ * env write did not PROVABLY write nothing, at the single point every mutation lane funnels through,
1332
+ * under the same three-valued law as the first-touch record's own retraction:
1333
+ * - a call the argument checks, the read-before-edit gate, the write gate or the first-touch
1334
+ * history seat refused never fires it (nothing was written);
1335
+ * - a call whose write FAILED with a code whose contract says nothing was written never fires it;
1336
+ * - a call whose write failed AMBIGUOUSLY (a non-atomic env that truncated and then errored) or
1337
+ * THREW fires it — exactly the arm where the retraction KEEPS the first-touch record because the
1338
+ * file may really have changed. The two seats agree on purpose: a file whose baseline was kept
1339
+ * for a possible modification must also be listed as possibly modified;
1340
+ * - a `Bash` command that changed a file never fires it: bash does not run through this band's
1341
+ * write lanes at all, which is the whole reason this seat is not a tool-name table.
1342
+ * PURE OBSERVATION: it returns nothing and it cannot refuse. A FAULT in it is contained in both
1343
+ * shapes an observer can fail in — a synchronous throw, and (the return type is `void`, but an
1344
+ * `async` function still type-checks there) a rejected promise, which is sunk rather than left
1345
+ * unhandled. Neither is awaited: an observer must never turn a landed write into a failed tool
1346
+ * answer, nor delay one.
1347
+ */
1348
+ export interface FileEditedNotice {
1349
+ tool: "Write" | "Edit" | "NotebookEdit";
1350
+ /** The model-supplied path argument — the SAME coordinate the delegated-child projection's
1351
+ * `SubagentEditedFile.path` carries, so the two seats can be read side by side. */
1352
+ path: string;
1353
+ /** The resolved canonical containment key the bytes actually landed on. */
1354
+ key: string;
1355
+ }
1356
+ export type FileEditedHook = (notice: FileEditedNotice) => void;
1302
1357
  /**
1303
1358
  * design/141 件2 — the SAFE deployment-configurable subset of the hands toolkit ({@link RunnerDeps.hands}).
1304
1359
  * Only fields whose injection is purely additive for a deployment are here; Runner-internal orchestration
@@ -2130,6 +2185,15 @@ export interface TaskSpec {
2130
2185
  * target with no history boundary fails loud (`rewind_snapshot.unresolvable`). Requires
2131
2186
  * `RunnerDeps.fileHistoryStore`. The session leaf is untouched — the next turn continues the
2132
2187
  * CURRENT conversation, only the files moved.
2188
+ *
2189
+ * A same-process child THE AGENT TOOL SPAWNED FRESH (the legs that are handed a lineage — see
2190
+ * `ToolExecuteContext.fileHistoryLineage` for the exact set, and for the legs that are not) which
2191
+ * shares its root session's tree records its edits into the ROOT session's history, so a rewind
2192
+ * that should cover those edits is the root session's action; a restore request on such a child run
2193
+ * is refused loud (`rewind.child_scope_unsupported`) rather than converging a scope that holds none
2194
+ * of its edits. A delegated child that was handed NO lineage — a revival, a workflow/team member —
2195
+ * keeps its own scope: its edits are outside this target's reach, and its own restore request is an
2196
+ * ordinary one.
2133
2197
  */
2134
2198
  rewindFilesTo?: string;
2135
2199
  /**
@@ -3532,7 +3596,7 @@ export interface TaskResult {
3532
3596
  * ignores the field is unchanged. A rewind that could NOT be delivered as asked is NOT a note — it is a
3533
3597
  * terminal failure (`rewind_snapshot.unresolvable` / `rewind.store_unconfigured` /
3534
3598
  * `rewind.restore_failed` / `rewind.invalid_spec` / `rewind.rewind_files_retired` /
3535
- * `rewind.conflicting_targets`).
3599
+ * `rewind.conflicting_targets` / `rewind.child_scope_unsupported`).
3536
3600
  *
3537
3601
  * - `conversation_only` — {@link TaskSpec.resumeAt} branched the transcript WITHOUT
3538
3602
  * {@link TaskSpec.restoreFiles}, so the working tree was deliberately left where it was (CC's
@@ -3555,6 +3619,56 @@ export interface TaskResult {
3555
3619
  /** Human-readable statement of what did NOT happen and why — safe to show a user verbatim. */
3556
3620
  message: string;
3557
3621
  }>;
3622
+ /**
3623
+ * The files THIS RUN's hands actually mutated, with how many times each — the same
3624
+ * `{path, edits}` shape the delegated-child projection publishes as
3625
+ * {@link import("../agents/subagent-steps.js").SubagentEditedFile}, so a host can read the two
3626
+ * seats side by side. A run is one host-side user message, which is the granularity a
3627
+ * "restore the code this message changed" affordance needs; the per-model-turn beat
3628
+ * (`turn_end`) deliberately has no such seat.
3629
+ *
3630
+ * **Source**: the hands band's own mutation lane. One entry is bumped when a `Write`/`Edit`/
3631
+ * `NotebookEdit` call's FINAL env write returns success — not when a tool call starts, not from a
3632
+ * tool-name table over the event stream. The consequences are the contract:
3633
+ * - a refused, gated or FAILED write is not counted (a write whose bytes never landed is not an
3634
+ * edit — the same lane point that retracts its first-touch history record);
3635
+ * - a `Bash` command that changed a file is not counted: it does not go through those lanes.
3636
+ * This matches the reference implementation's own trigger set;
3637
+ * - one tool CALL counts once, so a batch `Edit` (multiple `edits[]` against one file in one
3638
+ * call) is one edit here, exactly as the child projection counts it.
3639
+ *
3640
+ * **`path` form**: the model-supplied path argument, verbatim — the same coordinate
3641
+ * `SubagentEditedFile.path` uses, deliberately not the canonical containment key, so both seats
3642
+ * spell a file the way the transcript does. IDENTITY, however, is the canonical FILE, not the
3643
+ * spelling: a relative argument resolves against the run's LIVE cwd, so one spelling can name two
3644
+ * different files across a `cd` (two entries, each showing that spelling) and two spellings can
3645
+ * name one file (ONE entry, showing the first spelling that reached it). Insertion order = order
3646
+ * of first edit.
3647
+ *
3648
+ * **Three-valued on the failure edge, upper bound never lower**: a write failure is three-valued.
3649
+ * A failure whose error code's contract states nothing was written is a proven no-op and is NOT
3650
+ * counted. An AMBIGUOUS failure or a thrown write IS counted: an `ExecutionEnv` whose write is not
3651
+ * atomic can truncate a file and then report an error, that file may really have changed, and the
3652
+ * first-touch history record for it is kept for exactly that reason — so this list names it too,
3653
+ * and a `rewindFilesTo` will restore it. (The reference env replaces whole files by stage+rename —
3654
+ * fully old or fully new — so this edge belongs to its in-place fallback arms and to third-party
3655
+ * envs.) A host therefore reads this list as "files that may differ from before this run", never
3656
+ * as "writes the model saw succeed"; the tool answers carry that.
3657
+ *
3658
+ * **In-presence condition**: present iff this run's hands landed ≥1 such write — zero edits means
3659
+ * the key is ABSENT, never an empty array. It does NOT depend on whether a
3660
+ * `RunnerDeps.fileHistoryStore` is wired: this is an observation of what this run did, not a
3661
+ * durable history, and a deployment with no history store still gets it (what a store adds is the
3662
+ * ability to REWIND, not the ability to say what changed). It covers this run's OWN band only — a
3663
+ * delegated child's writes ride the child's own result and the delegation projection, not this
3664
+ * key.
3665
+ *
3666
+ * **Bound**: at most 1000 distinct files. Past that, already-listed files keep counting and new
3667
+ * ones are not added — a result seat cannot be unbounded, and a single user message reaching a
3668
+ * thousand distinct files is already outside what this observation is for. A consumer that must
3669
+ * know whether it is reading a saturated list can compare the length against that ceiling.
3670
+ */
3671
+ editedFiles?: import("../agents/subagent-steps.js").SubagentEditedFile[];
3558
3672
  /**
3559
3673
  * Present (`true`) exactly when the run's FINAL turn was halted by a person's BARE rejection of a
3560
3674
  * tool call — the parent-thread control-flow boundary: the rejected call's same-message siblings
@@ -5954,6 +6068,20 @@ export interface EngineNotice {
5954
6068
  * (whoever answered the card is the one entitled to hear the answer ran nothing); a host may
5955
6069
  * forward it on its own wire.
5956
6070
  *
6071
+ * - `"classifier.denial_limit"` (#548, CC 2.1.250 `FO`/"too many classifier denials in headless
6072
+ * mode") — the auto-mode classifier's DENIAL LIMIT was reached (3 consecutive blocks, or 20 in the
6073
+ * run; `RunnerDeps.autoMode.denialLimit`) and the fallback ask it turns into had NO approver to go
6074
+ * to: none wired, a blanket `onAsk:"allow"` (refused for a real-approval ask), or an approver that
6075
+ * reported unavailable with no durable park to take it. The run was STOPPED — `TaskResult.status`
6076
+ * `"failed"`, `errorCode` the same code, `errorMessage` the limit sentence — rather than kept
6077
+ * spending on a model the classifier denies without end; the triggering call's own result is the
6078
+ * deny that stood. Once per run (the first headless fallback owns the terminal);
6079
+ * `detail: { sessionId, runId, toolName, toolCallId, consecutive, total, limit }` — `limit` is
6080
+ * `"consecutive"` | `"total"`, the bound that tripped. Audience `"user"` (the person whose run
6081
+ * ended is the one entitled to hear why, and to review the transcript the sentence points at).
6082
+ * With an approver wired the same bound mints NO notice — the fallback ask reaches the person
6083
+ * instead (`AskRequest.denialLimitFallback`).
6084
+ *
5957
6085
  * - `"steering.parked_input_blocked"` (design/373 §4.3) — a PARKED steer entry was withheld when
5958
6086
  * a resume redelivered it (any resume kind that drains parked steers — wake included) by the
5959
6087
  * deployment's `userPromptSubmit` screen (block verdict, or a fail-closed non-answer/crash):
@@ -6125,23 +6253,61 @@ export interface EngineNotice {
6125
6253
  * disposition table with the error dispositions. The loser MUST NOT retry into the winner's
6126
6254
  * account. A consumer diffing its own table against the catalog must not add a row for it.
6127
6255
  *
6256
+ * - `"config.peer_admission_out_of_range"` (#551) — a `RunnerDeps.peerAdmission` field the
6257
+ * resolver could not use: outside its legal range, or not a finite number at all (a string off
6258
+ * an untyped host's wiring, `NaN`, `Infinity`). THAT FIELD falls back to its own default (never
6259
+ * a clamp to the nearest edge, never a whole-config reject) and every other field is unaffected.
6260
+ * One notice PER FIELD — two bad fields in one config are two notices — deduped per SINK on
6261
+ * `(field, given)` rather than per call (the resolver runs on every SendMessage and every drain
6262
+ * round; the fact is about the wiring, not about the call). Per SINK, not per process: two
6263
+ * Runners in one process each own an `onNotice`, and a process-global ledger would deliver the
6264
+ * fact to whichever resolved first and leave the other silently without its own disclosure. The
6265
+ * unwired arm (no `onNotice`: the shared `console.warn` throat) latches per process instead —
6266
+ * the console IS one process-wide channel. `detail: { field, given, default,
6267
+ * range: [lo, hi], reason }`, where `reason` is `"out_of_range" | "not_a_finite_number"`.
6268
+ * Audience `"operator"` (a configuration fact; the fix is the deployment's).
6128
6269
  * - `"config.peer_lane_unmounted"` (design/385 §1.2⑥) — `RunnerDeps.peerDirectory` is wired but
6129
6270
  * the cross-session lane could not mount on this leg: no `mailboxStore`, or one that does not
6130
6271
  * declare `crossProcessSafe: true` (several terminals would share one session box on luck). The
6131
6272
  * seat is inert for the run (no ListAgents, peer addresses refuse with the same reason). Once per
6132
6273
  * prepared leg; `detail: { reason, code, mailboxWired, sessionId, runId }`. Audience `"operator"`
6133
6274
  * (a wiring fact; the fix is the deployment's).
6134
- * - `"peer.inbound_disposition"` (design/385 §1.2④ / §4.6) — a message parked in THIS session's
6135
- * own box was settled at the drain WITHOUT reaching the model. `detail.disposition` is the arm:
6136
- * `"refused"` (this session's `crossSessionInbound` setting, or a record whose typed fields
6137
- * cannot be rendered canonically), `"admission_refused"` (the drain-stage admission re-check:
6138
- * duplicate / hop loop / runaway — `cause` names it), `"notice_unrouted"` (a notice-kind record
6139
- * idle/delivery notice met a build with no notice face: settled without delivery, never
6140
- * ridden through the peer-message envelope), or `"held"` (mode parity held it; the
6141
- * held-message review face is not mounted in this build, so the message STAYS PARKED and the
6142
- * drain stops at it disclosed once per seq per leg). `detail: { disposition, cause, seq, box,
6275
+ * - `"peer.inbound_disposition"` (design/385 §1.2④ / §4.6 / §4.4) — a message parked in THIS
6276
+ * session's own box was settled at the drain WITHOUT reaching the model. `detail.disposition` is
6277
+ * the arm (closed set `PeerInboundDisposition`): `"refused"` (this session's `crossSessionInbound`
6278
+ * setting, or a record whose typed fields cannot be rendered canonically), `"admission_refused"`
6279
+ * (the drain-stage admission re-check: duplicate / hop loop / runaway — `cause` names it),
6280
+ * `"notice_unrouted"` (a notice-kind record the notice face cannot render no typed `notice`, or
6281
+ * a state that does not fit its kind settled without delivery, never ridden through the
6282
+ * peer-message envelope), `"notice_user_only"` (a delivery receipt / idle notice whose sender this
6283
+ * session's parity would HOLD: surfaced here for the user, not read by the model CC's
6284
+ * `modelVisible:false` arm; `cause` carries the rendered notice text), or `"held"` — the parity
6285
+ * judgment held it: the message ENTERED this session's process-level held queue and was acked
6286
+ * from the box (later messages are not blocked behind it), where it is re-judged at every
6287
+ * boundary, released or refused with a receipt, and expires WITH a receipt under `dialogExpiry`
6288
+ * (see `peer.held_settled`); `cause` is a `PeerInboundHoldCause` — the parity causes, or the
6289
+ * drain gate's TRANSIENT `rate_limited` (that one does NOT enter the queue: the message stays in
6290
+ * the box and the drain stops at its seq until the bucket refills). Disclosed once per seq per leg,
6291
+ * and again when a re-judgment moves a held entry's cause. `detail: { disposition, cause, seq, box,
6143
6292
  * fromSession?, sessionId, runId }`. Audience `"user"`: the session's user is the one who did
6144
6293
  * not receive it.
6294
+ * - `"peer.held_settled"` (design/385 §4.4, slice 4) — a message left THIS session's held queue.
6295
+ * `detail.settlement` names how: `"delivered"` (a re-judgment or an approval released it into
6296
+ * this session's model context), `"refused"` (the setting moved to refuse, or an approval met a
6297
+ * refusing policy), `"expired"` (the `dialogExpiry` deadline, an eviction when the 100-entry queue
6298
+ * was full, a cancelled review, or a graceful shutdown), `"denied"` (the review face), `"dropped"`
6299
+ * (the injection lane refused the released frame). `detail.reason` is the trigger
6300
+ * (`deadline | evicted | shutdown | rejudge | approved | denied | cancelled | inject_failed`); the
6301
+ * sender was sent the matching receipt whenever the record carried a reply address.
6302
+ * `detail: { settlement, reason, seq, box, fromSession?, heldCount, sessionId, runId }`. Audience
6303
+ * `"user"`: it is this session's user who held (or was holding) the message.
6304
+ * - `"peer.idle_subscription"` (design/385 §5.2, slice 4) — a peer session asked to be told when
6305
+ * THIS session next goes idle (`SendMessage … notify_when_idle`). `detail.outcome`: `"recorded"`
6306
+ * / `"refreshed"` (a same-requester re-ask; the subscription is one-shot and expires unfired after
6307
+ * 12 h), `"full"` (the 32-entry table is full) or `"refused"` (this session's `crossSessionInbound`
6308
+ * is `refuse`) — the last two answered the requester with an `unavailable` notice. `detail:
6309
+ * { outcome, fromSession, live, seq, box, sessionId, runId }`. Audience `"user"`: the person whose
6310
+ * session is being watched is the one entitled to know (CC announces it in the session UI).
6145
6311
  *
6146
6312
  * - `"delegation.transcript_integrity"` (subagent transcript persistence) — a durable agent row
6147
6313
  * with a BOUND transcript sessionId met a session store that attests `not_found` for it: the
@@ -6891,6 +7057,25 @@ export interface RunnerDeps {
6891
7057
  consecutiveFailures: number;
6892
7058
  lastCause: string;
6893
7059
  }) => void;
7060
+ /**
7061
+ * The classifier DENIAL LIMIT (CC 2.1.250 `FO`/`AKe`): a run whose classifier keeps blocking falls
7062
+ * back to a PERSON instead of being denied without end. Per run: a `block` first increments the
7063
+ * consecutive and total counts and then judges `consecutive >= maxConsecutive || total >= maxTotal`
7064
+ * — the block that reaches a bound is itself the one that becomes an `ask` (the 3rd consecutive
7065
+ * block asks). That ask carries `requiresRealApproval: true` (no automatic lane may clear it — not
7066
+ * a sandbox admission, not an inherited resolver, not a blanket `onAsk:"allow"`) plus the additive
7067
+ * `denialLimitFallback` member with the counts and its own auto-deny window; an unanswered ask
7068
+ * auto-denies after `autoDenyAfterMs` (default 120s; `0` = no window). A classifier allow, or a
7069
+ * person's allow of the fallback ask, zeroes the consecutive count; reaching the total bound
7070
+ * zeroes everything. With no approver wired at all (headless), the fallback has nowhere to go and
7071
+ * the run STOPS with `TaskResult.errorCode = "classifier.denial_limit"` (a notice of the same
7072
+ * code is minted).
7073
+ *
7074
+ * Every member optional (defaults 3 / 20 / 120_000). A present member with a bad value is refused
7075
+ * loudly at prepare — never clamped, never silently read as the default.
7076
+ * See {@link import("./auto-mode.js").AutoModeDenialLimitOptions}.
7077
+ */
7078
+ denialLimit?: import("./auto-mode.js").AutoModeDenialLimitOptions;
6894
7079
  /**
6895
7080
  * #503 — OPT IN to recording this arming's serializable criteria (an
6896
7081
  * {@link import("./auto-mode-arming.js").AutoModeArmingRecipe}) on the constraint-chain entries a
@@ -7051,6 +7236,18 @@ export interface RunnerDeps {
7051
7236
  * everywhere (mode parity, the CC default). Inert without {@link peerDirectory}.
7052
7237
  */
7053
7238
  crossSessionInbound?: import("../agents/cross-session-judge.js").CrossSessionInboundSettingLayers | (() => import("../agents/cross-session-judge.js").CrossSessionInboundSettingLayers);
7239
+ /**
7240
+ * design/385 §4.4 (slice 4) — this deployment's `dialogExpiry` (CC settings key, verbatim vocabulary
7241
+ * `"60s" | "5m" | "10m" | "never"`, default `"5m"`): how long a HELD cross-session message whose cause
7242
+ * a human review could resolve (mode-mismatch / no-mode-asserted / invalid attestation) waits in this
7243
+ * session's held queue before it resolves to its safe no-action default — EXPIRED, dropped WITH a
7244
+ * receipt to the sender, never silently. `"never"` disables the deadline. Read at every drain round
7245
+ * (getter form for a host that re-reads its settings). A value outside the vocabulary is announced
7246
+ * ONCE per leg through `onError` (`classification: "peer-dialog-expiry"`) and the default applies —
7247
+ * a garbage setting never silently reads as a policy. Absent = the default. Inert without
7248
+ * {@link peerDirectory}.
7249
+ */
7250
+ crossSessionDialogExpiry?: import("../agents/peer-notices.js").CrossSessionDialogExpiry | (() => import("../agents/peer-notices.js").CrossSessionDialogExpiry | undefined);
7054
7251
  /**
7055
7252
  * design/164 件五 — DEPLOYMENT-level usage governance: allowances that span TASKS, evaluated per
7056
7253
  * principal (or once for the whole deployment when a task declares none). A different axis from
package/dist/index.d.ts CHANGED
@@ -125,7 +125,7 @@ export { type StoreDurability } from "./core/checkpoint-store.js";
125
125
  export { type StoreFidelity } from "./core/checkpoint-store.js";
126
126
  export { projectHumanInput, buildHumanInputEvent, type HumanInputSource, type HumanInputFrame, type HumanInputEvent, type HumanInputDelivery, } from "./core/human-input-projection.js";
127
127
  export { TaskRegistry, defaultTaskRegistry, createTaskOutputTool, createTaskStopTool, type SemaTaskType, type SemaTaskStatus, type SemaTaskHandle, type ParkedClaimTicket, type TaskAccess, type UnifiedTaskOutput, type TaskRetrievalStatus, type StopSource, type RegisterMonitorInput, type MonitorTimers, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccessWorkflowRun, DURABLE_AGENT_HANDLE_RE, } from "./core/task-registry.js";
128
- export { InMemoryMailboxStore, type MailboxStore, type MailboxMessage, type MailboxLease, MailboxStoreError, type MailboxStoreErrorCode, MAILBOX_TOMBSTONED_RECIPIENT_CODE, type MailboxAppendMessage, type MailboxPeerMeta, type MailboxPeerFromMode, type MailboxPeerRecordKind, MAILBOX_PEER_FROM_MODES, MAILBOX_PEER_RECORD_KINDS, MAILBOX_INVALID_PEER_META_CODE, MAILBOX_CROSS_PROCESS_UNSAFE_CODE, readMailboxPeerMeta, mailboxCrossProcessMountVerdict, type MailboxCrossProcessVerdict, } from "./core/mailbox-store.js";
128
+ export { InMemoryMailboxStore, type MailboxStore, type MailboxMessage, type MailboxLease, MailboxStoreError, type MailboxStoreErrorCode, MAILBOX_TOMBSTONED_RECIPIENT_CODE, type MailboxAppendMessage, type MailboxPeerMeta, type MailboxPeerFromMode, type MailboxPeerRecordKind, type MailboxPeerNoticeMeta, type MailboxPeerNoticeState, MAILBOX_PEER_NOTICE_STATES, MAILBOX_PEER_FROM_MODES, MAILBOX_PEER_RECORD_KINDS, MAILBOX_INVALID_PEER_META_CODE, MAILBOX_CROSS_PROCESS_UNSAFE_CODE, readMailboxPeerMeta, mailboxCrossProcessMountVerdict, type MailboxCrossProcessVerdict, } from "./core/mailbox-store.js";
129
129
  export { FileMailboxStore, type FileMailboxStoreOptions } from "./stores/file/mailbox-store.js";
130
130
  export { createFileTaskListStore } from "./stores/file/task-list-store.js";
131
131
  export { createCcFileTaskListStore } from "./stores/cc/task-list-store.js";
@@ -145,7 +145,8 @@ export { createSchedulerTools, type SchedulerToolContext, SCHEDULE_WAKEUP_TOOL_N
145
145
  export { resolveAutonomousLoopPrompt, AUTONOMOUS_LOOP_PREAMBLE, AUTONOMOUS_LOOP_PREAMBLE_PERSISTENT, type AutonomousLoopPromptOptions, } from "./tools/loop-tick.js";
146
146
  export { tightenTaskSpec, TaskSpecTightenError } from "./core/tighten-task-spec.js";
147
147
  export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, decisionText, resolveAsk, toolPolicyNameSets, type ToolPolicyNameSets, type NamedToolPolicy, type ToolPolicyProjection, type ToolPolicyProjectionComponent, type ConstraintChainEntry, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, type ToolPolicy, type ToolCallRequest, type PermissionResult, type DecisionReason, type ApprovalSettledBy, APPROVAL_SETTLED_BY_VALUES, isApprovalSettledBy, type AskDenyResolution, ASK_DENY_RESOLUTION_VALUES, isAskDenyResolution, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, type OnAsk, type AskOutcome, type ResolvedAsk, type AskRequest, type AskDelegationProvenance, type AskRuleEvidence, type AskEvidenceAbsence, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
148
- export { parseAutoModeResponse, createAutoModeDecider, type AutoModeVerdict, type AutoModeDecider, type AutoModeDeciderOptions, type AutoModeClassifyFn, type AutoModeClassifyInput, } from "./core/auto-mode.js";
148
+ export { parseAutoModeResponse, createAutoModeDecider, type AutoModeVerdict, type AutoModeDecider, type AutoModeDeciderOptions, type AutoModeClassifyFn, type AutoModeClassifyInput, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, type AutoModeDenialTracker, type AutoModeDenialLimitOptions, type DenialLimitFallback, type DenialLimitVerdict, } from "./core/auto-mode.js";
149
+ export { AUTO_MODE_DENIAL_LIMIT_DEFAULTS, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS } from "./core/auto-mode-defaults.js";
149
150
  export { buildAutoModePrompt, renderAutoModeWindow, renderAutoModeAction, AUTO_MODE_DEFAULTS_SENTINEL, type AutoModeRules, type BuildAutoModePromptOptions, type AutoModeWindowOptions, } from "./core/auto-mode-prompt.js";
150
151
  export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/auto-mode-prompt-assets.js";
151
152
  export { AUTO_MODE_ARMING_RECIPE_VERSION, autoModeArmingRecipeOf, sanitizeAutoModeArmingRecipe, foldAutoModeArming, type AutoModeArmingRecipe, type AutoModeArmingFace, type AutoModeArmingFold, type AutoModeRebuildRefusal, } from "./core/auto-mode-arming.js";
@@ -258,8 +259,12 @@ export { PEER_REF_MIN, PEER_REF_MAX, PEER_REF_RE, mintPeerRef, formatPeerNameRef
258
259
  export { PEER_SESSION_RECORD_SCHEMA_VERSION, PEER_SESSION_RECORD_MAX_BYTES, SESSION_BOX_PREFIX, peerSessionBoxHandle, isPeerSessionId, PEER_SESSION_ID_GRAMMAR, parsePeerSessionAddress, readPeerSessionRecord, defaultPeerLivenessProbe, isPeerSessionProcessAlive, mintPeerSessionCandidates, resolvePeerSessions, createInMemoryPeerDirectory, vetPeerRegistryDirectory, judgePeerRecordFile, type PeerSessionRecord, type PeerSessionLiveness, type PeerSessionTempo, type PeerSessionRecordRefusal, type PeerSessionRecordRead, type PeerLivenessProbe, type PeerDirectory, type PeerDirectoryAccess, type PeerSessionCandidate, type PeerSessionResolution, type InMemoryPeerDirectory, type PeerRegistryDirectoryRefusal, type PeerRegistryDirectoryVerdict, } from "./agents/peer-directory.js";
259
260
  export { createListAgentsTool, LIST_AGENTS_TOOL_NAME, LIST_AGENTS_TOOL_ALIAS, LIST_AGENTS_MAX_RESULT_CHARS, type ListAgentsToolOptions, type ListAgentsRow } from "./agents/list-agents-tool.js";
260
261
  export { CROSS_SESSION_CLASSIFIER_RULE } from "./agents/cross-session-envelope.js";
261
- export { PEER_SESSION_ID_UNGRAMMATICAL_CODE, type PeerLaneMountVerdict } from "./agents/peer-session-drain.js";
262
- export { renderCrossSessionMessageFrame } from "./core/task-notification.js";
262
+ export { PEER_SESSION_ID_UNGRAMMATICAL_CODE, type PeerLaneMountVerdict, type PeerInboundDisposition } from "./agents/peer-session-drain.js";
263
+ export { renderCrossSessionMessageFrame, renderCrossSessionNoticeLine } from "./core/task-notification.js";
264
+ export { PEER_HELD_QUEUE_CAP, PEER_IDLE_SUBSCRIPTION_TTL_MS, PEER_IDLE_SUBSCRIBER_TABLE_CAP, PEER_IDLE_OUTSTANDING_CAP, PEER_IDLE_PRIORS_KEPT, PEER_IDLE_FIRE_DEBOUNCE_MS, PEER_IDLE_HELD_BACKOFF_MS, PEER_IDLE_LABEL_MAX, CROSS_SESSION_DIALOG_EXPIRY_VALUES, CROSS_SESSION_DIALOG_EXPIRY_DEFAULT, resolveCrossSessionDialogExpiry, PEER_HELD_REVIEW_CAUSES, PEER_DELIVERY_RECEIPT_STATES, isPeerDeliveryReceiptState, PEER_DROP_REASONS, peerDeliveryReceiptLabel, describePeerDeliveryReceipt, peerRecipientSuffix, renderCrossSessionDeliveryNotice, describePeerDropReasons, renderCrossSessionDroppedNotice, PEER_IDLE_NOTICE_KINDS, isPeerIdleNoticeKind, PEER_IDLE_UNAVAILABLE_CAUSES, peerIdleNoticeLabel, peerIdleDetailOf, formatPeerNoticeClock, peerIdleNoticeSummary, renderCrossSessionIdleNotice, describePeerIdleSubscription, type CrossSessionDialogExpiry, type ResolvedCrossSessionDialogExpiry, type PeerDeliveryReceiptState, type PeerDropReason, type PeerIdleNoticeKind, type PeerIdleNoticeFields, } from "./agents/peer-notices.js";
265
+ export { createPeerHeldQueue, peerHeldQueueFor, peerHeldQueueIfAny, peerHeldQueueKey, realPeerClock, type PeerInboundHoldCause, type PeerHeldEntry, type PeerHeldSettlement, type PeerHeldSettleReason, type PeerHeldSettleOutcome, type PeerClock, type PeerHeldQueueSink, type PeerHeldQueueConfig, type PeerHeldQueue, } from "./agents/peer-held-queue.js";
266
+ export { createPeerIdleTarget, createPeerIdleRequester, peerIdleMachineFor, peerIdleMachineIfAny, peerIdleMachineKey, type PeerIdleSubscriber, type PeerIdleTargetNotice, type PeerIdleTargetSink, type PeerIdleSubscribeOutcome, type PeerIdleTarget, type PeerIdleOutstanding, type PeerIdleRequesterSink, type PeerIdleRequester, type PeerIdleMachine, } from "./agents/peer-idle.js";
267
+ export { routePeerDeliveryReceipt, routePeerDroppedReceipt, routePeerIdleNotice, type PeerNoticeRoute } from "./agents/peer-notice-route.js";
263
268
  export { type PeerAdmissionStage } from "./agents/peer-admission.js";
264
269
  export { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME, type AgentTranscriptToolOptions, } from "./agents/agent-transcript-tool.js";
265
270
  export { defineAgent } from "./agents/agent-definition.js";
@@ -284,7 +289,7 @@ export { ROUTE_ADJUDICATION_CONFORMANCE_CORPUS, type RouteAdjudicationVector } f
284
289
  export { type BrainTimeoutConfig } from "./brain/timeout.js";
285
290
  export { createAssistantMessageEventStream } from "./internal/llm.js";
286
291
  export type { AssistantMessage, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
287
- export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, TrackFileEditHook, TrackEditRequest, TrackEditResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, BrainRetryErrClass, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, A2aServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, ResumePreflightInfo, ResumePreflightVerdict, EngineNotice, RuntimeCaps, BackgroundChildEvent, DelegationLifecycleEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskLimits, StaleToolResultOffloadOptions, TaskResult, EffectiveMemoryScopes, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolInputVerdict, ToolInputValidationContext, ReversibilityVerdict, ToolEffect, ToolContentOrigin, WorkflowGovernanceBaseline, DelegationTaskType, } from "./core/types.js";
292
+ export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, TrackFileEditHook, TrackEditRequest, TrackEditResult, FileEditedHook, FileEditedNotice, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, BrainRetryErrClass, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, A2aServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, ResumePreflightInfo, ResumePreflightVerdict, EngineNotice, RuntimeCaps, BackgroundChildEvent, DelegationLifecycleEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskLimits, StaleToolResultOffloadOptions, TaskResult, EffectiveMemoryScopes, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolInputVerdict, ToolInputValidationContext, ReversibilityVerdict, ToolEffect, ToolContentOrigin, WorkflowGovernanceBaseline, DelegationTaskType, } from "./core/types.js";
288
293
  export { Type } from "typebox";
289
294
  export type { TSchema, Static } from "typebox";
290
295
  export { explainPromptAssembly, describeDefaultPack, type DefaultPackDescription, type ExplainInput } from "./prompt-assembly/explain.js";
package/dist/index.js CHANGED
@@ -102,7 +102,7 @@ export {} from "./core/checkpoint-store.js";
102
102
  export {} from "./core/checkpoint-store.js";
103
103
  export { projectHumanInput, buildHumanInputEvent, } from "./core/human-input-projection.js";
104
104
  export { TaskRegistry, defaultTaskRegistry, createTaskOutputTool, createTaskStopTool, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccessWorkflowRun, DURABLE_AGENT_HANDLE_RE, } from "./core/task-registry.js";
105
- export { InMemoryMailboxStore, MailboxStoreError, MAILBOX_TOMBSTONED_RECIPIENT_CODE, MAILBOX_PEER_FROM_MODES, MAILBOX_PEER_RECORD_KINDS, MAILBOX_INVALID_PEER_META_CODE, MAILBOX_CROSS_PROCESS_UNSAFE_CODE, readMailboxPeerMeta, mailboxCrossProcessMountVerdict, } from "./core/mailbox-store.js";
105
+ export { InMemoryMailboxStore, MailboxStoreError, MAILBOX_TOMBSTONED_RECIPIENT_CODE, MAILBOX_PEER_NOTICE_STATES, MAILBOX_PEER_FROM_MODES, MAILBOX_PEER_RECORD_KINDS, MAILBOX_INVALID_PEER_META_CODE, MAILBOX_CROSS_PROCESS_UNSAFE_CODE, readMailboxPeerMeta, mailboxCrossProcessMountVerdict, } from "./core/mailbox-store.js";
106
106
  export { FileMailboxStore } from "./stores/file/mailbox-store.js";
107
107
  export { createFileTaskListStore } from "./stores/file/task-list-store.js";
108
108
  export { createCcFileTaskListStore } from "./stores/cc/task-list-store.js";
@@ -120,7 +120,8 @@ export { createSchedulerTools, SCHEDULE_WAKEUP_TOOL_NAME, AUTONOMOUS_LOOP_SENTIN
120
120
  export { resolveAutonomousLoopPrompt, AUTONOMOUS_LOOP_PREAMBLE, AUTONOMOUS_LOOP_PREAMBLE_PERSISTENT, } from "./tools/loop-tick.js";
121
121
  export { tightenTaskSpec, TaskSpecTightenError } from "./core/tighten-task-spec.js";
122
122
  export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, decisionText, resolveAsk, toolPolicyNameSets, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, APPROVAL_SETTLED_BY_VALUES, isApprovalSettledBy, ASK_DENY_RESOLUTION_VALUES, isAskDenyResolution, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
123
- export { parseAutoModeResponse, createAutoModeDecider, } from "./core/auto-mode.js";
123
+ export { parseAutoModeResponse, createAutoModeDecider, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, } from "./core/auto-mode.js";
124
+ export { AUTO_MODE_DENIAL_LIMIT_DEFAULTS, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS } from "./core/auto-mode-defaults.js";
124
125
  export { buildAutoModePrompt, renderAutoModeWindow, renderAutoModeAction, AUTO_MODE_DEFAULTS_SENTINEL, } from "./core/auto-mode-prompt.js";
125
126
  export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/auto-mode-prompt-assets.js";
126
127
  export { AUTO_MODE_ARMING_RECIPE_VERSION, autoModeArmingRecipeOf, sanitizeAutoModeArmingRecipe, foldAutoModeArming, } from "./core/auto-mode-arming.js";
@@ -216,7 +217,11 @@ export { PEER_SESSION_RECORD_SCHEMA_VERSION, PEER_SESSION_RECORD_MAX_BYTES, SESS
216
217
  export { createListAgentsTool, LIST_AGENTS_TOOL_NAME, LIST_AGENTS_TOOL_ALIAS, LIST_AGENTS_MAX_RESULT_CHARS } from "./agents/list-agents-tool.js";
217
218
  export { CROSS_SESSION_CLASSIFIER_RULE } from "./agents/cross-session-envelope.js";
218
219
  export { PEER_SESSION_ID_UNGRAMMATICAL_CODE } from "./agents/peer-session-drain.js";
219
- export { renderCrossSessionMessageFrame } from "./core/task-notification.js";
220
+ export { renderCrossSessionMessageFrame, renderCrossSessionNoticeLine } from "./core/task-notification.js";
221
+ export { PEER_HELD_QUEUE_CAP, PEER_IDLE_SUBSCRIPTION_TTL_MS, PEER_IDLE_SUBSCRIBER_TABLE_CAP, PEER_IDLE_OUTSTANDING_CAP, PEER_IDLE_PRIORS_KEPT, PEER_IDLE_FIRE_DEBOUNCE_MS, PEER_IDLE_HELD_BACKOFF_MS, PEER_IDLE_LABEL_MAX, CROSS_SESSION_DIALOG_EXPIRY_VALUES, CROSS_SESSION_DIALOG_EXPIRY_DEFAULT, resolveCrossSessionDialogExpiry, PEER_HELD_REVIEW_CAUSES, PEER_DELIVERY_RECEIPT_STATES, isPeerDeliveryReceiptState, PEER_DROP_REASONS, peerDeliveryReceiptLabel, describePeerDeliveryReceipt, peerRecipientSuffix, renderCrossSessionDeliveryNotice, describePeerDropReasons, renderCrossSessionDroppedNotice, PEER_IDLE_NOTICE_KINDS, isPeerIdleNoticeKind, PEER_IDLE_UNAVAILABLE_CAUSES, peerIdleNoticeLabel, peerIdleDetailOf, formatPeerNoticeClock, peerIdleNoticeSummary, renderCrossSessionIdleNotice, describePeerIdleSubscription, } from "./agents/peer-notices.js";
222
+ export { createPeerHeldQueue, peerHeldQueueFor, peerHeldQueueIfAny, peerHeldQueueKey, realPeerClock, } from "./agents/peer-held-queue.js";
223
+ export { createPeerIdleTarget, createPeerIdleRequester, peerIdleMachineFor, peerIdleMachineIfAny, peerIdleMachineKey, } from "./agents/peer-idle.js";
224
+ export { routePeerDeliveryReceipt, routePeerDroppedReceipt, routePeerIdleNotice } from "./agents/peer-notice-route.js";
220
225
  export {} from "./agents/peer-admission.js";
221
226
  export { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME, } from "./agents/agent-transcript-tool.js";
222
227
  export { defineAgent } from "./agents/agent-definition.js";
@@ -768,6 +768,9 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
768
768
  };
769
769
  let journalTail = Promise.resolve();
770
770
  const JOURNAL_DRAIN_MAX_MS = 5_000;
771
+ const journalDurableResult = (result) => result.status !== "completed" && typeof result.errorMessage === "string" && result.errorMessage !== ""
772
+ ? { ...result, errorMessage: boundedRedactedSummary(result.errorMessage, MAX_TRANSCRIPT_CHARS) }
773
+ : result;
771
774
  const journalAppend = async (callKey, result, label) => {
772
775
  const dbg = typeof process !== "undefined" && process.env?.SEMA_DEBUG_WORKFLOW_JOURNAL === "1";
773
776
  if (!journalStore) {
@@ -775,9 +778,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
775
778
  console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} SKIP (no journalStore on this run)`);
776
779
  return;
777
780
  }
781
+ const stored = journalDurableResult(result);
778
782
  let serialized;
779
783
  try {
780
- serialized = JSON.stringify(result);
784
+ serialized = JSON.stringify(stored);
781
785
  }
782
786
  catch {
783
787
  serialized = undefined;
@@ -788,7 +792,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
788
792
  run.journalSkips = (run.journalSkips ?? 0) + 1;
789
793
  emitRunLog(`resume-journal: agent #${callKeyOrdinal(callKey)}${label !== undefined ? ` "${label.slice(0, 80)}"` : ""} result is ${bytes} bytes, ` +
790
794
  `over the ${MAX_JOURNAL_RESULT_BYTES}-byte per-entry cap — NOT cached. A resume from this run re-runs this agent and everything after it live.`);
791
- const tombstone = journalStore.append(runId, scope, { callKey, result: journalOversizeTombstone(result, bytes) });
795
+ const tombstone = journalStore.append(runId, scope, { callKey, result: journalOversizeTombstone(stored, bytes) });
792
796
  journalTail = journalTail.then(() => tombstone).catch(() => undefined);
793
797
  try {
794
798
  await tombstone;
@@ -801,7 +805,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
801
805
  }
802
806
  return;
803
807
  }
804
- const p = journalStore.append(runId, scope, { callKey, result });
808
+ const p = journalStore.append(runId, scope, { callKey, result: stored });
805
809
  journalTail = journalTail.then(() => p).catch(() => undefined);
806
810
  try {
807
811
  await p;
@@ -1,5 +1,5 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
- import type { BeforeWriteHook, TrackFileEditHook } from "../../core/types.js";
2
+ import type { BeforeWriteHook, FileEditedHook, TrackFileEditHook } from "../../core/types.js";
3
3
  import { type ReadFileState } from "./safety.js";
4
4
  import { type CwdRef } from "./fs-shared.js";
5
5
  /**
@@ -12,8 +12,8 @@ import { type CwdRef } from "./fs-shared.js";
12
12
  * prefix comparison there (零开销直通). Absent hook ⇒ byte-identical behavior.
13
13
  */
14
14
  export type { BeforeWriteRequest, BeforeWriteResult, BeforeWriteHook, TrackEditRequest, TrackEditResult, TrackFileEditHook } from "../../core/types.js";
15
- export declare function createEditFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook): AgentTool;
16
- export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook): AgentTool;
15
+ export declare function createEditFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook): AgentTool;
16
+ export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook): AgentTool;
17
17
  /**
18
18
  * design v1.163 — NotebookEdit: replace/insert/delete a single cell in a .ipynb. CC-parity tool over the SAME hand-band
19
19
  * safety skeleton as Edit/Write (resolveKey containment → requireRead read-before-edit → checkStale content-hash
@@ -21,4 +21,4 @@ export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileSt
21
21
  * .ipynb as the RB-227 cell projection but records read state (hash/totalLines) in the RAW notebook-text
22
22
  * coordinate — that raw-coordinate read record is what this tool's freshness check depends on.
23
23
  */
24
- export declare function createNotebookEditTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook): AgentTool;
24
+ export declare function createNotebookEditTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook): AgentTool;
@@ -5,16 +5,35 @@ import { sha256, resolveKey, violationText, violationDetails, requireRead, check
5
5
  import { decodeTextBytes, encodeTextForFile, normalizeEditText, normalizeFileText } from "./encoding.js";
6
6
  import { MAX_EDIT_BYTES, decodeEditBytes, tooLargeToEditMessage, truncatedUtf16BodyMessage, persistedTextOf, notReadRefusalText, enoentMessage, FILE_STATE_TRAILER, FILE_PATH_PARAMS, ipynbRedirect, countLines, applyRecordedEdit, } from "./fs-shared.js";
7
7
  async function envFinalWrite(env, key, content, signal, opts) {
8
+ const fireEdited = () => {
9
+ if (opts?.edited?.hook === undefined)
10
+ return;
11
+ try {
12
+ const ack = opts.edited.hook({ tool: opts.edited.tool, path: opts.edited.path, key });
13
+ if (ack !== null && (typeof ack === "object" || typeof ack === "function") && typeof ack.then === "function") {
14
+ void ack.then(undefined, () => undefined);
15
+ }
16
+ }
17
+ catch {
18
+ }
19
+ };
8
20
  let r;
9
21
  try {
10
22
  r = await writeThroughEnv(env, key, content, signal, opts);
11
23
  }
12
24
  catch (err) {
13
25
  await discardTrackedEdit(opts?.track, "verify");
26
+ fireEdited();
14
27
  throw err;
15
28
  }
16
- if (!r.ok)
17
- await discardTrackedEdit(opts?.track, NOTHING_WAS_WRITTEN.has(r.error.code) ? "proven" : "verify");
29
+ if (!r.ok) {
30
+ const proven = NOTHING_WAS_WRITTEN.has(r.error.code);
31
+ await discardTrackedEdit(opts?.track, proven ? "proven" : "verify");
32
+ if (!proven)
33
+ fireEdited();
34
+ return r;
35
+ }
36
+ fireEdited();
18
37
  return r;
19
38
  }
20
39
  const NOTHING_WAS_WRITTEN = new Set(["already_exists", "precondition_failed"]);
@@ -66,7 +85,7 @@ async function gateToolWrite(hook, tool, path, key, content) {
66
85
  return `Error (${tool}): write rejected by the write gate (${res.code}): ${clipHookText(res.reason)}`;
67
86
  return undefined;
68
87
  }
69
- export function createEditFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit) {
88
+ export function createEditFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited) {
70
89
  return defineTool({
71
90
  name: "Edit",
72
91
  contract: { contractId: "core.edit@1", implementationRevision: "1" },
@@ -151,7 +170,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
151
170
  const tracked = await trackToolEdit(trackEdit, "Edit", path, r.key, ctx.signal);
152
171
  if (tracked.refusal !== undefined)
153
172
  return errorResult(tracked.refusal);
154
- const write = await envFinalWrite(env, r.key, created, ctx.signal, { exclusive: true, track: tracked });
173
+ const write = await envFinalWrite(env, r.key, created, ctx.signal, { exclusive: true, track: tracked, edited: { hook: onEdited, tool: "Edit", path } });
155
174
  if (!write.ok) {
156
175
  if (write.error.code === "already_exists") {
157
176
  return errorResult(`Error (Edit): cannot create "${path}": file already exists (created concurrently since the existence check). Read the file first, then edit it normally (or Write after the Read to overwrite it).`);
@@ -205,7 +224,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
205
224
  if (trackedOverwrite.refusal !== undefined)
206
225
  return errorResult(trackedOverwrite.refusal);
207
226
  const encodedOverwrite = encodeTextForFile(newContent, preDec.encoding, preDec.endings);
208
- const write = await envFinalWrite(env, r.key, encodedOverwrite, ctx.signal, { track: trackedOverwrite });
227
+ const write = await envFinalWrite(env, r.key, encodedOverwrite, ctx.signal, { track: trackedOverwrite, edited: { hook: onEdited, tool: "Edit", path } });
209
228
  if (!write.ok)
210
229
  return errorResult(`Error (Edit): cannot write "${path}": ${write.error.message}`);
211
230
  const persistedOverwrite = persistedTextOf(encodedOverwrite);
@@ -282,7 +301,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
282
301
  if (trackedEdit.refusal !== undefined)
283
302
  return errorResult(trackedEdit.refusal);
284
303
  const encodedEdit = encodeTextForFile(working, decoded.encoding, decoded.endings);
285
- const write = await envFinalWrite(env, r.key, encodedEdit, ctx.signal, { track: trackedEdit });
304
+ const write = await envFinalWrite(env, r.key, encodedEdit, ctx.signal, { track: trackedEdit, edited: { hook: onEdited, tool: "Edit", path } });
286
305
  if (!write.ok)
287
306
  return errorResult(`Error (Edit): cannot write "${path}": ${write.error.message}`);
288
307
  const persistedEdit = persistedTextOf(encodedEdit);
@@ -305,7 +324,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
305
324
  },
306
325
  });
307
326
  }
308
- export function createWriteFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit) {
327
+ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited) {
309
328
  return defineTool({
310
329
  name: "Write",
311
330
  contract: { contractId: "core.write@1", implementationRevision: "1" },
@@ -373,7 +392,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
373
392
  if (trackedWrite.refusal !== undefined)
374
393
  return errorResult(trackedWrite.refusal);
375
394
  const encodedWrite = encodeTextForFile(content, decodedPrev.encoding, "preserve");
376
- const write = await envFinalWrite(env, r.key, encodedWrite, ctx.signal, { track: trackedWrite });
395
+ const write = await envFinalWrite(env, r.key, encodedWrite, ctx.signal, { track: trackedWrite, edited: { hook: onEdited, tool: "Write", path } });
377
396
  if (!write.ok)
378
397
  return errorResult(`Error (Write): cannot write "${path}": ${write.error.message}`);
379
398
  const persistedWrite = persistedTextOf(encodedWrite);
@@ -389,7 +408,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
389
408
  const trackedCreate = await trackToolEdit(trackEdit, "Write", path, r.key, ctx.signal);
390
409
  if (trackedCreate.refusal !== undefined)
391
410
  return errorResult(trackedCreate.refusal);
392
- const write = await envFinalWrite(env, r.key, content, ctx.signal, { track: trackedCreate });
411
+ const write = await envFinalWrite(env, r.key, content, ctx.signal, { track: trackedCreate, edited: { hook: onEdited, tool: "Write", path } });
393
412
  if (!write.ok)
394
413
  return errorResult(`Error (Write): cannot write "${path}": ${write.error.message}`);
395
414
  const totalLines = countLines(content);
@@ -405,7 +424,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
405
424
  const notebookNotIpynbMessage = (notebookPath) => `Error (NotebookEdit): "${notebookPath}" is not a .ipynb file; use Edit for other file types.`;
406
425
  const NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE = `Error (NotebookEdit): Cell type is required when using edit_mode=insert.`;
407
426
  const NOTEBOOK_CELL_ID_REQUIRED_MESSAGE = `Error (NotebookEdit): cell_id is required for replace/delete.`;
408
- export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit) {
427
+ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited) {
409
428
  return defineTool({
410
429
  name: "NotebookEdit",
411
430
  contract: { contractId: "core.notebook_edit@1", implementationRevision: "1" },
@@ -543,7 +562,7 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
543
562
  const trackedNb = await trackToolEdit(trackEdit, "NotebookEdit", notebook_path, r.key, ctx.signal);
544
563
  if (trackedNb.refusal !== undefined)
545
564
  return errorResult(trackedNb.refusal);
546
- const w = await envFinalWrite(env, r.key, encodeTextForFile(updated, decodedNb.encoding, decodedNb.endings), ctx.signal, { track: trackedNb });
565
+ const w = await envFinalWrite(env, r.key, encodeTextForFile(updated, decodedNb.encoding, decodedNb.endings), ctx.signal, { track: trackedNb, edited: { hook: onEdited, tool: "NotebookEdit", path: notebook_path } });
547
566
  if (!w.ok)
548
567
  return errorResult(`Error (NotebookEdit): cannot write "${notebook_path}": ${w.error.message}`);
549
568
  state.set(r.key, { hash: sha256(updated), totalLines: countLines(updated), truncated: false, lastReadAt: Date.now() });
@@ -1,5 +1,5 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
- import { type BeforeWriteHook, type TrackFileEditHook } from "../../core/types.js";
2
+ import { type BeforeWriteHook, type FileEditedHook, type TrackFileEditHook } from "../../core/types.js";
3
3
  import { type TaskRegistry } from "../../core/task-registry.js";
4
4
  import { type ReadFileState } from "./safety.js";
5
5
  import type { PdfModelCapabilities } from "./pdf.js";
@@ -120,6 +120,12 @@ export interface HandsToolkitOptions {
120
120
  * env write, so the file's pre-edit state is durably recorded before it changes. The Runner
121
121
  * wires it to `FileHistoryStore.trackEdit`; absent ⇒ byte-identical behavior (no history). */
122
122
  trackFileEdit?: TrackFileEditHook;
123
+ /** The mutation lane's LANDED observation seat — fired once per Write/Edit/NotebookEdit call whose
124
+ * bytes reached disk (never for a refused/failed one, never for a Bash-authored change). Wired by
125
+ * the Runner to the per-run accumulator behind `TaskResult.editedFiles`; absent ⇒ nothing observes.
126
+ * Purely additive: it cannot refuse a write, and a fault in it is contained in both shapes (a
127
+ * synchronous throw and a rejected promise), neither of them awaited. */
128
+ onFileEdited?: FileEditedHook;
123
129
  /** #181-F6 — see createBashTool's taskOpts field of the same name: whether the Monitor tool is on
124
130
  * this run's roster (the Runner mounts Monitor, this band never does). `false` drops the gh
125
131
  * rate-limit hint's Monitor clause; absent ⇒ historic full wording (byte-compat). */
@@ -50,7 +50,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
50
50
  createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, readFaceRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption, opts.readCyberReminder, readDeny, readFace, opts.reminderMark, opts.reminderDisclosureCounts),
51
51
  ];
52
52
  if (!readOnly) {
53
- tools.push(createEditFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit), createWriteFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit), createNotebookEditTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit));
53
+ tools.push(createEditFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited), createWriteFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited), createNotebookEditTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited));
54
54
  }
55
55
  tools.push(createGrepTool(env, rootCanonical, readFaceRoots, readDeny, readFace), createGlobTool(env, rootCanonical, readFaceRoots, readDeny, readFace), createRepoMapTool(env, rootCanonical, readFaceRoots, readDeny, readFace));
56
56
  if (includeShell) {