@amalgm/chat 0.2.8 → 0.2.9-main.34746990150

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 (114) hide show
  1. package/PURPOSE.md +94 -41
  2. package/README.md +17 -4
  3. package/dist/acp/projection.d.ts +8 -0
  4. package/dist/acp/projection.d.ts.map +1 -1
  5. package/dist/acp/projection.js +10 -3
  6. package/dist/acp/projection.js.map +1 -1
  7. package/dist/conversations/index.d.ts +1 -0
  8. package/dist/conversations/index.d.ts.map +1 -1
  9. package/dist/conversations/index.js +1 -0
  10. package/dist/conversations/index.js.map +1 -1
  11. package/dist/conversations/transcript-parts.d.ts +38 -0
  12. package/dist/conversations/transcript-parts.d.ts.map +1 -0
  13. package/dist/conversations/transcript-parts.js +2 -0
  14. package/dist/conversations/transcript-parts.js.map +1 -0
  15. package/dist/conversations/transcript-tools.d.ts +11 -0
  16. package/dist/conversations/transcript-tools.d.ts.map +1 -0
  17. package/dist/conversations/transcript-tools.js +47 -0
  18. package/dist/conversations/transcript-tools.js.map +1 -0
  19. package/dist/conversations/transcript.d.ts +5 -4
  20. package/dist/conversations/transcript.d.ts.map +1 -1
  21. package/dist/conversations/transcript.js +43 -143
  22. package/dist/conversations/transcript.js.map +1 -1
  23. package/dist/conversations/types.d.ts +19 -0
  24. package/dist/conversations/types.d.ts.map +1 -1
  25. package/dist/conversations/types.js.map +1 -1
  26. package/dist/events/constructors.d.ts.map +1 -1
  27. package/dist/events/constructors.js +2 -52
  28. package/dist/events/constructors.js.map +1 -1
  29. package/dist/events/schema.d.ts +72 -72
  30. package/dist/events/schema.js +16 -16
  31. package/dist/events/schema.js.map +1 -1
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/index.js.map +1 -1
  36. package/dist/input/prompt.js +1 -1
  37. package/dist/input/prompt.js.map +1 -1
  38. package/dist/mcp/server.js +13 -13
  39. package/dist/mcp/server.js.map +1 -1
  40. package/dist/normalizers/claude.d.ts +142 -142
  41. package/dist/normalizers/claude.d.ts.map +1 -1
  42. package/dist/normalizers/claude.js +23 -37
  43. package/dist/normalizers/claude.js.map +1 -1
  44. package/dist/normalizers/codex.d.ts +71 -71
  45. package/dist/normalizers/codex.d.ts.map +1 -1
  46. package/dist/normalizers/codex.js +12 -24
  47. package/dist/normalizers/codex.js.map +1 -1
  48. package/dist/normalizers/index.d.ts +0 -1
  49. package/dist/normalizers/index.d.ts.map +1 -1
  50. package/dist/normalizers/index.js +0 -1
  51. package/dist/normalizers/index.js.map +1 -1
  52. package/dist/normalizers/opencode.d.ts.map +1 -1
  53. package/dist/normalizers/opencode.js +9 -6
  54. package/dist/normalizers/opencode.js.map +1 -1
  55. package/dist/normalizers/pi.d.ts.map +1 -1
  56. package/dist/normalizers/pi.js +27 -14
  57. package/dist/normalizers/pi.js.map +1 -1
  58. package/dist/sessions/chat.d.ts +5 -5
  59. package/dist/sessions/chat.d.ts.map +1 -1
  60. package/dist/sessions/chat.js +51 -19
  61. package/dist/sessions/chat.js.map +1 -1
  62. package/dist/sessions/index.d.ts +1 -1
  63. package/dist/sessions/index.d.ts.map +1 -1
  64. package/dist/sessions/index.js +1 -1
  65. package/dist/sessions/index.js.map +1 -1
  66. package/dist/usage/acp.d.ts +4 -0
  67. package/dist/usage/acp.d.ts.map +1 -0
  68. package/dist/usage/acp.js +20 -0
  69. package/dist/usage/acp.js.map +1 -0
  70. package/dist/usage/finalize.d.ts +1 -11
  71. package/dist/usage/finalize.d.ts.map +1 -1
  72. package/dist/usage/finalize.js +29 -47
  73. package/dist/usage/finalize.js.map +1 -1
  74. package/dist/usage/index.d.ts +2 -1
  75. package/dist/usage/index.d.ts.map +1 -1
  76. package/dist/usage/index.js +2 -1
  77. package/dist/usage/index.js.map +1 -1
  78. package/dist/usage/normalize.d.ts +8 -9
  79. package/dist/usage/normalize.d.ts.map +1 -1
  80. package/dist/usage/normalize.js +20 -20
  81. package/dist/usage/normalize.js.map +1 -1
  82. package/dist/usage/ports.d.ts +4 -4
  83. package/dist/usage/ports.d.ts.map +1 -1
  84. package/dist/usage/ports.js +5 -5
  85. package/dist/usage/ports.js.map +1 -1
  86. package/dist/usage/tokens.d.ts +13 -38
  87. package/dist/usage/tokens.d.ts.map +1 -1
  88. package/dist/usage/tokens.js +39 -72
  89. package/dist/usage/tokens.js.map +1 -1
  90. package/host/adapters/acp-prompt.js +36 -0
  91. package/host/adapters/acp.js +50 -71
  92. package/host/adapters/attachments.js +37 -0
  93. package/host/adapters/claude.js +9 -2
  94. package/host/adapters/codex.js +36 -18
  95. package/host/adapters/cursor.js +72 -99
  96. package/host/adapters/opencode.js +39 -25
  97. package/host/adapters/pi.js +24 -19
  98. package/host/adapters/prompt.js +13 -21
  99. package/host/adapters/provider-url.js +8 -0
  100. package/host/auth.js +10 -9
  101. package/host/http.js +3 -2
  102. package/host/model-resolution.js +7 -0
  103. package/host/native-contract.js +4 -3
  104. package/host/native-runtime.js +25 -11
  105. package/host/sqlite/index.d.ts +2 -0
  106. package/host/sqlite/index.js +48 -4
  107. package/host/tooling/runtime-home.js +1 -1
  108. package/host/tooling/system-instructions.js +7 -6
  109. package/host/tooling/system-prompt.js +12 -18
  110. package/package.json +26 -2
  111. package/dist/normalizers/cursor.d.ts +0 -33
  112. package/dist/normalizers/cursor.d.ts.map +0 -1
  113. package/dist/normalizers/cursor.js +0 -196
  114. package/dist/normalizers/cursor.js.map +0 -1
