@gajae-code/coding-agent 0.17.4 → 0.17.6

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 (86) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/types/config/autorouting-tier-map.d.ts +1031 -3
  3. package/dist/types/config/file-lock.d.ts +4 -2
  4. package/dist/types/config/model-registry.d.ts +3 -1
  5. package/dist/types/config/model-resolver.d.ts +1 -1
  6. package/dist/types/exec/bash-executor.d.ts +2 -0
  7. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
  8. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  9. package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
  10. package/dist/types/harness-control-plane/finalize.d.ts +14 -3
  11. package/dist/types/main.d.ts +1 -1
  12. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  13. package/dist/types/runtime-mcp/manager.d.ts +13 -6
  14. package/dist/types/sdk/broker/broker.d.ts +4 -2
  15. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  16. package/dist/types/sdk/broker/identity.d.ts +2 -0
  17. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +11 -3
  18. package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
  19. package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
  20. package/dist/types/sdk/broker/spawn-authority.d.ts +8 -0
  21. package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
  22. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +5 -1
  23. package/dist/types/sdk/prompt-deadline-flush.d.ts +2 -0
  24. package/dist/types/sdk/session.d.ts +3 -3
  25. package/dist/types/session/agent-session.d.ts +7 -2
  26. package/dist/types/task/types.d.ts +2 -0
  27. package/dist/types/tools/bash-interactive.d.ts +1 -0
  28. package/dist/types/tools/fetch.d.ts +10 -1
  29. package/dist/types/tools/subagent.d.ts +8 -1
  30. package/package.json +7 -7
  31. package/src/cli/public-command-registry.ts +8 -2
  32. package/src/cli/setup-cli.ts +1 -0
  33. package/src/cli/stats-cli.ts +11 -1
  34. package/src/config/autorouting-tier-map.ts +529 -3
  35. package/src/config/file-lock.ts +59 -14
  36. package/src/config/model-discovery-manager.ts +4 -4
  37. package/src/config/model-profile-activation.ts +83 -23
  38. package/src/config/model-registry.ts +72 -12
  39. package/src/config/model-resolver.ts +6 -1
  40. package/src/coordinator-mcp/question-state.ts +4 -1
  41. package/src/coordinator-mcp/server.ts +14 -6
  42. package/src/exec/bash-executor.ts +3 -0
  43. package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
  44. package/src/gjc-runtime/state-writer.ts +317 -6
  45. package/src/harness-control-plane/control-endpoint.ts +24 -6
  46. package/src/harness-control-plane/finalize.ts +312 -25
  47. package/src/harness-control-plane/owner.ts +323 -89
  48. package/src/internal-urls/docs-index.generated.ts +6 -4
  49. package/src/main.ts +69 -19
  50. package/src/modes/acp/acp-agent.ts +581 -37
  51. package/src/modes/components/gjc-bundle-settings.ts +21 -1
  52. package/src/modes/components/tool-status-header.ts +72 -6
  53. package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
  54. package/src/runtime-mcp/loader.ts +6 -3
  55. package/src/runtime-mcp/manager.ts +177 -75
  56. package/src/sdk/broker/broker.ts +1058 -150
  57. package/src/sdk/broker/ensure.ts +65 -8
  58. package/src/sdk/broker/identity.ts +13 -0
  59. package/src/sdk/broker/lifecycle-ledger.ts +16 -4
  60. package/src/sdk/broker/managed-task-dag.ts +1927 -0
  61. package/src/sdk/broker/managed-task-verification.ts +642 -0
  62. package/src/sdk/broker/spawn-authority.ts +140 -7
  63. package/src/sdk/broker/startup-failure.ts +80 -1
  64. package/src/sdk/broker/transport.ts +1 -0
  65. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  66. package/src/sdk/bus/index.ts +22 -6
  67. package/src/sdk/bus/telegram-daemon-contract.ts +5 -1
  68. package/src/sdk/cli/session-cli.ts +18 -0
  69. package/src/sdk/host/session-runtime.test.ts +46 -0
  70. package/src/sdk/host/session-runtime.ts +13 -6
  71. package/src/sdk/lifecycle/service.ts +2 -0
  72. package/src/sdk/prompt-deadline-flush.ts +26 -4
  73. package/src/sdk/session.ts +587 -115
  74. package/src/session/agent-session.ts +60 -14
  75. package/src/session/internal/managed-session-scope.ts +10 -0
  76. package/src/session/internal/managed-session-storage.ts +30 -1
  77. package/src/setup/provider-presets.json +2 -1
  78. package/src/slash-commands/builtin-registry.ts +1 -1
  79. package/src/task/executor.ts +46 -17
  80. package/src/task/types.ts +2 -0
  81. package/src/tools/bash-interactive.ts +2 -0
  82. package/src/tools/bash.ts +20 -0
  83. package/src/tools/fetch.ts +21 -5
  84. package/src/tools/subagent-render.ts +24 -1
  85. package/src/tools/subagent.ts +27 -1
  86. package/src/utils/mupdf.ts +11 -4
@@ -5941,8 +5941,13 @@ export class AgentSession {
5941
5941
  if (next && MCPManager.instance() === undefined) MCPManager.setInstance(next);
5942
5942
  }
5943
5943
 
