@h1v35/hivex 0.2.0 → 0.2.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 (39) hide show
  1. package/README.md +55 -163
  2. package/docs/CONTEXT.md +20 -36
  3. package/docs/README.md +6 -12
  4. package/docs/adr/0003-independent-bun-installation.md +5 -19
  5. package/docs/adr/0010-practical-knowledge-assistance.md +28 -81
  6. package/docs/adr/0011-shared-knowledge-and-selective-history.md +16 -43
  7. package/docs/guidelines/engineering.md +74 -0
  8. package/docs/procedures/self-hosted-runner.md +7 -0
  9. package/package.json +32 -11
  10. package/skills/hivex/SKILL.md +28 -92
  11. package/skills/hivex/references/markdown.md +12 -42
  12. package/src/cli/diagnostic.ts +21 -11
  13. package/src/cli.ts +46 -36
  14. package/src/documents.ts +502 -320
  15. package/src/errors.ts +8 -6
  16. package/src/implementation.ts +185 -87
  17. package/src/ingestion-units.ts +107 -64
  18. package/src/knowledge-maintenance.ts +35 -22
  19. package/src/knowledge-model.ts +386 -268
  20. package/src/knowledge-serialization.ts +239 -0
  21. package/src/knowledge-snapshot.ts +100 -77
  22. package/src/knowledge-store.ts +634 -453
  23. package/src/knowledge.ts +1001 -758
  24. package/src/markdown.ts +107 -45
  25. package/src/model/connection.ts +134 -76
  26. package/src/model/failure.ts +46 -23
  27. package/src/model/invoke.ts +346 -166
  28. package/src/model/profile.ts +201 -103
  29. package/src/model/rpc-error.ts +21 -0
  30. package/src/model/server.ts +151 -82
  31. package/src/model/thread.ts +24 -14
  32. package/src/model/transcript.ts +87 -46
  33. package/src/ordering.ts +9 -0
  34. package/src/retrieval/lexical.ts +64 -41
  35. package/src/review.ts +83 -55
  36. package/src/runtime.d.ts +4 -0
  37. package/src/snapshot-command.ts +82 -43
  38. package/src/source-relocation.ts +222 -0
  39. package/docs/engineering.md +0 -174
@@ -1,5 +1,4 @@
1
1
  import { spawn, spawnSync } from 'node:child_process';
2
- import { setTimeout as delay } from 'node:timers/promises';
3
2
  import { AppServerConnection } from './connection.ts';
4
3
  import { ServerAdmissionFailure } from './failure.ts';
