@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.
Files changed (60) hide show
  1. package/dist/codex-home-launch.js +7 -0
  2. package/dist/codex-home-mode-controller.js +68 -0
  3. package/dist/daemon-migration-controller.js +210 -0
  4. package/dist/daemon-migration-wiring.js +22 -0
  5. package/dist/execution-runner.js +5 -2
  6. package/dist/local-executor.js +8 -1
  7. package/dist/project-skills/capability.js +109 -0
  8. package/dist/project-skills/controller-convergence.js +57 -0
  9. package/dist/project-skills/projection-set-switch.js +419 -0
  10. package/dist/project-skills/projection-state-domain.js +170 -0
  11. package/dist/project-skills/projection-state-store.js +842 -0
  12. package/dist/project-skills/projection-state-transaction.js +322 -0
  13. package/dist/project-skills/projection-state.js +3 -0
  14. package/dist/project-skills/runtime-root-bootstrap.js +47 -0
  15. package/dist/project-skills/runtime-root-domain.js +268 -0
  16. package/dist/project-skills/runtime-root-gc.js +293 -0
  17. package/dist/project-skills/runtime-root-lease-artifact.js +46 -0
  18. package/dist/project-skills/runtime-root-leases.js +487 -0
  19. package/dist/project-skills/runtime-root-source-identity.js +60 -0
  20. package/dist/project-skills/runtime-root-startup.js +49 -0
  21. package/dist/project-skills/runtime-root-state-artifact-domain.js +143 -0
  22. package/dist/project-skills/runtime-root-state-index.js +356 -0
  23. package/dist/project-skills/runtime-root-store.js +722 -0
  24. package/dist/project-skills/serve-capability.js +28 -0
  25. package/dist/project-skills/serve-startup.js +37 -0
  26. package/dist/runtime-observation.js +219 -0
  27. package/dist/runtimes/claude-observation-runner.js +55 -0
  28. package/dist/runtimes/claude-thread-runner.js +17 -3
  29. package/dist/runtimes/claude.js +5 -3
  30. package/dist/runtimes/codex-app-server-runner.js +16 -2
  31. package/dist/runtimes/codex-home-migration-cli.js +26 -0
  32. package/dist/runtimes/codex-home-migration.js +112 -0
  33. package/dist/runtimes/codex-home.js +236 -0
  34. package/dist/runtimes/deepseek-harness-runner.js +16 -0
  35. package/dist/runtimes/kimi-acp-runner.js +19 -0
  36. package/dist/runtimes/observation-context.js +18 -0
  37. package/dist/runtimes/observation-mapper.js +314 -0
  38. package/dist/runtimes/opencode-runner.js +18 -4
  39. package/dist/runtimes/pi-runner.js +17 -1
  40. package/dist/storage-control.js +212 -0
  41. package/dist/storage-copy.js +628 -0
  42. package/dist/storage-credential-guard.js +21 -0
  43. package/dist/storage-doctor.js +228 -0
  44. package/dist/storage-manifest.js +1027 -0
  45. package/dist/storage-migration-controller.js +441 -0
  46. package/dist/storage-migration-manifest.js +105 -0
  47. package/dist/storage-migration-policy.js +100 -0
  48. package/dist/storage-runtime-controller.js +361 -0
  49. package/dist/storage-runtime-readiness.js +206 -0
  50. package/dist/storage-safe-fs.js +302 -0
  51. package/dist/storage-status-delivery.js +224 -0
  52. package/dist/storage-status-replay.js +140 -0
  53. package/dist/storage-target-confirmation.js +129 -0
  54. package/dist/storage-worker-command.js +37 -0
  55. package/dist/storage-worker-executor.js +431 -0
  56. package/dist/storage-worker-handoff.js +151 -0
  57. package/dist/storage-worker-operation.js +288 -0
  58. package/dist/storage-worker-status.js +259 -0
  59. package/dist/token.js +4 -1
  60. package/package.json +1 -1
