agent-accelerator 0.5.7 → 0.5.8

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.
package/README.md CHANGED
@@ -136,7 +136,7 @@ An `Agent` holds:
136
136
  | `runRetries` | `number` | A run that FAILED for a transient reason (provider down or overloaded, timeouts, a cut stream, after `retry` gave up) is tried again from where it stopped, waiting 5 s, 15 s, 45 s, then 60 s. Default `3`, `0` = off. Finished tool calls are not repeated; the response reports the failed attempts' tool calls and usage. Never for aborts, 400/401/403/404, bad config, budgets or limits. `run()` and queued runs only, not `stream()`. |
137
137
  | `resume` | `"automatic" \| "explicit"` | Crash recovery (needs `persist: { dir }` + the same `sessionId`): `automatic` (default) continues a cut-off run when the Agent is constructed, `explicit` waits for `agent.resume()`. Finished tool calls are never run again. `agent.interrupted()` lists cut-off runs. |
138
138
  | `persist` | `{ dir?: string; file?: string }` | Durable sessions, saved after every step (user/assistant/tool/sub-agent). `{ dir }` keeps `sessions/<sessionId>/session.json` (snapshot) plus an append-only `journal.ndjson` (one line per step, so a long chat costs the same per step as a short one), `{ file }` a single `.session.json`, `{}` uses `<home>/sessions`. Writes are durable (fsync, `.bak` of the last good file, atomic rename) and never fail a turn. |
139
- | `home` | `string` | Folder for everything the SDK writes to disk (`sessions/`, `scheduled_actions/`, `telemetry/`). Defaults to `setHome(dir)`, then the `AGENT_ACCEL_HOME` env var, then `.agent-accel/` in the working directory. An explicit `persist.dir` / `persist.file` still wins. |
139
+ | `home` | `string` | Folder for everything the SDK writes to disk (`sessions/`, `scheduled/`, `telemetry/`). Defaults to `setHome(dir)`, then the `AGENT_ACCEL_HOME` env var, then `.agent-accel/` in the working directory. An explicit `persist.dir` / `persist.file` still wins. |
140
140
 
141
141
  When `dynamicSubagents.enabled` is set without a resolvable worker model, construction throws `SubAgentModelError`.
142
142
 
@@ -390,10 +390,10 @@ Every function except `createScheduledAction` finds the home folder through `set
390
390
 
391
391
  **How it behaves**
392
392
 
393
- * **One local clock.** A single timer per home folder (guarded by a one-process lock, `<home>/.lock`) ticks once a second against the machine's own time and only asks what is overdue, so drift or sleep cannot skip a run. There is no timezone or daylight-saving logic. The timer is `unref`'d and runs only while something is pending, so actions run only while your process is alive.
394
- * **The creating `Agent` runs it.** The task stores the Agent's `name`; each run goes to the live Agent with that name (latest wins), using its worker model and tool pool. The scheduler holds only a weak reference, so keep your `Agent` referenced; if none is alive, the run counts as missed.
393
+ * **One local clock.** A single timer per process and home folder ticks once a second against the machine's own time and only asks what is overdue, so drift or sleep cannot skip a run. There is no timezone or daylight-saving logic. The timer is `unref`'d and runs only while something is pending, so actions run only while your process is alive.
394
+ * **The creating `Agent` runs it.** The task stores the Agent's `name`; each run goes to the live Agent with that name (latest wins), using its worker model and tool pool. The scheduler holds only a weak reference, so keep your `Agent` referenced; if none is alive, the run counts as missed once its delivery time has passed. Several processes may share one home: only the process that registered the Agent starts, delivers and recovers its actions, each action has its own folder and short lock, and a run whose owner died is recovered by another process that owns the Agent.
395
395
  * **Each run is fresh.** A new stateless dynamic sub-agent built from the saved fields, on your fixed worker model and tool pool, never touching the Main Agent's session. It inherits the parent's limits, retry policy, `fileAccess` and `captureRaw`.
396
- * **Files are the truth.** Everything lives in `<home>/scheduled_actions/` (the format is a stable contract with a changelog: [docs/scheduled-action-files.md](docs/scheduled-action-files.md)): `tasks.json` (every action), `deliveries.json` (saved results waiting for their delivery time) and one folder per action with one `.md` file per run (a small header, then the result). The file is re-read and validated before each run, so editing `instructions` or `task` there takes effect on the next run. API keys, the model and tool code are never stored. Don't put secrets in `instructions`.
396
+ * **Files are the truth.** Everything lives in `<home>/scheduled/` (the format is a stable contract with a changelog: [docs/scheduled-action-files.md](docs/scheduled-action-files.md)): one folder per action holding its `task.json`, a `delivery.json` while a saved result waits for its delivery time, and one `.md` file per run (a small header, then the result). The file is re-read and validated before each run, so editing `instructions` or `task` there takes effect on the next run. API keys, the model and tool code are never stored. Don't put secrets in `instructions`.
397
397
  * **What a run costs.** Each run file's header records the run's exact spending, the same numbers `usageReport()` keeps for workers: `turns`, `tokens` (input + output), `inputTokens`, `freshInputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `outputTokens`, `thinkingTokens`, `cacheHitRate` (0 to 1), `costUsd`, `costSource` (`provider`, `catalog`, `mixed` or `unknown`) and `unpricedTurns` (only when above 0, then `costUsd` is a lower bound). Unknown pricing is `costUsd: unknown`, never 0. The numbers cover every attempt of the run (`retries` included) and are recorded for failed runs too. The report itself does not list scheduled runs.
