@gajae-code/coding-agent 0.16.4 → 0.16.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 (198) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/customize-doctor.d.ts +22 -0
  4. package/dist/types/config/file-lock.d.ts +31 -3
  5. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  6. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  7. package/dist/types/discovery/helpers.d.ts +33 -0
  8. package/dist/types/exec/bash-executor.d.ts +17 -1
  9. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  10. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  11. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  12. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  13. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  14. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  15. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  16. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  17. package/dist/types/exec/isolated-shell.d.ts +19 -0
  18. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  19. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  20. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  22. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  23. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  24. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  25. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  26. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  27. package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
  28. package/dist/types/extensibility/skill-management.d.ts +9 -4
  29. package/dist/types/extensibility/skills.d.ts +2 -0
  30. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  31. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
  33. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  34. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  35. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  36. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  37. package/dist/types/modes/interactive-mode.d.ts +2 -0
  38. package/dist/types/modes/types.d.ts +6 -0
  39. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  40. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  41. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  42. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  43. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  44. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  45. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  46. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  47. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  48. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  49. package/dist/types/sdk/client/client.d.ts +6 -0
  50. package/dist/types/sdk/host/host.d.ts +3 -1
  51. package/dist/types/sdk/host/session-runtime.d.ts +4 -2
  52. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  53. package/dist/types/sdk/router/session-router.d.ts +10 -2
  54. package/dist/types/sdk/session.d.ts +5 -1
  55. package/dist/types/session/agent-session.d.ts +2 -0
  56. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  57. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  58. package/dist/types/session/messages.d.ts +1 -0
  59. package/dist/types/session/session-manager.d.ts +3 -1
  60. package/dist/types/setup/paseo/announce.d.ts +4 -3
  61. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  62. package/dist/types/system-prompt.d.ts +8 -0
  63. package/dist/types/task/executor.d.ts +3 -0
  64. package/dist/types/tools/bash.d.ts +1 -0
  65. package/dist/types/tools/github-cache.d.ts +2 -1
  66. package/dist/types/tools/index.d.ts +2 -0
  67. package/package.json +9 -7
  68. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  69. package/src/capability/index.ts +14 -2
  70. package/src/capability/types.ts +4 -0
  71. package/src/cli/customize-doctor.ts +155 -12
  72. package/src/cli.ts +22 -0
  73. package/src/commands/sdk.ts +74 -31
  74. package/src/config/file-lock-gc.ts +40 -2
  75. package/src/config/file-lock.ts +198 -14
  76. package/src/config/model-preset-registry.ts +115 -16
  77. package/src/config/model-registry.ts +16 -12
  78. package/src/coordinator-mcp/question-state.ts +33 -2
  79. package/src/coordinator-mcp/server.ts +477 -101
  80. package/src/crash/sanitize.ts +1 -1
  81. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  82. package/src/discovery/builtin.ts +50 -62
  83. package/src/discovery/helpers.ts +297 -32
  84. package/src/exec/bash-executor.ts +330 -50
  85. package/src/exec/bash-shell-guardian.ts +388 -0
  86. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  87. package/src/exec/bash-shell-signals.ts +29 -0
  88. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  89. package/src/exec/bash-shell-supervisor.ts +359 -0
  90. package/src/exec/bash-shell-worker-entry.ts +5 -0
  91. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  92. package/src/exec/bash-shell-worker.ts +116 -0
  93. package/src/exec/isolated-shell.ts +393 -0
  94. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  95. package/src/extensibility/extensions/function-hooks.ts +959 -0
  96. package/src/extensibility/extensions/index.ts +22 -0
  97. package/src/extensibility/extensions/loader.ts +53 -2
  98. package/src/extensibility/extensions/runner.ts +895 -264
  99. package/src/extensibility/extensions/types.ts +42 -0
  100. package/src/extensibility/extensions/wrapper.ts +56 -15
  101. package/src/extensibility/gjc-plugins/compiler.ts +20 -2
  102. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  103. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  104. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  105. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  106. package/src/extensibility/gjc-plugins/types.ts +25 -0
  107. package/src/extensibility/hooks/loader.ts +133 -16
  108. package/src/extensibility/hooks/types.ts +28 -0
  109. package/src/extensibility/runtime-skill-discovery.ts +80 -47
  110. package/src/extensibility/skill-management.ts +200 -52
  111. package/src/extensibility/skills.ts +29 -9
  112. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  113. package/src/gjc-runtime/gc-runtime.ts +29 -0
  114. package/src/gjc-runtime/linux-proc.ts +14 -0
  115. package/src/gjc-runtime/session-state-lock.ts +51 -8
  116. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  117. package/src/gjc-runtime/state-writer.ts +30 -29
  118. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  119. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  120. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  121. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  122. package/src/harness-control-plane/session-lease.ts +2 -2
  123. package/src/internal-urls/docs-index.generated.ts +6 -6
  124. package/src/lsp/config.ts +10 -3
  125. package/src/lsp/index.ts +1 -1
  126. package/src/lsp/path-trust.ts +69 -14
  127. package/src/main.ts +6 -1
  128. package/src/modes/acp/acp-agent.ts +16 -3
  129. package/src/modes/components/bash-execution.ts +4 -1
  130. package/src/modes/components/execution-shared.ts +4 -1
  131. package/src/modes/components/session-observer-overlay.ts +1 -1
  132. package/src/modes/components/tool-status-header.ts +3 -1
  133. package/src/modes/controllers/command-controller.ts +1 -0
  134. package/src/modes/controllers/event-controller.ts +3 -2
  135. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  136. package/src/modes/controllers/input-controller.ts +6 -3
  137. package/src/modes/interactive-mode.ts +6 -5
  138. package/src/modes/print-mode.ts +5 -1
  139. package/src/modes/prompt-action-autocomplete.ts +54 -27
  140. package/src/modes/runtime-init.ts +2 -0
  141. package/src/modes/types.ts +16 -3
  142. package/src/modes/utils/ui-helpers.ts +1 -0
  143. package/src/prompts/tools/subagent.md +1 -0
  144. package/src/runtime-mcp/manager.ts +20 -2
  145. package/src/runtime-mcp/pool.test.ts +20 -11
  146. package/src/sdk/broker/broker.ts +119 -28
  147. package/src/sdk/broker/ensure.ts +248 -41
  148. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  149. package/src/sdk/broker/lifecycle.ts +56 -6
  150. package/src/sdk/broker/startup-failure.ts +15 -6
  151. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  152. package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
  153. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  154. package/src/sdk/bus/conversation-store.ts +29 -0
  155. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  156. package/src/sdk/bus/slack-conversation.ts +1 -0
  157. package/src/sdk/bus/slack-daemon.ts +387 -48
  158. package/src/sdk/bus/telegram-daemon.ts +16 -2
  159. package/src/sdk/bus/topic-registry.ts +19 -0
  160. package/src/sdk/cli/master-cli.ts +1 -1
  161. package/src/sdk/cli/session-cli.ts +2 -2
  162. package/src/sdk/client/client.ts +66 -17
  163. package/src/sdk/host/host.ts +29 -9
  164. package/src/sdk/host/query/revision-store.ts +19 -13
  165. package/src/sdk/host/reverse-leases.ts +7 -4
  166. package/src/sdk/host/session-runtime.test.ts +50 -8
  167. package/src/sdk/host/session-runtime.ts +158 -51
  168. package/src/sdk/lifecycle/service.ts +4 -0
  169. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  170. package/src/sdk/router/session-router.ts +89 -38
  171. package/src/sdk/session.ts +155 -26
  172. package/src/sdk/transport/relay.ts +35 -13
  173. package/src/session/agent-session.ts +117 -52
  174. package/src/session/fallback-chain-controller.ts +38 -0
  175. package/src/session/internal/managed-session-storage.ts +19 -5
  176. package/src/session/messages.ts +5 -0
  177. package/src/session/session-manager.ts +29 -5
  178. package/src/session-import/redact.ts +5 -1
  179. package/src/setup/paseo/announce.ts +49 -43
  180. package/src/setup/paseo/setup-deps.ts +1 -1
  181. package/src/skill-state/canonical-skills.ts +12 -0
  182. package/src/skills/skills.test.ts +41 -21
  183. package/src/system-prompt.ts +25 -3
  184. package/src/task/executor.ts +5 -0
  185. package/src/task/index.ts +4 -0
  186. package/src/tools/bash.ts +28 -11
  187. package/src/tools/gh.ts +26 -3
  188. package/src/tools/github-cache.ts +68 -16
  189. package/src/tools/index.ts +2 -0
  190. package/src/tools/job.ts +13 -2
  191. package/src/tools/read.ts +14 -2
  192. package/src/tools/render-utils.ts +1 -3
  193. package/src/tools/resource-gc.ts +28 -8
  194. package/src/tools/skill-discovery.ts +5 -0
  195. package/src/tools/skill.ts +15 -0
  196. package/src/tools/subagent.ts +3 -2
  197. package/src/tools/tool-catalog.generated.ts +2 -2
  198. package/src/tools/ultragoal-ask-guard.ts +2 -2