5
4
  import {
@@ -10,165 +9,235 @@ import {
10
9
  requestedPolicyHash,
11
10
  } from './profile.ts';
12
11
 
13
- function signalGroup(pid: number, signal: NodeJS.Signals | 0) {
12
+ const signalGroup = (pid: number, signal: Parameters<typeof process.kill>[1]) => {
14
13
  try {
15
14
  process.kill(-pid, signal);
16
15
  return true;
17
16
  } catch (error) {
18
- if (error instanceof Error && 'code' in error && error.code === 'ESRCH') return false;
19
- if (signal === 0 && error instanceof Error && 'code' in error && error.code === 'EPERM')
17
+ if (Error.isError(error) && 'code' in error && error.code === 'ESRCH') {
18
+ return false;
19
+ }
20
+ if (signal === 0 && Error.isError(error) && 'code' in error && error.code === 'EPERM') {
20
21
  return true;
22
+ }
21
23
  throw error;
22
24
  }
23
- }
25
+ };
24
26
 
25
- function onlyTerminatedMembersRemain(pid: number) {
26
- if (process.platform !== 'linux') return false;
27
+ const areOnlyTerminatedMembersRemaining = (pid: number) => {
28
+ if (process.platform !== 'linux') {
29
+ return false;
30
+ }
27
31
  const result = spawnSync('/bin/ps', ['-e', '-o', 'pgid=,stat='], {
28
- encoding: 'utf8',
29
- timeout: 1000,
32
+ encoding: 'utf-8',
33
+ env: Object.fromEntries([...Object.entries(nativeEnvironment()), ['LC_ALL', 'C']]),
30
34
  maxBuffer: 1_048_576,
31
- env: { ...nativeEnvironment(), LC_ALL: 'C' },
35
+ timeout: 1000,
32
36
  });
33
- if (result.status !== 0) return false;
37
+ if (result.status !== 0) {
38
+ return false;
39
+ }
34
40
  const rows = result.stdout.trim().split('\n');
35
- if (rows.some((row) => !/^\s*\d+\s+\S+\s*$/.test(row))) return false;
41
+ if (rows.some((row) => !/^\s*\d+\s+\S+\s*$/u.test(row))) {
42
+ return false;
43
+ }
36
44
  const members = rows
37
- .map((row) => row.trim().split(/\s+/))
45
+ .map((row) => row.trim().split(/\s+/u))
38
46
  .filter(([group]) => Number(group) === pid);
39
- if (!members.length) return !signalGroup(pid, 0);
47
+ if (members.length === 0) {
48
+ return !signalGroup(pid, 0);
49
+ }
40
50
  // Container init may retain orphan zombies; they cannot execute or receive signals.
41
- return members.every(([, state]) => state?.startsWith('Z') || state?.startsWith('X'));
42
- }
51
+ return members.every(
52
+ ([, state]) => state?.startsWith('Z') === true || state?.startsWith('X') === true
53
+ );
54
+ };
43
55
 
44
- async function groupTerminated(pid: number) {
56
+ const areGroupMembersTerminated = async (pid: number) => {
45
57
  const deadline = performance.now() + 2000;
46
- while (signalGroup(pid, 0)) {
47
- if (performance.now() >= deadline) return onlyTerminatedMembersRemain(pid);
48
- await delay(25);
49
- }
58
+ const result = Promise.withResolvers<boolean>();
59
+ const check = (): void => {
60
+ if (!signalGroup(pid, 0)) {
61
+ result.resolve(true);
62
+ return;
63
+ }
64
+ if (performance.now() >= deadline) {
65
+ result.resolve(areOnlyTerminatedMembersRemaining(pid));
66
+ return;
67
+ }
68
+ setTimeout(check, 25);
69
+ };
70
+ check();
71
+ return await result.promise;
72
+ };
73
+
74
+ const isExitComplete = async (exit: Promise<unknown>) => {
75
+ await exit;
50
76
  return true;
51
- }
77
+ };
52
78
 
53
- async function settledWithin(exit: Promise<unknown>, milliseconds: number) {
79
+ const isSettledWithin = async (exit: Promise<unknown>, milliseconds: number) => {
54
80
  const expired = Promise.withResolvers<boolean>();
55
- const timer = setTimeout(() => expired.resolve(false), milliseconds);
81
+ const timer = setTimeout(() => {
82
+ expired.resolve(false);
83
+ }, milliseconds);
56
84
  try {
57
- return await Promise.race([exit.then(() => true), expired.promise]);
85
+ return await Promise.race([isExitComplete(exit), expired.promise]);
58
86
  } finally {
59
87
  clearTimeout(timer);
60
88
  }
61
- }
89
+ };
62
90
 
63
- type ServerOptions = {
91
+ interface ServerOptions {
64
92
  binary: string;
65
93
  workspace: string;
66
- notification: (method: string, params: unknown) => void;
94
+ notification: (method: string, parameters: unknown) => void;
67
95
  interaction: (method: string) => void;
68
96
  signal: AbortSignal;
97
+ }
98
+
99
+ const captureFailure = async <Value>(promise: Promise<Value>) => {
100
+ try {
101
+ return { value: await promise };
102
+ } catch (error) {
103
+ return { error };
104
+ }
105
+ };
106
+
107
+ const terminateProcessGroup = async (pid: number, exit: Promise<unknown>) => {
108
+ await isSettledWithin(exit, 1000);
109
+ const isTerminated = signalGroup(pid, 'SIGTERM');
110
+ if (isTerminated) {
111
+ const areMembersTerminated = await areGroupMembersTerminated(pid);
112
+ if (!areMembersTerminated) {
113
+ signalGroup(pid, 'SIGKILL');
114
+ if (!(await areGroupMembersTerminated(pid))) {
115
+ throw new Error('Owned Codex process group survived cleanup');
116
+ }
117
+ }
118
+ }
119
+ if (!(await isSettledWithin(exit, 1000))) {
120
+ throw new Error('Native Codex process was not reaped');
121
+ }
69
122
  };
70
123
 
71
- async function launchServer(options: ServerOptions, disabledServers: string[]) {
124
+ const launchServer = async (options: ServerOptions, disabledServers: string[]) => {
72
125
  const version = spawnSync(options.binary, ['--version'], {
126
+ encoding: 'utf-8',
73
127
  env: nativeEnvironment(),
74
- encoding: 'utf8',
75
- timeout: 10_000,
76
128
  maxBuffer: 65_536,
129
+ timeout: 10_000,
77
130
  });
78
- if (version.status !== 0 || version.stdout.trim() !== nativeVersion)
131
+ if (version.status !== 0 || version.stdout.trim() !== nativeVersion) {
79
132
  throw new Error('Knowledge execution requires verified codex-cli 0.153.2');
133
+ }
80
134
  const child = spawn(options.binary, launchArguments(disabledServers), {
81
- env: nativeEnvironment(),
82
135
  cwd: options.workspace,
83
- stdio: 'pipe',
84
136
  detached: true,
137
+ env: nativeEnvironment(),
138
+ stdio: 'pipe',
139
+ });
140
+ const exit = Promise.withResolvers<boolean>();
141
+ child.once('exit', () => {
142
+ exit.resolve(true);
143
+ });
144
+ child.once('error', () => {
145
+ exit.resolve(true);
85
146
  });
86
- const exit = Promise.withResolvers<void>();
87
- child.once('exit', () => exit.resolve());
88
- child.once('error', () => exit.resolve());
89
147
  child.stderr.resume();
90
- if (child.pid === undefined) throw new Error('Native Codex could not start');
91
- const pid = child.pid;
148
+ const { pid } = child;
149
+ if (pid === undefined) {
150
+ throw new Error('Native Codex could not start');
151
+ }
92
152
  const rpc = new AppServerConnection({
93
153
  input: child.stdin,
94
- output: child.stdout,
95
- onNotification: options.notification,
96
154
  onInteractiveRequest: options.interaction,
155
+ onNotification: options.notification,
156
+ output: child.stdout,
97
157
  });
98
158
  const stop = async () => {
99
159
  child.stdin.end();
100
- try {
101
- await settledWithin(exit.promise, 1000);
102
- if (signalGroup(pid, 'SIGTERM') && !(await groupTerminated(pid))) {
103
- signalGroup(pid, 'SIGKILL');
104
- if (!(await groupTerminated(pid)))
105
- throw new Error('Owned Codex process group survived cleanup');
106
- }
107
- if (!(await settledWithin(exit.promise, 1000)))
108
- throw new Error('Native Codex process was not reaped');
109
- } finally {
110
- rpc.dispose();
111
- child.stdin.destroy();
112
- child.stdout.destroy();
113
- child.stderr.destroy();
160
+ const result = await captureFailure(terminateProcessGroup(pid, exit.promise));
161
+ rpc.dispose();
162
+ child.stdin.destroy();
163
+ child.stdout.destroy();
164
+ child.stderr.destroy();
165
+ if ('error' in result) {
166
+ throw result.error;
114
167
  }
115
168
  };
116
169
  try {
117
170
  await rpc.request(
118
171
  'initialize',
119
172
  { clientInfo: { name: 'hivex', version: '0.1.0' } },
120
- { signal: options.signal },
173
+ { signal: options.signal }
121
174
  );
122
175
  rpc.notify('initialized');
123
176
  const profile = await admitProfile({ rpc, signal: options.signal });
124
177
  return {
178
+ activeServers: profile.activeServers,
179
+ admission: {
180
+ ...profile.evidence,
181
+ launchPolicyHash: requestedPolicyHash(disabledServers),
182
+ },
183
+ pid,
125
184
  rpc,
126
185
  stop,
127
- pid,
128
- activeServers: profile.activeServers,
129
- admission: { ...profile.evidence, launchPolicyHash: requestedPolicyHash(disabledServers) },
130
186
  };
131
187
  } catch (error) {
132
188
  try {
133
189
  await stop();
134
190
  } catch {
135
- throw new ServerAdmissionFailure({ cause: error, cleanup: 'failed', processId: pid });
191
+ throw new ServerAdmissionFailure({
192
+ cause: error,
193
+ cleanup: 'failed',
194
+ processId: pid,
195
+ });
136
196
  }
137
- throw new ServerAdmissionFailure({ cause: error, cleanup: 'confirmed', processId: pid });
197
+ throw new ServerAdmissionFailure({
198
+ cause: error,
199
+ cleanup: 'confirmed',
200
+ processId: pid,
201
+ });
138
202
  }
139
- }
203
+ };
204
+
205
+ const stopForAdmission = async (server: Awaited<ReturnType<typeof launchServer>>) => {
206
+ try {
207
+ await server.stop();
208
+ } catch (error) {
209
+ throw new ServerAdmissionFailure({
210
+ admission: server.admission,
211
+ cause: error,
212
+ cleanup: 'failed',
213
+ processId: server.pid,
214
+ });
215
+ }
216
+ };
140
217
 
141
- export async function startServer(options: ServerOptions) {
218
+ export const startServer = async (options: ServerOptions) => {
142
219
  const initial = await launchServer(options, []);
143
- if (!initial.activeServers.length) return initial;
220
+ if (initial.activeServers.length === 0) {
221
+ return initial;
222
+ }
144
223
  await stopForAdmission(initial);
145
- if (options.signal.aborted)
224
+ if (options.signal.aborted) {
146
225
  throw new ServerAdmissionFailure({
226
+ admission: initial.admission,
147
227
  cause: options.signal.reason,
148
228
  cleanup: 'confirmed',
149
229
  processId: initial.pid,
150
- admission: initial.admission,
151
230
  });
231
+ }
152
232
  const isolated = await launchServer(options, initial.activeServers);
153
- if (!isolated.activeServers.length) return isolated;
233
+ if (isolated.activeServers.length === 0) {
234
+ return isolated;
235
+ }
154
236
  await stopForAdmission(isolated);
155
237
  throw new ServerAdmissionFailure({
238
+ admission: isolated.admission,
156
239
  cause: new Error('MCP configuration changed or did not honor process-local overrides'),
157
240
  cleanup: 'confirmed',
158
241
  processId: isolated.pid,
159
- admission: isolated.admission,
160
242
  });
161
- }
162
-
163
- async function stopForAdmission(server: Awaited<ReturnType<typeof launchServer>>) {
164
- try {
165
- await server.stop();
166
- } catch (error) {
167
- throw new ServerAdmissionFailure({
168
- cause: error,
169
- cleanup: 'failed',
170
- processId: server.pid,
171
- admission: server.admission,
172
- });
173
- }
174
- }
243
+ };
@@ -1,35 +1,35 @@
1
1
  import { z } from 'zod';
2
- import { AppServerConnection } from './connection.ts';
3
2
  import { knowledgeModel, knowledgeThread } from './profile.ts';
3
+ import type { AppServerConnection } from './connection.ts';
4
4
 
5
5
  const startedThread = z.looseObject({
6
- thread: z.looseObject({ id: z.string(), ephemeral: z.literal(true) }),
6
+ cwd: z.string(),
7
+ instructionSources: z.array(z.string()),
7
8
  model: z.literal(knowledgeModel.name),
8
9
  modelProvider: z.literal('openai'),
9
10
  reasoningEffort: z.literal('max'),
10
- cwd: z.string(),
11
11
  sandbox: z.looseObject({ type: z.literal('readOnly') }),
12
+ thread: z.looseObject({ ephemeral: z.literal(true), id: z.string() }),
12
13
  // Discovery paths can be reported even with project_doc_max_bytes=0, verified at admission.
13
- instructionSources: z.array(z.string()),
14
14
  });
15
15
  const mcpInventory = z.looseObject({
16
16
  data: z.array(
17
17
  z.looseObject({
18
+ resourceTemplates: z.array(z.unknown()).length(0),
19
+ resources: z.array(z.unknown()).length(0),
18
20
  runtimeStatus: z.literal('disabled'),
19
21
  tools: z.record(z.string(), z.unknown()).refine((value) => !Object.keys(value).length),
20
- resources: z.array(z.unknown()).length(0),
21
- resourceTemplates: z.array(z.unknown()).length(0),
22
- }),
22
+ })
23
23
  ),
24
24
  nextCursor: z.null().optional(),
25
25
  });
26
26
  const threadStartTimeoutMilliseconds = 90_000;
27
27
 
28
- export async function startKnowledgeThread(options: {
28
+ export const startKnowledgeThread = async (options: {
29
29
  rpc: AppServerConnection;
30
30
  workspace: string;
31
31
  signal: AbortSignal;
32
- }) {
32
+ }) => {
33
33
  const { rpc, workspace, signal } = options;
34
34
  const started = startedThread.parse(
35
35
  await rpc.request(
@@ -38,13 +38,23 @@ export async function startKnowledgeThread(options: {
38
38
  ...knowledgeThread,
39
39
  cwd: workspace,
40
40
  },
41
- { signal, timeoutMilliseconds: threadStartTimeoutMilliseconds },
42
- ),
41
+ { signal, timeoutMilliseconds: threadStartTimeoutMilliseconds }
42
+ )
43
43
  );
44
- if (started.cwd !== workspace) throw new Error('Native Codex workspace changed');
44
+ if (started.cwd !== workspace) {
45
+ throw new Error('Native Codex workspace changed');
46
+ }
45
47
  const threadId = started.thread.id;
48
+ const threadStatusThreadId = { threadId };
49
+ const threadStatusLimit = { limit: 100 };
50
+ const threadStatusParameters = {
51
+ ...threadStatusThreadId,
52
+ ...threadStatusLimit,
53
+ };
46
54
  mcpInventory.parse(
47
- await rpc.request('mcpServerStatus/list', { threadId, limit: 100 }, { signal }),
55
+ await rpc.request('mcpServerStatus/list', threadStatusParameters, {
56
+ signal,
57
+ })
48
58
  );
49
59
  return threadId;
50
- }
60
+ };
@@ -3,43 +3,66 @@ import { z } from 'zod';
3
3
  const identity = z.looseObject({ threadId: z.string(), turnId: z.string() });
4
4
  const terminal = z.looseObject({
5
5
  threadId: z.string(),
6
- turn: z.looseObject({ id: z.string(), status: z.enum(['completed', 'failed', 'interrupted']) }),
6
+ turn: z.looseObject({
7
+ id: z.string(),
8
+ status: z.enum(['completed', 'failed', 'interrupted']),
9
+ }),
7
10
  });
8
11
  const itemEvent = identity.extend({
9
- item: z.looseObject({ id: z.string(), type: z.string(), text: z.string().optional() }),
12
+ item: z.looseObject({
13
+ id: z.string(),
14
+ text: z.string().optional(),
15
+ type: z.string(),
16
+ }),
10
17
  });
11
18
  export const usageSchema = z.object({
12
- inputTokens: z.number().int().nonnegative(),
19
+ cacheWriteInputTokens: z.number().int().nonnegative().optional(),
13
20
  cachedInputTokens: z.number().int().nonnegative(),
21
+ inputTokens: z.number().int().nonnegative(),
14
22
  outputTokens: z.number().int().nonnegative(),
15
23
  reasoningOutputTokens: z.number().int().nonnegative(),
16
24
  totalTokens: z.number().int().nonnegative(),
17
- cacheWriteInputTokens: z.number().int().nonnegative().optional(),
18
25
  });
19
- const usageEvent = identity.extend({ tokenUsage: z.looseObject({ total: usageSchema }) });
26
+ const usageEvent = identity.extend({
27
+ tokenUsage: z.looseObject({ total: usageSchema }),
28
+ });
20
29
  export type Usage = z.infer<typeof usageSchema>;
21
30
 
22
- function consistentUsage(current: Usage, previous: Usage | undefined) {
23
- if (current.totalTokens !== current.inputTokens + current.outputTokens) return false;
31
+ const consistentUsage = (current: Usage, previous: Usage | undefined) => {
32
+ if (current.totalTokens !== current.inputTokens + current.outputTokens) {
33
+ return false;
34
+ }
24
35
  if (
25
36
  current.cachedInputTokens > current.inputTokens ||
26
37
  current.reasoningOutputTokens > current.outputTokens
27
- )
38
+ ) {
28
39
  return false;
40
+ }
29
41
  if (
30
42
  current.cacheWriteInputTokens !== undefined &&
31
43
  current.cacheWriteInputTokens > current.inputTokens
32
- )
44
+ ) {
33
45
  return false;
34
- if (!previous) return true;
46
+ }
47
+ if (previous === undefined) {
48
+ return true;
49
+ }
35
50
  return (
36
51
  current.inputTokens >= previous.inputTokens &&
37
52
  current.outputTokens >= previous.outputTokens &&
38
53
  current.totalTokens >= previous.totalTokens
39
54
  );
40
- }
55
+ };
41
56
 