@@ -1,95 +1,62 @@
1
1
  /**
2
2
  * Token counting primitives owned by the usage module.
3
3
  */
4
- /** Coerce to a finite number, else 0. */
5
- export function n(value) {
6
- const parsed = Number(value || 0);
7
- return Number.isFinite(parsed) ? parsed : 0;
8
- }
9
4
  /**
10
5
  * Return the numeric value when finite and non-negative, otherwise null.
11
- * `Number(null)` is zero, so callers representing absence pass `undefined`.
6
+ * Absence is never a measured zero.
12
7
  */
13
8
  export function nonNegativeOrNull(value) {
9
+ if ((typeof value !== 'number' && typeof value !== 'string') || value === '')
10
+ return null;
14
11
  const parsed = Number(value);
15
12
  return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
16
13
  }
17
- /**
18
- * Engine `normalizeTokenList()`: coerce any of the accepted key spellings
19
- * into the canonical six-bucket list. `total` prefers an explicit
20
- * non-negative total (nested or flat), else the computed sum of the four
21
- * context buckets — `thought` is informational and never counted into the
22
- * computed total.
23
- */
14
+ /** Unknown buckets stay unknown; reasoning is included in output, not added again. */
24
15
  export function normalizeTokenList(input = {}) {
25
- const tokenList = (input.tokenList && typeof input.tokenList === 'object'
26
- ? input.tokenList
27
- : {});
28
- const inputTokens = n(tokenList['input'] ?? tokenList['inputTokens'] ?? input.inputTokens);
29
- const outputTokens = n(tokenList['output'] ?? tokenList['outputTokens'] ?? input.outputTokens);
30
- const cacheReadTokens = n(tokenList['cacheRead'] ?? tokenList['cacheReadTokens'] ?? input.cacheReadTokens);
31
- const cacheWriteTokens = n(tokenList['cacheWrite'] ?? tokenList['cacheWriteTokens'] ?? input.cacheWriteTokens);
32
- const thoughtTokens = n(tokenList['thought'] ?? tokenList['thoughtTokens'] ?? input.thoughtTokens);
33
- const computedTotal = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
34
- const total = nonNegativeOrNull(tokenList['total'] ?? tokenList['totalTokens'] ?? input.totalTokens)
35
- ?? computedTotal;
36
- return {
37
- input: inputTokens,
38
- output: outputTokens,
39
- cacheRead: cacheReadTokens,
40
- cacheWrite: cacheWriteTokens,
41
- thought: thoughtTokens,
42
- total,
43
- };
44
- }
45
- /**
46
- * Engine `hasTokenListBuckets()`: true when any bucket coerces to a finite
47
- * number. Mirrors the engine's `Number()` coercion exactly — `null`
48
- * coerces to 0 (finite → true), `undefined` to NaN (→ false).
49
- */
50
- export function hasTokenListBuckets(tokenList = {}) {
16
+ const list = (input.tokenList && typeof input.tokenList === 'object' ? input.tokenList : {});
17
+ const inputTokens = nonNegativeOrNull(list['input'] ?? list['inputTokens'] ?? input.inputTokens);
18
+ const output = nonNegativeOrNull(list['output'] ?? list['outputTokens'] ?? input.outputTokens);
19
+ const cacheRead = nonNegativeOrNull(list['cacheRead'] ?? list['cacheReadTokens'] ?? input.cacheReadTokens);
20
+ const cacheWrite = nonNegativeOrNull(list['cacheWrite'] ?? list['cacheWriteTokens'] ?? input.cacheWriteTokens);
21
+ const thought = nonNegativeOrNull(list['thought'] ?? list['thoughtTokens'] ?? input.thoughtTokens);
22
+ const explicitTotal = nonNegativeOrNull(list['total'] ?? list['totalTokens'] ?? input.totalTokens);
23
+ const buckets = [inputTokens, output, cacheRead, cacheWrite];
24
+ const total = explicitTotal ?? (buckets.every(value => value !== null)
25
+ ? buckets.reduce((sum, value) => sum + value, 0) : null);
26
+ return { input: inputTokens, output, cacheRead, cacheWrite, thought, total };
27
+ }
28
+ export function hasTokenListBuckets(list = {}) {
51
29
  return ['input', 'output', 'cacheRead', 'cacheWrite', 'thought', 'total']
52
- .some((key) => Number.isFinite(Number(tokenList[key])));
30
+ .some(key => nonNegativeOrNull(list[key]) !== null);
53
31
  }
