@sema-agent/core 2.0.1 → 2.2.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 (71) hide show
  1. package/dist/agents/observer.d.ts +14 -0
  2. package/dist/agents/observer.js +66 -12
  3. package/dist/agents/send-message-tool.js +164 -88
  4. package/dist/agents/subagent.d.ts +11 -4
  5. package/dist/agents/subagent.js +166 -62
  6. package/dist/core/context-edit.js +16 -3
  7. package/dist/core/file-snapshot-store.js +10 -1
  8. package/dist/core/memory-engine/dual-root.js +2 -0
  9. package/dist/core/memory-engine/engine.d.ts +4 -0
  10. package/dist/core/memory-engine/engine.js +6 -1
  11. package/dist/core/runner/prepare-memory.d.ts +4 -0
  12. package/dist/core/runner/prepare-memory.js +4 -1
  13. package/dist/core/runner/prepare-task.d.ts +9 -2
  14. package/dist/core/runner/prepare-task.js +68 -13
  15. package/dist/core/runner/runtask.js +919 -865
  16. package/dist/core/runner/synthetic-tools.d.ts +1 -0
  17. package/dist/core/runner/synthetic-tools.js +18 -15
  18. package/dist/core/runner/turn-attachments.d.ts +27 -3
  19. package/dist/core/runner/turn-attachments.js +101 -12
  20. package/dist/core/task-registry-agent.d.ts +9 -1
  21. package/dist/core/task-registry-agent.js +23 -2
  22. package/dist/core/task-registry-monitor.js +79 -24
  23. package/dist/core/task-registry-shared.d.ts +13 -1
  24. package/dist/core/task-registry-shared.js +21 -0
  25. package/dist/core/task-registry.d.ts +2 -0
  26. package/dist/core/task-registry.js +24 -26
  27. package/dist/core/tool-result-budget.js +2 -2
  28. package/dist/core/tool-result-store.d.ts +2 -0
  29. package/dist/core/tool-result-store.js +27 -2
  30. package/dist/core/types.d.ts +4 -2
  31. package/dist/core/workflow-journal-store.d.ts +13 -0
  32. package/dist/engine/session/import-validate.js +29 -0
  33. package/dist/engine/session/memory-repo.js +5 -0
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/orchestration/workflow-size-guideline.d.ts +6 -1
  37. package/dist/orchestration/workflow-size-guideline.js +19 -9
  38. package/dist/orchestration/workflow.d.ts +1 -0
  39. package/dist/orchestration/workflow.js +44 -1
  40. package/dist/prompt-assembly/assemble.js +3 -7
  41. package/dist/prompt-assembly/event-registry.js +1 -1
  42. package/dist/prompt-assembly/packs/sema-default.js +8 -5
  43. package/dist/prompts/coordinator.d.ts +1 -1
  44. package/dist/prompts/coordinator.js +45 -0
  45. package/dist/prompts/default.d.ts +4 -5
  46. package/dist/prompts/default.js +16 -18
  47. package/dist/prompts/simple-sections.d.ts +3 -1
  48. package/dist/prompts/simple-sections.js +11 -1
  49. package/dist/stores/cc/task-list-store.js +3 -3
  50. package/dist/stores/file/memory-store.d.ts +3 -0
  51. package/dist/stores/file/memory-store.js +39 -12
  52. package/dist/stores/file/tool-result-store.js +16 -2
  53. package/dist/stores/file/workflow-journal-store.d.ts +23 -0
  54. package/dist/stores/file/workflow-journal-store.js +140 -3
  55. package/dist/tools/fs/bash-readonly-classifier.js +21 -2
  56. package/dist/tools/fs/fs-bash.d.ts +1 -0
  57. package/dist/tools/fs/fs-bash.js +10 -3
  58. package/dist/tools/fs/fs-read.js +12 -12
  59. package/dist/tools/fs/fs-search-tools.js +42 -7
  60. package/dist/tools/fs/fs-write.js +18 -6
  61. package/dist/tools/fs/index.d.ts +1 -0
  62. package/dist/tools/fs/index.js +2 -1
  63. package/dist/tools/fs/safety.d.ts +4 -0
  64. package/dist/tools/fs/safety.js +113 -10
  65. package/dist/tools/fs/search.d.ts +1 -0
  66. package/dist/tools/fs/search.js +23 -3
  67. package/dist/tools/monitor.js +1 -1
  68. package/dist/tools/task-list.d.ts +1 -0
  69. package/dist/tools/task-list.js +13 -2
  70. package/dist/tools/web.js +36 -6
  71. package/package.json +3 -2
@@ -19,12 +19,9 @@ import { BG_AGENT_REAP_STOP_ERROR } from "../core/task-registry.js";
19
19
  import { extractErrorCode } from "../brain/errors.js";
