@gajae-code/coding-agent 0.16.4 → 0.16.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/capability/types.d.ts +4 -0
  3. package/dist/types/cli/customize-doctor.d.ts +22 -0
  4. package/dist/types/config/file-lock.d.ts +31 -3
  5. package/dist/types/coordinator-mcp/question-state.d.ts +7 -0
  6. package/dist/types/coordinator-mcp/server.d.ts +7 -0
  7. package/dist/types/discovery/helpers.d.ts +33 -0
  8. package/dist/types/exec/bash-executor.d.ts +17 -1
  9. package/dist/types/exec/bash-shell-guardian.d.ts +15 -0
  10. package/dist/types/exec/bash-shell-runtime-entry.d.ts +2 -0
  11. package/dist/types/exec/bash-shell-signals.d.ts +2 -0
  12. package/dist/types/exec/bash-shell-supervisor-entry.d.ts +1 -0
  13. package/dist/types/exec/bash-shell-supervisor.d.ts +10 -0
  14. package/dist/types/exec/bash-shell-worker-entry.d.ts +2 -0
  15. package/dist/types/exec/bash-shell-worker-protocol.d.ts +63 -0
  16. package/dist/types/exec/bash-shell-worker.d.ts +1 -0
  17. package/dist/types/exec/isolated-shell.d.ts +19 -0
  18. package/dist/types/extensibility/extensions/function-hooks-internal.d.ts +9 -0
  19. package/dist/types/extensibility/extensions/function-hooks.d.ts +169 -0
  20. package/dist/types/extensibility/extensions/index.d.ts +1 -0
  21. package/dist/types/extensibility/extensions/runner.d.ts +46 -6
  22. package/dist/types/extensibility/extensions/types.d.ts +16 -0
  23. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -1
  24. package/dist/types/extensibility/gjc-plugins/constrained-hooks.d.ts +35 -2
  25. package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
  26. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  27. package/dist/types/extensibility/runtime-skill-discovery.d.ts +4 -1
  28. package/dist/types/extensibility/skill-management.d.ts +9 -4
  29. package/dist/types/extensibility/skills.d.ts +2 -0
  30. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +4 -1
  31. package/dist/types/gjc-runtime/linux-proc.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +12 -0
  33. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +41 -3
  34. package/dist/types/gjc-runtime/ultragoal-succession.d.ts +251 -0
  35. package/dist/types/modes/components/bash-execution.d.ts +1 -0
  36. package/dist/types/modes/components/execution-shared.d.ts +1 -0
  37. package/dist/types/modes/interactive-mode.d.ts +2 -0
  38. package/dist/types/modes/types.d.ts +6 -0
  39. package/dist/types/sdk/broker/ensure.d.ts +32 -0
  40. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +0 -1
  41. package/dist/types/sdk/broker/lifecycle.d.ts +6 -0
  42. package/dist/types/sdk/broker/startup-failure.d.ts +4 -1
  43. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -0
  44. package/dist/types/sdk/bus/chat-effect-journal.d.ts +17 -2
  45. package/dist/types/sdk/bus/conversation-store.d.ts +11 -0
  46. package/dist/types/sdk/bus/lifecycle-commands.d.ts +8 -8
  47. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  48. package/dist/types/sdk/bus/topic-registry.d.ts +8 -0
  49. package/dist/types/sdk/client/client.d.ts +6 -0
  50. package/dist/types/sdk/host/host.d.ts +3 -1
  51. package/dist/types/sdk/host/session-runtime.d.ts +4 -2
  52. package/dist/types/sdk/lifecycle/service.d.ts +1 -0
  53. package/dist/types/sdk/router/session-router.d.ts +10 -2
  54. package/dist/types/sdk/session.d.ts +5 -1
  55. package/dist/types/session/agent-session.d.ts +2 -0
  56. package/dist/types/session/fallback-chain-controller.d.ts +15 -0
  57. package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
  58. package/dist/types/session/messages.d.ts +1 -0
  59. package/dist/types/session/session-manager.d.ts +3 -1
  60. package/dist/types/setup/paseo/announce.d.ts +4 -3
  61. package/dist/types/skill-state/canonical-skills.d.ts +4 -0
  62. package/dist/types/system-prompt.d.ts +8 -0
  63. package/dist/types/task/executor.d.ts +3 -0
  64. package/dist/types/tools/bash.d.ts +1 -0
  65. package/dist/types/tools/github-cache.d.ts +2 -1
  66. package/dist/types/tools/index.d.ts +2 -0
  67. package/package.json +9 -7
  68. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  69. package/src/capability/index.ts +14 -2
  70. package/src/capability/types.ts +4 -0
  71. package/src/cli/customize-doctor.ts +155 -12
  72. package/src/cli.ts +22 -0
  73. package/src/commands/sdk.ts +74 -31
  74. package/src/config/file-lock-gc.ts +40 -2
  75. package/src/config/file-lock.ts +198 -14
  76. package/src/config/model-preset-registry.ts +115 -16
  77. package/src/config/model-registry.ts +16 -12
  78. package/src/coordinator-mcp/question-state.ts +33 -2
  79. package/src/coordinator-mcp/server.ts +477 -101
  80. package/src/crash/sanitize.ts +1 -1
  81. package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -0
  82. package/src/discovery/builtin.ts +50 -62
  83. package/src/discovery/helpers.ts +297 -32
  84. package/src/exec/bash-executor.ts +330 -50
  85. package/src/exec/bash-shell-guardian.ts +388 -0
  86. package/src/exec/bash-shell-runtime-entry.ts +5 -0
  87. package/src/exec/bash-shell-signals.ts +29 -0
  88. package/src/exec/bash-shell-supervisor-entry.ts +3 -0
  89. package/src/exec/bash-shell-supervisor.ts +359 -0
  90. package/src/exec/bash-shell-worker-entry.ts +5 -0
  91. package/src/exec/bash-shell-worker-protocol.ts +36 -0
  92. package/src/exec/bash-shell-worker.ts +116 -0
  93. package/src/exec/isolated-shell.ts +393 -0
  94. package/src/extensibility/extensions/function-hooks-internal.ts +73 -0
  95. package/src/extensibility/extensions/function-hooks.ts +959 -0
  96. package/src/extensibility/extensions/index.ts +22 -0
  97. package/src/extensibility/extensions/loader.ts +53 -2
  98. package/src/extensibility/extensions/runner.ts +895 -264
  99. package/src/extensibility/extensions/types.ts +42 -0
  100. package/src/extensibility/extensions/wrapper.ts +56 -15
  101. package/src/extensibility/gjc-plugins/compiler.ts +20 -2
  102. package/src/extensibility/gjc-plugins/constrained-hooks.ts +321 -23
  103. package/src/extensibility/gjc-plugins/registry.ts +113 -1
  104. package/src/extensibility/gjc-plugins/runtime-quarantine.ts +20 -2
  105. package/src/extensibility/gjc-plugins/schema.ts +121 -4
  106. package/src/extensibility/gjc-plugins/types.ts +25 -0
  107. package/src/extensibility/hooks/loader.ts +133 -16
  108. package/src/extensibility/hooks/types.ts +28 -0
  109. package/src/extensibility/runtime-skill-discovery.ts +80 -47
  110. package/src/extensibility/skill-management.ts +200 -52
  111. package/src/extensibility/skills.ts +29 -9
  112. package/src/gjc-runtime/empty-delete-gc.ts +94 -4
  113. package/src/gjc-runtime/gc-runtime.ts +29 -0
  114. package/src/gjc-runtime/linux-proc.ts +14 -0
  115. package/src/gjc-runtime/session-state-lock.ts +51 -8
  116. package/src/gjc-runtime/session-state-sidecar.ts +27 -11
  117. package/src/gjc-runtime/state-writer.ts +30 -29
  118. package/src/gjc-runtime/ultragoal-runtime.ts +189 -47
  119. package/src/gjc-runtime/ultragoal-succession.ts +1667 -0
  120. package/src/gjc-runtime/workflow-manifest.generated.json +52 -1
  121. package/src/gjc-runtime/workflow-manifest.ts +16 -0
  122. package/src/harness-control-plane/session-lease.ts +2 -2
  123. package/src/internal-urls/docs-index.generated.ts +6 -6
  124. package/src/lsp/config.ts +10 -3
  125. package/src/lsp/index.ts +1 -1
  126. package/src/lsp/path-trust.ts +69 -14
  127. package/src/main.ts +6 -1
  128. package/src/modes/acp/acp-agent.ts +16 -3
  129. package/src/modes/components/bash-execution.ts +4 -1
  130. package/src/modes/components/execution-shared.ts +4 -1
  131. package/src/modes/components/session-observer-overlay.ts +1 -1
  132. package/src/modes/components/tool-status-header.ts +3 -1
  133. package/src/modes/controllers/command-controller.ts +1 -0
  134. package/src/modes/controllers/event-controller.ts +3 -2
  135. package/src/modes/controllers/extension-ui-controller.ts +1 -0
  136. package/src/modes/controllers/input-controller.ts +6 -3
  137. package/src/modes/interactive-mode.ts +6 -5
  138. package/src/modes/print-mode.ts +5 -1
  139. package/src/modes/prompt-action-autocomplete.ts +54 -27
  140. package/src/modes/runtime-init.ts +2 -0
  141. package/src/modes/types.ts +16 -3
  142. package/src/modes/utils/ui-helpers.ts +1 -0
  143. package/src/prompts/tools/subagent.md +1 -0
  144. package/src/runtime-mcp/manager.ts +20 -2
  145. package/src/runtime-mcp/pool.test.ts +20 -11
  146. package/src/sdk/broker/broker.ts +119 -28
  147. package/src/sdk/broker/ensure.ts +248 -41
  148. package/src/sdk/broker/lifecycle-ledger.ts +7 -17
  149. package/src/sdk/broker/lifecycle.ts +56 -6
  150. package/src/sdk/broker/startup-failure.ts +15 -6
  151. package/src/sdk/bus/chat-daemon-control.ts +3 -1
  152. package/src/sdk/bus/chat-daemon-runtime.ts +35 -5
  153. package/src/sdk/bus/chat-effect-journal.ts +132 -7
  154. package/src/sdk/bus/conversation-store.ts +29 -0
  155. package/src/sdk/bus/lifecycle-commands.ts +17 -11
  156. package/src/sdk/bus/slack-conversation.ts +1 -0
  157. package/src/sdk/bus/slack-daemon.ts +387 -48
  158. package/src/sdk/bus/telegram-daemon.ts +16 -2
  159. package/src/sdk/bus/topic-registry.ts +19 -0
  160. package/src/sdk/cli/master-cli.ts +1 -1
  161. package/src/sdk/cli/session-cli.ts +2 -2
  162. package/src/sdk/client/client.ts +66 -17
  163. package/src/sdk/host/host.ts +29 -9
  164. package/src/sdk/host/query/revision-store.ts +19 -13
  165. package/src/sdk/host/reverse-leases.ts +7 -4
  166. package/src/sdk/host/session-runtime.test.ts +50 -8
  167. package/src/sdk/host/session-runtime.ts +158 -51
  168. package/src/sdk/lifecycle/service.ts +4 -0
  169. package/src/sdk/protocol/operation-inventory.generated.json +11 -0
  170. package/src/sdk/router/session-router.ts +89 -38
  171. package/src/sdk/session.ts +155 -26
  172. package/src/sdk/transport/relay.ts +35 -13
  173. package/src/session/agent-session.ts +117 -52
  174. package/src/session/fallback-chain-controller.ts +38 -0
  175. package/src/session/internal/managed-session-storage.ts +19 -5
  176. package/src/session/messages.ts +5 -0
  177. package/src/session/session-manager.ts +29 -5
  178. package/src/session-import/redact.ts +5 -1
  179. package/src/setup/paseo/announce.ts +49 -43
  180. package/src/setup/paseo/setup-deps.ts +1 -1
  181. package/src/skill-state/canonical-skills.ts +12 -0
  182. package/src/skills/skills.test.ts +41 -21
  183. package/src/system-prompt.ts +25 -3
  184. package/src/task/executor.ts +5 -0
  185. package/src/task/index.ts +4 -0
  186. package/src/tools/bash.ts +28 -11
  187. package/src/tools/gh.ts +26 -3
  188. package/src/tools/github-cache.ts +68 -16
  189. package/src/tools/index.ts +2 -0
  190. package/src/tools/job.ts +13 -2
  191. package/src/tools/read.ts +14 -2
  192. package/src/tools/render-utils.ts +1 -3
  193. package/src/tools/resource-gc.ts +28 -8
  194. package/src/tools/skill-discovery.ts +5 -0
  195. package/src/tools/skill.ts +15 -0
  196. package/src/tools/subagent.ts +3 -2
  197. package/src/tools/tool-catalog.generated.ts +2 -2
  198. package/src/tools/ultragoal-ask-guard.ts +2 -2
