@nowcrew/daemon 0.6.102 → 0.6.103
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/dist/codex-home-launch.js +7 -0
- package/dist/codex-home-mode-controller.js +68 -0
- package/dist/daemon-migration-controller.js +210 -0
- package/dist/daemon-migration-wiring.js +22 -0
- package/dist/execution-runner.js +5 -2
- package/dist/local-executor.js +8 -1
- package/dist/project-skills/capability.js +109 -0
- package/dist/project-skills/controller-convergence.js +57 -0
- package/dist/project-skills/projection-set-switch.js +419 -0
- package/dist/project-skills/projection-state-domain.js +170 -0
- package/dist/project-skills/projection-state-store.js +842 -0
- package/dist/project-skills/projection-state-transaction.js +322 -0
- package/dist/project-skills/projection-state.js +3 -0
- package/dist/project-skills/runtime-root-bootstrap.js +47 -0
- package/dist/project-skills/runtime-root-domain.js +268 -0
- package/dist/project-skills/runtime-root-gc.js +293 -0
- package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
- package/dist/project-skills/runtime-root-leases.js +487 -0
- package/dist/project-skills/runtime-root-source-identity.js +60 -0
- package/dist/project-skills/runtime-root-startup.js +49 -0
- package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
- package/dist/project-skills/runtime-root-state-index.js +356 -0
- package/dist/project-skills/runtime-root-store.js +722 -0
- package/dist/project-skills/serve-capability.js +28 -0
- package/dist/project-skills/serve-startup.js +37 -0
- package/dist/runtime-observation.js +219 -0
- package/dist/runtimes/claude-observation-runner.js +55 -0
- package/dist/runtimes/claude-thread-runner.js +17 -3
- package/dist/runtimes/claude.js +5 -3
- package/dist/runtimes/codex-app-server-runner.js +16 -2
- package/dist/runtimes/codex-home-migration-cli.js +26 -0
- package/dist/runtimes/codex-home-migration.js +112 -0
- package/dist/runtimes/codex-home.js +236 -0
- package/dist/runtimes/deepseek-harness-runner.js +16 -0
- package/dist/runtimes/kimi-acp-runner.js +19 -0
- package/dist/runtimes/observation-context.js +18 -0
- package/dist/runtimes/observation-mapper.js +314 -0
- package/dist/runtimes/opencode-runner.js +18 -4
- package/dist/runtimes/pi-runner.js +17 -1
- package/dist/storage-control.js +212 -0
- package/dist/storage-copy.js +628 -0
- package/dist/storage-credential-guard.js +21 -0
- package/dist/storage-doctor.js +228 -0
- package/dist/storage-manifest.js +1027 -0
- package/dist/storage-migration-controller.js +441 -0
- package/dist/storage-migration-manifest.js +105 -0
- package/dist/storage-migration-policy.js +100 -0
- package/dist/storage-runtime-controller.js +361 -0
- package/dist/storage-runtime-readiness.js +206 -0
- package/dist/storage-safe-fs.js +302 -0
- package/dist/storage-status-delivery.js +224 -0
- package/dist/storage-status-replay.js +140 -0
- package/dist/storage-target-confirmation.js +129 -0
- package/dist/storage-worker-command.js +37 -0
- package/dist/storage-worker-executor.js +431 -0
- package/dist/storage-worker-handoff.js +151 -0
- package/dist/storage-worker-operation.js +288 -0
- package/dist/storage-worker-status.js +259 -0
- package/dist/token.js +4 -1
- package/package.json +1 -1
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { chmod, copyFile, lstat, mkdir, open, readdir, readlink, rename, rm, symlink, unlink, link, writeFile, } from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
|
+
const PRIVATE_CODEX_HOME = ".codex";
|
|
5
|
+
export function defaultCodexHome(homeDir) {
|
|
6
|
+
return resolve(homeDir, PRIVATE_CODEX_HOME);
|
|
7
|
+
}
|
|
8
|
+
async function linkIfMissing(source, target) {
|
|
9
|
+
try {
|
|
10
|
+
await lstat(target);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
if (error.code !== "ENOENT")
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
try {
|
|
18
|
+
await lstat(source);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error.code === "ENOENT")
|
|
22
|
+
return;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
await symlink(source, target, "file");
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code !== "EEXIST")
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function isBelow(root, candidate) {
|
|
34
|
+
const child = relative(root, candidate);
|
|
35
|
+
return child !== "" && child !== ".." && !child.startsWith(`..${sep}`) && !child.startsWith(sep);
|
|
36
|
+
}
|
|
37
|
+
async function isExactSymlink(target, expectedSource) {
|
|
38
|
+
const linkTarget = await readlink(target);
|
|
39
|
+
return resolve(dirname(target), linkTarget) === resolve(expectedSource);
|
|
40
|
+
}
|
|
41
|
+
async function ensurePrivateSessions(target, legacySource) {
|
|
42
|
+
try {
|
|
43
|
+
const current = await lstat(target);
|
|
44
|
+
if (current.isSymbolicLink()) {
|
|
45
|
+
if (legacySource === undefined || !(await isExactSymlink(target, legacySource))) {
|
|
46
|
+
throw new Error(`Refusing to replace unexpected Codex sessions symlink: ${target}`);
|
|
47
|
+
}
|
|
48
|
+
const replacement = `${target}.private-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
49
|
+
const legacyBackup = `${target}.legacy-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
50
|
+
try {
|
|
51
|
+
await mkdir(replacement, { mode: 0o700 });
|
|
52
|
+
await rename(target, legacyBackup);
|
|
53
|
+
try {
|
|
54
|
+
await rename(replacement, target);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
await rename(legacyBackup, target).catch(() => undefined);
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
await rm(replacement, { recursive: true, force: true }).catch(() => undefined);
|
|
63
|
+
await rm(legacyBackup, { recursive: true, force: true }).catch(() => undefined);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (!current.isDirectory()) {
|
|
67
|
+
throw new Error(`Codex sessions target is not a directory: ${target}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error.code !== "ENOENT")
|
|
72
|
+
throw error;
|
|
73
|
+
await mkdir(target, { mode: 0o700 });
|
|
74
|
+
}
|
|
75
|
+
await chmod(target, 0o700);
|
|
76
|
+
}
|
|
77
|
+
async function atomicCopyFile(source, target, mode) {
|
|
78
|
+
const parent = dirname(target);
|
|
79
|
+
await mkdir(parent, { recursive: true, mode: 0o700 });
|
|
80
|
+
const temporary = `${target}.tmp-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
81
|
+
try {
|
|
82
|
+
await copyFile(source, temporary, constants.COPYFILE_EXCL);
|
|
83
|
+
await chmod(temporary, mode);
|
|
84
|
+
try {
|
|
85
|
+
await link(temporary, target);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error.code !== "EEXIST")
|
|
90
|
+
throw error;
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
await unlink(temporary).catch(() => undefined);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async function ensurePrivateIndex(target, source) {
|
|
99
|
+
let current;
|
|
100
|
+
try {
|
|
101
|
+
current = await lstat(target);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
if (error.code !== "ENOENT")
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
if (current?.isSymbolicLink()) {
|
|
108
|
+
if (source === undefined || !(await isExactSymlink(target, source))) {
|
|
109
|
+
throw new Error(`Refusing to replace unexpected Codex session index symlink: ${target}`);
|
|
110
|
+
}
|
|
111
|
+
const replacement = `${target}.private-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
112
|
+
const legacyBackup = `${target}.legacy-${process.pid}-${Math.random().toString(36).slice(2)}`;
|
|
113
|
+
try {
|
|
114
|
+
try {
|
|
115
|
+
await lstat(source);
|
|
116
|
+
await copyFile(source, replacement, constants.COPYFILE_EXCL);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error.code !== "ENOENT")
|
|
120
|
+
throw error;
|
|
121
|
+
await writeFile(replacement, "", { encoding: "utf8", flag: "wx" });
|
|
122
|
+
}
|
|
123
|
+
await chmod(replacement, 0o600);
|
|
124
|
+
await rename(target, legacyBackup);
|
|
125
|
+
try {
|
|
126
|
+
await rename(replacement, target);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
await rename(legacyBackup, target).catch(() => undefined);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await rm(replacement, { force: true }).catch(() => undefined);
|
|
135
|
+
await rm(legacyBackup, { force: true }).catch(() => undefined);
|
|
136
|
+
}
|
|
137
|
+
current = undefined;
|
|
138
|
+
}
|
|
139
|
+
if (current !== undefined && !current.isFile()) {
|
|
140
|
+
throw new Error(`Codex session index target is not a regular file: ${target}`);
|
|
141
|
+
}
|
|
142
|
+
if (current !== undefined) {
|
|
143
|
+
await chmod(target, 0o600);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (source !== undefined) {
|
|
147
|
+
try {
|
|
148
|
+
await lstat(source);
|
|
149
|
+
if (await atomicCopyFile(source, target, 0o600))
|
|
150
|
+
return;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (error.code !== "ENOENT")
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const handle = await open(target, "wx", 0o600).catch((error) => {
|
|
159
|
+
if (error.code === "EEXIST")
|
|
160
|
+
return null;
|
|
161
|
+
throw error;
|
|
162
|
+
});
|
|
163
|
+
await handle?.close();
|
|
164
|
+
}
|
|
165
|
+
async function findRollout(root, sessionId) {
|
|
166
|
+
const entries = await readdir(root, { withFileTypes: true }).catch((error) => {
|
|
167
|
+
if (error.code === "ENOENT")
|
|
168
|
+
return [];
|
|
169
|
+
throw error;
|
|
170
|
+
});
|
|
171
|
+
const matches = [];
|
|
172
|
+
for (const entry of entries) {
|
|
173
|
+
const path = join(root, entry.name);
|
|
174
|
+
if (entry.isDirectory()) {
|
|
175
|
+
const nested = await findRollout(path, sessionId);
|
|
176
|
+
if (nested !== null)
|
|
177
|
+
matches.push(nested);
|
|
178
|
+
}
|
|
179
|
+
else if (entry.isFile() && entry.name.includes(sessionId)) {
|
|
180
|
+
matches.push(path);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (matches.length > 1)
|
|
184
|
+
throw new Error(`Multiple Codex rollout files match session ${sessionId}`);
|
|
185
|
+
return matches[0] ?? null;
|
|
186
|
+
}
|
|
187
|
+
async function hydrateResumeRollout(sourceSessions, privateSessions, sessionId) {
|
|
188
|
+
if (sessionId.includes("/") || sessionId.includes("\\") || sessionId.includes("..")) {
|
|
189
|
+
throw new Error("Invalid Codex resume session id");
|
|
190
|
+
}
|
|
191
|
+
const sourcePath = await findRollout(sourceSessions, sessionId);
|
|
192
|
+
if (sourcePath === null)
|
|
193
|
+
return;
|
|
194
|
+
const relativePath = relative(sourceSessions, sourcePath);
|
|
195
|
+
const destination = resolve(privateSessions, relativePath);
|
|
196
|
+
if (!isBelow(resolve(privateSessions), destination)) {
|
|
197
|
+
throw new Error("Codex resume rollout escaped the private sessions directory");
|
|
198
|
+
}
|
|
199
|
+
let existing;
|
|
200
|
+
try {
|
|
201
|
+
existing = await lstat(destination);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (error.code !== "ENOENT")
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
if (existing !== undefined) {
|
|
208
|
+
if (!existing.isFile() || existing.isSymbolicLink()) {
|
|
209
|
+
throw new Error(`Refusing to replace existing Codex rollout: ${destination}`);
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const mode = (await lstat(sourcePath)).mode & 0o777;
|
|
214
|
+
await atomicCopyFile(sourcePath, destination, mode);
|
|
215
|
+
}
|
|
216
|
+
/** Isolate Codex high-volume state per Agent while preserving auth and exact resume. */
|
|
217
|
+
export async function materializeDefaultCodexHome(agentHome, sourceHome, resumeSessionId) {
|
|
218
|
+
const codexHome = defaultCodexHome(agentHome);
|
|
219
|
+
await mkdir(codexHome, { recursive: true, mode: 0o700 });
|
|
220
|
+
await chmod(codexHome, 0o700);
|
|
221
|
+
const source = sourceHome === undefined ? null : resolve(sourceHome);
|
|
222
|
+
if (source !== null && source !== codexHome) {
|
|
223
|
+
await Promise.all([
|
|
224
|
+
linkIfMissing(join(source, "auth.json"), join(codexHome, "auth.json")),
|
|
225
|
+
linkIfMissing(join(source, "config.toml"), join(codexHome, "config.toml")),
|
|
226
|
+
]);
|
|
227
|
+
}
|
|
228
|
+
const sourceSessions = source === null || source === codexHome ? undefined : join(source, "sessions");
|
|
229
|
+
await ensurePrivateSessions(join(codexHome, "sessions"), sourceSessions);
|
|
230
|
+
const sourceIndex = source === null || source === codexHome ? undefined : join(source, "session_index.jsonl");
|
|
231
|
+
await ensurePrivateIndex(join(codexHome, "session_index.jsonl"), sourceIndex);
|
|
232
|
+
if (sourceSessions !== undefined && resumeSessionId) {
|
|
233
|
+
await hydrateResumeRollout(sourceSessions, join(codexHome, "sessions"), resumeSessionId);
|
|
234
|
+
}
|
|
235
|
+
return codexHome;
|
|
236
|
+
}
|
|
@@ -11,6 +11,8 @@ import { deepSeekHarnessLaunch } from "./deepseek-harness-launch.js";
|
|
|
11
11
|
import { prepareHarnessProfile } from "./deepseek-harness-profile.js";
|
|
12
12
|
import { mapAcpUpdate } from "./kimi-acp-runner.js";
|
|
13
13
|
import { prepareThreadDelivery } from "../thread-input-delivery.js";
|
|
14
|
+
import { createRuntimeObservationCapture } from "../runtime-observation.js";
|
|
15
|
+
import { createObservationMapper } from "./observation-mapper.js";
|
|
14
16
|
const absolutePath = z.string().refine(isAbsolute, "must be an absolute path");
|
|
15
17
|
export const HarnessInputSchema = z
|
|
16
18
|
.object({
|
|
@@ -45,6 +47,17 @@ async function settleWithin(promise, ms) {
|
|
|
45
47
|
}
|
|
46
48
|
}
|
|
47
49
|
export async function runHarness(input, bin, patchPath) {
|
|
50
|
+
const capture = createRuntimeObservationCapture("deepseek-harness");
|
|
51
|
+
const observer = createObservationMapper("deepseek-harness", capture);
|
|
52
|
+
try {
|
|
53
|
+
return await runHarnessObserved(input, bin, patchPath, observer);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
observer.finish();
|
|
57
|
+
capture.finish();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function runHarnessObserved(input, bin, patchPath, observer) {
|
|
48
61
|
const delivery = await prepareThreadDelivery({
|
|
49
62
|
env: process.env,
|
|
50
63
|
resumed: false,
|
|
@@ -115,6 +128,7 @@ export async function runHarness(input, bin, patchPath) {
|
|
|
115
128
|
.onNotification(methods.client.session.update, async ({ params }) => {
|
|
116
129
|
if (params.sessionId !== sessionId)
|
|
117
130
|
return;
|
|
131
|
+
observer.observe(params);
|
|
118
132
|
for (const event of mapAcpUpdate("deepseek-harness", params.update))
|
|
119
133
|
await emit(event);
|
|
120
134
|
});
|
|
@@ -141,6 +155,7 @@ export async function runHarness(input, bin, patchPath) {
|
|
|
141
155
|
throw new Error("Harness session unavailable");
|
|
142
156
|
await emit({ type: "thread.started", thread_id: sessionId });
|
|
143
157
|
stage = "prompt";
|
|
158
|
+
observer.context({ systemPrompt: input.systemPrompt, prompt: delivery.prompt });
|
|
144
159
|
const response = await ctx.request(methods.agent.session.prompt, {
|
|
145
160
|
sessionId,
|
|
146
161
|
prompt: [{ type: "text", text: delivery.prompt }],
|
|
@@ -155,6 +170,7 @@ export async function runHarness(input, bin, patchPath) {
|
|
|
155
170
|
}
|
|
156
171
|
return response;
|
|
157
172
|
});
|
|
173
|
+
observer.observe({ type: "nowwork.acp.result", sessionId, ...result });
|
|
158
174
|
const usage = result.usage;
|
|
159
175
|
if (!cancelled)
|
|
160
176
|
await emit({
|
|
@@ -12,6 +12,8 @@ import { augmentedPath } from "../runtime-path.js";
|
|
|
12
12
|
import { prepareThreadDelivery } from "../thread-input-delivery.js";
|
|
13
13
|
import { startFirstProgressWatchdog } from "./progress-watchdog.js";
|
|
14
14
|
import { assertKimiLegacyPromptFits, buildKimiArgs } from "./kimi.js";
|
|
15
|
+
import { createRuntimeObservationCapture } from "../runtime-observation.js";
|
|
16
|
+
import { createObservationMapper } from "./observation-mapper.js";
|
|
15
17
|
const ERROR_MESSAGE_CAP = 2_000;
|
|
16
18
|
// macOS launchd cold-starts ACP runtimes materially slower than an interactive
|
|
17
19
|
// shell. DeepSeek Harness was measured at ~4.4s before returning initialize
|
|
@@ -322,6 +324,18 @@ export async function probeKimiAcp(options, spawnProcess = spawn) {
|
|
|
322
324
|
}
|
|
323
325
|
}
|
|
324
326
|
export async function runKimiAcp(requestedOptions) {
|
|
327
|
+
const provider = requestedOptions.provider ?? "kimi";
|
|
328
|
+
const capture = createRuntimeObservationCapture(provider);
|
|
329
|
+
const observer = createObservationMapper(provider, capture);
|
|
330
|
+
try {
|
|
331
|
+
return await runKimiObserved(requestedOptions, observer, capture);
|
|
332
|
+
}
|
|
333
|
+
finally {
|
|
334
|
+
observer.finish();
|
|
335
|
+
capture.finish();
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function runKimiObserved(requestedOptions, observer, capture) {
|
|
325
339
|
// Until this adapter verifies native restoration, thread-bound runs are cold.
|
|
326
340
|
const options = process.env.CREW_THREAD_ROOT ? { ...requestedOptions, resume: false } : requestedOptions;
|
|
327
341
|
const provider = options.provider ?? "kimi";
|
|
@@ -364,6 +378,7 @@ export async function runKimiAcp(requestedOptions) {
|
|
|
364
378
|
const app = client({ name: `nowcrew-daemon-${provider}` })
|
|
365
379
|
.onRequest(methods.client.session.requestPermission, ({ params }) => selectKimiPermission(params, permission))
|
|
366
380
|
.onNotification(methods.client.session.update, async ({ params }) => {
|
|
381
|
+
observer.observe(params);
|
|
367
382
|
acpSemanticProgress = true;
|
|
368
383
|
firstProgress.observe();
|
|
369
384
|
for (const event of mapAcpUpdate(provider, params.update))
|
|
@@ -433,6 +448,7 @@ export async function runKimiAcp(requestedOptions) {
|
|
|
433
448
|
progressTimedOut = true;
|
|
434
449
|
void cancel();
|
|
435
450
|
});
|
|
451
|
+
observer.context({ prompt });
|
|
436
452
|
return nextContext.request(methods.agent.session.prompt, {
|
|
437
453
|
sessionId,
|
|
438
454
|
prompt: [{ type: "text", text: prompt }],
|
|
@@ -443,6 +459,7 @@ export async function runKimiAcp(requestedOptions) {
|
|
|
443
459
|
process.stderr.write(`${displayName} produced no semantic progress within the startup window\n`);
|
|
444
460
|
return 1;
|
|
445
461
|
}
|
|
462
|
+
observer.observe({ type: "nowwork.acp.result", sessionId, ...result });
|
|
446
463
|
const usage = result.usage;
|
|
447
464
|
await jsonLine({
|
|
448
465
|
type: "turn.completed",
|
|
@@ -464,6 +481,8 @@ export async function runKimiAcp(requestedOptions) {
|
|
|
464
481
|
return 1;
|
|
465
482
|
}
|
|
466
483
|
if (provider === "kimi" && !acpSemanticProgress && isKimiAuthenticationRequired(error)) {
|
|
484
|
+
capture.observe({ observationId: "fallback", type: "event", phase: "event", name: "capture.unsupported",
|
|
485
|
+
unavailable: ["kimi_legacy_fallback_events"] });
|
|
467
486
|
firstProgress.stop();
|
|
468
487
|
await stopChild(child);
|
|
469
488
|
process.stderr.write("Kimi ACP requires account login; falling back to configured CLI provider transport\n");
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
/** 只观察实际发给 runtime 的 prompt;不读取配置、环境变量或凭据文件。 */
|
|
3
|
+
export async function observeClaudeContext(observer, args, prompt) {
|
|
4
|
+
if (!observer.enabled)
|
|
5
|
+
return;
|
|
6
|
+
const at = args.indexOf("--append-system-prompt-file");
|
|
7
|
+
const path = at >= 0 ? args[at + 1] : undefined;
|
|
8
|
+
if (!path) {
|
|
9
|
+
observer.context({ prompt }, ["system_prompt"]);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
observer.context({ systemPrompt: await readFile(path, "utf8"), prompt });
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
observer.context({ prompt }, ["system_prompt"]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
const record = (raw) => raw !== null && typeof raw === "object" ? raw : {};
|
|
3
|
+
const string = (raw) => typeof raw === "string" && raw.length > 0 ? raw : undefined;
|
|
4
|
+
const list = (raw) => Array.isArray(raw) ? raw : [];
|
|
5
|
+
const boundedId = (raw) => {
|
|
6
|
+
const value = string(raw);
|
|
7
|
+
return value && value.length <= 150 ? value : undefined;
|
|
8
|
+
};
|
|
9
|
+
function usage(raw) {
|
|
10
|
+
const value = record(raw);
|
|
11
|
+
const pairs = {
|
|
12
|
+
inputTokens: value.input_tokens ?? value.inputTokens ?? value.input,
|
|
13
|
+
outputTokens: value.output_tokens ?? value.outputTokens ?? value.output,
|
|
14
|
+
cacheReadTokens: value.cache_read_input_tokens ?? value.cachedInputTokens ?? value.cachedReadTokens ?? value.cacheRead ?? record(value.cache).read,
|
|
15
|
+
cacheCreationTokens: value.cache_creation_input_tokens ?? value.cachedWriteTokens ?? value.cacheWrite ?? record(value.cache).write,
|
|
16
|
+
reasoningTokens: value.reasoning_tokens ?? value.reasoningTokens ?? value.reasoning,
|
|
17
|
+
costUsd: value.total_cost_usd ?? value.costUsd ?? record(value.cost).total,
|
|
18
|
+
};
|
|
19
|
+
const entries = Object.entries(pairs).filter(([, v]) => typeof v === "number" && Number.isFinite(v) && v >= 0);
|
|
20
|
+
return entries.length ? Object.fromEntries(entries) : undefined;
|
|
21
|
+
}
|
|
22
|
+
const errorStatus = (raw) => ["failed", "error", "cancelled", "interrupted", "aborted"].includes(String(raw)) ? "error" : "ok";
|
|
23
|
+
/** 原始事件的轻量投影;状态仅属于一次短命 runtime attempt,不从日志邻接猜父子关系。 */
|
|
24
|
+
export function createObservationMapper(runtime, capture, prefix = "run") {
|
|
25
|
+
let eventIndex = 0;
|
|
26
|
+
let nativeSessionId;
|
|
27
|
+
let nativeTurnId;
|
|
28
|
+
let textBuffer = "";
|
|
29
|
+
let thoughtBuffer = "";
|
|
30
|
+
let bufferedText = false;
|
|
31
|
+
let bufferedThought = false;
|
|
32
|
+
let bufferTruncated = false;
|
|
33
|
+
const started = new Map();
|
|
34
|
+
const emit = (fields) => capture.observe({
|
|
35
|
+
...(nativeSessionId ? { nativeSessionId } : {}), ...(nativeTurnId ? { nativeTurnId } : {}), ...fields,
|
|
36
|
+
});
|
|
37
|
+
const id = (native) => `${prefix}:${native}`.slice(0, 200);
|
|
38
|
+
const event = (name, fields = {}) => {
|
|
39
|
+
emit({ observationId: id(`event:${++eventIndex}`), type: "event", phase: "event", name, ...fields });
|
|
40
|
+
};
|
|
41
|
+
const tool = (native, phase, name, fields = {}) => {
|
|
42
|
+
const callId = boundedId(native);
|
|
43
|
+
const key = callId ? id(`tool:${callId}`) : undefined;
|
|
44
|
+
const knownName = key ? started.get(key) : undefined;
|
|
45
|
+
// pending/running 等状态更新不重置开始时间,缓存仅限一次短命 attempt。
|
|
46
|
+
if (phase === "start" && knownName !== undefined)
|
|
47
|
+
return;
|
|
48
|
+
if (key && phase === "start" && started.size < 20_000)
|
|
49
|
+
started.set(key, name);
|
|
50
|
+
const hasStart = key !== undefined && started.has(key);
|
|
51
|
+
emit({ observationId: key ?? id(`tool-unpaired:${++eventIndex}`), type: "tool", phase: key ? phase : "event",
|
|
52
|
+
name: phase === "end" && knownName !== undefined ? knownName : name,
|
|
53
|
+
...(callId ? { nativeCallId: callId } : {}),
|
|
54
|
+
...(!callId ? { unavailable: ["native_call_id", "paired_duration"] }
|
|
55
|
+
: phase === "end" && !hasStart ? { unavailable: ["start_time"] } : {}), ...fields });
|
|
56
|
+
};
|
|
57
|
+
const flush = () => {
|
|
58
|
+
if (bufferedText)
|
|
59
|
+
event("model.output", { output: textBuffer, ...(bufferTruncated ? { truncated: true } : {}) });
|
|
60
|
+
if (bufferedThought)
|
|
61
|
+
event("model.reasoning", { output: thoughtBuffer, ...(bufferTruncated ? { truncated: true } : {}) });
|
|
62
|
+
textBuffer = "";
|
|
63
|
+
thoughtBuffer = "";
|
|
64
|
+
bufferedText = false;
|
|
65
|
+
bufferedThought = false;
|
|
66
|
+
bufferTruncated = false;
|
|
67
|
+
};
|
|
68
|
+
const buffer = (value, thought) => {
|
|
69
|
+
const text = typeof value === "string" ? value : "";
|
|
70
|
+
if (thought) {
|
|
71
|
+
bufferedThought = true;
|
|
72
|
+
thoughtBuffer += text;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
bufferedText = true;
|
|
76
|
+
textBuffer += text;
|
|
77
|
+
}
|
|
78
|
+
if (textBuffer.length > 8192 || thoughtBuffer.length > 8192)
|
|
79
|
+
bufferTruncated = true;
|
|
80
|
+
const clip = (value) => value.slice(0, value.charCodeAt(8191) >= 0xd800 && value.charCodeAt(8191) <= 0xdbff ? 8191 : 8192);
|
|
81
|
+
textBuffer = clip(textBuffer);
|
|
82
|
+
thoughtBuffer = clip(thoughtBuffer);
|
|
83
|
+
};
|
|
84
|
+
const totals = (raw, scope, name = "runtime.usage") => {
|
|
85
|
+
const measured = usage(raw);
|
|
86
|
+
if (measured)
|
|
87
|
+
event(name, { usage: measured, usageScope: scope });
|
|
88
|
+
};
|
|
89
|
+
const codex = (method, raw) => {
|
|
90
|
+
nativeSessionId = boundedId(raw.threadId) ?? boundedId(record(raw.thread).id) ?? nativeSessionId;
|
|
91
|
+
nativeTurnId = boundedId(raw.turnId) ?? boundedId(record(raw.turn).id) ?? nativeTurnId;
|
|
92
|
+
if (method === "thread/tokenUsage/updated") {
|
|
93
|
+
const explicitTurn = boundedId(raw.turnId);
|
|
94
|
+
const explicitSession = boundedId(raw.threadId);
|
|
95
|
+
for (const source of ["last", "total"]) {
|
|
96
|
+
const measured = usage(record(raw.tokenUsage)[source]);
|
|
97
|
+
if (!measured)
|
|
98
|
+
continue;
|
|
99
|
+
const owner = source === "last" && explicitTurn ? `turn:${explicitTurn}`
|
|
100
|
+
: explicitSession ? `thread:${explicitSession}` : undefined;
|
|
101
|
+
event(`runtime.usage.${source}`, { usage: measured,
|
|
102
|
+
...(owner ? { observationId: id(`usage:${source}:${owner}`) } : {}),
|
|
103
|
+
...(source === "last" && explicitTurn ? { parentObservationId: id(`turn:${explicitTurn}`) } : {}),
|
|
104
|
+
nativeTurnId: explicitTurn,
|
|
105
|
+
unavailable: ["usage_scope", "provider_request_id", ...(owner ? [] : ["native_usage_owner"])],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (method === "turn/started" || method === "turn/completed") {
|
|
111
|
+
const turn = record(raw.turn);
|
|
112
|
+
emit({ observationId: id(`turn:${nativeTurnId ?? ++eventIndex}`), type: "agent",
|
|
113
|
+
phase: method === "turn/started" ? "start" : "end", name: "agent.turn",
|
|
114
|
+
...(method === "turn/completed" ? { status: errorStatus(turn.status), output: record(turn.error).message } : {}),
|
|
115
|
+
unavailable: ["provider_request_id", "per_generation_input", "per_generation_usage"] });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (method !== "item/started" && method !== "item/completed")
|
|
119
|
+
return;
|
|
120
|
+
const item = record(raw.item);
|
|
121
|
+
const itemType = String(item.type ?? "unknown");
|
|
122
|
+
const phase = method === "item/started" ? "start" : "end";
|
|
123
|
+
const explicitTurn = boundedId(raw.turnId);
|
|
124
|
+
const turnParent = explicitTurn ? { parentObservationId: id(`turn:${explicitTurn}`) } : {};
|
|
125
|
+
if (["commandExecution", "mcpToolCall", "dynamicToolCall", "fileChange", "webSearch", "collabAgentToolCall"].includes(itemType)) {
|
|
126
|
+
const native = boundedId(item.id);
|
|
127
|
+
const name = itemType === "mcpToolCall" ? `mcp.${String(item.tool ?? "tool")}` : itemType;
|
|
128
|
+
tool(native, phase, name.slice(0, 200), {
|
|
129
|
+
...turnParent,
|
|
130
|
+
...(phase === "start" ? { input: item.arguments ?? item.command ?? item.prompt ?? item.changes } : {
|
|
131
|
+
output: item.result ?? item.aggregatedOutput ?? item.error ?? item.agentsStates ?? item.changes,
|
|
132
|
+
status: itemType === "commandExecution" && typeof item.exitCode === "number" && item.exitCode !== 0
|
|
133
|
+
? "error" : errorStatus(item.status),
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
if (itemType === "collabAgentToolCall" && phase === "end") {
|
|
137
|
+
for (const childId of list(item.receiverThreadIds)) {
|
|
138
|
+
const child = boundedId(childId);
|
|
139
|
+
if (child && native)
|
|
140
|
+
emit({ observationId: id(`child:${createHash("sha256").update(JSON.stringify([native, child])).digest("hex")}`), parentObservationId: id(`tool:${native}`),
|
|
141
|
+
type: "agent", phase: "event", name: "agent.delegated", nativeSessionId: child,
|
|
142
|
+
unavailable: ["child_internal_events", "child_start_time", "child_end_time"] });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (phase !== "end")
|
|
148
|
+
return;
|
|
149
|
+
const nativeItem = boundedId(item.id);
|
|
150
|
+
const itemIdentity = { ...turnParent, ...(nativeItem ? { observationId: id(`item:${nativeItem}`) } : {}) };
|
|
151
|
+
if (itemType === "agentMessage")
|
|
152
|
+
event("model.output", { output: item.text, ...itemIdentity });
|
|
153
|
+
else if (itemType === "reasoning")
|
|
154
|
+
event("model.reasoning", {
|
|
155
|
+
output: item.summary ?? item.content ?? item.text, ...itemIdentity,
|
|
156
|
+
...(!item.summary && !item.content && !item.text ? { unavailable: ["reasoning_content"] } : {}),
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
const claude = (raw) => {
|
|
160
|
+
nativeSessionId = boundedId(raw.session_id) ?? nativeSessionId;
|
|
161
|
+
const message = record(raw.message);
|
|
162
|
+
const messageId = boundedId(message.id) ?? boundedId(raw.uuid);
|
|
163
|
+
const parent = boundedId(raw.parent_tool_use_id);
|
|
164
|
+
const parentFields = parent ? { parentObservationId: id(`tool:${parent}`) } : {};
|
|
165
|
+
if (raw.type === "assistant") {
|
|
166
|
+
for (const [blockIndex, value] of list(message.content).entries()) {
|
|
167
|
+
const block = record(value);
|
|
168
|
+
const identity = messageId ? { observationId: id(`message:${messageId}:block:${blockIndex}`) } : {};
|
|
169
|
+
if (block.type === "tool_use")
|
|
170
|
+
tool(block.id, "start", String(block.name ?? "tool").slice(0, 200), { input: block.input, ...parentFields });
|
|
171
|
+
if (block.type === "text")
|
|
172
|
+
event("model.output", { output: block.text, ...parentFields, ...identity });
|
|
173
|
+
if (block.type === "thinking" || block.type === "redacted_thinking")
|
|
174
|
+
event("model.reasoning", {
|
|
175
|
+
...(string(block.thinking) ? { output: block.thinking } : { unavailable: ["reasoning_content"] }), ...parentFields, ...identity,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const measured = usage(message.usage);
|
|
179
|
+
if (measured)
|
|
180
|
+
event("model.response", { usage: measured, usageScope: "generation",
|
|
181
|
+
...(messageId ? { observationId: id(`message:${messageId}:usage`) } : {}),
|
|
182
|
+
...(string(message.model) ? { model: String(message.model) } : {}),
|
|
183
|
+
unavailable: ["request_start_time", "provider_request_id", ...(messageId ? [] : ["native_message_id"])], ...parentFields });
|
|
184
|
+
}
|
|
185
|
+
else if (raw.type === "user") {
|
|
186
|
+
for (const value of list(message.content)) {
|
|
187
|
+
const block = record(value);
|
|
188
|
+
if (block.type === "tool_result")
|
|
189
|
+
tool(block.tool_use_id, "end", "tool.result", {
|
|
190
|
+
output: block.content, status: block.is_error === true ? "error" : "ok", ...parentFields,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else if (raw.type === "system" && boundedId(raw.task_id)) {
|
|
195
|
+
const task = String(raw.task_id);
|
|
196
|
+
const callId = boundedId(raw.tool_use_id);
|
|
197
|
+
emit({ observationId: id(`task:${task}`), type: "agent", name: "agent.delegated",
|
|
198
|
+
phase: raw.subtype === "task_started" ? "start" : raw.subtype === "task_notification" ? "end" : "event",
|
|
199
|
+
...(callId ? { parentObservationId: id(`tool:${callId}`) } : {}), output: raw.summary,
|
|
200
|
+
...(raw.status ? { status: errorStatus(raw.status) } : {}) });
|
|
201
|
+
}
|
|
202
|
+
else if (raw.type === "result") {
|
|
203
|
+
totals({ ...record(raw.usage), total_cost_usd: raw.total_cost_usd }, "execution");
|
|
204
|
+
event("agent.result", { status: raw.is_error === true ? "error" : "ok", output: raw.result });
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
const acp = (raw) => {
|
|
208
|
+
nativeSessionId = boundedId(raw.sessionId) ?? nativeSessionId;
|
|
209
|
+
const update = raw.update ? record(raw.update) : raw;
|
|
210
|
+
const kind = update.sessionUpdate;
|
|
211
|
+
if (kind === "agent_message_chunk" || kind === "agent_thought_chunk") {
|
|
212
|
+
const part = record(update.content);
|
|
213
|
+
if (part.type === "text")
|
|
214
|
+
buffer(part.text, kind === "agent_thought_chunk");
|
|
215
|
+
}
|
|
216
|
+
else if (kind === "tool_call" || kind === "tool_call_update") {
|
|
217
|
+
flush();
|
|
218
|
+
const complete = ["completed", "failed"].includes(String(update.status));
|
|
219
|
+
const name = string(update.kind) ? `tool.${update.kind}` : "tool.result";
|
|
220
|
+
if (kind === "tool_call" && !complete)
|
|
221
|
+
tool(update.toolCallId, "start", name, { input: update.rawInput });
|
|
222
|
+
if (complete)
|
|
223
|
+
tool(update.toolCallId, "end", name, {
|
|
224
|
+
input: update.rawInput, output: update.rawOutput ?? update.content, status: errorStatus(update.status),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
else if (raw.type === "nowwork.acp.result") {
|
|
228
|
+
flush();
|
|
229
|
+
totals(raw.usage, "turn");
|
|
230
|
+
event("agent.result", { status: raw.stopReason === "end_turn" ? "ok" : "error", unavailable: ["per_generation_input", "per_generation_usage", "provider_request_id"] });
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
const openCode = (raw) => {
|
|
234
|
+
nativeSessionId = boundedId(raw.sessionID) ?? nativeSessionId;
|
|
235
|
+
const part = record(raw.part);
|
|
236
|
+
if (raw.type === "tool_use") {
|
|
237
|
+
const state = record(part.state);
|
|
238
|
+
if (state.status === "running" || state.status === "pending")
|
|
239
|
+
tool(part.callID, "start", String(part.tool ?? "tool"), { input: state.input });
|
|
240
|
+
if (state.status === "completed" || state.status === "error")
|
|
241
|
+
tool(part.callID, "end", String(part.tool ?? "tool"), {
|
|
242
|
+
input: state.input, output: state.output ?? state.error, status: errorStatus(state.status),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
else if (raw.type === "step_start" || raw.type === "step_finish") {
|
|
246
|
+
const measured = usage({ ...record(part.tokens), costUsd: part.cost });
|
|
247
|
+
event(String(raw.type), { ...(measured ? { usage: measured, usageScope: "generation" } : {}),
|
|
248
|
+
unavailable: ["provider_request_id", "request_start_time", "per_generation_input"] });
|
|
249
|
+
}
|
|
250
|
+
else if (raw.type === "text" || raw.type === "reasoning") {
|
|
251
|
+
event(raw.type === "text" ? "model.output" : "model.reasoning", { output: part.text });
|
|
252
|
+
}
|
|
253
|
+
else if (raw.type === "error")
|
|
254
|
+
event("runtime.error", { status: "error", output: raw.error });
|
|
255
|
+
};
|
|
256
|
+
const pi = (raw) => {
|
|
257
|
+
if (raw.type === "session")
|
|
258
|
+
nativeSessionId = boundedId(raw.id) ?? nativeSessionId;
|
|
259
|
+
const assistant = record(raw.assistantMessageEvent);
|
|
260
|
+
if (raw.type === "message_update" && ["text_end", "thinking_end"].includes(String(assistant.type))) {
|
|
261
|
+
event(assistant.type === "text_end" ? "model.output" : "model.reasoning", { output: assistant.content });
|
|
262
|
+
}
|
|
263
|
+
else if (raw.type === "tool_execution_start") {
|
|
264
|
+
tool(raw.toolCallId, "start", String(raw.toolName ?? "tool"), { input: raw.args });
|
|
265
|
+
}
|
|
266
|
+
else if (raw.type === "tool_execution_end") {
|
|
267
|
+
tool(raw.toolCallId, "end", String(raw.toolName ?? "tool.result"), { output: raw.result, status: raw.isError === true ? "error" : "ok" });
|
|
268
|
+
}
|
|
269
|
+
else if (raw.type === "message_end" && record(raw.message).role === "assistant") {
|
|
270
|
+
const message = record(raw.message);
|
|
271
|
+
const measured = usage(message.usage);
|
|
272
|
+
event("model.response", { ...(measured ? { usage: measured, usageScope: "generation" } : {}),
|
|
273
|
+
...(string(message.model) ? { model: String(message.model) } : {}),
|
|
274
|
+
unavailable: ["provider_request_id", "request_start_time", "per_generation_input"] });
|
|
275
|
+
}
|
|
276
|
+
else if (raw.type === "error" || raw.type === "agent_error")
|
|
277
|
+
event("runtime.error", { status: "error", output: raw.error });
|
|
278
|
+
};
|
|
279
|
+
return {
|
|
280
|
+
enabled: capture.enabled,
|
|
281
|
+
context(input, unavailable = []) {
|
|
282
|
+
if (!capture.enabled)
|
|
283
|
+
return;
|
|
284
|
+
try {
|
|
285
|
+
event("context.input", { input, inputBytes: Buffer.byteLength(typeof input === "string" ? input : JSON.stringify(input)),
|
|
286
|
+
...(unavailable.length ? { unavailable: [...unavailable] } : {}) });
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
event("capture.unsupported", { unavailable: ["input_context"] });
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
observe(raw, method) {
|
|
293
|
+
if (!capture.enabled)
|
|
294
|
+
return;
|
|
295
|
+
try {
|
|
296
|
+
const value = record(raw);
|
|
297
|
+
if (runtime === "codex")
|
|
298
|
+
codex(method ?? "", value);
|
|
299
|
+
else if (runtime === "claude")
|
|
300
|
+
claude(value);
|
|
301
|
+
else if (runtime === "opencode")
|
|
302
|
+
openCode(value);
|
|
303
|
+
else if (runtime === "pi")
|
|
304
|
+
pi(value);
|
|
305
|
+
else
|
|
306
|
+
acp(value);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
event("capture.unsupported", { unavailable: ["invalid_runtime_event"] });
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
finish: flush,
|
|
313
|
+
};
|
|
314
|
+
}
|