398
398
  * **Time limit of a run.** An action may carry `timeoutMs` (top level of the action, not inside `scheduled_action`): a run that takes longer is stopped and recorded as failed. By default there is no limit. `dynamicSubagents.workerTimeoutMs` is an optional ceiling: an action cannot ask for more, and it is the limit for actions that set none (`0` and `Infinity` mean no ceiling). The former `dynamicSubagents.timeout`, `modelMaySetTimeout` and the `-1` mode were removed; passing them throws a `ConfigError` that says what to use.
399
399
  * **Ready, then delivered.** A finished run is saved immediately (`scheduled_action.ready`); at `timeToDeliver` the SDK emits `scheduled_action.delivered` on `telemetryBus` with `{ id, name, status, file, readyAt, deliverAt, late }`. There is no notification system: what happens next is your code. Other events: `.created`, `.updated`, `.started`, `.failed`, `.missed`, `.skipped`, `.deleted`, plus the warnings `.invalid` and `.store_corrupt`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-accelerator",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "High-performance SDK for AI agents with a unified API across AI providers. Optimized for high cache hit rates, observability, and seamless agent orchestration. Built to power production-grade agentic systems and harnesses for teams at any scale.",
5
5
  "license": "MIT",
6
6
  "author": "Sashvat Bharat",