20
20
  import { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
21
21
  export { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
22
- import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getSessionRetainLedger, getOrCreateSessionRetainLedger, releaseSessionRetainLedger, ensureSessionReapHook, createResumePrompt, } from "./retain-ledger.js";
23
- export { SubagentRetainLedger, getSessionRetainLedger, releaseSessionRetainLedger };
24
- export { AGENT_TRANSCRIPT_TOOL_NAME, createAgentTranscriptTool } from "./agent-transcript-tool.js";
25
- export { SEND_MESSAGE_TOOL_NAME, createSendMessageTool } from "./send-message-tool.js";
22
+ import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getOrCreateSessionRetainLedger, ensureSessionReapHook, createResumePrompt, } from "./retain-ledger.js";
26
23
  import { recordRosterSpawn } from "./roster-store.js";
27
- import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, observerFramingPrompt, observerSlug, resolveObserverDeclaration, } from "./observer.js";
24
+ import { ObserverDigestTap, ObserverPairing, createObserverReportToolSpec, markObserverTaskId, unmarkObserverTaskId, isObserverTaskId, ObserverResumeStateError, ObserverStoppedByUserError, observerFramingPrompt, observerSlug, resolveObserverDeclaration, } from "./observer.js";
28
25
  import { SubagentStepRecorder } from "./subagent-steps.js";
29
26
  const BG_AGENT_RESULT_MAX = 4_000;
30
27
  const BG_AGENT_RESULT_FULL_MAX = 200_000;
@@ -68,6 +65,7 @@ export function inheritedManifestScopeFor(snapshot) {
68
65
  export const DEFAULT_SUBAGENT_TOOL_NAME = "Agent";
69
66
  export const LEGACY_SUBAGENT_TOOL_NAME = "Task";
70
67
  export const EXTRA_TOOLS_MAX_FACTORY_CALLS_PER_TREE = 64;
68
+ export const DEFAULT_SUBAGENT_MAX_DEPTH = 3;
71
69
  export const EXTRA_TOOLS_FAILED_NOTE = "note: extraTools evaluation failed — the injected tool set was skipped for this spawn.";
72
70
  export const EXTRA_TOOLS_BUDGET_NOTE = `note: extraTools evaluation was skipped — this spawn tree exhausted its factory-call budget (${EXTRA_TOOLS_MAX_FACTORY_CALLS_PER_TREE}); no dynamic tools were injected for this spawn.`;
73
71
  function countArgLines(v) {
@@ -131,6 +129,9 @@ function createToolStatsCounter(delegationToolName) {
131
129
  snapshot() {
132
130
  return t.readCount + t.searchCount + t.bashCount + t.editFileCount + t.otherToolCount > 0 ? { ...t } : undefined;
133
131
  },
132
+ total() {
133
+ return t.readCount + t.searchCount + t.bashCount + t.editFileCount + t.otherToolCount;
134
+ },
134
135
  };
135
136
  }
136
137
  export const FORK_SUBAGENT_TYPE = "fork";
@@ -580,6 +581,9 @@ function adaptLegacyForkArgs(args) {
580
581
  }
581
582
  return args;
582
583
  }
584
+ export function normalizeSubagentType(value) {
585
+ return value.normalize("NFKC").toLowerCase().replace(/[\p{White_Space}\p{Pd}_]+/gu, "");
586
+ }
583
587
  function foldGeneralPurposeAlias(args, shadowed) {
584
588
  if (shadowed)
585
589
  return args;
@@ -611,6 +615,31 @@ Guidelines (your directive may override any of these):
611
615
  </fork-boilerplate>
612
616
 
613
617
  Your directive: `;
618
+ export function forkWorktreeTranslationNote(parentCwd, worktreeDir) {
619
+ const where = parentCwd !== undefined ? parentCwd : "a different working directory";
620
+ return (`You've inherited the conversation context above from a parent agent working in ${where}. ` +
621
+ `You are operating in an isolated git worktree at ${worktreeDir} — same repository, same relative file structure, separate working copy. ` +
622
+ `Paths in the inherited context refer to the parent's working directory; translate them to your worktree root. ` +
623
+ `Re-read files before editing if the parent may have modified them since they appear in the context. ` +
624
+ `Your changes stay in this worktree and will not affect the parent's files.`);
625
+ }
626
+ export function asyncLaunchedReceipt(p) {
627
+ const noteLines = (p.notes ?? []).filter((n) => n !== undefined && n !== "");
628
+ const oneShotBlocked = p.notify && p.oneShot === true;
629
+ return (`Async agent launched successfully. (This tool result is internal metadata — never quote or paste any part of it, including the task_id below, into a user-facing reply.)\n` +
630
+ `task_id: ${p.taskId} (internal ID - do not mention to user. Use SendMessage with to: '${p.taskId}', summary: '<5-10 word recap>' to continue this agent.)\n` +
631
+ (oneShotBlocked
632
+ ? `${p.workingLine} This is a ONE-SHOT submission — there is no later turn for a background notification to land in, so do NOT end your turn expecting one. Actively wait instead: TaskOutput(task_id: "${p.taskId}", block: true). If it is still running after the wait, wait again (bounded) rather than ending the turn, or write out your best available answer now if you are near your own time budget.\n`
633
+ : p.notify
634
+ ? `${p.workingLine} You will be notified automatically when it completes. You know nothing about its results until that notification arrives — do not report, assume, or predict them; continue other work or respond to the user in the meantime.\n`
635
+ : `${p.workingLine} Its result is NOT pushed automatically — retrieve progress and results with TaskOutput(task_id) where mounted.\n`) +
636
+ noteLines.map((n) => `${n}\n`).join("") +
637
+ (oneShotBlocked
638
+ ? `In your own words, briefly tell the user what you launched — do not echo this tool result. Do not assume a later message will deliver the result: this submission ends after this turn, so retrieve it now with the blocking TaskOutput wait above before writing your final answer.`
639
+ : p.notify
640
+ ? `In your own words, briefly tell the user what you launched — do not echo this tool result. Agent results will arrive in a subsequent message. If the user asks for progress, say the agent is still running.`
641
+ : `In your own words, briefly tell the user what you launched — do not echo this tool result. The result will NOT arrive on its own — retrieve it with TaskOutput(task_id) where mounted before relying on it.`));
642
+ }
614
643
  export function createSubagentTool(opts) {
615
644
  const catalog = opts.runner.agentCatalog;
616
645
  if (opts.agents === undefined && catalog?.agents)
@@ -683,7 +712,7 @@ export function agentWhenToUseText(def, lean = true) {
683
712
  return (leanText || def.whenToUse) || undefined;
684
713
  }
685
714
  function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
686
- const maxDepth = opts.maxDepth ?? 5;
715
+ const maxDepth = opts.maxDepth ?? DEFAULT_SUBAGENT_MAX_DEPTH;
687
716
  const toolName = opts.name ?? DEFAULT_SUBAGENT_TOOL_NAME;
688
717
  const deploymentNames = new Set((opts.agents ?? []).map((a) => a.name));
689
718
  const builtins = opts.builtinAgents === false ? [] : builtinAgentDefinitions(toolName).filter((d) => !deploymentNames.has(d.name));
@@ -700,6 +729,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
700
729
  ];
