@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.
- package/README.md +55 -163
- package/docs/CONTEXT.md +20 -36
- package/docs/README.md +6 -12
- package/docs/adr/0003-independent-bun-installation.md +5 -19
- package/docs/adr/0010-practical-knowledge-assistance.md +28 -81
- package/docs/adr/0011-shared-knowledge-and-selective-history.md +16 -43
- package/docs/guidelines/engineering.md +74 -0
- package/docs/procedures/self-hosted-runner.md +7 -0
- package/package.json +32 -11
- package/skills/hivex/SKILL.md +28 -92
- package/skills/hivex/references/markdown.md +12 -42
- package/src/cli/diagnostic.ts +21 -11
- package/src/cli.ts +46 -36
- package/src/documents.ts +502 -320
- package/src/errors.ts +8 -6
- package/src/implementation.ts +185 -87
- package/src/ingestion-units.ts +107 -64
- package/src/knowledge-maintenance.ts +35 -22
- package/src/knowledge-model.ts +386 -268
- package/src/knowledge-serialization.ts +239 -0
- package/src/knowledge-snapshot.ts +100 -77
- package/src/knowledge-store.ts +634 -453
- package/src/knowledge.ts +1001 -758
- package/src/markdown.ts +107 -45
- package/src/model/connection.ts +134 -76
- package/src/model/failure.ts +46 -23
- package/src/model/invoke.ts +346 -166
- package/src/model/profile.ts +201 -103
- package/src/model/rpc-error.ts +21 -0
- package/src/model/server.ts +151 -82
- package/src/model/thread.ts +24 -14
- package/src/model/transcript.ts +87 -46
- package/src/ordering.ts +9 -0
- package/src/retrieval/lexical.ts +64 -41
- package/src/review.ts +83 -55
- package/src/runtime.d.ts +4 -0
- package/src/snapshot-command.ts +82 -43
- package/src/source-relocation.ts +222 -0
- package/docs/engineering.md +0 -174
package/src/model/server.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
26
|
-
if (process.platform !== 'linux')
|
|
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: '
|
|
29
|
-
|
|
32
|
+
encoding: 'utf-8',
|
|
33
|
+
env: Object.fromEntries([...Object.entries(nativeEnvironment()), ['LC_ALL', 'C']]),
|
|
30
34
|
maxBuffer: 1_048_576,
|
|
31
|
-
|
|
35
|
+
timeout: 1000,
|
|
32
36
|
});
|
|
33
|
-
if (result.status !== 0)
|
|
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
|
|
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 (
|
|
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(
|
|
42
|
-
|
|
51
|
+
return members.every(
|
|
52
|
+
([, state]) => state?.startsWith('Z') === true || state?.startsWith('X') === true
|
|
53
|
+
);
|
|
54
|
+
};
|
|
43
55
|
|
|
44
|
-
async
|
|
56
|
+
const areGroupMembersTerminated = async (pid: number) => {
|
|
45
57
|
const deadline = performance.now() + 2000;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
79
|
+
const isSettledWithin = async (exit: Promise<unknown>, milliseconds: number) => {
|
|
54
80
|
const expired = Promise.withResolvers<boolean>();
|
|
55
|
-
const timer = setTimeout(() =>
|
|
81
|
+
const timer = setTimeout(() => {
|
|
82
|
+
expired.resolve(false);
|
|
83
|
+
}, milliseconds);
|
|
56
84
|
try {
|
|
57
|
-
return await Promise.race([exit
|
|
85
|
+
return await Promise.race([isExitComplete(exit), expired.promise]);
|
|
58
86
|
} finally {
|
|
59
87
|
clearTimeout(timer);
|
|
60
88
|
}
|
|
61
|
-
}
|
|
89
|
+
};
|
|
62
90
|
|
|
63
|
-
|
|
91
|
+
interface ServerOptions {
|
|
64
92
|
binary: string;
|
|
65
93
|
workspace: string;
|
|
66
|
-
notification: (method: string,
|
|
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
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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({
|
|
191
|
+
throw new ServerAdmissionFailure({
|
|
192
|
+
cause: error,
|
|
193
|
+
cleanup: 'failed',
|
|
194
|
+
processId: pid,
|
|
195
|
+
});
|
|
136
196
|
}
|
|
137
|
-
throw new ServerAdmissionFailure({
|
|
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
|
|
218
|
+
export const startServer = async (options: ServerOptions) => {
|
|
142
219
|
const initial = await launchServer(options, []);
|
|
143
|
-
if (
|
|
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 (
|
|
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
|
+
};
|
package/src/model/thread.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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',
|
|
55
|
+
await rpc.request('mcpServerStatus/list', threadStatusParameters, {
|
|
56
|
+
signal,
|
|
57
|
+
})
|
|
48
58
|
);
|
|
49
59
|
return threadId;
|
|
50
|
-
}
|
|
60
|
+
};
|
package/src/model/transcript.ts
CHANGED
|
@@ -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({
|
|
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({
|
|
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
|
-
|
|
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({
|
|
26
|
+
const usageEvent = identity.extend({
|
|
27
|
+
tokenUsage: z.looseObject({ total: usageSchema }),
|
|
28
|
+
});
|
|
20
29
|
export type Usage = z.infer<typeof usageSchema>;
|
|
21
30
|
|
|
22
|
-
|
|
23
|
-
if (current.totalTokens !== current.inputTokens + current.outputTokens)
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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,
|
|
121
|
+
const notification = (method: string, parameters: unknown) => {
|
|
82
122
|
try {
|
|
83
|
-
receive(method,
|
|
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
|
-
|
|
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
|
+
};
|
package/src/ordering.ts
ADDED
|
@@ -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
|
+
};
|