@@ -22,7 +22,7 @@ import {
22
22
  import * as path from "node:path";
23
23
  import { registerAgent } from "../scheduler/scheduler.ts";
24
24
  import { resolveHome } from "../scheduler/store.ts";
25
- import { persistTelemetry } from "../telemetry-file.ts";
25
+ import { persistTelemetry, registerSessionFolder, unregisterSessionFolder } from "../telemetry-file.ts";
26
26
  import { lockFolder } from "../utils/durable.ts";
27
27
  import { createScheduledActionsTool, getDatetimeTool } from "../scheduler/tool.ts";
28
28
  import { applyEditable, assertModelAllowed, modelKey, toEditableConfig } from "./config-blocks.ts";
@@ -66,7 +66,7 @@ export function resolveEffectiveThinking(
66
66
  import { ConfigError, SessionLoadError, isAbortError } from "../types/errors.ts";
67
67
 
68
68
  /**
69
- * Disposers for the manual user-signal forward listeners (Q-21 fallback when
69
+ * Disposers for the manual user-signal forward listeners (fallback when
70
70
  * `AbortSignal.any` is unavailable). Keyed by run controller so no expando
71
71
  * properties are attached to AbortController instances.
72
72
  */
@@ -75,12 +75,12 @@ const mergeDisposers = new WeakMap<AbortController, () => void>();
75
75
  /** Waits before run retry 1, 2, 3 ... (the last one repeats). Module-level so tests can shrink it; not public API. */
76
76
  export const RUN_RETRY_DELAYS_MS: number[] = [5_000, 15_000, 45_000, 60_000];
77
77
 
78
- /** Max named sessions kept per Agent (Q-19a LRU). */
78
+ /** Max named sessions kept per Agent (LRU). */
79
79
  const SESSION_LRU_MAX = 100;
80
- /** Idle TTL for named sessions (Q-19a). */
80
+ /** Idle TTL for named sessions. */
81
81
  const SESSION_TTL_MS = 60 * 60 * 1000;
82
82
 
83
- /** Normalized per-run limits (Q-22). Sub-agents inherit these unless overridden. */
83
+ /** Normalized per-run limits. Sub-agents inherit these unless overridden. */
84
84
  export interface AgentRunLimits {
85
85
  maxTurns?: number;
86
86
  deadlineMs?: number;
@@ -91,7 +91,7 @@ export interface AgentRunLimits {
91
91
  streamIdleTimeoutMs: number;
92
92
  }
93
93
 
94
- /** Resolves `(config as any).limits` with Q-22 defaults (timeouts tolerate reasoning). */
94
+ /** Resolves `(config as any).limits` with defaults (timeouts tolerate reasoning). */
95
95
  function resolveAgentLimits(config: { limits?: Record<string, unknown> }): AgentRunLimits {
96
96
  const raw = (config as { limits?: Record<string, unknown> }).limits ?? {};
97
97
  const num = (v: unknown): number | undefined =>
@@ -181,7 +181,7 @@ export class Agent {
181
181
  /** Per-run spending cap of each worker (unset = unlimited). */
182
182
  workerBudget?: { maxCostUsd?: number; maxTotalTokens?: number };
183
183
  };
184
- /** Home folder for everything written to disk (`sessions/`, `scheduled_actions/`, `telemetry/`). */
184
+ /** Home folder for everything written to disk (`sessions/`, `scheduled/`, `telemetry/`). */
185
185
  readonly home: string;
186
186
  /** Registered model-callable tools. */
187
187
  readonly tools: Record<string, ToolDefinition> = {};
@@ -240,10 +240,10 @@ export class Agent {
240
240
  readonly persistConfig?: { dir?: string; file?: string };
241
241
  /** Mid-session queue policy for new queries arriving while a run is active. */
242
242
  readonly midSessionConfig: Required<Pick<MidSessionConfig, "mode">> & MidSessionConfig & { maxSteers?: number };
243
- /** Max buffered steer entries per run (Q-20, default 20). */
243
+ /** Max buffered steer entries per run (default 20). */
244
244
  readonly maxSteers: number;
245
245
  /**
246
- * Normalized run limits (Q-22). Read from `(config as any).limits`;
246
+ * Normalized run limits. Read from `(config as any).limits`;
247
247
  * sub-agents inherit these unless overridden. `maxTurns`/`deadlineMs`/
248
248
  * `maxToolCallsPerRun` default to unlimited; timeouts default to
249
249
  * request 300s / first-byte 120s / idle 300s (tolerate reasoning).
@@ -265,9 +265,9 @@ export class Agent {
265
265
  /** Put here by `interrupt(message)`: runs before every other waiting prompt. */
266
266
  urgent?: boolean;
267
267
  }> = [];
268
- /** Named session contexts for `sessionId` runs (Q-19a, LRU 100 + TTL). */
268
+ /** Named session contexts for `sessionId` runs (LRU 100 + TTL). */
269
269
  private sessions = new Map<string, { context: AgentContext; lastAccess: number }>();
270
- /** Coalesced persistence single-flight flag (Q-28). */
270
+ /** Coalesced persistence single-flight flag. */
271
271
  private persistScheduled = false;
272
272
  /** Step journal of the persisted session folder (dir mode only). */
273
273
  private journal?: SessionJournal;
@@ -384,11 +384,6 @@ export class Agent {
384
384
  config.persist && !config.persist.dir && !config.persist.file
385
385
  ? { ...config.persist, dir: path.join(this.home, "sessions") }
386
386
  : config.persist;
387
- // One process per folder: fail fast here, before anything is written (the home for `persist: {}`, else the folder named).
388
- if (config.persist) {
389
- const { dir, file } = config.persist;
390
- lockFolder(dir ?? (file ? path.dirname(path.resolve(file)) : this.home));
391
- }
392
387
  // `whenBusy` (canonical) wins over legacy `mode`; anything else is "auto".
393
388
  const normalizedMode = resolveWhenBusy(config.midSession);
394
389
  const rawMaxQueued = (config.midSession as any)?.maxQueued;
@@ -402,9 +397,9 @@ export class Agent {
402
397
  ? { maxSteers: Math.floor(rawMaxSteers as number) }
403
398
  : { maxSteers: 20 }),
404
399
  };
405
- // Q-20: bound on buffered steer entries (default 20).
400
+ // Bound on buffered steer entries (default 20).
406
401
  this.maxSteers = this.midSessionConfig.maxSteers ?? 20;
407
- // Q-22: run limits (sub-agents inherit unless overridden).
402
+ // Run limits (sub-agents inherit unless overridden).
408
403
  this.runLimits = resolveAgentLimits(config as unknown as { limits?: Record<string, unknown> });
409
404
 
410
405
  // DX4: single thinkingLevel flag — also inherit from ModelProviderInstance when omitted
@@ -463,6 +458,8 @@ export class Agent {
463
458
  ? { ...(config.fileAccess.roots ? { roots: [...config.fileAccess.roots] } : {}) }
464
459
  : undefined;
465
460
  this.stateless = config.stateless ?? false;
461
+ // One process per session folder: fail fast here, before anything is written (another process may use other sessions of the same home).
462
+ this.lockSessionFolder();
466
463
 
467
464
  // Tools registration (unified normalization shared with dynamic pool).
468
465
  if (config.tools) {
@@ -498,6 +495,7 @@ export class Agent {
498
495
  assertNoToolCollisions(this.tools);
499
496
 
500
497
  this.context = new AgentContext(this.getFullInstructions());
498
+ this.registerTelemetryFolder();
501
499
  if (dyn?.allowScheduledActions) registerAgent(this);
502
500
  if (this.recoverSession() && this.resumeMode === "automatic") {
503
501
  // Synchronously, so a run() issued right after construction queues BEHIND the resumed run.
@@ -510,6 +508,34 @@ export class Agent {
510
508
  return (this.workerRegistry ??= new WorkerRegistry(this));
511
509
  }
512
510
 
511
+ // Takes the one-process lock of the folder this Agent persists to: its own session folder, or the folder of a single `persist.file`; throws ConfigError when another live process holds it; in: the constructor and importSession (a new session id is a new folder), out: a `.lock` file there.
512
+ private lockSessionFolder(): void {
513
+ const cfg = this.persistConfig;
514
+ if (!cfg || this.stateless) return;
515
+ if (cfg.dir) {
516
+ let dir: string;
517
+ try {
518
+ dir = sessionDirFor(cfg.dir, this.sessionId);
519
+ } catch {
520
+ return; // not a usable session id for a folder name, so nothing is saved under it either
521
+ }
522
+ lockFolder(dir);
523
+ } else if (cfg.file) {
524
+ lockFolder(path.dirname(path.resolve(cfg.file)));
525
+ }
526
+ }
527
+
528
+ // Tells the telemetry sink where this Agent's session folder is (only for a session persisted to a folder), so its events are written next to its journal; in: the constructor and importSession, out: the telemetry routing map.
529
+ private registerTelemetryFolder(): void {
530
+ const root = this.persistConfig?.dir;
531
+ if (!root || this.stateless) return;
532
+ try {
533
+ registerSessionFolder(this.sessionId, sessionDirFor(root, this.sessionId));
534
+ } catch {
535
+ // why: not a usable session id for a folder name, so nothing is saved under it either.
536
+ }
537
+ }
538
+
513
539
  // Loads a persisted session that a crash cut off (a run without an ending) or that still had waiting prompts, so it can be resumed; true when there is something to resume; in: the session folder of `persist.dir`, out: context, runMarks, toolRecords, pendingQueue.
514
540
  private recoverSession(): boolean {
515
541
  const root = this.persistConfig?.dir;
@@ -678,13 +704,13 @@ export class Agent {
678
704
  this.context.thoughtSignatures = [];
679
705
  this.context.systemPrompt = this.getFullInstructions();
680
706
  this.runMarks.length = 0;
681
- // Q-25: reset also drops session-scoped provider chaining/routing.
707
+ // Reset also drops session-scoped provider chaining/routing.
682
708
  try { clearInteractionChains(sid); } catch {}
683
709
  try { clearSessionRouting(sid); } catch {}
684
710
  }
685
711
 
686
712
  /**
687
- * Returns a named session handle (Q-19a). `session()` creates an isolated
713
+ * Returns a named session handle. `session()` creates an isolated
688
714
  * `AgentContext` bound to `id` (separate histories per id); `agent.run`
689
715
  * with `{ sessionId }` selects/creates the same entry. Sessions are
690
716
  * ephemeral in-memory (LRU 100 + 1h TTL). `agent.run`/`stream` without a
@@ -696,14 +722,15 @@ export class Agent {
696
722
  return { id: sid, context: ctx, runController: null };
697
723
  }
698
724
 
699
- /** Drops one named session (Q-19a). No-op for unknown ids. */
725
+ /** Drops one named session. No-op for unknown ids. */
700
726
  disposeSession(id: string): void {
727
+ unregisterSessionFolder(id);
701
728
  try { this.sessions.delete(id); } catch {}
702
729
  try { clearInteractionChains(id); } catch {}
703
730
  try { clearSessionRouting(id); } catch {}
704
731
  }
705
732
 
706
- /** Drops all named sessions except the default context (Q-19a). */
733
+ /** Drops all named sessions except the default context. */
707
734
  clearSessions(): void {
708
735
  try {
709
736
  for (const id of [...this.sessions.keys()]) {
@@ -753,7 +780,7 @@ export class Agent {
753
780
  return fresh;
754
781
  }
755
782
 
756
- /** Rejects + drops all queued follow-ups (Q-20). Returns the cleared count. */
783
+ /** Rejects + drops all queued follow-ups. Returns the cleared count. */
757
784
  clearQueue(): number {
758
785
  const n = this.pendingQueue.length;
759
786
  if (n === 0) return 0;
@@ -767,7 +794,7 @@ export class Agent {
767
794
  return n;
768
795
  }
769
796
 
770
- /** Throws when the steer inbox is at its Q-20 bound. */
797
+ /** Throws when the steer inbox is at its bound. */
771
798
  private assertSteerRoom(inbox: SteerEntry[]): void {
772
799
  const max = this.maxSteers ?? 20;
773
800
  if (inbox.length >= max) {
@@ -964,7 +991,7 @@ export class Agent {
964
991
  const next = this.pendingQueue.shift();
965
992
  if (!next) return;
966
993
  this.persistNow();
967
- // Q-20: reject queued items whose signal aborted before start.
994
+ // Reject queued items whose signal aborted before start.
968
995
  try {
969
996
  if ((next.options as { signal?: AbortSignal } | undefined)?.signal?.aborted) {
970
997
  const reason = ((next.options as { signal?: AbortSignal }).signal as unknown as { reason?: unknown })?.reason;
@@ -1016,7 +1043,7 @@ export class Agent {
1016
1043
 
1017
1044
  // Ends the active run slot: leftover steers become follow-ups at the FRONT of the queue (behind urgent ones), then the queue is pumped; in: a settled run, out: pendingQueue + persistNow.
1018
1045
  private finishCurrentRun(): void {
1019
- // Q-20: leftover steer entries become queued follow-ups instead of dropping.
1046
+ // Leftover steer entries become queued follow-ups instead of dropping.
1020
1047
  let leftover: SteerEntry[] = [];
1021
1048
  try { leftover = this.currentRun?.steerInbox?.splice(0) ?? []; } catch { leftover = []; }
1022
1049
  this.currentRun = null;
@@ -1080,7 +1107,7 @@ export class Agent {
1080
1107
  }
1081
1108
  return runController.signal;
1082
1109
  }
1083
- // Q-21: prefer AbortSignal.any (no expando, no listener leak).
1110
+ // Prefer AbortSignal.any (no expando, no listener leak).
1084
1111
  try {
1085
1112
  const anyFn = (AbortSignal as unknown as { any?: (signals: AbortSignal[]) => AbortSignal }).any;
1086
1113
  if (typeof anyFn === "function") {
@@ -1233,7 +1260,7 @@ export class Agent {
1233
1260
  const steerInbox: SteerEntry[] = [];
1234
1261
  const mergedSignal = this.mergeUserSignal(options?.signal, runController);
1235
1262
  const runOptions = this.withResolvedThinking({ ...options, signal: mergedSignal });
1236
- // Q-19a: route to the named session context (default = this.context).
1263
+ // Route to the named session context (default = this.context).
1237
1264
  const targetContext = this.getSessionContext(runOptions?.sessionId || this.sessionId);
1238
1265
  const task = (async (): Promise<AgentResponse> => {
1239
1266
  await ensureModelCatalogFresh();
@@ -1274,7 +1301,7 @@ export class Agent {
1274
1301
  ...(resumeMark ? { resume: { stored: this.storedToolResults() } } : {}),
1275
1302
  steerInbox,
1276
1303
  onSteerInjected: () => this.persistNow(),
1277
- // Q-22: loop enforces these (plus local timeout races).
1304
+ // Loop enforces these (plus local timeout races).
1278
1305
  limits: { ...this.runLimits },
1279
1306
  ...(this.output !== undefined ? { output: this.output } : {}),
1280
1307
  };
@@ -1333,7 +1360,7 @@ export class Agent {
1333
1360
  if (effectiveLevel) {
1334
1361
  validateModelThinking(resolved.provider.id, resolved.modelId, effectiveLevel);
1335
1362
  }
1336
- // Q-19a: named session routing (default session for compat).
1363
+ // Named session routing (default session for compat).
1337
1364
  const targetContext = this.getSessionContext(options?.sessionId || this.sessionId);
1338
1365
  const mark = this.prepareTurn(prompt, options, targetContext);
1339
1366
  this.persistNow();
@@ -1365,7 +1392,7 @@ export class Agent {
1365
1392
  openToolLog: (call: { name: string }, sid: string | undefined) => this.openToolLog(call, sid),
1366
1393
  steerInbox,
1367
1394
  onSteerInjected: () => this.persistNow(),
1368
- // Q-22: loop enforces these (plus local timeout races).
1395
+ // Loop enforces these (plus local timeout races).
1369
1396
  limits: { ...this.runLimits },
1370
1397
  ...(this.output !== undefined ? { output: this.output } : {}),
1371
1398
  };
@@ -1455,14 +1482,14 @@ export class Agent {
1455
1482
  * Called automatically after every step during runs; safe to call
1456
1483
  * manually. Never throws (failures route to `onPersistError`).
1457
1484
  *
1458
- * Q-28: stateless agents skip persistence unless
1485
+ * Stateless agents skip persistence unless
1459
1486
  * `(persist as any).allowStateless` is set. Calls in the same tick are
1460
1487
  * coalesced via a `queueMicrotask` single flight.
1461
1488
  */
1462
1489
  persistNow(): void {
1463
1490
  const cfg = this.persistConfig;
1464
1491
  if (!cfg || (!cfg.dir && !cfg.file)) return;
1465
- // Q-28: no stateless persist by default.
1492
+ // No stateless persist by default.
1466
1493
  try {
1467
1494
  if (this.stateless && !(cfg as unknown as { allowStateless?: unknown }).allowStateless) return;
1468
1495
  } catch {}
@@ -1504,7 +1531,7 @@ export class Agent {
1504
1531
  if (cfg.dir) this.journalFor(cfg.dir).sync(this as any);
1505
1532
  else if (cfg.file) saveSessionFile(cfg.file, this as any, null);
1506
1533
  } catch (err) {
1507
- // Q-28: surface via hook instead of swallowing silently.
1534
+ // Surface via hook instead of swallowing silently.
1508
1535
  try { (cfg as unknown as { onPersistError?: (e: unknown) => void }).onPersistError?.(err); } catch {}
1509
1536
  }
1510
1537
  }
@@ -1574,6 +1601,16 @@ export class Agent {
1574
1601
  this.warnOnLocalMediaPaths(data.messages ?? []);
1575
1602
  const prevSid = this.sessionId;
1576
1603
  if (data.sessionId) (this as any).sessionId = data.sessionId;
1604
+ if (this.sessionId !== prevSid) {
1605
+ try {
1606
+ this.lockSessionFolder();
1607
+ } catch (err) {
1608
+ (this as any).sessionId = prevSid; // the session belongs to another process: keep ours untouched
1609
+ throw err;
1610
+ }
1611
+ unregisterSessionFolder(prevSid);
1612
+ this.registerTelemetryFolder();
1613
+ }
1577
1614
  if ((data as any).parentSessionId) (this as any).parentSessionId = (data as any).parentSessionId;
1578
1615
  if ((data as any).subagents && typeof (data as any).subagents === "object") {
1579
1616
  try {
@@ -1617,7 +1654,7 @@ export class Agent {
1617
1654
  } else {
1618
1655
  this.context.systemPrompt = this.getFullInstructions();
1619
1656
  }
1620
- // Q-25: importing a snapshot invalidates prior provider chaining/routing
1657
+ // Importing a snapshot invalidates prior provider chaining/routing
1621
1658
  // for both the previous and the restored session.
1622
1659
  try { clearInteractionChains(prevSid); } catch {}
1623
1660
  try { clearSessionRouting(prevSid); } catch {}
@@ -1653,7 +1690,7 @@ export class Agent {
1653
1690
 
1654
1691
  // Adds the user prompt to the target context (fresh context when stateless, stable system prompt, additionalContext as a user prefix) and marks the run start for the default session; in: a run start, out: the new SessionRunMark.
1655
1692
  private prepareTurn(prompt: string | ContentPart[], options?: AgentRunOptions, target?: AgentContext): SessionRunMark | undefined {
1656
- // Q-20: prompt validation throws synchronously in both run() and
1693
+ // Prompt validation throws synchronously in both run() and
1657
1694
  // stream() (same timing; documented here).
1658
1695
  if (Array.isArray(prompt) && prompt.length === 0) {
1659
1696
  throw new Error(
@@ -1662,7 +1699,7 @@ export class Agent {
1662
1699
  }
1663
1700
  const ctx = target ?? this.getSessionContext(options?.sessionId || this.sessionId);
1664
1701
  if (this.stateless) {
1665
- // Q-19a: stateless runs use a fresh ephemeral context (never shared).
1702
+ // Stateless runs use a fresh ephemeral context (never shared).
1666
1703
  ctx.messages = [];
1667
1704
  ctx.thoughtSignatures = [];
1668
1705
  }
@@ -1728,7 +1765,7 @@ export class Agent {
1728
1765
  prompt: string | ContentPart[],
1729
1766
  options?: AgentRunOptions
1730
1767
  ): Promise<AgentResponse> | AssistantMessageEventStream {
1731
- // Q-20: prompt validation throws synchronously in both run() and
1768
+ // Prompt validation throws synchronously in both run() and
1732
1769
  // stream() (same timing; async work starts only after validation).
1733
1770
  this.assertPromptValid(prompt);
1734
1771
  const isStream = options?.stream === true;
@@ -1866,7 +1903,7 @@ export class Agent {
1866
1903
  prompt: string | ContentPart[],
1867
1904
  options?: AgentRunOptions
1868
1905
  ): AssistantMessageEventStream {
1869
- // Q-20: same sync validation timing as run().
1906
+ // Same sync validation timing as run().
1870
1907
  this.assertPromptValid(prompt);
1871
1908
  // Mid-session: busy agent serializes via steer (same turn) or queue (next turn).
1872
1909
  if (this.currentRun) {
@@ -170,8 +170,8 @@ export class AgentContext {
170
170
  }
171
171
 
172
172
  /**
173
- * Captures a detached snapshot of conversation state for run-level rollback
174
- * (Q-19). The snapshot is a structural deep copy (NOT JSON): `Uint8Array` /
173
+ * Captures a detached snapshot of conversation state for run-level rollback.
174
+ * The snapshot is a structural deep copy (NOT JSON): `Uint8Array` /
175
175
  * `ArrayBuffer` media must survive a rollback byte-for-byte, and large
176
176
  * base64 strings are shared by reference rather than re-serialized.
177
177
  */
@@ -185,7 +185,7 @@ export class AgentContext {
185
185
  /**
186
186
  * Restores a checkpoint captured by `checkpoint()`. Malformed checkpoints
187
187
  * are ignored (current state kept). When `sessionId` is provided, also
188
- * clears session-scoped provider state (Q-25) so a rollback never resumes
188
+ * clears session-scoped provider state so a rollback never resumes
189
189
  * a stale interaction chain.
190
190
  */
191
191
  rollback(cp: { messages: Message[]; signatures: string[] }, sessionId?: string): void {
@@ -74,7 +74,7 @@ export function isSessionDescendant(child: string, parent: string): boolean {
74
74
  if (!child || !parent) return false;
75
75
  if (child === parent) return true;
76
76
  if (child.startsWith(parent + "-")) return true;
77
- // Q-34: hash includes() is collision-prone; require delimited match to reduce spoofing. Explicit {rootSessionId,parentSessionId} lineage is Q-61 future.
77
+ // Hash includes() is collision-prone; require delimited match to reduce spoofing. Explicit {rootSessionId,parentSessionId} lineage is future work.
78
78
  const h = hashSessionPart(parent);
79
79
  return child.includes(`-${h}-`) || child.endsWith(`-${h}`);
80
80
  }
@@ -101,13 +101,13 @@ export interface SubAgentTrace {
101
101
  const subagentTraces = new Map<string, SubAgentTrace>();
102
102
  const subagentListeners = new Map<string, Set<(trace: SubAgentTrace) => void>>();
103
103
 
104
- /** Max trace keys held (LRU, evicts oldest whole trace on insert) (Q-30). */
104
+ /** Max trace keys held (LRU, evicts oldest whole trace on insert). */
105
105
  const MAX_SUBAGENT_TRACES = 500;
106
- /** Trace TTL: 1h wall-clock, lazily expired on read (Q-30). */
106
+ /** Trace TTL: 1h wall-clock, lazily expired on read. */
107
107
  const SUBAGENT_TRACE_TTL_MS = 60 * 60 * 1000;
108
- /** Max steps retained per trace; oldest steps are dropped beyond this (Q-30). */
108
+ /** Max steps retained per trace; oldest steps are dropped beyond this. */
109
109
  const MAX_SUBAGENT_STEPS_PER_TRACE = 100;
110
- /** Live subscriptions auto-expire after 5min (Q-30). */
110
+ /** Live subscriptions auto-expire after 5min. */
111
111
  const SUBAGENT_SUBSCRIBE_TTL_MS = 5 * 60 * 1000;
112
112
 
113
113
  /** Last-write wall-clock per canonical trackingId (drives TTL; reads do not extend it). */
@@ -135,7 +135,7 @@ function touchTrace(canonical: string, now = Date.now()): void {
135
135
  }
136
136
 
137
137
  /**
138
- * Evicts expired traces, then oldest whole traces while over budget (Q-30).
138
+ * Evicts expired traces, then oldest whole traces while over budget.
139
139
  * Called on every insert. Whole-trace eviction removes both the canonical
140
140
  * key and its display-id alias.
141
141
  */
@@ -155,7 +155,7 @@ function pruneTraces(now = Date.now()): void {
155
155
  }
156
156
  }
157
157
 
158
- /** Drops oldest steps beyond {@link MAX_SUBAGENT_STEPS_PER_TRACE} (Q-30). */
158
+ /** Drops oldest steps beyond {@link MAX_SUBAGENT_STEPS_PER_TRACE}. */
159
159
  function trimTraceSteps(trace: SubAgentTrace): void {
160
160
  if (trace.steps.length > MAX_SUBAGENT_STEPS_PER_TRACE) {
161
161
  trace.steps.splice(0, trace.steps.length - MAX_SUBAGENT_STEPS_PER_TRACE);
@@ -268,7 +268,7 @@ export function subscribeToSubAgentForSessions(
268
268
 
269
269
  /**
270
270
  * Subscribes to live updates of one worker trace. Returns an unsubscribe fn.
271
- * Subscriptions auto-expire after 5min to avoid listener leaks (Q-30).
271
+ * Subscriptions auto-expire after 5min to avoid listener leaks.
272
272
  */
273
273
  export function subscribeToSubAgent(id: string, fn: (trace: SubAgentTrace) => void): () => void {
274
274
  const t = lookupTrace(id);
@@ -469,7 +469,7 @@ export function workerMetadata(a: {
469
469
  }
470
470
 
471
471
  // ---------------------------------------------------------------------------
472
- // Budgets (Q-39): counters + guards. The loop charges usage per turn; the
472
+ // Budgets: counters + guards. The loop charges usage per turn; the
473
473
  // worker registry enforces `maxTotalSubagents` through `reserveSpawn`. Durations use the monotonic `nowMs()`
474
474
  // clock; TTL timestamps above use wall-clock `Date.now()`.
475
475
  // ---------------------------------------------------------------------------
@@ -552,7 +552,7 @@ export function checkBudget(
552
552
  }
553
553
 
554
554
  /**
555
- * Adds worker usage to the parent's budget counters (Q-39). Never throws;
555
+ * Adds worker usage to the parent's budget counters. Never throws;
556
556
  * call {@link checkBudget} afterwards to enforce.
557
557
  */
558
558
  export function chargeBudgetUsage(parentAgent: Agent, usage: unknown): BudgetState {
@@ -563,7 +563,7 @@ export function chargeBudgetUsage(parentAgent: Agent, usage: unknown): BudgetSta
563
563
  }
564
564
 
565
565
  /**
566
- * Reserves `count` spawns against `maxTotalSubagents` (Q-39). Increments the
566
+ * Reserves `count` spawns against `maxTotalSubagents`. Increments the
567
567
  * parent's `budgetState.spawned` and throws {@link BudgetExceededError} when
568
568
  * the reservation would exceed the budget. Call before spawning.
569
569
  */
@@ -623,7 +623,7 @@ export function agentToTool(
623
623
  }),
624
624
  execute: async ({ task }, ctx) => {
625
625
  const startTime = nowMs();
626
- // Q-42 lineage for fixed-delegation calls.
626
+ // Lineage for fixed-delegation calls.
627
627
  const callRunId = newRunId();
628
628
  const callTurnId = newTurnId();
629
629
  const parentSessionId = ctx?.sessionId || agentInstance.sessionId || "session";