@sema-agent/core 2.13.0 → 4.0.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 (184) hide show
  1. package/CHANGELOG.md +2345 -0
  2. package/dist/agents/cascade.d.ts +6 -1
  3. package/dist/agents/cascade.js +76 -32
  4. package/dist/agents/cumulative-stats.d.ts +29 -0
  5. package/dist/agents/cumulative-stats.js +29 -0
  6. package/dist/agents/observer.d.ts +4 -0
  7. package/dist/agents/observer.js +11 -10
  8. package/dist/agents/repair-loop.d.ts +2 -0
  9. package/dist/agents/repair-loop.js +110 -3
  10. package/dist/agents/retain-ledger.d.ts +20 -2
  11. package/dist/agents/retain-ledger.js +38 -8
  12. package/dist/agents/roster-store.d.ts +4 -0
  13. package/dist/agents/roster-store.js +20 -1
  14. package/dist/agents/subagent.js +66 -64
  15. package/dist/agents/teacher.d.ts +1 -9
  16. package/dist/agents/teacher.js +23 -24
  17. package/dist/agents/team.js +7 -4
  18. package/dist/agents/verify.d.ts +15 -2
  19. package/dist/agents/verify.js +39 -3
  20. package/dist/bin/sema-tb.js +2 -2
  21. package/dist/brain/anthropic.js +24 -21
  22. package/dist/brain/circuit-breaker.d.ts +1 -0
  23. package/dist/brain/circuit-breaker.js +26 -9
  24. package/dist/brain/degrading.js +13 -1
  25. package/dist/brain/failover.js +2 -0
  26. package/dist/brain/openai.js +24 -21
  27. package/dist/brain/repetition.js +10 -1
  28. package/dist/brain/routing.js +5 -1
  29. package/dist/brain/status-sink.js +5 -1
  30. package/dist/brain/stream-engine.js +5 -2
  31. package/dist/brain/stream-shared.d.ts +1 -0
  32. package/dist/brain/stream-shared.js +14 -0
  33. package/dist/brain/terminal-cause.d.ts +4 -0
  34. package/dist/brain/terminal-cause.js +12 -0
  35. package/dist/brain/tool-call-id.d.ts +1 -0
  36. package/dist/brain/tool-call-id.js +3 -0
  37. package/dist/brain/tool-call-repair.js +34 -15
  38. package/dist/core/auto-compaction.js +33 -32
  39. package/dist/core/background-agent-store.d.ts +22 -22
  40. package/dist/core/background-agent-store.js +19 -23
  41. package/dist/core/checkpoint-store.d.ts +7 -13
  42. package/dist/core/checkpoint-store.js +39 -15
  43. package/dist/core/consolidate-scope.js +4 -3
  44. package/dist/core/context-edit.js +3 -0
  45. package/dist/core/context-guard.js +3 -0
  46. package/dist/core/file-snapshot-store.d.ts +8 -0
  47. package/dist/core/file-snapshot-store.js +4 -3
  48. package/dist/core/git-worktree-env.d.ts +6 -1
  49. package/dist/core/git-worktree-env.js +12 -1
  50. package/dist/core/hooks.d.ts +4 -1
  51. package/dist/core/hooks.js +35 -7
  52. package/dist/core/lsp-diagnostics.js +13 -7
  53. package/dist/core/lsp-protocol.d.ts +1 -1
  54. package/dist/core/lsp-protocol.js +20 -8
  55. package/dist/core/lsp-session.d.ts +12 -2
  56. package/dist/core/lsp-session.js +114 -49
  57. package/dist/core/lsp.d.ts +13 -1
  58. package/dist/core/lsp.js +65 -9
  59. package/dist/core/mailbox-store.d.ts +7 -10
  60. package/dist/core/mcp.d.ts +7 -10
  61. package/dist/core/mcp.js +7 -3
  62. package/dist/core/memory-engine/data-plane.js +4 -0
  63. package/dist/core/memory-engine/engine.d.ts +2 -2
  64. package/dist/core/memory-engine/engine.js +96 -31
  65. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  66. package/dist/core/memory-engine/file-backend.js +38 -39
  67. package/dist/core/memory-engine/layout.d.ts +8 -0
  68. package/dist/core/memory-engine/layout.js +71 -4
  69. package/dist/core/memory-engine/scope-contract.js +9 -3
  70. package/dist/core/memory-engine/types.d.ts +1 -1
  71. package/dist/core/memory.js +3 -0
  72. package/dist/core/permission-rules.d.ts +2 -1
  73. package/dist/core/permission-rules.js +24 -3
  74. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  75. package/dist/core/runner/active-skill-scope.js +1 -15
  76. package/dist/core/runner/assemble-result.d.ts +3 -8
  77. package/dist/core/runner/assemble-result.js +3 -5
  78. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  79. package/dist/core/runner/compaction-call-options.js +30 -0
  80. package/dist/core/runner/prepare-task.d.ts +6 -23
  81. package/dist/core/runner/prepare-task.js +31 -53
  82. package/dist/core/runner/runtask.d.ts +1 -0
  83. package/dist/core/runner/runtask.js +86 -178
  84. package/dist/core/runner/session-rule-policy.js +4 -1
  85. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  86. package/dist/core/runner/teardown-bounded.js +36 -0
  87. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  88. package/dist/core/runner/turn-attachments.d.ts +5 -15
  89. package/dist/core/runner/turn-attachments.js +1 -1
  90. package/dist/core/runner/usage-accounting.d.ts +6 -0
  91. package/dist/core/runner/usage-accounting.js +9 -8
  92. package/dist/core/safe-notify.d.ts +17 -0
  93. package/dist/core/safe-notify.js +59 -0
  94. package/dist/core/secret-env.js +5 -1
  95. package/dist/core/sensitive-path-policy.js +11 -4
  96. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  97. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  98. package/dist/core/stub-env.d.ts +2 -6
  99. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  100. package/dist/core/surrogate-safe-slice.js +18 -0
  101. package/dist/core/task-registry-monitor.js +15 -14
  102. package/dist/core/task-registry-shared.d.ts +22 -0
  103. package/dist/core/task-registry-shared.js +16 -0
  104. package/dist/core/task-registry.d.ts +1 -0
  105. package/dist/core/task-registry.js +40 -57
  106. package/dist/core/tool-errors.js +5 -4
  107. package/dist/core/tool-name-aliases.js +2 -1
  108. package/dist/core/tool-policy.js +72 -19
  109. package/dist/core/tool-result-store.d.ts +1 -0
  110. package/dist/core/tool-result-store.js +17 -1
  111. package/dist/core/trace.d.ts +3 -9
  112. package/dist/core/types.d.ts +16 -12
  113. package/dist/core/untrusted-egress.js +7 -5
  114. package/dist/core/workflow-journal-store.d.ts +9 -20
  115. package/dist/core/workflow-run-store.d.ts +11 -10
  116. package/dist/core/workflow-run-store.js +22 -16
  117. package/dist/engine/compaction/compaction.d.ts +1 -0
  118. package/dist/engine/compaction/compaction.js +14 -5
  119. package/dist/engine/compaction/utils.js +4 -2
  120. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  121. package/dist/engine/harness/agent-harness.d.ts +1 -0
  122. package/dist/engine/harness/agent-harness.js +13 -33
  123. package/dist/engine/harness/types.d.ts +29 -67
  124. package/dist/engine/llm/types.d.ts +1 -1
  125. package/dist/engine/loop/agent-loop.js +11 -4
  126. package/dist/engine/loop/types.d.ts +11 -9
  127. package/dist/engine/lsp/frame-decoder.js +6 -3
  128. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  129. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  130. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  131. package/dist/engine/session/memory-repo.d.ts +2 -6
  132. package/dist/engine/session/session.d.ts +5 -30
  133. package/dist/index.d.ts +3 -1
  134. package/dist/index.js +2 -1
  135. package/dist/internal/harness-types.d.ts +3 -1
  136. package/dist/internal/harness.d.ts +1 -1
  137. package/dist/internal/harness.js +1 -1
  138. package/dist/orchestration/run-spec.js +7 -4
  139. package/dist/orchestration/run-workflow-tool.js +3 -35
  140. package/dist/orchestration/workflow-governance.js +43 -9
  141. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  142. package/dist/orchestration/workflow-script-store.js +34 -4
  143. package/dist/orchestration/workflow.d.ts +3 -11
  144. package/dist/orchestration/workflow.js +164 -204
  145. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  146. package/dist/prompt-assembly/artifact-store.js +39 -24
  147. package/dist/prompt-assembly/assemble.js +38 -7
  148. package/dist/prompt-assembly/epoch.js +19 -2
  149. package/dist/prompt-assembly/event-registry.js +3 -3
  150. package/dist/prompt-assembly/explain.js +1 -1
  151. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  152. package/dist/prompt-assembly/tool-catalog.js +16 -1
  153. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  154. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  155. package/dist/stores/file/background-agent-store.d.ts +3 -12
  156. package/dist/stores/file/background-agent-store.js +3 -41
  157. package/dist/stores/file/checkpoint-store.js +6 -6
  158. package/dist/stores/file/fs-atomic.d.ts +3 -0
  159. package/dist/stores/file/fs-atomic.js +53 -8
  160. package/dist/stores/file/index.d.ts +1 -0
  161. package/dist/stores/file/index.js +7 -0
  162. package/dist/stores/file/mailbox-store.d.ts +2 -6
  163. package/dist/stores/file/session-policy-store.js +15 -1
  164. package/dist/stores/file/session-store.d.ts +4 -6
  165. package/dist/stores/file/session-store.js +28 -1
  166. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  167. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  168. package/dist/stores/file/workflow-run-store.js +4 -17
  169. package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -0
  170. package/dist/tools/fs/bash-readonly-classifier.js +73 -21
  171. package/dist/tools/fs/fs-bash.d.ts +1 -1
  172. package/dist/tools/fs/fs-bash.js +407 -91
  173. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  174. package/dist/tools/fs/fs-shared.js +2 -2
  175. package/dist/tools/fs/notebook.d.ts +2 -5
  176. package/dist/tools/fs/safety.d.ts +3 -0
  177. package/dist/tools/fs/safety.js +43 -14
  178. package/dist/tools/fs/search.js +5 -5
  179. package/dist/tools/loop-tick.js +1 -1
  180. package/dist/tools/monitor.d.ts +1 -0
  181. package/dist/tools/monitor.js +57 -9
  182. package/dist/tools/worktree.d.ts +4 -7
  183. package/dist/tools/worktree.js +235 -143
  184. package/package.json +9 -2