@@ -152,6 +152,26 @@ function evictExpired(db: Database, hardTtlMs: number): void {
152
152
  */
153
153
  const SWEEP_INTERVAL_MS = 60_000;
154
154
  let lastSweepAt = 0;
155
+ const backgroundRefreshes = new Map<string, { generation: number; controller: AbortController }>();
156
+ let backgroundRefreshGeneration = 0;
157
+ const BACKGROUND_REFRESH_TIMEOUT_MS = 30_000;
158
+
159
+ function cacheIdentityKey(
160
+ authKey: string,
161
+ repo: string,
162
+ kind: CacheKind,
163
+ number: number,
164
+ includeComments: boolean,
165
+ ): string {
166
+ return JSON.stringify([authKey, normalizeRepo(repo), kind, number, includeComments]);
167
+ }
168
+
169
+ function retireBackgroundRefresh(key: string): void {
170
+ const entry = backgroundRefreshes.get(key);
171
+ if (!entry) return;
172
+ entry.controller.abort();
173
+ backgroundRefreshes.delete(key);
174
+ }
155
175
 
156
176
  function sweepIfDue(hardTtlMs: number): void {
157
177
  const now = Date.now();
@@ -296,6 +316,12 @@ export function invalidate(
296
316
  includeComments?: boolean,
297
317
  authKey: string = DEFAULT_CACHE_AUTH_KEY,
298
318
  ): void {
319
+ if (includeComments === undefined) {
320
+ retireBackgroundRefresh(cacheIdentityKey(authKey, repo, kind, number, false));
321
+ retireBackgroundRefresh(cacheIdentityKey(authKey, repo, kind, number, true));
322
+ } else {
323
+ retireBackgroundRefresh(cacheIdentityKey(authKey, repo, kind, number, includeComments));
324
+ }
299
325
  const db = openDb();
300
326
  if (!db) return;
301
327
  try {
@@ -342,6 +368,9 @@ export function resetForTests(): void {
342
368
  cachedDb = null;
343
369
  openAttempted = false;
344
370
  lastSweepAt = 0;
371
+ for (const { controller } of backgroundRefreshes.values()) controller.abort();
372
+ backgroundRefreshes.clear();
373
+ backgroundRefreshGeneration += 1;
345
374
  }
346
375
 
347
376
  // ────────────────────────────────────────────────────────────────────────────
@@ -365,9 +394,10 @@ export interface CacheLookupOptions<T> {
365
394
  * must bypass persistent cache reads/writes.
366
395
  */
367
396
  authKey?: string | null;
368
- fetchFresh: () => Promise<FreshResult<T>>;
397
+ fetchFresh: (signal?: AbortSignal) => Promise<FreshResult<T>>;
369
398
  settings?: Settings | undefined;
370
399
  now?: number;
400
+ signal?: AbortSignal;
371
401
  }
372
402
 
373
403
  export type CacheStatus = "miss" | "fresh" | "stale" | "disabled";
@@ -448,22 +478,41 @@ function scheduleBackgroundRefresh<T>(
448
478
  kind: CacheKind,
449
479
  number: number,
450
480
  includeComments: boolean,
451
- fetchFresh: () => Promise<FreshResult<T>>,
481
+ fetchFresh: (signal?: AbortSignal) => Promise<FreshResult<T>>,
452
482
  ): void {
453
- queueMicrotask(() => {
454
- const promise = fetchFresh();
455
- promise
456
- .then(fresh => {
483
+ const key = cacheIdentityKey(authKey, repo, kind, number, includeComments);
484
+ if (backgroundRefreshes.has(key)) return;
485
+ const generation = backgroundRefreshGeneration;
486
+ const controller = new AbortController();
487
+ const entry = { generation, controller };
488
+ backgroundRefreshes.set(key, entry);
489
+ queueMicrotask(async () => {
490
+ const timeoutHandle = setTimeout(() => {
491
+ controller.abort(new Error("GitHub background refresh timed out"));
492
+ if (backgroundRefreshes.get(key) === entry) backgroundRefreshes.delete(key);
493
+ logger.debug("github cache: background refresh timed out", { repo, kind, number });
494
+ }, BACKGROUND_REFRESH_TIMEOUT_MS);
495
+ try {
496
+ const fresh = await fetchFresh(controller.signal);
497
+ if (
498
+ !controller.signal.aborted &&
499
+ backgroundRefreshes.get(key) === entry &&
500
+ backgroundRefreshGeneration === generation
501
+ ) {
457
502
  storeResult(authKey, repo, kind, number, includeComments, fresh, Date.now());
458
- })
459
- .catch(err => {
460
- logger.debug("github cache: background refresh failed", {
461
- err: String(err),
462
- repo,
463
- kind,
464
- number,
465
- });
503
+ }
504
+ } catch (err) {
505
+ controller.abort();
506
+ logger.debug("github cache: background refresh failed", {
507
+ err: String(err),
508
+ repo,
509
+ kind,
510
+ number,
466
511
  });
512
+ } finally {
513
+ clearTimeout(timeoutHandle);
514
+ if (backgroundRefreshes.get(key) === entry) backgroundRefreshes.delete(key);
515
+ }
467
516
  });
468
517
  }
469
518
 
@@ -473,9 +522,10 @@ export async function getOrFetchView<T>(options: CacheLookupOptions<T>): Promise
473
522
  const authKey = options.authKey === undefined ? DEFAULT_CACHE_AUTH_KEY : options.authKey;
474
523
 
475
524
  if (!ttl.enabled || authKey === null) {
476
- const fresh = await options.fetchFresh();
525
+ const fresh = await options.fetchFresh(options.signal);
477
526
  return { ...fresh, status: "disabled", fetchedAt: now };
478
527
  }
528
+ const cacheKey = cacheIdentityKey(authKey, options.repo, options.kind, options.number, options.includeComments);
479
529
 
480
530
  // Enforce the *configured* hard TTL against on-disk rows. This is what
481
531
  // makes `github.cache.hardTtlSec` a real retention cap rather than a soft
@@ -496,6 +546,7 @@ export async function getOrFetchView<T>(options: CacheLookupOptions<T>): Promise
496
546
  // Past hard TTL: drop the row eagerly so the on-disk exposure window
497
547
  // is bounded even if `fetchFresh()` then fails (network down, gh
498
548
  // auth lapse, etc.) and we never get to overwrite it.
549
+ retireBackgroundRefresh(cacheKey);
499
550
  invalidate(options.repo, options.kind, options.number, options.includeComments, authKey);
500
551
  } else if (age <= ttl.softMs) {
501
552
  return {
@@ -524,8 +575,9 @@ export async function getOrFetchView<T>(options: CacheLookupOptions<T>): Promise
524
575
  }
525
576
  }
526
577
 
527
- const fresh = await options.fetchFresh();
578
+ const fresh = await options.fetchFresh(options.signal);
528
579
  const fetchedAt = Date.now();
580
+ retireBackgroundRefresh(cacheKey);
529
581
  storeResult(authKey, options.repo, options.kind, options.number, options.includeComments, fresh, fetchedAt);
530
582
  return { ...fresh, status: "miss", fetchedAt };
531
583
  }
@@ -203,6 +203,8 @@ export interface ToolSession {
203
203
  * Runtime discovery only; never threaded into capability loading.
204
204
  */
205
205
  home?: string;
206
+ /** Resolver-owned classification for the session's selected agent directory. */
207
+ profileAuthority?: "default" | "custom";
206
208
  skills?: Skill[];
207
209
  /** Currently executing skill prompt, when this tool session is inside one. */
208
210
  getActiveSkillState?: () => Pick<SkillActiveEntry, "skill" | "session_id"> | undefined;
package/src/tools/job.ts CHANGED
@@ -256,10 +256,21 @@ export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
256
256
  },
257
257
  });
258
258
  };
259
- const progressTimer = onUpdate ? setInterval(emitProgress, PROGRESS_INTERVAL_MS) : undefined;
260
- emitProgress();
259
+ const { promise: progressError, reject: rejectProgress } = Promise.withResolvers<never>();
260
+ racePromises.push(progressError);
261
+ let progressTimer: Timer | undefined;
261
262
 
262
263
  try {
264
+ progressTimer = onUpdate
265
+ ? setInterval(() => {
266
+ try {
267
+ emitProgress();
268
+ } catch (error) {
269
+ rejectProgress(error);
270
+ }
271
+ }, PROGRESS_INTERVAL_MS)
272
+ : undefined;
273
+ emitProgress();
263
274
  if (signal) {
264
275
  const { promise: abortPromise, resolve: abortResolve } = Promise.withResolvers<void>();
265
276
  const onAbort = () => abortResolve();
package/src/tools/read.ts CHANGED
@@ -818,10 +818,22 @@ export async function streamLinesFromFile(
818
818
  fitBytes += separatorBytes + currentLineLength;
819
819
  } else {
820
820
  transitionToSplit();
821
- pushSplit(lineIndex, lineText, currentLineLength, currentLineDropped, capturePartialPreview());
821
+ pushSplit(
822
+ lineIndex,
823
+ lineText,
824
+ currentLineLength,
825
+ currentLineDropped,
826
+ currentLineDropped || currentLineLength > ringBudgetBytes ? capturePartialPreview() : undefined,
827
+ );
822
828
  }
823
829
  } else {
824
- pushSplit(lineIndex, lineText, currentLineLength, currentLineDropped, capturePartialPreview());
830
+ pushSplit(
831
+ lineIndex,
832
+ lineText,
833
+ currentLineLength,
834
+ currentLineDropped,
835
+ currentLineDropped || currentLineLength > ringBudgetBytes ? capturePartialPreview() : undefined,
836
+ );
825
837
  }
826
838
  }
827
839
 
@@ -308,6 +308,7 @@ export function formatDiagnostics(
308
308
  // Count total diagnostics for "... X more" calculation
309
309
  const totalParsedDiags = files.reduce((sum, [, diags]) => sum + diags.length, 0);
310
310
  const totalDiags = totalParsedDiags + unparsed.length;
311
+ const willShowAllRemaining = totalDiags <= maxDiags;
311
312
 
312
313
  // Helper to check if this is the very last item in the tree
313
314
  const isTreeEnd = (fileIdx: number, diagIdx: number | null, unparsedIdx: number | null): boolean => {
@@ -329,9 +330,6 @@ export function formatDiagnostics(
329
330
  for (let fi = 0; fi < files.length && diagsShown < maxDiags; fi++) {
330
331
  const [filePath, diagnostics] = files[fi];
331
332
  // File is "last" only if no more files AND no unparsed AND we'll show all diags AND no "... X more"
332
- const remainingDiagsInFile = diagnostics.length;
333
- const remainingDiagsAfter = files.slice(fi + 1).reduce((sum, [, d]) => sum + d.length, 0) + unparsed.length;
334
- const willShowAllRemaining = diagsShown + remainingDiagsInFile + remainingDiagsAfter <= maxDiags;
335
333
  const isLastFileNode = fi === files.length - 1 && unparsed.length === 0 && willShowAllRemaining;
336
334
  const fileBranch = isLastFileNode ? theme.tree.last : theme.tree.branch;
337
335
 
@@ -106,7 +106,10 @@ const defaultDeps: ResourceGcDeps = {
106
106
  };
107
107
 
108
108
  // ── Controller state (process-global; tabs/browsers are module-global too) ──────────────────
109
- const activeSessions = new Map<string, { settings: Settings; cwd: () => string }>();
109
+ const activeSessions = new Map<
110
+ string,
111
+ { settings: Settings; cwd: () => string; memoryGuardGeneration: symbol; memoryGuardEnabled: boolean }
112
+ >();
110
113
  const scheduler = new MemoryGuardHost({
111
114
  run: async () => {
112
115
  await sweepOnce(deps);
@@ -141,12 +144,26 @@ function resolveSessionSweepIntervalMs(settings: Settings): number {
141
144
  export function registerResourceGcSession(reg: ResourceGcRegistration): () => void {
142
145
  const registeredCwd = reg.cwd;
143
146
  const cwd = typeof registeredCwd === "function" ? registeredCwd : () => registeredCwd ?? process.cwd();
144
- activeSessions.set(reg.sessionId, { settings: reg.settings, cwd });
147
+ const registration = {
148
+ settings: reg.settings,
149
+ cwd,
150
+ memoryGuardGeneration: Symbol(),
151
+ memoryGuardEnabled: resolveMemoryGuardPolicy(reg.settings).enabled,
152
+ };
153
+ activeSessions.set(reg.sessionId, registration);
145
154
  const unregisterSchedule = scheduler.register({
146
155
  ownerId: reg.sessionId,
147
156
  intervalMs: resolveSessionSweepIntervalMs(reg.settings),
148
157
  });
149
158
  const unregisterSettings = reg.settings.onChanged(path => {
159
+ if (path === "memoryGuard.enabled") {
160
+ const enabled = resolveMemoryGuardPolicy(reg.settings).enabled;
161
+ if (enabled !== registration.memoryGuardEnabled) {
162
+ // Fence samples already in flight, even if the guard is re-enabled before they settle.
163
+ registration.memoryGuardGeneration = Symbol();
164
+ registration.memoryGuardEnabled = enabled;
165
+ }
166
+ }
150
167
  if (
151
168
  path === "memoryGuard.enabled" ||
152
169
  path === "memoryGuard.checkIntervalMs" ||
@@ -498,8 +515,8 @@ function sweepMemoryPressureGuard(d: ResourceGcDeps): Promise<void> | undefined
498
515
  }
499
516
  async function sweepEnabledMemoryPressureGuard(d: ResourceGcDeps): Promise<void> {
500
517
  const now = d.monotonicNow();
501
- const dueSessions: Array<{ sessionId: string; policy: MemoryGuardPolicy; cwd: string }> = [];
502
- for (const [sessionId, { settings, cwd: resolveCwd }] of activeSessions) {
518
+ const dueSessions: Array<{ sessionId: string; policy: MemoryGuardPolicy; cwd: string; generation: symbol }> = [];
519
+ for (const [sessionId, { settings, cwd: resolveCwd, memoryGuardGeneration }] of activeSessions) {
503
520
  const policy = resolveMemoryGuardPolicy(settings);
504
521
  const cwd = resolveCwd();
505
522
  if (!policy.enabled) {
@@ -511,14 +528,15 @@ async function sweepEnabledMemoryPressureGuard(d: ResourceGcDeps): Promise<void>
511
528
  const lastEvaluated = memoryGuardLastEvaluatedAt.get(sessionId);
512
529
  if (lastEvaluated !== undefined && now - lastEvaluated < policy.checkIntervalMs) continue;
513
530
  memoryGuardLastEvaluatedAt.set(sessionId, now);
514
- dueSessions.push({ sessionId, policy, cwd });
531
+ dueSessions.push({ sessionId, policy, cwd, generation: memoryGuardGeneration });
515
532
  }
516
533
  if (dueSessions.length === 0) return;
517
534
 
518
535
  const snapshot = await d.memorySnapshot();
519
536
  let gcRequested = false;
520
- const gcTelemetry: Array<{ sessionId: string } & Record<string, unknown>> = [];
521
- for (const { sessionId, policy } of dueSessions) {
537
+ const gcTelemetry: Array<{ sessionId: string; generation: symbol } & Record<string, unknown>> = [];
538
+ for (const { sessionId, policy, generation } of dueSessions) {
539
+ if (activeSessions.get(sessionId)?.memoryGuardGeneration !== generation) continue;
522
540
  const pressure = __selectMemoryPressureDomainForTest(snapshot, policy.policyLimitBytes);
523
541
  const limit = resolveEffectiveMemoryLimit({
524
542
  hardCapBytes: pressure.hardCapBytes,
@@ -548,6 +566,7 @@ async function sweepEnabledMemoryPressureGuard(d: ResourceGcDeps): Promise<void>
548
566
  gcRequested = true;
549
567
  gcTelemetry.push({
550
568
  sessionId,
569
+ generation,
551
570
  parentBytes: pressure.parentBytes,
552
571
  totalUsageBytes: pressure.totalUsageBytes,
553
572
  effectiveLimitBytes: limit.effectiveBytes,
@@ -587,7 +606,8 @@ async function sweepEnabledMemoryPressureGuard(d: ResourceGcDeps): Promise<void>
587
606
  if (gcRequested) {
588
607
  try {
589
608
  d.runGc();
590
- for (const { sessionId, ...telemetry } of gcTelemetry) {
609
+ for (const { sessionId, generation, ...telemetry } of gcTelemetry) {
610
+ if (activeSessions.get(sessionId)?.memoryGuardGeneration !== generation) continue;
591
611
  memoryGuardGcActive.add(sessionId);
592
612
  d.logWarn("Memory guard: GC threshold reached", { sessionId, ...telemetry });
593
613
  }
@@ -76,9 +76,14 @@ export class SkillDiscoveryTool implements AgentTool<typeof skillDiscoverySchema
76
76
  ): Promise<AgentToolResult<SkillDiscoveryToolDetails>> {
77
77
  return untilAborted(signal, async () => {
78
78
  const source = input.source ?? "all";
79
+ const agentDir =
80
+ this.#session.getSessionAgentDir?.() ??
81
+ (this.#session.home === undefined ? this.#session.settings.getAgentDir() : undefined);
79
82
  const result = await discoverRuntimeSkills({
80
83
  cwd: this.#session.cwd,
81
84
  home: this.#session.home,
85
+ agentDir,
86
+ profileAuthority: this.#session.profileAuthority,
82
87
  query: input.query,
83
88
  source,
84
89
  limit: input.limit,
@@ -32,6 +32,10 @@ import { getSkillManifest } from "../gjc-runtime/workflow-manifest";
32
32
  import skillDescription from "../prompts/tools/skill.md" with { type: "text" };
33
33
  import { SKILL_PROMPT_MESSAGE_TYPE } from "../session/messages";
34
34
  import { isCanonicalGjcWorkflowSkill } from "../skill-state/active-state";
35
+ import {
36
+ getSkillFilesystemIdentity,
37
+ isCanonicalGjcWorkflowSkillFilesystemAlias,
38
+ } from "../skill-state/canonical-skills";
35
39
  import type { ToolSession } from ".";
36
40
  import { ToolError } from "./tool-errors";
37
41
 
@@ -157,6 +161,12 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
157
161
  `skill tool: "${requestedName}" is not a valid skill name. The name must be a single concrete skill (e.g. "ralplan"), not a glob or wildcard pattern. Pass one exact skill name as shown in /skill:<name>.`,
158
162
  );
159
163
  }
164
+ if (
165
+ isCanonicalGjcWorkflowSkillFilesystemAlias(requestedName) &&
166
+ getSkillFilesystemIdentity(requestedName) !== requestedName.toLowerCase()
167
+ ) {
168
+ throw new ToolError(`skill tool: refusing filesystem alias for bundled workflow skill "${requestedName}"`);
169
+ }
160
170
  const activeState = this.#session.getActiveSkillState?.();
161
171
  const activeSkill = normalizeSkillName(activeState?.skill);
162
172
  if (activeSkill && requestedName === activeSkill) {
@@ -165,6 +175,9 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
165
175
  );
166
176
  }
167
177
 
178
+ const agentDir =
179
+ this.#session.getSessionAgentDir?.() ??
180
+ (this.#session.home === undefined ? this.#session.settings.getAgentDir() : undefined);
168
181
  const skill =
169
182
  skills.find(s => s.name === requestedName) ??
170
183
  (await findRuntimeSkillByName(
@@ -172,6 +185,8 @@ export class SkillTool implements AgentTool<typeof skillSchema, SkillToolDetails
172
185
  requestedName,
173
186
  this.#getRuntimeSkillPolicy(),
174
187
  this.#session.home,
188
+ agentDir,
189
+ this.#session.profileAuthority,
175
190
  ));
176
191
  if (!skill) {
177
192
  const available = formatAvailableSkills(skills);
@@ -12,6 +12,7 @@ import { ToolError } from "./tool-errors";
12
12
 
13
13
  const DEFAULT_AWAIT_TIMEOUT_MS = 30_000;
14
14
  const MAX_AWAIT_TIMEOUT_MS = 60 * 60 * 1000;
15
+ const MAX_AWAIT_HEARTBEAT_MS = 60_000;
15
16
  /**
16
17
  * Bounded cadence for await liveness: even when the rendered-state signature is
17
18
  * stable (a running subagent emitting no new progress), the await panel re-emits
@@ -47,9 +48,9 @@ const subagentSchema = z.object({
47
48
  .describe("terminal wait condition; defaults to all_terminal"),
48
49
  heartbeat_ms: z
49
50
  .number()
50
- .refine(value => value === 0 || (Number.isInteger(value) && value >= 100 && value <= 5000))
51
+ .refine(value => value === 0 || (Number.isInteger(value) && value >= 100 && value <= MAX_AWAIT_HEARTBEAT_MS))
51
52
  .optional()
52
- .describe("heartbeat interval; 0 disables"),
53
+ .describe(`heartbeat interval in milliseconds (100-${MAX_AWAIT_HEARTBEAT_MS}); 0 disables`),
53
54
  timeout_ms: z.number().min(0).max(MAX_AWAIT_TIMEOUT_MS).optional().describe("await timeout in milliseconds"),
54
55
  limit: z.number().min(1).max(MAX_LIST_LIMIT).optional().describe("maximum subagents to return"),
55
56
  verbosity: z
@@ -2243,7 +2243,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
2243
2243
  "subagent": {
2244
2244
  "name": "subagent",
2245
2245
  "label": "Subagent",
2246
- "description": "Lists, inspects, awaits, pauses, resumes, steers, or cancels detached task subagents.\n\nTask launches return immediately. Use this tool when you need direct control over those running subagents. Prefer `subagent` for task subagents; generic `job` remains available for non-subagent jobs and compatibility fallback access.\n\n`verbosity` controls output size: `receipt` (default) returns status metadata plus a single ≤280-character result/error preview and an `agent://<id>` output ref when available; `preview` returns ≤2000 characters; `full` returns ≤12000 characters and requires explicit `ids`.\n\n# Operations\n\n## `action: \"list\"`\nSnapshot your visible detached subagents, including `running`, `paused`, `queued`, and terminal subagents when retained. Optional `limit` (1–50) caps how many subagents are returned. Output is receipt-only by default; use `verbosity: \"preview\"` for a bounded preview or inspect explicit `ids` with `verbosity: \"full\"` when fuller retained text is necessary.\n\n## `action: \"inspect\"`\nInspect selected subagents by `ids`; omit `ids` to inspect current running subagents. Terminal subagents return receipt-only output by default, with an `agent://<id>` ref when a verified output artifact is available. `verbosity: \"full\"` requires explicit `ids`.\n\n## `action: \"await\"`\nWait for selected subagents by `ids`; omit `ids` to wait for current running subagents.\n- Always set `timeout_ms` when the result is not immediately required forever.\n- Await timeout only bounds this tool call's wait; it does not stop the subagent and is not a failure reason.\n- On timeout, inspect progress and keep doing independent work. Never cancel just because an await timed out; cancel only if the subagent has actually failed, gone off-track, or become unrecoverably wrong.\n- Completed results are receipt-first by default: bounded preview plus `agent://<id>` output ref when available, not full retained output.\n\n## `action: \"pause\"`\nRequest a graceful safe-boundary pause for selected subagents by `ids`.\n- Non-running subagents are a no-op and return their current status snapshot.\n- A paused subagent keeps its session context and can be resumed later.\n\n## `action: \"resume\"`\nResume one subagent by `id` (preferred) or a single-item `ids` array.\n- Optional `message` is delivered into that one resumed run.\n- Running subagents are a no-op and return their current status snapshot.\n- Terminal subagents require `message` to start a follow-up resume run; without `message`, the tool returns the current snapshot with guidance.\n- `paused` subagents resume from saved context; `queued` subagents are already waiting for capacity.\n- Multiple targets are rejected because one global `message` must not broadcast to several subagents.\n\n## `action: \"steer\"`\nSend a non-empty `message` to one subagent by `id` (preferred) or a single-item `ids` array.\n- A running subagent receives the message through its live handle.\n- Optional `pause: true` requests a safe-boundary pause after steering a running subagent.\n- `pause` only matters while the target is running.\n- A non-active subagent (`paused`, `queued`, or terminal) automatically resumes with the message; `pause` is ignored for that target.\n- Multiple targets are rejected because one global `message` must not broadcast to several subagents.\n\n## `action: \"cancel\"`\nStop selected subagents by `ids`, including running, paused, or queued subagents.\n- Use only when the subagent has actually failed, gone off-track, or become unrecoverably wrong; an await timeout alone is never a cancellation reason.\n- Cancellation keeps the subagent session file for possible later context recovery.\n\n# Statuses\n\n- `running` — currently executing.\n- `paused` — stopped at a safe boundary with resumable context.\n- `queued` — resume requested and waiting for execution capacity.\n- `completed` — finished successfully.\n- `failed` — finished with an error.\n- `cancelled` — stopped by cancellation.\n- `not_found` — no visible subagent matches the requested id.",
2246
+ "description": "Lists, inspects, awaits, pauses, resumes, steers, or cancels detached task subagents.\n\nTask launches return immediately. Use this tool when you need direct control over those running subagents. Prefer `subagent` for task subagents; generic `job` remains available for non-subagent jobs and compatibility fallback access.\n\n`verbosity` controls output size: `receipt` (default) returns status metadata plus a single ≤280-character result/error preview and an `agent://<id>` output ref when available; `preview` returns ≤2000 characters; `full` returns ≤12000 characters and requires explicit `ids`.\n\n# Operations\n\n## `action: \"list\"`\nSnapshot your visible detached subagents, including `running`, `paused`, `queued`, and terminal subagents when retained. Optional `limit` (1–50) caps how many subagents are returned. Output is receipt-only by default; use `verbosity: \"preview\"` for a bounded preview or inspect explicit `ids` with `verbosity: \"full\"` when fuller retained text is necessary.\n\n## `action: \"inspect\"`\nInspect selected subagents by `ids`; omit `ids` to inspect current running subagents. Terminal subagents return receipt-only output by default, with an `agent://<id>` ref when a verified output artifact is available. `verbosity: \"full\"` requires explicit `ids`.\n\n## `action: \"await\"`\nWait for selected subagents by `ids`; omit `ids` to wait for current running subagents.\n- Always set `timeout_ms` when the result is not immediately required forever.\n- `heartbeat_ms` accepts integer intervals from 100 to 60,000 milliseconds (for example, 10,000 for ten seconds); `0` disables heartbeat updates and omission retains the 500 ms default. Terminal completion is event-driven, not delayed until the next heartbeat.\n- Await timeout only bounds this tool call's wait; it does not stop the subagent and is not a failure reason.\n- On timeout, inspect progress and keep doing independent work. Never cancel just because an await timed out; cancel only if the subagent has actually failed, gone off-track, or become unrecoverably wrong.\n- Completed results are receipt-first by default: bounded preview plus `agent://<id>` output ref when available, not full retained output.\n\n## `action: \"pause\"`\nRequest a graceful safe-boundary pause for selected subagents by `ids`.\n- Non-running subagents are a no-op and return their current status snapshot.\n- A paused subagent keeps its session context and can be resumed later.\n\n## `action: \"resume\"`\nResume one subagent by `id` (preferred) or a single-item `ids` array.\n- Optional `message` is delivered into that one resumed run.\n- Running subagents are a no-op and return their current status snapshot.\n- Terminal subagents require `message` to start a follow-up resume run; without `message`, the tool returns the current snapshot with guidance.\n- `paused` subagents resume from saved context; `queued` subagents are already waiting for capacity.\n- Multiple targets are rejected because one global `message` must not broadcast to several subagents.\n\n## `action: \"steer\"`\nSend a non-empty `message` to one subagent by `id` (preferred) or a single-item `ids` array.\n- A running subagent receives the message through its live handle.\n- Optional `pause: true` requests a safe-boundary pause after steering a running subagent.\n- `pause` only matters while the target is running.\n- A non-active subagent (`paused`, `queued`, or terminal) automatically resumes with the message; `pause` is ignored for that target.\n- Multiple targets are rejected because one global `message` must not broadcast to several subagents.\n\n## `action: \"cancel\"`\nStop selected subagents by `ids`, including running, paused, or queued subagents.\n- Use only when the subagent has actually failed, gone off-track, or become unrecoverably wrong; an await timeout alone is never a cancellation reason.\n- Cancellation keeps the subagent session file for possible later context recovery.\n\n# Statuses\n\n- `running` — currently executing.\n- `paused` — stopped at a safe boundary with resumable context.\n- `queued` — resume requested and waiting for execution capacity.\n- `completed` — finished successfully.\n- `failed` — finished with an error.\n- `cancelled` — stopped by cancellation.\n- `not_found` — no visible subagent matches the requested id.",
2247
2247
  "parameters": {
2248
2248
  "type": "object",
2249
2249
  "properties": {
@@ -2288,7 +2288,7 @@ export const TOOL_CATALOG: Readonly<Record<string, ToolCatalogEntry>> = {
2288
2288
  ]
2289
2289
  },
2290
2290
  "heartbeat_ms": {
2291
- "description": "heartbeat interval; 0 disables",
2291
+ "description": "heartbeat interval in milliseconds (100-60000); 0 disables",
2292
2292
  "type": "number"
2293
2293
  },
2294
2294
  "timeout_ms": {
@@ -70,9 +70,9 @@ export function guardToolForUltragoalAsk<T extends AgentTool>(
70
70
  const candidate = tool as GuardedTool;
71
71
  if (candidate[ULTRAGOAL_ASK_GUARD]) return tool;
72
72
  const wrapped = new Proxy(tool, {
73
- get(target, prop, receiver) {
73
+ get(target, prop) {
74
74
  if (prop === ULTRAGOAL_ASK_GUARD) return true;
75
- if (prop !== "execute") return Reflect.get(target, prop, receiver);
75
+ if (prop !== "execute") return Reflect.get(target, prop);
76
76
  return async (...args: unknown[]): Promise<unknown> => {
77
77
  // The wrapper runs BEFORE AskTool.execute(): resolve the nudge
78
78
  // budget against the SESSION profile, never the process-global one.