@nowcrew/daemon 0.5.45 → 0.5.46

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.
@@ -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
+ }
@@ -0,0 +1,224 @@
1
+ import { dslog } from "./slog.js";
2
+ import { analyzeLocalMemoryInjection, createLocalMemoryAccessObserver, inspectLocalMemoryFilesWithinDeadline, localMemoryContentFacts, localMemoryFileChanged, } from "./local-memory-diagnostics.js";
3
+ function safeLog(eventType, message, fields) {
4
+ try {
5
+ dslog(eventType, message, fields);
6
+ }
7
+ catch {
8
+ // Local-memory diagnostics must never alter Agent execution.
9
+ }
10
+ }
11
+ function errorFields(error) {
12
+ const code = error?.code;
13
+ return {
14
+ error_name: error instanceof Error ? error.name : "unknown",
15
+ ...(typeof code === "string" ? { error_code: code } : {}),
16
+ };
17
+ }
18
+ export function logLocalMemoryContextPrepareFailure(identity, phase, error) {
19
+ safeLog("local_memory.context_prepare_failed", "本地记忆上下文准备失败", {
20
+ level: "WARN",
21
+ execution_id: identity.executionId,
22
+ agent_handle: identity.agentHandle,
23
+ task_key: identity.taskKey,
24
+ failure_phase: phase,
25
+ ...errorFields(error),
26
+ });
27
+ }
28
+ export function logLocalMemoryDiagnosticsFailure(identity, phase, error) {
29
+ safeLog("local_memory.diagnostics_failed", "本地记忆诊断失败并已放行", {
30
+ level: "WARN",
31
+ execution_id: identity.executionId,
32
+ agent_handle: identity.agentHandle,
33
+ task_key: identity.taskKey,
34
+ diagnostics_phase: phase,
35
+ ...errorFields(error),
36
+ });
37
+ }
38
+ function snapshotFields(snapshot) {
39
+ if (snapshot === null)
40
+ return {};
41
+ return {
42
+ memory_exists: snapshot.memory.exists,
43
+ memory_size_bytes: snapshot.memory.size_bytes,
44
+ memory_mtime_ms: snapshot.memory.mtime_ms,
45
+ memory_sha256: snapshot.memory.sha256,
46
+ memory_hash_skipped_reason: snapshot.memory.hash_skipped_reason,
47
+ memory_error_code: snapshot.memory.error_code,
48
+ memory_markdown_heading_count: snapshot.memory.markdown_heading_count,
49
+ memory_lessons_reference_count: snapshot.memory.lessons_reference_count,
50
+ lessons_exists: snapshot.lessons.exists,
51
+ lessons_size_bytes: snapshot.lessons.size_bytes,
52
+ lessons_mtime_ms: snapshot.lessons.mtime_ms,
53
+ lessons_sha256: snapshot.lessons.sha256,
54
+ lessons_hash_skipped_reason: snapshot.lessons.hash_skipped_reason,
55
+ lessons_error_code: snapshot.lessons.error_code,
56
+ lessons_markdown_heading_count: snapshot.lessons.markdown_heading_count,
57
+ };
58
+ }
59
+ function promptDelivery(runtime, stableProtocolRuntime) {
60
+ if (runtime === "claude")
61
+ return "claude_system_prompt_file";
62
+ if (!stableProtocolRuntime)
63
+ return "combined_wake_prompt";
64
+ return runtime === "codex" ? "codex_developer_instructions" : "kimi_protocol_prompt";
65
+ }
66
+ function logAccess(options, observation, sequence) {
67
+ safeLog("local_memory.file_access_observed", "观察到本地记忆文件访问", {
68
+ execution_id: options.executionId,
69
+ agent_handle: options.agentHandle,
70
+ task_key: options.taskKey,
71
+ runtime: options.runtime,
72
+ access_sequence: sequence,
73
+ ...observation,
74
+ });
75
+ }
76
+ export function createLocalMemoryTelemetry(options) {
77
+ const accessObserver = createLocalMemoryAccessObserver({
78
+ homeDir: options.agentDir,
79
+ runDir: options.runDir,
80
+ });
81
+ let beforeSnapshot = null;
82
+ let diagnosticsMs;
83
+ let accessSequence = 0;
84
+ let contextWasPrepared = false;
85
+ let runtimeStarted = false;
86
+ let runtimeReady = false;
87
+ let runtimeExitCode;
88
+ let executorCompleted = false;
89
+ return {
90
+ async captureBefore() {
91
+ const startedAt = Date.now();
92
+ try {
93
+ beforeSnapshot = await inspectLocalMemoryFilesWithinDeadline(options.agentDir, options.diagnosticsTimeoutMs);
94
+ }
95
+ catch (error) {
96
+ logLocalMemoryDiagnosticsFailure(options, "before", error);
97
+ }
98
+ finally {
99
+ diagnosticsMs = Date.now() - startedAt;
100
+ }
101
+ },
102
+ contextPrepared(context) {
103
+ try {
104
+ const sourceFacts = localMemoryContentFacts(context.memory);
105
+ const injectionFacts = analyzeLocalMemoryInjection(context.systemPrompt, context.memory);
106
+ safeLog("local_memory.context_prepared", "本地记忆上下文已准备", {
107
+ execution_id: options.executionId,
108
+ agent_handle: options.agentHandle,
109
+ task_key: options.taskKey,
110
+ runtime: options.runtime,
111
+ resumed: context.resumed,
112
+ is_memory_prune: options.isMemoryPrune,
113
+ memory_seed_created: context.memorySeedCreated,
114
+ memory_source_read: true,
115
+ memory_source_size_bytes: sourceFacts.size_bytes,
116
+ memory_source_sha256: sourceFacts.sha256,
117
+ memory_source_scan_skipped_reason: sourceFacts.scan_skipped_reason,
118
+ memory_read_matches_snapshot: sourceFacts.sha256 === undefined
119
+ || beforeSnapshot?.memory.sha256 === undefined
120
+ ? undefined
121
+ : sourceFacts.sha256 === beforeSnapshot.memory.sha256,
122
+ memory_source_nonblank: sourceFacts.nonblank,
123
+ memory_source_markdown_heading_count: sourceFacts.markdown_heading_count,
124
+ memory_source_lessons_reference_count: sourceFacts.lessons_reference_count,
125
+ memory_injection_state: injectionFacts.state,
126
+ memory_injection_source_bytes: injectionFacts.source_bytes,
127
+ memory_injection_bounded_bytes: injectionFacts.bounded_bytes,
128
+ memory_injected_bytes: injectionFacts.injected_bytes,
129
+ system_prompt_bytes: Buffer.byteLength(context.systemPrompt, "utf8"),
130
+ prompt_delivery: promptDelivery(options.runtime, options.stableProtocolRuntime),
131
+ diagnostics_ms: diagnosticsMs,
132
+ ...snapshotFields(beforeSnapshot),
133
+ });
134
+ contextWasPrepared = true;
135
+ }
136
+ catch (error) {
137
+ logLocalMemoryDiagnosticsFailure(options, "event_observe", error);
138
+ }
139
+ },
140
+ observe(event) {
141
+ try {
142
+ for (const observation of accessObserver.observe(event)) {
143
+ accessSequence += 1;
144
+ logAccess(options, observation, accessSequence);
145
+ }
146
+ }
147
+ catch (error) {
148
+ logLocalMemoryDiagnosticsFailure(options, "event_observe", error);
149
+ }
150
+ },
151
+ markRuntimeStarted() {
152
+ runtimeStarted = true;
153
+ },
154
+ markRuntimeReady() {
155
+ runtimeReady = true;
156
+ },
157
+ markRuntimeExited(exitCode) {
158
+ runtimeExitCode = exitCode;
159
+ },
160
+ markExecutorCompleted() {
161
+ executorCompleted = true;
162
+ },
163
+ async finish() {
164
+ let afterSnapshot = null;
165
+ try {
166
+ afterSnapshot = await inspectLocalMemoryFilesWithinDeadline(options.agentDir, options.diagnosticsTimeoutMs);
167
+ }
168
+ catch (error) {
169
+ logLocalMemoryDiagnosticsFailure(options, "after", error);
170
+ }
171
+ const memoryChanged = beforeSnapshot !== null && afterSnapshot !== null
172
+ ? localMemoryFileChanged(beforeSnapshot.memory, afterSnapshot.memory)
173
+ : undefined;
174
+ const lessonsChanged = beforeSnapshot !== null && afterSnapshot !== null
175
+ ? localMemoryFileChanged(beforeSnapshot.lessons, afterSnapshot.lessons)
176
+ : undefined;
177
+ const accessSummary = accessObserver.summary();
178
+ const sharedFilesChanged = memoryChanged === undefined || lessonsChanged === undefined
179
+ ? undefined
180
+ : memoryChanged || lessonsChanged;
181
+ const currentExecutionSharedWriteObserved = accessSummary.shared_memory_write_succeeded > 0
182
+ || accessSummary.shared_lessons_write_succeeded > 0;
183
+ safeLog("local_memory.execution_observed", "本地记忆执行观察已完成", {
184
+ execution_id: options.executionId,
185
+ agent_handle: options.agentHandle,
186
+ task_key: options.taskKey,
187
+ runtime: options.runtime,
188
+ is_memory_prune: options.isMemoryPrune,
189
+ context_prepared: contextWasPrepared,
190
+ runtime_started: runtimeStarted,
191
+ runtime_ready: runtimeReady,
192
+ runtime_exit_code: runtimeExitCode,
193
+ executor_outcome: executorCompleted ? "succeeded" : "failed",
194
+ memory_changed: memoryChanged,
195
+ lessons_changed: lessonsChanged,
196
+ shared_files_changed_during_execution: sharedFilesChanged,
197
+ unexpected_shared_write: options.isMemoryPrune
198
+ ? undefined
199
+ : currentExecutionSharedWriteObserved
200
+ ? true
201
+ : sharedFilesChanged === false ? false : undefined,
202
+ access_observation_count: accessSequence,
203
+ ...accessSummary,
204
+ ...(afterSnapshot === null ? {} : {
205
+ memory_after_exists: afterSnapshot.memory.exists,
206
+ memory_after_size_bytes: afterSnapshot.memory.size_bytes,
207
+ memory_after_mtime_ms: afterSnapshot.memory.mtime_ms,
208
+ memory_after_sha256: afterSnapshot.memory.sha256,
209
+ memory_after_hash_skipped_reason: afterSnapshot.memory.hash_skipped_reason,
210
+ memory_after_error_code: afterSnapshot.memory.error_code,
211
+ memory_after_markdown_heading_count: afterSnapshot.memory.markdown_heading_count,
212
+ memory_after_lessons_reference_count: afterSnapshot.memory.lessons_reference_count,
213
+ lessons_after_exists: afterSnapshot.lessons.exists,
214
+ lessons_after_size_bytes: afterSnapshot.lessons.size_bytes,
215
+ lessons_after_mtime_ms: afterSnapshot.lessons.mtime_ms,
216
+ lessons_after_sha256: afterSnapshot.lessons.sha256,
217
+ lessons_after_hash_skipped_reason: afterSnapshot.lessons.hash_skipped_reason,
218
+ lessons_after_error_code: afterSnapshot.lessons.error_code,
219
+ lessons_after_markdown_heading_count: afterSnapshot.lessons.markdown_heading_count,
220
+ }),
221
+ });
222
+ },
223
+ };
224
+ }
@@ -15,6 +15,8 @@ import { dirname, resolve } from "node:path";
15
15
  import { executableRuntimes } from "./runtime-capabilities.js";