54
- /** Engine `zeroTokenList()`: a fresh all-zero list. */
32
+ /** Additive identity, used only for a proven empty accumulator. */
55
33
  export function zeroTokenList() {
56
34
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, thought: 0, total: 0 };
57
35
  }
58
- /**
59
- * Engine `addTokenLists()`: bucket-wise sum, re-normalized (so `total` is
60
- * recomputed from the four context buckets, never carried over).
61
- */
36
+ function combine(a, b, subtract) {
37
+ a = normalizeTokenList({ tokenList: a });
38
+ b = normalizeTokenList({ tokenList: b });
39
+ const bucket = (key) => {
40
+ const left = nonNegativeOrNull(a[key]);
41
+ const right = nonNegativeOrNull(b[key]);
42
+ if (left === null || right === null || (subtract && left < right))
43
+ return null;
44
+ return subtract ? left - right : left + right;
45
+ };
46
+ return { input: bucket('input'), output: bucket('output'), cacheRead: bucket('cacheRead'),
47
+ cacheWrite: bucket('cacheWrite'), thought: bucket('thought'), total: bucket('total') };
48
+ }
62
49
  export function addTokenLists(a = {}, b = {}) {
63
- return normalizeTokenList({
64
- inputTokens: n(a.input) + n(b.input),
65
- outputTokens: n(a.output) + n(b.output),
66
- cacheReadTokens: n(a.cacheRead) + n(b.cacheRead),
67
- cacheWriteTokens: n(a.cacheWrite) + n(b.cacheWrite),
68
- thoughtTokens: n(a.thought) + n(b.thought),
69
- });
50
+ return combine(a, b, false);
70
51
  }
71
- /**
72
- * Engine `subtractTokenLists()`: bucket-wise `after - before`, clamped at
73
- * 0 per bucket — a cumulative meter that reset (e.g. across compaction)
74
- * can never produce a negative delta.
75
- */
52
+ /** A missing baseline or a counter reset cannot prove a delta. */
76
53
  export function subtractTokenLists(after = {}, before = {}) {
77
- return normalizeTokenList({
78
- inputTokens: Math.max(0, n(after.input) - n(before.input)),
79
- outputTokens: Math.max(0, n(after.output) - n(before.output)),
80
- cacheReadTokens: Math.max(0, n(after.cacheRead) - n(before.cacheRead)),
81
- cacheWriteTokens: Math.max(0, n(after.cacheWrite) - n(before.cacheWrite)),
82
- thoughtTokens: Math.max(0, n(after.thought) - n(before.thought)),
83
- });
54
+ return combine(after, before, true);
84
55
  }
