@adecore/agents 0.21.1 → 0.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/dist/async.js +3 -8
  2. package/dist/chat/chat-core.d.ts +2 -2
  3. package/dist/chat/chat-core.js +4 -4
  4. package/dist/chat/chat-log.js +2 -2
  5. package/dist/chat/chat-store.js +4 -7
  6. package/dist/chat/chat-title.js +4 -8
  7. package/dist/chat/claude-backend.d.ts +1 -0
  8. package/dist/chat/claude-backend.js +8 -6
  9. package/dist/chat/claude-title.js +1 -1
  10. package/dist/chat/codex-backend.d.ts +2 -2
  11. package/dist/chat/codex-backend.js +5 -8
  12. package/dist/chat/codex-protocol.js +1 -1
  13. package/dist/chat/codex-thread.js +4 -5
  14. package/dist/chat/composer-preferences.js +2 -3
  15. package/dist/chat/limit-resume.js +2 -7
  16. package/dist/chat/visual-store.js +8 -6
  17. package/dist/context/refusal.js +3 -7
  18. package/dist/context/verb.js +1 -5
  19. package/dist/host/agent-host.js +2 -2
  20. package/dist/host/handlers.js +0 -1
  21. package/dist/host/wiring.js +1 -1
  22. package/dist/lineage.js +2 -3
  23. package/dist/messages/deliver-message.js +6 -11
  24. package/dist/messages/deliver-notice.js +8 -12
  25. package/dist/messages/notice-store.js +3 -5
  26. package/dist/outbox/outbox-worker.js +5 -2
  27. package/dist/providers/accounts/service.d.ts +2 -1
  28. package/dist/providers/accounts/service.js +11 -13
  29. package/dist/providers/claude.js +0 -1
  30. package/dist/providers/detect.js +2 -10
  31. package/dist/record-directory.js +0 -1
  32. package/dist/tasks/task-coordinator.d.ts +1 -0
  33. package/dist/tasks/task-coordinator.js +34 -34
  34. package/dist/tasks/task-store.js +1 -1
  35. package/dist/tasks/task-work.js +2 -2
  36. package/dist/tasks/waiting-child.js +5 -3
  37. package/dist/tasks/wake-parent.js +15 -10
  38. package/dist/tasks/wiring.js +32 -37
  39. package/dist/usage/aggregate.js +62 -46
  40. package/dist/usage/exchange.js +1 -1
  41. package/dist/usage/limits/monitor.d.ts +2 -2
  42. package/dist/usage/limits/monitor.js +2 -2
  43. package/dist/usage/limits/probe.js +1 -1
  44. package/dist/usage/pricing.js +1 -1
  45. package/dist/usage/roots.js +1 -2
  46. package/dist/usage/scanner.js +7 -9
  47. package/dist/usage/usage-service.d.ts +1 -2
  48. package/dist/usage/usage-service.js +3 -6
  49. package/package.json +3 -3
  50. package/src/async.ts +3 -9
  51. package/src/chat/chat-core.ts +4 -4
  52. package/src/chat/chat-log.ts +2 -2
  53. package/src/chat/chat-store.ts +4 -8
  54. package/src/chat/chat-title.ts +4 -8
  55. package/src/chat/claude-backend.ts +9 -6
  56. package/src/chat/claude-title.ts +1 -1
  57. package/src/chat/claude-transcript.ts +1 -1
  58. package/src/chat/codex-backend.ts +5 -8
  59. package/src/chat/codex-protocol.ts +1 -1
  60. package/src/chat/codex-thread.ts +6 -5
  61. package/src/chat/composer-preferences.ts +2 -3
  62. package/src/chat/limit-resume.ts +2 -7
  63. package/src/chat/thread.ts +1 -1
  64. package/src/chat/visual-store.ts +9 -6
  65. package/src/chat/wake-chat.ts +4 -1
  66. package/src/context/refusal.ts +3 -7
  67. package/src/context/verb.ts +1 -5
  68. package/src/host/agent-host.ts +2 -2
  69. package/src/host/handlers.ts +0 -1
  70. package/src/host/wiring.ts +1 -1
  71. package/src/lineage.ts +2 -3
  72. package/src/messages/deliver-message.ts +6 -11
  73. package/src/messages/deliver-notice.ts +8 -12
  74. package/src/messages/notice-store.ts +3 -5
  75. package/src/outbox/outbox-worker.ts +5 -2
  76. package/src/providers/accounts/service.ts +12 -13
  77. package/src/providers/accounts/variables.ts +1 -0
  78. package/src/providers/claude.ts +0 -1
  79. package/src/providers/detect.ts +2 -11
  80. package/src/record-directory.ts +0 -1
  81. package/src/skills.ts +0 -1
  82. package/src/tasks/task-coordinator.ts +35 -33
  83. package/src/tasks/task-store.ts +1 -1
  84. package/src/tasks/task-work.ts +2 -2
  85. package/src/tasks/waiting-child.ts +6 -3
  86. package/src/tasks/wake-parent.ts +21 -16
  87. package/src/tasks/wiring.ts +33 -41
  88. package/src/usage/aggregate.ts +80 -50
  89. package/src/usage/exchange.ts +1 -1
  90. package/src/usage/limits/monitor.ts +4 -4
  91. package/src/usage/limits/probe.ts +2 -2
  92. package/src/usage/pricing.ts +1 -1
  93. package/src/usage/roots.ts +1 -2
  94. package/src/usage/scanner.ts +8 -10
  95. package/src/usage/usage-service.ts +5 -9
