@namzu/cli 15.1.2 → 16.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 (59) hide show
  1. package/README.md +22 -6
  2. package/dist/config/load.d.ts.map +1 -1
  3. package/dist/config/load.js +8 -0
  4. package/dist/config/load.js.map +1 -1
  5. package/dist/config/schema.d.ts +8 -0
  6. package/dist/config/schema.d.ts.map +1 -1
  7. package/dist/config/schema.js.map +1 -1
  8. package/dist/context/capabilities.d.ts +7 -4
  9. package/dist/context/capabilities.d.ts.map +1 -1
  10. package/dist/context/capabilities.js +7 -4
  11. package/dist/context/capabilities.js.map +1 -1
  12. package/dist/context/sandbox.d.ts +5 -0
  13. package/dist/context/sandbox.d.ts.map +1 -1
  14. package/dist/context/sandbox.js +11 -4
  15. package/dist/context/sandbox.js.map +1 -1
  16. package/dist/integrations/subagents/activity.d.ts +70 -0
  17. package/dist/integrations/subagents/activity.d.ts.map +1 -0
  18. package/dist/integrations/subagents/activity.js +332 -0
  19. package/dist/integrations/subagents/activity.js.map +1 -0
  20. package/dist/integrations/subagents/runtime.d.ts +11 -1
  21. package/dist/integrations/subagents/runtime.d.ts.map +1 -1
  22. package/dist/integrations/subagents/runtime.js +27 -2
  23. package/dist/integrations/subagents/runtime.js.map +1 -1
  24. package/dist/tui/AgentExplorer.d.ts +36 -0
  25. package/dist/tui/AgentExplorer.d.ts.map +1 -0
  26. package/dist/tui/AgentExplorer.js +174 -0
  27. package/dist/tui/AgentExplorer.js.map +1 -0
  28. package/dist/tui/App.d.ts.map +1 -1
  29. package/dist/tui/App.js +269 -106
  30. package/dist/tui/App.js.map +1 -1
  31. package/dist/tui/Composer.d.ts +7 -1
  32. package/dist/tui/Composer.d.ts.map +1 -1
  33. package/dist/tui/Composer.js +26 -5
  34. package/dist/tui/Composer.js.map +1 -1
  35. package/dist/tui/LiveActivity.d.ts +9 -3
  36. package/dist/tui/LiveActivity.d.ts.map +1 -1
  37. package/dist/tui/LiveActivity.js +27 -10
  38. package/dist/tui/LiveActivity.js.map +1 -1
  39. package/dist/tui/PermissionOverlay.d.ts +8 -9
  40. package/dist/tui/PermissionOverlay.d.ts.map +1 -1
  41. package/dist/tui/PermissionOverlay.js +6 -10
  42. package/dist/tui/PermissionOverlay.js.map +1 -1
  43. package/dist/tui/StatusBar.d.ts +1 -47
  44. package/dist/tui/StatusBar.d.ts.map +1 -1
  45. package/dist/tui/StatusBar.js +9 -79
  46. package/dist/tui/StatusBar.js.map +1 -1
  47. package/dist/tui/agent.d.ts +3 -0
  48. package/dist/tui/agent.d.ts.map +1 -1
  49. package/dist/tui/agent.js +199 -176
  50. package/dist/tui/agent.js.map +1 -1
  51. package/dist/tui/permission-review.d.ts +18 -2
  52. package/dist/tui/permission-review.d.ts.map +1 -1
  53. package/dist/tui/permission-review.js +88 -2
  54. package/dist/tui/permission-review.js.map +1 -1
  55. package/dist/tui/slashCommands.d.ts +4 -0
  56. package/dist/tui/slashCommands.d.ts.map +1 -1
  57. package/dist/tui/slashCommands.js +14 -0
  58. package/dist/tui/slashCommands.js.map +1 -1
  59. package/package.json +2 -2
package/dist/tui/agent.js CHANGED
@@ -20,9 +20,10 @@
20
20
  * `emptySession()` whose `send()` yields a single error event so the UI
