@nowcrew/daemon 0.5.45 → 0.5.47

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.
@@ -19,6 +19,7 @@ import { awaitWithCancellation, RuntimeCancelledError, } from "./runtime-cancell
19
19
  import { isRuntimeReadyEvent, } from "./runtime-startup-gate.js";
20
20
  import { dslog } from "./slog.js";
21
21
  import { evaluateMemoryPrunePostcondition, inspectMemoryPruneFilesWithinDeadline, parseMemoryPruneTraceId, } from "./memory-prune-diagnostics.js";
22
+ import { createLocalMemoryTelemetry, logLocalMemoryContextPrepareFailure, logLocalMemoryDiagnosticsFailure, } from "./local-memory-telemetry.js";
22
23
  function memoryPruneSnapshotFields(snapshot) {
23
24
  const fields = {};
24
25
  for (const [label, fact] of Object.entries(snapshot)) {
@@ -79,6 +80,7 @@ export function withLocalExecutionFacts(serverPrompt, maxBytes) {
79
80
  }
80
81
  const STDERR_TAIL_CAP = 2_000;
81
82
  const MEMORY_PRUNE_DIAGNOSTICS_TIMEOUT_MS = 2_000;
83
+ const LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS = 250;
82
84
  const ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
83
85
  const RESERVED_ENV = new Set([
84
86
  "PATH",
@@ -112,15 +114,17 @@ export function awaitExit(child) {
112
114
  });
113
115
  }
114
116
  export function exitActivity(runtime, exitCode, stderrTail) {
115
- if ((runtime === "codex" || runtime === "kimi" || exitCode === -1) && exitCode !== 0) {
117
+ if ((runtime === "codex" || runtime === "kimi" || runtime === "hermes"
118
+ || runtime === "opencode" || exitCode === -1) && exitCode !== 0) {
116
119
  return {
117
120
  kind: "error",
118
121
  label: "运行出错",
119
122
  detail: `${runtime} exited with code ${exitCode}${stderrTail ? `: ${stderrTail}` : ""}`,
120
123
  };
121
124
  }
122
- if (runtime === "kimi" && exitCode === 0)
125
+ if ((runtime === "kimi" || runtime === "hermes" || runtime === "opencode") && exitCode === 0) {
123
126
  return { kind: "done", label: "本轮结束" };
127
+ }
124
128
  return null;
125
129
  }
126
130
  function wrapChild(child) {
@@ -222,16 +226,27 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
222
226
  : "high"
223
227
  : runtime.reasoning;
224
228
  const providerFp = providerFingerprint(runtime.name, providerConfig);
225
- const workspace = await awaitWithCancellation(prepareWorkspace({
226
- agentsRoot: input.launch.agentsRoot,
227
- handle: input.handle,
228
- cliPath: input.launch.cliPath,
229
- executionId: input.executionId,
230
- ...(input.keyMode === undefined ? {} : { keyMode: input.keyMode }),
231
- ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
232
- ...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
233
- ...(input.launch.description ? { description: input.launch.description } : {}),
234
- }), dependencies.cancellation);
229
+ let workspace;
230
+ try {
231
+ workspace = await awaitWithCancellation(prepareWorkspace({
232
+ agentsRoot: input.launch.agentsRoot,
233
+ handle: input.handle,
234
+ cliPath: input.launch.cliPath,
235
+ executionId: input.executionId,
236
+ ...(input.keyMode === undefined ? {} : { keyMode: input.keyMode }),
237
+ ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
238
+ ...(input.resumeKey === undefined ? {} : { resumeKey: input.resumeKey }),
239
+ ...(input.launch.description ? { description: input.launch.description } : {}),
240
+ }), dependencies.cancellation);
241
+ }
242
+ catch (error) {
243
+ logLocalMemoryContextPrepareFailure({
244
+ executionId: input.executionId,
245
+ agentHandle: input.handle,
246
+ ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
247
+ }, "workspace_prepare", error);
248
+ throw error;
249
+ }
235
250
  let materialized = null;
236
251
  let knownAttachmentDirectory = null;
237
252
  let startupReservation = null;
@@ -242,6 +257,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
242
257
  let memoryPruneBeforeSnapshot = null;
243
258
  let memoryPruneSharedWriteKey = null;
244
259
  let executionWorkspace = workspace;
260
+ let localMemoryTelemetry = null;
245
261
  try {
246
262
  if (isDeepSeekCodex && !providerConfig.providerApiKey) {
247
263
  throw new Error("DeepSeek API key is not configured for this Agent");
@@ -345,6 +361,29 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
345
361
  const attachmentPlan = routeRuntimeAttachments(runtime.name, materialized?.attachments ?? []);
346
362
  const wakePrompt = `${resolvePrompt(input.wakePrompt, promptContext)}${attachmentPlan.promptSuffix}`;
347
363
  memoryPruneTraceId = parseMemoryPruneTraceId(wakePrompt);
364
+ try {
365
+ localMemoryTelemetry = createLocalMemoryTelemetry({
366
+ executionId: input.executionId,
367
+ agentHandle: input.handle,
368
+ ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
369
+ runtime: runtime.name,
370
+ stableProtocolRuntime: dependencies.launchRuntime !== undefined,
371
+ agentDir: executionWorkspace.dir,
372
+ runDir: executionWorkspace.runDir,
373
+ isMemoryPrune: memoryPruneTraceId !== null,
374
+ diagnosticsTimeoutMs: dependencies.localMemoryDiagnosticsTimeoutMs
375
+ ?? LOCAL_MEMORY_DIAGNOSTICS_TIMEOUT_MS,
376
+ });
377
+ await localMemoryTelemetry.captureBefore();
378
+ }
379
+ catch (error) {
380
+ localMemoryTelemetry = null;
381
+ logLocalMemoryDiagnosticsFailure({
382
+ executionId: input.executionId,
383
+ agentHandle: input.handle,
384
+ ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
385
+ }, "initialize", error);
386
+ }
348
387
  if (memoryPruneTraceId !== null) {
349
388
  memoryPruneSharedWriteKey = JSON.stringify([input.launch.agentsRoot, input.handle]);
350
389
  const activePruneCount = (activeMemoryPrunes.get(memoryPruneSharedWriteKey) ?? 0) + 1;
@@ -378,7 +417,25 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
378
417
  }
379
418
  }
380
419
  memoryPruneFailurePhase = "prompt_write";
381
- await awaitWithCancellation(writeFile(workspace.systemPromptPath, systemPrompt, "utf8"), dependencies.cancellation);
420
+ try {
421
+ await awaitWithCancellation(writeFile(workspace.systemPromptPath, systemPrompt, "utf8"), dependencies.cancellation);
422
+ }
423
+ catch (error) {
424
+ logLocalMemoryContextPrepareFailure({
425
+ executionId: input.executionId,
426
+ agentHandle: input.handle,
427
+ ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
428
+ }, "system_prompt_write", error);
429
+ throw error;
430
+ }
431
+ localMemoryTelemetry?.contextPrepared({
432
+ systemPrompt,
433
+ memory: executionWorkspace.memory,
434
+ resumed: resuming,
435
+ ...(executionWorkspace.memorySeedCreated === undefined
436
+ ? {}
437
+ : { memorySeedCreated: executionWorkspace.memorySeedCreated }),
438
+ });
382
439
  memoryPruneFailurePhase = "runtime_prepare";
383
440
  const inheritedEnv = { ...process.env };
384
441
  for (const key of Object.keys(inheritedEnv)) {
@@ -454,6 +511,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
454
511
  const child = input.projectSkills !== undefined && dependencies.projectSkills !== undefined
455
512
  ? await dependencies.projectSkills.prepareAndLaunch(input.launch.agentsRoot, input.handle, input.projectSkills, () => launchRuntime(launchRequest))
456
513
  : await launchRuntime(launchRequest);
514
+ localMemoryTelemetry?.markRuntimeStarted();
457
515
  memoryPruneFailurePhase = "runtime_execution";
458
516
  if (child.cancel !== undefined) {
459
517
  dependencies.cancellation?.register(child.cancel);
@@ -479,6 +537,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
479
537
  if (runtimeReady)
480
538
  return;
481
539
  runtimeReady = true;
540
+ localMemoryTelemetry?.markRuntimeReady();
482
541
  startupReservation?.release();
483
542
  dslog("runtime.start_ready", "runtime 已完成初始化", {
484
543
  execution_id: input.executionId,
@@ -493,6 +552,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
493
552
  const event = parseLine(line);
494
553
  if (!event)
495
554
  return;
555
+ localMemoryTelemetry?.observe(event);
496
556
  if (isRuntimeReadyEvent(runtime.name, event))
497
557
  markRuntimeReady();
498
558
  const meta = extractRunMeta(event);
@@ -511,7 +571,8 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
511
571
  const extracted = extractFinalText(event);
512
572
  if (extracted) {
513
573
  const incremental = typeof event === "object" && event !== null
514
- && "type" in event && event.type === "kimi.acp.text_delta";
574
+ && "type" in event && (event.type === "kimi.acp.text_delta"
575
+ || event.type === "hermes.acp.text_delta" || event.type === "opencode.text_delta");
515
576
  finalText = incremental ? `${finalText ?? ""}${extracted}` : extracted;
516
577
  }
517
578
  for (const text of decodeExternalOutputEvent(runtime.name, event, externalOutput)) {
@@ -569,6 +630,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
569
630
  throw error;
570
631
  }
571
632
  const { exitCode, spawnError, terminationSignal } = runtimeExit;
633
+ localMemoryTelemetry?.markRuntimeExited(exitCode);
572
634
  memoryPruneRuntimeExitCode = exitCode;
573
635
  const errorTail = [
574
636
  stderrTail.trim(),
@@ -602,6 +664,7 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
602
664
  }
603
665
  }
604
666
  memoryPruneExecutorCompleted = true;
667
+ localMemoryTelemetry?.markExecutorCompleted();
605
668
  return {
606
669
  workspaceRunDir: executionWorkspace.runDir,
607
670
  exitCode,
@@ -624,6 +687,16 @@ async function executeLocalUnlocked(input, callbacks, dependencies) {
624
687
  }
625
688
  finally {
626
689
  startupReservation?.release();
690
+ try {
691
+ await localMemoryTelemetry?.finish();
692
+ }
693
+ catch (error) {
694
+ logLocalMemoryDiagnosticsFailure({
695
+ executionId: input.executionId,
696
+ agentHandle: input.handle,
697
+ ...(input.taskKey === undefined ? {} : { taskKey: input.taskKey }),
698
+ }, "finish", error);
699
+ }
627
700
  if (memoryPruneTraceId !== null) {
628
701
  try {
629
702
  const snapshot = await inspectMemoryPruneFilesWithinDeadline(executionWorkspace.dir, executionWorkspace.runDir, executionWorkspace.workLogPath, dependencies.memoryPruneDiagnosticsTimeoutMs
@@ -0,0 +1,336 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile, stat } from "node:fs/promises";
3
+ import { isAbsolute, join, normalize, resolve } from "node:path";
4
+ import { capMemoryForInject, MEMORY_INJECT_CAP } from "./prompt.js";
5
+ const INJECTION_MARKERS = [
6
+ "## Injected MEMORY.md (bounded local context)\n",
7
+ "## [注入] 你的 MEMORY.md(索引,只读参考)\n",
8
+ ];
9
+ const LOCAL_MEMORY_HASH_CAP_BYTES = 10 * 1024 * 1024;
10
+ function errorCode(error) {
11
+ const code = error?.code;
12
+ return typeof code === "string" ? code : error instanceof Error ? error.name : "unknown";
13
+ }
14
+ async function inspectFile(path, signal) {
15
+ let metadata;
16
+ try {
17
+ metadata = await stat(path);
18
+ }
19
+ catch (error) {
20
+ const code = errorCode(error);
21
+ return code === "ENOENT" ? { exists: false } : { exists: false, error_code: code };
22
+ }
23
+ const base = { exists: true, size_bytes: metadata.size, mtime_ms: metadata.mtimeMs };
24
+ if (!metadata.isFile())
25
+ return { ...base, hash_skipped_reason: "not_regular_file" };
26
+ if (metadata.size > LOCAL_MEMORY_HASH_CAP_BYTES) {
27
+ return { ...base, hash_skipped_reason: "too_large" };
28
+ }
29
+ try {
30
+ const content = await readFile(path, { encoding: "utf8", signal });
31
+ const facts = localMemoryContentFacts(content);
32
+ if (facts.scan_skipped_reason !== undefined) {
33
+ return { ...base, hash_skipped_reason: facts.scan_skipped_reason };
34
+ }
35
+ return { ...base, ...facts };
36
+ }
37
+ catch (error) {
38
+ return { ...base, error_code: errorCode(error) };
39
+ }
40
+ }
41
+ export async function inspectLocalMemoryFilesWithinDeadline(homeDir, timeoutMs) {
42
+ const controller = new AbortController();
43
+ let timer;
44
+ const inspection = Promise.all([
45
+ inspectFile(join(homeDir, "MEMORY.md"), controller.signal),
46
+ inspectFile(join(homeDir, "notes", "lessons.md"), controller.signal),
47
+ ]).then(([memory, lessons]) => ({ memory, lessons }));
48
+ const timeout = new Promise((_resolve, reject) => {
49
+ timer = setTimeout(() => {
50
+ controller.abort();
51
+ reject(Object.assign(new Error("local memory diagnostics timed out"), {
52
+ code: "diagnostics_timeout",
53
+ }));
54
+ }, Math.max(1, timeoutMs));
55
+ timer.unref?.();
56
+ });
57
+ try {
58
+ return await Promise.race([inspection, timeout]);
59
+ }
60
+ finally {
61
+ if (timer !== undefined)
62
+ clearTimeout(timer);
63
+ }
64
+ }
65
+ export function localMemoryContentFacts(content, maxScanBytes = LOCAL_MEMORY_HASH_CAP_BYTES) {
66
+ const sizeBytes = Buffer.byteLength(content, "utf8");
67
+ if (sizeBytes > Math.max(0, maxScanBytes)) {
68
+ return { size_bytes: sizeBytes, scan_skipped_reason: "too_large" };
69
+ }
70
+ return {
71
+ size_bytes: sizeBytes,
72
+ sha256: createHash("sha256").update(content).digest("hex"),
73
+ nonblank: content.trim().length > 0,
74
+ markdown_heading_count: content.split(/\r?\n/).filter((line) => /^#{1,6}\s+\S/.test(line)).length,
75
+ lessons_reference_count: content.match(/\bnotes[\\/]lessons\.md\b/gi)?.length ?? 0,
76
+ };
77
+ }
78
+ export function analyzeLocalMemoryInjection(systemPrompt, sourceMemory) {
79
+ const sourceBytes = Buffer.byteLength(sourceMemory, "utf8");
80
+ if (sourceMemory.length === 0) {
81
+ return { state: "empty_source", source_bytes: 0, bounded_bytes: 0, injected_bytes: 0 };
82
+ }
83
+ const bounded = capMemoryForInject(sourceMemory);
84
+ const marker = INJECTION_MARKERS.find((candidate) => systemPrompt.includes(candidate));
85
+ if (marker === undefined) {
86
+ return {
87
+ state: "omitted",
88
+ source_bytes: sourceBytes,
89
+ bounded_bytes: Buffer.byteLength(bounded, "utf8"),
90
+ injected_bytes: 0,
91
+ };
92
+ }
93
+ const markerStart = systemPrompt.indexOf(marker) + marker.length;
94
+ const sectionTail = systemPrompt.slice(markerStart);
95
+ const nextSection = sectionTail.search(/\n\n## (?:Injected work-log|\[注入\] 本任务 work-log)/);
96
+ const injected = nextSection < 0 ? sectionTail : sectionTail.slice(0, nextSection);
97
+ const fullyInjected = injected.startsWith(bounded);
98
+ return {
99
+ state: fullyInjected
100
+ ? sourceMemory.length > MEMORY_INJECT_CAP ? "cap_truncated" : "full"
101
+ : "prompt_budget_truncated",
102
+ source_bytes: sourceBytes,
103
+ bounded_bytes: Buffer.byteLength(bounded, "utf8"),
104
+ injected_bytes: Buffer.byteLength(injected, "utf8"),
105
+ };
106
+ }
107
+ export function localMemoryFileChanged(before, after) {
108
+ if (before.exists !== after.exists)
109
+ return true;
110
+ if (!before.exists && !after.exists)
111
+ return false;
112
+ if (before.sha256 !== undefined && after.sha256 !== undefined) {
113
+ return before.sha256 !== after.sha256;
114
+ }
115
+ return before.size_bytes !== after.size_bytes || before.mtime_ms !== after.mtime_ms;
116
+ }
117
+ function normalized(path) {
118
+ return normalize(path).replaceAll("\\", "/");
119
+ }
120
+ function targetPaths(options) {
121
+ return {
122
+ homeDir: normalized(resolve(options.homeDir)),
123
+ runDir: normalized(resolve(options.runDir)),
124
+ sharedMemory: normalized(resolve(options.homeDir, "MEMORY.md")),
125
+ sharedLessons: normalized(resolve(options.homeDir, "notes", "lessons.md")),
126
+ sharedNotes: normalized(resolve(options.homeDir, "notes")),
127
+ cwdMemory: normalized(resolve(options.runDir, "MEMORY.md")),
128
+ cwdLessons: normalized(resolve(options.runDir, "notes", "lessons.md")),
129
+ };
130
+ }
131
+ function classifyExplicitPath(path, paths) {
132
+ const replaced = path
133
+ .replace(/^\$\{CREW_HOME\}/, paths.homeDir)
134
+ .replace(/^\$CREW_HOME/, paths.homeDir);
135
+ const absolute = normalized(isAbsolute(replaced) ? replaced : resolve(paths.runDir, replaced));
136
+ if (absolute === paths.sharedMemory)
137
+ return "shared_memory";
138
+ if (absolute === paths.sharedLessons)
139
+ return "shared_lessons";
140
+ if (absolute === paths.cwdMemory && paths.cwdMemory !== paths.sharedMemory)
141
+ return "cwd_shadow_memory";
142
+ if (absolute === paths.cwdLessons && paths.cwdLessons !== paths.sharedLessons)
143
+ return "cwd_shadow_lessons";
144
+ if (absolute.startsWith(`${paths.sharedNotes}/`))
145
+ return "other_shared_note";
146
+ return null;
147
+ }
148
+ function commandTargets(command, paths) {
149
+ const targets = new Set();
150
+ const expandedCommand = command
151
+ .replaceAll("${CREW_HOME}", paths.homeDir)
152
+ .replaceAll("$CREW_HOME", paths.homeDir)
153
+ .replace(/["']/g, "");
154
+ const references = [
155
+ { values: [paths.sharedMemory], target: "shared_memory" },
156
+ { values: [paths.sharedLessons], target: "shared_lessons" },
157
+ { values: [paths.cwdMemory], target: "cwd_shadow_memory" },
158
+ { values: [paths.cwdLessons], target: "cwd_shadow_lessons" },
159
+ ];
160
+ for (const reference of references) {
161
+ if (reference.values.some((value) => expandedCommand.includes(value)))
162
+ targets.add(reference.target);
163
+ }
164
+ const withoutKnownPaths = references.flatMap((reference) => reference.values)
165
+ .reduce((value, reference) => value.replaceAll(reference, ""), expandedCommand);
166
+ if (/(?:^|[\s'"`])notes[\\/]lessons\.md(?:$|[\s'"`;|&])/i.test(withoutKnownPaths)) {
167
+ targets.add("cwd_shadow_lessons");
168
+ }
169
+ else if (/(?:^|[\s'"`])MEMORY\.md(?:$|[\s'"`;|&])/i.test(withoutKnownPaths)) {
170
+ targets.add("cwd_shadow_memory");
171
+ }
172
+ return [...targets];
173
+ }
174
+ function commandOperation(command) {
175
+ if (/(?:^|[;&|]\s*|\s)(?:rm|unlink)\s/i.test(command))
176
+ return "delete";
177
+ if (/(?:>>?|\btee\b|\btouch\b|\btruncate\b|\bsed\s+-i\b|\bperl\b[^\n]*\s-i\b|\bapply_patch\b)/i.test(command)) {
178
+ return "write";
179
+ }
180
+ return "read";
181
+ }
182
+ function toolOperation(name) {
183
+ const lowered = name.toLowerCase();
184
+ if (["read", "readfile", "view"].includes(lowered))
185
+ return "read";
186
+ if (["write", "writefile", "edit", "multiedit", "apply_patch", "patch"].includes(lowered))
187
+ return "write";
188
+ if (["delete", "remove", "unlink"].includes(lowered))
189
+ return "delete";
190
+ return null;
191
+ }
192
+ function toolPath(input) {
193
+ if (!input || typeof input !== "object" || Array.isArray(input))
194
+ return null;
195
+ const record = input;
196
+ for (const key of ["file_path", "path", "filePath"]) {
197
+ if (typeof record[key] === "string")
198
+ return record[key];
199
+ }
200
+ return null;
201
+ }
202
+ function recordObservation(observations, counts, observation) {
203
+ observations.push(observation);
204
+ const key = `${observation.target}:${observation.operation}:${observation.outcome}`;
205
+ counts.set(key, (counts.get(key) ?? 0) + 1);
206
+ }
207
+ export function createLocalMemoryAccessObserver(options) {
208
+ const paths = targetPaths(options);
209
+ const pending = new Map();
210
+ const counts = new Map();
211
+ return {
212
+ observe(event) {
213
+ const observations = [];
214
+ if (!event || typeof event !== "object")
215
+ return observations;
216
+ const record = event;
217
+ const message = record.message && typeof record.message === "object"
218
+ ? record.message
219
+ : undefined;
220
+ const blocks = Array.isArray(message?.content) ? message.content : [];
221
+ if (record.type === "assistant") {
222
+ for (const block of blocks) {
223
+ if (block.type !== "tool_use" || typeof block.id !== "string" || typeof block.name !== "string")
224
+ continue;
225
+ const operation = toolOperation(block.name);
226
+ const path = toolPath(block.input);
227
+ const target = operation === null || path === null ? null : classifyExplicitPath(path, paths);
228
+ if (operation !== null && target !== null)
229
+ pending.set(block.id, { operation, target });
230
+ }
231
+ }
232
+ if (record.type === "user") {
233
+ for (const block of blocks) {
234
+ if (block.type !== "tool_result" || typeof block.tool_use_id !== "string")
235
+ continue;
236
+ const access = pending.get(block.tool_use_id);
237
+ pending.delete(block.tool_use_id);
238
+ if (access === undefined)
239
+ continue;
240
+ recordObservation(observations, counts, {
241
+ ...access,
242
+ outcome: block.is_error === true ? "failed" : "succeeded",
243
+ evidence: "structured_tool",
244
+ });
245
+ }
246
+ }
247
+ if (record.type === "kimi.acp.tool_call" && typeof record.id === "string") {
248
+ const operation = typeof record.kind === "string"
249
+ ? toolOperation(record.kind)
250
+ : null;
251
+ const fallbackOperation = operation ?? (typeof record.title === "string"
252
+ ? toolOperation(record.title)
253
+ : null);
254
+ const path = toolPath(record.input);
255
+ const target = fallbackOperation === null || path === null
256
+ ? null
257
+ : classifyExplicitPath(path, paths);
258
+ if (fallbackOperation !== null && target !== null) {
259
+ pending.set(record.id, { operation: fallbackOperation, target });
260
+ }
261
+ }
262
+ if (record.type === "kimi.acp.tool_result" && typeof record.id === "string") {
263
+ if (record.status !== "completed" && record.status !== "failed")
264
+ return observations;
265
+ const access = pending.get(record.id);
266
+ pending.delete(record.id);
267
+ if (access !== undefined) {
268
+ recordObservation(observations, counts, {
269
+ ...access,
270
+ outcome: record.status === "failed" ? "failed" : "succeeded",
271
+ evidence: "structured_tool",
272
+ });
273
+ }
274
+ }
275
+ const item = record.type === "item.completed" && record.item && typeof record.item === "object"
276
+ ? record.item
277
+ : null;
278
+ if (item?.type === "command_execution" && typeof item.command === "string") {
279
+ const operation = commandOperation(item.command);
280
+ const outcome = typeof item.exit_code === "number"
281
+ ? item.exit_code === 0 ? "succeeded" : "failed"
282
+ : item.status === "completed"
283
+ ? "succeeded"
284
+ : item.status === "failed" || item.status === "declined"
285
+ ? "failed"
286
+ : "unknown";
287
+ for (const target of commandTargets(item.command, paths)) {
288
+ recordObservation(observations, counts, { target, operation, outcome, evidence: "shell_command" });
289
+ }
290
+ }
291
+ const fileChange = record.type === "diagnostic.file_change" && Array.isArray(record.changes)
292
+ ? record
293
+ : item?.type === "file_change" && Array.isArray(item.changes)
294
+ ? item
295
+ : null;
296
+ if (fileChange !== null && Array.isArray(fileChange.changes)) {
297
+ const outcome = fileChange.status === "completed"
298
+ ? "succeeded"
299
+ : fileChange.status === "failed" || fileChange.status === "declined"
300
+ ? "failed"
301
+ : fileChange === item ? "succeeded" : "unknown";
302
+ for (const change of fileChange.changes) {
303
+ if (!change || typeof change !== "object")
304
+ continue;
305
+ const detail = change;
306
+ if (typeof detail.path !== "string")
307
+ continue;
308
+ const target = classifyExplicitPath(detail.path, paths);
309
+ if (target === null)
310
+ continue;
311
+ recordObservation(observations, counts, {
312
+ target,
313
+ operation: detail.kind === "delete" ? "delete" : "write",
314
+ outcome,
315
+ evidence: "file_change",
316
+ });
317
+ }
318
+ }
319
+ return observations;
320
+ },
321
+ summary() {
322
+ const count = (target, operation, outcome) => counts.get(`${target}:${operation}:${outcome}`) ?? 0;
323
+ return {
324
+ memory_read_succeeded: count("shared_memory", "read", "succeeded"),
325
+ memory_read_failed: count("shared_memory", "read", "failed"),
326
+ lessons_read_succeeded: count("shared_lessons", "read", "succeeded"),
327
+ lessons_read_failed: count("shared_lessons", "read", "failed"),
328
+ shared_memory_write_succeeded: count("shared_memory", "write", "succeeded"),
329
+ shared_lessons_write_succeeded: count("shared_lessons", "write", "succeeded"),
330
+ cwd_shadow_write_succeeded: count("cwd_shadow_memory", "write", "succeeded")
331
+ + count("cwd_shadow_lessons", "write", "succeeded"),
332
+ pending_access_count: pending.size,
333
+ };
334
+ },
335
+ };
336
+ }