5944
- /** Swap named custom/project tools after a cwd rescope. */
5945
- async replaceNamedCustomTools(previousNames: readonly string[], nextTools: CustomTool[]): Promise<void> {
5944
+ /** Swap named custom tools and their optional mandatory MCP selection. */
5945
+ async replaceNamedCustomTools(
5946
+ previousNames: readonly string[],
5947
+ nextTools: readonly CustomTool[],
5948
+ options?: { mandatoryMCPToolNames?: readonly string[]; activateNewTools?: boolean },
5949
+ ): Promise<void> {
5950
+ const previousSelectedMCPToolNames = this.getSelectedMCPToolNames();
5946
5951
  const previous = new Set(previousNames);
5947
5952
  const previousActive = this.getActiveToolNames();
5948
5953
  for (const name of previous) this.#toolRegistry.delete(name);
@@ -5956,11 +5961,21 @@ export class AgentSession {
5956
5961
  this.#toolRegistry.set(finalTool.name, finalTool);
5957
5962
  added.push(finalTool.name);
5958
5963
  }
5959
- this.#invalidateDiscoveryCaches();
5960
- await this.#applyActiveToolsByName([
5961
- ...previousActive.filter(name => !previous.has(name)),
5962
- ...added.filter(name => !previous.has(name) || previousActive.includes(name)),
5963
- ]);
5964
+ if (options?.mandatoryMCPToolNames) {
5965
+ this.#mandatoryMCPToolNames = new Set(
5966
+ options.mandatoryMCPToolNames.map(name => name.toLowerCase()).filter(name => this.#toolRegistry.has(name)),
5967
+ );
5968
+ }
5969
+ this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
5970
+ await this.#applyActiveToolsByName(
5971
+ [
5972
+ ...previousActive.filter(name => !previous.has(name)),
5973
+ ...added.filter(
5974
+ name => previousActive.includes(name) || (options?.activateNewTools !== false && !previous.has(name)),
5975
+ ),
5976
+ ],
5977
+ { previousSelectedMCPToolNames },
5978
+ );
5964
5979
  }
5965
5980
 
