@adecore/agents 0.39.0 → 0.40.0

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 (168) hide show
  1. package/dist/async.d.ts +5 -0
  2. package/dist/async.js +13 -5
  3. package/dist/chat/acp-protocol.js +3 -5
  4. package/dist/chat/chat-core.js +0 -2
  5. package/dist/chat/chat-process.d.ts +1 -0
  6. package/dist/chat/chat-process.js +1 -1
  7. package/dist/chat/chat-session.js +6 -8
  8. package/dist/chat/claude-protocol.js +1 -6
  9. package/dist/chat/claude-transcript.js +1 -3
  10. package/dist/chat/codex-protocol.js +1 -6
  11. package/dist/chat/projector.js +1 -3
  12. package/dist/chat/request-summary.js +1 -3
  13. package/dist/coded-error.js +1 -5
  14. package/dist/context/argv.js +1 -2
  15. package/dist/context/verb.js +1 -3
  16. package/dist/error-text.d.ts +1 -0
  17. package/dist/error-text.js +4 -0
  18. package/dist/fields.d.ts +3 -0
  19. package/dist/fields.js +12 -0
  20. package/dist/fs.d.ts +1 -0
  21. package/dist/fs.js +8 -4
  22. package/dist/lineage.js +11 -5
  23. package/dist/messages/notice-store.js +3 -4
  24. package/dist/providers/accounts/acp-account.js +3 -20
  25. package/dist/providers/accounts/service.d.ts +1 -2
  26. package/dist/providers/accounts/service.js +5 -9
  27. package/dist/providers/accounts/status.js +1 -3
  28. package/dist/providers/acp-agent-state.js +1 -3
  29. package/dist/providers/claude.js +0 -1
  30. package/dist/providers/codex.js +0 -1
  31. package/dist/providers/detect.js +1 -1
  32. package/dist/providers/shell-commands.js +1 -2
  33. package/dist/run-process.js +1 -3
  34. package/dist/skills.d.ts +1 -1
  35. package/dist/skills.js +5 -8
  36. package/dist/tasks/background-limit.js +3 -2
  37. package/dist/tasks/task-coordinator.js +11 -17
  38. package/dist/testing/watch.js +0 -1
  39. package/dist/usage/limits/monitor.js +1 -2
  40. package/dist/usage/readers/claude.js +3 -10
  41. package/dist/usage/readers/codex.js +2 -9
  42. package/dist/usage/readers/grok.js +3 -10
  43. package/dist/usage/readers/host-log.js +3 -10
  44. package/dist/usage/record.d.ts +2 -0
  45. package/dist/usage/record.js +13 -0
  46. package/package.json +6 -6
  47. package/src/async.ts +23 -5
  48. package/src/chat/acp-backend.ts +121 -0
  49. package/src/chat/acp-protocol.ts +172 -6
  50. package/src/chat/attachment-store.ts +21 -0
  51. package/src/chat/backend.ts +0 -2
  52. package/src/chat/bookmark-store.ts +23 -0
  53. package/src/chat/chat-core.ts +208 -7
  54. package/src/chat/chat-log.ts +19 -0
  55. package/src/chat/chat-process.ts +52 -1
  56. package/src/chat/chat-session.ts +401 -10
  57. package/src/chat/chat-store.ts +50 -0
  58. package/src/chat/chat-title.ts +9 -0
  59. package/src/chat/claude-backend.ts +50 -0
  60. package/src/chat/claude-protocol.ts +155 -8
  61. package/src/chat/claude-title.ts +10 -0
  62. package/src/chat/claude-transcript.ts +47 -4
  63. package/src/chat/codex-backend.ts +131 -0
  64. package/src/chat/codex-protocol.ts +180 -8
  65. package/src/chat/codex-thread.ts +23 -0
  66. package/src/chat/composer-preferences.ts +6 -0
  67. package/src/chat/context-breakdown.ts +11 -0
  68. package/src/chat/delta-coalescer.ts +8 -0
  69. package/src/chat/external-sessions.ts +28 -0
  70. package/src/chat/generated-image.ts +25 -0
  71. package/src/chat/index.ts +5 -0
  72. package/src/chat/input.ts +13 -0
  73. package/src/chat/json-rpc-transport.ts +28 -0
  74. package/src/chat/limit-resume.ts +13 -0
  75. package/src/chat/projector.ts +160 -4
  76. package/src/chat/readable-text.ts +3 -0
  77. package/src/chat/request-summary.ts +18 -4
  78. package/src/chat/subagent-projection.ts +3 -0
  79. package/src/chat/subagent-reader.ts +76 -0
  80. package/src/chat/subagent-settlement.ts +24 -0
  81. package/src/chat/thread-search.ts +13 -0
  82. package/src/chat/thread.ts +31 -0
  83. package/src/chat/ui-queries.ts +122 -0
  84. package/src/chat/ui-replies.ts +34 -0
  85. package/src/chat/visual-store.ts +49 -0
  86. package/src/chat/wake-chat.ts +3 -0
  87. package/src/client-sinks.ts +2 -0
  88. package/src/coded-error.ts +1 -5
  89. package/src/context/argv.ts +18 -2
  90. package/src/context/index.ts +1 -0
  91. package/src/context/refusal.ts +3 -0
  92. package/src/context/verb.ts +32 -4
  93. package/src/coordination/index.ts +10 -0
  94. package/src/error-text.ts +7 -0
  95. package/src/fields.ts +14 -0
  96. package/src/fs.ts +20 -4
  97. package/src/host/agent-host.ts +20 -0
  98. package/src/host/environment.ts +6 -0
  99. package/src/host/handlers.ts +18 -0
  100. package/src/host/memory-port.ts +4 -0
  101. package/src/host/wiring.ts +10 -0
  102. package/src/index.ts +1 -0
  103. package/src/lineage.ts +24 -5
  104. package/src/messages/deliver-message.ts +6 -0
  105. package/src/messages/deliver-notice.ts +13 -0
  106. package/src/messages/notice-store.ts +16 -4
  107. package/src/modes.ts +1 -0
  108. package/src/outbox/outbox-worker.ts +31 -0
  109. package/src/outbox/outbox.ts +5 -0
  110. package/src/providers/accounts/accounts.ts +36 -0
  111. package/src/providers/accounts/acp-account.ts +22 -27
  112. package/src/providers/accounts/launch.ts +4 -0
  113. package/src/providers/accounts/service.ts +110 -12
  114. package/src/providers/accounts/shadow-home.ts +10 -0
  115. package/src/providers/accounts/status.ts +13 -4
  116. package/src/providers/accounts/store.ts +25 -0
  117. package/src/providers/accounts/variables.ts +11 -1
  118. package/src/providers/acp-agent-state.ts +24 -3
  119. package/src/providers/acp-provider.ts +5 -0
  120. package/src/providers/acp-usage-log.ts +2 -0
  121. package/src/providers/catalog.ts +17 -0
  122. package/src/providers/claude.ts +10 -1
  123. package/src/providers/codex-rules.ts +3 -0
  124. package/src/providers/codex.ts +2 -1
  125. package/src/providers/detect.ts +6 -1
  126. package/src/providers/provider.ts +1 -1
  127. package/src/providers/registry.ts +18 -2
  128. package/src/providers/shell-commands.ts +37 -2
  129. package/src/record-directory.ts +19 -0
  130. package/src/run-process.ts +24 -3
  131. package/src/serializer.ts +3 -0
  132. package/src/skills.ts +73 -10
  133. package/src/tasks/background-limit.ts +9 -2
  134. package/src/tasks/end-children.ts +9 -0
  135. package/src/tasks/give-task.ts +8 -0
  136. package/src/tasks/task-coordinator.ts +81 -17
  137. package/src/tasks/task-store.ts +28 -0
  138. package/src/tasks/waiting-child.ts +21 -0
  139. package/src/tasks/wake-parent.ts +26 -0
  140. package/src/tasks/wiring.ts +22 -0
  141. package/src/testing/accounts.ts +2 -0
  142. package/src/testing/fake-acp-capture.ts +16 -0
  143. package/src/testing/fake-acp.ts +66 -0
  144. package/src/testing/fake-claude.ts +177 -0
  145. package/src/testing/fake-cli.ts +29 -0
  146. package/src/testing/fake-codex.ts +122 -0
  147. package/src/testing/manual-clock.ts +2 -0
  148. package/src/testing/watch.ts +7 -1
  149. package/src/title-file.ts +12 -0
  150. package/src/usage/accounts.ts +6 -0
  151. package/src/usage/aggregate.ts +19 -0
  152. package/src/usage/exchange.ts +12 -0
  153. package/src/usage/index-file.ts +27 -0
  154. package/src/usage/limits/monitor.ts +31 -3
  155. package/src/usage/limits/normalize.ts +30 -0
  156. package/src/usage/limits/probe.ts +17 -0
  157. package/src/usage/pricing.ts +35 -0
  158. package/src/usage/projects.ts +8 -0
  159. package/src/usage/readers/claude.ts +12 -9
  160. package/src/usage/readers/codex.ts +40 -8
  161. package/src/usage/readers/grok.ts +17 -9
  162. package/src/usage/readers/host-log.ts +9 -9
  163. package/src/usage/record.ts +22 -0
  164. package/src/usage/roots.ts +6 -0
  165. package/src/usage/scanner.ts +61 -0
  166. package/src/usage/usage-service.ts +14 -0
  167. package/src/watch-seam.ts +8 -0
  168. package/src/windows-command.ts +12 -0
