@gajae-code/coding-agent 0.16.3 → 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 (277) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/args.d.ts +2 -2
  4. package/dist/types/cli/customize-doctor.d.ts +22 -0
  5. package/dist/types/cli/root-flags.d.ts +4 -2
  6. package/dist/types/cli.d.ts +1 -1
  7. package/dist/types/commands/launch.d.ts +1 -1
  8. package/dist/types/config/autorouting-tier-map.d.ts +3 -0
  9. package/dist/types/config/file-lock.d.ts +31 -3
  10. package/dist/types/config/model-registry.d.ts +4 -0
  11. package/dist/types/config/model-resolver.d.ts +9 -0
  12. package/dist/types/config/settings-schema.d.ts +29 -1
  13. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  14. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  15. package/dist/types/discovery/claude-plugins.d.ts +3 -1
  16. package/dist/types/discovery/helpers.d.ts +35 -0
  17. package/dist/types/exec/bash-executor.d.ts +17 -1
  18. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  19. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  20. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  21. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  22. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  23. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  24. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  25. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  26. package/dist/types/exec/isolated-shell.d.ts +19 -0
  27. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  28. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  29. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  30. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  31. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  32. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  33. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  34. package/dist/types/extensibility/gjc-plugins/mcp-policy.d.ts +16 -0
  35. package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -0
  36. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  37. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  38. package/dist/types/extensibility/runtime-skill-discovery.d.ts +5 -2
  39. package/dist/types/extensibility/shared-events.d.ts +1 -1
  40. package/dist/types/extensibility/skill-management.d.ts +9 -4
  41. package/dist/types/extensibility/skills.d.ts +2 -0
  42. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  43. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  44. package/dist/types/gjc-runtime/session-state-lock.d.ts +14 -0
  45. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  46. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  47. package/dist/types/modes/components/assistant-message.d.ts +1 -2
  48. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  49. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  50. package/dist/types/modes/components/settings-selector.d.ts +6 -1
  51. package/dist/types/modes/components/status-line/command.d.ts +35 -0
  52. package/dist/types/modes/components/status-line/index.d.ts +1 -0
  53. package/dist/types/modes/components/status-line/types.d.ts +5 -0
  54. package/dist/types/modes/components/tool-status-header.d.ts +14 -0
  55. package/dist/types/modes/controllers/event-controller.d.ts +6 -0
  56. package/dist/types/modes/interactive-mode.d.ts +3 -0
  57. package/dist/types/modes/types.d.ts +7 -0
  58. package/dist/types/runtime-mcp/client.d.ts +9 -1
  59. package/dist/types/runtime-mcp/manager.d.ts +7 -1
  60. package/dist/types/runtime-mcp/pool.d.ts +22 -3
  61. package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
  62. package/dist/types/runtime-mcp/types.d.ts +22 -0
  63. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  64. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  65. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  66. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  67. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  68. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +12 -0
  69. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  70. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  71. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  72. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  73. package/dist/types/sdk/bus/slack-live-provider.d.ts +6 -0
  74. package/dist/types/sdk/bus/slack-provider.d.ts +12 -0
  75. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  76. package/dist/types/sdk/client/client.d.ts +6 -0
  77. package/dist/types/sdk/host/host.d.ts +3 -1
  78. package/dist/types/sdk/host/session-runtime.d.ts +26 -1
  79. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  80. package/dist/types/sdk/router/session-router.d.ts +10 -2
  81. package/dist/types/sdk/session-api-key.d.ts +26 -0
  82. package/dist/types/sdk/session.d.ts +5 -1
  83. package/dist/types/sdk/transport/serve-cli.d.ts +1 -1
  84. package/dist/types/session/agent-session.d.ts +11 -0
  85. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  86. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  87. package/dist/types/session/messages.d.ts +1 -0
  88. package/dist/types/session/session-manager.d.ts +3 -1
  89. package/dist/types/setup/paseo/announce.d.ts +112 -0
  90. package/dist/types/setup/paseo/setup-deps.d.ts +2 -1
  91. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  92. package/dist/types/system-prompt.d.ts +10 -0
  93. package/dist/types/task/executor.d.ts +5 -1
  94. package/dist/types/tools/bash.d.ts +1 -0
  95. package/dist/types/tools/fetch.d.ts +7 -0
  96. package/dist/types/tools/github-cache.d.ts +2 -1
  97. package/dist/types/tools/index.d.ts +10 -0
  98. package/dist/types/tools/path-utils.d.ts +2 -0
  99. package/dist/types/tools/search.d.ts +6 -0
  100. package/package.json +9 -7
  101. package/scripts/capture-status-line-custom-editor-showcase.ts +151 -0
  102. package/scripts/generate-sdk-operation-inventory.ts +11 -0
  103. package/scripts/verify-status-line-custom-editor-showcase.ts +309 -0
  104. package/src/capability/index.ts +14 -2
  105. package/src/capability/types.ts +4 -0
  106. package/src/cli/args.ts +20 -9
  107. package/src/cli/customize-doctor.ts +155 -12
  108. package/src/cli/root-flags.ts +5 -3
  109. package/src/cli.ts +22 -0
  110. package/src/commands/sdk.ts +74 -31
  111. package/src/config/autorouting-tier-map.ts +1 -0
  112. package/src/config/file-lock-gc.ts +40 -2
  113. package/src/config/file-lock.ts +379 -31
  114. package/src/config/model-preset-registry.ts +120 -19
  115. package/src/config/model-profiles.ts +40 -0
  116. package/src/config/model-registry.ts +73 -23
  117. package/src/config/model-resolver.ts +48 -0
  118. package/src/config/settings-schema.ts +33 -1
  119. package/src/config/settings.ts +1 -0
  120. package/src/coordinator-mcp/question-state.ts +33 -2
  121. package/src/coordinator-mcp/server.ts +477 -101
  122. package/src/crash/sanitize.ts +1 -1
  123. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +11 -11
  124. package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +13 -2
  125. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  126. package/src/discovery/builtin.ts +52 -55
  127. package/src/discovery/claude-plugins.ts +12 -4
  128. package/src/discovery/helpers.ts +307 -34
  129. package/src/exec/bash-executor.ts +330 -50
  130. package/src/exec/bash-shell-guardian.ts +388 -0
  131. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  132. package/src/exec/bash-shell-signals.ts +29 -0
  133. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  134. package/src/exec/bash-shell-supervisor.ts +359 -0
  135. package/src/exec/bash-shell-worker-entry.ts +5 -0
  136. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  137. package/src/exec/bash-shell-worker.ts +116 -0
  138. package/src/exec/isolated-shell.ts +393 -0
  139. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  140. package/src/extensibility/extensions/function-hooks.ts +959 -0
  141. package/src/extensibility/extensions/index.ts +22 -0
  142. package/src/extensibility/extensions/loader.ts +53 -2
  143. package/src/extensibility/extensions/runner.ts +895 -264
  144. package/src/extensibility/extensions/types.ts +42 -0
  145. package/src/extensibility/extensions/wrapper.ts +56 -15
  146. package/src/extensibility/gjc-plugins/compiler.ts +228 -60
  147. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  148. package/src/extensibility/gjc-plugins/installer.ts +13 -12
  149. package/src/extensibility/gjc-plugins/mcp-policy.ts +81 -52
  150. package/src/extensibility/gjc-plugins/paths.ts +13 -0
  151. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  152. package/src/extensibility/gjc-plugins/runtime-adapters.ts +758 -16
  153. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  154. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  155. package/src/extensibility/gjc-plugins/types.ts +25 -0
  156. package/src/extensibility/hooks/loader.ts +133 -16
  157. package/src/extensibility/hooks/types.ts +28 -0
  158. package/src/extensibility/runtime-skill-discovery.ts +149 -190
  159. package/src/extensibility/shared-events.ts +1 -1
  160. package/src/extensibility/skill-management.ts +200 -52
  161. package/src/extensibility/skills.ts +73 -25
  162. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  163. package/src/gjc-runtime/gc-runtime.ts +29 -0
  164. package/src/gjc-runtime/linux-proc.ts +14 -0
  165. package/src/gjc-runtime/repository-binding.ts +1 -1
  166. package/src/gjc-runtime/session-state-lock.ts +143 -10
  167. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  168. package/src/gjc-runtime/state-writer.ts +30 -29
  169. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  170. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  171. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  172. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  173. package/src/harness-control-plane/session-lease.ts +2 -2
  174. package/src/internal-urls/docs-index.generated.ts +12 -12
  175. package/src/lsp/config.ts +10 -3
  176. package/src/lsp/index.ts +1 -1
  177. package/src/lsp/path-trust.ts +69 -14
  178. package/src/main.ts +78 -35
  179. package/src/modes/DESIGN.md +54 -0
  180. package/src/modes/acp/acp-agent.ts +16 -3
  181. package/src/modes/components/assistant-message.ts +22 -9
  182. package/src/modes/components/bash-execution.ts +4 -1
  183. package/src/modes/components/execution-shared.ts +4 -1
  184. package/src/modes/components/model-selector.ts +166 -33
  185. package/src/modes/components/session-observer-overlay.ts +1 -1
  186. package/src/modes/components/settings-selector.ts +1075 -235
  187. package/src/modes/components/status-line/command.ts +190 -0
  188. package/src/modes/components/status-line/index.ts +1 -0
  189. package/src/modes/components/status-line/segments.ts +32 -5
  190. package/src/modes/components/status-line/types.ts +5 -0
  191. package/src/modes/components/tool-status-header.ts +396 -22
  192. package/src/modes/controllers/command-controller.ts +3 -0
  193. package/src/modes/controllers/event-controller.ts +151 -7
  194. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  195. package/src/modes/controllers/input-controller.ts +6 -3
  196. package/src/modes/controllers/selector-controller.ts +21 -0
  197. package/src/modes/interactive-mode.ts +53 -6
  198. package/src/modes/print-mode.ts +5 -1
  199. package/src/modes/prompt-action-autocomplete.ts +54 -27
  200. package/src/modes/runtime-init.ts +2 -0
  201. package/src/modes/types.ts +17 -3
  202. package/src/modes/utils/ui-helpers.ts +2 -0
  203. package/src/prompts/system/system-prompt.md +8 -0
  204. package/src/prompts/tools/search.md +2 -0
  205. package/src/prompts/tools/subagent.md +1 -0
  206. package/src/runtime-mcp/client.ts +60 -17
  207. package/src/runtime-mcp/manager.ts +189 -14
  208. package/src/runtime-mcp/pool.test.ts +218 -0
  209. package/src/runtime-mcp/pool.ts +224 -23
  210. package/src/runtime-mcp/transports/stdio.ts +158 -24
  211. package/src/runtime-mcp/types.ts +24 -0
  212. package/src/sdk/broker/broker.ts +124 -32
  213. package/src/sdk/broker/ensure.ts +248 -41
  214. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  215. package/src/sdk/broker/lifecycle.ts +56 -6
  216. package/src/sdk/broker/startup-failure.ts +15 -6
  217. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  218. package/src/sdk/bus/chat-daemon-runtime.ts +72 -6
  219. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  220. package/src/sdk/bus/conversation-store.ts +29 -0
  221. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  222. package/src/sdk/bus/slack-conversation.ts +1 -0
  223. package/src/sdk/bus/slack-daemon.ts +478 -49
  224. package/src/sdk/bus/slack-live-provider.ts +12 -0
  225. package/src/sdk/bus/slack-provider.ts +5 -0
  226. package/src/sdk/bus/telegram-daemon.ts +16 -2
  227. package/src/sdk/bus/topic-registry.ts +19 -0
  228. package/src/sdk/cli/master-cli.ts +1 -1
  229. package/src/sdk/cli/session-cli.ts +11 -8
  230. package/src/sdk/client/client.ts +66 -17
  231. package/src/sdk/host/host.ts +29 -9
  232. package/src/sdk/host/query/revision-store.ts +19 -13
  233. package/src/sdk/host/reverse-leases.ts +7 -4
  234. package/src/sdk/host/session-runtime.test.ts +52 -10
  235. package/src/sdk/host/session-runtime.ts +383 -44
  236. package/src/sdk/lifecycle/service.ts +4 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +77 -0
  238. package/src/sdk/router/session-router.ts +89 -38
  239. package/src/sdk/session-api-key.ts +94 -0
  240. package/src/sdk/session.ts +228 -55
  241. package/src/sdk/transport/relay.ts +35 -13
  242. package/src/sdk/transport/serve-cli.ts +3 -3
  243. package/src/session/agent-session.ts +240 -90
  244. package/src/session/contribution-prep.ts +214 -21
  245. package/src/session/fallback-chain-controller.ts +38 -0
  246. package/src/session/internal/managed-session-storage.ts +19 -5
  247. package/src/session/messages.ts +5 -0
  248. package/src/session/session-manager.ts +29 -5
  249. package/src/session-import/redact.ts +5 -1
  250. package/src/setup/paseo/announce.ts +456 -0
  251. package/src/setup/paseo/setup-deps.ts +15 -3
  252. package/src/skill-state/canonical-skills.ts +12 -0
  253. package/src/skills/skills.test.ts +41 -21
  254. package/src/slash-commands/builtin-registry.ts +1 -0
  255. package/src/system-prompt.ts +29 -3
  256. package/src/task/executor.ts +7 -1
  257. package/src/task/index.ts +21 -4
  258. package/src/tools/bash.ts +63 -19
  259. package/src/tools/cron.ts +1 -1
  260. package/src/tools/fetch.ts +29 -1
  261. package/src/tools/gh.ts +26 -3
  262. package/src/tools/github-cache.ts +68 -16
  263. package/src/tools/index.ts +10 -0
  264. package/src/tools/job.ts +21 -4
  265. package/src/tools/monitor.ts +8 -2
  266. package/src/tools/path-utils.ts +7 -0
  267. package/src/tools/read.ts +14 -2
  268. package/src/tools/render-utils.ts +1 -3
  269. package/src/tools/resource-gc.ts +28 -8
  270. package/src/tools/search.ts +116 -58
  271. package/src/tools/skill-discovery.ts +5 -0
  272. package/src/tools/skill.ts +37 -2
  273. package/src/tools/subagent.ts +6 -3
  274. package/src/tools/tool-catalog.generated.ts +10 -3
  275. package/src/tools/ultragoal-ask-guard.ts +2 -2
  276. package/src/web/insane/bridge.ts +113 -23
  277. package/src/web/search/providers/perplexity.ts +102 -41