@@ -1,3 +1,4 @@
1
+ import type { McpDroppedTool } from "../mcp.js";
1
2
  import { type SkillListingEntry } from "./synthetic-tools.js";
2
3
  export declare const TODO_REMINDER_CONFIG: {
3
4
  readonly TURNS_SINCE_WRITE: 10;
@@ -112,11 +113,7 @@ export interface AttachmentInputs {
112
113
  }>;
113
114
  removed: readonly string[];
114
115
  };
115
- mcpDroppedTools?: ReadonlyArray<{
116
- server: string;
117
- tool: string;
118
- reason: string;
119
- }>;
116
+ mcpDroppedTools?: ReadonlyArray<McpDroppedTool>;
120
117
  }
121
118
  export declare function collectDueAttachments(state: AttachmentState, inp: AttachmentInputs): readonly TurnAttachment[];
122
119
  export declare function renderToolSearchUsageReminder(undiscovered: readonly string[]): string;
@@ -178,14 +175,7 @@ export declare function renderMcpInstructionsDelta(added: ReadonlyArray<{
178
175
  server: string;
179
176
  text: string;
180
177
  }>, removed: readonly string[]): string | undefined;
181
- export declare function renderMcpDroppedTools(entries: ReadonlyArray<{
182
- server: string;
183
- tool: string;
184
- reason: string;
185
- }>): string;
178
+ export declare function renderMcpDroppedTools(entries: ReadonlyArray<McpDroppedTool>): string;
186
179
  export declare const MCP_DROPPED_BATCH_MAX_BYTES = 4096;
