@gajae-code/coding-agent 0.11.3 → 0.11.5

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 (213) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/dist/types/cli/notify-cli.d.ts +5 -1
  3. package/dist/types/cli/worktree-cli.d.ts +8 -25
  4. package/dist/types/commands/gc.d.ts +4 -0
  5. package/dist/types/commands/worktree.d.ts +7 -32
  6. package/dist/types/config/settings-schema.d.ts +20 -0
  7. package/dist/types/export/html/template.generated.d.ts +1 -1
  8. package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
  9. package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
  10. package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
  11. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
  12. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
  13. package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
  14. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
  15. package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
  16. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
  17. package/dist/types/harness-control-plane/owner.d.ts +3 -1
  18. package/dist/types/internal-urls/local-protocol.d.ts +18 -0
  19. package/dist/types/main.d.ts +2 -2
  20. package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
  21. package/dist/types/modes/components/custom-editor.d.ts +8 -3
  22. package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
  23. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
  24. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  25. package/dist/types/modes/components/tool-transcript-format.d.ts +77 -0
  26. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
  27. package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
  28. package/dist/types/modes/controllers/input-controller.d.ts +8 -2
  29. package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
  30. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
  31. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
  32. package/dist/types/plan-mode/approved-plan.d.ts +1 -0
  33. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  34. package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
  35. package/dist/types/sdk/broker/broker.d.ts +1 -0
  36. package/dist/types/sdk/broker/discovery.d.ts +10 -0
  37. package/dist/types/sdk/broker/ensure.d.ts +24 -1
  38. package/dist/types/sdk/broker/internal.d.ts +3 -0
  39. package/dist/types/sdk/broker/session-index.d.ts +12 -0
  40. package/dist/types/sdk/broker/state-version.d.ts +2 -1
  41. package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
  42. package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
  43. package/dist/types/sdk/bus/config-commands.d.ts +8 -0
  44. package/dist/types/sdk/bus/config.d.ts +22 -0
  45. package/dist/types/sdk/bus/index.d.ts +162 -2
  46. package/dist/types/sdk/bus/notification-service.d.ts +71 -8
  47. package/dist/types/sdk/bus/session-control.d.ts +11 -8
  48. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
  49. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
  50. package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
  51. package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
  52. package/dist/types/sdk/session.d.ts +16 -1
  53. package/dist/types/secrets/index.d.ts +4 -2
  54. package/dist/types/secrets/obfuscator.d.ts +1 -1
  55. package/dist/types/session/agent-session.d.ts +31 -2
  56. package/dist/types/session/artifacts.d.ts +30 -8
  57. package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
  58. package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
  59. package/dist/types/session/session-manager.d.ts +88 -18
  60. package/dist/types/session/session-storage.d.ts +27 -0
  61. package/dist/types/session/streaming-output.d.ts +4 -0
  62. package/dist/types/task/executor.d.ts +9 -0
  63. package/dist/types/tool-discovery/tool-index.d.ts +2 -0
  64. package/dist/types/tools/ask.d.ts +87 -12
  65. package/dist/types/tools/bash-skill-urls.d.ts +2 -2
  66. package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
  67. package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
  68. package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
  69. package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
  70. package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
  71. package/dist/types/tools/index.d.ts +4 -0
  72. package/dist/types/tools/path-utils.d.ts +9 -14
  73. package/dist/types/tools/render-utils.d.ts +1 -8
  74. package/dist/types/tools/resource-gc.d.ts +14 -0
  75. package/dist/types/utils/image-loading.d.ts +23 -0
  76. package/dist/types/utils/image-resize.d.ts +2 -0
  77. package/dist/types/utils/pasted-image-loading.d.ts +39 -0
  78. package/dist/types/utils/pasted-image-path.d.ts +21 -19
  79. package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
  80. package/dist/types/web/scrapers/types.d.ts +1 -5
  81. package/package.json +25 -9
  82. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  83. package/scripts/run-threat-model-check.ts +60 -0
  84. package/src/cli/notify-cli.ts +40 -18
  85. package/src/cli/worktree-cli.ts +9 -287
  86. package/src/cli.ts +10 -9
  87. package/src/commands/gc.ts +11 -1
  88. package/src/commands/sdk.ts +10 -4
  89. package/src/commands/worktree.ts +10 -53
  90. package/src/config/model-profiles.ts +15 -15
  91. package/src/config/settings-schema.ts +17 -0
  92. package/src/daemon/runtime.ts +2 -2
  93. package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
  94. package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
  95. package/src/defaults/gjc/skills/team/SKILL.md +7 -9
  96. package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
  97. package/src/eval/js/shared/runtime.ts +1 -1
  98. package/src/eval/py/index.ts +2 -2
  99. package/src/export/html/template.generated.ts +1 -1
  100. package/src/extensibility/plugins/marketplace/manager.ts +94 -10
  101. package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
  102. package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
  103. package/src/gjc-runtime/deep-interview-state.ts +127 -3
  104. package/src/gjc-runtime/gc-render.ts +19 -1
  105. package/src/gjc-runtime/gc-runtime.ts +68 -3
  106. package/src/gjc-runtime/launch-tmux.ts +61 -6
  107. package/src/gjc-runtime/managed-owner-admission.ts +352 -0
  108. package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
  109. package/src/gjc-runtime/state-argv.ts +292 -0
  110. package/src/gjc-runtime/state-runtime.ts +80 -222
  111. package/src/gjc-runtime/team-runtime.ts +1 -1
  112. package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
  113. package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
  114. package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
  115. package/src/harness-control-plane/owner.ts +59 -3
  116. package/src/hindsight/client.ts +65 -2
  117. package/src/internal-urls/agent-protocol.ts +76 -5
  118. package/src/internal-urls/docs-index.generated.ts +7 -6
  119. package/src/internal-urls/local-protocol.ts +416 -18
  120. package/src/lsp/index.ts +4 -88
  121. package/src/main.ts +37 -13
  122. package/src/modes/acp/acp-agent.ts +8 -0
  123. package/src/modes/components/ansi-display-validator.ts +54 -0
  124. package/src/modes/components/custom-editor.ts +103 -32
  125. package/src/modes/components/custom-provider-wizard.ts +26 -1
  126. package/src/modes/components/notifications-settings-editor.ts +26 -2
  127. package/src/modes/components/session-observer-overlay.ts +40 -37
  128. package/src/modes/components/settings-selector.ts +3 -1
  129. package/src/modes/components/tool-transcript-format.ts +393 -0
  130. package/src/modes/components/transcript-viewer-overlay.ts +63 -1
  131. package/src/modes/controllers/goal-mode-controller.ts +20 -1
  132. package/src/modes/controllers/input-controller.ts +98 -37
  133. package/src/modes/controllers/plan-mode-controller.ts +2 -0
  134. package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
  135. package/src/modes/controllers/selector-controller.ts +71 -9
  136. package/src/modes/interactive-mode.ts +14 -2
  137. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
  138. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
  139. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
  140. package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
  141. package/src/plan-mode/approved-plan.ts +3 -1
  142. package/src/prompts/agents/architect.md +1 -0
  143. package/src/prompts/agents/critic.md +1 -0
  144. package/src/prompts/system/custom-system-prompt.md +1 -1
  145. package/src/prompts/system/system-prompt.md +2 -1
  146. package/src/prompts/tools/lsp.md +3 -3
  147. package/src/runtime-mcp/manager.ts +16 -1
  148. package/src/runtime-mcp/oauth-discovery.ts +116 -26
  149. package/src/sdk/broker/broker.ts +139 -32
  150. package/src/sdk/broker/discovery.ts +125 -0
  151. package/src/sdk/broker/ensure.ts +103 -19
  152. package/src/sdk/broker/internal.ts +10 -0
  153. package/src/sdk/broker/session-index.ts +309 -38
  154. package/src/sdk/broker/state-version.ts +5 -2
  155. package/src/sdk/bus/chat-daemon-cli.ts +72 -10
  156. package/src/sdk/bus/chat-daemon-control.ts +526 -130
  157. package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
  158. package/src/sdk/bus/config-commands.ts +22 -0
  159. package/src/sdk/bus/config.ts +37 -0
  160. package/src/sdk/bus/index.ts +384 -92
  161. package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
  162. package/src/sdk/bus/notification-service.ts +284 -62
  163. package/src/sdk/bus/recent-activity.ts +14 -5
  164. package/src/sdk/bus/session-control.ts +150 -44
  165. package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
  166. package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
  167. package/src/sdk/bus/telegram-daemon-control.ts +294 -76
  168. package/src/sdk/bus/telegram-daemon.ts +1842 -281
  169. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  170. package/src/sdk/session.ts +155 -27
  171. package/src/secrets/index.ts +53 -10
  172. package/src/secrets/obfuscator.ts +21 -18
  173. package/src/secrets/regex.ts +2 -0
  174. package/src/session/agent-session.ts +495 -253
  175. package/src/session/artifacts.ts +188 -23
  176. package/src/session/context-estimation.ts +7 -2
  177. package/src/session/internal/managed-session-scope.ts +215 -39
  178. package/src/session/internal/managed-session-storage.ts +1152 -46
  179. package/src/session/session-manager.ts +1055 -148
  180. package/src/session/session-storage.ts +109 -11
  181. package/src/session/streaming-output.ts +6 -0
  182. package/src/task/executor.ts +71 -26
  183. package/src/task/index.ts +63 -20
  184. package/src/tool-discovery/tool-index.ts +18 -0
  185. package/src/tools/ask.ts +329 -57
  186. package/src/tools/bash-allowed-prefixes.ts +79 -29
  187. package/src/tools/bash-skill-urls.ts +14 -3
  188. package/src/tools/bash.ts +2 -2
  189. package/src/tools/browser/launch.ts +3 -1
  190. package/src/tools/browser/readable.ts +1 -1
  191. package/src/tools/browser/screenshot-format.ts +34 -0
  192. package/src/tools/browser/tab-protocol.ts +3 -0
  193. package/src/tools/browser/tab-supervisor.ts +125 -122
  194. package/src/tools/browser/tab-worker-entry.ts +33 -14
  195. package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
  196. package/src/tools/browser/tab-worker-smoke.ts +87 -0
  197. package/src/tools/browser/tab-worker.ts +7 -5
  198. package/src/tools/index.ts +4 -0
  199. package/src/tools/path-utils.ts +42 -35
  200. package/src/tools/plan-mode-guard.ts +1 -0
  201. package/src/tools/read.ts +7 -1
  202. package/src/tools/render-utils.ts +1 -27
  203. package/src/tools/resource-gc.ts +123 -34
  204. package/src/tools/sqlite-reader.ts +3 -2
  205. package/src/tools/subagent.ts +2 -0
  206. package/src/utils/image-loading.ts +85 -34
  207. package/src/utils/image-resize.ts +16 -9
  208. package/src/utils/pasted-image-loading.ts +514 -0
  209. package/src/utils/pasted-image-path.ts +223 -98
  210. package/src/web/scrapers/html-to-markdown.ts +68 -0
  211. package/src/web/scrapers/readthedocs.ts +1 -1
  212. package/src/web/scrapers/types.ts +1 -68
  213. package/src/web/search/providers/openai-compatible.ts +76 -23