package/src/lsp/config.ts CHANGED
@@ -336,14 +336,21 @@ export function resolveCommand(command: string, cwd: string): string | null {
336
336
  return piUtils.$which(command);
337
337
  }
338
338
 
339
- /** Resolve an LSP executable without consulting project-controlled bin directories. */
339
+ /**
340
+ * Resolve an LSP executable without consulting project-controlled bin directories.
341
+ *
342
+ * Trust is evaluated on both the discovered and canonical paths, but the returned
343
+ * path is the discovered one: multiplexed launchers such as rustup pick the proxied
344
+ * tool from argv[0], so `~/.cargo/bin/rust-analyzer -> rustup` must be spawned as
345
+ * `rust-analyzer`, not as the resolved `rustup` binary.
346
+ */
340
347
  function resolveTrustedLspCommand(command: string, cwd: string): string | null {
341
348
  if (!path.isAbsolute(command) && (command.includes("/") || command.includes("\\"))) return null;
342
349
  const discovered = path.isAbsolute(command) ? command : piUtils.$which(command);
343
350
  if (!discovered) return null;
344
351
  if (isProjectControlledPath(discovered, cwd)) return null;
345
- const canonical = canonicalExistingPath(discovered);
346
- return canonical;
352
+ if (!canonicalExistingPath(discovered)) return null;
353
+ return discovered;
347
354
  }
348
355
 
349
356
  interface ConfigSource {
package/src/lsp/index.ts CHANGED
@@ -1160,7 +1160,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
1160
1160
 
1161
1161
  const serverStatus =
1162
1162
  servers.length > 0
1163
- ? `Active language servers: ${servers.join(", ")}`
1163
+ ? `Configured language servers: ${servers.join(", ")}`
1164
1164
  : "No language servers configured for this project";
1165
1165
 
1166
1166
  const output = lspmuxStatus ? `${serverStatus}\n${lspmuxStatus}` : serverStatus;
@@ -59,29 +59,84 @@ function findProjectTrustRoot(start: string, stopPaths: ReadonlySet<string>): st
59
59
  }
60
60
  }
61
61
 