701
730
  const rosterNames = opts.models ? Object.keys(opts.models) : undefined;
702
731
  const agentListing = [
732
+ ...(generalPurposeShadowed
733
+ ? []
734
+ : [{
735
+ name: GENERAL_PURPOSE_SUBAGENT_TYPE,
736
+ description: "General-purpose agent for researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you.",
737
+ tools: "All tools",
738
+ }]),
703
739
  ...available.map((a) => ({ name: a.name, description: agentWhenToUseText(a) ?? "", tools: agentToolsNote(a) })),
704
740
  ...(forkOffered
705
741
  ? [{
@@ -821,12 +857,29 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
821
857
  details: { error: "unknown subagent_type" },
822
858
  };
823
859
  }
824
- const rawType = a.subagent_type ?? "";
860
+ const requestedType = a.subagent_type ?? "";
861
+ let rawType = requestedType;
862
+ if (requestedType !== "") {
863
+ const accepted = acceptedSubagentTypes();
864
+ if (!accepted.includes(requestedType)) {
865
+ const wanted = normalizeSubagentType(requestedType);
866
+ const hits = wanted === "" ? [] : accepted.filter((n) => normalizeSubagentType(n) === wanted);
867
+ if (hits.length > 1) {
868
+ return {
869
+ isError: true,
870
+ content: `Sub-agent not started: subagent_type "${requestedType}" is ambiguous — it matches ${hits.join(", ")}. Use the exact name.`,
871
+ details: { error: "ambiguous subagent_type" },
872
+ };
873
+ }
874
+ if (hits.length === 1)
875
+ rawType = hits[0];
876
+ }
877
+ }
825
878
  const wantsFork = forkOffered && rawType === FORK_SUBAGENT_TYPE;
826
879
  if (reviveClaim !== undefined && wantsFork) {
827
880
  return { isError: true, content: `Sub-agent not revived: a "${FORK_SUBAGENT_TYPE}" agent cannot be revived — launch a new agent with the needed context instead.`, details: { error: "revive.fork_unsupported" } };
828
881
  }
829
- const omitted = rawType === "";
882
+ const omitted = rawType === "" || (!generalPurposeShadowed && rawType === GENERAL_PURPOSE_SUBAGENT_TYPE);
830
883
  const def = hasAgents && !wantsFork && !omitted ? agentMap.get(rawType) : undefined;
831
884
  const spawnAgentType = wantsFork ? FORK_SUBAGENT_TYPE : omitted ? GENERAL_PURPOSE_SUBAGENT_TYPE : rawType;
832
885
  if (!def && !wantsFork && !omitted) {
@@ -1060,9 +1113,17 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1060
1113
  let observerTap;
1061
1114
  let observerSessionId;
1062
1115
  let observerFailure;
1116
+ let observerDegradedCount = 0;
1117
+ let observerDegradedFirst;
1063
1118
  const noteObserverFailure = (err) => {
1064
- if (observerFailure === undefined) {
1065
- observerFailure = (err instanceof Error ? err.message : String(err)).slice(0, REPORT_FIELD_MAX);
1119
+ const msg = (err instanceof Error ? err.message : String(err)).slice(0, REPORT_FIELD_MAX);
1120
+ if (observerPairing !== undefined && observerPairing.state === "armed") {
1121
+ observerDegradedCount++;
1122
+ if (observerDegradedFirst === undefined)
1123
+ observerDegradedFirst = msg;
1124
+ }
1125
+ else if (observerFailure === undefined) {
1126
+ observerFailure = msg;
1066
1127
  }
1067
1128
  try {
1068
1129
  opts.onObserverError?.(err, {
@@ -1074,19 +1135,31 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1074
1135
  }
1075
1136
  };
1076
1137
  const observedSteerRef = {};
1138
+ let pendingTrigger = [];
1139
+ const takePendingTrigger = () => {
1140
+ if (pendingTrigger.length === 0)
1141
+ return undefined;
1142
+ const joined = pendingTrigger.join("\n\n");
1143
+ pendingTrigger = [];
1144
+ return joined;
1145
+ };
1146
+ const injectObservedRequest = async (text, origin) => {
1147
+ const steer = observedSteerRef.steer;
1148
+ if (!steer)
1149
+ throw new Error("observed child has no steer lane (report undeliverable)");
1150
+ if (origin !== "observer")
1151
+ pendingTrigger.push(text);
1152
+ await steer(text);
1153
+ };
1077
1154
  if (observerArm) {
1155
+ pendingTrigger.push(prompt);
1078
1156
  const observerDef = observerArm.observerDefinition;
1079
1157
  const envelopeName = observerSlug(childAgentName ?? def.name);
1080
- const sid = uuidv7();
1158
+ let sid = uuidv7();
1081
1159
  observerSessionId = sid;
1082
1160
  markObserverTaskId(sid);
1083
1161
  const reportOpts = {
1084
- queueReport: async (framed) => {
1085
- const steer = observedSteerRef.steer;
1086
- if (!steer)
1087
- throw new Error("observed child has no steer lane (report undeliverable)");
1088
- await steer(framed);
1089
- },
1162
+ queueReport: async (framed) => injectObservedRequest(framed, "observer"),
1090
1163
  };
1091
1164
  const reportTool = createObserverReportToolSpec(reportOpts);
1092
1165
  const observerSpec = (objectiveText, resume) => ({
@@ -1106,15 +1179,36 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1106
1179
  });
1107
1180
  const runObserverLeg = async (spec) => {
1108
1181
  if (!isObserverTaskId(sid))
1109
- throw new Error("observer identity revoked — delivery refused");
1182
+ throw new ObserverStoppedByUserError("observer identity revoked — delivery refused");
1110
1183
  const r = await opts.runner.runTask(spec);
1111
1184
  if (r.status !== "completed") {
1185
+ if (r.errorCode === "resume.session_not_found") {
1186
+ throw new ObserverResumeStateError(`observer session no longer exists${r.errorMessage ? `: ${r.errorMessage}` : ""}`);
1187
+ }
1112
1188
  throw new Error(`observer run ended ${r.status}${r.errorMessage ? `: ${r.errorMessage}` : ""}`);
1113
1189
  }
1114
1190
  };
1115
1191
  const spawner = {
1116
1192
  spawnFirstRun: ({ framingPrompt, digest }) => runObserverLeg(observerSpec(`${framingPrompt}\n\n${digest}`, false)),
1117
1193
  deliver: ({ digest }) => runObserverLeg(observerSpec(digest, true)),
1194
+ restartFresh: async ({ framingPrompt, digest }) => {
1195
+ const dead = sid;
1196
+ const fresh = uuidv7();
1197
+ markObserverTaskId(fresh);
1198
+ sid = fresh;
1199
+ observerSessionId = fresh;
1200
+ try {
1201
+ await runObserverLeg(observerSpec(`${framingPrompt}\n\n${digest}`, false));
1202
+ }
1203
+ finally {
1204
+ unmarkObserverTaskId(dead);
1205
+ try {
1206
+ await opts.runner.sessions.release(dead);
1207
+ }
1208
+ catch {
1209
+ }
1210
+ }
1211
+ },
1118
1212
  };
1119
1213
  observerPairing = new ObserverPairing({
1120
1214
  observedEnvelopeName: envelopeName,
@@ -1125,7 +1219,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1125
1219
  onError: noteObserverFailure,
1126
1220
  });
1127
1221
  reportOpts.pairing = observerPairing;
1128
- observerTap = new ObserverDigestTap((activity) => observerPairing.enqueueSegment(activity));
1222
+ observerTap = new ObserverDigestTap((activity) => observerPairing.enqueueSegment(activity, takePendingTrigger()));
1129
1223
  }
1130
1224
  const closeObserverWindow = (status) => {
1131
1225
  if (observerTap)
@@ -1152,9 +1246,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1152
1246
  t.unref?.();
1153
1247
  });
1154
1248
  const outcome = await Promise.race([observerPairing.drain().then(() => "drained"), timeout]);
1249
+ observerPairing.retire(outcome === "timeout" ? "stopped" : "retired");
1155
1250
  if (outcome === "timeout")
1156
1251
  noteObserverFailure(new Error("observer unresponsive at settle (delivery drain timed out)"));
1157
- observerPairing.retire(outcome === "timeout" ? "stopped" : "retired");
1158
1252
  if (observerSessionId !== undefined) {
1159
1253
  try {
1160
1254
  await opts.runner.sessions.release(observerSessionId);
@@ -1183,7 +1277,13 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1183
1277
  clearTimeout(timer);
1184
1278
  return outcome;
1185
1279
  };
1186
- const observerNoteFor = (outcome) => observerFailure !== undefined ? " [observer: failed]" : outcome === "still_draining" ? " [observer: still draining]" : "";
1280
+ const observerNoteFor = (outcome) => observerFailure !== undefined
1281
+ ? " [observer: failed]"
1282
+ : outcome === "still_draining"
1283
+ ? " [observer: still draining]"
1284
+ : observerDegradedCount > 0
1285
+ ? ` [observer: degraded, ${observerDegradedCount} digest batch(es) lost]`
1286
+ : "";
1187
1287
  const stepRecorder = new SubagentStepRecorder(ctx.toolCallId);
1188
1288
  const toolStatsCounter = createToolStatsCounter(opts.name ?? DEFAULT_SUBAGENT_TOOL_NAME);
1189
1289
  let assistantTextTail = "";
@@ -1440,7 +1540,8 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1440
1540
  }
1441
1541
  const forkInternals = { ...childInternals, insideFork: true };
1442
1542
  stepRecorder.lockTo(forkedId);
1443
- const forkObjective = `${FORK_DIRECTIVE_FRAME}${prompt}`;
1543
+ const forkWorktreeNote = worktreeDir !== undefined ? forkWorktreeTranslationNote(ctx.parentCwd, worktreeDir) : undefined;
1544
+ const forkObjective = `${FORK_DIRECTIVE_FRAME}${prompt}${forkWorktreeNote !== undefined ? `\n\n${forkWorktreeNote}` : ""}`;
1444
1545
  if (wantsBackground) {
1445
1546
  const bg = opts.background;
1446
1547
  const notify = ctx.onTaskNotification ?? bg.notify;
@@ -1674,25 +1775,22 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1674
1775
  }
1675
1776
  });
1676
1777
  return {
1677
- content: `Async agent launched successfully.
1678
- task_id: ${taskId}
1679
- ` +
1680
- `The forked agent inherits your full current context and works on its prompt in the background. ${notify ? "You will be notified automatically when it completes." : "Its result is NOT pushed automatically — retrieve progress and results with TaskOutput(task_id) where mounted."}
1681
- ` +
1682
- (modelNote ? `${modelNote}\n` : "") +
1683
- (extraToolsNote ? `${extraToolsNote}\n` : "") +
1684
- (worktreeDir !== undefined
1685
- ? `worktree: ${worktreeDir} (the agent works in its own detached worktree; auto-removed when it settles unchanged, kept when it made changes)
1686
- `
1687
- : "") +
1688
- (sessionScopedBg
1689
- ? `It is session-scoped: it keeps running even after this task ends.
1690
- `
1691
- : `Note: it is stopped automatically (killed) if still running when this task ends — do not promise the user results beyond this task.
1692
- `) +
1693
- (notify
1694
- ? `Briefly tell the user what you launched and end your response. Do not generate any other text — agent results will arrive in a subsequent message.`
1695
- : `Briefly tell the user what you launched. The result will NOT arrive on its own — retrieve it with TaskOutput(task_id) where mounted before relying on it.`),
1778
+ content: asyncLaunchedReceipt({
1779
+ taskId,
1780
+ workingLine: `The forked agent inherits your full current context and works on its prompt in the background.`,
1781
+ notify: Boolean(notify),
1782
+ oneShot: ctx.oneShot === true,
1783
+ notes: [
1784
+ modelNote,
1785
+ extraToolsNote,
1786
+ worktreeDir !== undefined
1787
+ ? `worktree: ${worktreeDir} (the agent works in its own detached worktree; auto-removed when it settles unchanged, kept when it made changes)`
1788
+ : undefined,
1789
+ sessionScopedBg
1790
+ ? `It is session-scoped: it keeps running even after this task ends.`
1791
+ : `Note: it is stopped automatically (killed) if still running when this task ends — do not promise the user results beyond this task.`,
1792
+ ],
1793
+ }),
1696
1794
  details: { type: "agent", subagent_type: FORK_SUBAGENT_TYPE, status: "async_launched", isAsync: true, task_id: taskId, description: shortDesc, prompt },
1697
1795
  };
1698
1796
  }
@@ -2308,29 +2406,27 @@ task_id: ${taskId}
2308
2406
  }
2309
2407
  }
