@gajae-code/coding-agent 0.12.0 → 0.12.2

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 (247) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/types/async/job-manager.d.ts +17 -3
  3. package/dist/types/config/settings-schema.d.ts +56 -2
  4. package/dist/types/exa/mcp-client.d.ts +10 -1
  5. package/dist/types/exec/bash-executor.d.ts +45 -12
  6. package/dist/types/extensibility/extensions/types.d.ts +16 -2
  7. package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
  8. package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
  9. package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
  10. package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
  11. package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
  12. package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
  13. package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
  14. package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
  15. package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
  16. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +23 -3
  17. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  18. package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
  19. package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
  20. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
  21. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +37 -80
  22. package/dist/types/modes/acp/acp-agent.d.ts +2 -19
  23. package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
  24. package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
  25. package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
  26. package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  28. package/dist/types/modes/components/status-line/gh.d.ts +5 -0
  29. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
  30. package/dist/types/modes/controllers/command-controller.d.ts +1 -1
  31. package/dist/types/modes/controllers/event-controller.d.ts +1 -1
  32. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
  33. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  34. package/dist/types/modes/interactive-mode.d.ts +9 -0
  35. package/dist/types/modes/theme/theme.d.ts +3 -1
  36. package/dist/types/modes/types.d.ts +32 -0
  37. package/dist/types/reminders/star-reminder.d.ts +3 -13
  38. package/dist/types/runtime-mcp/manager.d.ts +1 -0
  39. package/dist/types/sdk/acp/adapter.d.ts +3 -1
  40. package/dist/types/sdk/broker/transport.d.ts +2 -0
  41. package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +5 -1
  43. package/dist/types/sdk/bus/config.d.ts +6 -2
  44. package/dist/types/sdk/bus/index.d.ts +29 -0
  45. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
  46. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  47. package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
  48. package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
  49. package/dist/types/sdk/bus/rich-render.d.ts +2 -0
  50. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
  51. package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
  52. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -4
  53. package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
  54. package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
  55. package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
  56. package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
  57. package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
  58. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  59. package/dist/types/sdk/prompt-status.d.ts +23 -3
  60. package/dist/types/sdk/session.d.ts +6 -0
  61. package/dist/types/session/agent-session.d.ts +40 -2
  62. package/dist/types/session/artifacts.d.ts +8 -0
  63. package/dist/types/session/blob-store.d.ts +57 -4
  64. package/dist/types/session/internal/managed-session-storage.d.ts +30 -4
  65. package/dist/types/session/session-manager.d.ts +23 -6
  66. package/dist/types/session/session-storage.d.ts +4 -0
  67. package/dist/types/session/streaming-output.d.ts +36 -7
  68. package/dist/types/setup/credential-import.d.ts +12 -0
  69. package/dist/types/skill-state/active-state.d.ts +12 -0
  70. package/dist/types/skill-state/workflow-hud.d.ts +6 -0
  71. package/dist/types/task/executor.d.ts +3 -2
  72. package/dist/types/task/index.d.ts +7 -1
  73. package/dist/types/task/output-manager.d.ts +1 -0
  74. package/dist/types/task/provider-retry-status.d.ts +8 -0
  75. package/dist/types/task/receipt.d.ts +9 -1
  76. package/dist/types/task/types.d.ts +23 -0
  77. package/dist/types/tools/ask-answer-registry.d.ts +2 -2
  78. package/dist/types/tools/bash-interactive.d.ts +4 -1
  79. package/dist/types/tools/bash.d.ts +3 -1
  80. package/dist/types/tools/image-gen.d.ts +4 -0
  81. package/dist/types/tools/index.d.ts +6 -0
  82. package/dist/types/tools/output-meta.d.ts +23 -2
  83. package/dist/types/tools/path-utils.d.ts +1 -0
  84. package/dist/types/tools/read.d.ts +0 -6
  85. package/dist/types/tools/review.d.ts +5 -7
  86. package/dist/types/tools/subagent.d.ts +2 -0
  87. package/dist/types/utils/gh.d.ts +13 -0
  88. package/dist/types/utils/image-resize.d.ts +1 -0
  89. package/dist/types/web/search/providers/kimi.d.ts +2 -0
  90. package/dist/types/web/search/providers/searxng.d.ts +7 -0
  91. package/dist/types/web/search/providers/xai.d.ts +2 -0
  92. package/package.json +10 -8
  93. package/scripts/acp-conformance-agent.ts +218 -0
  94. package/scripts/capture-gjc-bundle-settings.ts +306 -0
  95. package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
  96. package/scripts/generate-sdk-operation-inventory.ts +5 -0
  97. package/scripts/resident-memory-bench.ts +1341 -0
  98. package/scripts/run-acp-conformance.ts +290 -0
  99. package/src/async/job-manager.ts +99 -39
  100. package/src/cli/fast-help.ts +1 -1
  101. package/src/cli/plugin-cli.ts +176 -18
  102. package/src/commands/team.ts +19 -1
  103. package/src/config/file-lock.ts +11 -2
  104. package/src/config/settings-schema.ts +53 -2
  105. package/src/coordinator-mcp/question-gate-codec.ts +1 -1
  106. package/src/coordinator-mcp/server.ts +5 -12
  107. package/src/debug/index.ts +42 -17
  108. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
  109. package/src/defaults/gjc/skills/ralplan/SKILL.md +25 -20
  110. package/src/defaults/gjc/skills/team/SKILL.md +2 -0
  111. package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -26
  112. package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
  113. package/src/defaults/gjc-defaults.ts +4 -4
  114. package/src/exa/mcp-client.ts +12 -3
  115. package/src/exec/bash-executor.ts +144 -27
  116. package/src/extensibility/extensions/runner.ts +15 -0
  117. package/src/extensibility/extensions/types.ts +18 -1
  118. package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
  119. package/src/extensibility/gjc-plugins/index.ts +19 -2
  120. package/src/extensibility/gjc-plugins/installer.ts +189 -50
  121. package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
  122. package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
  123. package/src/extensibility/gjc-plugins/observability.ts +3 -3
  124. package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
  125. package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
  126. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
  127. package/src/extensibility/gjc-plugins/schema.ts +84 -12
  128. package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
  129. package/src/extensibility/gjc-plugins/types.ts +141 -0
  130. package/src/extensibility/gjc-plugins/validation.ts +9 -0
  131. package/src/gjc-runtime/launch-tmux.ts +25 -6
  132. package/src/gjc-runtime/launch-worktree.ts +103 -1
  133. package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
  134. package/src/gjc-runtime/managed-owner-admission.ts +1 -1
  135. package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
  136. package/src/gjc-runtime/ralplan-runtime.ts +491 -27
  137. package/src/gjc-runtime/team-launch.ts +8 -0
  138. package/src/gjc-runtime/team-runtime.ts +140 -32
  139. package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
  140. package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
  141. package/src/gjc-runtime/tmux-sessions.ts +58 -13
  142. package/src/gjc-runtime/ultragoal-evidence.ts +6 -74
  143. package/src/gjc-runtime/ultragoal-runtime.ts +635 -612
  144. package/src/gjc-runtime/workflow-manifest.generated.json +37 -99
  145. package/src/gjc-runtime/workflow-manifest.ts +21 -70
  146. package/src/internal-urls/docs-index.generated.ts +16 -16
  147. package/src/modes/DESIGN.md +104 -0
  148. package/src/modes/acp/acp-agent.ts +298 -186
  149. package/src/modes/components/gjc-bundle-settings.ts +508 -0
  150. package/src/modes/components/irc-sidebar.ts +30 -1
  151. package/src/modes/components/notifications-settings-editor.ts +12 -1
  152. package/src/modes/components/oauth-selector.ts +68 -30
  153. package/src/modes/components/session-observer-overlay.ts +305 -86
  154. package/src/modes/components/settings-selector.ts +56 -5
  155. package/src/modes/components/status-line/gh.ts +16 -0
  156. package/src/modes/components/tool-status-header.ts +3 -15
  157. package/src/modes/components/transcript-viewer-overlay.ts +99 -24
  158. package/src/modes/controllers/command-controller.ts +42 -34
  159. package/src/modes/controllers/event-controller.ts +27 -16
  160. package/src/modes/controllers/extension-ui-controller.ts +193 -34
  161. package/src/modes/controllers/selector-controller.ts +160 -22
  162. package/src/modes/interactive-mode.ts +189 -62
  163. package/src/modes/jobs-observer.ts +1 -1
  164. package/src/modes/runtime-init.ts +3 -0
  165. package/src/modes/theme/theme.ts +11 -9
  166. package/src/modes/types.ts +88 -0
  167. package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
  168. package/src/prompts/system/eager-todo.md +8 -4
  169. package/src/prompts/system/system-prompt.md +11 -4
  170. package/src/prompts/tools/bash.md +5 -5
  171. package/src/prompts/tools/search-tool-bm25.md +5 -0
  172. package/src/prompts/tools/task.md +1 -1
  173. package/src/reminders/star-reminder.ts +4 -42
  174. package/src/runtime-mcp/manager.ts +19 -4
  175. package/src/sdk/acp/adapter.ts +22 -6
  176. package/src/sdk/broker/lifecycle.ts +18 -13
  177. package/src/sdk/broker/transport.ts +35 -3
  178. package/src/sdk/bus/attachment-registry.ts +5 -1
  179. package/src/sdk/bus/chat-daemon-cli.ts +4 -39
  180. package/src/sdk/bus/chat-daemon-control.ts +7 -3
  181. package/src/sdk/bus/config.ts +18 -2
  182. package/src/sdk/bus/index.ts +661 -126
  183. package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
  184. package/src/sdk/bus/lifecycle-commands.ts +4 -1
  185. package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
  186. package/src/sdk/bus/reconciliation-store.ts +95 -16
  187. package/src/sdk/bus/rich-draft.ts +1 -1
  188. package/src/sdk/bus/rich-render.ts +2 -2
  189. package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
  190. package/src/sdk/bus/telegram-cli.ts +33 -8
  191. package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
  192. package/src/sdk/bus/telegram-daemon-contract.ts +12 -4
  193. package/src/sdk/bus/telegram-daemon.ts +916 -23
  194. package/src/sdk/bus/telegram-reference.ts +40 -3
  195. package/src/sdk/bus/threaded-render.ts +48 -3
  196. package/src/sdk/bus/topic-registry.ts +20 -0
  197. package/src/sdk/host/query/revision-store.ts +36 -2
  198. package/src/sdk/host/reverse-leases.ts +44 -7
  199. package/src/sdk/prompt-status.ts +16 -3
  200. package/src/sdk/protocol/operation-inventory.generated.json +33 -0
  201. package/src/sdk/session.ts +89 -9
  202. package/src/session/agent-session.ts +844 -173
  203. package/src/session/artifacts.ts +22 -0
  204. package/src/session/auth-broker-config.ts +10 -3
  205. package/src/session/blob-store.ts +915 -24
  206. package/src/session/internal/managed-session-scope.ts +200 -111
  207. package/src/session/internal/managed-session-storage.ts +492 -62
  208. package/src/session/session-manager.ts +1614 -435
  209. package/src/session/session-storage.ts +38 -45
  210. package/src/session/streaming-output.ts +238 -68
  211. package/src/setup/credential-import.ts +75 -13
  212. package/src/skill-state/active-state.ts +32 -0
  213. package/src/skill-state/workflow-hud.ts +17 -0
  214. package/src/skill-state/workflow-mutation-guard.ts +496 -2
  215. package/src/task/executor.ts +122 -39
  216. package/src/task/index.ts +280 -45
  217. package/src/task/output-manager.ts +24 -14
  218. package/src/task/provider-retry-status.ts +16 -0
  219. package/src/task/receipt.ts +34 -8
  220. package/src/task/render.ts +6 -2
  221. package/src/task/types.ts +108 -0
  222. package/src/tools/ask-answer-registry.ts +12 -6
  223. package/src/tools/ask.ts +86 -55
  224. package/src/tools/ast-edit.ts +1 -0
  225. package/src/tools/ast-grep.ts +1 -0
  226. package/src/tools/bash-interactive.ts +8 -3
  227. package/src/tools/bash.ts +503 -60
  228. package/src/tools/find.ts +1 -0
  229. package/src/tools/image-gen.ts +29 -5
  230. package/src/tools/index.ts +6 -0
  231. package/src/tools/output-meta.ts +108 -14
  232. package/src/tools/path-utils.ts +3 -2
  233. package/src/tools/read.ts +18 -0
  234. package/src/tools/review.ts +5 -7
  235. package/src/tools/search.ts +1 -0
  236. package/src/tools/subagent-render.ts +6 -2
  237. package/src/tools/subagent.ts +6 -0
  238. package/src/tools/telegram-send.ts +6 -3
  239. package/src/utils/gh.ts +39 -0
  240. package/src/utils/image-resize.ts +3 -0
  241. package/src/web/search/providers/anthropic.ts +4 -4
  242. package/src/web/search/providers/kimi.ts +15 -5
  243. package/src/web/search/providers/searxng.ts +47 -4
  244. package/src/web/search/providers/xai.ts +7 -2
  245. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
  246. package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
  247. package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