@@ -75,12 +75,31 @@ const defaultDeps: ResourceGcDeps = {
75
75
 
76
76
  // ── Controller state (process-global; tabs/browsers are module-global too) ──────────────────
77
77
  const activeSessions = new Map<string, Settings>();
78
- let timer: ReturnType<typeof setTimeout> | null = null;
78
+
79
+ interface ScheduleOwner {
80
+ generation: number;
81
+ token: number;
82
+ }
83
+
84
+ interface DeferredSchedule {
85
+ generation: number;
86
+ deadline: number;
87
+ }
88
+
89
+ interface WorkOwner {
90
+ generation: number;
91
+ source: "timer" | "external";
92
+ }
93
+
94
+ let pendingTimer: NodeJS.Timeout | null = null;
95
+ let pendingDeadline: number | null = null;
96
+ let pendingOwner: ScheduleOwner | null = null;
97
+ let deferredSchedule: DeferredSchedule | null = null;
98
+ let inProgressOwner: WorkOwner | null = null;
79
99
  let stopped = false;
80
- // Bumped on every stop so an in-flight tick from a previous schedule cannot reschedule after a
81
- // stop+re-register and leak a duplicate timer.
82
100
  let timerGeneration = 0;
83
- let inProgress = false;
101
+ let nextTimerToken = 0;
102
+ let schedulerNow = (): number => performance.now();
84
103
  let rssWarningActive = false;
85
104
  let lastScreenshotScanAt = 0;
86
105
  let deps: ResourceGcDeps = defaultDeps;
@@ -96,8 +115,20 @@ export interface ResourceGcRegistration {
96
115
  * session unregisters.
97
116
  */
98
117
  export function registerResourceGcSession(reg: ResourceGcRegistration): () => void {
118
+ const isNewSession = !activeSessions.has(reg.sessionId);
99
119
  activeSessions.set(reg.sessionId, reg.settings);
100
- ensureTimerStarted();
120
+ stopped = false;
121
+ if (isNewSession) {
122
+ const deadline = schedulerNow() + resolveSweepIntervalMs(reg.settings);
123
+ if (
124
+ inProgressOwner?.generation === timerGeneration &&
125
+ (pendingDeadline === null || pendingDeadline <= deadline)
126
+ ) {
127
+ deferSchedule(timerGeneration, deadline);
128
+ } else {
129
+ requestSchedule(deadline);
130
+ }
131
+ }
101
132
  let unregistered = false;
102
133
  return () => {
103
134
  if (unregistered) return;
@@ -113,51 +144,78 @@ function currentSweepIntervalMs(): number {
113
144
  return Number.isFinite(min) ? min : DEFAULT_SWEEP_INTERVAL_MS;
114
145
  }
115
146
 
116
- function ensureTimerStarted(): void {
117
- if (timer) return;
118
- stopped = false;
119
- scheduleNextSweep();
147
+ function clearPendingSchedule(): void {
148
+ if (pendingTimer) clearTimeout(pendingTimer);
149
+ pendingTimer = null;
150
+ pendingDeadline = null;
151
+ pendingOwner = null;
120
152
  }
121
153
 
122
- // Recursive setTimeout (not setInterval) so the cadence is recomputed every cycle and later
123
- // session register/unregister changes to resourceGc.sweepIntervalMs are honored live.
124
- function scheduleNextSweep(): void {
125
- if (stopped || activeSessions.size === 0) {
126
- timer = null;
154
+ function requestSchedule(deadline: number): void {
155
+ if (stopped || activeSessions.size === 0) return;
156
+ if (pendingDeadline !== null && pendingDeadline <= deadline) return;
157
+ clearPendingSchedule();
158
+ const owner = { generation: timerGeneration, token: ++nextTimerToken };
159
+ pendingDeadline = deadline;
160
+ pendingOwner = owner;
161
+ pendingTimer = setTimeout(
162
+ () => {
163
+ void handleTimerCallback(owner, deadline);
164
+ },
165
+ Math.max(0, deadline - schedulerNow()),
166
+ );
167
+ pendingTimer.unref?.();
168
+ }
169
+
170
+ function deferSchedule(generation: number, deadline: number): void {
171
+ if (generation !== timerGeneration) return;
172
+ if (deferredSchedule?.generation === generation) {
173
+ deferredSchedule.deadline = Math.min(deferredSchedule.deadline, deadline);
127
174
  return;
128
175
  }
129
- const generation = timerGeneration;
130
- timer = setTimeout(() => {
131
- void tickAndReschedule(generation);
132
- }, currentSweepIntervalMs());
133
- timer.unref?.();
176
+ deferredSchedule = { generation, deadline };
134
177
  }
135
178
 
136
- async function tickAndReschedule(generation: number): Promise<void> {
137
- await runTick();
138
- // A stop (and possible re-register) happened during the tick: a newer cycle owns the timer now.
139
- if (generation !== timerGeneration) return;
140
- scheduleNextSweep();
179
+ async function handleTimerCallback(owner: ScheduleOwner, deadline: number): Promise<void> {
180
+ if (pendingOwner?.generation !== owner.generation || pendingOwner.token !== owner.token) return;
181
+ pendingTimer = null;
182
+ pendingDeadline = null;
183
+ pendingOwner = null;
184
+ if (inProgressOwner) {
185
+ deferSchedule(owner.generation, deadline);
186
+ return;
187
+ }
188
+ await runTick(owner.generation, "timer");
189
+ }
190
+
191
+ function reconcileCurrentSchedule(): void {
192
+ if (stopped || activeSessions.size === 0 || inProgressOwner) return;
193
+ const normalDeadline = schedulerNow() + currentSweepIntervalMs();
194
+ const deferredDeadline = deferredSchedule?.generation === timerGeneration ? deferredSchedule.deadline : null;
195
+ if (deferredDeadline !== null) deferredSchedule = null;
196
+ requestSchedule(deferredDeadline === null ? normalDeadline : Math.min(deferredDeadline, normalDeadline));
141
197
  }
142
198
 
143
199
  function stopTimer(): void {
144
200
  stopped = true;
145
201
  timerGeneration++;
146
- if (timer) {
147
- clearTimeout(timer);
148
- timer = null;
149
- }
202
+ clearPendingSchedule();
203
+ deferredSchedule = null;
150
204
  }
151
205
 
152
- async function runTick(): Promise<void> {
153
- if (inProgress) return;
154
- inProgress = true;
206
+ async function runTick(generation = timerGeneration, source: WorkOwner["source"] = "external"): Promise<void> {
207
+ if (inProgressOwner || activeSessions.size === 0) return;
208
+ const owner: WorkOwner = { generation, source };
209
+ inProgressOwner = owner;
155
210
  try {
156
211
  await sweepOnce(deps);
157
212
  } catch (err) {
158
213
  logger.debug("resource GC sweep failed", { error: (err as Error).message });
159
214
  } finally {
160
- inProgress = false;
215
+ if (inProgressOwner === owner) inProgressOwner = null;
216
+ // A stale completion only releases its own work lock. Once that lock is released, current
217
+ // generation demand (which may have deferred behind it) can be reconciled independently.
218
+ reconcileCurrentSchedule();
161
219
  }
162
220
  }
163
221
 
@@ -268,24 +326,55 @@ export function __setResourceGcDepsForTest(overrides: Partial<ResourceGcDeps>):
268
326
  deps = { ...defaultDeps, ...overrides };
269
327
  }
270
328
 
329
+ export function __setResourceGcSchedulerNowForTest(now: () => number): void {
330
+ schedulerNow = now;
331
+ }
332
+
271
333
  export async function __runResourceGcTickForTest(): Promise<void> {
272
334
  await runTick();
273
335
  }
274
336
 
337
+ export async function __runResourceGcTimerCallbackForTest(
338
+ owner: { generation: number; token: number },
339
+ deadline: number,
340
+ ): Promise<void> {
341
+ await handleTimerCallback(owner, deadline);
342
+ }
343
+
275
344
  export function __getResourceGcStateForTest(): {
276
345
  timerActive: boolean;
277
346
  sessionCount: number;
278
347
  rssWarningActive: boolean;
279
348
  inProgress: boolean;
349
+ generation: number;
350
+ pendingDeadline: number | null;
351
+ pendingOwner: { generation: number; token: number } | null;
352
+ deferredDeadline: number | null;
353
+ deferredGeneration: number | null;
354
+ activeGeneration: number | null;
280
355
  } {
281
- return { timerActive: timer !== null, sessionCount: activeSessions.size, rssWarningActive, inProgress };
356
+ return {
357
+ timerActive: pendingTimer !== null,
358
+ sessionCount: activeSessions.size,
359
+ rssWarningActive,
360
+ inProgress: inProgressOwner !== null,
361
+ generation: timerGeneration,
362
+ pendingDeadline,
363
+ pendingOwner: pendingOwner ? { ...pendingOwner } : null,
364
+ deferredDeadline: deferredSchedule?.deadline ?? null,
365
+ deferredGeneration: deferredSchedule?.generation ?? null,
366
+ activeGeneration: inProgressOwner?.generation ?? null,
367
+ };
282
368
  }
283
369
 
284
370
  export function __resetResourceGcForTest(): void {
285
371
  stopTimer();
286
372
  activeSessions.clear();
287
- inProgress = false;
373
+ inProgressOwner = null;
374
+ deferredSchedule = null;
288
375
  rssWarningActive = false;
289
376
  lastScreenshotScanAt = 0;
377
+ nextTimerToken = 0;
378
+ schedulerNow = (): number => performance.now();
290
379
  deps = defaultDeps;
291
380
  }
@@ -12,6 +12,7 @@ const MAX_QUERY_LIMIT = 500;
12
12
  const MAX_RENDER_WIDTH = 120;
13
13
  const MAX_COLUMN_WIDTH = 40;
14
14
  const MIN_COLUMN_WIDTH = 1;
15
+ const SQLITE_USER_TABLE_PREDICATE = "name NOT LIKE 'sqlite!_%' ESCAPE '!'";
15
16
 
16
17
  type SqliteBinding = Exclude<SQLQueryBindings, Record<string, unknown>>;
17
18
 
@@ -182,7 +183,7 @@ function getTableMasterRow(db: Database, table: string): SqliteMasterRow {
182
183
  const row =
183
184
  db
184
185
  .prepare<SqliteMasterRow, [string]>(
185
- "SELECT name, sql FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name = ?",
186
+ `SELECT name, sql FROM sqlite_master WHERE type = 'table' AND ${SQLITE_USER_TABLE_PREDICATE} AND name = ?`,
186
187
  )
187
188
  .get(table) ?? null;
188
189
  if (!row) {
@@ -502,7 +503,7 @@ export function parseSqliteSelector(subPath: string, queryString: string): Sqlit
502
503
  export function listTables(db: Database): { name: string; rowCount: number }[] {
503
504
  const names = db
504
505
  .prepare<Pick<SqliteMasterRow, "name">, []>(
505
- "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name COLLATE NOCASE",
506
+ `SELECT name FROM sqlite_master WHERE type = 'table' AND ${SQLITE_USER_TABLE_PREDICATE} ORDER BY name COLLATE NOCASE`,
506
507
  )
507
508
  .all();
508
509
 
@@ -227,6 +227,8 @@ export class SubagentTool implements AgentTool<typeof subagentSchema, SubagentTo
227
227
  if (!result.ok && result.reason === "context_unavailable") throw new ToolError("context unavailable");
228
228
  if (!result.ok && result.reason === "not_found") {
229
229
  missing.push(this.#missingSnapshot(id, "not_found", "No visible detached subagent matches this id."));
230
+ } else if (!result.ok) {
231
+ throw new ToolError(`Failed to resume subagent ${record.subagentId}: ${result.reason ?? "unknown"}.`);
230
232
  } else {
231
233
  records.push(manager.getSubagentRecord(record.subagentId, ownerFilter) ?? record);
232
234
  }
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
2
2
  import type { ImageContent } from "@gajae-code/ai";
3
3
  import { formatBytes, readImageMetadata, SUPPORTED_IMAGE_MIME_TYPES } from "@gajae-code/utils";
4
4
  import { resolveReadPath } from "../tools/path-utils";
5
- import { formatDimensionNote, resizeImage } from "./image-resize";
5
+ import { formatDimensionNote, resizeImageBuffer } from "./image-resize";
6
6
 
7
7
  export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
8
8
  export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = SUPPORTED_IMAGE_MIME_TYPES;
@@ -14,6 +14,26 @@ export interface LoadImageInputOptions {
14
14
  maxBytes?: number;
15
15
  resolvedPath?: string;
16
16
  detectedMimeType?: string;
17
+ signal?: AbortSignal;
18
+ }
19
+
20
+ export interface LoadImageInputBytesOptions {
21
+ inputBuffer: Buffer;
22
+ resolvedPath: string;
23
+ mimeType: string;
24
+ autoResize: boolean;
25
+ maxBytes?: number;
26
+ maxOutputBytes?: number;
27
+ signal?: AbortSignal;
28
+ }
29
+
30
+ export interface TransformedImageInput {
31
+ resolvedPath: string;
32
+ mimeType: string;
33
+ buffer: Uint8Array;
34
+ textNote: string;
35
+ dimensionNote?: string;
36
+ bytes: number;
17
37
  }
18
38
 
19
39
  export interface LoadedImageInput {
@@ -38,9 +58,7 @@ export class ImageInputTooLargeError extends Error {
38
58
  }
39
59
 
40
60
  export async function ensureSupportedImageInput(image: ImageContent): Promise<ImageContent | null> {
41
- if (SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(image.mimeType)) {
42
- return image;
43
- }
61
+ if (SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(image.mimeType)) return image;
44
62
  try {
45
63
  const bytes = Buffer.from(image.data, "base64");
46
64
  const data = await new Bun.Image(bytes).png().toBase64();
@@ -50,53 +68,86 @@ export async function ensureSupportedImageInput(image: ImageContent): Promise<Im
50
68
  }
51
69
  }
52
70
 
53
- export async function loadImageInput(options: LoadImageInputOptions): Promise<LoadedImageInput | null> {
71
+ /** Transform trusted source bytes without materializing a base64 payload. */
72
+ export async function transformImageInputBytes(options: LoadImageInputBytesOptions): Promise<TransformedImageInput> {
54
73
  const maxBytes = options.maxBytes ?? MAX_IMAGE_INPUT_BYTES;
55
- const resolvedPath = options.resolvedPath ?? resolveReadPath(options.path, options.cwd);
56
- const metadata = options.detectedMimeType
57
- ? { mimeType: options.detectedMimeType }
58
- : await readImageMetadata(resolvedPath);
59
- const mimeType = metadata?.mimeType;
60
- if (!mimeType) return null;
61
-
62
- const stat = await Bun.file(resolvedPath).stat();
63
- if (stat.size > maxBytes) {
64
- throw new ImageInputTooLargeError(stat.size, maxBytes);
65
- }
66
-
67
- const inputBuffer = await fs.readFile(resolvedPath);
68
- if (inputBuffer.byteLength > maxBytes) {
69
- throw new ImageInputTooLargeError(inputBuffer.byteLength, maxBytes);
74
+ options.signal?.throwIfAborted();
75
+ if (options.inputBuffer.byteLength > maxBytes) {
76
+ throw new ImageInputTooLargeError(options.inputBuffer.byteLength, maxBytes);
70
77
  }
71
78
 
72
- let outputData = Buffer.from(inputBuffer).toBase64();
73
- let outputMimeType = mimeType;
74
- let outputBytes = inputBuffer.byteLength;
79
+ let outputBuffer: Uint8Array = options.inputBuffer;
80
+ let outputMimeType = options.mimeType;
75
81
  let dimensionNote: string | undefined;
76
82
 
77
83
  if (options.autoResize) {
84
+ options.signal?.throwIfAborted();
78
85
  try {
79
- const resized = await resizeImage({ type: "image", data: outputData, mimeType });
80
- outputData = resized.data;
86
+ const resized = await resizeImageBuffer(
87
+ options.inputBuffer,
88
+ options.mimeType,
89
+ options.maxOutputBytes === undefined ? undefined : { maxBytes: options.maxOutputBytes },
90
+ );
91
+ options.signal?.throwIfAborted();
92
+ outputBuffer = resized.buffer;
81
93
  outputMimeType = resized.mimeType;
82
- outputBytes = resized.buffer.byteLength;
83
94
  dimensionNote = formatDimensionNote(resized);
84
95
  } catch {
85
- // keep original image when resize fails
96
+ options.signal?.throwIfAborted();
97
+ // Keep the original image when optional resize fails.
86
98
  }
87
99
  }
88
100
 
101
+ options.signal?.throwIfAborted();
89
102
  let textNote = `Read image file [${outputMimeType}]`;
90
- if (dimensionNote) {
91
- textNote += `\n${dimensionNote}`;
92
- }
93
-
103
+ if (dimensionNote) textNote += `\n${dimensionNote}`;
94
104
  return {
95
- resolvedPath,
105
+ resolvedPath: options.resolvedPath,
96
106
  mimeType: outputMimeType,
97
- data: outputData,
107
+ buffer: outputBuffer,
98
108
  textNote,
99
109
  dimensionNote,
100
- bytes: outputBytes,
110
+ bytes: outputBuffer.byteLength,
111
+ };
112
+ }
113
+
114
+ export function materializeImageInput(transformed: TransformedImageInput): LoadedImageInput {
115
+ return {
116
+ resolvedPath: transformed.resolvedPath,
117
+ mimeType: transformed.mimeType,
118
+ data: Buffer.from(transformed.buffer).toBase64(),
119
+ textNote: transformed.textNote,
120
+ dimensionNote: transformed.dimensionNote,
121
+ bytes: transformed.bytes,
101
122
  };
102
123
  }
124
+
125
+ /** Build a model image payload from bytes already read through a trusted descriptor. */
126
+ export async function loadImageInputBytes(options: LoadImageInputBytesOptions): Promise<LoadedImageInput> {
127
+ return materializeImageInput(await transformImageInputBytes(options));
128
+ }
129
+
130
+ export async function loadImageInput(options: LoadImageInputOptions): Promise<LoadedImageInput | null> {
131
+ const maxBytes = options.maxBytes ?? MAX_IMAGE_INPUT_BYTES;
132
+ const resolvedPath = options.resolvedPath ?? resolveReadPath(options.path, options.cwd);
133
+ options.signal?.throwIfAborted();
134
+ const metadata = options.detectedMimeType
135
+ ? { mimeType: options.detectedMimeType }
136
+ : await readImageMetadata(resolvedPath);
137
+ const mimeType = metadata?.mimeType;
138
+ if (!mimeType) return null;
139
+
140
+ options.signal?.throwIfAborted();
141
+ const stat = await Bun.file(resolvedPath).stat();
142
+ if (stat.size > maxBytes) throw new ImageInputTooLargeError(stat.size, maxBytes);
143
+
144
+ const inputBuffer = await fs.readFile(resolvedPath, { signal: options.signal });
145
+ return loadImageInputBytes({
146
+ inputBuffer,
147
+ resolvedPath,
148
+ mimeType,
149
+ autoResize: options.autoResize,
150
+ maxBytes,
151
+ signal: options.signal,
152
+ });
153
+ }
@@ -21,14 +21,14 @@ export interface ResizedImage {
21
21
 
22
22
  // 500KB target — aggressive compression; Anthropic's 5MB per-image cap is rarely the
23
23
  // binding constraint once images are downsized to 1568px (Anthropic's internal threshold).
24
- const DEFAULT_MAX_BYTES = 500 * 1024;
24
+ export const DEFAULT_IMAGE_RESIZE_MAX_BYTES = 500 * 1024;
25
25
 
26
26
  const DEFAULT_OPTIONS: Required<Omit<ImageResizeOptions, "excludeWebP">> = {
27
27
  // Anthropic's "internal recommended size" — Anthropic model internally caps images at
28
28
  // 1568px on the longest edge before vision processing.
29
29
  maxWidth: 1568,
30
30
  maxHeight: 1568,
31
- maxBytes: DEFAULT_MAX_BYTES,
31
+ maxBytes: DEFAULT_IMAGE_RESIZE_MAX_BYTES,
32
32
  jpegQuality: 80,
33
33
  };
34
34
 
@@ -72,14 +72,17 @@ Buffer.prototype.toBase64 = function (this: Buffer) {
72
72
  * Backed by `Bun.Image`: a chainable native pipeline that runs decode/transform/encode
73
73
  * off the JS thread when the terminal (`.bytes()`) is awaited.
74
74
  */
75
- export async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {
75
+ export async function resizeImageBuffer(
76
+ inputBuffer: Buffer,
77
+ inputMimeType: string,
78
+ options?: ImageResizeOptions,
79
+ ): Promise<ResizedImage> {
76
80
  const excludeWebP = options?.excludeWebP ?? isWebPExcluded();
77
81
  const opts = { ...DEFAULT_OPTIONS, ...options, excludeWebP };
78
- const inputBuffer = Buffer.from(img.data, "base64");
79
82
 
80
83
  try {
81
84
  const { width: originalWidth, height: originalHeight, format } = await new Bun.Image(inputBuffer).metadata();
82
- const sourceMime = img.mimeType ?? `image/${format}`;
85
+ const sourceMime = inputMimeType ?? `image/${format}`;
83
86
 
84
87
  // Fast path: already within dimensions AND well under budget.
85
88
  // Threshold is 1/4 of budget — if already that compact, don't re-encode.
@@ -102,7 +105,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
102
105
  height: originalHeight,
103
106
  wasResized: false,
104
107
  get data() {
105
- return img.data;
108
+ return inputBuffer.toBase64();
106
109
  },
107
110
  };
108
111
  }
@@ -272,24 +275,28 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
272
275
  // When the caller demanded WebP exclusion AND the original is WebP,
273
276
  // returning the original buffer would silently violate that contract,
274
277
  // so surface an explicit error instead.
275
- if (excludeWebP && (img.mimeType === "image/webp" || !img.mimeType)) {
278
+ if (excludeWebP && (inputMimeType === "image/webp" || !inputMimeType)) {
276
279
  throw new Error("resizeImage: failed to decode image and cannot honor excludeWebP for a WebP source");
277
280
  }
278
281
  return {
279
282
  buffer: inputBuffer,
280
- mimeType: img.mimeType,
283
+ mimeType: inputMimeType,
281
284
  originalWidth: 0,
282
285
  originalHeight: 0,
283
286
  width: 0,
284
287
  height: 0,
285
288
  wasResized: false,
286
289
  get data() {
287
- return img.data;
290
+ return inputBuffer.toBase64();
288
291
  },
289
292
  };
290
293
  }
291
294
  }
292
295
 
296
+ export function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {
297
+ return resizeImageBuffer(Buffer.from(img.data, "base64"), img.mimeType, options);
298
+ }
299
+
293
300
  /**
294
301
  * Format a dimension note for resized images.
295
302
  * This helps the model understand the coordinate mapping.