2310
2408
  return {
2311
- content: `Async agent launched successfully.
2312
- task_id: ${taskId}
2313
- ` +
2314
- `The agent is working in the background. ${notify ? "You will be notified automatically when it completes." : "Its result is NOT pushed automatically — retrieve progress and results with TaskOutput(task_id) where mounted."}
2315
- ` +
2316
- (modelNote ? `${modelNote}\n` : "") +
2317
- (extraToolsNote ? `${extraToolsNote}\n` : "") +
2318
- (worktreeDir !== undefined
2319
- ? `worktree: ${worktreeDir} (the agent works in its own detached worktree; auto-removed when it settles unchanged, kept when it made changes)
2320
- `
2321
- : "") +
2322
- (sessionScopedBg
2323
- ? `It is session-scoped: it keeps running even after this task ends.
2324
- `
2325
- : `Note: it is stopped automatically (killed) if still running when this task ends — do not promise the user results beyond this task.
2326
- `) +
2327
- (notify
2328
- ? `Briefly tell the user what you launched and end your response. Do not generate any other text — agent results will arrive in a subsequent message.`
2329
- : `Briefly tell the user what you launched. The result will NOT arrive on its own — retrieve it with TaskOutput(task_id) where mounted before relying on it.`),
2409
+ content: asyncLaunchedReceipt({
2410
+ taskId,
2411
+ workingLine: `The agent is working in the background.`,
2412
+ notify: Boolean(notify),
2413
+ oneShot: ctx.oneShot === true,
2414
+ notes: [
2415
+ modelNote,
2416
+ extraToolsNote,
2417
+ worktreeDir !== undefined
2418
+ ? `worktree: ${worktreeDir} (the agent works in its own detached worktree; auto-removed when it settles unchanged, kept when it made changes)`
2419
+ : undefined,
2420
+ sessionScopedBg
2421
+ ? `It is session-scoped: it keeps running even after this task ends.`
2422
+ : `Note: it is stopped automatically (killed) if still running when this task ends — do not promise the user results beyond this task.`,
2423
+ ],
2424
+ }),
2330
2425
  details: { type: "agent", status: "async_launched", isAsync: true, task_id: taskId, description: shortDesc, prompt },
2331
2426
  };