85
- /**
86
- * Total tokens for a usage record: tokenList total, else context size, else
87
- * the sum of the four buckets. Precedence uses `||`, exactly as the engine —
88
- * a zero total falls through to the next source.
89
- */
56
+ /** Explicit totals and measured context are distinct from unknown token buckets. */
90
57
  export function totalUsageTokens(usage = {}) {
91
- return (n(usage.tokenList?.total) ||
92
- n(usage.inputContextSize) ||
93
- n(usage.inputTokens) + n(usage.outputTokens) + n(usage.cacheReadTokens) + n(usage.cacheWriteTokens));
58
+ return nonNegativeOrNull(usage.tokenList?.total)
59
+ ?? nonNegativeOrNull(usage.inputContextSize)
60
+ ?? normalizeTokenList(usage).total;
94
61
  }
95
62
  //# sourceMappingURL=tokens.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/usage/tokens.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/usage/tokens.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ /** One ACP prompt loop preserves official updates and drains them before completion. */
2
+ export async function* runAcpPrompt(session, prompt, { onUpdate, onResult } = {}) {
3
+ const queue = [];
4
+ let wake = null;
5
+ let finished = false;
6
+ let failure = null;
7
+ const wakeLoop = () => { if (wake) { wake(); wake = null; } };
8
+ const off = session.client.onNotification((message) => {
9
+ if (message.method !== 'session/update') return;
10
+ if (message.params?.sessionId && message.params.sessionId !== session.providerSessionId) return;
11
+ const update = message.params?.update;
12
+ if (!update) return;
13
+ onUpdate?.(message);
14
+ if (update.sessionUpdate === 'error') {
15
+ failure = new Error(update.message || 'ACP agent failed');
16
+ } else queue.push(update);
17
+ wakeLoop();
18
+ });
19
+ session.cancelRequested = false;
20
+ session.activePrompt = true;
21
+ session.client.request('session/prompt', { sessionId: session.providerSessionId, prompt }, 0)
22
+ .then(async result => { await onResult?.(result); })
23
+ .catch(error => { if (!session.cancelRequested) failure = error; })
24
+ .finally(() => { finished = true; wakeLoop(); });
25
+ try {
26
+ while (!finished || queue.length) {
27
+ if (queue.length) yield queue.shift();
28
+ else await new Promise(resolve => { wake = resolve; });
29
+ }
30
+ if (failure) throw failure;
31
+ } finally {
32
+ off();
33
+ session.activePrompt = false;
34
+ session.cancelRequested = false;
35
+ }
36
+ }
@@ -1,3 +1,5 @@
1
+ import { usageFromAcpPrompt } from '../../dist/usage/index.js';
2
+ import { runAcpPrompt } from './acp-prompt.js';
1
3
  /** Generic ACP agent adapter. Agent-specific launch data comes from the resolved revision. */
2
4
 
3
5
  import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk';
@@ -155,47 +157,52 @@ export class AcpAdapter {
155
157
  permissionMode: contract.permissionMode,
156
158
  activePrompt: false,
157
159
  };