package/dist/async.d.ts CHANGED
@@ -1,2 +1,7 @@
1
1
  export declare function wait(ms: number, signal?: AbortSignal): Promise<void>;
2
+ export interface DeadlineTimer {
3
+ setTimeout(run: () => void, ms: number): unknown;
4
+ clearTimeout(handle: unknown): void;
5
+ }
6
+ export declare function withDeadline<T>(promise: Promise<T>, ms: number, message: string, timer?: DeadlineTimer): Promise<T>;
2
7
  export declare function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T>;
package/dist/async.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { asError } from './error-text.js';
1
2
  /*
2
3
  * Resolves after `ms`, or at once when the signal is raised. An abort resolves instead of rejecting:
3
4
  * every caller is a loop that checks the signal itself on its next turn.
@@ -11,16 +12,23 @@ export function wait(ms, signal) {
11
12
  }, { once: true });
12
13
  });
13
14
  }
15
+ const SYSTEM_TIMER = {
16
+ setTimeout: (run, ms) => setTimeout(run, ms),
17
+ clearTimeout: (handle) => clearTimeout(handle)
18
+ };
14
19
  /* The timer is cleared either way, so a call that answers at once does not hold the process until its deadline. */
15
- export function withTimeout(promise, ms, message) {
20
+ export function withDeadline(promise, ms, message, timer = SYSTEM_TIMER) {
16
21
  return new Promise((resolve, reject) => {
17
- const timer = setTimeout(() => reject(new Error(message)), ms);
22
+ const handle = timer.setTimeout(() => reject(new Error(message)), ms);
18
23
  promise.then((value) => {
19
- clearTimeout(timer);
24
+ timer.clearTimeout(handle);
20
25
  resolve(value);
21
26
  }, (e) => {
22
- clearTimeout(timer);
23
- reject(e instanceof Error ? e : new Error(String(e)));
27
+ timer.clearTimeout(handle);
28
+ reject(asError(e));
24
29
  });
25
30
  });
26
31
  }
32
+ export function withTimeout(promise, ms, message) {
33
+ return withDeadline(promise, ms, message);
34
+ }
@@ -1,5 +1,6 @@
1
1
  import { isRecord } from '@adecore/agent-contracts';
2
2
  import { z } from 'zod';
3
+ import { filledStr } from '../fields.js';
3
4
  import { MODE_ORDER } from '../modes.js';
4
5
  import { cleanTitle } from '../title-file.js';
5
6
  /*
@@ -357,12 +358,9 @@ function withheld(text, afterRefusal) {
357
358
  function foldSpace(text) {
358
359
  return text.replace(/\s+/g, ' ').trim();
359
360
  }
360
- function str(value) {
361
- return typeof value === 'string' && value !== '' ? value : null;
362
- }
363
- /* A field of the agent's own input, which no schema promises; the first one that is a non-empty string. */
364
361
  // Where agents put the command a call runs; Antigravity names it `CommandLine`.
365
362
  const COMMAND_KEYS = ['command', 'cmd', 'CommandLine'];
363
+ /* A field of the agent's own input, which no schema promises; the first one that is a non-empty string. */
366
364
  function rawField(raw, ...keys) {
367
365
  if (!isRecord(raw)) {
368
366
  return null;
@@ -372,7 +370,7 @@ function rawField(raw, ...keys) {
372
370
  if (Array.isArray(value) && value.every((part) => typeof part === 'string') && value.length > 0) {
373
371
  return value.join(' ');
374
372
  }
375
- if (str(value) !== null) {
373
+ if (filledStr(value) !== null) {
376
374
  return value;
377
375
  }
378
376
  }
@@ -173,7 +173,6 @@ export class ChatCore {
173
173
  const folder = this.accounts?.transcriptFolder('claude', info.account) ?? null;
174
174
  return folder === null ? '' : join(folder, 'projects');
175
175
  }
176
- /* Whether a conversation of one account of this CLI can go on under the other. */
177
176
  canContinue(kind, from, to) {
178
177
  if ((from ?? kind) === (to ?? kind)) {
179
178
  return true;
@@ -184,7 +183,6 @@ export class ChatCore {
184
183
  requireAccount(kind, account) {
185
184
  launchEnv(this.accounts, kind, account, this.env);
186
185
  }
187
- /* What a person calls an account of this CLI. */
188
186
  accountLabel(kind, account) {
189
187
  return isDefaultAccountOf(kind, account) || account === undefined ? this.providers.get(kind).name : (this.accounts?.labelOf(account) ?? account);
190
188
  }
@@ -24,6 +24,7 @@ export interface ProcessTreeKillOptions {
24
24
  run?(command: string[]): void;
25
25
  }
26
26
  export declare function killProcessTree(pid: number, signal: 'SIGTERM' | 'SIGKILL', options?: ProcessTreeKillOptions): void;
27
+ export declare const KILL_GRACE_MS = 2000;
27
28
  export declare class StreamTail {
28
29
  readonly done: Promise<void>;
29
30
  private text;
@@ -77,7 +77,7 @@ function runQuietly([command, ...args]) {
77
77
  // After stdin closed, a CLI that is still around is not going to say more.
78
78
  const EXIT_GRACE_MS = 3000;
79
79
  // What a CLI and everything it started get between the SIGTERM and the SIGKILL.
80
- const KILL_GRACE_MS = 2000;
80
+ export const KILL_GRACE_MS = 2000;
81
81
  // How long a crash note waits for the last of stderr; a process the CLI started may hold the pipe open.
82
82
  const STDERR_DRAIN_MS = 250;
83
83
  // A pipe nobody reads is buffered in memory for as long as the CLI lives, so stderr is read into this much and no more.
@@ -8,7 +8,7 @@ import { isMainAgentOutput, ThreadProjector } from './projector.js';
8
8
  import { readableAssistantText } from './readable-text.js';
9
9
  import { ChatThread } from './thread.js';
10
10
  import { UiReplies } from './ui-replies.js';
11
- import { errorText } from '../error-text.js';
11
+ import { asError, errorText } from '../error-text.js';
12
12
  import { systemClock } from '../outbox/outbox-worker.js';
13
13
  import { cleanTitle } from '../title-file.js';
14
14
  // Claude Code names a session about six seconds after its first prompt, and a first turn can run for minutes.
@@ -239,9 +239,7 @@ export class ChatSession {
239
239
  this.drainQueue();
240
240
  this.emit([this.thread.patchInfo({})]);
241
241
  }
242
- /*
243
- * Internal follow-ups wait for a new turn. User input goes through `sendInput`, which can steer native work.
244
- */
242
+ /* Internal follow-ups wait for a new turn. User input goes through `sendInput`, which can steer native work. */
245
243
  send(text, sent = {}) {
246
244
  const extras = this.vetted(sent);
247
245
  const turnId = newId('turn');
@@ -1524,7 +1522,7 @@ export class ChatSession {
1524
1522
  }
1525
1523
  catch (error) {
1526
1524
  // What the host prepares for a launch (its folders, its instructions) can throw too; the turn already opened, so it fails.
1527
- backend = Promise.reject(error instanceof Error ? error : new Error(String(error)));
1525
+ backend = Promise.reject(asError(error));
1528
1526
  }
1529
1527
  const generation = this.generation;
1530
1528
  void Promise.all([backend, this.turnReady, this.options.save?.()])
@@ -1627,7 +1625,7 @@ export class ChatSession {
1627
1625
  this.launchedAccount = this.thread.info.account;
1628
1626
  }
1629
1627
  catch (error) {
1630
- return Promise.reject(error instanceof Error ? error : new Error(String(error)));
1628
+ return Promise.reject(asError(error));
1631
1629
  }
1632
1630
  this.generation = nextGeneration();
1633
1631
  const generation = this.generation;
@@ -1669,7 +1667,7 @@ export class ChatSession {
1669
1667
  });
1670
1668
  }
1671
1669
  catch (error) {
1672
- return Promise.reject(error instanceof Error ? error : new Error(String(error)));
1670
+ return Promise.reject(asError(error));
1673
1671
  }
1674
1672
  const backend = made.backend;
1675
1673
  this.backend = backend;
@@ -1681,7 +1679,7 @@ export class ChatSession {
1681
1679
  }
1682
1680
  catch (error) {
1683
1681
  // A spawn that throws on the spot (no such executable) is a start that failed like any other.
1684
- started = Promise.reject(error instanceof Error ? error : new Error(String(error)));
1682
+ started = Promise.reject(asError(error));
1685
1683
  }
1686
1684
  this.starting = started
1687
1685
  .then(() => backend)
@@ -1,12 +1,7 @@
1
1
  import { isRecord } from '@adecore/agent-contracts';
2
2
  import { readClaudeEvent } from '../usage/limits/normalize.js';
3
+ import { num, str } from '../fields.js';
3
4
  import { claudeRuntimeMode } from '../providers/claude.js';
4
- function str(value) {
5
- return typeof value === 'string' ? value : null;
6
- }
7
- function num(value) {
8
- return typeof value === 'number' && Number.isFinite(value) ? value : 0;
9
- }
10
5
  // A tool result is a string or a list of content blocks; only the text is worth showing.
11
6
  function resultText(content) {
12
7
  if (typeof content === 'string') {
@@ -2,14 +2,12 @@ import { existsSync } from 'node:fs';
2
2
  import { readdir, readFile, stat } from 'node:fs/promises';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { isRecord } from '@adecore/agent-contracts';
5
+ import { filledStr as str } from '../fields.js';
5
6
  import { fileExists } from '../fs.js';
6
7
  import { readLines } from '../title-file.js';
7
8
  import { ClaudeProtocol } from './claude-protocol.js';
8
9
  import { readingThread, settledReading } from './subagent-projection.js';
9
10
  const CHUNK_BYTES = 4 * 1024 * 1024;
10
- function str(value) {
11
- return typeof value === 'string' && value !== '' ? value : null;
12
- }
13
11
  /* The words of a user line that somebody said, or null for a line that carries tool results. */
14
12
  export function spokenText(content) {
15
13
  if (typeof content === 'string') {
@@ -2,13 +2,8 @@ import { generatedImage } from './generated-image.js';
2
2
  import { isRecord } from '@adecore/agent-contracts';
3
3
  import { readCodexLimits } from '../usage/limits/normalize.js';
4
4
  import { codexRuntimeMode } from '../providers/codex.js';
5
+ import { num, str } from '../fields.js';
5
6
  import { cleanTitle } from '../title-file.js';
6
- function str(value) {
7
- return typeof value === 'string' ? value : null;
8
- }
9
- function num(value) {
10
- return typeof value === 'number' && Number.isFinite(value) ? value : 0;
11
- }
12
7
  const GOAL_STATUSES = {
13
8
  active: 'active',
14
9
  paused: 'paused',
@@ -1,5 +1,6 @@
1
1
  import { isRecord } from '@adecore/agent-contracts';
2
2
  import { clipText } from '@adecore/agent-contracts/text';
3
+ import { str } from '../fields.js';
3
4
  import { estimateContextBreakdown } from './context-breakdown.js';
4
5
  /*
5
6
  * Content of the main conversation, which the CLI only writes inside a turn it closes with a `result`.
@@ -26,9 +27,6 @@ export function isMainAgentOutput(event) {
26
27
  function startsAgentTurn(event) {
27
28
  return isMainAgentOutput(event) || ((event.type === 'approval.requested' || event.type === 'question.requested') && event.background !== true);
28
29
  }
29
- function str(value) {
30
- return typeof value === 'string' ? value : null;
31
- }
32
30
  /* A markdown code block around text that may hold backticks of its own. */
33
31
  function fenced(text) {
34
32
  const longest = Math.max(0, ...(text.match(/`+/g) ?? []).map((run) => run.length));
@@ -1,7 +1,5 @@
1
1
  import { isRecord, CHAT_REQUEST_LIMITS } from '@adecore/agent-contracts';
2
- function str(value) {
3
- return typeof value === 'string' ? value : null;
4
- }
2
+ import { str } from '../fields.js';
5
3
  // The keys a tool's input names its subject under, in the order a person would look for one.
6
4
  const SUBJECT_KEYS = ['file_path', 'notebook_path', 'path', 'url', 'query', 'pattern', 'skill', 'description'];
7
5
  /* Cuts text at `max` code units, never between the halves of a surrogate pair. */
@@ -1,8 +1,4 @@
1
- /*
2
- * The shape every failure on the wire has: a code a client can branch on and a message a person reads.
3
- * A domain gives it a union of its own codes, so a typo in a code is a compile error instead of a
4
- * string the other end never matches.
5
- */
1
+ /* The shape every failure on the wire has: a code a client can branch on and a message a person reads. */
6
2
  export class CodedError extends Error {
7
3
  code;
8
4
  /* What the actor can do instead. Only a refusal fills this; the rest carry an empty list. */
@@ -2,8 +2,7 @@
2
2
  * Splits the words after a verb into positionals, `--flag value` pairs (`--flag=value` too, for a
3
3
  * value that itself starts with `--`) and the switches the verb declared. Anything not declared a
4
4
  * switch takes a value, so a flag that lost its argument is refused instead of swallowing the next
5
- * word, and a flag given twice is refused rather than one of the two silently winning. A name that is
6
- * both a switch and a flag is on when written bare and takes a value only after `=` (`--state=full`).
5
+ * word. A name that is both a switch and a flag takes a value only after `=` (`--state=full`).
7
6
  */
8
7
  export function parseArgv(argv, known, switches = []) {
9
8
  const positionals = [];
@@ -57,11 +57,9 @@ export function createVerbRegistry(options) {
57
57
  * points at these, so an agent never gets a silent nothing from a dry run that was never dry.
58
58
  */
59
59
  const dryRunVerbs = [];
60
- /* What a refusal over arguments points at; for an action `name` is both words, which is what `help` takes. */
61
- const usageLinesOf = (name, usage) => [`usage\t${name}\t${usage}`, `detail\t${cli} help ${name}`];
62
60
  /* One set of arguments parsed and run. `name` is what a refusal calls it, so `view new` refuses under both its words. */
63
61
  const runArgs = async (name, usage, spec, argv, call) => {
64
- const usageLines = usageLinesOf(name, usage);
62
+ const usageLines = [`usage\t${name}\t${usage}`, `detail\t${cli} help ${name}`];
65
63
  const { values, switches } = flagsOf(spec);
66
64
  const parsed = parseArgv(argv, values, switches);
67
65
  if (!parsed.ok) {
@@ -1,5 +1,6 @@
1
1
  /** The message of an error, or its whole stack with `debug` set. */
2
2
  export declare function describeError(error: unknown, debug: boolean): string;
3
+ export declare function asError(thrown: unknown): Error;
3
4
  export declare function setErrorStacks(on: boolean): void;
4
5
  /** `describeError` as the host set it, for a log line or a note. */
5
6
  export declare function errorText(error: unknown): string;
@@ -13,6 +13,10 @@ export function describeError(error, debug) {
13
13
  }
14
14
  return error.message === '' ? error.name : error.message;
15
15
  }
16
+ /* What was thrown, as an Error a rejection can carry. */
17
+ export function asError(thrown) {
18
+ return thrown instanceof Error ? thrown : new Error(String(thrown));
19
+ }
16
20
  // The host decides once whether a stack is worth showing; the package reads no variable of its own.
17
21
  let showStacks = false;
18
22
  export function setErrorStacks(on) {
@@ -0,0 +1,3 @@
1
+ export declare function str(value: unknown): string | null;
2
+ export declare function num(value: unknown): number;
3
+ export declare function filledStr(value: unknown): string | null;
package/dist/fields.js ADDED
@@ -0,0 +1,12 @@
1
+ /* A field of a frame or a file that no schema promises a string in. */
2
+ export function str(value) {
3
+ return typeof value === 'string' ? value : null;
4
+ }
5
+ /* A count of a frame, zero for anything that is not a finite number. */
6
+ export function num(value) {
7
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
8
+ }
9
+ /* The same, with an empty string read as nothing said. */
10
+ export function filledStr(value) {
11
+ return typeof value === 'string' && value !== '' ? value : null;
12
+ }
package/dist/fs.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export declare function hasErrorCode(e: unknown, ...codes: string[]): boolean;
1
2
  export declare function tempNameFor(target: string): string;
2
3
  export interface WriteAtomicOptions {
3
4
  durable?: boolean;
package/dist/fs.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { renameSync, writeFileSync } from 'node:fs';
2
2
  import { open, rename, rm, stat, writeFile } from 'node:fs/promises';
3
3
  import { dirname } from 'node:path';
4
+ import { wait } from './async.js';
4
5
  // Windows can refuse a rename while a watcher or an indexer holds the target; a few short retries cover it.
5
6
  const RENAME_RETRIES = 5;
6
7
  const RENAME_RETRY_MS = 40;
@@ -8,8 +9,11 @@ const RENAME_RETRY_MS = 40;
8
9
  function sleepSync(ms) {
9
10
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
10
11
  }
12
+ export function hasErrorCode(e, ...codes) {
13
+ return typeof e === 'object' && e !== null && 'code' in e && codes.includes(e.code);
14
+ }
11
15
  function isTransient(e) {
12
- return typeof e === 'object' && e !== null && 'code' in e && (e.code === 'EPERM' || e.code === 'EBUSY' || e.code === 'EACCES');
16
+ return hasErrorCode(e, 'EPERM', 'EBUSY', 'EACCES');
13
17
  }
14
18
  // Two writes of the same file in flight at once (a fire-and-forget save next to an awaited one) must not share a temp name.
15
19
  let nextTemp = 0;
@@ -62,7 +66,7 @@ export async function writeAtomic(target, content, mode = 0o600, options = {}) {
62
66
  if (!isTransient(e) || attempt >= RENAME_RETRIES) {
63
67
  throw e;
64
68
  }
65
- await new Promise((resolve) => setTimeout(resolve, RENAME_RETRY_MS));
69
+ await wait(RENAME_RETRY_MS);
66
70
  }
67
71
  }
68
72
  if (options.durable) {
@@ -94,7 +98,7 @@ export function writeAtomicSync(target, content, mode = 0o600) {
94
98
  replaceSync(temp, target);
95
99
  }
96
100
  export function isNotFound(e) {
97
- return typeof e === 'object' && e !== null && 'code' in e && e.code === 'ENOENT';
101
+ return hasErrorCode(e, 'ENOENT');
98
102
  }
99
103
  // Bun on Windows fails a forced rm with EFAULT while another delete of the same file is pending; it goes either way.
100
104
  export async function removeFile(path, remove = rm) {
@@ -102,7 +106,7 @@ export async function removeFile(path, remove = rm) {
102
106
  await remove(path, { force: true });
103
107
  }
104
108
  catch (e) {
105
- if (!(typeof e === 'object' && e !== null && 'code' in e && e.code === 'EFAULT')) {
109
+ if (!hasErrorCode(e, 'EFAULT')) {
106
110
  throw e;
107
111
  }
108
112
  }
package/dist/lineage.js CHANGED
@@ -51,9 +51,10 @@ export class AgentLineageStore {
51
51
  descendants(nodeId) {
52
52
  const found = [];
53
53
  const seen = new Set([nodeId]);
54
+ const entries = this.opened.all();
54
55
  for (let i = -1; i < found.length; i++) {
55
56
  const parent = i === -1 ? nodeId : found[i];
56
- for (const entry of this.opened.all()) {
57
+ for (const entry of entries) {
57
58
  if (entry.openedBy === parent && openedByAgent(entry) && entry.endedAt === undefined && !seen.has(entry.nodeId)) {
58
59
  seen.add(entry.nodeId);
59
60
  found.push(entry.nodeId);
@@ -89,7 +90,8 @@ export class AgentLineageStore {
89
90
  }
90
91
  /* The nodes of this project whose opener it no longer places, and that no cascade ended yet. */
91
92
  orphans(projectId, ids) {
92
- return [...this.opened.all()]
93
+ return this.opened
94
+ .all()
93
95
  .filter((entry) => entry.projectId === projectId && openedByAgent(entry) && entry.endedAt === undefined && ids.has(entry.nodeId) && !ids.has(entry.openedBy))
94
96
  .map((entry) => ({ nodeId: entry.nodeId, openedBy: entry.openedBy }));
95
97
  }
@@ -102,7 +104,7 @@ export class AgentLineageStore {
102
104
  peerOf(nodeId) {
103
105
  return this.opened.get(nodeId)?.peer ?? null;
104
106
  }
105
- /* How deep a node sits. A node nobody wrote down is one a person made, which is where a chain starts. */
107
+ /* A node nobody wrote down is one a person made, which is where a chain starts. */
106
108
  depthOf(nodeId) {
107
109
  return this.opened.get(nodeId)?.depth ?? 0;
108
110
  }
@@ -132,11 +134,15 @@ export class AgentLineageStore {
132
134
  return entry?.relation === 'fork' ? entry.openedBy : null;
133
135
  }
134
136
  forksOf(nodeId) {
135
- return [...this.opened.all()].filter((entry) => entry.relation === 'fork' && entry.openedBy === nodeId).map((entry) => entry.nodeId);
137
+ return this.opened
138
+ .all()
139
+ .filter((entry) => entry.relation === 'fork' && entry.openedBy === nodeId)
140
+ .map((entry) => entry.nodeId);
136
141
  }
137
142
  /* The forks of this project whose node is gone, read before `prune` forgets them. */
138
143
  forksLeaving(projectId, ids) {
139
- return [...this.opened.all()]
144
+ return this.opened
145
+ .all()
140
146
  .filter((entry) => entry.projectId === projectId && entry.relation === 'fork' && !ids.has(entry.nodeId))
141
147
  .map((entry) => entry.nodeId);
142
148
  }
@@ -28,10 +28,9 @@ const NoticeSchema = z.object({
28
28
  /* A queue waits in the file of the node it is for, so the first message in it names that node. */
29
29
  const FileSchema = z.object({ notices: z.array(NoticeSchema).min(1) });
30
30
  /*
31
- * The messages waiting for each node, held until that node's agent has a moment to hear them, under
32
- * `<dataDir>/notices`. On disk because the receiver may well be a node nobody has mounted yet, and a
33
- * host that restarts must not lose what was left for it. Delivered once, whichever channel gets there
34
- * first.
31
+ * The messages waiting for each node, held until that node's agent has a moment to hear them. On disk
32
+ * because the receiver may well be a node nobody has mounted yet, and a host that restarts must not
33
+ * lose what was left for it.
35
34
  */
36
35
  export class NoticeStore {
37
36
  dir;
@@ -1,31 +1,14 @@
1
1
  import { mkdtemp, rm } from 'node:fs/promises';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { join } from 'node:path';
4
+ import { withDeadline } from '../../async.js';
4
5
  import { DEFAULT_ACP_CLIENT } from '../../chat/acp-backend.js';
5
6
  import { ACP_AUTH_REQUIRED, ACP_PROTOCOL_VERSION, parseInitialize, parseSessionInfo, sessionIdOf, sessionUpdateOf } from '../../chat/acp-protocol.js';
6
7
  import { JsonRpcError, JsonRpcTransport } from '../../chat/json-rpc-transport.js';
8
+ import { definedEnv } from './launch.js';
7
9
  import { ASK_TIMEOUT_MS } from './status.js';
8
10
  const SIGNED_OUT = { signedIn: false, email: null, plan: null, organization: null };
9
11
  const SIGNED_IN = { signedIn: true, email: null, plan: null, organization: null };
10
- const REAL_TIMER = {
11
- setTimeout: (run, ms) => setTimeout(run, ms),
12
- clearTimeout: (handle) => clearTimeout(handle)
13
- };
14
- function definedEnv(env) {
15
- return Object.fromEntries(Object.entries(env).filter((entry) => entry[1] !== undefined));
16
- }
17
- function withDeadline(work, ms, timer, message) {
18
- return new Promise((resolve, reject) => {
19
- const handle = timer.setTimeout(() => reject(new Error(message)), ms);
20
- work.then((value) => {
21
- timer.clearTimeout(handle);
22
- resolve(value);
23
- }, (e) => {
24
- timer.clearTimeout(handle);
25
- reject(e instanceof Error ? e : new Error(String(e)));
26
- });
27
- });
28
- }
29
12
  /*
30
13
  * Asks an ACP agent whether the account's environment is signed in: a session it opens means yes, "authentication
31
14
  * required" means no. ACP has no request that says who, so a reading carries no email or plan. The session is opened
@@ -102,7 +85,7 @@ export async function askAcpAccount(probe) {
102
85
  }
103
86
  return SIGNED_IN;
104
87
  };
105
- return await withDeadline(reading(), probe.timeoutMs ?? ASK_TIMEOUT_MS, probe.timer ?? REAL_TIMER, `${probe.name} did not answer in time`);
88
+ return await withDeadline(reading(), probe.timeoutMs ?? ASK_TIMEOUT_MS, `${probe.name} did not answer in time`, probe.timer);
106
89
  }
107
90
  finally {
108
91
  await transport?.dispose();
@@ -42,7 +42,7 @@ export declare class ProviderAccountsService implements AccountLaunches {
42
42
  private readonly now;
43
43
  private readonly sleep;
44
44
  private readonly watches;
45
- private writes;
45
+ private readonly writes;
46
46
  private readonly sinks;
47
47
  private readonly listeners;
48
48
  private readonly launches;
@@ -74,7 +74,6 @@ export declare class ProviderAccountsService implements AccountLaunches {
74
74
  private runWatch;
75
75
  private kinds;
76
76
  private accountProviders;
77
- private serial;
78
77
  private commit;
79
78
  private planSecrets;
80
79
  private requireSecrets;
@@ -5,6 +5,7 @@ import { KNOWN_AGENT_KINDS } from '@adecore/agent-contracts';
5
5
  import { DEFAULT_CODEX_CLIENT } from '../../chat/codex-transport.js';
6
6
  import { ClientSinks } from '../../client-sinks.js';
7
7
  import { errorText } from '../../error-text.js';
8
+ import { Serializer } from '../../serializer.js';
8
9
  import { accountEnv, accountFolder, canContinue, defaultFolder, expandHome, isDefaultAccount, isOfferedKind, transcriptFolder } from './accounts.js';
9
10
  import { AccountError, isDefaultAccountOf } from './launch.js';
10
11
  import { prepareShadowHome } from './shadow-home.js';
@@ -72,7 +73,8 @@ export class ProviderAccountsService {
72
73
  now;
73
74
  sleep;
74
75
  watches = new Map();
75
- writes = Promise.resolve();
76
+ // One change of the file at a time: a create and a save both read what the other just wrote.
77
+ writes = new Serializer();
76
78
  sinks = new ClientSinks();
77
79
  listeners = new Set();
78
80
  // When a CLI last started under each account, by id; kept in memory, so a restart forgets it.
@@ -228,14 +230,14 @@ export class ProviderAccountsService {
228
230
  }
229
231
  /* Answers at once; an account that changed reads `checking` until its CLI answered, which arrives as `accounts.changed`. */
230
232
  save(accounts) {
231
- return this.serial(() => this.commit(accounts, this.folderExists));
233
+ return this.writes.run(() => this.commit(accounts, this.folderExists));
232
234
  }
233
235
  /*
234
236
  * An account in a folder of its own under `<home>/accounts`, signed in by nobody yet. A
235
237
  * Codex account is a shadow home over the CLI's own folder, so its threads go on under the other.
236
238
  */
237
239
  create(payload) {
238
- return this.serial(async () => {
240
+ return this.writes.run(async () => {
239
241
  const { kind, label, color } = payload;
240
242
  const provider = this.providers.get(kind);
241
243
  if (provider.home === undefined) {
@@ -303,12 +305,6 @@ export class ProviderAccountsService {
303
305
  accountProviders() {
304
306
  return { kinds: this.kinds(), get: (kind) => this.providers.get(kind) };
305
307
  }
306
- /* One change of the file at a time: a create and a save both read what the other just wrote. */
307
- serial(work) {
308
- const next = this.writes.then(work);
309
- this.writes = next.catch(() => undefined);
310
- return next;
311
- }
312
308
  async commit(accounts, isDirectory) {
313
309
  const stored = acceptAccounts(accounts, this.stored, this.accountProviders(), this.env, isDirectory, this.host);
314
310
  const { writes, removals } = this.planSecrets(stored);
@@ -1,12 +1,10 @@
1
1
  import { isRecord } from '@adecore/agent-contracts';
2
2
  import { withTimeout } from '../../async.js';
3
+ import { filledStr as text } from '../../fields.js';
3
4
  import { CodexTransport } from '../../chat/codex-transport.js';
4
5
  import { runProcess } from '../../run-process.js';
5
6
  /* A CLI that has not said who is signed in by now is not going to; the next pass asks again. */
6
7
  export const ASK_TIMEOUT_MS = 20_000;
7
- function text(value) {
8
- return typeof value === 'string' && value !== '' ? value : null;
9
- }
10
8
  /* `claude auth status --json`, as Claude Code 2.1.282 prints it: `loggedIn`, `email`, `orgName`, `subscriptionType`. */
11
9
  export function readClaudeAuthStatus(output) {
12
10
  let status;
@@ -79,9 +79,7 @@ export class AcpAgentState {
79
79
  if (!this.options.adoptModels || info.models === null) {
80
80
  return;
81
81
  }
82
- const adopted = { defaultModel: info.models.current, models: adoptableModels(info), options: effortOption(info) };
83
- const changed = this.adopt(adopted);
84
- this.settle(changed);
82
+ this.settle(this.adopt({ defaultModel: info.models.current, models: adoptableModels(info), options: effortOption(info) }));
85
83
  }
86
84
  commandsListed(commands) {
87
85
  const names = [...new Set(commands)].sort();
@@ -80,7 +80,6 @@ export function claudeArgs(launch) {
80
80
  export function claudeEnv(selection) {
81
81
  return selection.options.contextWindow === '200k' ? { CLAUDE_CODE_AUTO_COMPACT_WINDOW: '200000' } : {};
82
82
  }
83
- // What the CLI can do, as far as a client has to know.
84
83
  export const CLAUDE_CAPABILITIES = {
85
84
  chat: true,
86
85
  terminal: true,
@@ -28,7 +28,6 @@ const CODEX_PRIORITY_TIER = 'priority';
28
28
  export function codexServiceTier(selection) {
29
29
  return selection.options.serviceTier === true ? CODEX_PRIORITY_TIER : null;
30
30
  }
31
- // What the app-server can do, as far as a client has to know.
32
31
  export const CODEX_CAPABILITIES = {
33
32
  chat: true,
34
33
  terminal: true,
@@ -16,7 +16,7 @@ export async function detectCli(command, env = process.env, spawn = spawnChatPro
16
16
  if ((await code) !== 0) {
17
17
  return { installed: false, version: null };
18
18
  }
19
- const version = /\d+\.\d+\.\d+/.exec(output)?.[0] ?? output.trim() ?? null;
19
+ const version = /\d+\.\d+\.\d+/.exec(output)?.[0] ?? output.trim();
20
20
  return { installed: true, version };
21
21
  }
22
22
  catch {
@@ -4,8 +4,7 @@ const REFUSED = new Set(['|', '>', '<', '$', '`', '(', ')', '{', '}', '*', '?',
4
4
  /*
5
5
  * Whether a shell command line is only simple commands, joined by `&&`, `||`, `;` or newlines, each of
6
6
  * which starts with one of `allowed`. Anything the tokenizer cannot prove simple answers false, so a
7
- * true is safe to act on without a person: a pipe, a redirect, an expansion, a subshell, a background
8
- * `&`, an unquoted glob or an unbalanced quote all refuse the whole line.
7
+ * true is safe to act on without a person.
9
8
  */
10
9
  export function isOnlyCommands(command, allowed) {
11
10
  let word = '';
@@ -1,8 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { killProcessTree } from './chat/chat-process.js';
2
+ import { KILL_GRACE_MS, killProcessTree } from './chat/chat-process.js';
3
3
  import { spawnTarget } from './windows-command.js';
4
- // What a command and everything it started get between the SIGTERM and the SIGKILL.
5
- const KILL_GRACE_MS = 2000;
6
4
  // Stdout and stderr together; a short command that prints more is ended and fails instead of filling memory.
7
5
  const MAX_OUTPUT_BYTES = 16 * 1024 * 1024;
8
6
  /* One short command to its end, with what it printed. A command that cannot start rejects. */
package/dist/skills.d.ts CHANGED
@@ -17,7 +17,7 @@ export interface SkillRootOptions {
17
17
  export declare function parseFrontmatter(text: string): Record<string, string>;
18
18
  export declare function pluginSkillRoots(configDir: string): Promise<SkillRoot[]>;
19
19
  export declare function skillRootsFor(kind: AgentKind, cwd: string, options?: SkillRootOptions): Promise<SkillRoot[]>;
20
- export declare function whileBusy<T>(operation: () => Promise<T>, wait?: (ms: number) => Promise<void>): Promise<T>;
20
+ export declare function whileBusy<T>(operation: () => Promise<T>, sleep?: (ms: number) => Promise<void>): Promise<T>;
21
21
  export declare function mapLimited<T, R>(items: readonly T[], limit: number, work: (item: T) => Promise<R>): Promise<R[]>;
22
22
  export declare function scanSkillRoots(roots: SkillRoot[], limits?: SkillScanLimits): Promise<ChatSkill[]>;
23
23
  export declare function discoverSkills(kind: AgentKind, cwd: string, options?: SkillRootOptions): Promise<ChatSkill[]>;