5966
5981
  /** Best-effort accessor for the active skill's `current_phase` field from
@@ -11884,6 +11899,7 @@ export class AgentSession {
11884
11899
  options: {
11885
11900
  selectedMCPToolNames?: string[];
11886
11901
  activeMCPToolNames?: string[];
11902
+ mandatoryMCPToolNames?: string[];
11887
11903
  persistMCPSelection?: boolean;
11888
11904
  } = {},
11889
11905
  ): Promise<void> {
@@ -11918,6 +11934,11 @@ export class AgentSession {
11918
11934
  }
11919
11935
 
11920
11936
  this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
11937
+ if (options.mandatoryMCPToolNames !== undefined) {
11938
+ this.#mandatoryMCPToolNames = new Set(
11939
+ options.mandatoryMCPToolNames.map(name => name.toLowerCase()).filter(name => this.#toolRegistry.has(name)),
11940
+ );
11941
+ }
11921
11942
  this.#pruneSelectedMCPToolNames();
11922
11943
  const hasPersistedMCPToolSelection = this.buildDisplaySessionContext().hasPersistedMCPToolSelection;
11923
11944
  if (options.selectedMCPToolNames) {
@@ -16752,6 +16773,16 @@ export class AgentSession {
16752
16773
  ...(registeredScope ? { terminalScope: registeredScope } : {}),
16753
16774
  };
16754
16775
  }
16776
+ const retainedProof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: 0 });
16777
+ if (
16778
+ retainedProof.status === "settled" ||
16779
+ (retainedProof.status === "unfenced" && retainedProof.reason !== "unknown_run")
16780
+ ) {
16781
+ return {
16782
+ ...retainedProof,
16783
+ ...(registeredScope ? { terminalScope: registeredScope } : {}),
16784
+ };
16785
+ }
16755
16786
  return {
16756
16787
  status: "unfenced",
16757
16788
  reason: "unknown_run",
@@ -16761,9 +16792,17 @@ export class AgentSession {
16761
16792
  }
16762
16793
  if (handle === this.agent.activeResourceRunId) this.agent.abort();
16763
16794
  const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
16764
- if (proof.status === "unfenced") this.agent.resourceLedger.quarantine(handle);
16795
+ if (proof.status === "unfenced" && proof.reason !== "resources_pending") {
16796
+ this.agent.resourceLedger.quarantine(handle);
16797
+ }
16765
16798
  // The run's agent_end (if any) consumed the disposition; a settled or
16766
- // already-ended run must not leave it for an unrelated later exit.
16799
+ // already-ended run must not leave it for an unrelated later exit. A
16800
+ // `resources_pending` proof is different: the run is already sealed, and
16801
+ // the ledger still owns exact promises for the outstanding resources.
16802
+ // Keep that sealed accounting live so a later resolution of those exact
16803
+ // tracked promises can remove its entries; this ledger proof does not
16804
+ // establish that an OS process or remote tool stopped. Quarantining here
16805
+ // would freeze a stale tombstone even after every tracked promise completed.
16767
16806
  this.#disownedSteeringDisposition = undefined;
16768
16807
  // Rearm surviving owned-completion follow-ups once the abort has
16769
16808
  // settled: the aborted loop exits before polling the follow-up queue,
@@ -18209,6 +18248,7 @@ export class AgentSession {
18209
18248
  reason?: TemporaryModelReason;
18210
18249
  providerSessionScope?: TemporaryProviderSessionScope;
18211
18250
  signal?: AbortSignal;
18251
+ onMutationStarted?: () => void;
18212
18252
  },
18213
18253
  // biome-ignore lint/suspicious/noConfusingVoidType: Existing session adapters return Promise<void>; a scope is optional.
18214
18254
  ): Promise<TemporaryProviderSessionScope | void> {
@@ -18226,6 +18266,7 @@ export class AgentSession {
18226
18266
  throw new Error(`No API key for ${model.provider}/${model.id}`);
18227
18267
  }
18228
18268
  if (suppliedScope && this.#temporaryProviderSessionScopes.at(-1)?.token !== suppliedScope) return;
18269
+ options?.onMutationStarted?.();
18229
18270
 
18230
18271
  const isTemporaryOperation = options?.cause === undefined || options.cause === "temporary-operation";
18231
18272
  const autoCreateScope = isTemporaryOperation && !suppliedScope;
@@ -18277,13 +18318,18 @@ export class AgentSession {
18277
18318
  model: Model | undefined,
18278
18319
  thinkingLevel: ThinkingLevel | undefined,
18279
18320
  ): Promise<void> {
18321
+ const previousEditMode = this.#resolveActiveEditMode();
18280
18322
  if (model) {
18281
- await this.setModelTemporary(model, thinkingLevel, { cause: "rollback", reason: "other" });
18282
- return;
18323
+ // Restoring a captured live model is compensation, not a new selection:
18324
+ // the old credential may have disappeared after the forward mutation.
18325
+ this.#setModelAuthoritatively(model, "rollback");
18326
+ this.sessionManager.appendModelChange(`${model.provider}/${model.id}`, "temporary");
18327
+ this.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
18328
+ } else {
18329
+ this.#clearActiveRetryFallback();
18330
+ this.#setModelWithProviderSessionReset(undefined);
18331
+ this.#syncAppendOnlyContext(undefined);
18283
18332
  }
18284
- const previousEditMode = this.#resolveActiveEditMode();
18285
- this.#clearActiveRetryFallback();
18286
- this.#setModelWithProviderSessionReset(undefined);
18287
18333
  this.setThinkingLevel(thinkingLevel);
18288
18334
  await this.#syncEditToolModeAfterModelChange(previousEditMode);
18289
18335
  }
@@ -1852,6 +1852,16 @@ function receiptMatches(
1852
1852
  tree?: unknown;
1853
1853
  };
1854
1854
  };
1855
+ // A receipt is keyed by its source and names exactly one destination. The
1856
+ // resume listing probes every (legacy source, v2 destination) pair, so reject
1857
+ // on these cheap receipt fields before hashing both transcripts and walking
1858
+ // the artifact manifest; the full conjunction below would be false anyway.
1859
+ if (
1860
+ record.destination?.path !== destination.path ||
1861
+ record.source?.path !== source.path ||
1862
+ record.source?.sha256 !== source.identity.sha256
1863
+ )
1864
+ return false;
1855
1865
  const exact = (
1856
1866
  recorded: { dev?: unknown; ino?: unknown; size?: unknown; mtimeNs?: unknown } | undefined,
1857
1867
  candidate: ManagedCandidate,
@@ -1178,11 +1178,40 @@ export function retainManagedDirectoryAuthority(
1178
1178
  )
1179
1179
  throw new Error("Managed root authority changed");
1180
1180
  const relative = path.relative(root.canonicalPath, resolved).split(path.sep).join("/");
1181
- return rootAuthority.retainManagedDirectory(
1181
+ const rootRecovery = rootAuthority.recoveryReaperMetrics();
1182
+ const rootRecoveryUnhealthy = !rootRecovery.ok || Boolean(rootRecovery.code);
1183
+ if (rootRecoveryUnhealthy)
1184
+ logger.warn("Managed recovery sidecar reaping", {
1185
+ ok: rootRecovery.ok,
1186
+ code: rootRecovery.code,
1187
+ reapedCount: rootRecovery.totalReapedFiles,
1188
+ reapedBytes: rootRecovery.totalReapedBytes,
1189
+ failureCount: rootRecovery.totalFailures,
1190
+ scanLimited: rootRecovery.scanLimited,
1191
+ });
1192
+ const authority = rootAuthority.retainManagedDirectory(
1182
1193
  relative,
1183
1194
  canonicalFileId(named.dev).toString(),
1184
1195
  canonicalFileId(named.ino).toString(),
1185
1196
  );
1197
+ const recovery = authority.recoveryReaperMetrics();
1198
+ if (
1199
+ !rootRecoveryUnhealthy &&
1200
+ (!recovery.ok ||
1201
+ recovery.code ||
1202
+ recovery.totalReapedFiles !== "0" ||
1203
+ recovery.totalFailures !== "0" ||
1204
+ recovery.scanLimited)
1205
+ )
1206
+ logger.warn("Managed recovery sidecar reaping", {
1207
+ ok: recovery.ok,
1208
+ code: recovery.code,
1209
+ reapedCount: recovery.totalReapedFiles,
1210
+ reapedBytes: recovery.totalReapedBytes,
1211
+ failureCount: recovery.totalFailures,
1212
+ scanLimited: recovery.scanLimited,
1213
+ });
1214
+ return authority;
1186
1215
  } finally {
1187
1216
  rootAuthority.close();
1188
1217
  }
@@ -6,5 +6,6 @@
6
6
  {"id":"cline-pass","aliases":["clinepass","cline"],"name":"ClinePass","description":"ClinePass subscription models discovered from Cline's upstream models.dev catalog","compatibility":"openai","api":"openai-completions","providerId":"cline-pass","baseUrl":"https://api.cline.bot/api/v1","apiKeyEnv":"CLINE_API_KEY","discovery":{"type":"models-dev","modelsDevProvider":"cline-pass"},"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"reasoningContentField":"reasoning"}},
7
7
  {"id":"commandcode-goat","aliases":["commandcode","command-code","goat"],"name":"Command Code GOAT","description":"Command Code GOAT coding-plan models discovered from its OpenAI-compatible Provider API","compatibility":"openai","api":"openai-completions","providerId":"commandcode-goat","baseUrl":"https://api.commandcode.ai/provider/v1","apiKeyEnv":"CMD_API_KEY","discovery":{"type":"openai-models-list","apiByModelPrefix":{"claude":"anthropic-messages","anthropic/claude":"anthropic-messages"}},"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"maxTokensField":"max_tokens","reasoningContentField":"reasoning_content"}},
8
8
  {"id":"litellm","aliases":["litellm-proxy"],"name":"LiteLLM Proxy","description":"OpenAI-compatible LiteLLM proxy endpoint (user-supplied base URL) with live model discovery","compatibility":"openai","api":"openai-completions","providerId":"litellm","parameterized":true,"apiKeyEnv":"LITELLM_API_KEY","discovery":{"type":"openai-models-list"}},
9
- {"id":"openai-compatible-proxy","aliases":["openai-proxy","compatible-proxy","custom-proxy"],"name":"OpenAI-Compatible Proxy","description":"Generic OpenAI-compatible gateway (OpenRouter, OpenCode Go, custom) with live model discovery","compatibility":"openai","api":"openai-completions","providerId":"openai-compatible-proxy","parameterized":true,"apiKeyEnv":"OPENAI_COMPATIBLE_API_KEY","discovery":{"type":"openai-models-list"}}
9
+ {"id":"openai-compatible-proxy","aliases":["openai-proxy","compatible-proxy","custom-proxy"],"name":"OpenAI-Compatible Proxy","description":"Generic OpenAI-compatible gateway (OpenRouter, OpenCode Go, custom) with live model discovery","compatibility":"openai","api":"openai-completions","providerId":"openai-compatible-proxy","parameterized":true,"apiKeyEnv":"OPENAI_COMPATIBLE_API_KEY","discovery":{"type":"openai-models-list"}},
10
+ {"id":"ionet","aliases":["io-net","io-intelligence"],"name":"IO Intelligence (io.net)","description":"OpenAI-compatible IO Intelligence endpoint by io.net with live model discovery","compatibility":"openai","api":"openai-completions","providerId":"ionet","baseUrl":"https://api.intelligence.io.solutions/api/v1","apiKeyEnv":"IONET_API_KEY","discovery":{"type":"openai-models-list"},"compat":{"supportsStore":false,"supportsDeveloperRole":false,"supportsReasoningEffort":false,"supportsUsageInStreaming":false,"maxTokensField":"max_tokens","reasoningContentField":"reasoning_content","extraBody":{"tool_choice":"auto"}}}
10
11
  ]
@@ -286,7 +286,7 @@ function providerSetupUsage(): string {
286
286
  return [
287
287
  "Provider onboarding",
288
288
  "Presets: /provider add --preset <id> [--force]",
289
- "Aliases include minimax, zai, alibaba, cline, command-code, and goat.",
289
+ "Aliases include minimax, zai, alibaba, cline, command-code, goat, and ionet.",
290
290
  `API providers: ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} [--force]`,
291
291
  `Available presets:\n${formatProviderPresetList()}`,
292
292
  "OAuth/subscription providers: /provider login [provider-id] or /login [provider-id]",
@@ -37,6 +37,7 @@ import { ModelRegistry } from "../config/model-registry";
37
37
  import {
38
38
  formatModelString,
39
39
  isExplicitProviderModelOverride,
40
+ refreshMissingQualifiedModelProviders,
40
41
  resolveModelOverrideWithAuthFallback,
41
42
  } from "../config/model-resolver";
42
43
  import type { PromptTemplate } from "../config/prompt-templates";
@@ -1418,6 +1419,7 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
1418
1419
  forwardSubagentEvent(event);
1419
1420
 
1420
1421
  const now = Date.now();
1422
+ progress.lastActivityMs = now;
1421
1423
  let flushProgress = false;
1422
1424
  // A retry is recovered at the first assistant provider event, before the
1423
1425
  // session emits auto_retry_end after message completion.
@@ -1719,19 +1721,20 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
1719
1721
  checkAbort();
1720
1722
  // Pin authStorage to modelRegistry.authStorage — mirrors the createAgentSession invariant.
1721
1723
  const registryFromParent = options.modelRegistry !== undefined;
1724
+ const effectiveAgentDir = options.agentDir ?? settings.getAgentDir();
1722
1725
  const registryAuthStorage =
1723
1726
  options.authStorage ??
1724
1727
  options.modelRegistry?.authStorage ??
1725
- (await awaitAbortable(discoverAuthStorage(settings.getAgentDir())));
1728
+ (await awaitAbortable(discoverAuthStorage(effectiveAgentDir)));
1726
1729
  if (options.modelRegistry === undefined && options.authStorage === undefined)
1727
1730
  ownedAuthStorage = registryAuthStorage;
1728
1731
  ownedModelRegistry = options.modelRegistry
1729
1732
  ? undefined
1730
- : new ModelRegistry(registryAuthStorage, path.join(settings.getAgentDir(), "models.yml"), settings, {
1731
- agentDir: settings.getAgentDir(),
1733
+ : new ModelRegistry(registryAuthStorage, path.join(effectiveAgentDir, "models.yml"), settings, {
1734
+ agentDir: effectiveAgentDir,
1732
1735
  automaticRefresh: false,
1733
1736
  });
1734
- const modelRegistry = options.modelRegistry ?? ownedModelRegistry!;
1737
+ let modelRegistry = options.modelRegistry ?? ownedModelRegistry!;
1735
1738
  const authStorage = modelRegistry.authStorage;
1736
1739
  if (options.authStorage && options.authStorage !== authStorage) {
1737
1740
  throw new Error(
@@ -1754,17 +1757,7 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
1754
1757
  const canonicalChildScope = getSubagentCanonicalScope(options.parentSessionId, options.subagentId ?? id);
1755
1758
 
1756
1759
  if (options.preflightProbe || options.preflightDurable) preflightOperation = "auth_resolve";
1757
- const {
1758
- model,
1759
- thinkingLevel: resolvedThinkingLevel,
1760
- explicitThinkingLevel,
1761
- authFallbackUsed: resolvedAuthFallbackUsed,
1762
- requestedModel,
1763
- fallbackReason,
1764
- activeIndex,
1765
- parentFallbackSelector,
1766
- skips,
1767
- } = await awaitAbortable(
1760
+ const resolveConfiguredModel = () =>
1768
1761
  resolveModelOverrideWithAuthFallback(
1769
1762
  modelPatterns,
1770
1763
  options.preflightProbe || options.preflightDurable ? undefined : options.parentActiveModelPattern,
@@ -1776,8 +1769,44 @@ export async function runSubprocessOnce(options: ExecutorOptions): Promise<Singl
1776
1769
  ...(options.parentActiveModelProfile ? { aliasIntent: "preset-equivalent" } : {}),
1777
1770
  },
1778
1771
  canonicalChildScope,
1779
- ),
1780
- );
1772
+ );
1773
+ let resolution = await awaitAbortable(resolveConfiguredModel());
1774
+ if (
1775
+ !resolution.model &&
1776
+ registryFromParent &&
1777
+ isExplicitProviderModelOverride(modelPatterns) &&
1778
+ resolution.skips.some(skip => skip.reason === "unknown_model")
1779
+ ) {
1780
+ ownedModelRegistry = new ModelRegistry(
1781
+ registryAuthStorage,
1782
+ path.join(effectiveAgentDir, "models.yml"),
1783
+ settings,
1784
+ {
1785
+ agentDir: effectiveAgentDir,
1786
+ automaticRefresh: false,
1787
+ },
1788
+ );
1789
+ await awaitAbortable(
1790
+ refreshMissingQualifiedModelProviders(
1791
+ modelPatterns,
1792
+ ownedModelRegistry,
1793
+ options.parentCredentialSessionId ?? options.parentSessionId,
1794
+ ),
1795
+ );
1796
+ modelRegistry = ownedModelRegistry;
1797
+ resolution = await awaitAbortable(resolveConfiguredModel());
1798
+ }
1799
+ const {
1800
+ model,
1801
+ thinkingLevel: resolvedThinkingLevel,
1802
+ explicitThinkingLevel,
1803
+ authFallbackUsed: resolvedAuthFallbackUsed,
1804
+ requestedModel,
1805
+ fallbackReason,
1806
+ activeIndex,
1807
+ parentFallbackSelector,
1808
+ skips,
1809
+ } = resolution;
1781
1810
  if (!model && isExplicitProviderModelOverride(modelPatterns)) {
1782
1811
  const skipReasons = [...new Set(skips.map(skip => skip.reason))];
1783
1812
  if ((options.preflightProbe || options.preflightDurable) && skipReasons.includes("unauthenticated")) {
package/src/task/types.ts CHANGED
@@ -285,6 +285,8 @@ export interface AgentProgress {
285
285
  recentTools: Array<{ tool: string; args: string; endMs: number }>;
286
286
  recentOutput: string[];
287
287
  toolCount: number;
288
+ /** Epoch ms of the latest event observed from this subagent (any agent event). */
289
+ lastActivityMs?: number;
288
290
  /** Cumulative input + output + cacheWrite tokens across all turns. Excludes cacheRead (re-reads cached context every turn, making cumulative sum misleading). */