2332
2427
  }
2333
2428
  let child;
2429
+ const syncStartedAt = Date.now();
2334
2430
  let retainEntry;
2335
2431
  if (ctx.onSubagentSpawn || observerPairing) {
2336
2432
  retainEntry = ctx.onSubagentSpawn ? await tryRetainChild(ctx.subagentRetain) : undefined;
@@ -2438,9 +2534,13 @@ task_id: ${taskId}
2438
2534
  child.blockedReason ? `blocked_reason: ${inlineUntrusted(child.blockedReason, REPORT_FIELD_MAX)}` : "",
2439
2535
  child.errorMessage ? `error: ${inlineUntrusted(child.errorMessage, REPORT_FIELD_MAX)}` : "",
2440
2536
  errClass ? `error_kind: ${errClass.errorKind} (retryable: ${errClass.retryable})` : "",
2441
- observerFailure !== undefined ? `observer: failed (${inlineUntrusted(observerFailure, REPORT_FIELD_MAX)})` : "",
2537
+ observerFailure !== undefined
2538
+ ? `observer: failed (${inlineUntrusted(observerFailure, REPORT_FIELD_MAX)})`
2539
+ : observerDegradedCount > 0
2540
+ ? `observer: degraded — ${observerDegradedCount} digest batch(es) never reached it (first: ${inlineUntrusted(observerDegradedFirst ?? "", REPORT_FIELD_MAX)})`
2541
+ : "",
2442
2542
  `stats: turns=${child.stats.turns} tokens=${child.stats.tokens}`,
2443
- retainEntry || failureRetained ? `transcript_id: ${child.sessionId}` : "",
2543
+ !retainEntry && failureRetained ? `transcript_id: ${child.sessionId} (internal ID - do not mention to user; inspect via AgentTranscript where mounted)` : "",
2444
2544
  retainEntry ? `resumable: true` : "",
2445
2545
  failureRetained
2446
2546
  ? `session_retained: this child's session is kept for a short time (~15 min) so its transcript stays inspectable via transcript_id; it is released automatically afterwards.`
@@ -2471,6 +2571,10 @@ task_id: ${taskId}
2471
2571
  : "(no text result)",
2472
2572
  "",
2473
2573
  "Verify this result before concluding. If it leaves more to do, continue or delegate again.",
2574
+ retainEntry
2575
+ ? `transcript_id: ${child.sessionId} (internal ID - do not mention to user. Transcript inspectable via AgentTranscript where mounted; this child is continued through the deployment's resume handle, not by SendMessage.)`
2576
+ : "",
2577
+ `<usage>subagent_tokens: ${child.stats.tokens}\ntool_uses: ${toolStatsCounter.total()}\nduration_ms: ${Date.now() - syncStartedAt}</usage>`,
2474
2578
  ].filter(Boolean);