@@ -0,0 +1,28 @@
1
+ import { WebSocket } from "ws";
2
+ import { daemonCapabilityBindings, } from "../machine-info.js";
3
+ import { prepareProjectSkillConnectionCapabilities, } from "./capability.js";
4
+ export function createProjectSkillConnectionCapabilityProvider(input) {
5
+ return async () => daemonCapabilityBindings(input.platform, await prepareProjectSkillConnectionCapabilities(input));
6
+ }
7
+ export function createProjectSkillMachineHelloPublisher(input) {
8
+ let latest = null;
9
+ let latestSocket = null;
10
+ const republish = () => {
11
+ if (latest === null || latestSocket?.readyState !== WebSocket.OPEN)
12
+ return;
13
+ const hello = input.effective(latest, input.status());
14
+ try {
15
+ latestSocket.send(JSON.stringify(hello));
16
+ input.onSent(hello);
17
+ }
18
+ catch { /* 非 OPEN,忽略 */ }
19
+ };
20
+ return Object.freeze({
21
+ publish: (hello, socket) => {
22
+ latest = hello;
23
+ latestSocket = socket;
24
+ republish();
25
+ },
26
+ republish,
27
+ });
28
+ }
@@ -0,0 +1,37 @@
1
+ import { createExecutionJournal } from "../execution-journal.js";
2
+ import { reconcileExecutionJournal } from "../execution-recovery.js";
3
+ import { dslog, flushSlog } from "../slog.js";
4
+ export async function initializeProjectSkillProtectedExecutionJournal(input) {
5
+ return input.startup.initializeAfterLeaseProtection(input.config.agentsRoot, async (snapshot) => {
6
+ input.protectedExecutionIds.clear();
7
+ for (const executionId of snapshot)
8
+ input.protectedExecutionIds.add(executionId);
9
+ const journal = input.injectedJournal ?? createExecutionJournal(input.config.agentsRoot, {
10
+ protectedExecutionIds: () => input.protectedExecutionIds,
11
+ onOperationTiming: (event) => {
12
+ if (event.operation !== "mark_runtime_ready"
13
+ && event.queueWaitMs < 250
14
+ && event.operationMs < 250)
15
+ return;
16
+ dslog("execution.journal_operation", "execution journal 操作完成", {
17
+ level: event.outcome === "succeeded" ? "INFO" : "ERROR",
18
+ execution_id: event.executionId,
19
+ operation: event.operation,
20
+ outcome: event.outcome,
21
+ queue_wait_ms: event.queueWaitMs,
22
+ operation_ms: event.operationMs,
23
+ total_ms: event.queueWaitMs + event.operationMs,
24
+ });
25
+ },
26
+ });
27
+ await reconcileExecutionJournal(journal, {
28
+ agentsRoot: input.config.agentsRoot,
29
+ serverUrl: input.config.serverUrl,
30
+ ...(input.profileName === undefined ? {} : { profileName: input.profileName }),
31
+ log: dslog,
32
+ flush: flushSlog,
33
+ writeStderr: (line) => process.stderr.write(line),
34
+ });
35
+ return journal;
36
+ });
37
+ }
@@ -0,0 +1,219 @@
1
+ import { createHash } from "node:crypto";
2
+ import { z } from "zod";
3
+ export const OBSERVABILITY_ENV = "NOWWORK_OBSERVABILITY_POLICY";
4
+ export const ObservabilityPolicySchema = z.object({
5
+ version: z.literal(1), captureId: z.string().min(1).max(128),
6
+ contentMode: z.enum(["metadata", "redacted"]),
7
+ }).strict();
8
+ const Id = z.string().min(1).max(200);
9
+ const Count = z.number().int().nonnegative().safe();
10
+ export const RuntimeObservationSchema = z.object({
11
+ kind: z.literal("agent_observation"), version: z.literal(1), captureId: z.string().min(1).max(128),
12
+ sourceEventIndex: z.number().int().positive().safe(),
13
+ runtime: z.enum(["codex", "claude", "kimi", "hermes", "deepseek-harness", "opencode", "pi"]),
14
+ observationId: Id, parentObservationId: Id.optional(), nativeSessionId: Id.optional(),
15
+ nativeTurnId: Id.optional(), nativeCallId: Id.optional(),
16
+ type: z.enum(["agent", "tool", "generation", "event", "span", "retriever"]),
17
+ phase: z.enum(["start", "end", "event"]), name: z.string().min(1).max(200),
18
+ observedAt: z.string().datetime({ offset: true }), status: z.enum(["ok", "error"]).optional(),
19
+ input: z.string().max(8192).optional(), output: z.string().max(8192).optional(), inputBytes: Count.optional(), outputBytes: Count.optional(), model: z.string().min(1).max(200).optional(),
20
+ usage: z.object({ inputTokens: Count.optional(), outputTokens: Count.optional(), cacheReadTokens: Count.optional(),
21
+ cacheCreationTokens: Count.optional(), reasoningTokens: Count.optional(), costUsd: z.number().finite().nonnegative().optional() }).strict().optional(),
22
+ usageScope: z.enum(["generation", "turn", "execution"]).optional(),
23
+ unavailable: z.array(z.string().min(1).max(120)).max(30).optional(), truncated: z.boolean().optional(),
24
+ expectedCount: z.number().int().nonnegative().safe().optional(), dropped: z.number().int().nonnegative().safe().optional(),
25
+ }).strict().superRefine((event, ctx) => {
26
+ if (event.name === "capture.summary") {
27
+ if (event.phase !== "event" || event.type !== "event" || event.expectedCount === undefined
28
+ || event.sourceEventIndex !== event.expectedCount + 1)
29
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Invalid capture summary" });
30
+ }
31
+ else if (event.expectedCount !== undefined || event.dropped !== undefined) {
32
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Only capture summary carries counts" });
33
+ }
34
+ });
35
+ export function parseObservabilityPolicy(raw) {
36
+ const parsed = ObservabilityPolicySchema.safeParse(raw);
37
+ return parsed.success ? parsed.data : undefined;
38
+ }
39
+ export function observationPolicyFromEnvironment(env) {
40
+ try {
41
+ return parseObservabilityPolicy(JSON.parse(env[OBSERVABILITY_ENV] ?? "null"));
42
+ }
43
+ catch {
44
+ return undefined;
45
+ }
46
+ }
47
+ /** 最后应用,清掉继承/用户配置中的同名变量,旧执行默认关闭。 */
48
+ export function observationEnvironment(env, policy) {
49
+ const clean = Object.fromEntries(Object.entries(env).filter(([key]) => key.toUpperCase() !== OBSERVABILITY_ENV));
50
+ const parsed = parseObservabilityPolicy(policy);
51
+ return parsed ? { ...clean, [OBSERVABILITY_ENV]: JSON.stringify(parsed) } : clean;
52
+ }
53
+ const REDACTED = "[REDACTED]";
54
+ // 与 Server 保持相同的边界规则;原 env/spec 不能先进入本地 journal 再由 Server 清洗。
55
+ const normalizeKey = (key) => key.toLowerCase().replace(/[^a-z0-9]/gu, "");
56
+ const forbiddenContainer = (key) => /^(?:env|envvars|runtimeenv|childenv|inheritedenv|environmentvariables|processenv|environmentplan|spec|executionspec|runtimeconfig|providerconfig)$/u.test(normalizeKey(key));
57
+ const sensitiveKey = (key) => /(?:apikey|accesskey(?:id)?|secretkey|secret|password|passwd|token|authorization|cookie|credential|privatekey|signature)$/u.test(normalizeKey(key))
58
+ || /^(?:key|sig|session|auth|authkey|authstring)$/u.test(normalizeKey(key));
59
+ function clip(value, limit) {
60
+ const end = value.charCodeAt(limit - 1);
61
+ return value.slice(0, end >= 0xd800 && end <= 0xdbff ? limit - 1 : limit);
62
+ }
63
+ function cleanUrl(raw) {
64
+ try {
65
+ const url = new URL(raw);
66
+ if (url.username || url.password) {
67
+ url.username = REDACTED;
68
+ url.password = "";
69
+ }
70
+ for (const name of [...url.searchParams.keys()]) {
71
+ if (sensitiveKey(name) || /^(?:code|ticket|sessionid|sas)$/iu.test(name) || /^(?:x-amz-|x-oss-|x-goog-)/iu.test(name))
72
+ url.searchParams.set(name, REDACTED);
73
+ }
74
+ if (url.hash && /(?:token|secret|password|credential|key|signature)/iu.test(url.hash))
75
+ url.hash = REDACTED;
76
+ return url.toString();
77
+ }
78
+ catch {
79
+ return REDACTED;
80
+ }
81
+ }
82
+ export function redactObservationText(value) {
83
+ const urls = [];
84
+ const text = value.replace(/\b[a-z][a-z0-9+.-]*:\/\/[^\s<>"'`]+/giu, raw => {
85
+ const index = urls.push(cleanUrl(raw)) - 1;
86
+ return `__NOWWORK_TRACE_URL_${index}__`;
87
+ });
88
+ return text
89
+ .replace(/-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z ]+ )?PRIVATE KEY-----/gu, REDACTED)
90
+ .replace(/\b(?:sk|pk)[-_]lf[-_][A-Za-z0-9_-]+\b/gu, REDACTED)
91
+ .replace(/\b(?:sk[-_][A-Za-z0-9_-]{6,}|gh[pousr]_[A-Za-z0-9_]{12,}|github_pat_[A-Za-z0-9_]{12,}|npm_[A-Za-z0-9]{12,})\b/gu, REDACTED)
92
+ .replace(/\b(authorization\s*[:=]\s*)(?:(?:bearer|basic)\s+)?[^\r\n,;]+/giu, `$1${REDACTED}`)
93
+ .replace(/((?:[A-Za-z0-9_]*(?:api[_-]?key|access[_-]?key|secret|password|passwd|token|cookie|credential|signature)|\b(?:key|session))\s*["']?\s*[:=]\s*)(?:"[^"\r\n]*"|'[^'\r\n]*'|[^\s,;}]+)/giu, `$1${REDACTED}`)
94
+ .replace(/(\/\/[^\s"'`]+\/:_auth(?:token)?\s*=)\s*[^\s"'`]+/giu, `$1${REDACTED}`)
95
+ .replace(/__NOWWORK_TRACE_URL_(\d+)__/gu, (_match, index) => urls[Number(index)] ?? REDACTED);
96
+ }
97
+ function safeIdentity(value) {
98
+ // 含敏感值的 ID 用稳定别名,不能把不同调用都改成同一个 [REDACTED]。
99
+ return redactObservationText(value) === value ? value : `redacted-id:${createHash("sha256").update(value).digest("hex")}`;
100
+ }
101
+ function content(value) {
102
+ let truncated = false;
103
+ let remaining = 2000;
104
+ const visit = (item, depth) => {
105
+ if (--remaining < 0 || depth > 12) {
106
+ truncated = true;
107
+ return "[TRUNCATED]";
108
+ }
109
+ if (typeof item === "string") {
110
+ const text = redactObservationText(item);
111
+ if (text.length > 8192)
112
+ truncated = true;
113
+ return clip(text, 8192);
114
+ }
115
+ if (item === null || typeof item === "number" || typeof item === "boolean")
116
+ return item;
117
+ if (Array.isArray(item)) {
118
+ if (item.length > 100)
119
+ truncated = true;
120
+ return item.slice(0, 100).map(child => visit(child, depth + 1));
121
+ }
122
+ if (typeof item === "object") {
123
+ const entries = Object.entries(item);
124
+ if (entries.length > 100)
125
+ truncated = true;
126
+ return Object.fromEntries(entries.slice(0, 100).map(([key, child]) => [redactObservationText(key), forbiddenContainer(key) || sensitiveKey(key)
127
+ || (normalizeKey(key) === "environment" && typeof child === "object") ? REDACTED : visit(child, depth + 1)]));
128
+ }
129
+ return String(item);
130
+ };
131
+ let structured = value;
132
+ if (typeof value === "string" && /^[\s]*[\[{]/u.test(value)) {
133
+ try {
134
+ structured = JSON.parse(value);
135
+ }
136
+ catch { /* 不是完整JSON则走文本清洗 */ }
137
+ }
138
+ const safe = visit(structured, 0);
139
+ const text = typeof safe === "string" ? safe : JSON.stringify(safe);
140
+ return { text: clip(text, 8192), truncated: truncated || text.length > 8192 };
141
+ }
142
+ /** 校验与本地内容策略在进入现有 telemetry journal 前再执行一次。 */
143
+ export function safeRuntimeObservation(raw, policy) {
144
+ if (!policy)
145
+ return undefined;
146
+ const result = RuntimeObservationSchema.safeParse(raw);
147
+ if (!result.success || result.data.captureId !== policy.captureId)
148
+ return undefined;
149
+ const { input, output, ...metadata } = result.data;
150
+ const cleanMetadata = { ...metadata, name: redactObservationText(metadata.name),
151
+ ...(metadata.model ? { model: redactObservationText(metadata.model) } : {}),
152
+ ...(metadata.unavailable ? { unavailable: metadata.unavailable.map(value => clip(redactObservationText(value), 120)) } : {}),
153
+ ...Object.fromEntries(["observationId", "parentObservationId", "nativeSessionId", "nativeTurnId", "nativeCallId"]
154
+ .flatMap(key => metadata[key] === undefined ? [] : [[key, safeIdentity(metadata[key])]])),
155
+ };
156
+ const inputContent = policy.contentMode === "redacted" && input !== undefined ? content(input) : undefined;
157
+ const outputContent = policy.contentMode === "redacted" && output !== undefined ? content(output) : undefined;
158
+ const safe = policy.contentMode === "metadata" ? cleanMetadata : {
159
+ ...cleanMetadata,
160
+ ...(inputContent ? { input: inputContent.text } : {}),
161
+ ...(outputContent ? { output: outputContent.text } : {}),
162
+ ...(inputContent?.truncated || outputContent?.truncated ? { truncated: true } : {}),
163
+ };
164
+ const checked = RuntimeObservationSchema.safeParse(safe);
165
+ return checked.success && Buffer.byteLength(JSON.stringify(checked.data)) <= 24 * 1024 ? checked.data : undefined;
166
+ }
167
+ /** 仅短命 runner 使用;不持久化、不联网、不影响业务事件流。 */
168
+ export function createRuntimeObservationCapture(runtime, env = process.env, emit = event => { process.stdout.write(`${JSON.stringify(event)}\n`); }) {
169
+ const policy = observationPolicyFromEnvironment(env);
170
+ let index = 0;
171
+ let dropped = 0;
172
+ let finished = false;
173
+ const send = (fields, summary = false) => {
174
+ if (!policy || finished)
175
+ return;
176
+ if (!summary && index >= 20_000) {
177
+ dropped += 1;
178
+ return;
179
+ }
180
+ try {
181
+ const { input, output, ...metadata } = fields;
182
+ const bytes = (value) => Buffer.byteLength(typeof value === "string" ? value : JSON.stringify(value));
183
+ const inputText = policy.contentMode === "redacted" && input !== undefined ? content(input) : undefined;
184
+ const outputText = policy.contentMode === "redacted" && output !== undefined ? content(output) : undefined;
185
+ let candidate = {
186
+ ...metadata, kind: "agent_observation", version: 1, captureId: policy.captureId,
187
+ runtime, sourceEventIndex: index + 1, observedAt: fields.observedAt ?? new Date().toISOString(),
188
+ ...(input === undefined ? {} : { inputBytes: bytes(input) }), ...(output === undefined ? {} : { outputBytes: bytes(output) }),
189
+ ...(inputText ? { input: inputText.text } : {}), ...(outputText ? { output: outputText.text } : {}),
190
+ ...(inputText?.truncated || outputText?.truncated ? { truncated: true } : {}),
191
+ };
192
+ if (Buffer.byteLength(JSON.stringify(candidate)) > 24 * 1024) {
193
+ candidate = { ...candidate, ...(inputText ? { input: clip(inputText.text, 1500) } : {}),
194
+ ...(outputText ? { output: clip(outputText.text, 1500) } : {}), truncated: true };
195
+ }
196
+ const safe = safeRuntimeObservation(candidate, policy);
197
+ if (!safe) {
198
+ dropped += 1;
199
+ return;
200
+ }
201
+ index += 1;
202
+ emit({ type: "nowwork.observation", observation: safe });
203
+ }
204
+ catch {
205
+ dropped += 1; /* 诊断永远不改变 runtime 结果 */
206
+ }
207
+ };
208
+ return {
209
+ enabled: policy !== undefined,
210
+ observe: fields => send(fields),
211
+ finish() {
212
+ if (finished)
213
+ return;
214
+ send({ observationId: "capture.summary", type: "event", phase: "event", name: "capture.summary",
215
+ expectedCount: index, dropped, ...(index === 0 ? { unavailable: ["runtime_events"] } : {}) }, true);
216
+ finished = true;
217
+ },
218
+ };
219
+ }
@@ -0,0 +1,55 @@
1
+ /** 无线程 Claude 的短命观察桥:保留原命令/参数,不引入线程或原生会话约束。 */
2
+ import { createInterface } from "node:readline";
3
+ import { pathToFileURL } from "node:url";
4
+ import spawn from "cross-spawn";
5
+ import { z } from "zod";
6
+ import { createRuntimeObservationCapture } from "../runtime-observation.js";
7
+ import { createObservationMapper } from "./observation-mapper.js";
8
+ import { observeClaudeContext } from "./observation-context.js";
9
+ const Input = z.object({ bin: z.string().min(1), args: z.array(z.string()) }).strict();
10
+ export async function runClaudeObservation(bin, args) {
11
+ const capture = createRuntimeObservationCapture("claude");
12
+ const observer = createObservationMapper("claude", capture);
13
+ await observeClaudeContext(observer, args, args.at(-1) ?? "");
14
+ const child = spawn(bin, [...args], { cwd: process.cwd(), env: process.env, stdio: ["ignore", "pipe", "pipe"], detached: false });
15
+ const stopped = new Promise((resolve, reject) => {
16
+ child.once("error", reject);
17
+ child.once("close", (code, signal) => resolve(code ?? (signal ? 128 : 1)));
18
+ });
19
+ void stopped.catch(() => undefined);
20
+ const cancel = () => { child.kill("SIGTERM"); };
21
+ process.once("SIGTERM", cancel);
22
+ process.once("SIGINT", cancel);
23
+ try {
24
+ if (!child.stdout || !child.stderr)
25
+ throw new Error("Claude observation runner requires output streams");
26
+ child.stderr.pipe(process.stderr, { end: false });
27
+ for await (const line of createInterface({ input: child.stdout })) {
28
+ try {
29
+ observer.observe(JSON.parse(line));
30
+ }
31
+ catch { /* 原非 JSON 输出仍透传 */ }
32
+ process.stdout.write(`${line}\n`);
33
+ }
34
+ return await stopped;
35
+ }
36
+ finally {
37
+ observer.finish();
38
+ capture.finish();
39
+ process.off("SIGTERM", cancel);
40
+ process.off("SIGINT", cancel);
41
+ }
42
+ }
43
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
44
+ let raw = "";
45
+ for await (const chunk of process.stdin) {
46
+ raw += String(chunk);
47
+ if (Buffer.byteLength(raw) > 8 * 1024 * 1024)
48
+ throw new Error("Claude observation input too large");
49
+ }
50
+ const input = Input.parse(JSON.parse(raw));
51
+ runClaudeObservation(input.bin, input.args).then(code => { process.exitCode = code; }).catch(() => {
52
+ process.stderr.write("Claude observation runner failed\n");
53
+ process.exitCode = 1;
54
+ });
55
+ }
@@ -8,6 +8,9 @@ import { fileURLToPath, pathToFileURL } from "node:url";
8
8
  import spawn from "cross-spawn";
9
9
  import { z } from "zod";
10
10
  import { prepareThreadDelivery, ThreadDeliveryColdStartRequired } from "../thread-input-delivery.js";
11
+ import { createRuntimeObservationCapture } from "../runtime-observation.js";
12
+ import { createObservationMapper } from "./observation-mapper.js";
13
+ import { observeClaudeContext } from "./observation-context.js";
11
14
  const InputSchema = z.object({ bin: z.string().min(1), args: z.array(z.string()),
12
15
  wakePrompt: z.string().min(1), sessionId: z.string().min(1).optional(), resume: z.boolean() }).strict();
13
16
  class PreflightError extends Error {
@@ -52,7 +55,7 @@ async function bounded(promise, milliseconds, label) {
52
55
  clearTimeout(timer);
53
56
  }
54
57
  }
55
- async function attempt(input, sessionId, resume, options) {
58
+ async function attempt(input, sessionId, resume, options, observer) {
56
59
  const proofDir = await mkdtemp(join(tmpdir(), "nowwork-claude-session-"));
57
60
  const proofPath = join(proofDir, "proof.json");
58
61
  const nonce = randomUUID();
@@ -182,6 +185,7 @@ async function attempt(input, sessionId, resume, options) {
182
185
  // work is outstanding; background shells must not hold it open forever.
183
186
  if (event.type === "result")
184
187
  sawResult = pendingAgents.size === 0;
188
+ observer.observe(event);
185
189
  output(event);
186
190
  }).catch(error => { fail(error instanceof Error ? error : new Error("claude_event_failed")); void stop().catch(fail); });
187
191
  });
@@ -207,6 +211,7 @@ async function attempt(input, sessionId, resume, options) {
207
211
  if (resume)
208
212
  output({ type: "nowwork.native_resume", runtime: "claude", session_id: sessionId });
209
213
  submitted = true;
214
+ await observeClaudeContext(observer, input.args, delivery.prompt);
210
215
  await send({ type: "user", session_id: "", uuid: userId, parent_tool_use_id: null,
211
216
  message: { role: "user", content: delivery.prompt } });
212
217
  await bounded(accepted.promise, options.ackTimeoutMs ?? 30_000, "claude_user_message_not_acknowledged");
@@ -237,16 +242,25 @@ async function attempt(input, sessionId, resume, options) {
237
242
  }
238
243
  }
239
244
  export async function runClaudeThread(input, options = {}) {
245
+ const capture = createRuntimeObservationCapture("claude", options.env ?? process.env, options.output);
246
+ try {
247
+ return await runClaudeObserved(input, options, capture);
248
+ }
249
+ finally {
250
+ capture.finish();
251
+ }
252
+ }
253
+ async function runClaudeObserved(input, options, capture) {
240
254
  const sessionId = input.sessionId ?? randomUUID();
241
255
  try {
242
- return await attempt(input, sessionId, input.resume, options);
256
+ return await attempt(input, sessionId, input.resume, options, createObservationMapper("claude", capture, "attempt:1"));
243
257
  }
244
258
  catch (error) {
245
259
  // The failed attempt never received a business user turn or entered its
246
260
  // model/tool loop. Native startup hooks may run again on cold recovery.
247
261
  if (!input.resume || !(error instanceof PreflightError || error instanceof ThreadDeliveryColdStartRequired))
248
262
  throw error;
249
- return attempt(input, randomUUID(), false, options);
263
+ return attempt(input, randomUUID(), false, options, createObservationMapper("claude", capture, "attempt:2"));
250
264
  }
251
265
  }
252
266
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
@@ -7,6 +7,7 @@ import { posix, win32 } from "node:path";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
8
8
  // cross-spawn:win32 上 npm CLI 是 .cmd shim,node 原生 spawn 不带 shell 无法执行(ENOENT/EINVAL)
9
9
  import spawn from "cross-spawn";
10
+ import { observationPolicyFromEnvironment } from "../runtime-observation.js";
10
11
  // Claude Code 原生 --effort 档位(claude 2.1.196 实测:--help 与非法值告警均枚举这五档)。
11
12
  // 白名单外的值(含 "default" 与 codex 专属档)回落 CLAUDE_DEFAULT_EFFORT,脏数据不影响启动。
12
13
  export const CLAUDE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
@@ -280,14 +281,15 @@ export function buildClaudeArgs(input) {
280
281
  }
281
282
  export function buildClaudeLaunch(input) {
282
283
  const args = buildClaudeArgs(input);
283
- if (!input.env.CREW_THREAD_ROOT?.trim())
284
+ const thread = Boolean(input.env.CREW_THREAD_ROOT?.trim());
285
+ if (!thread && !observationPolicyFromEnvironment(input.env))
284
286
  return { command: input.bin, args, cwd: input.cwd, env: input.env };
285
287
  const source = import.meta.url.endsWith(".ts");
286
288
  return { command: process.execPath,
287
289
  args: [...(source ? ["--import", pathToFileURL(createRequire(import.meta.url).resolve("tsx/esm")).href] : []),
288
- fileURLToPath(new URL(`./claude-thread-runner.${source ? "ts" : "js"}`, import.meta.url))],
290
+ fileURLToPath(new URL(`./${thread ? "claude-thread-runner" : "claude-observation-runner"}.${source ? "ts" : "js"}`, import.meta.url))],
289
291
  cwd: input.cwd, env: input.env,
290
- stdinText: JSON.stringify({ bin: input.bin, args: args.slice(0, -1), wakePrompt: input.wakePrompt,
292
+ stdinText: JSON.stringify(!thread ? { bin: input.bin, args } : { bin: input.bin, args: args.slice(0, -1), wakePrompt: input.wakePrompt,
291
293
  ...(input.sessionId ? { sessionId: input.sessionId } : {}), resume: input.resume === true }) };
292
294
  }
293
295
  export function spawnClaude(input) {
@@ -11,6 +11,8 @@ import { decideTurnRetry, itemMakesTurnUnsafe } from "./codex-turn-retry.js";
11
11
  import { startFirstProgressWatchdog } from "./progress-watchdog.js";
12
12
  import { prepareThreadDelivery, ThreadDeliveryColdStartRequired } from "../thread-input-delivery.js";
13
13
  import { codexExecutionPolicyArgs } from "./codex-execution-policy.js";
14
+ import { createRuntimeObservationCapture } from "../runtime-observation.js";
15
+ import { createObservationMapper } from "./observation-mapper.js";
14
16
  const RunnerInputSchema = z.object({
15
17
  systemPrompt: z.string().min(1),
16
18
  wakePrompt: z.string().min(1),
@@ -443,7 +445,8 @@ async function stopChildTree(child) {
443
445
  throw new Error(`Codex app-server process tree ${child.pid ?? "unknown"} did not exit`);
444
446
  }
445
447
  }
446
- async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs, resumeEventSessionId) {
448
+ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs, resumeEventSessionId, capture) {
449
+ const observer = createObservationMapper("codex", capture, `attempt:${attempt}`);
447
450
  const attemptStartedAt = Date.now();
448
451
  const child = spawn(bin, codexAppServerArgs({
449
452
  ...(input.projectRootMarkers === undefined ? {} : { projectRootMarkers: input.projectRootMarkers }),
@@ -483,6 +486,7 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
483
486
  let firstProgress = startFirstProgressWatchdog(() => undefined);
484
487
  firstProgress.stop();
485
488
  const rpc = new CodexRpcClient(child, input.effectivePermission, async (method, params) => {
489
+ observer.observe(params, method);
486
490
  if (method === "thread/tokenUsage/updated") {
487
491
  lastUsage = params.tokenUsage;
488
492
  }
@@ -625,6 +629,7 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
625
629
  });
626
630
  activeStage = "model_init";
627
631
  stageStartedAt = Date.now();
632
+ observer.context({ systemPrompt: input.systemPrompt, prompt: delivery.prompt });
628
633
  const started = await rpc.request("turn/start", {
629
634
  threadId,
630
635
  input: [
@@ -712,6 +717,15 @@ async function runCodexAppServerAttempt(bin, input, attempt, initializeTimeoutMs
712
717
  }
713
718
  }
714
719
  export async function runCodexAppServer(bin, options = {}) {
720
+ const capture = createRuntimeObservationCapture("codex");
721
+ try {
722
+ return await runCodexObserved(bin, options, capture);
723
+ }
724
+ finally {
725
+ capture.finish();
726
+ }
727
+ }
728
+ async function runCodexObserved(bin, options, capture) {
715
729
  const input = await readRunnerInput();
716
730
  const initializeTimeoutMs = Math.min(options.initializeTimeoutMs ?? INITIALIZE_RPC_TIMEOUT_MS, INITIALIZE_RPC_TIMEOUT_MS);
717
731
  const turnRetryDelayMs = options.turnRetryDelayMs ?? TRANSIENT_TURN_RETRY_DELAY_MS;
@@ -721,7 +735,7 @@ export async function runCodexAppServer(bin, options = {}) {
721
735
  let attemptInput = input;
722
736
  const resumeEventSessionId = input.resume ? input.sessionId : undefined;
723
737
  for (let attempt = 1;; attempt += 1) {
724
- const result = await runCodexAppServerAttempt(bin, attemptInput, attempt, initializeTimeoutMs, resumeEventSessionId);
738
+ const result = await runCodexAppServerAttempt(bin, attemptInput, attempt, initializeTimeoutMs, resumeEventSessionId, capture);
725
739
  if (result.restartCold) {
726
740
  if (coldRestarted)
727
741
  return result.code;
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from "node:util";
3
+ import { migrateCodexHome } from "./codex-home-migration.js";
4
+ const { values } = parseArgs({
5
+ args: process.argv.slice(2),
6
+ strict: true,
7
+ options: {
8
+ "agent-home": { type: "string" },
9
+ "source-home": { type: "string" },
10
+ "backup-root": { type: "string" },
11
+ yes: { type: "boolean", default: false },
12
+ },
13
+ });
14
+ function required(value, name) {
15
+ if (!value?.trim())
16
+ throw new Error(`Missing --${name}`);
17
+ return value;
18
+ }
19
+ if (!values.yes)
20
+ throw new Error("Refusing Codex home migration without --yes");
21
+ const result = await migrateCodexHome({
22
+ agentHome: required(values["agent-home"], "agent-home"),
23
+ sourceHome: required(values["source-home"], "source-home"),
24
+ backupRoot: required(values["backup-root"], "backup-root"),
25
+ });
26
+ process.stdout.write(`${result.status}${result.backupDir ? ` backup=${result.backupDir}` : ""}\n`);
@@ -0,0 +1,112 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ import { lstat, mkdir, readlink, rename, rm, symlink, unlink, } from "node:fs/promises";
4
+ import { join, resolve } from "node:path";
5
+ import { randomUUID } from "node:crypto";
6
+ import { atomicPrivateWrite } from "../atomic-private-write.js";
7
+ import { defaultCodexHome, materializeDefaultCodexHome } from "./codex-home.js";
8
+ const execFileAsync = promisify(execFile);
9
+ const SQLITE_FILES = ["state_5.sqlite", "state_5.sqlite-wal", "state_5.sqlite-shm"];
10
+ const MARKER_NAME = ".codex-home-migration.json";
11
+ async function exists(path) {
12
+ try {
13
+ await lstat(path);
14
+ return true;
15
+ }
16
+ catch (error) {
17
+ if (error.code === "ENOENT")
18
+ return false;
19
+ throw error;
20
+ }
21
+ }
22
+ async function assertNoOpenSqliteFiles(codexHome) {
23
+ for (const name of SQLITE_FILES) {
24
+ const path = join(codexHome, name);
25
+ if (!(await exists(path)))
26
+ continue;
27
+ try {
28
+ const result = await execFileAsync("lsof", ["-t", "--", path], { encoding: "utf8" });
29
+ if (result.stdout.trim()) {
30
+ throw new Error(`Codex SQLite file is still open: ${path}`);
31
+ }
32
+ }
33
+ catch (error) {
34
+ const code = error.code;
35
+ if (code === 1)
36
+ continue;
37
+ if (code === "ENOENT")
38
+ throw new Error("lsof is required to verify Codex home quiescence");
39
+ throw error;
40
+ }
41
+ }
42
+ }
43
+ async function symlinkTarget(path) {
44
+ try {
45
+ const info = await lstat(path);
46
+ if (!info.isSymbolicLink())
47
+ return null;
48
+ return await readlink(path);
49
+ }
50
+ catch (error) {
51
+ if (error.code === "ENOENT")
52
+ return null;
53
+ throw error;
54
+ }
55
+ }
56
+ export async function migrateCodexHome(input, dependencies = {}) {
57
+ const agentHome = resolve(input.agentHome);
58
+ const sourceHome = resolve(input.sourceHome);
59
+ const backupRoot = resolve(input.backupRoot);
60
+ const codexHome = defaultCodexHome(agentHome);
61
+ const sessions = join(codexHome, "sessions");
62
+ const index = join(codexHome, "session_index.jsonl");
63
+ const expectedSessions = resolve(sourceHome, "sessions");
64
+ const expectedIndex = resolve(sourceHome, "session_index.jsonl");
65
+ const marker = join(codexHome, MARKER_NAME);
66
+ const sessionLink = await symlinkTarget(sessions);
67
+ const indexLink = await symlinkTarget(index);
68
+ if (sessionLink === null && indexLink === null && await exists(marker)) {
69
+ return { status: "already-migrated" };
70
+ }
71
+ if (sessionLink === null || indexLink === null) {
72
+ throw new Error("Refusing migration: Codex home is not in the complete legacy symlink layout");
73
+ }
74
+ if (resolve(codexHome, sessionLink) !== expectedSessions || resolve(codexHome, indexLink) !== expectedIndex) {
75
+ throw new Error("Refusing migration: legacy symlinks do not point to the configured source home");
76
+ }
77
+ await (dependencies.ensureQuiesced ?? assertNoOpenSqliteFiles)(codexHome);
78
+ const stamp = (dependencies.now ?? (() => new Date()))().toISOString().replaceAll(/[^0-9]/g, "").slice(0, 14);
79
+ const backupDir = join(backupRoot, `${stamp}-${dependencies.id?.() ?? randomUUID()}`);
80
+ await mkdir(backupDir, { recursive: true, mode: 0o700 });
81
+ await symlink(sessionLink, join(backupDir, "sessions"), "dir");
82
+ await symlink(indexLink, join(backupDir, "session_index.jsonl"), "file");
83
+ const moved = [];
84
+ try {
85
+ for (const name of SQLITE_FILES) {
86
+ const current = join(codexHome, name);
87
+ if (await exists(current)) {
88
+ await rename(current, join(backupDir, name));
89
+ moved.push(name);
90
+ }
91
+ }
92
+ await materializeDefaultCodexHome(agentHome, sourceHome);
93
+ await atomicPrivateWrite(marker, JSON.stringify({
94
+ version: 1,
95
+ migrated_at: new Date().toISOString(),
96
+ source_home: sourceHome,
97
+ backup_dir: backupDir,
98
+ }) + "\n");
99
+ return { status: "migrated", backupDir };
100
+ }
101
+ catch (error) {
102
+ for (const name of moved.reverse()) {
103
+ await rename(join(backupDir, name), join(codexHome, name)).catch(() => undefined);
104
+ }
105
+ await rm(sessions, { recursive: true, force: true });
106
+ await unlink(index).catch(() => undefined);
107
+ await symlink(sessionLink, sessions, "dir").catch(() => undefined);
108
+ await symlink(indexLink, index, "file").catch(() => undefined);
109
+ throw error;
110
+ }
111
+ }
112
+ export const codexHomeMigrationMarkerName = MARKER_NAME;