187
- export declare function selectMcpDroppedBatch<T extends {
188
- server: string;
189
- tool: string;
190
- reason: string;
191
- }>(entries: ReadonlyArray<T>): T[];
180
+ export declare function selectMcpDroppedBatch<T extends McpDroppedTool>(entries: ReadonlyArray<T>): T[];
181
+ export declare function clipToBytes(s: string, maxBytes: number): string;
@@ -634,7 +634,7 @@ function capAttachments(atts) {
634
634
  }
635
635
  return out;
636
636
  }
637
- function clipToBytes(s, maxBytes) {
637
+ export function clipToBytes(s, maxBytes) {
638
638
  if (Buffer.byteLength(s, "utf8") <= maxBytes)
639
639
  return s;
640
640
  let lo = 0;
@@ -7,6 +7,11 @@ export declare function promptTokensOf(family: CacheFamily, u: {
7
7
  cacheRead: number;
8
8
  cacheWrite: number;
9
9
  }): number;
10
+ export declare function uncachedInputTokensOf(family: CacheFamily, u: {
11
+ input: number;
12
+ cacheRead: number;
13
+ cacheWrite: number;
14
+ }): number;
10
15
  export declare function usageCostMicroUsd(family: CacheFamily, u: {
11
16
  input?: number;
12
17
  output?: number;
@@ -14,5 +19,6 @@ export declare function usageCostMicroUsd(family: CacheFamily, u: {
14
19
  cacheWrite?: number;
15
20
  }, pricing: ModelPricing): {
16
21
  totalInputTokens: number;
22
+ uncachedInputTokens: number;
17
23
  costMicroUsd: number;
18
24
  };
@@ -12,18 +12,19 @@ export function cacheFamilyOf(model) {
12
12
  export function promptTokensOf(family, u) {
13
13
  return family === "input-excludes-cached" ? u.input + u.cacheRead + u.cacheWrite : u.input;
14
14
  }
15
+ export function uncachedInputTokensOf(family, u) {
16
+ return Math.max(0, promptTokensOf(family, u) - u.cacheRead - u.cacheWrite);
17
+ }
15
18
  export function usageCostMicroUsd(family, u, pricing) {
16
- const totalInputTokens = promptTokensOf(family, {
17
- input: u.input || 0,
18
- cacheRead: u.cacheRead || 0,
19
- cacheWrite: u.cacheWrite || 0,
20
- });
19
+ const normalized = { input: u.input || 0, cacheRead: u.cacheRead || 0, cacheWrite: u.cacheWrite || 0 };
20
+ const totalInputTokens = promptTokensOf(family, normalized);
21
+ const uncachedInputTokens = uncachedInputTokensOf(family, normalized);
21
22
  const costMicroUsd = computeCostMicroUsd({
22
23
  totalInputTokens,
23
- cacheReadTokens: u.cacheRead || 0,
24
- cacheWriteTokens: u.cacheWrite || 0,
24
+ cacheReadTokens: normalized.cacheRead,
25
+ cacheWriteTokens: normalized.cacheWrite,
25
26
  cacheWriteTokensLong: 0,
26
27
  outputTokens: u.output || 0,
27
28
  }, pricing);
28
- return { totalInputTokens, costMicroUsd };
29
+ return { totalInputTokens, uncachedInputTokens, costMicroUsd };
29
30
  }
@@ -0,0 +1,17 @@
1
+ export interface SafeNotifyFailure {
2
+ readonly site: string;
3
+ readonly error: Error;
4
+ readonly failures: number;
5
+ }
6
+ export interface SafeNotifierOptions {
7
+ readonly onError?: (failure: SafeNotifyFailure) => void;
8
+ }
9
+ export interface SafeNotifier {
10
+ notify(run: () => void, site: string): boolean;
11
+ notifyAsync(run: () => unknown, site: string): Promise<boolean>;
12
+ readonly failures: number;
13
+ failuresAt(site: string): number;
14
+ lastErrorAt(site: string): Error | undefined;
15
+ failedSites(): readonly string[];
16
+ }
17
+ export declare function createSafeNotifier(opts?: SafeNotifierOptions): SafeNotifier;
@@ -0,0 +1,59 @@
1
+ export function createSafeNotifier(opts) {
2
+ const onError = opts?.onError;
3
+ let sites;
4
+ let total = 0;
5
+ const record = (site, thrown) => {
6
+ const error = thrown instanceof Error ? thrown : new Error(String(thrown));
7
+ total += 1;
8
+ if (sites === undefined)
9
+ sites = new Map();
10
+ const state = sites.get(site);
11
+ if (state !== undefined) {
12
+ state.count += 1;
13
+ state.last = error;
14
+ return;
15
+ }
16
+ sites.set(site, { count: 1, last: error });
17
+ if (onError === undefined)
18
+ return;
19
+ try {
20
+ onError({ site, error, failures: 1 });
21
+ }
22
+ catch {
23
+ }
24
+ };
25
+ return {
26
+ notify(run, site) {
27
+ try {
28
+ run();
29
+ return true;
30
+ }
31
+ catch (e) {
32
+ record(site, e);
33
+ return false;
34
+ }
35
+ },
36
+ async notifyAsync(run, site) {
37
+ try {
38
+ await run();
39
+ return true;
40
+ }
41
+ catch (e) {
42
+ record(site, e);
43
+ return false;
44
+ }
45
+ },
46
+ get failures() {
47
+ return total;
48
+ },
49
+ failuresAt(site) {
50
+ return sites?.get(site)?.count ?? 0;
51
+ },
52
+ lastErrorAt(site) {
53
+ return sites?.get(site)?.last;
54
+ },
55
+ failedSites() {
56
+ return sites === undefined ? [] : [...sites.keys()];
57
+ },
58
+ };
59
+ }
@@ -1,5 +1,9 @@
1
1
  const SECRET_ENV_RE = /(^|_)(KEY|TOKEN|SECRET|SECRETS|PASSWORD|PASSWD|CREDENTIAL|CREDENTIALS|PRIVATEKEY|APIKEY|ACCESS_KEY|SESSION_TOKEN)(?:_(?:\d+|ID))?$/i;
2
- const SECRET_ENV_EXACT_NAMES = new Set(["DATABASE_URL", "CONNECTION_STRING", "DSN", "NPM_AUTH"]);
2
+ const SECRET_ENV_EXACT_NAMES = new Set([
3
+ "DATABASE_URL", "CONNECTION_STRING", "DSN", "NPM_AUTH",
4
+ "PGPASSWORD", "MYSQL_PWD", "SSHPASS", "GPG_PASSPHRASE", "GH_PAT", "GITHUB_PAT",
5
+ "NPM_CONFIG__AUTH", "NPM_CONFIG__AUTHTOKEN",
6
+ ]);
3
7
  export function isSecretEnvKey(key) {
4
8
  return SECRET_ENV_RE.test(key) || SECRET_ENV_EXACT_NAMES.has(key.toUpperCase());
5
9
  }
@@ -14,6 +14,9 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
14
14
  ".ssh",
15
15
  ".git/hooks",
16
16
  ".git/config",
17
+ ".git/modules",
18
+ ".mcp.json",
19
+ ".claude*",
17
20
  ".aws",
18
21
  ".config/gcloud",
19
22
  ".azure",
@@ -24,15 +27,19 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
24
27
  ".zsh_history",
25
28
  ];
26
29
  function segmentGlobToRegExp(segment) {
27
- const escaped = segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*");
28
- return new RegExp(`^${escaped}$`);
30
+ const escaped = segment.replace(/[.+^${}()|[\]\\?]/g, "\\$&").replace(/\*/g, "[^/]*");
31
+ return new RegExp(`^${escaped}$`, CASE_INSENSITIVE_FS ? "i" : "");
29
32
  }
33
+ const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
30
34
  function compilePatterns(patterns) {
31
35
  const out = [];
32
36
  for (const raw of patterns) {
33
37
  const segments = raw.split("/").filter(Boolean).map(segmentGlobToRegExp);
34
- if (segments.length > 0)
35
- out.push({ raw, segments });
38
+ if (segments.length === 0) {
39
+ throw new Error(`createSensitivePathPolicy: pattern ${JSON.stringify(raw)} contains no path segments and would guard nothing. ` +
40
+ `Patterns are "/"-separated runs of path SEGMENTS (e.g. ".ssh", ".git/hooks", "*.pem"); remove the entry or spell the segments.`);
41
+ }
42
+ out.push({ raw, segments });
36
43
  }
37
44
  return out;
38
45
  }
@@ -20,7 +20,7 @@ export function createCheckpointFixture(over = {}) {
20
20
  },
21
21
  state: {
22
22
  activeTools: [],
23
- nestedStats: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0 },
23
+ nestedStats: { tokens: 0, turns: 0, tasks: 0, costMicroUsd: 0, anyUnpriced: false },
24
24
  },
25
25
  status: "pending",
26
26
  createdAt: 1_700_000_000_000,
@@ -32,5 +32,45 @@ export async function sessionRepoContract(make, runAssertion) {
32
32
  const repo = make();
33
33
  await assert.rejects(repo.open({ id: "0198fa00-0000-7000-8000-0000000000ff", createdAt: "" }), /not.?found/i);
34
34
  });
35
+ run("delete() makes the id not_found — the history is gone, not merely hidden from open()", async () => {
36
+ const repo = make();
37
+ const sess = await repo.create({ id: A });
38
+ await sess.getStorage().appendEntry(SESSION_ENTRY("e1", null));
39
+ await repo.delete({ id: A, createdAt: "" });
40
+ await assert.rejects(repo.open({ id: A, createdAt: "" }), /not.?found/i);
41
+ const fresh = await repo.create({ id: A });
42
+ assert.deepEqual((await fresh.getStorage().getEntries()).map((e) => e.id), []);
43
+ });
44
+ run("fork() copies the log, stamps forkedFrom, and leaves the SOURCE untouched", async () => {
45
+ const repo = make();
46
+ const src = await repo.create({ id: A });
47
+ await src.getStorage().appendEntry(SESSION_ENTRY("e1", null));
48
+ await src.getStorage().appendEntry(SESSION_ENTRY("e2", "e1"));
49
+ const B = "0198fa00-0000-7000-8000-00000000000b";
50
+ const forked = await repo.fork({ id: A, createdAt: "" }, { id: B });
51
+ assert.deepEqual((await forked.getStorage().getEntries()).map((e) => e.id), ["e1", "e2"]);
52
+ assert.equal((await forked.getMetadata()).forkedFrom, A, "fork lineage must be stamped (a reap walks it)");
53
+ await src.getStorage().appendEntry(SESSION_ENTRY("e3", "e2"));
54
+ assert.deepEqual((await (await repo.open({ id: A, createdAt: "" })).getStorage().getEntries()).map((e) => e.id), ["e1", "e2", "e3"]);
55
+ assert.deepEqual((await (await repo.open({ id: B, createdAt: "" })).getStorage().getEntries()).map((e) => e.id), ["e1", "e2"]);
56
+ });
57
+ run("exportEntries → importEntries round-trips the log verbatim, and the imported session REPLACES what was there", async () => {
58
+ const repo = make();
59
+ if (repo.exportEntries === undefined || repo.importEntries === undefined)
60
+ return;
61
+ const src = await repo.create({ id: A });
62
+ await src.getStorage().appendEntry(SESSION_ENTRY("e1", null));
63
+ await src.getStorage().appendEntry(SESSION_ENTRY("e2", "e1"));
64
+ const donor = await repo.exportEntries(A);
65
+ assert.deepEqual(donor.map((e) => e.id), ["e1", "e2"]);
66
+ const C = "0198fa00-0000-7000-8000-00000000000c";
67
+ const target = await repo.create({ id: C });
68
+ await target.getStorage().appendEntry(SESSION_ENTRY("old", null));
69
+ await repo.importEntries(C, undefined, donor);
70
+ const after = await repo.exportEntries(C);
71
+ assert.deepEqual(after.map((e) => e.id), ["e1", "e2"]);
72
+ assert.deepEqual(after.map((e) => e.parentId), [null, "e1"]);
73
+ assert.equal((await repo.exportEntries(A)).length, 2, "importing into C must not disturb the donor session");
74
+ });
35
75
  await settle();
36
76
  }
@@ -1,4 +1,4 @@
1
- import { type ExecutionEnv, ExecutionError, FileError, type FileInfo, type Result } from "../internal/harness.js";
1
+ import { type ExecResult, type ExecutionEnv, ExecutionError, FileError, type FileInfo, type Result } from "../internal/harness.js";
2
2
  export declare class StubExecutionEnv implements ExecutionEnv {
3
3
  cwd: string;
4
4
  constructor(cwd?: string);
@@ -18,10 +18,6 @@ export declare class StubExecutionEnv implements ExecutionEnv {
18
18
  remove(path: string): Promise<Result<void, FileError>>;
19
19
  createTempDir(): Promise<Result<string, FileError>>;
20
20
  createTempFile(): Promise<Result<string, FileError>>;
21
- exec(): Promise<Result<{
22
- stdout: string;
23
- stderr: string;
24
- exitCode: number;
25
- }, ExecutionError>>;
21
+ exec(): Promise<Result<ExecResult, ExecutionError>>;
26
22
  cleanup(): Promise<void>;
27
23
  }
@@ -0,0 +1,4 @@
1
+ export declare const isHighSurrogate: (c: number) => boolean;
2
+ export declare const isLowSurrogate: (c: number) => boolean;
3
+ export declare function sliceHeadSafe(s: string, keep: number): string;
4
+ export declare function sliceTailSafe(s: string, keep: number): string;
@@ -0,0 +1,18 @@
1
+ export const isHighSurrogate = (c) => c >= 0xd800 && c <= 0xdbff;
2
+ export const isLowSurrogate = (c) => c >= 0xdc00 && c <= 0xdfff;
3
+ export function sliceHeadSafe(s, keep) {
4
+ if (keep <= 0)
5
+ return "";
6
+ if (s.length <= keep)
7
+ return s;
8
+ const head = s.slice(0, keep);
9
+ return keep > 0 && isHighSurrogate(head.charCodeAt(keep - 1)) ? head.slice(0, -1) : head;
10
+ }
11
+ export function sliceTailSafe(s, keep) {
12
+ if (keep <= 0)
13
+ return "";
14
+ if (s.length <= keep)
15
+ return s;
16
+ const tail = s.slice(-keep);
17
+ return keep > 0 && isLowSurrogate(tail.charCodeAt(0)) ? tail.slice(1) : tail;
18
+ }
@@ -1,5 +1,5 @@
1
1
  import { delimitUntrusted } from "./untrusted-text.js";
2
- import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, MONITOR_STORM_BURST, MONITOR_STORM_KILL_AFTER_MS, MONITOR_LINE_BUF_CAP, MONITOR_SPILL_CAP_CHARS, TASK_OUTPUT_MAX_CHARS, mintCompletionId, clipMonitorEvent, clipMonitorLine, terminalTaskSummary, accountDroppedBytes, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
2
+ import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, MONITOR_STORM_BURST, MONITOR_STORM_KILL_AFTER_MS, MONITOR_LINE_BUF_CAP, MONITOR_SPILL_CAP_CHARS, TASK_OUTPUT_MAX_CHARS, mintCompletionId, clipMonitorEvent, clipMonitorLine, terminalTaskSummary, accountDroppedBytes, renderSpoolBody, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
3
3
  import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
4
4
  export function registerMonitorLane(core, input) {
5
5
  assertOwnership(input, "registerMonitor");
@@ -42,7 +42,13 @@ export function registerMonitorLane(core, input) {
42
42
  ...(input.sessionId !== undefined ? { spillSessionId: input.sessionId } : {}),
43
43
  };
44
44
  core.handles.set(id, handle);
45
- startMonitorWatcherLane(core, id);
45
+ try {
46
+ startMonitorWatcherLane(core, id);
47
+ }
48
+ catch (e) {
49
+ core.handles.delete(id);
50
+ throw e;
51
+ }
46
52
  return id;
47
53
  }
48
54
  function spillRolledMonitorChunk(handle, stream, dropped) {
@@ -312,18 +318,13 @@ export async function pollMonitorLane(handle, filter, deadline, signal) {
312
318
  const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
313
319
  const running = handle.status === "running";
314
320
  const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
315
- const timedOutClause = spool.timedOut === true ? ` (timed out after ${spool.timeoutSec !== undefined ? `${spool.timeoutSec}s` : "its background time budget"})` : "";
316
- const displayStatus = (spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : running ? "running" : handle.status) + timedOutClause;
317
- const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)${monitorSpillNote(handle)}\n` : "";
318
- const droppedNote = (spool.droppedBytes ?? 0) > 0
319
- ? `[!] ${spool.droppedBytes} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n`
320
- : spool.dropUnknown === true
321
- ? "[!] an unknown amount of earlier output was permanently dropped before this window (buffer evicted)\n"
322
- : "";
323
- const drainNote = spool.drainFailed === true ? "[!] the final output drain at stop failed — the tail of the output may be missing\n" : "";
324
- const stdoutHeading = droppedNote !== "" || drainNote !== "" ? "stdout (re-readable spool — INCOMPLETE, see note above)" : "stdout (full, re-readable)";
325
- const body = `status: ${displayStatus}\n${rolledNote}${droppedNote}${drainNote}--- ${stdoutHeading} ---\n${shape(spool.stdout) || "(none)"}\n` +
326
- `--- stderr ---\n${shape(spool.stderr) || "(none)"}`;
321
+ const body = renderSpoolBody(spool, {
322
+ running,
323
+ handleStatus: handle.status,
324
+ shape,
325
+ rolledExtraClause: monitorSpillNote(handle),
326
+ drainNote: spool.drainFailed === true ? "[!] the final output drain at stop failed — the tail of the output may be missing\n" : "",
327
+ });
327
328
  return {
328
329
  content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
329
330
  details: {
@@ -252,6 +252,28 @@ export declare function accountDroppedBytes(spool: {
252
252
  truncated?: boolean;
253
253
  bytesDroppedBeforeCursor?: number;
254
254
  }): void;
255
+ export declare function spoolDropNote(acct: {
256
+ droppedBytes?: number;
257
+ dropUnknown?: true;
258
+ }): string;
259
+ export interface RenderableSpool {
260
+ stdout: string;
261
+ stderr: string;
262
+ rolledChars: number;
263
+ exitCode?: number;
264
+ timedOut?: boolean;
265
+ timeoutSec?: number;
266
+ droppedBytes?: number;
267
+ dropUnknown?: true;
268
+ }
269
+ export interface SpoolBodyOptions {
270
+ running: boolean;
271
+ handleStatus: SemaTaskStatus;
272
+ shape: (text: string) => string;
273
+ rolledExtraClause: string;
274
+ drainNote: string;
275
+ }
276
+ export declare function renderSpoolBody(spool: RenderableSpool, opts: SpoolBodyOptions): string;
255
277
  export declare function droppedGapNote(spool: {
256
278
  droppedBytes?: number;
257
279
  dropUnknown?: true;
@@ -102,6 +102,22 @@ export function accountDroppedBytes(spool, poll) {
102
102
  else if (poll.truncated === true)
103
103
  spool.dropUnknown = true;
104
104
  }
105
+ export function spoolDropNote(acct) {
106
+ if ((acct.droppedBytes ?? 0) > 0)
107
+ return `[!] ${acct.droppedBytes} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n`;
108
+ if (acct.dropUnknown === true)
109
+ return "[!] an unknown amount of earlier output was permanently dropped before this window (buffer evicted)\n";
110
+ return "";
111
+ }
112
+ export function renderSpoolBody(spool, opts) {
113
+ const timedOutClause = spool.timedOut === true ? ` (timed out after ${spool.timeoutSec !== undefined ? `${spool.timeoutSec}s` : "its background time budget"})` : "";
114
+ const displayStatus = (spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : opts.running ? "running" : opts.handleStatus) + timedOutClause;
115
+ const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)${opts.rolledExtraClause}\n` : "";
116
+ const droppedNote = spoolDropNote(spool);
117
+ const stdoutHeading = droppedNote !== "" || opts.drainNote !== "" ? "stdout (re-readable spool — INCOMPLETE, see note above)" : "stdout (full, re-readable)";
118
+ return (`status: ${displayStatus}\n${rolledNote}${droppedNote}${opts.drainNote}--- ${stdoutHeading} ---\n${opts.shape(spool.stdout) || "(none)"}\n` +
119
+ `--- stderr ---\n${opts.shape(spool.stderr) || "(none)"}`);
120
+ }
105
121
  export function droppedGapNote(spool) {
106
122
  if ((spool.droppedBytes ?? 0) > 0)
107
123
  return ` [!] ${spool.droppedBytes} earlier byte(s) of output were permanently dropped before this window (execution-environment tail buffer evicted) — the event stream and this spool are INCOMPLETE.`;
@@ -69,6 +69,7 @@ export declare class TaskRegistry {
69
69
  private sessionReapHooks;
70
70
  private bgQuiescenceWatchers;
71
71
  private legacyToTaskId;
72
+ private readonly notifier;
72
73
  private readonly core;
73
74
  durableAgentArmed(id: string): boolean;
74
75
  durableAgentRowProbe(id: string): (() => Promise<boolean>) | undefined;
@@ -4,9 +4,10 @@ import { hasBackgroundShell } from "./background-shell.js";
4
4
  import { shutdownDebug } from "./shutdown-debug.js";
5
5
  import { canAccessAgentRecord, } from "./background-agent-store.js";
6
6
  import { delimitUntrusted } from "./untrusted-text.js";
7
+ import { createSafeNotifier } from "./safe-notify.js";
7
8
  import { registerMonitorLane, pollMonitorLane, stopMonitorLane } from "./task-registry-monitor.js";
8
9
  import { registerWorkflowLane, pollWorkflowLane, stopWorkflowLane } from "./task-registry-workflow.js";
9
- import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, droppedGapNote, alreadyTerminalStopNote, terminalTaskSummary, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
10
+ import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, renderSpoolBody, spoolDropNote, droppedGapNote, alreadyTerminalStopNote, terminalTaskSummary, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
10
11
  export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR } from "./task-registry-shared.js";
11
12
  import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_ALIASES, TASK_STOP_ALIASES, TASK_OUTPUT_CONTRACT, TASK_STOP_CONTRACT, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, REGISTRY_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "./task-tool-shape.js";
12
13
  import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, runningAgentFooterLane, notFoundRunningAgentsTail, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
@@ -47,6 +48,31 @@ function bashTerminalResult(spool) {
47
48
  const err = spool.stderr.slice(-2_000);
48
49
  return err.length > 0 ? `${out}${out.length > 0 ? "\n" : ""}--- stderr (tail) ---\n${err}` : out;
49
50
  }
51
+ async function drainBashTailBestEffort(handle) {
52
+ if (handle.outputFile === undefined)
53
+ return;
54
+ try {
55
+ const drained = await handle.env.pollBackground(handle.shellId);
56
+ if (drained.ok && handle.spool !== undefined)
57
+ accountDroppedBytes(handle.spool, drained.value);
58
+ if (drained.ok && (drained.value.stdout.length > 0 || drained.value.stderr.length > 0)) {
59
+ if (handle.spool !== undefined) {
60
+ const spool = handle.spool;
61
+ const cap = 2 * TASK_OUTPUT_MAX_CHARS;
62
+ spool.stdout = rollSpoolText(spool, spool.stdout + drained.value.stdout, cap);
63
+ spool.stderr = rollSpoolText(spool, spool.stderr + drained.value.stderr, cap);
64
+ const ap = await handle.env.appendFile(handle.outputFile, drained.value.stdout + drained.value.stderr);
65
+ if (!ap.ok)
66
+ spool.mirrorFailed = true;
67
+ }
68
+ else {
69
+ await handle.env.appendFile(handle.outputFile, drained.value.stdout + drained.value.stderr);
70
+ }
71
+ }
72
+ }
73
+ catch {
74
+ }
75
+ }
50
76
  function bashMirrorGapNote(handle) {
51
77
  return handle.outputFile !== undefined && handle.spool?.mirrorFailed === true
52
78
  ? " [!] The output file is INCOMPLETE (one or more writes to it failed) — trust the result text / TaskOutput over the file."
@@ -77,6 +103,7 @@ export class TaskRegistry {
77
103
  sessionReapHooks = new Set();
78
104
  bgQuiescenceWatchers = new Map();
79
105
  legacyToTaskId = new Map();
106
+ notifier = createSafeNotifier();
80
107
  core = (() => {
81
108
  const self = this;
82
109
  return {
@@ -407,22 +434,7 @@ export class TaskRegistry {
407
434
  handle.stoppedBy = handle.stopSource ?? source;
408
435
  handle.updatedAt = Date.now();
409
436
  mintCompletionId(handle);
410
- if (handle.outputFile !== undefined) {
411
- try {
412
- const drained = await handle.env.pollBackground(handle.shellId);
413
- if (drained.ok && (drained.value.stdout.length > 0 || drained.value.stderr.length > 0)) {
414
- const spool = (handle.spool ??= { stdout: "", stderr: "", rolledChars: 0 });
415
- const cap = 2 * TASK_OUTPUT_MAX_CHARS;
416
- spool.stdout = rollSpoolText(spool, spool.stdout + drained.value.stdout, cap);
417
- spool.stderr = rollSpoolText(spool, spool.stderr + drained.value.stderr, cap);
418
- const ap = await handle.env.appendFile(handle.outputFile, drained.value.stdout + drained.value.stderr);
419
- if (!ap.ok)
420
- spool.mirrorFailed = true;
421
- }
422
- }
423
- catch {
424
- }
425
- }
437
+ await drainBashTailBestEffort(handle);
426
438
  const resultText = handle.spool !== undefined ? bashTerminalResult(handle.spool) : "";
427
439
  this.notifyTerminalOnce(handle, () => handle.onTerminal?.({
428
440
  task_id: handle.id,
@@ -521,21 +533,13 @@ export class TaskRegistry {
521
533
  this.settleBackgroundAgent(handle.id, { status: "killed", error: "session released", cycle: handle.reviveCycle ?? 0 });
522
534
  if (reapTerminalNote !== undefined && handle.terminalNotified !== true) {
523
535
  handle.terminalNotified = true;
524
- try {
525
- reapTerminalNote();
526
- }
527
- catch {
528
- }
536
+ this.notifier.notify(() => reapTerminalNote(), "taskRegistry.reapTerminalNote");
529
537
  }
530
538
  reaped++;
531
539
  }
532
540
  this.pokeBgQuiescence(sessionId);
533
541
  for (const hook of this.sessionReapHooks) {
534
- try {
535
- hook(sessionId, scope);
536
- }
537
- catch {
538
- }
542
+ this.notifier.notify(() => hook(sessionId, scope), "taskRegistry.onSessionReap");
539
543
  }
540
544
  return reaped;
541
545
  }
@@ -585,11 +589,7 @@ export class TaskRegistry {
585
589
  this.bgQuiescenceWatchers.delete(oldest);
586
590
  queueMicrotask(() => {
587
591
  for (const victim of victims) {
588
- try {
589
- victim("evicted");
590
- }
591
- catch {
592
- }
592
+ this.notifier.notify(() => victim("evicted"), "taskRegistry.bgQuiescence.evicted");
593
593
  }
594
594
  });
595
595
  }
@@ -617,22 +617,14 @@ export class TaskRegistry {
617
617
  return;
618
618
  this.bgQuiescenceWatchers.delete(owner);
619
619
  for (const cb of set) {
620
- try {
621
- cb("quiescent");
622
- }
623
- catch {
624
- }
620
+ this.notifier.notify(() => cb("quiescent"), "taskRegistry.bgQuiescence.quiescent");
625
621
  }
626
622
  }
627
623
  notifyTerminalOnce(handle, send) {
628
624
  if (handle.terminalNotified === true)
629
625
  return;
630
626
  handle.terminalNotified = true;
631
- try {
632
- send();
633
- }
634
- catch {
635
- }
627
+ this.notifier.notify(() => send(), "taskRegistry.notifyTerminalOnce");
636
628
  this.pokeBgQuiescence(handle.owner);
637
629
  }
638
630
  startBashWatcher(id, onTerminal) {
@@ -1049,17 +1041,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1049
1041
  const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
1050
1042
  const running = handle.status === "running";
1051
1043
  const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
1052
- const timedOutClause = spool.timedOut === true ? ` (timed out after ${spool.timeoutSec !== undefined ? `${spool.timeoutSec}s` : "its background time budget"})` : "";
1053
- const displayStatus = (spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : running ? "running" : handle.status) + timedOutClause;
1054
- const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)\n` : "";
1055
- const droppedNote = (spool.droppedBytes ?? 0) > 0
1056
- ? `[!] ${spool.droppedBytes} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n`
1057
- : spool.dropUnknown === true
1058
- ? "[!] an unknown amount of earlier output was permanently dropped before this window (buffer evicted)\n"
1059
- : "";
1060
- const stdoutHeading = droppedNote !== "" ? "stdout (re-readable spool — INCOMPLETE, see note above)" : "stdout (full, re-readable)";
1061
- const body = `status: ${displayStatus}\n${rolledNote}${droppedNote}--- ${stdoutHeading} ---\n${shape(spool.stdout) || "(none)"}\n` +
1062
- `--- stderr ---\n${shape(spool.stderr) || "(none)"}`;
1044
+ const body = renderSpoolBody(spool, { running, handleStatus: handle.status, shape, rolledExtraClause: "", drainNote: "" });
1063
1045
  return {
1064
1046
  content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
1065
1047
  details: {
@@ -1089,7 +1071,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1089
1071
  };
1090
1072
  let stdout = "";
1091
1073
  let stderr = "";
1092
- let dropped = 0;
1074
+ const dropAcct = {};
1093
1075
  for (;;) {
1094
1076
  const r = await handle.env.pollBackground(handle.shellId);
1095
1077
  if (!r.ok) {
@@ -1107,7 +1089,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1107
1089
  }
1108
1090
  stdout = roll(stdout + r.value.stdout);
1109
1091
  stderr = roll(stderr + r.value.stderr);
1110
- dropped += r.value.bytesDroppedBeforeCursor ?? 0;
1092
+ accountDroppedBytes(dropAcct, r.value);
1111
1093
  handle.status = statusFromBackground(r.value.status, r.value.exitCode);
1112
1094
  if (handle.status === "killed" && handle.stoppedBy === undefined)
1113
1095
  handle.stoppedBy = handle.stopSource ?? (r.value.timedOut === true ? "timeout" : "system");
@@ -1120,7 +1102,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1120
1102
  const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
1121
1103
  const timedOutClause = r.value.timedOut === true ? ` (timed out after ${r.value.timeoutSec !== undefined ? `${r.value.timeoutSec}s` : "its background time budget"})` : "";
1122
1104
  const displayStatus = (r.value.status === "exited" ? `exited(code ${r.value.exitCode})` : r.value.status) + timedOutClause;
1123
- const droppedNote = (dropped > 0 ? `[!] ${dropped} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n` : "") +
1105
+ const droppedNote = spoolDropNote(dropAcct) +
1124
1106
  (rolledChars > 0 ? `[!] ${rolledChars} accumulated char(s) dropped from the middle during the blocked wait (memory bound)\n` : "");
1125
1107
  const body = `status: ${displayStatus}\n${droppedNote}--- new stdout ---\n${shape(stdout) || "(none)"}\n` +
1126
1108
  `--- new stderr ---\n${shape(stderr) || "(none)"}`;
@@ -1136,7 +1118,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1136
1118
  details: {
1137
1119
  status: r.value.status,
1138
1120
  exitCode: r.value.exitCode,
1139
- bytesDroppedBeforeCursor: dropped,
1121
+ bytesDroppedBeforeCursor: dropAcct.droppedBytes ?? 0,
1140
1122
  ...(r.value.timedOut === true ? { timedOut: true, ...(r.value.timeoutSec !== undefined ? { timeoutSec: r.value.timeoutSec } : {}) } : {}),
1141
1123
  },
1142
1124
  },
@@ -1205,6 +1187,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1205
1187
  handle.status = "killed";
1206
1188
  handle.updatedAt = Date.now();
1207
1189
  mintCompletionId(handle);
1190
+ await drainBashTailBestEffort(handle);
1208
1191
  this.notifyTerminalOnce(handle, () => explicitStopBashTerminalNote(handle, false));
1209
1192
  return {
1210
1193
  content: `Terminated ${handle.id}.`,