2475
2579
  return {
2476
2580
  content: lines.join("\n"),
@@ -2,7 +2,20 @@ import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens, estimateTokens } from "
2
2
  import { isToolResult } from "./message-utils.js";
3
3
  const CLEARED_MARKER = "[tool result cleared to save context]";
4
4
  const refNote = (ref) => `full text persisted; call ReadToolResult with ref "${ref}" to read it back`;
5
- const mediaNote = (n) => `${n} attachment${n === 1 ? "" : "s"} no longer visible after this clear`;
5
+ const mediaNote = (blocks) => {
6
+ const byType = new Map();
7
+ for (const b of blocks) {
8
+ const t = b.type ?? "untyped";
9
+ byType.set(t, (byType.get(t) ?? 0) + 1);
10
+ }
11
+ const n = blocks.length;
12
+ if (byType.size === 1) {
13
+ const [t, c] = [...byType][0];
14
+ return `${c} ${t} attachment${c === 1 ? "" : "s"} no longer visible after this clear`;
15
+ }
16
+ const breakdown = [...byType].map(([t, c]) => `${c} ${t}`).join(", ");
17
+ return `${n} attachments (${breakdown}) no longer visible after this clear`;
18
+ };
6
19
  const clearedMarker = (notes) => {
7
20
  const present = notes.filter((n) => n !== undefined);
8
21
  return present.length > 0 ? `[tool result cleared to save context — ${present.join("; ")}]` : CLEARED_MARKER;
@@ -91,8 +104,8 @@ export function clearStaleToolResults(messages, opts) {
91
104
  }
92
105
  }