158
- client.onRequest((method, params) => this.handleRequest(session, method, params));
159
- const initialized = await client.request('initialize', {
160
- protocolVersion: PROTOCOL_VERSION,
161
- clientInfo: { name: 'amalgm-chat', version: '0.1.2' },
162
- clientCapabilities: {
163
- fs: { readTextFile: false, writeTextFile: false },
164
- session: { configOptions: { boolean: {} } },
165
- },
166
- });
167
- if (initialized?.protocolVersion !== PROTOCOL_VERSION) {
168
- throw new UnsupportedExecutionInputError('acp', 'protocol', `agent negotiated version ${initialized?.protocolVersion}`);
169
- }
170
- session.capabilities = initialized.agentCapabilities || {};
171
- const methodId = record(launch.authMethodIds)?.[contract.auth.mode];
172
- if (methodId) await client.request('authenticate', { methodId }, 120000);
160
+ try {
161
+ client.onRequest((method, params) => this.handleRequest(session, method, params));
162
+ const initialized = await client.request('initialize', {
163
+ protocolVersion: PROTOCOL_VERSION,
164
+ clientInfo: { name: 'amalgm-chat', version: '0.1.2' },
165
+ clientCapabilities: {
166
+ fs: { readTextFile: false, writeTextFile: false },
167
+ session: { configOptions: { boolean: {} } },
168
+ },
169
+ });
170
+ if (initialized?.protocolVersion !== PROTOCOL_VERSION) {
171
+ throw new UnsupportedExecutionInputError('acp', 'protocol', `agent negotiated version ${initialized?.protocolVersion}`);
172
+ }
173
+ session.capabilities = initialized.agentCapabilities || {};
174
+ const methodId = record(launch.authMethodIds)?.[contract.auth.mode];
175
+ if (methodId) await client.request('authenticate', { methodId }, 120000);
173
176
 
174
- const mcpServers = toAcpMcpServers(contract);
175
- assertAcpMcpCapabilities('acp', session.capabilities, mcpServers);
176
- const sessionParams = acpSessionParams('acp', session.capabilities, contract, mcpServers);
177
- let response;
178
- if (session.providerSessionId) {
179
- if (session.capabilities.loadSession === true) {
180
- response = await client.request('session/load', {
181
- ...sessionParams,
182
- sessionId: session.providerSessionId,
183
- }, 600000);
184
- } else if (session.capabilities.sessionCapabilities?.resume != null) {
185
- response = await client.request('session/resume', {
186
- ...sessionParams,
187
- sessionId: session.providerSessionId,
188
- }, 600000);
177
+ const mcpServers = toAcpMcpServers(contract);
178
+ assertAcpMcpCapabilities('acp', session.capabilities, mcpServers);
179
+ const sessionParams = acpSessionParams('acp', session.capabilities, contract, mcpServers);
180
+ let response;
181
+ if (session.providerSessionId) {
182
+ if (session.capabilities.loadSession === true) {
183
+ response = await client.request('session/load', {
184
+ ...sessionParams,
185
+ sessionId: session.providerSessionId,
186
+ }, 600000);
187
+ } else if (session.capabilities.sessionCapabilities?.resume != null) {
188
+ response = await client.request('session/resume', {
189
+ ...sessionParams,
190
+ sessionId: session.providerSessionId,
191
+ }, 600000);
192
+ } else {
193
+ throw new UnsupportedExecutionInputError('acp', 'resume', 'agent cannot load or resume its provider session');
194
+ }
189
195
  } else {
190
- throw new UnsupportedExecutionInputError('acp', 'resume', 'agent cannot load or resume its provider session');
196
+ response = await client.request('session/new', sessionParams, 600000);
197
+ session.providerSessionId = response?.sessionId || null;
191
198
  }
192
- } else {
193
- response = await client.request('session/new', sessionParams, 600000);
194
- session.providerSessionId = response?.sessionId || null;
199
+ if (!session.providerSessionId) throw new Error('ACP agent did not return a sessionId');
200
+ await applyConfiguration(client, session.providerSessionId, response, contract, launch);
201
+ return session;
202
+ } catch (error) {
203
+ client.close();
204
+ throw error;
195
205
  }
196
- if (!session.providerSessionId) throw new Error('ACP agent did not return a sessionId');
197
- await applyConfiguration(client, session.providerSessionId, response, contract, launch);
198
- return session;
199
206
  }
200
207
 
201
208
  async handleRequest(session, method, params) {
@@ -221,6 +228,7 @@ export class AcpAdapter {
221
228
  }
222
229
 
223
230
  async stop(session) {
231
+ session.cancelRequested = true;
224
232
  if (session.activePrompt) {
225
233
  session.client.notify('session/cancel', { sessionId: session.providerSessionId });
226
234
  }
@@ -233,41 +241,12 @@ export class AcpAdapter {
233
241
  session.client.close();
234
242
  }
235
243
 
236
- async *prompt(session, prompt, contract) {
244
+ async *prompt(session, prompt, contract, effects = {}) {
237
245
  assertAcpPromptCapabilities('acp', session.capabilities, prompt);
238
- const queue = [];
239
- let wake = null;
240
- let finished = false;
241
- let failure = null;
242
- const wakeLoop = () => { if (wake) { wake(); wake = null; } };
243
- const off = session.client.onNotification((message) => {
244
- if (message.method !== 'session/update') return;
245
- if (message.params?.sessionId && message.params.sessionId !== session.providerSessionId) return;
246
- if (message.params?.update) queue.push(message.params.update);
247
- wakeLoop();
248
- });
249
- session.activePrompt = true;
250
- session.client.request('session/prompt', {
251
- sessionId: session.providerSessionId,
252
- prompt: acpPromptParts(prompt),
253
- }, 0).then(() => {
254
- finished = true;
255
- wakeLoop();
256
- }).catch((error) => {
257
- failure = error;
258
- finished = true;
259
- wakeLoop();
260
- });
261
- try {
262
- while (!finished || queue.length) {
263
- if (queue.length) yield queue.shift();
264
- else await new Promise((resolve) => { wake = resolve; });
265
- }
266
- if (failure) throw failure;
267
- } finally {
268
- off();
269
- session.activePrompt = false;
270
- }
246
+ yield* runAcpPrompt(session, acpPromptParts(prompt), { onResult: async result => {
247
+ const usage = usageFromAcpPrompt(result?.usage);
248
+ if (usage) await effects.recordUsage?.({ usage, raw: { source: 'acp.prompt_result', payload: result } });
249
+ } });
271
250
  }
272
251
  }
273
252
 
@@ -0,0 +1,37 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { mkdir, rename, rm, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { pathToFileURL } from 'node:url';
5
+
6
+ /** Materialize file resources for native harnesses; generic ACP retains bytes. */
7
+ export async function materializePromptAttachments(prompt, root) {
8
+ const parts = [];
9
+ for (const part of prompt.parts) {
10
+ if (part.type !== 'resource'
11
+ || /^(image|audio)\//.test(part.resource.mimeType || '')) {
12
+ parts.push(part);
13
+ continue;
14
+ }
15
+ const bytes = typeof part.resource.text === 'string'
16
+ ? Buffer.from(part.resource.text, 'utf8')
17
+ : Buffer.from(part.resource.blob, 'base64');
18
+ const hash = createHash('sha256').update(bytes).digest('hex');
19
+ let basename = part.resource.uri.split('/').pop() || 'attachment';
20
+ try { basename = decodeURIComponent(basename); } catch { /* A URI need not contain percent-encoded text. */ }
21
+ const safeName = path.basename(basename).replace(/[^a-zA-Z0-9._-]/g, '_');
22
+ const name = !safeName || /^\.+$/.test(safeName) ? 'attachment' : safeName;
23
+ const directory = path.join(root, 'attachments', hash);
24
+ await mkdir(directory, { recursive: true, mode: 0o700 });
25
+ const destination = path.join(directory, name);
26
+ const temporary = `${destination}.${randomUUID()}.tmp`;
27
+ try {
28
+ await writeFile(temporary, bytes, { flag: 'wx', mode: 0o600 });
29
+ await rename(temporary, destination);
30
+ } finally {
31
+ await rm(temporary, { force: true });
32
+ }
33
+ parts.push({ type: 'resource_link', uri: pathToFileURL(destination).href,
34
+ name: basename, ...(part.resource.mimeType ? { mimeType: part.resource.mimeType } : {}), size: bytes.length });
35
+ }
36
+ return { parts };
37
+ }
@@ -118,7 +118,7 @@ class ClaudeAdapter {
118
118
  ? { debug: true, debugFile: path.join(runtime.runtimeHome, 'claude-debug.log') }
119
119
  : {}),
120
120
  settingSources: [],
121
- strictMcpConfig: false,
121
+ strictMcpConfig: true,
122
122
  ...extra,
123
123
  };
124
124
  }
@@ -151,6 +151,7 @@ class ClaudeAdapter {
151
151
  claudeUsageSnapshotFingerprints: new Set(),
152
152
  };
153
153
  const stderrChunks = [];
154
+ let latestContextSize = null;
154
155
  const captureStderr = (chunk) => {
155
156
  const text = redactSecrets(String(chunk || ''));
156
157
  if (!text) return;
@@ -184,7 +185,12 @@ class ClaudeAdapter {
184
185
  }
185
186
  const normalized = normalizeClaudeMessage(message, state);
186
187
  if (normalized.length > 0) {
187
- for (const e of normalized) yield e;
188
+ for (const e of normalized) {
189
+ if (e.type === 'usage.final' && !message.parent_tool_use_id && e.usage?.inputContextSize != null) {
190
+ latestContextSize = e.usage.inputContextSize;
191
+ }
192
+ yield e;
193
+ }
188
194
  continue;
189
195
  }
190
196
  if (message.type === 'result') {
@@ -195,6 +201,7 @@ class ClaudeAdapter {
195
201
  for (const [index, usageRecord] of usageRecords.entries()) {
196
202
  yield usageFinal({
197
203
  ...usageRecord,
204
+ inputContextSize: usageRecord.inputContextSize ?? latestContextSize,
198
205
  costUsd: usageRecords.length === 1 ? message.total_cost_usd ?? null : null,
199
206
  }, {
200
207
  providerSessionId: state.providerSessionId,
@@ -14,10 +14,11 @@ import { spawn, spawnSync } from 'node:child_process';
14
14
  import fs from 'node:fs';
15
15
  import os from 'node:os';
16
16
  import path from 'node:path';
17
+ import { zeroTokenList } from '../../dist/usage/index.js';
17
18
  import { createEventConstructors } from '../../dist/events/index.js';
18
19
  import { codexErrorMessage, createCodexNormalizer } from '../../dist/normalizers/codex.js';
19
20
  import { recordNativeEvent } from '../recorder.js';
20
- import { codexMcpSectionName, relayedMcpServers, toCodexMcpToml } from '../tooling/mcp-bundle.js';
21
+ import { toCodexMcpToml } from '../tooling/mcp-bundle.js';
21
22
  import { bundledCodexBinary, bundledCodexPathDirs, executableExists, findOnPath } from '../tooling/native-binaries.js';
22
23
  import { syncCodexProviderAuth } from '../tooling/native-config.js';
23
24
  import { prepareHarnessRuntime } from '../tooling/runtime-home.js';
@@ -50,6 +51,7 @@ class JsonLineRpc {
50
51
  this.id = 1;
51
52
  this.pending = new Map();
52
53
  this.handlers = new Set();
54
+ this.failureHandlers = new Set();
53
55
  this.requestHandler = null;
54
56
  this.buffer = '';
55
57
  this.closed = false;
@@ -81,10 +83,12 @@ class JsonLineRpc {
81
83
  }
82
84
 
83
85
  failAll(err) {
86
+ if (this.closed) return;
84
87
  this.closed = true;
85
88
  this.closeError = err;
86
89
  for (const pending of this.pending.values()) pending.reject(err);
87
90
  this.pending.clear();
91
+ for (const handler of this.failureHandlers) handler(err);
88
92
  }
89
93
 
90
94
  rejectPending(id, err) {
@@ -187,6 +191,12 @@ class JsonLineRpc {
187
191
  this.requestHandler = handler;
188
192
  }
189
193
 
194
+ onFailure(handler) {
195
+ this.failureHandlers.add(handler);
196
+ if (this.closeError) handler(this.closeError);
197
+ return () => this.failureHandlers.delete(handler);
198
+ }
199
+
190
200
  close() {
191
201
  try { this.child.stdin.end(); } catch {}
192
202
  try {
@@ -446,10 +456,6 @@ function generatedCodexHookTrust(contract, renderers = defaultRenderers) {
446
456
  return lines.join('\n');
447
457
  }
448
458
 
449
- function generatedMcpSectionNames(contract) {
450
- return relayedMcpServers(contract).map((server) => codexMcpSectionName(server.name));
451
- }
452
-
453
459
  function buildCodexConfig(contract, existingConfig, syncInfo, renderers = defaultRenderers) {
454
460
  const mcpToml = toCodexMcpToml(contract);
455
461
  const topLevelKeys = [
@@ -459,8 +465,11 @@ function buildCodexConfig(contract, existingConfig, syncInfo, renderers = defaul
459
465
  let config = removeTopLevelKeys(existingConfig, topLevelKeys);
460
466
  config = removeTomlSections(config, [
461
467
  ...(contract.authMethod === 'amalgm' ? ['model_providers.amalgm'] : []),
462
- ...generatedMcpSectionNames(contract),
468
+ 'mcp_servers',
463
469
  ]);
470
+ for (const feature of ['apps', 'plugins', 'remote_plugin']) {
471
+ config = ensureFeatureBoolean(config, feature, false);
472
+ }
464
473
  if (renderers.hasHooks(contract.agentConfig)) {
465
474
  config = ensureFeatureBoolean(config, 'codex_hooks', true);
466
475
  }
@@ -533,6 +542,7 @@ class CodexAdapter {
533
542
  writeConfig(contract, runtime.syncInfo, this.renderers);
534
543
  const binary = resolveBinary();
535
544
  const client = new JsonLineRpc({ binary, cwd: contract.cwd, env: codexEnv(runtime.env, binary) });
545
+ try {
536
546
  await client.request('initialize', { clientInfo: { name: 'amalgm-chat', version: '1.0.0' }, capabilities: { experimentalApi: true } });
537
547
  client.notify('initialized');
538
548
  const fullAccess = contract.permissionMode === 'full_access';
@@ -552,8 +562,8 @@ class CodexAdapter {
552
562
  permissionMode: contract.permissionMode,
553
563
  activeTurnId: null,
554
564
  activeTurnHadAssistant: false,
555
- cancelCurrentTurn: null,
556
565
  cancelRequested: false,
566
+ codexCumulativeTokenList: contract.providerSessionId ? null : zeroTokenList(),
557
567
  };
558
568
  client.onRequest((method, params) => this.handleServerRequest(session, method, params));
559
569
  const thread = contract.providerSessionId
@@ -565,6 +575,10 @@ class CodexAdapter {
565
575
  : await client.request('thread/start', threadOptions);
566
576
  session.providerSessionId = thread.thread.id;
567
577
  return session;
578
+ } catch (error) {
579
+ client.close();
580
+ throw error;
581
+ }
568
582
  }
569
583
 
570
584
  async handleServerRequest(session, method, params) {
@@ -629,9 +643,10 @@ class CodexAdapter {
629
643
 
630
644
  async stop(session) {
631
645
  session.cancelRequested = true;
632
- try { session.cancelCurrentTurn?.(); } catch {}
646
+ // A stop during dispatch must target the turn that dispatch actually accepts.
647
+ try { await session.startRequest; } catch { return; }
633
648
  if (!session.activeTurnId) return;
634
- await session.client.request('turn/interrupt', { threadId: session.providerSessionId, turnId: session.activeTurnId }).catch(() => {});
649
+ await session.client.request('turn/interrupt', { threadId: session.providerSessionId, turnId: session.activeTurnId });
635
650
  }
636
651
 
637
652
  async destroy(session) { session.client.close(); }
@@ -644,11 +659,8 @@ class CodexAdapter {
644
659
  const wakeLoop = () => { if (wake) { wake(); wake = null; } };
645
660
  const push = (e) => { queue.push(e); wakeLoop(); };
646
661
  session.cancelRequested = false;
647
- session.cancelCurrentTurn = () => {
648
- if (complete) return;
649
- complete = true;
650
- push(done({ providerSessionId: session.providerSessionId, stopReason: 'cancelled' }));
651
- };
662
+ session.activeTurnId = null;
663
+ session.activeTurnHadAssistant = false;
652
664
  // Subscription can synchronously deliver already-buffered provider
653
665
  // notifications. Everything the handler reads must therefore be fixed
654
666
  // before `on` opens that door.
@@ -689,8 +701,11 @@ class CodexAdapter {
689
701
  }, 500);
690
702
  }
691
703
  });
692
- session.activeTurnId = null;
693
- session.activeTurnHadAssistant = false;
704
+ const offFailure = session.client.onFailure((error) => {
705
+ if (complete) return;
706
+ complete = true;
707
+ push(errorEvent(error.message, { providerSessionId: session.providerSessionId }));
708
+ });
694
709
  const startRequest = compactCommand
695
710
  ? session.client.request('thread/compact/start', { threadId: session.providerSessionId })
696
711
  : session.client.request('turn/start', {
@@ -702,7 +717,7 @@ class CodexAdapter {
702
717
  serviceTier: serviceTierForContract(contract),
703
718
  approvalPolicy: contract.permissionMode === 'full_access' ? 'never' : 'on-request',
704
719
  });
705
- startRequest.then((result) => {
720
+ session.startRequest = startRequest.then((result) => {
706
721
  if (compactCommand) return;
707
722
  session.activeTurnId = result?.turn?.id || session.activeTurnId;
708
723
  const resultError = result?.error || result?.turn?.error || result?.turn?.lastError;
@@ -710,6 +725,7 @@ class CodexAdapter {
710
725
  push(errorEvent(codexErrorMessage(resultError), { providerSessionId: session.providerSessionId }));
711
726
  push(done({ providerSessionId: session.providerSessionId, stopReason: 'error' }));
712
727
  complete = true;
728
+ return result;
713
729
  }).catch((err) => {
714
730
  if (session.cancelRequested) {
715
731
  push(done({ providerSessionId: session.providerSessionId, stopReason: 'cancelled' }));
@@ -728,7 +744,9 @@ class CodexAdapter {
728
744
  } finally {
729
745
  if (compactCompletionTimer) clearTimeout(compactCompletionTimer);
730
746
  off();
731
- session.cancelCurrentTurn = null;
747
+ offFailure();
748
+ session.startRequest = null;
749
+ session.activeTurnId = null;
732
750
  session.cancelRequested = false;
733
751
  }
734
752
  }