@@ -1,12 +1,12 @@
1
1
  import { mkdir, open, readFile, readdir, stat } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
+ import { errorText } from '../error-text.js';
3
4
  import { isNotFound, writeAtomic } from '../fs.js';
4
5
  import { decodeIndex, encodeIndex } from './index-file.js';
5
6
  import { foldByKey } from './record.js';
6
7
  import { claudeMightCarryUsage, parseClaudeLine } from './readers/claude.js';
7
8
  import { cloneCodexState, codexMightCarryUsage, createCodexState, parseCodexLine } from './readers/codex.js';
8
9
  import { usageRoots } from './roots.js';
9
- import { errorText } from '../error-text.js';
10
10
  /* What a file holds from `from` to its end, as it is at the read. */
11
11
  async function readFrom(path, from) {
12
12
  const handle = await open(path, 'r');
@@ -53,11 +53,11 @@ async function listJsonl(dir, out = []) {
53
53
  * read again next time. The Codex state is snapshotted before that tail for the same reason.
54
54
  */
55
55
  function parseChunk(text, provider, from, state) {
56
- let records = [];
57
56
  const end = text.lastIndexOf('\n');
58
57
  const whole = end === -1 ? '' : text.slice(0, end);
59
58
  const tail = text.slice(end + 1);
60
59
  const read = (chunk, codex) => {
60
+ const records = [];
61
61
  for (const raw of chunk.split('\n')) {
62
62
  const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw;
63
63
  if (line === '') {
@@ -79,14 +79,12 @@ function parseChunk(text, provider, from, state) {
79
79
  }
80
80
  }
81
81
  }
82
+ return records;
82
83
  };
83
84
  const codex = provider === 'codex' ? (state ?? createCodexState()) : null;
84
- read(whole, codex);
85
+ const counted = read(whole, codex);
85
86
  const committed = codex === null ? null : cloneCodexState(codex);
86
- const counted = records;
87
- records = [];
88
- read(tail, codex);
89
- return { records: counted, tail: records, offset: from + (end === -1 ? 0 : Buffer.byteLength(whole, 'utf8') + 1), codex: committed };
87
+ return { records: counted, tail: read(tail, codex), offset: from + (end === -1 ? 0 : Buffer.byteLength(whole, 'utf8') + 1), codex: committed };
90
88
  }
91
89
  /*
92
90
  * The account a record of a root belongs to, as a record keeps it (absent for the default account):
@@ -113,8 +111,8 @@ function withAccount(records, accountOf) {
113
111
  });
114
112
  }
115
113
  /*
116
- * Read appended transcript bytes incrementally, but restart after rewrites or truncation. Preserve
117
- * totals for deleted transcripts because their usage still happened.
114
+ * Reads only what a transcript appended since the last scan, and starts a file over once it was
115
+ * rewritten or cut. A deleted transcript keeps its totals, since its usage still happened.
118
116
  */
119
117
  export class UsageScanner {
120
118
  home;
@@ -1,8 +1,8 @@
1
1
  import type { UsageAccount, UsageSummaryPayload, UsageSummaryResult } from '@adecore/agent-contracts';
2
2
  import type { AgentSink } from '../events.ts';
3
3
  import type { KnownProject } from './projects.ts';
4
- import { type UsageScannerOptions } from './scanner.ts';
5
4
  import type { UsageRootPath } from './roots.ts';
5
+ import { type UsageScannerOptions } from './scanner.ts';
6
6
  export interface UsageServiceOptions {
7
7
  home: string;
8
8
  allowPriceFetch?: boolean;
@@ -31,5 +31,4 @@ export declare class UsageService {
31
31
  stop(): void;
32
32
  private rescan;
33
33
  private runScan;
34
- private emit;
35
34
  }
@@ -1,9 +1,9 @@
1
+ import { ClientSinks } from '../client-sinks.js';
2
+ import { errorText } from '../error-text.js';
1
3
  import { accountOfRecord, aggregate } from './aggregate.js';
2
4
  import { ExchangeRates } from './exchange.js';
3
5
  import { PriceBook } from './pricing.js';
4
6
  import { UsageScanner } from './scanner.js';
5
- import { errorText } from '../error-text.js';
6
- import { ClientSinks } from '../client-sinks.js';
7
7
  /* A scan this fresh answers the question the page is asking, so nothing is opened for it. */
8
8
  const SCAN_TTL_MS = 60_000;
9
9
  /* The accounts of the machine, and after them any a record names that the machine no longer has, under its id. */
@@ -121,10 +121,7 @@ export class UsageService {
121
121
  }
122
122
  this.report = report;
123
123
  if (report.changedFiles > 0) {
124
- this.emit({ event: 'usage.changed', payload: { scannedAt: report.at } });
124
+ this.sinks.emit({ event: 'usage.changed', payload: { scannedAt: report.at } });
125
125
  }
126
126
  }
127
- emit(event) {
128
- this.sinks.emit(event);
129
- }
130
127
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/agents",
3
- "version": "0.21.1",
3
+ "version": "0.21.2",
4
4
  "description": "Node-compatible agent chat hosts, providers, accounts, usage and durable task coordination.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -20,8 +20,8 @@
20
20
  "test": "bun --conditions=source test"
21
21
  },
22
22
  "dependencies": {
23
- "@adecore/agent-contracts": "0.21.1",
24
- "@adecore/intelligent-ui": "0.21.1",
23
+ "@adecore/agent-contracts": "0.21.2",
24
+ "@adecore/intelligent-ui": "0.21.2",
25
25
  "zod": "^4.6.5"
26
26
  },
27
27
  "devDependencies": {
package/src/async.ts CHANGED
@@ -1,9 +1,6 @@
1
- /* The two waits the host does: a pause it can be woken from, and a deadline on somebody else's promise. */
2
-
3
1
  /*
4
- * Resolves after `ms`, or at once when the signal is raised. An abort ends the wait rather than
5
- * failing it: every caller here is a loop that checks the signal itself on the next turn, and a
6
- * rejection would only be caught and thrown away.
2
+ * Resolves after `ms`, or at once when the signal is raised. An abort resolves instead of rejecting:
3
+ * every caller is a loop that checks the signal itself on its next turn.
7
4
  */
8
5
  export function wait(ms: number, signal?: AbortSignal): Promise<void> {
9
6
  return new Promise((resolve) => {
@@ -19,10 +16,7 @@ export function wait(ms: number, signal?: AbortSignal): Promise<void> {
19
16
  });
20
17
  }
21
18
 
22
- /*
23
- * The promise, or `message` when it takes longer than `ms`. The timer is cleared either way, so a
24
- * call that answers at once does not hold the process for the rest of its deadline.
25
- */
19
+ /* The timer is cleared either way, so a call that answers at once does not hold the process until its deadline. */
26
20
  export function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
27
21
  return new Promise((resolve, reject) => {
28
22
  const timer = setTimeout(() => reject(new Error(message)), ms);
@@ -27,7 +27,6 @@ import {
27
27
  type ModelSelection,
28
28
  type RuntimeMode
29
29
  } from '@adecore/agent-contracts';
30
- import { ChatUiQueries, type ChatUiHost } from './ui-queries.ts';
31
30
  import { ClientSinks } from '../client-sinks.ts';
32
31
  import { errorText } from '../error-text.ts';
33
32
  import type { AgentEvent, AgentSink } from '../events.ts';
@@ -38,6 +37,7 @@ import { SkillIndex } from '../skills.ts';
38
37
  import type { LimitsUpdate } from '../usage/limits/normalize.ts';
39
38
  import { usageRoots } from '../usage/roots.ts';
40
39
  import type { AttachmentStore } from './attachment-store.ts';
40
+ import { commandLabel, isBackgroundWork, runsInBackground, type BackgroundWork } from './background-work.ts';
41
41
  import type { BookmarkStore } from './bookmark-store.ts';
42
42
  import { ChatLog, COMPACT_ABOVE_BYTES } from './chat-log.ts';
43
43
  import type { SpawnChatProcess } from './chat-process.ts';
@@ -57,10 +57,10 @@ import type { CodexProcessSpec } from './codex-thread.ts';
57
57
  import type { CodexClientInfo } from './codex-transport.ts';
58
58
  import { ComposerPreferences } from './composer-preferences.ts';
59
59
  import { DeltaCoalescer } from './delta-coalescer.ts';
60
- import { readableAssistantText } from './readable-text.ts';
61
60
  import { ChatError } from './errors.ts';
62
- import { commandLabel, isBackgroundWork, runsInBackground, type BackgroundWork } from './background-work.ts';
61
+ import { readableAssistantText } from './readable-text.ts';
63
62
  import { SubagentReader, type SubagentReaderOptions } from './subagent-reader.ts';
63
+ import { ChatUiQueries, type ChatUiHost } from './ui-queries.ts';
64
64
  import type { VisualInput, VisualStore } from './visual-store.ts';
65
65
 
66
66
  /* A turn that was running when the host went down, and the attempt that would take it up again. */
@@ -72,12 +72,12 @@ export interface InterruptedRun {
72
72
 
73
73
  type BookmarkableItem = Extract<ChatItem, { kind: 'user' | 'assistant' }>;
74
74
 
75
- /* A message a person or the agent wrote in the chat's own thread; a subagent's words belong to its row. */
76
75
  function withoutHidden(info: ChatInfo): ChatInfo {
77
76
  const { hidden: _hidden, ...rest } = info;
78
77
  return rest;
79
78
  }
80
79
 
80
+ /* A message a person or the agent wrote in the chat's own thread; a subagent's words belong to its row. */
81
81
  function isBookmarkable(item: ChatItem): item is BookmarkableItem {
82
82
  return item.kind === 'user' || (item.kind === 'assistant' && (item.parentToolUseId ?? null) === null);
83
83
  }
@@ -48,8 +48,8 @@ export interface ChatLogState {
48
48
  }
49
49
 
50
50
  /*
51
- * Number and synchronously append each broadcast event so disk, memory and snapshot boundaries keep
52
- * one order. Recent lines stay in memory for immediate `since` attachment.
51
+ * Numbers every broadcast event and appends it synchronously, so disk, memory and snapshot boundaries
52
+ * keep one order. Recent lines stay in memory to answer an attach with `since` at once.
53
53
  */
54
54
  export class ChatLog {
55
55
  private readonly path: string | null;
@@ -4,12 +4,12 @@ import { join } from 'node:path';
4
4
  import { ChatInfoSchema, ChatItemSchema, type ChatInfo, type ChatItem } from '@adecore/agent-contracts';
5
5
  import { z } from 'zod';
6
6
  import { isNotFound, writeAtomic, writeAtomicSync } from '../fs.ts';
7
- import { isBookmarkFileName } from './bookmark-store.ts';
7
+ import { recordFileName } from '../record-directory.ts';
8
8
  import { migrateInlineAttachments, type AttachmentStore } from './attachment-store.ts';
9
+ import { isBookmarkFileName } from './bookmark-store.ts';
9
10
  import { parseLog, type ChatLogLine } from './chat-log.ts';
10
11
  import { ChatThread } from './thread.ts';
11
12
  import { isVisualFileName } from './visual-store.ts';
12
- import { recordFileName } from '../record-directory.ts';
13
13
 
14
14
  // zod strips what it does not know inside the info and the items, so a file written by an older build
15
15
  // (with `interactionMode`, say) still parses and loses only the dropped field. A record without `seq`
@@ -30,7 +30,7 @@ const OWN_KEYS = new Set(['info', 'items', 'seq', 'resetSeq', 'preambles', 'uiAc
30
30
  /* What a host keeps in a chat's record beside the thread; it never reaches the wire. */
31
31
  export type ChatRecordExtras = Record<string, unknown>;
32
32
 
33
- function extrasOf(record: Record<string, unknown>): ChatRecordExtras {
33
+ function withoutOwnKeys(record: Record<string, unknown>): ChatRecordExtras {
34
34
  return Object.fromEntries(Object.entries(record).filter(([key]) => !OWN_KEYS.has(key)));
35
35
  }
36
36
 
@@ -90,10 +90,6 @@ function liftUiAccess(record: unknown): unknown {
90
90
  return Object.keys(uiAccess).length === 0 ? record : { ...record, uiAccess };
91
91
  }
92
92
 
93
- function withoutOwnKeys(extras: ChatRecordExtras): ChatRecordExtras {
94
- return Object.fromEntries(Object.entries(extras).filter(([key]) => !OWN_KEYS.has(key)));
95
- }
96
-
97
93
  export interface ChatStoreOptions {
98
94
  attachments?: AttachmentStore | null;
99
95
  // Files a host keeps beside a record under names that also end in .json (a chat's plans), which are no chats.
@@ -214,7 +210,7 @@ export class ChatStore {
214
210
  resetSeq,
215
211
  preambles: snapshot.preambles ?? [],
216
212
  uiAccess: snapshot.uiAccess ?? {},
217
- extras: extrasOf(snapshot),
213
+ extras: withoutOwnKeys(snapshot),
218
214
  lines
219
215
  };
220
216
  }
@@ -1,7 +1,7 @@
1
1
  import type { AgentKind } from '@adecore/agent-contracts';
2
- import { cleanTitle } from '../title-file.ts';
3
2
  import type { ProviderRegistry } from '../providers/registry.ts';
4
3
  import { runProcess } from '../run-process.ts';
4
+ import { cleanTitle } from '../title-file.ts';
5
5
 
6
6
  // Enough of a conversation to name it; the rest of a long prompt or answer says nothing a title needs.
7
7
  const MAX_PROMPT_CHARS = 2000;
@@ -19,10 +19,7 @@ function cap(text: string, limit: number): string {
19
19
  return text.length <= limit ? text : `${text.slice(0, limit)}...`;
20
20
  }
21
21
 
22
- /*
23
- * What the CLI is asked. JSON again, as for a commit message: a model left to write freely opens
24
- * with a sentence, and that sentence would become the name of the node.
25
- */
22
+ /* JSON, since a model left to write freely opens with a sentence, and that sentence would become the name. */
26
23
  export function buildTitlePrompt(prompt: string, answer: string): string {
27
24
  return [
28
25
  'Write a short title for the conversation below, as a person would name it in a list of chats.',
@@ -42,9 +39,8 @@ export function buildTitlePrompt(prompt: string, answer: string): string {
42
39
  }
43
40
 
44
41
  /*
45
- * The title out of what the CLI printed, or null. Strict on purpose, unlike a commit message: a run
46
- * that holds no `{"title": "..."}` object printed something other than a title, and a derived name
47
- * is better than a stray line of it.
42
+ * Strict on purpose: a run that holds no `{"title": "..."}` object printed something other than a
43
+ * title, and a derived name is better than a stray line of it.
48
44
  */
49
45
  export function parseTitle(output: string): string | null {
50
46
  const start = [...output.matchAll(/\{\s*"title"/g)].at(-1)?.index;
@@ -1,5 +1,5 @@
1
- import { errorText } from '../error-text.ts';
2
1
  import { randomUUID } from 'node:crypto';
2
+ import { errorText } from '../error-text.ts';
3
3
  import { claudeArgs, claudeEnv, promptPrefix } from '../providers/claude.ts';
4
4
  import type { ApprovalDecision, BackendHost, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
5
5
  import { ChatChild } from './chat-process.ts';
@@ -71,12 +71,10 @@ export class ClaudeBackend implements ChatBackend {
71
71
  }
72
72
 
73
73
  sendTurn(input: TurnInput): void {
74
- // The prefix is what the CLI must see first (ultrathink), then the note about the links, then what was typed.
75
- const prefix = `${promptPrefix(this.launch.selection)}${input.preamble === null ? '' : `${input.preamble}\n\n`}`;
76
74
  const promptId = input.promptId ?? randomUUID();
77
75
  this.active = true;
78
76
  this.protocol.beginPrompt(promptId);
79
- this.write(buildUserMessage({ text: input.text, attachments: input.attachments, prefix, skills: input.skills, promptId }));
77
+ this.write(this.userMessage(input, promptId));
80
78
  }
81
79
 
82
80
  steerTurn(input: TurnInput): Promise<boolean> {
@@ -88,8 +86,7 @@ export class ClaudeBackend implements ChatBackend {
88
86
  this.steers.set(promptId, { resolve, reject });
89
87
  this.protocol.beginSteer(promptId);
90
88
  try {
91
- const prefix = `${promptPrefix(this.launch.selection)}${input.preamble === null ? '' : `${input.preamble}\n\n`}`;
92
- this.write(buildUserMessage({ text: input.text, attachments: input.attachments, prefix, skills: input.skills, promptId }));
89
+ this.write(this.userMessage(input, promptId));
93
90
  } catch (error) {
94
91
  this.steers.delete(promptId);
95
92
  reject(error instanceof Error ? error : new Error(errorText(error)));
@@ -147,6 +144,12 @@ export class ClaudeBackend implements ChatBackend {
147
144
  return child?.end() ?? Promise.resolve();
148
145
  }
149
146
 
147
+ private userMessage(input: TurnInput, promptId: string): Record<string, unknown> {
148
+ // What the CLI must see first (ultrathink), then the preamble, then what was typed.
149
+ const prefix = `${promptPrefix(this.launch.selection)}${input.preamble === null ? '' : `${input.preamble}\n\n`}`;
150
+ return buildUserMessage({ text: input.text, attachments: input.attachments, prefix, skills: input.skills, promptId });
151
+ }
152
+
150
153
  private async readLines(child: ChatChild): Promise<void> {
151
154
  const reader = child.process.stdout.getReader();
152
155
  const decoder = new TextDecoder();
@@ -1,8 +1,8 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readdir, stat } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
- import { usageRoots } from '../usage/roots.ts';
5
4
  import { cleanTitle, readLines } from '../title-file.ts';
5
+ import { usageRoots } from '../usage/roots.ts';
6
6
 
7
7
  interface Progress {
8
8
  offset: number;
@@ -5,8 +5,8 @@ import type { ChatHistoryResult, ChatItem } from '@adecore/agent-contracts';
5
5
  import { readLines } from '../title-file.ts';
6
6
  import { ClaudeProtocol } from './claude-protocol.ts';
7
7
  import { readingThread, settledReading } from './subagent-projection.ts';
8
- import type { ChatThread } from './thread.ts';
9
8
  import type { ThreadProjector } from './projector.ts';
9
+ import type { ChatThread } from './thread.ts';
10
10
 
11
11
  const CHUNK_BYTES = 4 * 1024 * 1024;
12
12
 
@@ -1,11 +1,11 @@
1
1
  import { accessSync, constants } from 'node:fs';
2
2
  import { attachmentImageMime, type ChatSkill } from '@adecore/agent-contracts';
3
+ import { errorText } from '../error-text.ts';
3
4
  import { codexServiceTier, codexThreadOptions } from '../providers/codex.ts';
4
5
  import type { ApprovalDecision, BackendEvent, BackendHost, BackendLaunch, ChatBackend, TurnInput } from './backend.ts';
5
6
  import { CodexProtocol } from './codex-protocol.ts';
6
- import { attachmentNote } from './input.ts';
7
7
  import { CodexTransport, DEFAULT_CODEX_CLIENT, type CodexClientInfo, type CodexFrame } from './codex-transport.ts';
8
- import { errorText } from '../error-text.ts';
8
+ import { attachmentNote } from './input.ts';
9
9
 
10
10
  function textInput(text: string) {
11
11
  return [{ type: 'text', text, text_elements: [] }];
@@ -46,18 +46,15 @@ export interface CodexBackendOptions {
46
46
  */
47
47
  export class CodexBackend implements ChatBackend {
48
48
  readonly acknowledgesTurns = true;
49
- private promptId: string | null = null;
50
49
  private readonly launch: BackendLaunch;
51
- private imageWrites: Promise<void> | null = null;
52
50
  private readonly host: BackendHost;
53
51
  private readonly protocol: CodexProtocol;
54
52
  private readonly client: CodexClientInfo;
53
+ private promptId: string | null = null;
54
+ private imageWrites: Promise<void> | null = null;
55
55
  private transport: CodexTransport | null = null;
56
56
  private threadId = '';
57
- /*
58
- * Codex 0.154 ignores developer instructions on `thread/resume` (measured), so a thread started
59
- * before anything was linked would never hear of its links; the first prompt of a resumed process says so instead.
60
- */
57
+ // Codex 0.154 ignores developer instructions on `thread/resume` (measured), so the first prompt of a resumed process carries them.
61
58
  private contextPending: string | null = null;
62
59
  private imageInputSupported: boolean | null = null;
63
60
  /* A turn asked for that Codex has not named yet; `turn/interrupt` needs that name. */
@@ -172,7 +172,7 @@ function elicitationInput(meta: Frame): Frame {
172
172
  return input;
173
173
  }
174
174
 
175
- // Every spent window must reset; one unknown reset keeps resuming a person's choice.
175
+ // The latest reset of every spent window; one without a known reset leaves resuming to a person.
176
176
  function spentUntil(windows: readonly UsageWindow[]): number | null {
177
177
  const spent = windows.filter((window) => window.used >= 1);
178
178
  if (spent.length === 0 || spent.some((window) => window.resetsAt === null)) {
@@ -1,10 +1,10 @@
1
1
  import type { ChatItem } from '@adecore/agent-contracts';
2
+ import { errorText } from '../error-text.ts';
2
3
  import type { SpawnChatProcess } from './chat-process.ts';
3
4
  import { CodexProtocol } from './codex-protocol.ts';
4
5
  import { CodexTransport, DEFAULT_CODEX_CLIENT, type CodexClientInfo } from './codex-transport.ts';
5
6
  import { ChatError } from './errors.ts';
6
7
  import { readingThread, settledReading } from './subagent-projection.ts';
7
- import { errorText } from '../error-text.ts';
8
8
 
9
9
  function isRecord(value: unknown): value is Record<string, unknown> {
10
10
  return typeof value === 'object' && value !== null && !Array.isArray(value);
@@ -116,14 +116,15 @@ async function nthTurnId(transport: CodexTransport, threadId: string, turns: num
116
116
  let seen = 0;
117
117
  let cursor: string | null = null;
118
118
  do {
119
- const page = await transport.request('thread/turns/list', { threadId, limit: 100, sortDirection: 'asc', ...(cursor === null ? {} : { cursor }) });
120
- const data = isRecord(page) && Array.isArray(page.data) ? page.data.filter(isRecord) : [];
121
- for (const turn of data) {
119
+ const page = parseThreadItemsPage(
120
+ await transport.request('thread/turns/list', { threadId, limit: 100, sortDirection: 'asc', ...(cursor === null ? {} : { cursor }) })
121
+ );
122
+ for (const turn of page.entries) {
122
123
  if (++seen === turns) {
123
124
  return typeof turn.id === 'string' ? turn.id : null;
124
125
  }
125
126
  }
126
- cursor = isRecord(page) && typeof page.nextCursor === 'string' && page.nextCursor !== '' ? page.nextCursor : null;
127
+ cursor = page.nextCursor;
127
128
  } while (cursor !== null);
128
129
  return null;
129
130
  }
@@ -15,9 +15,8 @@ interface Held {
15
15
 
16
16
  /*
17
17
  * What each connected client says a new chat starts with, for the chats the host starts with no
18
- * client mounting them. Held per socket like the approval switch: a machine never remembers a
19
- * person's pick after that person's client is gone, so with nobody connected a chat gets the
20
- * host's own defaults.
18
+ * client mounting them. Held per client only, so with nobody connected a chat gets the host's own
19
+ * defaults rather than the pick of a person who left.
21
20
  */
22
21
  export class ComposerPreferences {
23
22
  private readonly byClient = new Map<string, Held>();
@@ -5,13 +5,8 @@ export const LIMIT_RETRY_DELAYS_MS: readonly number[] = [60_000, 5 * 60_000, 15
5
5
 
6
6
  /* The last turn of a thread, when it stopped on a limit. */
7
7
  export function limitedTurn(items: readonly ChatItem[]): (ChatTurnItem & { limit: ChatTurnLimit }) | null {
8
- for (let i = items.length - 1; i >= 0; i--) {
9
- const item = items[i]!;
10
- if (item.kind === 'turn') {
11
- return item.state === 'error' && item.limit !== undefined ? (item as ChatTurnItem & { limit: ChatTurnLimit }) : null;
12
- }
13
- }
14
- return null;
8
+ const last = items.findLast((item) => item.kind === 'turn');
9
+ return last?.kind === 'turn' && last.state === 'error' && last.limit !== undefined ? (last as ChatTurnItem & { limit: ChatTurnLimit }) : null;
15
10
  }
16
11
 
17
12
  /* How many turns in a row, up to and including this one, stopped on a limit: the tries a person did not step in between. */
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import type { AgentStatus, ChatApprovalItem, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem } from '@adecore/agent-contracts';
2
3
  import { ChatError } from './errors.ts';
3
4
  import { requestSummaries } from './request-summary.ts';
4
- import type { AgentStatus, ChatApprovalItem, ChatEvent, ChatHistoryResult, ChatInfo, ChatItem, ChatQuestionItem } from '@adecore/agent-contracts';
5
5
 
6
6
  /*
7
7
  * The state of one chat as the client sees it. Every mutation answers the event that describes
@@ -113,6 +113,13 @@ function tooLarge(bytes: number): never {
113
113
  );
114
114
  }
115
115
 
116
+ function refuseOversized(html: string): void {
117
+ const bytes = Buffer.byteLength(html, 'utf8');
118
+ if (bytes > VISUAL_LIMITS.bytes) {
119
+ tooLarge(bytes);
120
+ }
121
+ }
122
+
116
123
  /*
117
124
  * The visuals of every chat. The list of a chat is one file beside its record under `<home>/chats`;
118
125
  * each page is a file in the chat's attachment folder, `<id>.html`, so whatever serves an attachment
@@ -164,9 +171,7 @@ export class VisualStore {
164
171
  if (html.trim() === '') {
165
172
  refuse('visual-invalid', 'The page is empty; pass one self-contained HTML document');
166
173
  }
167
- if (Buffer.byteLength(html, 'utf8') > VISUAL_LIMITS.bytes) {
168
- tooLarge(Buffer.byteLength(html, 'utf8'));
169
- }
174
+ refuseOversized(html);
170
175
  return this.writeWorkspaceFile(chatId, name, html);
171
176
  }
172
177
 
@@ -178,9 +183,7 @@ export class VisualStore {
178
183
  async publish(chatId: string, input: VisualInput): Promise<ChatVisual> {
179
184
  const { title, html, maxHeight, layout, heights } = checked(input);
180
185
  // Before the bootstrap is added, so a page far too large is never copied first.
181
- if (Buffer.byteLength(html, 'utf8') > VISUAL_LIMITS.bytes) {
182
- tooLarge(Buffer.byteLength(html, 'utf8'));
183
- }
186
+ refuseOversized(html);
184
187
  const page = Buffer.from(injectVisualBootstrap(html), 'utf8');
185
188
  if (page.byteLength > VISUAL_LIMITS.bytes) {
186
189
  tooLarge(page.byteLength);
@@ -2,7 +2,10 @@ import type { ChatItem } from '@adecore/agent-contracts';
2
2
  import type { ChatCore } from './chat-core.ts';
3
3
  import type { ChatSession } from './chat-session.ts';
4
4
 
5
- /* A chat the host may open a turn in, as the outbox handlers see it: what its thread holds, and the one call that opens one, false while a turn or an owed resume is in the way. */
5
+ /*
6
+ * A chat the host may open a turn in, as the outbox handlers see it. `wake` answers false while a
7
+ * turn or an owed resume is in the way.
8
+ */
6
9
  export interface WakeChat {
7
10
  items(): ChatItem[];
8
11
  persist?(): Promise<void>;
@@ -1,8 +1,7 @@
1
1
  /*
2
2
  * How every no reads: `refused`, the code a script branches on and the sentence a model reads, and
3
- * under it a line per thing the actor can do instead. One row per line, and a line is a row of
4
- * tab-separated fields of its own, so only a newline is taken out of it: that would split the row in
5
- * two, and everything under the first row is read as advice.
3
+ * under it a line per thing the actor can do instead. A line keeps its tabs but loses its newlines,
4
+ * since everything under the first row is read as advice.
6
5
  */
7
6
 
8
7
  /* A field that goes into a tab-separated line; a tab or a newline in a title would split the row. */
@@ -24,10 +23,7 @@ export function refusalBody(code: string, message: string, lines: readonly strin
24
23
  return [`refused\t${first}`, ...rest].join('\n');
25
24
  }
26
25
 
27
- /*
28
- * The same without the word that names it, for a host that adds the prefix in its CLI:
29
- * The context CLI puts the prefix on, so a CLI of an older build still prints one refusal.
30
- */
26
+ /* Without the `refused` prefix, for a host whose CLI adds it, so a CLI of an older build still prints one refusal. */
31
27
  export function refusalRows(code: string, message: string, lines: readonly string[] = []): string {
32
28
  return rows(code, message, lines).join('\n');
33
29
  }
@@ -382,11 +382,7 @@ export function requiredField(needs: string): z.ZodString {
382
382
  return z.string({ error: needs }).min(1, needs);
383
383
  }
384
384
 
385
- /*
386
- * What a refusal lists, or one line saying the set is empty. A refusal that promises the groups of a
387
- * canvas and then prints nothing reads as a daemon that lost them, where the truth is that there are
388
- * none, which is a different thing to do something about.
389
- */
385
+ /* A refusal that promises a list and prints nothing reads as a host that lost it, where there is none. */
390
386
  export function orNote(lines: string[], note: string): string[] {
391
387
  return lines.length === 0 ? [`note\t${note}`] : lines;
392
388
  }
@@ -24,8 +24,8 @@ function isAgentRequest(type: string): type is AgentRequestType {
24
24
 
25
25
  /*
26
26
  * The chats of an app that has no daemon: it answers the agent requests over any `FramePort` and
27
- * sends the agent events back on it, frames checked on arrival. One port is one client. Closing the host writes every thread and ends every CLI; a chat
28
- * goes on at the next start through its CLI's own session.
27
+ * sends the agent events back on it, frames checked on arrival. One port is one client. A chat
28
+ * closed with the host goes on at the next start through its CLI's own session.
29
29
  */
30
30
  export class AgentHost<Core extends ChatCore = ChatCore> {
31
31
  readonly chats: Core;
@@ -116,7 +116,6 @@ export function chatHandlers(chats: ChatCore, providers: ProviderRegistry): { [T
116
116
  };
117
117
  }
118
118
 
119
- /* The accounts are the person's settings of this host. */
120
119
  export function accountHandlers(accounts: ProviderAccountsService): { [T in AccountRequestType]: AgentHandler<T> } {
121
120
  return {
122
121
  'accounts.list': () => accounts.snapshot(),
@@ -9,12 +9,12 @@ import { VisualStore } from '../chat/visual-store.ts';
9
9
  import type { ClaudeBackendOptions } from '../chat/claude-backend.ts';
10
10
  import { ClaudeTitleReader } from '../chat/claude-title.ts';
11
11
  import { DEFAULT_CODEX_CLIENT, type CodexClientInfo } from '../chat/codex-transport.ts';
12
+ import { errorText } from '../error-text.ts';
12
13
  import type { AgentEvent } from '../events.ts';
13
14
  import { definedEnv } from '../providers/accounts/launch.ts';
14
15
  import { ProviderAccountsService } from '../providers/accounts/service.ts';
15
16
  import type { AccountsHost } from '../providers/accounts/variables.ts';
16
17
  import { createClaudeProvider } from '../providers/claude-provider.ts';
17
- import { errorText } from '../error-text.ts';
18
18
  import { createCodexProvider } from '../providers/codex-provider.ts';
19
19
  import { codexRulesPathIn, defaultCodexHome, installCodexRules, type CodexRules } from '../providers/codex-rules.ts';
20
20
  import { ProviderRegistry, type ProviderRegistryOptions } from '../providers/registry.ts';
package/src/lineage.ts CHANGED
@@ -30,8 +30,8 @@ function openedByAgent(entry: Lineage): boolean {
30
30
  }
31
31
 
32
32
  /*
33
- * Lineage lives in `lineage` under the host's data folder, outside the agent-writable project, so agents
34
- * cannot reset their own depth. Persisting it also prevents a restart of the host from resetting recursion limits.
33
+ * Lives under the host's data folder, outside the agent-writable project, so an agent cannot reset its
34
+ * own depth; on disk, so a restart of the host does not reset it either.
35
35
  */
36
36
  export class AgentLineageStore {
37
37
  readonly dir: string;
@@ -140,7 +140,6 @@ export class AgentLineageStore {
140
140
  return entry?.relation === 'fork' ? entry.openedBy : null;
141
141
  }
142
142
 
143
- /* The forks made from a chat. */
144
143
  forksOf(nodeId: string): string[] {
145
144
  return [...this.opened.all()].filter((entry) => entry.relation === 'fork' && entry.openedBy === nodeId).map((entry) => entry.nodeId);
146
145
  }