62
+ function isProjectMarkerRoot(root: string): boolean {
63
+ return fs.existsSync(path.join(root, ".git")) || isDirectory(path.join(root, CONFIG_DIR_NAME));
64
+ }
65
+
66
+ /**
67
+ * Spellings of `candidate` that differ only in how HOME is named: the path as
68
+ * given, plus the same path with any ancestor that is HOME under another name
69
+ * replaced by canonical HOME. The suffix beneath HOME is kept as spelled, and an
70
+ * alias that lives inside a project (`repo/home-link -> HOME`) is project
71
+ * content, not a HOME spelling, so it is never expanded.
72
+ */
73
+ function candidateSpellings(candidate: string, canonicalHome: string, projectRoot: string | undefined): string[] {
74
+ const resolved = path.resolve(candidate);
75
+ const spellings = [resolved];
76
+ const normalizedHome = normalizePathForComparison(canonicalHome);
77
+ let current = path.dirname(resolved);
78
+ for (;;) {
79
+ if (
80
+ normalizePathForComparison(current) !== normalizedHome &&
81
+ (projectRoot === undefined || !pathIsLexicallyWithin(projectRoot, current)) &&
82
+ normalizePathForComparison(canonicalPath(current)) === normalizedHome
83
+ ) {
84
+ spellings.push(path.join(canonicalHome, path.relative(current, resolved)));
85
+ }
86
+ const parent = path.dirname(current);
87
+ if (parent === current) return spellings;
88
+ current = parent;
89
+ }
90
+ }
91
+
62
92
  export function isProjectControlledPath(candidate: string, cwd: string): boolean {
63
93
  const home = os.homedir();
64
- const stopPaths = new Set([path.resolve(home), canonicalPath(home)].map(normalizePathForComparison));
94
+ const canonicalHome = canonicalPath(home);
95
+ const stopPaths = new Set([path.resolve(home), canonicalHome].map(normalizePathForComparison));
65
96
  const lexicalTrustRoot = findProjectTrustRoot(cwd, stopPaths);
66
- const candidateIsHomeScoped = pathIsLexicallyWithin(home, candidate) || pathIsWithin(home, candidate);
67
- if (
68
- lexicalTrustRoot !== undefined &&
69
- pathIsLexicallyWithin(lexicalTrustRoot, candidate) &&
70
- (!candidateIsHomeScoped || pathIsLexicallyWithin(home, lexicalTrustRoot))
71
- )
72
- return true;
97
+ // A trust root outside HOME must not claim user executables inside HOME
98
+ // (e.g. ~/.gjc/bin); a root under HOME still owns what it contains. Each check
99
+ // judges home scope on the same view of the path it inspects, without
100
+ // dereferencing the candidate's final component: mixing views let a project
101
+ // link into HOME, or a HOME link into the project, escape rejection. HOME may
102
+ // be spelled lexically or canonically on either side, so the lexical check
103
+ // runs over every HOME-alias spelling of both the candidate and the root.
104
+ if (lexicalTrustRoot !== undefined) {
105
+ const trustRootIsHomeScoped = pathIsWithin(canonicalHome, lexicalTrustRoot);
106
+ // A root under HOME may legitimately be reached through a HOME alias, and a
107
+ // bare fallback root (no project marker) owns nothing on its own; a real
108
+ // project root outside HOME owns every alias directory it contains.
109
+ const aliasBoundary =
110
+ !trustRootIsHomeScoped && isProjectMarkerRoot(lexicalTrustRoot) ? lexicalTrustRoot : undefined;
111
+ const rootSpellings = candidateSpellings(lexicalTrustRoot, canonicalHome, undefined);
112
+ const spellings = candidateSpellings(candidate, canonicalHome, aliasBoundary);
113
+ // Every spelling names the same location, so home scope is a property of
114
+ // the location: one spelling under HOME makes all of them HOME-scoped.
115
+ const candidateIsHomeScoped = spellings.some(
116
+ spelling => pathIsLexicallyWithin(home, spelling) || pathIsLexicallyWithin(canonicalHome, spelling),
117
+ );
118
+ if (
119
+ (!candidateIsHomeScoped || trustRootIsHomeScoped) &&
120
+ spellings.some(spelling => rootSpellings.some(root => pathIsLexicallyWithin(root, spelling)))
121
+ ) {
122
+ return true;
123
+ }
124
+ }
73
125
 
126
+ const canonicalCandidate = canonicalPath(candidate);
127
+ const canonicalCandidateParent = canonicalParentPath(candidate);
128
+ const parentIsHomeScoped = pathIsLexicallyWithin(canonicalHome, canonicalCandidateParent);
129
+ const targetIsHomeScoped = pathIsWithin(canonicalHome, canonicalCandidate);
74
130
  const canonicalTrustRoots = new Set<string>();
75
131
  if (lexicalTrustRoot !== undefined) canonicalTrustRoots.add(canonicalPath(lexicalTrustRoot));
76
132
  const canonicalTrustRoot = findProjectTrustRoot(canonicalPath(cwd), stopPaths);
77
133
  if (canonicalTrustRoot !== undefined) canonicalTrustRoots.add(canonicalPath(canonicalTrustRoot));
78
134
  for (const trustRoot of canonicalTrustRoots) {
79
- if (
80
- (pathIsLexicallyWithin(trustRoot, canonicalParentPath(candidate)) ||
81
- pathIsWithin(trustRoot, canonicalPath(candidate))) &&
82
- (!candidateIsHomeScoped || pathIsLexicallyWithin(home, trustRoot) || pathIsWithin(home, trustRoot))
83
- )
84
- return true;
135
+ const trustRootIsHomeScoped = pathIsLexicallyWithin(canonicalHome, trustRoot);
136
+ const parentOwned =
137
+ pathIsLexicallyWithin(trustRoot, canonicalCandidateParent) && (!parentIsHomeScoped || trustRootIsHomeScoped);
138
+ const targetOwned = pathIsWithin(trustRoot, canonicalCandidate) && (!targetIsHomeScoped || trustRootIsHomeScoped);
139
+ if (parentOwned || targetOwned) return true;
85
140
  }
86
141
  return false;
87
142
  }