289
291
  tokens: number;
290
292
  /**
@@ -331,6 +331,7 @@ export async function runInteractiveBashPty(
331
331
  timeoutMs: number;
332
332
  signal?: AbortSignal;
333
333
  env?: Record<string, string>;
334
+ unsetEnv?: string[];
334
335
  artifactPath?: string;
335
336
  artifactId?: string;
336
337
  artifactPublisher?: TerminalArtifactPublisher;
@@ -422,6 +423,7 @@ export async function runInteractiveBashPty(
422
423
  cwd: options.cwd,
423
424
  timeoutMs: options.timeoutMs,
424
425
  env: { ...NON_INTERACTIVE_ENV, ...options.env },
426
+ unsetEnv: options.unsetEnv,
425
427
  signal: processAbortController.signal,
426
428
  cols: initialPtySize.cols,
427
429
  rows: initialPtySize.rows,
package/src/tools/bash.ts CHANGED
@@ -94,6 +94,15 @@ const ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256;
94
94
  const BASH_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
95
95
  const MASTER_CAPABILITY_ENV = "GJC_MASTER_CAPABILITY";
96
96
  const MASTER_OWNER_SESSION_ENV = "GJC_MASTER_OWNER_SESSION_ID";
97
+ const COORDINATOR_ONLY_BASH_ENV = [
98
+ "GJC_COORDINATOR_SESSION_STATE_FILE",
99
+ "GJC_COORDINATOR_SESSION_ID",
100
+ "GJC_COORDINATOR_SESSION_BRANCH",
101
+ "GJC_COORDINATOR_SESSION_LAUNCH_ID",
102
+ "GJC_COORDINATOR_SESSION_READINESS_FILE",
103
+ "GJC_COORDINATOR_SIDECAR_SIGNATURE_REQUIRED",
104
+ "GJC_COORDINATOR_SIDECAR_KEY_ID",
105
+ ] as const;
97
106
  const DEFAULT_AUTO_BACKGROUND_THRESHOLD_MS = 60_000;
98
107
  const ACP_RELEASE_TIMEOUT_MS = 1_000;
99
108
  const READ_ONLY_BASH_ENV: Record<string, string> = {
@@ -1051,6 +1060,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1051
1060
  notices?: readonly string[];
1052
1061
 
1053
1062
  resolvedEnv?: Record<string, string>;
1063
+ unsetEnv: string[];
1054
1064
  directMasterSpawn: boolean;
1055
1065
  onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
1056
1066
  startBackgrounded: boolean;
@@ -1108,6 +1118,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1108
1118
  timeout: options.timeoutMs,
1109
1119
  signal: runSignal,
1110
1120
  env: options.resolvedEnv,
1121
+ unsetEnv: options.unsetEnv,
1111
1122
  artifactPath,
1112
1123
  artifactId,
1113
1124
  artifactPublisher,
@@ -1290,6 +1301,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1290
1301
  command: string;
1291
1302
  commandCwd: string;
1292
1303
  resolvedEnv: Record<string, string>;
1304
+ unsetEnv: string[];
1293
1305
  directMasterSpawn: boolean;
1294
1306
  requestedTimeoutSec: number;
1295
1307
  timeoutSec: number;
@@ -1462,6 +1474,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1462
1474
  ...(this.session.bashRestrictionProfile === "read-only" ? READ_ONLY_BASH_ENV : {}),
1463
1475
  ...(allowedPrefixes && allowedPrefixes.length > 0 ? { [GJC_RESTRICTED_ROLE_AGENT_BASH_ENV]: "1" } : {}),
1464
1476
  };
1477
+ const unsetEnv = COORDINATOR_ONLY_BASH_ENV.filter(name => !Object.hasOwn(expandedEnv ?? {}, name));
1465
1478
 
1466
1479
  if (cwd?.includes("://") || cwd?.includes("local:/")) {
1467
1480
  cwd = await expandInternalUrls(cwd, { ...internalUrlOptions, noEscape: true });
@@ -1494,6 +1507,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1494
1507
  command,
1495
1508
  commandCwd,
1496
1509
  resolvedEnv,
1510
+ unsetEnv,
1497
1511
  directMasterSpawn,
1498
1512
  requestedTimeoutSec,
1499
1513
  timeoutSec,
@@ -1657,6 +1671,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1657
1671
  timeout: monitorTimeoutMs,
1658
1672
  signal,
1659
1673
  env: prepared.resolvedEnv,
1674
+ unsetEnv: prepared.unsetEnv,
1660
1675
  artifactPath,
1661
1676
  artifactId,
1662
1677
  artifactPublisher,
@@ -1743,6 +1758,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1743
1758
  command,
1744
1759
  commandCwd,
1745
1760
  resolvedEnv,
1761
+ unsetEnv,
1746
1762
  directMasterSpawn,
1747
1763
  requestedTimeoutSec,
1748
1764
  timeoutSec,
@@ -1782,6 +1798,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1782
1798
  notices: pendingNotices,
1783
1799
 
1784
1800
  resolvedEnv,
1801
+ unsetEnv,
1785
1802
  directMasterSpawn,
1786
1803
  onUpdate,
1787
1804
  startBackgrounded: true,
@@ -1834,6 +1851,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
1834
1851
  notices: pendingNotices,
1835
1852
 
1836
1853
  resolvedEnv,
1854
+ unsetEnv,
1837
1855
  directMasterSpawn,
1838
1856
  onUpdate,
1839
1857
  startBackgrounded,
@@ -2606,6 +2624,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
2606
2624
  timeoutMs,
2607
2625
  signal,
2608
2626
  env: resolvedEnv,
2627
+ unsetEnv,
2609
2628
  artifactPath,
2610
2629
  artifactId,
2611
2630
  artifactPublisher,
@@ -2774,6 +2793,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
2774
2793
  timeout: timeoutMs,
2775
2794
  signal,
2776
2795
  env: resolvedEnv,
2796
+ unsetEnv,
2777
2797
  artifactPath,
2778
2798
  artifactId,
2779
2799
  artifactPublisher,
@@ -11,15 +11,20 @@ import { type Theme, theme } from "../modes/theme/theme";
11
11
  import type { ToolSession } from "../sdk";
12
12
  import type { AgentStorage } from "../session/agent-storage";
13
13
  import { DEFAULT_MAX_BYTES, type TruncationDirection, truncateContent } from "../session/streaming-output";
14
- import { renderStatusLine } from "../tui";
15
14
  import { CachedOutputBlock } from "../tui/output-block";
15
+ import { renderStatusLine } from "../tui/status-line";
16
16
  import { formatDimensionNote, resizeImage } from "../utils/image-resize";
17
17
  import { parseHtmlLazy } from "../utils/linkedom";
18
18
  import { INSANE_NOTES } from "../web/insane/bridge";
19
19
  import { validatePublicHttpUrl } from "../web/insane/url-guard";
20
- import { specialHandlers } from "../web/scrapers";
21
- import type { RenderResult } from "../web/scrapers/types";
22
- import { finalizeOutput, loadPage, looksLikeHtml, MAX_OUTPUT_CHARS } from "../web/scrapers/types";
20
+ import {
21
+ finalizeOutput,
22
+ loadPage,
23
+ looksLikeHtml,
24
+ MAX_OUTPUT_CHARS,
25
+ type RenderResult,
26
+ type SpecialHandler,
27
+ } from "../web/scrapers/types";
23
28
  import { convertWithMarkit, fetchBinary } from "../web/scrapers/utils";
24
29
  import { applyListLimit } from "./list-limit";
25
30
  import { formatStyledArtifactReference, type OutputMeta } from "./output-meta";
@@ -30,6 +35,12 @@ import { clampTimeout } from "./tool-timeouts";
30
35
 
31
36
  type NativeHtmlBindings = { htmlToMarkdown: typeof htmlToMarkdownFn };
32
37
  let nativeHtmlBindings: NativeHtmlBindings | undefined;
38
+ let specialHandlersCache: SpecialHandler[] | undefined;
39
+
40
+ function getSpecialHandlers(): SpecialHandler[] {
41
+ specialHandlersCache ??= (require("../web/scrapers") as { specialHandlers: SpecialHandler[] }).specialHandlers;
42
+ return specialHandlersCache;
43
+ }
33
44
 
34
45
  /**
35
46
  * Lazy native access for HTML conversion. The module is cached, never the
@@ -659,7 +670,7 @@ async function handleSpecialUrls(
659
670
  signal: AbortSignal | undefined,
660
671
  storage: AgentStorage | null,
661
672
  ): Promise<FetchRenderResult | null> {
662
- for (const handler of specialHandlers) {
673
+ for (const handler of getSpecialHandlers()) {
663
674
  if (signal?.aborted) {
664
675
  throw new ToolAbortError();
665
676
  }
@@ -669,6 +680,11 @@ async function handleSpecialUrls(
669
680
  return null;
670
681
  }
671
682
 
683
+ /** Test-only seam for verifying lazy special-handler loading and dispatch. */
684
+ export const fetchSpecialHandlerTestHooks = {
685
+ dispatch: handleSpecialUrls,
686
+ };
687
+
672
688
  // =============================================================================