@@ -63,6 +63,88 @@ export function canApplyComposerSubmission(
63
63
  ): boolean {
64
64
  return options === undefined || (options.ownsComposer && editor === options.editor);
65
65
  }
66
+
67
+ type PartialActivityStatusContainer = Partial<Pick<Container, "children" | "clear" | "detachChild" | "addChild">>;
68
+
69
+ export function stopInteractiveActivityIndicator(
70
+ ctx: {
71
+ loadingAnimation?: Loader;
72
+ statusContainer?: PartialActivityStatusContainer;
73
+ stopLoadingAnimation?: (options?: { restoreBackground?: boolean }) => void;
74
+ },
75
+ options?: { restoreBackground?: boolean },
76
+ ): void {
77
+ if (ctx.stopLoadingAnimation) {
78
+ ctx.stopLoadingAnimation(options);
79
+ return;
80
+ }
81
+ ctx.loadingAnimation?.stop();
82
+ ctx.loadingAnimation = undefined;
83
+ ctx.statusContainer?.clear?.();
84
+ }
85
+
86
+ export function clearInteractiveActivityLoaders(
87
+ ctx: Pick<
88
+ InteractiveModeContext,
89
+ | "autoCompactionLoader"
90
+ | "autoCompactionEscapeHandler"
91
+ | "retryLoader"
92
+ | "retryCountdownTimer"
93
+ | "retryEscapeHandler"
94
+ | "retryEscapePrimed"
95
+ | "editor"
96
+ >,
97
+ ): void {
98
+ ctx.autoCompactionLoader?.stop();
99
+ ctx.autoCompactionLoader = undefined;
100
+ if (ctx.autoCompactionEscapeHandler) ctx.editor.onEscape = ctx.autoCompactionEscapeHandler;
101
+ ctx.autoCompactionEscapeHandler = undefined;
102
+ ctx.retryLoader?.stop();
103
+ ctx.retryLoader = undefined;
104
+ if (ctx.retryCountdownTimer) clearInterval(ctx.retryCountdownTimer);
105
+ ctx.retryCountdownTimer = undefined;
106
+ if (ctx.retryEscapeHandler) ctx.editor.onEscape = ctx.retryEscapeHandler;
107
+ ctx.retryEscapeHandler = undefined;
108
+ ctx.retryEscapePrimed = false;
109
+ }
110
+
111
+ export function suspendInteractiveActivityIndicator(ctx: {
112
+ loadingAnimation?: Loader;
113
+ statusContainer?: PartialActivityStatusContainer;
114
+ stopLoadingAnimation?: (options?: { restoreBackground?: boolean }) => void;
115
+ syncActivityIndicator?: () => void;
116
+ suspendActivityIndicator?: () => () => void;
117
+ }): () => void {
118
+ if (ctx.suspendActivityIndicator) return ctx.suspendActivityIndicator();
119
+ const suspendedLoader = ctx.loadingAnimation;
120
+ const statusContainer = ctx.statusContainer;
121
+ const canTransferMountedLoader =
122
+ Array.isArray(statusContainer?.children) &&
123
+ typeof statusContainer.detachChild === "function" &&
124
+ typeof statusContainer.addChild === "function";
125
+ const wasMounted =
126
+ canTransferMountedLoader && suspendedLoader !== undefined && statusContainer.children?.includes(suspendedLoader);
127
+ if (suspendedLoader && wasMounted) statusContainer.detachChild?.(suspendedLoader);
128
+ let released = false;
129
+ return () => {
130
+ if (released) return;
131
+ released = true;
132
+ if (
133
+ wasMounted &&
134
+ suspendedLoader &&
135
+ ctx.loadingAnimation === suspendedLoader &&
136
+ Array.isArray(statusContainer?.children) &&
137
+ !statusContainer.children.includes(suspendedLoader)
138
+ ) {
139
+ statusContainer.addChild?.(suspendedLoader);
140
+ }
141
+ syncInteractiveActivityIndicator(ctx);
142
+ };
143
+ }
144
+
145
+ export function syncInteractiveActivityIndicator(ctx: { syncActivityIndicator?: () => void }): void {
146
+ ctx.syncActivityIndicator?.();
147
+ }
66
148
  export type TodoStatus = "pending" | "in_progress" | "completed" | "abandoned";