package/src/main.ts CHANGED
@@ -68,6 +68,7 @@ import {
68
68
  import { processIncarnation } from "./sdk/broker/process-incarnation";
69
69
  import { newMasterAttestationEpoch, resolveSessionLocator, SessionIndex } from "./sdk/broker/session-index";
70
70
  import type { AgentSession } from "./session/agent-session";
71
+ import { ManagedAppendIdentityMismatchError } from "./session/internal/managed-session-storage";
71
72
  import { SessionMigrationBusyError } from "./session/internal/session-open-errors";
72
73
  import {
73
74
  type ResumeSessionIdentity,
@@ -551,7 +552,11 @@ export async function applyStartupModelProfiles(args: StartupModelProfileArgs):
551
552
 
552
553
  async function exitForStartupModelProfileError(args: StartupModelProfileArgs, error: unknown): Promise<never> {
553
554
  const message = error instanceof Error ? error.message : String(error);
554
- process.stderr.write(`${chalk.red(`Error: ${message}`)}\n`);
555
+ process.stderr.write(
556
+ error instanceof ManagedAppendIdentityMismatchError
557
+ ? `${error.operatorMessage}\n`
558
+ : `${chalk.red(`Error: ${message}`)}\n`,
559
+ );
555
560
  await args.session.dispose();
556
561
  process.exit(1);
557
562
  }
@@ -101,6 +101,16 @@ const MAX_PROMPT_FRAME_BYTES = 256 * 1024;
101
101
  * envelope. A canonical-length UUID keeps the measurement equal to the real frame.
102
102
  */
103
103
  const PROMPT_FRAME_ID_PLACEHOLDER = "00000000-0000-4000-8000-000000000000";
104
+ /**
105
+ * Readiness budget every ACP session launch requests, independent of MCP.
106
+ *
107
+ * Host cold start costs the same whether or not the client declared MCP servers,
108
+ * so leaving the MCP-less path on the broker's 10s default made it the only ACP
109
+ * launch that fails under ordinary concurrency: a second host starting beside the
110
+ * first measurably crosses that deadline, and the broker then reports the launch
111
+ * as `terminal_uncertain`, which an external client surfaces as a dead provider.
112
+ */
113
+ const ACP_SESSION_READINESS_TIMEOUT_MS = ACP_MCP_LIFECYCLE_TIMEOUT_MS;
104
114
 
105
115
  type JsonObject = Record<string, unknown>;
106
116
  interface PromptWaiter {
@@ -1362,7 +1372,8 @@ export class AcpAgent implements Agent {
1362
1372
  cwd: params.cwd,
1363
1373
  target: { path: params.cwd },
1364
1374
  ...(this.#startupOptions?.modelPreset ? { modelPreset: this.#startupOptions.modelPreset } : {}),
1365
- ...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
1375
+ readinessTimeoutMs: ACP_SESSION_READINESS_TIMEOUT_MS,
1376
+ ...(mcpServers.length > 0 ? { mcpServers } : {}),
1366
1377
  },
1367
1378
  randomUUID(),
1368
1379
  mcpServers,
@@ -1418,7 +1429,8 @@ export class AcpAgent implements Agent {
1418
1429
  sourceSessionId: params.sessionId,
1419
1430
  sourceSessionPath: source,
1420
1431
  target: { path: params.cwd },
1421
- ...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
1432
+ readinessTimeoutMs: ACP_SESSION_READINESS_TIMEOUT_MS,
1433
+ ...(mcpServers.length > 0 ? { mcpServers } : {}),
1422
1434
  },
1423
1435
  randomUUID(),
1424
1436
  mcpServers,
@@ -2268,7 +2280,8 @@ export class AcpAgent implements Agent {
2268
2280
  sessionId: id,
2269
2281
  sessionPath: saved,
2270
2282
  target: { path: cwd },
2271
- ...(mcpServers.length > 0 ? { mcpServers, readinessTimeoutMs: ACP_MCP_LIFECYCLE_TIMEOUT_MS } : {}),
2283
+ readinessTimeoutMs: ACP_SESSION_READINESS_TIMEOUT_MS,
2284
+ ...(mcpServers.length > 0 ? { mcpServers } : {}),
2272
2285
  },
2273
2286
  randomUUID(),
2274
2287
  mcpServers,
@@ -43,6 +43,7 @@ export class BashExecutionComponent extends Container {
43
43
  #outputLines: string[] = [];
44
44
  #status: ExecutionStatus = "running";
45
45
  #exitCode: number | undefined = undefined;
46
+ #signal: string | undefined = undefined;
46
47
  #loader: Loader;
47
48
  #truncation?: TruncationMeta;
48
49
  #expanded = false;
@@ -127,9 +128,10 @@ export class BashExecutionComponent extends Container {
127
128
  setComplete(
128
129
  exitCode: number | undefined,
129
130
  cancelled: boolean,
130
- options?: { output?: string; truncation?: TruncationMeta },
131
+ options?: { output?: string; signal?: string; truncation?: TruncationMeta },
131
132
  ): void {
132
133
  this.#exitCode = exitCode;
134
+ this.#signal = options?.signal;
133
135
  this.#status = resolveExecutionStatus(exitCode, cancelled);
134
136
  this.#truncation = options?.truncation;
135
137
  if (options?.output !== undefined) {
@@ -215,6 +217,7 @@ export class BashExecutionComponent extends Container {
215
217
  const footer = buildStatusFooter({
216
218
  status: this.#status,
217
219
  exitCode: this.#exitCode,
220
+ signal: this.#signal,
218
221
  truncation: this.#truncation,
219
222
  hiddenLineCount,
220
223
  suppressHiddenCount: hasSixelOutput && !fallbackActive,
@@ -68,6 +68,7 @@ export function createCollapsedPreview(previewText: string, previewLines: number
68
68
  export function buildStatusFooter(opts: {
69
69
  status: ExecutionStatus;
70
70
  exitCode: number | undefined;
71
+ signal?: string;
71
72
  truncation: TruncationMeta | undefined;
72
73
  hiddenLineCount: number;
73
74
  /** Suppress the "… N more lines" hint (used when sixel passthrough renders the full output). */
@@ -81,7 +82,9 @@ export function buildStatusFooter(opts: {
81
82
  if (opts.status === "cancelled") {
82
83
  parts.push(theme.fg("warning", "(cancelled)"));
83
84
  } else if (opts.status === "error") {
84
- parts.push(theme.fg("error", `(exit ${opts.exitCode})`));
85
+ parts.push(
86
+ theme.fg("error", opts.signal ? `(${opts.signal}, exit ${opts.exitCode})` : `(exit ${opts.exitCode})`),
87
+ );
85
88
  }
86
89
  if (opts.truncation) {
87
90
  parts.push(theme.fg("warning", formatTruncationMetaNotice(opts.truncation)));
@@ -272,7 +272,7 @@ export class SessionObserverOverlayComponent extends TranscriptViewerOverlay {
272
272
  this.#cache = {
273
273
  ...cache,
274
274
  completeOffset: cache.completeOffset + complete.bytes.length,
275
- prefixDigest: digest(Buffer.concat([snapshot.prefix, complete.bytes])),
275
+ prefixDigest: crypto.createHash("sha256").update(snapshot.prefix).update(complete.bytes).digest("hex"),
276
276
  entries,
277
277
  model: parsed.model ?? cache.model,
278
278
  ...projection.state,
@@ -854,6 +854,8 @@ export class StatusLineComponent implements Component {
854
854
  // treats a null value as hidden, so gating here is behavior-identical.
855
855
  const prSegmentActive =
856
856
  effectiveSettings.leftSegments.includes("pr") || effectiveSettings.rightSegments.includes("pr");
857
+ const gitSegmentActive =
858
+ effectiveSettings.leftSegments.includes("git") || effectiveSettings.rightSegments.includes("git");
857
859
  const commandSegmentActive =
858
860
  effectiveSettings.leftSegments.includes("command") || effectiveSettings.rightSegments.includes("command");
859
861
  const commandOptions = commandSegmentActive ? this.#trustedCommandOptions() : undefined;
@@ -883,7 +885,7 @@ export class StatusLineComponent implements Component {
883
885
  sessionStartTime: this.#sessionStartTime,
884
886
  git: {
885
887
  branch: this.#getCurrentBranch(),
886
- status: this.#getGitStatus(),
888
+ status: gitSegmentActive ? this.#getGitStatus() : null,
887
889
  pr: prSegmentActive ? (previewOnly ? (this.#cachedPr ?? null) : this.#lookupPr()) : null,
888
890
  },
889
891
  usage: this.#cachedUsage,
@@ -1233,6 +1233,7 @@ export class CommandController {
1233
1233
  const meta = outputMeta().truncationFromSummary(result, { direction: "tail" }).get();
1234
1234
  this.ctx.bashComponent.setComplete(result.exitCode, result.cancelled, {
1235
1235
  output: result.output,
1236
+ signal: result.signal,
1236
1237
  truncation: meta?.truncation,
1237
1238
  });
1238
1239
  }
@@ -16,6 +16,7 @@ import { ToolExecutionComponent, type ToolExecutionHandle } from "../../modes/co
16
16
  import { TtsrNotificationComponent } from "../../modes/components/ttsr-notification";
17
17
  import { getSymbolTheme, theme } from "../../modes/theme/theme";
18
18
  import {
19
+ clearInteractiveActivityIndicator,
19
20
  type InteractiveModeContext,
20
21
  stopInteractiveActivityIndicator,
21
22
  syncInteractiveActivityIndicator,
@@ -1104,7 +1105,7 @@ export class EventController {
1104
1105
  this.ctx.editor.onEscape = () => {
1105
1106
  this.ctx.session.abortCompaction();
1106
1107
  };
1107
- stopInteractiveActivityIndicator(this.ctx, { restoreBackground: false, foregroundSettled: true });
1108
+ clearInteractiveActivityIndicator(this.ctx);
1108
1109
  const reasonText =
1109
1110
  event.reason === "overflow" ? "Context overflow detected, " : event.reason === "idle" ? "Idle " : "";
1110
1111
  const actionLabel = event.action === "handoff" ? "Auto-handoff" : "Auto context-full maintenance";
@@ -1205,7 +1206,7 @@ export class EventController {
1205
1206
  this.ctx.session.abortRetry();
1206
1207
  }
1207
1208
  };
1208
- stopInteractiveActivityIndicator(this.ctx, { restoreBackground: false, foregroundSettled: true });
1209
+ clearInteractiveActivityIndicator(this.ctx);
1209
1210
  // Stop any prior retry loader/timer before installing a new one.
1210
1211
  this.ctx.retryLoader?.stop();
1211
1212
  this.#clearRetryCountdown();
@@ -292,6 +292,7 @@ export class ExtensionUiController {
292
292
  const result = await session.executeBash(input.cmd, undefined, { excludeFromContext: true });
293
293
  return {
294
294
  exitCode: result.exitCode,
295
+ signal: result.signal,
295
296
  cancelled: result.cancelled,
296
297
  output: result.output,
297
298
  truncated: result.truncated,
@@ -273,16 +273,19 @@ export class InputController {
273
273
 
274
274
  #syncTranscriptTurnPosition(): readonly string[] {
275
275
  const anchorIds = getUserMessageViewportAnchorIds(this.ctx.session.messages);
276
+ const previousAnchorIds = new Set(this.#transcriptTurnAnchorIds);
276
277
  if (
277
278
  anchorIds.length !== this.#transcriptTurnAnchorIds.length ||
278
- anchorIds.some(id => !this.#transcriptTurnAnchorIds.includes(id))
279
+ anchorIds.some(id => !previousAnchorIds.has(id))
279
280
  ) {
280
281
  const hadAnchors = this.#transcriptTurnAnchorIds.length > 0;
281
282
  const currentAnchorId = this.#transcriptTurnAnchorIds[this.#transcriptTurnPosition];
282
- const retainedAnchorIds = this.#transcriptTurnAnchorIds.filter(id => anchorIds.includes(id));
283
+ const incomingAnchorIds = new Set(anchorIds);
284
+ const retainedAnchorIds = this.#transcriptTurnAnchorIds.filter(id => incomingAnchorIds.has(id));
285
+ const retainedAnchorIdSet = new Set(retainedAnchorIds);
283
286
  this.#transcriptTurnAnchorIds = [
284
287
  ...retainedAnchorIds,
285
- ...anchorIds.filter(id => !retainedAnchorIds.includes(id)),
288
+ ...anchorIds.filter(id => !retainedAnchorIdSet.has(id)),
286
289
  ];
287
290
  const currentPosition = currentAnchorId ? this.#transcriptTurnAnchorIds.indexOf(currentAnchorId) : -1;
288
291
  this.#transcriptTurnPosition =
@@ -1700,7 +1700,7 @@ export class InteractiveMode implements InteractiveModeContext {
1700
1700
  }
1701
1701
  }
1702
1702
  this.#stopListeners.clear();
1703
- this.#stopLoadingAnimation();
1703
+ this.clearLoadingAnimation();
1704
1704
  this.#suspendedActivityIndicator = undefined;
1705
1705
  this.#petProtocolUnsubscribe?.();
1706
1706
  this.#petProtocolUnsubscribe = undefined;
@@ -1996,7 +1996,8 @@ export class InteractiveMode implements InteractiveModeContext {
1996
1996
  return tallyBackgroundActivity(this.session.getAsyncJobSnapshot()?.running ?? []);
1997
1997
  }
1998
1998
 
1999
- #stopLoadingAnimation(): void {
1999
+ /** Clear the status display without settling the foreground run during maintenance/retry. */
2000
+ clearLoadingAnimation(): void {
2000
2001
  this.loadingAnimation?.stop();
2001
2002
  this.loadingAnimation = undefined;
2002
2003
  this.statusContainer.clear();
@@ -2007,7 +2008,7 @@ export class InteractiveMode implements InteractiveModeContext {
2007
2008
  return;
2008
2009
  const foregroundActive = this.#foregroundActivity || (!this.#foregroundTurnSettled && this.session.isStreaming);
2009
2010
  if (!this.isInitialized && !foregroundActive) {
2010
- this.#stopLoadingAnimation();
2011
+ this.clearLoadingAnimation();
2011
2012
  return;
2012
2013
  }
2013
2014
  const message = resolveActivityIndicatorMessage(
@@ -2016,7 +2017,7 @@ export class InteractiveMode implements InteractiveModeContext {
2016
2017
  this.#pendingWorkingMessage ?? this.#defaultWorkingMessage,
2017
2018
  );
2018
2019
  if (!message) {
2019
- this.#stopLoadingAnimation();
2020
+ this.clearLoadingAnimation();
2020
2021
  return;
2021
2022
  }
2022
2023
  if (!this.loadingAnimation) {
@@ -2047,7 +2048,7 @@ export class InteractiveMode implements InteractiveModeContext {
2047
2048
  this.#foregroundActivity = false;
2048
2049
  if (options?.foregroundSettled) this.#foregroundTurnSettled = true;
2049
2050
  if (options?.restoreBackground === false) {
2050
- this.#stopLoadingAnimation();
2051
+ this.clearLoadingAnimation();
2051
2052
  return;
2052
2053
  }
2053
2054
  this.syncActivityIndicator();
@@ -10,6 +10,7 @@ import { type AssistantMessage, type ImageContent, isContextOverflow } from "@ga
10
10
  import { isKnownSinkPeerClosedError, logger, sanitizeText } from "@gajae-code/utils";
11
11
  import { loadSlashCommands } from "../extensibility/slash-commands";
12
12
  import type { AgentSession } from "../session/agent-session";
13
+ import { ManagedAppendIdentityMismatchError } from "../session/internal/managed-session-storage";
13
14
  import { isSilentAbort } from "../session/messages";
14
15
  import { resolveProviderSafetyStopHint } from "../session/provider-safety-stop-hint";
15
16
  import { executeLocalHeadlessBuiltinSlashCommand } from "../slash-commands/builtin-registry";
@@ -388,7 +389,10 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
388
389
  // Observe callback and stream errors from every preceding print-mode write.
389
390
  await stdout.flush();
390
391
  } catch (error) {
391
- failures.push(error);
392
+ if (error instanceof ManagedAppendIdentityMismatchError) {
393
+ process.exitCode = 1;
394
+ await writeStderrAndQuiesce(`${error.operatorMessage}\n`);
395
+ } else failures.push(error);
392
396
  } finally {
393
397
  // The JSON subscriber remains live while disposal emits its final events.
394
398
  try {
@@ -89,23 +89,37 @@ function getSlashCommandPriority(command: SlashCommand | undefined, item: Autoco
89
89
  return ADVANCED_SLASH_COMMAND_PRIORITIES.get(item.value) ?? 0;
90
90
  }
91
91
 
92
+ interface SlashCommandMetadata {
93
+ command: SlashCommand;
94
+ index: number;
95
+ lowerName: string;
96
+ normalizedName: string;
97
+ normalizedDescription: string | undefined;
98
+ }
99
+
100
+ interface SkillCommandMetadata {
101
+ command: SlashCommand;
102
+ lowerSkillName: string;
103
+ searchTargets: string[];
104
+ collisionSearchTargets: string[];
105
+ }
106
+
92
107
  function sortSlashCommandSuggestions(
93
108
  suggestions: { items: AutocompleteItem[]; prefix: string } | null,
94
- commands: SlashCommand[],
109
+ commandMetadata: ReadonlyMap<string, SlashCommandMetadata>,
95
110
  ): { items: AutocompleteItem[]; prefix: string } | null {
96
111
  if (!suggestions) return null;
97
112
  const query = suggestions.prefix.slice(1).toLowerCase();
98
113
  const normalizedQuery = normalizeFuzzyText(query);
99
- const commandIndexes = new Map(commands.map((command, index) => [command.name, index]));
100
- const commandByName = new Map(commands.map(command => [command.name, command]));
101
114
  const items = suggestions.items
102
115
  .map((item, index) => {
103
- const command = commandByName.get(item.value);
104
- const commandIndex = commandIndexes.get(item.value) ?? index;
105
- const lowerName = item.value.toLowerCase();
106
- const lowerDesc = command?.description?.toLowerCase() ?? item.description?.toLowerCase() ?? "";
107
- const normalizedName = normalizeFuzzyText(item.value);
108
- const normalizedDesc = normalizeFuzzyText(lowerDesc);
116
+ const metadata = commandMetadata.get(item.value);
117
+ const command = metadata?.command;
118
+ const commandIndex = metadata?.index ?? index;
119
+ const lowerName = metadata?.lowerName ?? item.value.toLowerCase();
120
+ const normalizedName = metadata?.normalizedName ?? normalizeFuzzyText(item.value);
121
+ const normalizedDesc =
122
+ metadata?.normalizedDescription ?? normalizeFuzzyText(item.description?.toLowerCase() ?? "");
109
123
  const nameScore = autocompleteFuzzyMatch(normalizedQuery, normalizedName)
110
124
  ? autocompleteFuzzyScore(normalizedQuery, normalizedName)
111
125
  : 0;
@@ -180,7 +194,8 @@ function isSlashTokenInsideInlineCodeSpan(
180
194
  export class PromptActionAutocompleteProvider implements AutocompleteProvider {
181
195
  #baseProvider: CombinedAutocompleteProvider;
182
196
  #actions: PromptActionDefinition[];
183
- #commands: SlashCommand[];
197
+ #commandMetadata = new Map<string, SlashCommandMetadata>();
198
+ #skillCommandMetadata: SkillCommandMetadata[] = [];
184
199
  #getPromptSuggestion: (() => string | null) | undefined;
185
200
 
186
201
  constructor(
@@ -191,7 +206,28 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
191
206
  ) {
192
207
  this.#baseProvider = new CombinedAutocompleteProvider(commands, basePath);
193
208
  this.#actions = actions;
194
- this.#commands = commands;
209
+ for (const [index, command] of commands.entries()) {
210
+ this.#commandMetadata.set(command.name, {
211
+ command,
212
+ index,
213
+ lowerName: command.name.toLowerCase(),
214
+ normalizedName: normalizeFuzzyText(command.name),
215
+ normalizedDescription:
216
+ command.description === undefined ? undefined : normalizeFuzzyText(command.description.toLowerCase()),
217
+ });
218
+ if (command.name.startsWith("skill:")) {
219
+ const skillName = command.name.slice("skill:".length);
220
+ const name = normalizeFuzzyText(command.name);
221
+ const alias = normalizeFuzzyText(`skill-${skillName}`);
222
+ const description = normalizeFuzzyText(command.description ?? "");
223
+ this.#skillCommandMetadata.push({
224
+ command,
225
+ lowerSkillName: skillName.toLowerCase(),
226
+ searchTargets: [name, alias, normalizeFuzzyText(skillName), description],
227
+ collisionSearchTargets: [name, alias, description],
228
+ });
229
+ }
230
+ }
195
231
  this.#getPromptSuggestion = getPromptSuggestion;
196
232
  }
197
233
 
@@ -244,7 +280,7 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
244
280
  });
245
281
  return sortSlashCommandSuggestions(
246
282
  mergeAutocompleteSuggestions(baseSuggestions, skillCommandSuggestions),
247
- this.#commands,
283
+ this.#commandMetadata,
248
284
  );
249
285
  }
250
286
  if (rawSlashPrefix && slashTokenInsideCode) {
@@ -334,7 +370,7 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
334
370
  const skillCommandSuggestions = this.#getSkillCommandSuggestions(textBeforeCursor, { includeEmpty: false });
335
371
  return sortSlashCommandSuggestions(
336
372
  mergeAutocompleteSuggestions(baseSuggestions, skillCommandSuggestions),
337
- this.#commands,
373
+ this.#commandMetadata,
338
374
  );
339
375
  }
340
376
  trySyncInlineReplace(textBeforeCursor: string): { replaceLen: number; insert: string } | null {
@@ -354,20 +390,11 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
354
390
  query.startsWith("skill-") ? `skill:${query.slice("skill-".length)}` : query,
355
391
  );
356
392
  if (query.length > 0 && normalizedQuery.length === 0) return null;
357
- const exactNonSkillCommand = this.#commands.some(
358
- command => command.name === query && !command.name.startsWith("skill:"),
359
- );
360
- const items = this.#commands
361
- .filter(command => command.name.startsWith("skill:"))
362
- .map(command => {
363
- const skillName = command.name.slice("skill:".length);
364
- if (exactNonSkillCommand && query === skillName.toLowerCase()) return null;
365
- const searchTargets = [
366
- command.name,
367
- `skill-${skillName}`,
368
- ...(exactNonSkillCommand ? [] : [skillName]),
369
- command.description ?? "",
370
- ].map(target => normalizeFuzzyText(target));
393
+ const exactNonSkillCommand = this.#commandMetadata.has(query) && !query.startsWith("skill:");
394
+ const items = this.#skillCommandMetadata
395
+ .map(({ command, lowerSkillName, searchTargets: allSearchTargets, collisionSearchTargets }) => {
396
+ if (exactNonSkillCommand && query === lowerSkillName) return null;
397
+ const searchTargets = exactNonSkillCommand ? collisionSearchTargets : allSearchTargets;
371
398
  if (
372
399
  !searchTargets.some(
373
400
  target => autocompleteFuzzyMatch(normalizedQuery, target) || target.includes(normalizedQuery),
@@ -201,6 +201,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
201
201
  },
202
202
  getJobs: () => session.getAsyncJobSnapshot(),
203
203
  onJobFold: listener => session.onJobFold(listener),
204
+ onSessionEvent: listener => session.subscribe(listener),
204
205
  setSdkPermissionProvider: provider => session.setSdkPermissionProvider(provider),
205
206
  setSdkClientBridge: bridge => session.setClientBridge(bridge),
206
207
  sdkControl: async (operation, input) => {
@@ -325,6 +326,7 @@ export async function initializeExtensions(session: AgentSession, options: Initi
325
326
  const result = await session.executeBash(input.cmd, undefined, { excludeFromContext: true });
326
327
  return {
327
328
  exitCode: result.exitCode,
329
+ signal: result.signal,
328
330
  cancelled: result.cancelled,
329
331
  output: result.output,
330
332
  truncated: result.truncated,
@@ -74,6 +74,20 @@ export type ActivityIndicatorStopOptions = Readonly<{
74
74
  foregroundSettled?: boolean;
75
75
  }>;
76
76
 
77
+ export function clearInteractiveActivityIndicator(ctx: {
78
+ loadingAnimation?: Loader;
79
+ statusContainer?: PartialActivityStatusContainer;
80
+ clearLoadingAnimation?: () => void;
81
+ }): void {
82
+ if (ctx.clearLoadingAnimation) {
83
+ ctx.clearLoadingAnimation();
84
+ return;
85
+ }
86
+ ctx.loadingAnimation?.stop();
87
+ ctx.loadingAnimation = undefined;
88
+ ctx.statusContainer?.clear?.();
89
+ }
90
+
77
91
  export function stopInteractiveActivityIndicator(
78
92
  ctx: {
79
93
  loadingAnimation?: Loader;
@@ -86,9 +100,7 @@ export function stopInteractiveActivityIndicator(
86
100
  ctx.stopLoadingAnimation(options);
87
101
  return;
88
102
  }
89
- ctx.loadingAnimation?.stop();
90
- ctx.loadingAnimation = undefined;
91
- ctx.statusContainer?.clear?.();
103
+ clearInteractiveActivityIndicator(ctx);
92
104
  }
93
105
 
94
106
  export function clearInteractiveActivityLoaders(
@@ -285,6 +297,7 @@ export interface InteractiveModeContext {
285
297
  setWorkingMessage(message?: string): void;
286
298
  applyPendingWorkingMessage(): void;
287
299
  ensureLoadingAnimation(): void;
300
+ clearLoadingAnimation(): void;
288
301
  syncActivityIndicator(): void;
289
302
  suspendActivityIndicator(): () => void;
290
303
  stopLoadingAnimation(options?: ActivityIndicatorStopOptions): void;
@@ -530,6 +530,7 @@ export class UiHelpers {
530
530
  component.appendOutput(message.output);
531
531
  }
532
532
  component.setComplete(message.exitCode, message.cancelled, {
533
+ signal: message.signal,
533
534
  truncation: message.meta?.truncation,
534
535
  });
535
536
  addChatChild(this.ctx, component);
@@ -15,6 +15,7 @@ Inspect selected subagents by `ids`; omit `ids` to inspect current running subag
15
15
  ## `action: "await"`
16
16
  Wait for selected subagents by `ids`; omit `ids` to wait for current running subagents.
17
17
  - Always set `timeout_ms` when the result is not immediately required forever.
18
+ - `heartbeat_ms` accepts integer intervals from 100 to 60,000 milliseconds (for example, 10,000 for ten seconds); `0` disables heartbeat updates and omission retains the 500 ms default. Terminal completion is event-driven, not delayed until the next heartbeat.
18
19
  - Await timeout only bounds this tool call's wait; it does not stop the subagent and is not a failure reason.
19
20
  - On timeout, inspect progress and keep doing independent work. Never cancel just because an await timed out; cancel only if the subagent has actually failed, gone off-track, or become unrecoverably wrong.
20
21
  - Completed results are receipt-first by default: bounded preview plus `agent://<id>` output ref when available, not full retained output.