21
21
  * renders an actionable hint rather than crashing.
22
22
  */
23
- import { BOOT_EVENT_NAMES, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, SearchToolsTool, ToolRegistry, asProjectId, asRunId, asSessionId, asTenantId, asTopicId, buildMemoryTools, buildSessionGoalTools, compactNow, createComputerUseTool, createMemoryPromoter, createToolPresenter, genericLabel, getBuiltinTools, isTrustedReadOnly, query, resumeRun, withProviderFallback, } from '@namzu/sdk';
23
+ import { BOOT_EVENT_NAMES, DiskMemoryStore, DiskTaskStore, EVENT_NAME_ATTRIBUTE, ProviderRegistry, SESSION_GOAL_TOOL_NAMES, SearchToolsTool, ToolRegistry, asProjectId, asRunId, asSessionId, asTenantId, asTopicId, buildMemoryTools, buildSessionGoalTools, compactNow, createComputerUseTool, createMemoryPromoter, createToolPresenter, generateRunId, getBuiltinTools, isTrustedReadOnly, query, resumeRun, withProviderFallback, } from '@namzu/sdk';
24
24
  import { SubprocessComputerUseHost } from '@namzu/computer-use';
25
- import { join } from 'node:path';
25
+ import { realpath, stat } from 'node:fs/promises';
26
+ import { join, parse, resolve } from 'node:path';
26
27
  import { probeCapabilities } from '../context/capabilities.js';
27
28
  import { composeEnvironmentPrompt, readEnvironmentFacts } from '../context/environment.js';
28
29
  import { ProjectInstructionTracker } from '../context/project-tracker.js';
@@ -350,7 +351,24 @@ async function currentPluginSkills(registry) {
350
351
  }