67
149
 
68
150
  export type TodoItem = {
@@ -164,6 +246,8 @@ export interface InteractiveModeContext {
164
246
  init(): Promise<void>;
165
247
  shutdown(): Promise<void>;
166
248
  checkShutdownRequested(): Promise<void>;
249
+ isStopped?(): boolean;
250
+ onStop(callback: () => void): () => void;
167
251
 
168
252
  // Extension UI integration
169
253
  setToolUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void;
@@ -190,6 +274,9 @@ export interface InteractiveModeContext {
190
274
  setWorkingMessage(message?: string): void;
191
275
  applyPendingWorkingMessage(): void;
192
276
  ensureLoadingAnimation(): void;
277
+ syncActivityIndicator(): void;
278
+ suspendActivityIndicator(): () => void;
279
+ stopLoadingAnimation(options?: { restoreBackground?: boolean }): void;
193
280
  /**
194
281
  * Commit a pet mode through the shared result-returning policy: capability
195
282
  * is rechecked immediately before mutation and the preference persists only
@@ -253,6 +340,7 @@ export interface InteractiveModeContext {
253
340
  updateEditorTopBorder(): void;
254
341
  updateEditorBorderColor(): void;
255
342
  rebuildChatFromMessages(policy: TranscriptRebuildPolicy): void;
343
+ updateEditorChrome(): void;
256
344
  setTodos(todos: TodoItem[] | TodoPhase[]): void;
257
345
  reloadTodos(): Promise<void>;
258
346
  toggleTodoExpansion(): void;
@@ -1,6 +1,7 @@
1
1
  Persistence (ralplan runs only):
2
- - Only when the assignment references a ralplan stage or `stage_n`, persist the full artifact through:
2
+ - Only when the assignment references a ralplan stage or `stage_n`, it must also provide the ralplan owner `session_id` and `run_id`. If either identifier is missing, do not persist; return a compact error asking the caller to supply both.
3
+ - Persist the full artifact through:
3
4
 
4
- gjc ralplan --write --stage {{stage}} --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json
5
+ gjc ralplan --write --session-id <owner-session-id> --run-id <run-id> --stage {{stage}} --stage_n <N> --artifact-env GJC_RALPLAN_ARTIFACT --json
5
6
 
6
- Use the assignment-provided `stage_n`; on a duplicate-write error retry with the incremented N. Return the write receipt (`run_id`, `path`, `sha256`, `stage`, `stage_n`) and the role's compact verdict only. Otherwise, do not call `gjc ralplan --write`; return the full result in `yield.result.data`.
7
+ Use the assignment-provided owner `session_id`, `run_id`, and `stage_n`; never substitute the role subagent's own session id. On a duplicate-write error retry with the incremented N. Return the write receipt (`session_id`, `run_id`, `path`, `sha256`, `stage`, `stage_n`) and the role's compact verdict only. Otherwise, do not call `gjc ralplan --write`; return the full result in `yield.result.data`.
@@ -2,11 +2,15 @@
2
2
  Before substantive work, create a phased todo.
3
3
 
4
4
  You MUST call `todo_write` first in this turn.
5
- You MUST initialize the todo list with a single `init` op.
5
+ Your call MUST use the top-level `ops` array with exactly one `init` operation.
6
+ The `init` operation MUST contain `list`, whose entries each have a unique `phase` and an `items` array of task strings.
7
+ Use this canonical shape:
8
+ ```json
9
+ {"ops":[{"op":"init","list":[{"phase":"Investigation","items":["Locate relevant source and existing tests","Reproduce failure with focused contract test"]},{"phase":"Implementation","items":["Apply root cause fix across callsites"]},{"phase":"Verification","items":["Run focused tests and static checks"]}]}]}
10
+ ```
6
11
  You MUST cover the entire request from investigation through implementation and verification — not just the next immediate step.
7
- Task descriptions MUST be specific. A future turn MUST execute them without re-planning.
8
- You MUST keep task `content` to a short label (5-10 words). Put file paths, implementation steps, and specifics in `details`.
9
- You MUST keep exactly one task `in_progress` and all later tasks `pending`.
12
+ Task strings MUST be specific, unique labels of 5-10 words. A future turn MUST execute them without re-planning.
13
+ The tool automatically promotes the first task after initialization. Add extra context later with a separate `note` operation only when needed.
10
14
 
11
15
  After `todo_write` succeeds, continue the request in the same turn.
12
16
  Do not call `todo_write` again unless task state materially changed.
@@ -22,12 +22,13 @@ Optimize for correctness first, maintainability second, and brevity third. Prefe
22
22
  - Clear, low-risk implementation requests use direct tools and focused verification; do not invoke workflows or role agents for ceremony. Small verification needs do not turn a clear request into a planning workflow.
23
23
  - Ambiguous implementation asks with a missing target, scope, acceptance criteria, or safety boundary require clarification or the appropriate planning workflow before mutation.
24
24
  - Informational questions are answer-only/read-only unless the user explicitly requests a change, command, or execution.
25
- - Vague requirements use `/skill:deep-interview`: a requirements-only workflow that must not mutate product code. Its spec hands off as deep-interview → ralplan consensus → pending approval → separately approved execution.
26
- - Clear work with non-trivial architecture or sequencing risk uses `/skill:ralplan --deliberate` and stops pending approval.
25
+ - Vague requirements use `/skill:deep-interview`: a requirements-only workflow that must not mutate product code. Its spec hands off as deep-interview → ralplan consensus → final receipt → approval or valid non-off runtime admission → separately authorized execution.
26
+ - Clear work with non-trivial architecture or sequencing risk uses `/skill:ralplan --deliberate`; reconciliation must persist its final receipt before choosing approval or an admitted handoff. A valid non-off final runtime receipt enters the existing handoff chain; otherwise it stops pending approval.
27
27
  - Use `/skill:ultragoal` for durable goal ledgers and `/skill:team` for approved coordinated persistent work.
28
28
  - Delegate large implementation slices to `executor`; use `planner`, `architect`, or `critic` for bounded planning and review.
29
+ - An explicit user request to use a worktree (for example, "use worktree") overrides direct editing: delegate implementation through `task` with `isolated: true`. This is the in-session counterpart of launching `gjc --worktree`; if task isolation is unavailable, report that conflict instead of editing in the parent session.
29
30
  - Active skills are authoritative: never ignore an invoked skill; read the full skill text and follow it exactly.
30
- - Before explicit execution approval, planning and interview workflows NEVER edit product source, run mutating shell commands, commit, push, open PRs, or delegate implementation.
31
+ - Before explicit execution approval or a valid non-off ralplan final runtime receipt, planning and interview workflows NEVER edit product source, run mutating shell commands, commit, push, open PRs, or delegate implementation.
31
32
  </routing>
32
33
  </gjc-runtime>
33
34
  {{/unless}}
@@ -82,8 +83,14 @@ Use tools whenever they materially improve correctness, completeness, or groundi
82
83
 
83
84
  {{#if toolDiscoveryActive}}
84
85
  <tool-discovery>
85
- Use `{{toolRefs.search_tool_bm25}}` to activate hidden tools when a purpose-built capability would improve the task; then call the activated tool. Essential tools stay loaded up front.
86
+ Use `{{toolRefs.search_tool_bm25}}` to activate hidden tools when a purpose-built capability would improve the task; then call the activated tool. Activation makes a tool available, but does not perform its action. Essential tools stay loaded up front.
86
87
  Discoverable capabilities include browser automation, scheduling, debugging, and external integrations.
88
+ {{#has tools "task"}}
89
+ For requested subagents, delegation, or independent parallel lanes, call `{{toolRefs.task}}` before saying agents are running.
90
+ {{else}}
91
+ For requested subagents, delegation, or independent parallel lanes, search for `subagent delegation` before saying agents are running.
92
+ {{/has}}
93
+ Do not claim a web search, browser action, integration, or subagent ran without that tool's result.
87
94
  </tool-discovery>
88
95
  {{/if}}
89
96
 
@@ -44,13 +44,13 @@ Use it only for sanctioned GJC workflow CLI persistence or state read/write/cont
44
44
  {{else}}
45
45
  - Use bash only for terminal operations that dedicated tools do not cover.
46
46
  {{/when}}
47
- - Never pipe through `| head -n N` or `| tail -n N` — output is already truncated with the full result available via `artifact://<id>`.
47
+ - Never pipe through `| head -n N` or `| tail -n N` — output is already truncated. Recover omitted output only when the result includes an `artifact://<id>` footer or metadata reference; truncation without a reference leaves the visible output incomplete with no recoverable artifact.
48
48
  - Never redirect with `2>&1` or `2>/dev/null` — stdout and stderr are already merged.
49
49
  </critical>
50
50
 
51
51
  <output>
52
52
  - Returns output and exit code.
53
- - Truncated output is retrievable from `artifact://<id>` (linked in metadata)
53
+ - Truncated output is recoverable only when the result includes an `artifact://<id>` footer or metadata reference; truncation evidence without such a reference means the visible output is incomplete and no artifact is recoverable.
54
54
  - Exit codes shown on non-zero exit
55
55
  </output>
56
56
 
@@ -64,6 +64,6 @@ Use it only for sanctioned GJC workflow CLI persistence or state read/write/cont
64
64
 
65
65
  # Output minimizer
66
66
 
67
- - Bash stdout/stderr may be rewritten before you see it: long output is head/tail truncated, and test/lint runners (e.g. `bun test`, `cargo test`, ESLint) are passed through heuristic filters that drop noise and keep failures.
68
- - When the minimizer changes the visible text, the tool appends a `[raw output: artifact://<id>]` footer pointing at the **full untouched capture**. If a run looks suspicious (e.g. only a version banner) or you need the exact bytes, read that artifact.
69
- - If no footer is present, what you see is what the command actually emitted.
67
+ - Bash stdout/stderr may be rewritten before you see it: long output keeps only the last 1 KiB by default to reduce noise and input-token use. Explicit `tools.artifactTailBytes` / `tools.artifactHeadBytes` settings can set the tail budget or retain both ends. Prefer focused commands and dedicated `search`/`find` tools over producing broad output. Test/lint runners (e.g. `bun test`, `cargo test`, ESLint) are also passed through heuristic filters that drop noise and keep failures.
68
+ - When the local minimizer changes visible text, successful artifact storage appends a footer containing an `artifact://<id>` reference. Complete artifacts are labeled as full output; hard-capped artifacts report omitted bytes instead. If artifact allocation/storage is unavailable before a writer/save operation is attempted, truncation may have no reference or diagnostic. If an artifact writer/save operation is attempted and fails, a bounded diagnostic is emitted without inventing an artifact URI.
69
+ - ACP/client-terminal output can arrive already truncated from the beginning. Treat any truncation notice or metadata as evidence that the visible tail is incomplete. Recover omitted output only when an `artifact://` footer or metadata reference is present; truncation evidence without a reference means the visible output is incomplete and no artifact is recoverable. Output with neither truncation evidence nor an artifact reference is the complete emitted output.
@@ -12,6 +12,11 @@ Behavior:
12
12
  - Repeated searches add to the active tool set; they do not remove earlier selections
13
13
  - Newly activated tools become available before the next model call in the same overall turn
14
14
 
15
+ Follow-through:
16
+ - Activation only changes the active tool set; it does not execute a discovered tool or complete its work.
17
+ - If the task still needs a newly activated capability, call that tool in the next model turn. Do not claim that a browser action, web search, integration, or subagent ran until its tool result is present.
18
+ - If discovery was the only requested action, report availability as availability, not as completed work.
19
+
15
20
  Not for repository/file/code search. Tool discovery only.
16
21
 
17
22
  Returns JSON with:
@@ -28,7 +28,7 @@ Subagents have no conversation history. Every fact, file path, and direction the
28
28
  {{#if independentMode}}- `.inheritContext`: independent mode cannot inherit parent conversation. Omit it or set `"none"`; any non-`none` value is rejected before scheduling.{{/if}}
29
29
  {{#if customSchemaEnabled}}- `schema`: JTD schema for expected structured output (do not put format rules in assignments){{/if}}
30
30
  - `spawnPlan` (optional): required before any batch with more than 4 tasks; include whyParallel, whyNotLocal, independence, expectedReceiptShape, and maxInlineTokens.
31
- {{#if isolationEnabled}}- `isolated`: run in isolated env; use when tasks edit overlapping files{{/if}}
31
+ {{#if isolationEnabled}}- `isolated`: run in an isolated environment; REQUIRED when the user explicitly requests a worktree (for example, "use worktree"), and use when tasks edit overlapping files{{/if}}
32
32
  </parameters>
33
33
 
34
34
  <rules>
@@ -18,6 +18,10 @@ import type { ImageContent, MessageAttribution } from "@gajae-code/ai";
18
18
  import { getConfigRootDir, isEnoent } from "@gajae-code/utils";
19
19
  import { withFileLock } from "../config/file-lock";
20
20
  import type { CustomMessage } from "../session/messages";
21
+ import { type GhResult, type RunGh, runGhDefault } from "../utils/gh";
22
+
23
+ export type { GhResult, RunGh };
24
+ export { runGhDefault };
21
25
 
22
26
  export const STAR_REMINDER_REPO = "Yeachan-Heo/gajae-code";
23
27
  export const STAR_REMINDER_CUSTOM_TYPE = "star-reminder";
@@ -34,15 +38,6 @@ export interface StarReminderState {
34
38
 
35
39
  export type StarCheckStatus = "starred" | "unstarred" | "unavailable";
36
40
 
37
- export interface GhResult {
38
- exitCode: number;
39
- stdout: string;
40
- stderr: string;
41
- timedOut?: boolean;
42
- }
43
-
44
- export type RunGh = (args: string[], options?: { timeoutMs?: number }) => Promise<GhResult>;
45
-
46
41
  export interface StarReminderDeps {
47
42
  statePath?: string;
48
43
  now?: () => Date;
@@ -187,39 +182,6 @@ export async function recordDeclinedAfterNo(deps?: StarReminderDeps): Promise<St
187
182
  }, deps);
188
183
  }
189
184
 
190
- // --------------------------------------------------------------------------
191
- // gh helpers
192
- // --------------------------------------------------------------------------
193
-
194
- /** Default `gh` runner. Returns an unavailable-style result instead of throwing. */
195
- export async function runGhDefault(args: string[], options?: { timeoutMs?: number }): Promise<GhResult> {
196
- const ghPath = Bun.which("gh");
197
- if (!ghPath) {
198
- return { exitCode: -1, stdout: "", stderr: "gh not found", timedOut: false };
199
- }
200
- const timeoutMs = options?.timeoutMs ?? GH_TIMEOUT_MS;
201
- let timedOut = false;
202
- try {
203
- const proc = Bun.spawn([ghPath, ...args], { stdout: "pipe", stderr: "pipe", stdin: "ignore" });
204
- const timer = setTimeout(() => {
205
- timedOut = true;
206
- proc.kill();
207
- }, timeoutMs);
208
- try {
209
- const [stdout, stderr] = await Promise.all([
210
- new Response(proc.stdout).text(),
211
- new Response(proc.stderr).text(),
212
- ]);
213
- const exitCode = await proc.exited;
214
- return { exitCode, stdout, stderr, timedOut };
215
- } finally {
216
- clearTimeout(timer);
217
- }
218
- } catch (err) {
219
- return { exitCode: -1, stdout: "", stderr: err instanceof Error ? err.message : String(err), timedOut };
220
- }
221
- }
222
-
223
185
  function resolveRunGh(deps?: StarReminderDeps): RunGh {
224
186
  return deps?.runGh ?? runGhDefault;
225
187
  }
@@ -75,6 +75,7 @@ const STARTUP_TIMEOUT_GRACE_MS = 500;
75
75
  * budget derived from the readiness deadline (see `maxStartupTimeoutMs`).
76
76
  */
77
77
  const MAX_STARTUP_TIMEOUT_MS = 1_750;
78
+ const DEFAULT_EXACT_CONFIG_STARTUP_TIMEOUT_MS = 30_000;
78
79
 
79
80
  export function resolveStartupTimeoutMs(configs: MCPServerConfig[], maxStartupTimeoutMs?: number): number {
80
81
  const ceiling =
@@ -88,6 +89,17 @@ export function resolveStartupTimeoutMs(configs: MCPServerConfig[], maxStartupTi
88
89
  return Math.min(ceiling, Math.max(STARTUP_TIMEOUT_MS, Math.max(...configuredTimeouts) + STARTUP_TIMEOUT_GRACE_MS));
89
90
  }
90
91
 
92
+ export function resolveExactConfigStartupTimeoutMs(configs: MCPServerConfig[]): number {
93
+ const effectiveTimeouts = configs.map(config => {
94
+ const timeout = config.timeout;
95
+ return typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0
96
+ ? timeout
97
+ : DEFAULT_EXACT_CONFIG_STARTUP_TIMEOUT_MS;
98
+ });
99
+ if (effectiveTimeouts.length === 0) return STARTUP_TIMEOUT_MS;
100
+ return Math.max(...effectiveTimeouts) + STARTUP_TIMEOUT_GRACE_MS;
101
+ }
102
+
91
103
  function trackPromise<T>(promise: Promise<T>): TrackedPromise<T> {
92
104
  const tracked: TrackedPromise<T> = { promise, status: "pending" };
93
105
  promise.then(
@@ -650,10 +662,13 @@ export class MCPManager {
650
662
  }
651
663
 
652
664
  if (connectionTasks.length > 0) {
653
- const startupTimeoutMs = resolveStartupTimeoutMs(
654
- connectionTasks.map(task => task.config),
655
- this.#maxStartupTimeoutMs,
656
- );
665
+ const configs = connectionTasks.map(task => task.config);
666
+ // An exact config is explicit operator intent, so its declared connection
667
+ // windows govern startup. Ordinary discovery keeps the short ceiling.
668
+ const startupTimeoutMs =
669
+ this.#toolsOnly && this.#maxStartupTimeoutMs === undefined
670
+ ? resolveExactConfigStartupTimeoutMs(configs)
671
+ : resolveStartupTimeoutMs(configs, this.#maxStartupTimeoutMs);
657
672
  const firstUnexpectedFailure = Promise.withResolvers<{ reason: unknown }>();
658
673
  if (this.#toolsOnly) {
659
674
  for (const task of connectionTasks) {
@@ -8,7 +8,7 @@ type JsonObject = Record<string, unknown>;
8
8
 
9
9
  /** The small agent-side ACP surface used for reverse requests. */
10
10
  export interface AcpReverseConnection {
11
- request?(method: string, params: JsonObject): Promise<unknown>;
11
+ request?(method: string, params: JsonObject, options?: { cancellationSignal?: AbortSignal }): Promise<unknown>;
12
12
  [key: string]: unknown;
13
13
  }
14
14
 
@@ -42,6 +42,7 @@ export type AcpReconnectFailedHandler = (error: SdkClientError) => void;
42
42
  export type AcpFrameHandler = (frame: SdkFrame) => void;
43
43
  type ReverseRequest = {
44
44
  state: "pending" | "cancelled";
45
+ controller?: AbortController;
45
46
  cancelTimer?: NodeJS.Timeout;
46
47
  };
47
48
 
@@ -142,7 +143,10 @@ export class AcpSdkAdapter {
142
143
  if (this.#closed) return;
143
144
  this.#closed = true;
144
145
  if (this.#heartbeat) clearInterval(this.#heartbeat);
145
- for (const request of this.#reverseRequests.values()) if (request.cancelTimer) clearTimeout(request.cancelTimer);
146
+ for (const request of this.#reverseRequests.values()) {
147
+ request.controller?.abort();
148
+ if (request.cancelTimer) clearTimeout(request.cancelTimer);
149
+ }
146
150
  this.#reverseRequests.clear();
147
151
  this.#unsubscribe?.();
148
152
  this.#unsubscribeReconnect?.();
@@ -263,6 +267,7 @@ export class AcpSdkAdapter {
263
267
  if (this.#closed || !this.#providers.length) return;
264
268
  if (this.#reclaiming) return await this.#reclaiming;
265
269
  this.#reclaiming = (async () => {
270
+ this.#abortActiveReverseRequests();
266
271
  this.#connectionId = this.#client.connectionId;
267
272
  if (!this.#connectionId) this.#connectionId = await this.#waitForConnectionId();
268
273
  for (const provider of this.#providers) await this.registerProvider(provider);
@@ -349,13 +354,14 @@ export class AcpSdkAdapter {
349
354
  !this.#ownsReverseLease(connectionId, capability, leaseId)
350
355
  )
351
356
  return;
352
- const active: ReverseRequest = { state: "pending" };
357
+ const controller = new AbortController();
358
+ const active: ReverseRequest = { state: "pending", controller };
353
359
  this.#reverseRequests.set(id, active);
354
360
  try {
355
361
  const request = frame.payload as JsonObject | undefined;
356
362
  const method = typeof request?.method === "string" ? request.method : "";
357
363
  const payload = request?.payload && typeof request.payload === "object" ? (request.payload as JsonObject) : {};
358
- const result = await this.#forwardReverse(method, payload);
364
+ const result = await this.#forwardReverse(method, payload, controller.signal);
359
365
  if (!this.#canRespondToReverse(id, active, connectionId, capability, leaseId)) return;
360
366
 
361
367
  this.#client.send({ type: "reverse_response", id, connectionId, leaseId, ok: true, result });
@@ -404,6 +410,7 @@ export class AcpSdkAdapter {
404
410
  const request: ReverseRequest = this.#reverseRequests.get(id) ?? { state: "pending" };
405
411
  if (request.state === "cancelled") return;
406
412
  request.state = "cancelled";
413
+ request.controller?.abort();
407
414
  request.cancelTimer = setTimeout(() => this.#finishReverse(id, request), this.#reverseCancelTtlMs);
408
415
  this.#reverseRequests.set(id, request);
409
416
  }
@@ -414,9 +421,18 @@ export class AcpSdkAdapter {
414
421
  this.#reverseRequests.delete(id);
415
422
  }
416
423
 
417
- async #forwardReverse(method: string, payload: JsonObject): Promise<unknown> {
424
+ #abortActiveReverseRequests(): void {
425
+ for (const [id, request] of this.#reverseRequests) {
426
+ request.state = "cancelled";
427
+ request.controller?.abort();
428
+ this.#finishReverse(id, request);
429
+ }
430
+ }
431
+
432
+ async #forwardReverse(method: string, payload: JsonObject, signal: AbortSignal): Promise<unknown> {
418
433
  if (!method || !this.#connection) throw new AcpSdkAdapterError("acp_reverse_unavailable");
419
- if (this.#connection.request) return await this.#connection.request(method, payload);
434
+ if (this.#connection.request)
435
+ return await this.#connection.request(method, payload, { cancellationSignal: signal });
420
436
  const target = this.#connection[method];
421
437
  if (typeof target !== "function")
422
438
  throw new AcpSdkAdapterError("acp_reverse_unsupported", `ACP client does not support ${method}.`);
@@ -3260,26 +3260,31 @@ async function executeLifecycleResponse(
3260
3260
  `Unable to delete saved session artifacts: ${error instanceof Error ? error.message : String(error)}`,
3261
3261
  );
3262
3262
  }
3263
- const retainedArtifactsCleanup =
3263
+ const retainedArtifactsRootOnly =
3264
3264
  deleted.kind === "cleanup_pending" &&
3265
3265
  deleted.phase === "artifacts" &&
3266
3266
  deleted.detachedArtifactsPath !== undefined &&
3267
- cleanupTarget.plannedArtifactsPath !== undefined &&
3268
- (deleted.detachedArtifactsPath === cleanupTarget.plannedArtifactsPath ||
3269
- deleted.detachedArtifactsPath === `${cleanupTarget.plannedArtifactsPath}.removing`);
3270
- if (deleted.kind === "artifacts_removed" || retainedArtifactsCleanup) {
3267
+ (() => {
3268
+ const snapshot = native.snapshotDirectoryTree(deleted.detachedArtifactsPath);
3269
+ if (!snapshot.ok || !snapshot.snapshot) return false;
3270
+ const entries = snapshot.snapshot.entries;
3271
+ return entries.length === 1 && entries[0]?.relativePath === "" && entries[0].kind === "directory";
3272
+ })();
3273
+ const retainedArtifactsEvidence =
3274
+ retainedArtifactsRootOnly && deleted.kind === "cleanup_pending" && deleted.phase === "artifacts"
3275
+ ? {
3276
+ detachedArtifactsPath: deleted.detachedArtifactsPath,
3277
+ ...(deleted.artifactsIdentity
3278
+ ? { artifactsIdentity: serializeCleanupIdentity(deleted.artifactsIdentity) }
3279
+ : {}),
3280
+ }
3281
+ : {};
3282
+ if (deleted.kind === "artifacts_removed" || retainedArtifactsRootOnly) {
3271
3283
  const transcriptPhaseCleanup = {
3272
3284
  ...preauthorizedCleanup,
3273
3285
  phase: "transcript" as const,
3274
3286
  artifactsRemoved: true,
3275
- ...(deleted.kind === "cleanup_pending" && deleted.phase === "artifacts"
3276
- ? {
3277
- detachedArtifactsPath: deleted.detachedArtifactsPath,
3278
- ...(deleted.artifactsIdentity
3279
- ? { artifactsIdentity: serializeCleanupIdentity(deleted.artifactsIdentity) }
3280
- : {}),
3281
- }
3282
- : {}),
3287
+ ...(retainedArtifactsRootOnly ? retainedArtifactsEvidence : {}),
3283
3288
  ...(preauthorizedCleanup.artifactTree
3284
3289
  ? { artifactTree: { ...preauthorizedCleanup.artifactTree, completed: true as const } }
3285
3290
  : {}),
@@ -14,6 +14,7 @@ const BROKER_OPERATIONS = new Set([
14
14
  "session.resume",
15
15
  "session.close",
16
16
  "session.delete",
17
+ "broker.shutdown",
17
18
  ]);
18
19
  type RequestInput = Record<string, unknown>;
19
20
  type BrokerRequest = {
@@ -32,8 +33,15 @@ function tokenMatches(expected: string, actual: string | null): boolean {
32
33
  function isInput(value: unknown): value is RequestInput {
33
34
  return typeof value === "object" && value !== null && !Array.isArray(value);
34
35
  }
35
- function send(socket: ServerWebSocket<unknown>, frame: Record<string, unknown>): void {
36
- socket.send(JSON.stringify(frame));
36
+ function send(socket: ServerWebSocket<unknown>, frame: Record<string, unknown>): number {
37
+ return socket.send(JSON.stringify(frame));
38
+ }
39
+
40
+ export type BrokerShutdownSendAction = "dropped" | "wait_for_drain" | "close";
41
+
42
+ export function brokerShutdownSendAction(status: number): BrokerShutdownSendAction {
43
+ if (status === 0) return "dropped";
44
+ return status < 0 ? "wait_for_drain" : "close";
37
45
  }
38
46
  function sendError(socket: ServerWebSocket<unknown>, id: string | undefined, code: string, message: string): void {
39
47
  send(socket, { type: "broker_response", ...(id === undefined ? {} : { id }), ok: false, error: { code, message } });
@@ -45,6 +53,8 @@ export class BrokerTransport {
45
53
  readonly #requestedPort: number;
46
54
  #server: Bun.Server<undefined> | null = null;
47
55
  #port = 0;
56
+ readonly #shutdownBackpressured = new WeakSet<ServerWebSocket<unknown>>();
57
+ #shutdownRequested = false;
48
58
  constructor(broker: Broker, token: string, port = 0) {
49
59
  this.#broker = broker;
50
60
  this.#token = token;
@@ -71,8 +81,13 @@ export class BrokerTransport {
71
81
  },
72
82
  websocket: {
73
83
  maxPayloadLength: MAX_BROKER_JSON_FRAME_BYTES * 2,
74
- open: socket => send(socket, { type: "broker_hello", protocolVersion: PROTOCOL_VERSION }),
84
+ open: socket => {
85
+ send(socket, { type: "broker_hello", protocolVersion: PROTOCOL_VERSION });
86
+ },
75
87
  message: (socket, message) => void this.#handleMessage(socket, message),
88
+ drain: socket => {
89
+ if (this.#shutdownBackpressured.delete(socket)) this.#scheduleStopAfterShutdownResponse();
90
+ },
76
91
  },
77
92
  });
78
93
  this.#port = this.#server.port ?? 0;
@@ -84,6 +99,10 @@ export class BrokerTransport {
84
99
  if (server) await server.stop(true);
85
100
  }
86
101
  async #handleMessage(socket: ServerWebSocket<unknown>, raw: string | Buffer): Promise<void> {
102
+ if (this.#shutdownRequested) {
103
+ sendError(socket, undefined, "unavailable", "broker is shutting down");
104
+ return;
105
+ }
87
106
  if (Buffer.byteLength(raw) > MAX_BROKER_JSON_FRAME_BYTES) {
88
107
  sendError(socket, undefined, "payload_too_large", "broker JSON frame exceeds 4 MiB limit");
89
108
  return;
@@ -115,6 +134,16 @@ export class BrokerTransport {
115
134
  sendError(socket, frame.id, "invalid_input", "idempotencyKey must be a string");
116
135
  return;
117
136
  }
137
+ if (frame.operation === "broker.shutdown") {
138
+ const action = brokerShutdownSendAction(
139
+ send(socket, { type: "broker_response", id: frame.id, ok: true, result: { accepted: true } }),
140
+ );
141
+ if (action === "dropped") return;
142
+ this.#shutdownRequested = true;
143
+ if (action === "wait_for_drain") this.#shutdownBackpressured.add(socket);
144
+ else this.#scheduleStopAfterShutdownResponse();
145
+ return;
146
+ }
118
147
  try {
119
148
  const result = await this.#broker.handleRequest(frame.operation, frame.input, frame.idempotencyKey);
120
149
  send(socket, { type: "broker_response", id: frame.id, ...result });
@@ -122,4 +151,7 @@ export class BrokerTransport {
122
151
  sendError(socket, frame.id, "unavailable", "broker request failed");
123
152
  }
124
153
  }
154
+ #scheduleStopAfterShutdownResponse(): void {
155
+ setTimeout(() => void this.#broker.stop(), 25);
156
+ }
125
157
  }
@@ -5,7 +5,11 @@
5
5
  */
6
6
 
7
7
  /** Delivers a local file to the session's Telegram chat. */
8
- export type TelegramFileSink = (file: { path: string; caption?: string }) => Promise<{ ok: boolean; error?: string }>;
8
+ export type TelegramFileSink = (file: {
9
+ path: string;
10
+ caption?: string;
11
+ mime?: string;
12
+ }) => Promise<{ ok: boolean; error?: string }>;
9
13
 
10
14
  const sinks = new Map<string, TelegramFileSink>();
11
15
 
@@ -60,27 +60,10 @@ async function loadConfig(agentDir: string, kind: ChatDaemonKind): Promise<ChatD
60
60
  const config = notificationConfigFromFile(loaded.value);
61
61
  if (!config.enabled) return undefined;
62
62
  if (kind === "discord") {
63
- if (
64
- !isDiscordConfigured({
65
- ...config,
66
- sessionScope: "all",
67
- idleTimeoutMs: 60_000,
68
- rich: { enabled: true },
69
- richDraft: { enabled: false },
70
- toolActivity: { enabled: false },
71
- topics: { nameTemplate: undefined },
72
- btw: { enabled: true },
73
- streaming: { enabled: true },
74
- })
75
- ) {
63
+ if (!isDiscordConfigured(config)) {
76
64
  throw new Error("Discord notifications are enabled but configuration is incomplete");
77
65
  }
78
- const discord = config.discord as {
79
- botToken: string;
80
- applicationId: string;
81
- guildId: string;
82
- parentChannelId: string;
83
- };
66
+ const discord = config.discord;
84
67
  const { botToken, applicationId, guildId, parentChannelId } = discord;
85
68
  const identity = crypto
86
69
  .createHash("sha256")
@@ -95,28 +78,10 @@ async function loadConfig(agentDir: string, kind: ChatDaemonKind): Promise<ChatD
95
78
  presentation: { redact: config.redact, verbosity: config.verbosity },
96
79
  };
97
80
  }
98
- if (
99
- !isSlackConfigured({
100
- ...config,
101
- sessionScope: "all",
102
- idleTimeoutMs: 60_000,
103
- rich: { enabled: true },
104
- richDraft: { enabled: false },
105
- toolActivity: { enabled: false },
106
- topics: { nameTemplate: undefined },
107
- btw: { enabled: true },
108
- streaming: { enabled: true },
109
- })
110
- ) {
81
+ if (!isSlackConfigured(config)) {
111
82
  throw new Error("Slack notifications are enabled but configuration is incomplete");
112
83
  }
113
- const slack = config.slack as {
114
- botToken: string;
115
- appToken: string;
116
- workspaceId: string;
117
- channelId: string;
118
- authorizedUserId?: string;
119
- };
84
+ const slack = config.slack;
120
85
  const { botToken, appToken, workspaceId, channelId, authorizedUserId } = slack;
121
86
  const identity = crypto
122
87
  .createHash("sha256")
@@ -35,11 +35,15 @@ export type ChatDaemonAction = "stop" | "reload";
35
35
  * when deleting an observed owner-lock lease. Generation 12 refreshes retained
36
36
  * native path and process authority semantics. Generation 13 rejects special
37
37
  * files before retained native authority opens. Generation 14 reloads shared
38
- * chat daemons after notification configuration parsing changes.
38
+ * chat daemons after notification configuration parsing changes. Generation 15
39
+ * applies the current notification configuration directly when starting chat
40
+ * daemon transports. Generation 16 applies Telegram sound-policy configuration
41
+ * through shared notification parsing. Generation 17 binds managed-session
42
+ * replacement to exact native filesystem authority.
39
43
  */
40
44
  export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
41
- discord: 14,
42
- slack: 14,
45
+ discord: 17,
46
+ slack: 17,
43
47
  };
44
48
 
45
49
  export function chatDaemonGeneration(kind: ChatDaemonKind): number {