673
689
  // Main Render Function
674
690
  // =============================================================================
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import type { Component } from "@gajae-code/tui";
11
11
  import { Text } from "@gajae-code/tui";
12
+ import { formatNumber } from "@gajae-code/utils";
12
13
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
13
14
  import type { Theme } from "../modes/theme/theme";
14
15
  import { providerRetryPhaseLabel } from "../task/provider-retry-status";
@@ -22,6 +23,7 @@ import {
22
23
  truncateToWidth,
23
24
  } from "./render-utils";
24
25
  import {
26
+ SUBAGENT_ACTIVITY_BUCKET_MS,
25
27
  type SubagentLiveProgress,
26
28
  type SubagentSnapshot,
27
29
  type SubagentToolDetails,
@@ -186,7 +188,28 @@ function renderSubagentStatusLine(snapshot: SubagentSnapshot, theme: Theme, spin
186
188
  )
187
189
  : theme.fg("dim", snapshot.status);
188
190
  const duration = theme.fg("dim", formatDuration(snapshot.durationMs));
189
- return `${icon} ${id} ${status} ${duration}`;
191
+ const live = snapshot.liveProgressAvailable !== false ? snapshot.progress : undefined;
192
+ return `${icon} ${id} ${status} ${duration}${live ? formatLiveStats(live, theme, Date.now()) : ""}`;
193
+ }
194
+
195
+ // Live stats ride the cheap status line (rebuilt every render), never the cached
196
+ // body. Quantize age to the producer cadence so incidental renders do not churn it.
197
+ function formatLiveStats(progress: SubagentLiveProgress, theme: Theme, nowMs: number): string {
198
+ const parts: string[] = [];
199
+ if (progress.toolCount) parts.push(`${progress.toolCount} ${progress.toolCount === 1 ? "tool" : "tools"}`);
200
+ if (progress.contextTokens) {
201
+ parts.push(
202
+ progress.contextWindow
203
+ ? `${formatNumber(progress.contextTokens)}/${formatNumber(progress.contextWindow)} ctx`
204
+ : `${formatNumber(progress.contextTokens)} ctx`,
205
+ );
206
+ }
207
+ if (progress.lastActivityMs !== undefined && progress.status === "running") {
208
+ const ageMs = Math.max(0, nowMs - progress.lastActivityMs);
209
+ const ageBucketMs = Math.floor(ageMs / SUBAGENT_ACTIVITY_BUCKET_MS) * SUBAGENT_ACTIVITY_BUCKET_MS;
210
+ parts.push(`last activity ${formatDuration(ageBucketMs)} ago`);
211
+ }
212
+ return parts.map(part => `${theme.sep.dot}${theme.fg("dim", part)}`).join("");
190
213
  }