42
- export function captureTranscript() {
57
+ const observeRejection = async (promise: Promise<unknown>): Promise<void> => {
58
+ try {
59
+ await promise;
60
+ } catch {
61
+ // The rejection is handled by the caller through the public promise.
62
+ }
63
+ };
64
+
65
+ export const captureTranscript = () => {
43
66
  const done = Promise.withResolvers<z.infer<typeof terminal>>();
44
67
  const items: z.infer<typeof itemEvent>[] = [];
45
68
  const state: {
@@ -51,36 +74,53 @@ export function captureTranscript() {
51
74
  const identities = new Set<string>();
52
75
  const trackIdentity = (value: { threadId: string; turnId: string }) => {
53
76
  identities.add(JSON.stringify([value.threadId, value.turnId]));
54
- if (identities.size > 1) throw new Error('Native evidence mixed thread or turn identities');
55
- };
56
- void done.promise.catch(() => undefined);
57
- const receive = (method: string, params: unknown) => {
58
- if (method === 'turn/completed') {
59
- const event = terminal.parse(params);
60
- trackIdentity({ threadId: event.threadId, turnId: event.turn.id });
61
- if (state.terminalSeen) throw new Error('Duplicate native terminal event');
62
- state.terminalSeen = true;
63
- done.resolve(event);
77
+ if (identities.size > 1) {
78
+ throw new Error('Native evidence mixed thread or turn identities');
64
79
  }
65
- if (method === 'thread/tokenUsage/updated') {
66
- const event = usageEvent.parse(params);
67
- trackIdentity(event);
68
- if (!consistentUsage(event.tokenUsage.total, state.counter?.tokenUsage.total)) {
69
- state.usageInvalid = true;
70
- throw new Error('Native usage is inconsistent or regressed');
80
+ };
81
+ void observeRejection(done.promise);
82
+ const receive = (method: string, parameters: unknown) => {
83
+ switch (method) {
84
+ case 'turn/completed': {
85
+ const event = terminal.parse(parameters);
86
+ trackIdentity({ threadId: event.threadId, turnId: event.turn.id });
87
+ if (state.terminalSeen) {
88
+ throw new Error('Duplicate native terminal event');
89
+ }
90
+ state.terminalSeen = true;
91
+ done.resolve(event);
92
+ break;
93
+ }
94
+ case 'thread/tokenUsage/updated': {
95
+ const event = usageEvent.parse(parameters);
96
+ trackIdentity(event);
97
+ if (!consistentUsage(event.tokenUsage.total, state.counter?.tokenUsage.total)) {
98
+ state.usageInvalid = true;
99
+ throw new Error('Native usage is inconsistent or regressed');
100
+ }
101
+ state.counter = event;
102
+ break;
103
+ }
104
+ case 'item/completed':
105
+ case 'item/started': {
106
+ const entry = itemEvent.parse(parameters);
107
+ trackIdentity(entry);
108
+ if (!['userMessage', 'reasoning', 'agentMessage'].includes(entry.item.type)) {
109
+ throw new Error('Unexpected knowledge-model tool activity');
110
+ }
111
+ if (method === 'item/completed' && entry.item.type === 'agentMessage') {
112
+ items.push(entry);
113
+ }
114
+ break;
115
+ }
116
+ default: {
117
+ break;
71
118
  }
72
- state.counter = event;
73
119
  }
74
- if (method !== 'item/started' && method !== 'item/completed') return;
75
- const entry = itemEvent.parse(params);
76
- trackIdentity(entry);
77
- if (!['userMessage', 'reasoning', 'agentMessage'].includes(entry.item.type))
78
- throw new Error('Unexpected knowledge-model tool activity');
79
- if (method === 'item/completed' && entry.item.type === 'agentMessage') items.push(entry);
80
120
  };
81
- const notification = (method: string, params: unknown) => {
121
+ const notification = (method: string, parameters: unknown) => {
82
122
  try {
83
- receive(method, params);
123
+ receive(method, parameters);
84
124
  } catch (error) {
85
125
  state.invalid = true;
86
126
  throw error;
@@ -90,25 +130,26 @@ export function captureTranscript() {
90
130
  const current = state.counter;
91
131
  if (
92
132
  state.usageInvalid ||
93
- !current ||
94
- current.threadId !== expected.threadId ||
133
+ current?.threadId !== expected.threadId ||
95
134
  current.turnId !== expected.turnId
96
- )
135
+ ) {
97
136
  return null;
137
+ }
98
138
  return current.tokenUsage.total;
99
139
  };
100
140
  const assertValid = (expected: { threadId: string; turnId: string }) => {
101
- if (state.invalid || !identities.has(JSON.stringify([expected.threadId, expected.turnId])))
141
+ if (state.invalid || !identities.has(JSON.stringify([expected.threadId, expected.turnId]))) {
102
142
  throw new Error('Native evidence did not retain one consistent invocation identity');
143
+ }
103
144
  };
104
145
  return {
105
- done,
106
- items,
107
- notification,
108
- measured,
109
146
  assertValid,
147
+ done,
110
148
  get invalid() {
111
149
  return state.invalid;
112
150
  },
151
+ items,
152
+ measured,
153
+ notification,
113
154
  };
114
- }
155
+ };
@@ -0,0 +1,9 @@
1
+ // Existing snapshots and work keys use JavaScript's UTF-16 string order.
2
+ export const compareSerializedStrings = function compareSerializedStrings(
3
+ left: string,
4
+ right: string
5
+ ) {
6
+ const leftUnits = Buffer.from(left, 'utf-16le').swap16();
7
+ const rightUnits = Buffer.from(right, 'utf-16le').swap16();
8
+ return leftUnits.compare(rightUnits);
9
+ };