16
16
  import { executionBackendCapability } from "./execution-backend.js";
17
17
  import { probeKimiAcp } from "./runtimes/kimi-acp-runner.js";
18
+ import { probeHermesAcp } from "./runtimes/hermes.js";
19
+ import { probeOpenCodeRun } from "./runtimes/opencode.js";
18
20
  export { executableRuntimes } from "./runtime-capabilities.js";
19
21
  const execFileP = promisify(execFile);
20
22
  export const DAEMON_CAPABILITIES = [
@@ -39,6 +41,7 @@ const RUNTIME_BINS = [
39
41
  ["codex", "codex"],
40
42
  ["cursor", "cursor-agent"],
41
43
  ["gemini", "gemini"],
44
+ ["hermes", "hermes"],
42
45
  ["opencode", "opencode"],
43
46
  ["copilot", "copilot"],
44
47
  ["kimi", "kimi"],
@@ -59,17 +62,29 @@ export async function detectRuntimes() {
59
62
  const checks = await Promise.all(RUNTIME_BINS.map(async ([name, bin]) => ((await isInstalled(bin)) ? name : null)));
60
63
  return checks.filter((x) => x !== null);
61
64
  }
62
- async function supportsKimiAcp() {
63
- return probeKimiAcp({ bin: "kimi" });
65
+ async function supportsKimiAcp(signal) {
66
+ return probeKimiAcp({ bin: "kimi", ...(signal ? { signal } : {}) });
67
+ }
68
+ async function supportsHermesAcp(signal) {
69
+ return probeHermesAcp({ bin: "hermes", ...(signal ? { signal } : {}) });
70
+ }
71
+ async function supportsOpenCodeRun(signal) {
72
+ return probeOpenCodeRun(signal);
64
73
  }
65
74
  /** Runtime adapters that can satisfy the durable protocol-v1 process contract. */
66
- export async function detectExecutionRuntimes(installed = detectRuntimes(), kimiAcpProbe = supportsKimiAcp) {
75
+ export async function detectExecutionRuntimes(installed = detectRuntimes(), kimiAcpProbe = supportsKimiAcp, hermesAcpProbe = supportsHermesAcp, openCodeProbe = supportsOpenCodeRun, signal) {
67
76
  const present = await installed;
68
- const supported = executableRuntimes(present).filter((runtime) => runtime !== "kimi");
69
- if (present.includes("kimi") && await kimiAcpProbe())
70
- supported.push("kimi");
71
- return supported;
77
+ const [kimiReady, hermesReady, openCodeReady] = await Promise.all([
78
+ present.includes("kimi") ? kimiAcpProbe(signal) : false,
79
+ present.includes("hermes") ? hermesAcpProbe(signal) : false,
80
+ present.includes("opencode") ? openCodeProbe(signal) : false,
81
+ ]);
82
+ return executableRuntimes(present).filter((runtime) => (runtime === "kimi" ? kimiReady
83
+ : runtime === "hermes" ? hermesReady
84
+ : runtime === "opencode" ? openCodeReady
85
+ : true));
72
86
  }
87
+ export const detectExecutionRuntimesWithSignal = (installed, signal) => detectExecutionRuntimes(installed, supportsKimiAcp, supportsHermesAcp, supportsOpenCodeRun, signal);
73
88
  export function daemonVersion() {
74
89
  try {
75
90
  const here = dirname(fileURLToPath(import.meta.url));
package/dist/main.js CHANGED
@@ -113,7 +113,12 @@ async function main() {
113
113
  process.exit(2);
114
114
  }
115
115
  process.stdout.write(formatDaemonLogLine(`🚀 ${td("Waking agent")} "${values.agent}" ${td("for channel")} ${values.channel}`) + "\n");
116
- initSlog(config.serverUrl, config.machineToken); // 一次性 run 模式也上报 SLS(runner 里的埋点生效)
116
+ initSlog(config.serverUrl, config.machineToken, {
117
+ daemonVersion: daemonVersion(),
118
+ cliVersion: cliVersion(),
119
+ ...(values.profile === undefined ? {} : { profileName: values.profile }),
120
+ agentsRoot: config.agentsRoot,
121
+ }); // 一次性 run 模式也上报 SLS(runner 里的埋点生效)
117
122
  const result = await runAgent(config, {
118
123
  handle: values.agent,
119
124
  channelId: values.channel,
package/dist/normalize.js CHANGED
@@ -34,7 +34,8 @@ export function classifyCommand(command) {
34
34
  /** 从各 runtime 的最终事件提取可交付文本。调用方按事件顺序保留最后一个非空值。 */
35
35
  export function extractFinalText(event) {
36
36
  const e = (event ?? {});
37
- if (e.type === "kimi.acp.text_delta" && e.text)
37
+ if ((e.type === "kimi.acp.text_delta" || e.type === "hermes.acp.text_delta"
38
+ || e.type === "opencode.text_delta") && e.text)
38
39
  return e.text;
39
40
  if (e.type === "result" && !e.is_error && e.result?.trim())
40
41
  return e.result.trim();
@@ -61,13 +62,16 @@ function parseKimiBashCommand(args) {
61
62
  /** 把一个 stream-json 事件归一化为 0..N 个活动。 */
62
63
  export function normalizeEvent(event) {
63
64
  const e = (event ?? {});
64
- if (e.type === "kimi.acp.text_delta" && e.text?.trim()) {
65
+ if ((e.type === "kimi.acp.text_delta" || e.type === "hermes.acp.text_delta"
66
+ || e.type === "opencode.text_delta") && e.text?.trim()) {
65
67
  return [{ kind: "text", label: "思考/说明", detail: e.text }];
66
68
  }
67
- if (e.type === "kimi.acp.tool_call") {
69
+ if (e.type === "kimi.acp.tool_call" || e.type === "hermes.acp.tool_call"
70
+ || e.type === "opencode.tool_call") {
68
71
  return [{ kind: "tool", label: e.title ? `工具:${e.title}` : "工具调用" }];
69
72
  }
70
- if (e.type === "kimi.acp.tool_result") {
73
+ if (e.type === "kimi.acp.tool_result" || e.type === "hermes.acp.tool_result"
74
+ || e.type === "opencode.tool_result") {
71
75
  return [{ kind: "tool_result", label: "工具返回" }];
72
76
  }
73
77
  if (e.type === "system" && e.subtype === "init") {
@@ -86,6 +90,12 @@ export function normalizeEvent(event) {
86
90
  if (e.type === "turn.completed") {
87
91
  return [{ kind: "done", label: "本轮结束" }];
88
92
  }
93
+ if (e.type === "opencode.error") {
94
+ const detail = typeof event.message === "string"
95
+ ? event.message
96
+ : undefined;
97
+ return [{ kind: "error", label: "运行出错", ...(detail ? { detail } : {}) }];
98
+ }
89
99
  if (e.type === "result") {
90
100
  return e.is_error
91
101
  ? [{ kind: "error", label: "运行出错", ...(e.result ? { detail: e.result } : {}) }]