191
214
 
192
215
  function renderSubagentLiveProgress(
@@ -120,6 +120,10 @@ export interface SubagentLiveProgress {
120
120
  currentTool?: string;
121
121
  recentTool?: string;
122
122
  recentOutputSummary?: { lineCount: number };
123
+ toolCount?: number;
124
+ contextTokens?: number;
125
+ contextWindow?: number;
126
+ lastActivityMs?: number;
123
127
  fastMode?: boolean;
124
128
  retryState?: {
125
129
  attempt: number;
@@ -145,6 +149,14 @@ function toSubagentLiveProgress(progress: AgentProgress): SubagentLiveProgress {
145
149
  ...(progress.recentOutput.length > 0
146
150
  ? { recentOutputSummary: { lineCount: Math.min(progress.recentOutput.length, 6) } }
147
151
  : {}),
152
+ ...(progress.toolCount > 0 ? { toolCount: progress.toolCount } : {}),
153
+ ...(progress.contextTokens !== undefined && progress.contextTokens > 0
154
+ ? { contextTokens: progress.contextTokens }
155
+ : {}),
156
+ ...(progress.contextWindow !== undefined && progress.contextWindow > 0
157
+ ? { contextWindow: progress.contextWindow }
158
+ : {}),
159
+ ...(progress.lastActivityMs !== undefined ? { lastActivityMs: progress.lastActivityMs } : {}),
148
160
  ...(progress.fastMode ? { fastMode: true } : {}),
149
161
  ...(progress.retryState
150
162
  ? {
@@ -1104,11 +1116,15 @@ function previewJobOutput(
1104
1116
  * unsafe.
1105
1117
  *
1106
1118
  * Time-derived fields are intentionally excluded so the panel does not churn while
1107
- * idle: raw durations (`durationMs`), current-tool elapsed (`currentToolStartMs`),
1119
+ * idle (the child's last-event timestamp is included; it moves only on real
1120
+ * activity): raw durations (`durationMs`), current-tool elapsed (`currentToolStartMs`),
1108
1121
  * and retry countdowns (`retryState.startedAtMs`) are omitted. Idle duration and
1109
1122
  * countdown ticking is sacrificed by design; every real transition still changes
1110
1123
  * the signature.
1111
1124
  */
1125
+ /** Shared cadence for await progress refreshes and rendered last-activity age. */
1126
+ export const SUBAGENT_ACTIVITY_BUCKET_MS = 5_000;
1127
+
1112
1128
  export function subagentAwaitRenderedStateSignature(
1113
1129
  subagents: readonly SubagentSnapshot[],
1114
1130
  receipt?: Pick<SubagentToolDetails, "awaitOutcome" | "interrupted">,
@@ -1156,6 +1172,16 @@ function canonicalizeProgressForSignature(progress: SubagentLiveProgress): unkno
1156
1172
  currentTool: progress.currentTool ?? null,
1157
1173
  recentTool: progress.recentTool ?? null,
1158
1174
  recentOutputSummary: progress.recentOutputSummary ?? null,
1175
+ toolCount: progress.toolCount ?? 0,
1176
+ contextTokens: progress.contextTokens ?? null,
1177
+ contextWindow: progress.contextWindow ?? null,
1178
+ // Event timestamp, bucketed: it only moves when the child actually does
1179
+ // something (idle gating holds), and the bucket keeps a streaming child from
1180
+ // re-emitting on every delta. The status-line age is quantized to this cadence.
1181
+ lastActivityBucket:
1182
+ progress.lastActivityMs === undefined
1183
+ ? null
1184
+ : Math.floor(progress.lastActivityMs / SUBAGENT_ACTIVITY_BUCKET_MS),
1159
1185
  fastMode: progress.fastMode ?? false,
1160
1186
  retryFailure: progress.retryFailure ?? null,
1161
1187
  retryState: progress.retryState
@@ -122,8 +122,15 @@ function formatMuPdfCauseChain(error: unknown): string {
122
122
  // internal inspection and operational debug logging. Conservatively redact the
123
123
  // rest of a path-bearing field so spaces in install paths cannot leak suffixes.
124
124
  export function sanitizeMuPdfDiagnostic(message: string): string {
125
- return util
126
- .stripVTControlCharacters(message)
127
- .replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g, "")
128
- .replace(/[^\s"'`]*(?:[/\\]|%2f|%5c)[^"'`]*(?=["'`]|$)/gi, "[path redacted]");
125
+ return (
126
+ util
127
+ .stripVTControlCharacters(message)
128
+ .replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g, "")
129
+ // The prefix run is boundary anchored. Unbounded, `[^\s"'`]*` is re-tried at
130
+ // every offset of a long slash-free run before failing to find a separator,
131
+ // which is quadratic in the message length: 100 KB cost about 13s. A MuPDF
132
+ // diagnostic is derived from the document being converted, and
133
+ // `normalizeError` runs this once per link in the cause chain.
134
+ .replace(/(?<![^\s"'`])[^\s"'`]*(?:[/\\]|%2f|%5c)[^"'`]*(?=["'`]|$)/gi, "[path redacted]")
135
+ );
129
136
  }