351
352
  export async function createAgentSession(prefs, detected, options = {}) {
352
353
  const scope = options.scope ?? mintScope();
353
- const cwd = options.cwd ?? process.cwd();
354
+ const requestedCwd = resolve(options.cwd ?? process.cwd());
355
+ let cwd;
356
+ try {
357
+ const entry = await stat(requestedCwd);
358
+ if (!entry.isDirectory()) {
359
+ return emptySession(`Working directory is not a directory: ${requestedCwd}`, 'invocation');
360
+ }
361
+ cwd = await realpath(requestedCwd);
362
+ }
363
+ catch (error) {
364
+ return emptySession(`Working directory is unavailable: ${requestedCwd} — ${describeError(error)}`, 'invocation');
365
+ }
366
+ const sandboxWorkspace = options.sandbox?.workspace ?? 'working-directory';
367
+ if (options.sandbox?.enabled !== false &&
368
+ sandboxWorkspace === 'working-directory' &&
369
+ cwd === parse(cwd).root) {
370
+ return emptySession(`Working-directory sandboxing refuses filesystem root ${cwd}; choose a project directory so confinement has a boundary, or explicitly select an ephemeral workspace.`, 'invocation');
371
+ }
354
372
  // The head serves; the tail is fallen over to, in order, when it cannot.
355
373
  const primary = primaryProvider(prefs);
356
374
  const entry = PROVIDER_REGISTRY[primary.id];
@@ -662,6 +680,10 @@ export async function createAgentSession(prefs, detected, options = {}) {
662
680
  // `buildToolRegistry` directly below, so they never receive these tools.
663
681
  // Per-send denial further keeps the schemas out of ordinary human turns.
664
682
  const goalAuthorities = new Map();
683
+ // A child is created after the parent query has started, from inside its
684
+ // Agent tool. Keying the review channel by the executing run keeps two
685
+ // concurrent sends from borrowing each other's prompt or approval latch.
686
+ const delegatedResumeHandlers = new Map();
665
687
  const goalToolNames = new Set(SESSION_GOAL_TOOL_NAMES);
666
688
  if (options.sessionGoals) {
667
689
  registry.register(buildSessionGoalTools(options.sessionGoals, (runId) => goalAuthorities.get(runId)));
@@ -714,11 +736,6 @@ export async function createAgentSession(prefs, detected, options = {}) {
714
736
  // Best-effort — if the runtime can't stand up, the chat still works.
715
737
  let subagentGateway;
716
738
  let subagentRuntime;
717
- // Buffer of the in-flight sub-agent's tool steps. The gateway streams the
718
- // child's events here while the parent's `Agent` tool call blocks; runTurn
719
- // drains it onto the `Agent` result as a `├─/└─` tree. Scoped per `Agent`
720
- // call (cleared when one starts).
721
- const childSteps = [];
722
739
  // Stays empty when the runtime below throws, which is the honest answer: the
723
740
  // catch is non-fatal and the session then genuinely has no delegate to
724
741
  // dispatch to. A roster reported from the request rather than the result
@@ -728,6 +745,8 @@ export async function createAgentSession(prefs, detected, options = {}) {
728
745
  const sub = await createSubagentRuntime({
729
746
  cwd,
730
747
  model,
748
+ sandboxWorkspace,
749
+ resolveResumeHandler: (runId) => delegatedResumeHandlers.get(runId),
731
750
  ...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
732
751
  ...(options.sandbox?.teardownTimeoutMs !== undefined
733
752
  ? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
@@ -769,11 +788,6 @@ export async function createAgentSession(prefs, detected, options = {}) {
769
788
  return buildToolRegistry(cwd).registry;
770
789
  },
771
790
  authorizationGate: gateFor(options.rules),
772
- onEvent: (e) => {
773
- if (e.type === 'tool_executing') {
774
- childSteps.push(`${e.toolName}(${genericLabel(e.input)})`);
775
- }
776
- },
777
791
  });
778
792
  subagentRuntime = sub;
779
793
  registry.register([sub.agentTool]);
@@ -905,6 +919,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
905
919
  ...(sandbox.environment ? { environment: sandbox.environment } : {}),
906
920
  enforced: sandbox.enforced,
907
921
  required: sandbox.required,
922
+ workspace: sandbox.workspace,
908
923
  },
909
924
  providerSummary: entry.label,
910
925
  modelSummary: model,
@@ -928,6 +943,7 @@ export async function createAgentSession(prefs, detected, options = {}) {
928
943
  .map((t) => t.name)
929
944
  .filter((name) => !goalToolNames.has(name)),
930
945
  agentIds: allowedAgentIds,
946
+ ...(subagentRuntime ? { subagents: subagentRuntime.activity } : {}),
931
947
  get instructionFiles() {
932
948
  return projectInstructions.instructionFiles;
933
949
  },
@@ -960,172 +976,198 @@ export async function createAgentSession(prefs, detected, options = {}) {
960
976
  },
961
977
  promptExemptTools: () => promptExemptToolNames(registry).filter((name) => !goalToolNames.has(name)),
962
978
  send: (messages, opts) => operations.stream(opts?.signal, (signal) => (async function* () {
963
- // Renew a lapsed OAuth token before the turn runs (no-op for valid
964
- // tokens and non-subscription credentials).
965
- await prepareProviderCredential(signal);
966
- // namzu identity first (so it establishes who the agent is even when
967
- // the credential layer prepends whatever prefix its token requires),
968
- // then memory and per-turn extra context. Project instructions are a
969
- // separate retained user-context snapshot prepared by the controller;
970
- // that is what lets nested scopes be replaced and persisted safely.
971
- //
972
- // The environment block is read fresh every turn because both facts in it
973
- // can change WHILE the session
974
- // runs — midnight passes, and the agent checks out a branch itself.
975
- // Its text only changes when a fact changes, so it costs a prompt-cache
976
- // miss exactly when a hit would have been a stale claim.
979
+ const runId = opts?.runId ?? generateRunId();
980
+ const turnOpts = { ...opts, runId, signal };
981
+ const resumeHandler = makeResumeHandler(approval, opts?.onPermission, opts?.permissionMode ?? options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
982
+ if (delegatedResumeHandlers.has(runId)) {
983
+ throw new Error(`Run ${runId} already owns a delegated review channel.`);
984
+ }
985
+ delegatedResumeHandlers.set(runId, resumeHandler);
986
+ try {
987
+ // Renew a lapsed OAuth token before the turn runs (no-op for valid
988
+ // tokens and non-subscription credentials).
989
+ await prepareProviderCredential(signal);
990
+ // namzu identity first (so it establishes who the agent is even when
991
+ // the credential layer prepends whatever prefix its token requires),
992
+ // then memory and per-turn extra context. Project instructions are a
993
+ // separate retained user-context snapshot prepared by the controller;
994
+ // that is what lets nested scopes be replaced and persisted safely.
995
+ //
996
+ // The environment block is read fresh every turn because both facts in it
997
+ // can change WHILE the session
998
+ // runs — midnight passes, and the agent checks out a branch itself.
999
+ // Its text only changes when a fact changes, so it costs a prompt-cache
1000
+ // miss exactly when a hit would have been a stale claim.
1001
+ const pluginSkills = pluginRuntime
1002
+ ? await currentPluginSkills(pluginRuntime.skills)
1003
+ : undefined;
1004
+ const memoryPrompt = composeMemoryPrompt(readMemory());
1005
+ const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
1006
+ const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt, opts?.extraSystem]
1007
+ .filter((s) => Boolean(s))
1008
+ .join('\n\n') || undefined;
1009
+ let capturedAuthority;
1010
+ if (opts?.goalRound) {
1011
+ if (!opts.runId)
1012
+ throw new Error('A goal round requires a caller-reserved runId.');
1013
+ if (!options.sessionGoals)
1014
+ throw new Error('This session has no durable goal store.');
1015
+ if (opts.goalRound.sessionId !== scope.sessionId ||
1016
+ opts.goalRound.tenantId !== scope.tenantId) {
1017
+ throw new Error('Goal-round authority does not belong to this agent session scope.');
1018
+ }
1019
+ const current = await options.sessionGoals.getGoal(scope.sessionId, scope.tenantId);
1020
+ if (!current ||
1021
+ current.phase !== 'active' ||
1022
+ current.id !== opts.goalRound.id ||
1023
+ current.revision !== opts.goalRound.revision ||
1024
+ current.objective !== opts.goalRound.objective ||
1025
+ current.roundsAdmitted !== opts.goalRound.round ||
1026
+ current.maxGoalRounds !== opts.goalRound.maxGoalRounds) {
1027
+ throw new Error('Goal-round authority is stale or does not match the durable goal.');
1028
+ }
1029
+ if (goalAuthorities.has(opts.runId)) {
1030
+ throw new Error(`Run ${opts.runId} already owns goal-round authority.`);
1031
+ }
1032
+ capturedAuthority = Object.freeze({ ...opts.goalRound });
1033
+ goalAuthorities.set(opts.runId, capturedAuthority);
1034
+ }
1035
+ try {
1036
+ yield* runTurn({
1037
+ provider,
1038
+ // Constructed HERE, per turn, and that is not an optimisation to
1039
+ // undo. `refreshTokenIfNeeded` above replaces the head's client
1040
+ // object when an OAuth token rotates, so a member list built once at
1041
+ // session creation would hand the kernel a client holding a token
1042
+ // that expired hours ago — and a chain whose own members are stale
1043
+ // is a fallback that fails for the reason the fallback exists to
1044
+ // survive. Building a driver is a client object, not a request.
1045
+ fallbackProviders: fallbackPlan.build(currentToken),
1046
+ model,
1047
+ tools: registry,
1048
+ pluginManager: pluginRuntime?.manager,
1049
+ skillRegistry: pluginRuntime?.skills,
1050
+ skills: pluginSkills,
1051
+ scope,
1052
+ workingDirectory: cwd,
1053
+ sandboxWorkspace,
1054
+ rules: options.rules,
1055
+ reviewAnswer: options.reviewAnswer,
1056
+ maxAnswerReviews: options.maxAnswerReviews,
1057
+ promoteMemory,
1058
+ taskStore,
1059
+ systemPrompt,
1060
+ messages,
1061
+ projectInstructionContext: projectInstructions.createRunContext(),
1062
+ opts: turnOpts,
1063
+ resumeHandler,
1064
+ taskGateway: subagentGateway,
1065
+ onRunEvent: options.onRunEvent,
1066
+ ...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
1067
+ ...(options.sandbox?.teardownTimeoutMs !== undefined
1068
+ ? {
1069
+ sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs,
1070
+ }
1071
+ : {}),
1072
+ });
1073
+ }
1074
+ finally {
1075
+ if (opts?.runId &&
1076
+ capturedAuthority &&
1077
+ goalAuthorities.get(opts.runId) === capturedAuthority) {
1078
+ goalAuthorities.delete(opts.runId);
1079
+ }
1080
+ }
1081
+ }
1082
+ finally {
1083
+ if (delegatedResumeHandlers.get(runId) === resumeHandler) {
1084
+ delegatedResumeHandlers.delete(runId);
1085
+ }
1086
+ }
1087
+ })()),
1088
+ resumeDurable: ({ entry, checkpointStore, claimFence, signal }) => operations.promise(signal, async (ownedSignal) => {
1089
+ // The same prelude a turn runs, and for the same reasons: a lapsed
1090
+ // OAuth token has to be renewed before the provider is used, and the
1091
+ // fallback chain has to be built AFTER that so its members do not
1092
+ // hold a client the refresh just replaced.
1093
+ await prepareProviderCredential(ownedSignal);
977
1094
  const pluginSkills = pluginRuntime
978
1095
  ? await currentPluginSkills(pluginRuntime.skills)
979
1096
  : undefined;
980
1097
  const memoryPrompt = composeMemoryPrompt(readMemory());
981
1098
  const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
982
- const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt, opts?.extraSystem]
1099
+ const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt]
983
1100
  .filter((s) => Boolean(s))
984
1101
  .join('\n\n') || undefined;
985
- let capturedAuthority;
986
- if (opts?.goalRound) {
987
- if (!opts.runId)
988
- throw new Error('A goal round requires a caller-reserved runId.');
989
- if (!options.sessionGoals)
990
- throw new Error('This session has no durable goal store.');
991
- if (opts.goalRound.sessionId !== scope.sessionId ||
992
- opts.goalRound.tenantId !== scope.tenantId) {
993
- throw new Error('Goal-round authority does not belong to this agent session scope.');
994
- }
995
- const current = await options.sessionGoals.getGoal(scope.sessionId, scope.tenantId);
996
- if (!current ||
997
- current.phase !== 'active' ||
998
- current.id !== opts.goalRound.id ||
999
- current.revision !== opts.goalRound.revision ||
1000
- current.objective !== opts.goalRound.objective ||
1001
- current.roundsAdmitted !== opts.goalRound.round ||
1002
- current.maxGoalRounds !== opts.goalRound.maxGoalRounds) {
1003
- throw new Error('Goal-round authority is stale or does not match the durable goal.');
1004
- }
1005
- if (goalAuthorities.has(opts.runId)) {
1006
- throw new Error(`Run ${opts.runId} already owns goal-round authority.`);
1007
- }
1008
- capturedAuthority = Object.freeze({ ...opts.goalRound });
1009
- goalAuthorities.set(opts.runId, capturedAuthority);
1102
+ const resumeHandler = makeResumeHandler(approval, undefined, options.permissionMode, (name, input) => isPromptExempt(registry, name, input));
1103
+ if (delegatedResumeHandlers.has(entry.runId)) {
1104
+ throw new Error(`Run ${entry.runId} already owns a delegated review channel.`);
1010
1105
  }
1106
+ delegatedResumeHandlers.set(entry.runId, resumeHandler);
1011
1107
  try {
1012
- yield* runTurn({
1108
+ return await resumeRun({
1013
1109
  provider,
1014
- // Constructed HERE, per turn, and that is not an optimisation to
1015
- // undo. `refreshTokenIfNeeded` above replaces the head's client
1016
- // object when an OAuth token rotates, so a member list built once at
1017
- // session creation would hand the kernel a client holding a token
1018
- // that expired hours ago — and a chain whose own members are stale
1019
- // is a fallback that fails for the reason the fallback exists to
1020
- // survive. Building a driver is a client object, not a request.
1021
1110
  fallbackProviders: fallbackPlan.build(currentToken),
1022
- model,
1023
1111
  tools: registry,
1024
1112
  pluginManager: pluginRuntime?.manager,
1025
1113
  skillRegistry: pluginRuntime?.skills,
1026
1114
  skills: pluginSkills,
1027
- scope,
1028
- workingDirectory: cwd,
1029
- rules: options.rules,
1030
- permissionMode: opts?.permissionMode ?? options.permissionMode,
1031
- reviewAnswer: options.reviewAnswer,
1032
- maxAnswerReviews: options.maxAnswerReviews,
1033
- promoteMemory,
1034
- approval,
1035
1115
  taskStore,
1036
- systemPrompt,
1037
- messages,
1116
+ ...(subagentGateway ? { taskGateway: subagentGateway } : {}),
1117
+ authorizationGate: gateFor(options.rules),
1118
+ compactionConfig: COMPACTION_CONFIG,
1038
1119
  projectInstructionContext: projectInstructions.createRunContext(),
1039
- opts: { ...opts, signal },
1040
- taskGateway: subagentGateway,
1041
- onRunEvent: options.onRunEvent,
1042
- childSteps,
1043
1120
  ...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
1044
1121
  ...(options.sandbox?.teardownTimeoutMs !== undefined
1045
- ? {
1046
- sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs,
1047
- }
1122
+ ? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
1048
1123
  : {}),
1124
+ // NOT `emergencySave`, unlike a turn. The manager is a singleton
1125
+ // whose `attach` detaches whoever held it before, so a caller
1126
+ // resuming several runs in one process would leave only the last
1127
+ // one covered — and would look covered. A turn owns its process
1128
+ // end to end; a drainer does not.
1129
+ runConfig: {
1130
+ model,
1131
+ ...(sandbox.provider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
1132
+ timeoutMs: 600_000,
1133
+ tokenBudget: 1_000_000,
1134
+ maxIterations: 50,
1135
+ maxResponseTokens: 8192,
1136
+ permissionMode: 'auto',
1137
+ },
1138
+ agentId: 'namzu',
1139
+ agentName: 'namzu',
1140
+ ...(systemPrompt ? { systemPrompt } : {}),
1141
+ workingDirectory: cwd,
1142
+ // No `onPermission`: there is nobody at a drainer's terminal, so a
1143
+ // prompt would block the pass forever on a run nobody is watching.
1144
+ // The gate's deny rules still apply.
1145
+ // One presenter for the whole stream, built from the registry this
1146
+ // scope already holds. It was the absence of the registry HERE that
1147
+ // forced presentation to be name matching: `toAgentEvent` was pure
1148
+ // over a `RunEvent` and could not ask a tool anything.
1149
+ resumeHandler,
1150
+ signal: ownedSignal,
1151
+ // Attribution comes from the ENTRY, not from this session: the run
1152
+ // belongs to whoever started it, and stamping the drainer's ids onto
1153
+ // it would file another tenant's work under this one.
1154
+ tenantId: entry.tenantId,
1155
+ projectId: entry.projectId,
1156
+ sessionId: entry.sessionId,
1157
+ // …except the topic, which no checkpoint records — see
1158
+ // `RunStateScope`. This one is the drainer's, and honestly so:
1159
+ // supplied here rather than pretended to have been recovered.
1160
+ topicId: scope.topicId,
1161
+ scope: { ...entry, topicId: scope.topicId },
1162
+ checkpointStore,
1163
+ ...(claimFence !== undefined ? { claimFence } : {}),
1049
1164
  });
1050
1165
  }
1051
1166
  finally {
1052
- if (opts?.runId &&
1053
- capturedAuthority &&
1054
- goalAuthorities.get(opts.runId) === capturedAuthority) {
1055
- goalAuthorities.delete(opts.runId);
1167
+ if (delegatedResumeHandlers.get(entry.runId) === resumeHandler) {
1168
+ delegatedResumeHandlers.delete(entry.runId);
1056
1169
  }
1057
1170
  }
1058
- })()),
1059
- resumeDurable: ({ entry, checkpointStore, claimFence, signal }) => operations.promise(signal, async (ownedSignal) => {
1060
- // The same prelude a turn runs, and for the same reasons: a lapsed
1061
- // OAuth token has to be renewed before the provider is used, and the
1062
- // fallback chain has to be built AFTER that so its members do not
1063
- // hold a client the refresh just replaced.
1064
- await prepareProviderCredential(ownedSignal);
1065
- const pluginSkills = pluginRuntime
1066
- ? await currentPluginSkills(pluginRuntime.skills)
1067
- : undefined;
1068
- const memoryPrompt = composeMemoryPrompt(readMemory());
1069
- const environmentPrompt = composeEnvironmentPrompt(await readEnvironmentFacts(cwd));
1070
- const systemPrompt = [NAMZU_IDENTITY, environmentPrompt, memoryPrompt]
1071
- .filter((s) => Boolean(s))
1072
- .join('\n\n') || undefined;
1073
- return resumeRun({
1074
- provider,
1075
- fallbackProviders: fallbackPlan.build(currentToken),
1076
- tools: registry,
1077
- pluginManager: pluginRuntime?.manager,
1078
- skillRegistry: pluginRuntime?.skills,
1079
- skills: pluginSkills,
1080
- taskStore,
1081
- ...(subagentGateway ? { taskGateway: subagentGateway } : {}),
1082
- authorizationGate: gateFor(options.rules),
1083
- compactionConfig: COMPACTION_CONFIG,
1084
- projectInstructionContext: projectInstructions.createRunContext(),
1085
- ...(sandbox.provider ? { sandboxProvider: sandbox.provider } : {}),
1086
- ...(options.sandbox?.teardownTimeoutMs !== undefined
1087
- ? { sandboxTeardownTimeoutMs: options.sandbox.teardownTimeoutMs }
1088
- : {}),
1089
- // NOT `emergencySave`, unlike a turn. The manager is a singleton
1090
- // whose `attach` detaches whoever held it before, so a caller
1091
- // resuming several runs in one process would leave only the last
1092
- // one covered — and would look covered. A turn owns its process
1093
- // end to end; a drainer does not.
1094
- runConfig: {
1095
- model,
1096
- timeoutMs: 600_000,
1097
- tokenBudget: 1_000_000,
1098
- maxIterations: 50,
1099
- maxResponseTokens: 8192,
1100
- permissionMode: 'auto',
1101
- },
1102
- agentId: 'namzu',
1103
- agentName: 'namzu',
1104
- ...(systemPrompt ? { systemPrompt } : {}),
1105
- workingDirectory: cwd,
1106
- // No `onPermission`: there is nobody at a drainer's terminal, so a
1107
- // prompt would block the pass forever on a run nobody is watching.
1108
- // The gate's deny rules still apply.
1109
- // One presenter for the whole stream, built from the registry this
1110
- // scope already holds. It was the absence of the registry HERE that
1111
- // forced presentation to be name matching: `toAgentEvent` was pure
1112
- // over a `RunEvent` and could not ask a tool anything.
1113
- resumeHandler: makeResumeHandler(approval, undefined, options.permissionMode, (n, i) => isPromptExempt(registry, n, i)),
1114
- signal: ownedSignal,
1115
- // Attribution comes from the ENTRY, not from this session: the run
1116
- // belongs to whoever started it, and stamping the drainer's ids onto
1117
- // it would file another tenant's work under this one.
1118
- tenantId: entry.tenantId,
1119
- projectId: entry.projectId,
1120
- sessionId: entry.sessionId,
1121
- // …except the topic, which no checkpoint records — see
1122
- // `RunStateScope`. This one is the drainer's, and honestly so:
1123
- // supplied here rather than pretended to have been recovered.
1124
- topicId: scope.topicId,
1125
- scope: { ...entry, topicId: scope.topicId },
1126
- checkpointStore,
1127
- ...(claimFence !== undefined ? { claimFence } : {}),
1128
- });
1129
1171
  }),
1130
1172
  };
1131
1173
  }
@@ -1485,7 +1527,7 @@ const COMPACTION_CONFIG = {
1485
1527
  maxCharsPerRequirement: 300,
1486
1528
  maxCharsPerTask: 400,
1487
1529
  };
1488
- async function* runTurn({ provider, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, workingDirectory, rules, permissionMode, reviewAnswer, maxAnswerReviews, promoteMemory, approval, taskStore, systemPrompt, messages, projectInstructionContext, opts, taskGateway, childSteps, sandboxProvider, sandboxTeardownTimeoutMs, onRunEvent, }) {
1530
+ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManager, skillRegistry, skills, scope, workingDirectory, sandboxWorkspace, rules, reviewAnswer, maxAnswerReviews, promoteMemory, resumeHandler, taskStore, systemPrompt, messages, projectInstructionContext, opts, taskGateway, sandboxProvider, sandboxTeardownTimeoutMs, onRunEvent, }) {
1489
1531
  const signal = opts?.signal;
1490
1532
  // One presenter for the whole stream, built from the registry this scope
1491
1533
  // already holds. Its absence HERE is what forced presentation to be name
@@ -1523,6 +1565,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
1523
1565
  emergencySave: true,
1524
1566
  runConfig: {
1525
1567
  model,
1568
+ ...(sandboxProvider ? { sandbox: { workspace: sandboxWorkspace } } : {}),
1526
1569
  ...(opts?.effort !== undefined ? { effort: opts.effort } : {}),
1527
1570
  timeoutMs: 600_000,
1528
1571
  tokenBudget: 1_000_000,
@@ -1550,7 +1593,7 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
1550
1593
  // The exemption reads `tools` at decision time, so it sees the task
1551
1594
  // tools `query()` registers deferred below and any tool server that
1552
1595
  // connected after this session was built.
1553
- resumeHandler: makeResumeHandler(approval, opts?.onPermission, permissionMode, (name, input) => isPromptExempt(tools, name, input)),
1596
+ resumeHandler,
1554
1597
  signal,
1555
1598
  ...scope,
1556
1599
  });
@@ -1581,22 +1624,6 @@ async function* runTurn({ provider, fallbackProviders, model, tools, pluginManag
1581
1624
  const mapped = toAgentEvent(event, presenter);
1582
1625
  if (!mapped)
1583
1626
  continue;
1584
- // On an `Agent` delegation finishing, attach the sub-agent's tool
1585
- // steps (collected via the gateway while the call blocked) as a
1586
- // `├─/└─` tree under its result, then reset for the next delegation.
1587
- // (Don't clear on tool_executing: the parent's events can be buffered
1588
- // and pulled only after the child already ran, which would wipe it.)
1589
- if (event.type === 'tool_completed' &&
1590
- event.toolName === 'Agent' &&
1591
- childSteps.length > 0 &&
1592
- mapped.kind === 'tool-end') {
1593
- yield {
1594
- ...mapped,
1595
- detail: [...(mapped.detail ?? []), ...asTree(childSteps)],
1596
- };
1597
- childSteps.length = 0;
1598
- continue;
1599
- }
1600
1627
  yield mapped;
1601
1628
  }
1602
1629
  }
@@ -2014,10 +2041,6 @@ function describeFallback(event) {
2014
2041
  `${status} ${why} (${event.code}). ` +
2015
2042
  `${name(event.toIndex, event.toProviderId, event.toModel)} — is serving the rest of this turn.`);
2016
2043
  }
2017
- /** Render a sub-agent's tool steps as a `├─/└─` tree for the Agent result. */
2018
- function asTree(steps) {
2019
- return steps.map((s, i) => `${i === steps.length - 1 ? '└─' : '├─'} ${s}`);
2020
- }
2021
2044
  /**
2022
2045
  * The one presentation function this host keeps.
2023
2046
  *