93
106
  }
94
- const mediaCount = rawContent.filter((c) => c?.type !== "text").length;
95
- const marker = clearedMarker([ref, mediaCount > 0 ? mediaNote(mediaCount) : undefined]);
107
+ const mediaBlocks = rawContent.filter((c) => c?.type !== "text");
108
+ const marker = clearedMarker([ref, mediaBlocks.length > 0 ? mediaNote(mediaBlocks) : undefined]);
96
109
  const cleared = {
97
110
  ...out[idx],
98
111
  content: [{ type: "text", text: marker }],
@@ -204,9 +204,18 @@ export class InMemoryFileSnapshotStore {
204
204
  try {
205
205
  for (const hash of new Set(manifest.values())) {
206
206
  written.add(hash);
207
+ if (hash === "" || hash === "." || hash === ".." || !/^[A-Za-z0-9_.-]+$/.test(hash)) {
208
+ return { ok: false, error: { code: "read_failed", message: `unsafe blob hash ${JSON.stringify(hash)}` } };
209
+ }
207
210
  if (this.blobs.has(hash))
208
211
  continue;
209
- const bytes = await srcGetBlob(hash);
212
+ let bytes;
213
+ try {
214
+ bytes = await srcGetBlob(hash);
215
+ }
216
+ catch (err) {
217
+ return { ok: false, error: { code: "read_failed", message: `source blob ${hash} fetch failed: ${err.message}` } };
218
+ }
210
219
  if (!bytes)
211
220
  return { ok: false, error: { code: "read_failed", message: `missing source blob ${hash}` } };
212
221
  if (createHash("sha256").update(bytes).digest("hex") !== hash) {
@@ -34,11 +34,13 @@ export function mergeInjections(project, personal) {
34
34
  const announcements = [...(project.announcements ?? []), ...(personal.announcements ?? [])];
35
35
  const announceParts = [project.announceBlock, personal.announceBlock].filter((s) => Boolean(s && s.trim()));
36
36
  const blockParts = [instruction, ...indexParts, ...announceParts];
37
+ const indexSeed = project.indexSeed ?? personal.indexSeed;
37
38
  return {
38
39
  instruction,
39
40
  ...(indexParts.length > 0 ? { index: indexParts.join("\n\n") } : {}),
40
41
  ...(announcements.length > 0 ? { announcements } : {}),
41
42
  ...(announceParts.length > 0 ? { announceBlock: announceParts.join("\n\n") } : {}),
43
+ ...(indexSeed !== undefined ? { indexSeed } : {}),
42
44
  block: blockParts.filter((s) => s && s.trim()).join("\n\n"),
43
45
  };
44
46
  }
@@ -26,6 +26,10 @@ export interface MemoryInjection {
26
26
  announcements?: MemoryAnnouncement[];
27
27
  announceBlock?: string;
28
28
  block: string;
29
+ indexSeed?: {
30
+ path: string;
31
+ content: string;
32
+ };
29
33
  }
30
34
  export declare class MemoryEngine {
31
35
  private readonly backend;
@@ -162,10 +162,14 @@ export class MemoryEngine {
162
162
  }
163
163
  inject(handle) {
164
164
  const instruction = handle.writeScope !== null ? buildMemoryInstruction(handle.writableRoot) : "";
165
- const onDisk = readSafe(join(handle.writableRoot, MEMORY_INDEX_FILENAME));
165
+ const indexPath = join(handle.writableRoot, MEMORY_INDEX_FILENAME);
166
+ const onDisk = readSafe(indexPath);
166
167
  const indexText = onDisk !== undefined && onDisk.trim() !== "" ? onDisk : handle.indexText;
167
168
  const truncated = truncateIndex(indexText);
168
169
  const index = composeMemoryBlock(truncated, handle.writeScope ?? handle.scopes[0] ?? "memory");
170
+ const indexSeed = handle.writeScope !== null && onDisk !== undefined && (onDisk.trim() === "" || truncated === onDisk)
171
+ ? { path: indexPath, content: onDisk }
172
+ : undefined;
169
173
  let announcements;
170
174
  let announceBlock;
171
175
  try {
@@ -183,6 +187,7 @@ export class MemoryEngine {
183
187
  ...(index !== undefined ? { index } : {}),
184
188
  ...(announcements !== undefined ? { announcements } : {}),
185
189
  ...(announceBlock !== undefined ? { announceBlock } : {}),
190
+ ...(indexSeed !== undefined ? { indexSeed } : {}),
186
191
  block,
187
192
  };
188
193
  }
@@ -12,5 +12,9 @@ export interface PrepareMemoryInput {
12
12
  export interface PrepareMemoryResult {
13
13
  memoryEngineSession: Prepared["memoryEngineSession"];
14
14
  memoryBlock: string | undefined;
15
+ seedFiles?: ReadonlyArray<{
16
+ path: string;
17
+ content: string;
18
+ }>;
15
19
  }
16
20
  export declare function prepareMemory(input: PrepareMemoryInput): Promise<PrepareMemoryResult>;
@@ -178,6 +178,7 @@ export async function prepareMemory(input) {
178
178
  }
179
179
  }
180
180
  let memoryBlock;
181
+ let seedFiles;
181
182
  if (memorySpec && memorySpec.enabled && !useMemoryEngine) {
182
183
  const detail = "TaskSpec.memory is enabled but RunnerDeps.memoryBackend is not configured; memory is inactive for this task (the legacy memoryStore fallback was retired in design/138 S4 and the dep was removed in design/157 B19).";
183
184
  deps.onError?.(new Error(detail), { phase: "config", sessionId });
@@ -186,6 +187,8 @@ export async function prepareMemory(input) {
186
187
  const injection = memoryEngineSession.inject();
187
188
  if (injection.block.trim())
188
189
  memoryBlock = injection.block;
190
+ if (memoryBlock !== undefined && injection.indexSeed !== undefined)
191
+ seedFiles = [injection.indexSeed];
189
192
  }
190
- return { memoryEngineSession, memoryBlock };
193
+ return { memoryEngineSession, memoryBlock, ...(seedFiles !== undefined ? { seedFiles } : {}) };
191
194
  }
@@ -4,7 +4,7 @@ import { type MaterializedMcp } from "../mcp.js";
4
4
  import type { HarvestReport, MemorySessionHandle } from "../memory-engine/types.js";
5
5
  import { StoredSession } from "../session.js";
6
6
  import type { SessionStore } from "../session.js";
7
- import { SubagentRetainLedger } from "../../agents/subagent.js";
7
+ import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
8
8
  import type { OnAsk, ToolPolicy } from "../tool-policy.js";
9
9
  import { type ActiveSkillFrame } from "./active-skill-scope.js";
10
10
  import { type CutKillRegistry } from "./cut-kill.js";
@@ -114,6 +114,7 @@ export interface Prepared {
114
114
  apply: (committedArtifactDigest?: string) => void;
115
115
  } | undefined;
116
116
  activeTools: Set<string>;
117
+ deferredToolNames?: ReadonlySet<string>;
117
118
  memoryEngineSession?: {
118
119
  engine: MemoryEngine;
119
120
  handle: MemorySessionHandle;
@@ -207,6 +208,12 @@ export interface Prepared {
207
208
  }>;
208
209
  toolsDeltaRef?: {
209
210
  pending: string[];
211
+ pendingRemoved: string[];
212
+ pendingReadded: string[];
213
+ pendingFailed: Array<{
214
+ name: string;
215
+ error?: string;
216
+ }>;
210
217
  };
211
218
  agentListing?: {
212
219
  entries: ReadonlyArray<{
@@ -311,7 +318,7 @@ export interface RunInternals {
311
318
  parentNotify?: (notification: TaskNotificationPayload, opts?: {
312
319
  priority?: import("../task-notification.js").SystemInjectionPriority;
313
320
  }) => void;
314
- parentRetainLedger?: import("../../agents/subagent.js").SubagentRetainLedger;
321
+ parentRetainLedger?: import("../../agents/retain-ledger.js").SubagentRetainLedger;
315
322
  onNotifyInjectorReady?: (inject: (notification: TaskNotificationPayload, opts?: {
316
323
  priority?: import("../task-notification.js").SystemInjectionPriority;
317
324
  }) => Promise<"queued" | "parked" | "dropped_duplicate">) => void;