@@ -1,8 +1,9 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
+ import { processIncarnation } from "./process-incarnation";
3
4
 
4
5
  export interface BrokerStartupFailureMarker {
5
- version: 1;
6
+ version: 2;
6
7
  reason: string;
7
8
  exitCode: number | null;
8
9
  signal: string | null;
@@ -16,6 +17,8 @@ export interface BrokerStartupFailureMarker {
16
17
  * failure.
17
18
  */
18
19
  pid: number;
20
+ /** Kernel-derived identity for the exact process incarnation that wrote the marker. */
21
+ incarnation: string;
19
22
  }
20
23
 
21
24
  const BROKER_STARTUP_FAILURE_FILE = "broker.startup-failure.json";
@@ -30,14 +33,16 @@ function boundedMarker(
30
33
  exitCode: number | null,
31
34
  signal: string | null,
32
35
  pid: number,
36
+ incarnation: string,
33
37
  ): BrokerStartupFailureMarker {
34
38
  return {
35
- version: 1,
39
+ version: 2,
36
40
  reason: reason.slice(0, MAX_BROKER_STARTUP_FAILURE_REASON),
37
41
  exitCode,
38
42
  signal,
39
43
  writtenAt: Date.now(),
40
44
  pid,
45
+ incarnation,
41
46
  };
42
47
  }
43
48
 
@@ -49,13 +54,15 @@ function boundedMarker(
49
54
  */
50
55
  export async function writeBrokerStartupFailureMarker(
51
56
  agentDir: string,
52
- failure: { reason: string; exitCode: number | null; signal: string | null; pid: number },
57
+ failure: { reason: string; exitCode: number | null; signal: string | null; pid: number; incarnation?: string },
53
58
  ): Promise<void> {
54
59
  try {
60
+ const incarnation = failure.incarnation ?? processIncarnation(failure.pid);
61
+ if (!incarnation) return;
55
62
  await fs.mkdir(path.dirname(brokerStartupFailurePath(agentDir)), { recursive: true, mode: 0o700 });
56
63
  await Bun.write(
57
64
  brokerStartupFailurePath(agentDir),
58
- JSON.stringify(boundedMarker(failure.reason, failure.exitCode, failure.signal, failure.pid)),
65
+ JSON.stringify(boundedMarker(failure.reason, failure.exitCode, failure.signal, failure.pid, incarnation)),
59
66
  );
60
67
  } catch {
61
68
  // Best-effort only.
@@ -74,7 +81,7 @@ export async function readBrokerStartupFailureMarker(
74
81
  if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
75
82
  const marker = value as Partial<BrokerStartupFailureMarker>;
76
83
  if (
77
- marker.version !== 1 ||
84
+ marker.version !== 2 ||
78
85
  typeof marker.reason !== "string" ||
79
86
  marker.reason.length === 0 ||
80
87
  (marker.exitCode !== null && typeof marker.exitCode !== "number") ||
@@ -82,7 +89,9 @@ export async function readBrokerStartupFailureMarker(
82
89
  typeof marker.writtenAt !== "number" ||
83
90
  typeof marker.pid !== "number" ||
84
91
  !Number.isInteger(marker.pid) ||
85
- marker.pid <= 0
92
+ marker.pid <= 0 ||
93
+ typeof marker.incarnation !== "string" ||
94
+ marker.incarnation.length === 0
86
95
  )
87
96
  return undefined;
88
97
  return marker as BrokerStartupFailureMarker;
@@ -133,10 +133,12 @@ export type ChatDaemonAction = "stop" | "reload";
133
133
  * Discord generation 72 / Slack generation 75 serialize same-generation
134
134
  * successor attachment admission behind predecessor provider retirement, so a
135
135
  * pre-upgrade owner cannot retain the queue race fixed by #5120.
136
+ * Slack generation 80 persists and fences the inbound SDK dispatch boundary so
137
+ * crashes, recovery, and attachment retirement cannot replay ambiguous work.
136
138
  */
137
139
  export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
138
140
  discord: 73,
139
- slack: 77,
141
+ slack: 80,
140
142
  };
141
143
 
142
144
  export function chatDaemonGeneration(kind: ChatDaemonKind): number {
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { logger } from "@gajae-code/utils";
3
- import { SdkClientError } from "../client/client";
3
+ import { SdkClientError, SdkPreparedDispatchError } from "../client/client";
4
4
  import { SESSION_PREPARED_EVENT } from "../host/host";
5
5
 
6
6
  import type { SessionRouterDeps } from "../router";
@@ -50,6 +50,7 @@ function chatDeliveryPhase(error: unknown): ChatDeliveryPhase | undefined {
50
50
  if (error instanceof SessionRouterError) return error.phase;
51
51
 
52
52
  if (error instanceof ChatDeliveryError) return error.phase;
53
+ if (error instanceof SdkPreparedDispatchError) return "pre_send";
53
54
  if (!(error instanceof SdkClientError)) return undefined;
54
55
  // `uncertain_after_send` is the one code that states outright that the frame
55
56
  // reached the host: reporting it as a definite failure would tell the operator a
@@ -92,6 +93,36 @@ function isReservedIdentity(name: string | undefined): boolean {
92
93
  return isLifecycleEvent(name) || isControlPlaneFrameType(name);
93
94
  }
94
95
 
96
+ /**
97
+ * Chat notifications in lean mode are an answer surface, not a mirror of the
98
+ * session protocol. Lifecycle, identity, and activity frames remain available
99
+ * to verbose consumers, while finalized answers and action-needed events are
100
+ * delivered in lean mode.
101
+ */
102
+ export function shouldPublishChatFrame(
103
+ frame: Record<string, unknown>,
104
+ verbosity: "lean" | "verbose" | undefined,
105
+ ): boolean {
106
+ if (verbosity !== "lean") return frame.type !== "turn_stream" || frame.phase !== "live";
107
+ return frame.type === "turn_stream" && frame.phase === "finalized" && frame.finalAnswer === true;
108
+ }
109
+
110
+ /**
111
+ * A final answer's message ref identifies its one rendered message even when
112
+ * the SDK transport delivers an unpositioned duplicate frame.
113
+ */
114
+ export function publicationIdForFinalChatAnswer(sessionId: string, frame: Record<string, unknown>): string | undefined {
115
+ if (
116
+ frame.type !== "turn_stream" ||
117
+ frame.phase !== "finalized" ||
118
+ frame.finalAnswer !== true ||
119
+ typeof frame.messageRef !== "string" ||
120
+ frame.messageRef.length === 0
121
+ )
122
+ return undefined;
123
+ return `turn:${sessionId}:${frame.messageRef}`;
124
+ }
125
+
95
126
  /** One delivered frame reduced to a single event identity. */
96
127
  type CorrelatedFrame = SessionRouterFrame;
97
128
 
@@ -384,8 +415,16 @@ export class ChatDaemonRuntime {
384
415
  authorizeActor: async actorId => config.authorizedUserId === actorId,
385
416
  resolveAttachment: async sessionId => this.#router.attachment(sessionId),
386
417
  resolveBindingAuthority: async sessionId => await this.#router.bindingAuthority(sessionId),
387
- onCommand: async (sessionId, content, attachment, idempotencyKey) =>
388
- await this.#runChatCommand("slack", sessionId, content, attachment, idempotencyKey),
418
+ onCommand: async (sessionId, content, attachment, idempotencyKey, beforeDispatch, dispatchFence) =>
419
+ await this.#runChatCommand(
420
+ "slack",
421
+ sessionId,
422
+ content,
423
+ attachment,
424
+ idempotencyKey,
425
+ beforeDispatch,
426
+ dispatchFence,
427
+ ),
389
428
  });
390
429
  }
391
430
  try {
@@ -571,8 +610,9 @@ export class ChatDaemonRuntime {
571
610
  if (this.#attachments.get(attachment.sessionId) !== attachment) return;
572
611
  await this.#attachmentBarriers.get(attachment.sessionId);
573
612
  if (this.#attachments.get(attachment.sessionId) !== attachment) return;
574
- const publicationId = correlated.publicationId;
575
613
  const normalizedFrame = correlated.body;
614
+ const publicationId =
615
+ publicationIdForFinalChatAnswer(attachment.sessionId, normalizedFrame) ?? correlated.publicationId;
576
616
  const bodyType = typeof normalizedFrame.type === "string" ? normalizedFrame.type : undefined;
577
617
  if (isControlPlaneFrameType(correlated.name) || isControlPlaneFrameType(bodyType)) return;
578
618
  if (normalizedFrame.type === "turn_stream" && normalizedFrame.phase === "live") return;
@@ -607,6 +647,11 @@ export class ChatDaemonRuntime {
607
647
  return;
608
648
  }
609
649
  if (!notification) return;
650
+ if (
651
+ notification.type === "frame" &&
652
+ !shouldPublishChatFrame(normalizedFrame, this.input.config.presentation?.verbosity)
653
+ )
654
+ return;
610
655
  const payload = this.#presentation?.fanout(notification)[0];
611
656
  const body = payload?.body;
612
657
  const content =
@@ -694,6 +739,8 @@ export class ChatDaemonRuntime {
694
739
  content: string,
695
740
  expectedAttachment: SessionAttachment,
696
741
  idempotencyKey: string = randomUUID(),
742
+ beforeDispatch?: () => void,
743
+ dispatchFence?: <T>(dispatch: () => Promise<T>) => Promise<T>,
697
744
  ): Promise<boolean> {
698
745
  const match = /^\/sdk\s+(control|query|global)\s+([^\s]+)(?:\s+(.+))?\s*$/.exec(content);
699
746
  if (!match) return false;
@@ -712,7 +759,13 @@ export class ChatDaemonRuntime {
712
759
  try {
713
760
  outcome = await sendAuthorizedChatOperation(transport, { kind, operation, input }, async () => {
714
761
  if (kind === "global")
715
- return await this.#runGlobalCommand(operation, input as Record<string, unknown>, idempotencyKey);
762
+ return await this.#runGlobalCommand(
763
+ operation,
764
+ input as Record<string, unknown>,
765
+ idempotencyKey,
766
+ beforeDispatch,
767
+ dispatchFence,
768
+ );
716
769
  if (!expectedAttachment.isCurrent()) throw new ChatDeliveryError("pre_send");
717
770
  return await this.#router.request(
718
771
  sessionId,
@@ -721,6 +774,12 @@ export class ChatDaemonRuntime {
721
774
  : { type: "query_request", query: operation, input, idempotencyKey },
722
775
  expectedAttachment.generation,
723
776
  expectedAttachment,
777
+ beforeDispatch || dispatchFence
778
+ ? {
779
+ beforeDispatch,
780
+ dispatchFence: dispatchFence ? dispatch => dispatchFence(dispatch) : undefined,
781
+ }
782
+ : undefined,
724
783
  );
725
784
  });
726
785
  } catch (error) {
@@ -740,10 +799,17 @@ export class ChatDaemonRuntime {
740
799
  operation: string,
741
800
  input: Record<string, unknown>,
742
801
  idempotencyKey: string,
802
+ beforeDispatch?: () => void,
803
+ dispatchFence?: <T>(dispatch: () => Promise<T>) => Promise<T>,
743
804
  ): Promise<Record<string, unknown>> {
744
805
  if (operation !== "session.list") throw new ChatDeliveryError("pre_send");
745
806
  try {
746
- return await this.#router.listBrokerSessions(input, idempotencyKey);
807
+ return await this.#router.listBrokerSessions(
808
+ input,
809
+ idempotencyKey,
810
+ { beforeDispatch },
811
+ dispatchFence ? dispatch => dispatchFence(dispatch) : undefined,
812
+ );
747
813
  } catch (error) {
748
814
  if (error instanceof SessionRouterError) throw new ChatDeliveryError(error.phase);
749
815
  throw error;
@@ -14,7 +14,7 @@ export function chatEffectJournalPath(agentDir: string, transport: "discord" | "
14
14
  return conversationStorePath(agentDir, transport, "effects.json");
15
15
  }
16
16
 
17
- export type ChatEffectState = "pending" | "leased" | "accepted" | "uncertain" | "terminal";
17
+ export type ChatEffectState = "pending" | "leased" | "dispatching" | "accepted" | "uncertain" | "terminal";
18
18
 
19
19
  /** Provider receipts are identifiers/status only. Never put request or response bodies here. */
20
20
  export interface ChatEffectReceipt {
@@ -262,7 +262,11 @@ export class ChatEffectJournal {
262
262
  let renewed: ChatEffect<TPayload> | undefined;
263
263
  const now = this.#now();
264
264
  await this.#store.transact(id, current => {
265
- if (current?.state !== "leased" || current.owner !== lease.owner || current.epoch !== lease.epoch)
265
+ if (
266
+ (current?.state !== "leased" && current?.state !== "dispatching") ||
267
+ current.owner !== lease.owner ||
268
+ current.epoch !== lease.epoch
269
+ )
266
270
  return current;
267
271
  renewed = {
268
272
  ...current,
@@ -275,6 +279,78 @@ export class ChatEffectJournal {
275
279
  return renewed;
276
280
  }
277
281
 
282
+ async markDispatching<TPayload = unknown>(
283
+ id: string,
284
+ lease: ChatEffectLease,
285
+ ): Promise<ChatEffect<TPayload> | undefined> {
286
+ let marked: ChatEffect<TPayload> | undefined;
287
+ const now = this.#now();
288
+ await this.#store.transact(id, current => {
289
+ if (current?.state !== "leased" || current.owner !== lease.owner || current.epoch !== lease.epoch)
290
+ return current;
291
+ marked = {
292
+ ...current,
293
+ generation: current.generation + 1,
294
+ state: "dispatching",
295
+ updatedAt: now,
296
+ } as ChatEffect<TPayload>;
297
+ return marked;
298
+ });
299
+ return marked;
300
+ }
301
+
302
+ async ownsDispatching(id: string, lease: ChatEffectLease): Promise<boolean> {
303
+ const current = await this.#store.read(id);
304
+ return (
305
+ current?.state === "dispatching" &&
306
+ current.owner === lease.owner &&
307
+ current.epoch === lease.epoch &&
308
+ (current.leaseExpiresAt ?? 0) > this.#now()
309
+ );
310
+ }
311
+
312
+ /** Hold the durable dispatching lease until the synchronous SDK prewire boundary. */
313
+ async dispatchWithLeaseFence<R>(
314
+ id: string,
315
+ lease: ChatEffectLease,
316
+ dispatch: () => Promise<R>,
317
+ dispatched: Promise<void>,
318
+ ): Promise<{ value: R } | undefined> {
319
+ return await this.#store.dispatchWithSnapshotFence(id, current => {
320
+ if (
321
+ current?.state !== "dispatching" ||
322
+ current.owner !== lease.owner ||
323
+ current.epoch !== lease.epoch ||
324
+ (current.leaseExpiresAt ?? 0) <= this.#now()
325
+ )
326
+ return undefined;
327
+ return { result: dispatch(), dispatched };
328
+ });
329
+ }
330
+
331
+ async recoverDispatchingAsUncertain<TPayload = unknown>(
332
+ id: string,
333
+ options: { onlyIfExpired?: boolean } = {},
334
+ ): Promise<ChatEffect<TPayload> | undefined> {
335
+ let recovered: ChatEffect<TPayload> | undefined;
336
+ const now = this.#now();
337
+ await this.#store.transact(id, current => {
338
+ if (current?.state !== "dispatching") return current;
339
+ if (options.onlyIfExpired && (current.leaseExpiresAt ?? 0) > now) return current;
340
+ recovered = {
341
+ ...current,
342
+ generation: current.generation + 1,
343
+ state: "uncertain",
344
+ owner: undefined,
345
+ leaseExpiresAt: undefined,
346
+ receipt: { status: "uncertain" },
347
+ updatedAt: now,
348
+ } as ChatEffect<TPayload>;
349
+ return recovered;
350
+ });
351
+ return recovered;
352
+ }
353
+
278
354
  /** Persists provider progress without releasing the owner/epoch fence. */
279
355
  async recordReceipt<TPayload = unknown>(
280
356
  id: string,
@@ -284,7 +360,11 @@ export class ChatEffectJournal {
284
360
  let recorded: ChatEffect<TPayload> | undefined;
285
361
  const now = this.#now();
286
362
  await this.#store.transact(id, current => {
287
- if (current?.state !== "leased" || current.owner !== lease.owner || current.epoch !== lease.epoch)
363
+ if (
364
+ (current?.state !== "leased" && current?.state !== "dispatching") ||
365
+ current.owner !== lease.owner ||
366
+ current.epoch !== lease.epoch
367
+ )
288
368
  return current;
289
369
  recorded = {
290
370
  ...current,
@@ -300,13 +380,17 @@ export class ChatEffectJournal {
300
380
  async record<TPayload = unknown>(
301
381
  id: string,
302
382
  lease: ChatEffectLease,
303
- state: Exclude<ChatEffectState, "pending" | "leased">,
304
- receipt?: ChatEffectReceipt,
383
+ state: "accepted" | "uncertain" | "terminal",
384
+ receipt: ChatEffectReceipt = {},
305
385
  ): Promise<ChatEffect<TPayload> | undefined> {
306
386
  let recorded: ChatEffect<TPayload> | undefined;
307
387
  const now = this.#now();
308
388
  await this.#store.transact(id, current => {
309
- if (current?.state !== "leased" || current.owner !== lease.owner || current.epoch !== lease.epoch)
389
+ if (
390
+ (current?.state !== "leased" && current?.state !== "dispatching") ||
391
+ current.owner !== lease.owner ||
392
+ current.epoch !== lease.epoch
393
+ )
310
394
  return current;
311
395
  recorded = {
312
396
  ...current,
@@ -329,7 +413,10 @@ export class ChatEffectJournal {
329
413
  const now = this.#now();
330
414
  await this.#store.transact(id, current => {
331
415
  if (!current || current.state === "terminal") return current;
332
- if (current.state === "leased" && (!lease || current.owner !== lease.owner || current.epoch !== lease.epoch))
416
+ if (
417
+ (current.state === "leased" || current.state === "dispatching") &&
418
+ (!lease || current.owner !== lease.owner || current.epoch !== lease.epoch)
419
+ )
333
420
  return current;
334
421
 
335
422
  terminalized = {
@@ -347,6 +434,44 @@ export class ChatEffectJournal {
347
434
  return terminalized;
348
435
  }
349
436
 
437
+ /**
438
+ * Terminalize only a decided effect. The uncertainty check and state change
439
+ * share one store transaction so a concurrent sender cannot publish an
440
+ * ambiguity tombstone between a caller's read and stale cleanup.
441
+ */
442
+ async terminalizeDecided(
443
+ id: string,
444
+ receipt: ChatEffectReceipt,
445
+ lease?: ChatEffectLease,
446
+ ): Promise<ChatEffect | undefined> {
447
+ let terminalized: ChatEffect | undefined;
448
+ const now = this.#now();
449
+ await this.#store.transact(id, current => {
450
+ if (!current || current.state === "uncertain") return current;
451
+ if (current.state === "terminal") {
452
+ terminalized = current;
453
+ return current;
454
+ }
455
+ if (
456
+ (current.state === "leased" || current.state === "dispatching") &&
457
+ (!lease || current.owner !== lease.owner || current.epoch !== lease.epoch)
458
+ )
459
+ return current;
460
+ terminalized = {
461
+ ...current,
462
+ generation: current.generation + 1,
463
+ state: "terminal",
464
+ owner: undefined,
465
+ leaseExpiresAt: undefined,
466
+ receipt,
467
+ updatedAt: now,
468
+ };
469
+ return terminalized;
470
+ });
471
+ if (terminalized?.state === "terminal") await this.#pruneTerminal();
472
+ return terminalized;
473
+ }
474
+
350
475
  async #pruneTerminal(): Promise<void> {
351
476
  const terminal = (await this.list())
352
477
  .filter(effect => effect.state === "terminal")
@@ -208,6 +208,35 @@ export class ConversationStore<T extends ConversationRecord> {
208
208
  });
209
209
  }
210
210
 
211
+ /**
212
+ * Hold the cross-process mapping lock until a prepared dispatch either reaches
213
+ * its synchronous wire boundary or fails before sending. The result may remain
214
+ * pending after the lock is released while the remote response is awaited.
215
+ */
216
+ async dispatchWithSnapshotFence<R>(
217
+ key: string,
218
+ prepare: (
219
+ current: T | undefined,
220
+ conversations: Readonly<Record<string, T>>,
221
+ ) => { result: Promise<R>; dispatched: Promise<void> } | undefined,
222
+ ): Promise<{ value: R } | undefined> {
223
+ let result: Promise<R> | undefined;
224
+ await this.#withLock(async () => {
225
+ const document = await this.#readDocument();
226
+ const prepared = prepare(document.conversations[key], Object.freeze({ ...document.conversations }));
227
+ if (!prepared) return;
228
+ result = prepared.result;
229
+ await Promise.race([
230
+ prepared.dispatched,
231
+ prepared.result.then(
232
+ () => undefined,
233
+ () => undefined,
234
+ ),
235
+ ]);
236
+ });
237
+ return result ? { value: await result } : undefined;
238
+ }
239
+
211
240
  async #withLock<R>(operation: () => Promise<R>): Promise<R> {
212
241
  const previous = this.#locks.get(this.#file) ?? Promise.resolve();
213
242
  const gate = Promise.withResolvers<void>();
@@ -15,10 +15,15 @@ import * as os from "node:os";
15
15
  import type { SessionCloseTarget, SessionCreateTarget, SessionLifecycleResponse, SessionResumeTarget } from "./index";
16
16
 
17
17
  export type LifecycleCommandVerb = "session_create" | "session_close" | "session_resume";
18
- function normalizeLifecycleCommandToken(
19
- token: string,
20
- ctx: { chatType?: string; botUsername?: string } = {},
21
- ): string | undefined {
18
+
19
+ export interface LifecycleCommandContext {
20
+ chatType?: string;
21
+ botUsername?: string;
22
+ /** Session durably associated with the Telegram topic containing the command. */
23
+ threadSessionId?: string;
24
+ }
25
+
26
+ function normalizeLifecycleCommandToken(token: string, ctx: LifecycleCommandContext = {}): string | undefined {
22
27
  const at = token.indexOf("@");
23
28
  const command = at === -1 ? token : token.slice(0, at);
24
29
  if (!/^\/session_(create|close|resume|recent)\b/.test(command)) return undefined;
@@ -44,8 +49,8 @@ const USAGE = [
44
49
  "/session_create path <dir> [--mpreset <profile>]",
45
50
  "/session_create worktree <repo> <branch> [--mpreset <profile>]",
46
51
  "/session_create dir <newdir> [--mpreset <profile>]",
47
- "/session_close <sessionId>",
48
- "/session_resume <sessionId|prefix>",
52
+ "/session_close <sessionId> (ID optional inside its session thread)",
53
+ "/session_resume <sessionId|prefix> (ID optional inside its session thread)",
49
54
  "/session_recent [create|resume]",
50
55
  ].join("\n");
51
56
 
@@ -103,10 +108,7 @@ export function isLifecycleCommandLikeText(text: string | undefined): boolean {
103
108
  }
104
109
 
105
110
  /** True when the text begins an addressable /session_* command (cheap pre-gate). */
106
- export function isLifecycleCommandText(
107
- text: string | undefined,
108
- ctx: { chatType?: string; botUsername?: string } = {},
109
- ): boolean {
111
+ export function isLifecycleCommandText(text: string | undefined, ctx: LifecycleCommandContext = {}): boolean {
110
112
  if (!text) return false;
111
113
  const [rawCommand] = text.trim().split(/\s+/, 1);
112
114
  return normalizeLifecycleCommandToken(rawCommand ?? "", ctx) !== undefined;
@@ -139,7 +141,7 @@ function extractModelPreset(args: string[]): { positional: string[]; modelPreset
139
141
  */
140
142
  export function parseLifecycleCommand(
141
143
  text: string | undefined,
142
- ctx: { chatType?: string; botUsername?: string } = {},
144
+ ctx: LifecycleCommandContext = {},
143
145
  ): ParsedLifecycleCommand {
144
146
  const tokens = tokenizeLifecycleCommand((text ?? "").trim());
145
147
  if (!tokens) return { kind: "usage", message: USAGE };
@@ -166,6 +168,8 @@ export function parseLifecycleCommand(
166
168
  }
167
169
 
168
170
  if (command === "/session_close") {
171
+ if (args.length === 0 && ctx.threadSessionId)
172
+ return { kind: "close", target: { sessionId: ctx.threadSessionId } };
169
173
  if (args.length !== 1) return { kind: "usage", message: USAGE };
170
174
  const sessionId = args[0]!;
171
175
  if (!isSafeIdentifier(sessionId)) {
@@ -175,6 +179,8 @@ export function parseLifecycleCommand(
175
179
  }
176
180
 
177
181
  if (command === "/session_resume") {
182
+ if (args.length === 0 && ctx.threadSessionId)
183
+ return { kind: "resume", target: { sessionIdOrPrefix: ctx.threadSessionId } };
178
184
  if (args.length !== 1) return { kind: "usage", message: USAGE };
179
185
  const idOrPrefix = args[0]!;
180
186
  if (!isSafeIdentifier(idOrPrefix)) {
@@ -76,6 +76,7 @@ export function normalizeSlackConversation(record: SlackConversation): SlackConv
76
76
  export function acceptsSlackInbound(record: SlackConversation, rootTs: string, endpointGeneration: number): boolean {
77
77
  return (
78
78
  (record.state === "active" || record.state === "resumed_root") &&
79
+ record.cleanupEffectId === undefined &&
79
80
  record.rootTs === rootTs &&
80
81
  record.endpointGeneration === endpointGeneration
81
82
  );