evolcore 0.0.16 → 0.0.18

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 (65) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/bin/codex-managed-hook.mjs +16 -7
  3. package/bin/install-codex-managed-hooks.mjs +4 -2
  4. package/dist/agents/claude-runner.js +132 -14
  5. package/dist/agents/codex-app-server-client.js +6 -1
  6. package/dist/agents/codex-runner.js +21 -6
  7. package/dist/agents/ecagent-runner.js +39 -10
  8. package/dist/agents/gemini-runner.js +90 -19
  9. package/dist/aun/aid/store.js +36 -0
  10. package/dist/aun/msg/group.js +3 -1
  11. package/dist/aun/msg/p2p.js +23 -9
  12. package/dist/channels/aun.js +159 -21
  13. package/dist/cli/agent-command.js +67 -6
  14. package/dist/cli/agent.js +26 -0
  15. package/dist/cli/command-log.js +23 -4
  16. package/dist/cli/daemon-commands.js +53 -12
  17. package/dist/cli/init.js +21 -5
  18. package/dist/cli/restart-monitor.js +13 -6
  19. package/dist/cli/task-context.js +46 -1
  20. package/dist/cli/watch-logs.js +2 -2
  21. package/dist/config/builtin-roles.js +5 -1
  22. package/dist/config/role-ranks.js +4 -0
  23. package/dist/core/audit/event-key.js +29 -0
  24. package/dist/core/audit/log-integrity.js +13 -3
  25. package/dist/core/auth/auth-gateway.js +14 -18
  26. package/dist/core/auth/authorization-audit.js +110 -3
  27. package/dist/core/auth/authorization-denial.js +17 -0
  28. package/dist/core/auth/operation-authorizer.js +143 -18
  29. package/dist/core/auth/operation-catalog.js +21 -5
  30. package/dist/core/bootstrap-messages.js +11 -6
  31. package/dist/core/bootstrap-service.js +26 -4
  32. package/dist/core/causation/aun-association.js +7 -4
  33. package/dist/core/command/agent-control.js +25 -16
  34. package/dist/core/command/command-handler.js +50 -4
  35. package/dist/core/command/group-menu.js +1 -1
  36. package/dist/core/command/menu-catalog.js +32 -7
  37. package/dist/core/command/menu-handler.js +59 -23
  38. package/dist/core/command/menu-protocol.js +196 -0
  39. package/dist/core/command/slash-gate.js +14 -5
  40. package/dist/core/command/slash-handler.js +81 -99
  41. package/dist/core/event-catalog.js +18 -0
  42. package/dist/core/message/message-bridge.js +72 -9
  43. package/dist/core/message/pause-controller.js +53 -0
  44. package/dist/core/message/response-engine.js +98 -11
  45. package/dist/core/permission/sandbox-runtime.js +79 -13
  46. package/dist/core/permission/tool-policy.js +1 -1
  47. package/dist/index.js +357 -48
  48. package/dist/ipc.js +75 -4
  49. package/dist/utils/atomic-write.js +45 -11
  50. package/dist/utils/error-utils.js +38 -0
  51. package/dist/utils/logger.js +27 -0
  52. package/dist/utils/windows-autostart.js +740 -83
  53. package/ecagent/dist/harness/agent-harness.d.ts +1 -1
  54. package/ecagent/dist/harness/agent-harness.js +6 -4
  55. package/kits/docs/evolcore/config.md +1 -1
  56. package/kits/docs/evolcore/group-rules.md +2 -1
  57. package/kits/docs/identity/ROLE_DETAIL.md +3 -1
  58. package/kits/eck_manifest.json +25 -16
  59. package/kits/rules/01-overview.md +5 -5
  60. package/kits/rules/03-identity.md +1 -1
  61. package/kits/rules/04-relation.md +4 -4
  62. package/kits/rules/05-venue.md +5 -5
  63. package/kits/templates/bootstrap-welcome.md +3 -1
  64. package/kits/templates/system-fragments/bootstrap.md +17 -9
  65. package/package.json +1 -1
package/dist/ipc.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import net from 'net';
2
+ import { performance } from 'node:perf_hooks';
2
3
  import fs from 'fs';
3
4
  import crypto from 'crypto';
4
5
  import os from 'os';
5
6
  import path from 'path';
6
7
  import { logger } from './utils/logger.js';
8
+ import { withMenuProcessingTime, logMenuRequestCompleted, logMenuRequestReceived } from './core/command/menu-protocol.js';
7
9
  import { fileCache } from './core/daemon-file-cache.js';
8
10
  import { HANDOFF_QUERY_MAX_LIMIT, HANDOFF_STATES } from './core/handoff/types.js';
9
11
  import { AgentReloadBusyError } from './core/agent-reload-coordinator.js';
@@ -27,11 +29,14 @@ const LOCAL_CONTROL_COMMANDS = new Set([
27
29
  'wechat.contact-bind.register',
28
30
  'queue-snapshot',
29
31
  'aun-aid-stats-record-outbound',
32
+ 'evolagent.list',
33
+ 'evolagent.show',
30
34
  'evolagent.reload',
31
35
  'evolagent.load',
32
36
  'evolagent.resync',
33
37
  'menu.exec',
34
38
  'monitor-snapshot',
39
+ 'shutdown',
35
40
  ]);
36
41
  function requiresLocalControlToken(cmd) {
37
42
  // Managed bootstrap completion is authenticated by the session delegation
@@ -61,6 +66,7 @@ export class IpcServer {
61
66
  aunAidStatsProvider;
62
67
  aunAidStatsRecorder;
63
68
  menuExecutor;
69
+ agentOperationExecutor;
64
70
  statsProvider;
65
71
  agentStatsProvider;
66
72
  queueSnapshotProvider;
@@ -71,6 +77,7 @@ export class IpcServer {
71
77
  triggerExecutor;
72
78
  bootstrapCompleteExecutor;
73
79
  agentReloadExecutor;
80
+ shutdownExecutor;
74
81
  taskRuntimeContextProvider;
75
82
  codexPreToolUseExecutor;
76
83
  aunMsgSender;
@@ -121,6 +128,9 @@ export class IpcServer {
121
128
  setMenuExecutor(executor) {
122
129
  this.menuExecutor = executor;
123
130
  }
131
+ setAgentOperationExecutor(executor) {
132
+ this.agentOperationExecutor = executor;
133
+ }
124
134
  setConfigOperationExecutor(executor) {
125
135
  this.configOperationExecutor = executor;
126
136
  }
@@ -142,6 +152,10 @@ export class IpcServer {
142
152
  setAgentReloadExecutor(executor) {
143
153
  this.agentReloadExecutor = executor;
144
154
  }
155
+ /** Inject the daemon's graceful shutdown lifecycle for local stop/restart. */
156
+ setShutdownExecutor(executor) {
157
+ this.shutdownExecutor = executor;
158
+ }
145
159
  /** Inject AUN AID state aggregator for aun-aids IPC handler */
146
160
  setAunAidProvider(provider) {
147
161
  this.aunAidProvider = provider;
@@ -332,23 +346,31 @@ export class IpcServer {
332
346
  return new Promise((resolve, reject) => {
333
347
  this.server = net.createServer((conn) => {
334
348
  let buf = '';
349
+ let handled = false;
350
+ let requestReceivedAtMono;
351
+ const requestAbort = new AbortController();
335
352
  conn.on('data', async (data) => {
353
+ if (handled)
354
+ return;
355
+ requestReceivedAtMono ??= performance.now();
336
356
  buf += data.toString();
337
357
  // Simple newline-delimited JSON protocol
338
358
  const idx = buf.indexOf('\n');
339
359
  if (idx === -1)
340
360
  return;
361
+ handled = true;
341
362
  const line = buf.slice(0, idx);
342
363
  buf = buf.slice(idx + 1);
343
364
  try {
344
365
  const cmd = JSON.parse(line);
345
- const response = await this.handleCommand(cmd);
366
+ const response = await this.handleCommand(cmd, requestAbort.signal, requestReceivedAtMono);
346
367
  conn.end(JSON.stringify(response) + '\n');
347
368
  }
348
369
  catch {
349
370
  conn.end(JSON.stringify({ error: 'invalid request' }) + '\n');
350
371
  }
351
372
  });
373
+ conn.on('close', () => requestAbort.abort('Codex hook client disconnected'));
352
374
  conn.on('error', () => { }); // ignore client errors
353
375
  });
354
376
  const onListenError = (err) => {
@@ -385,7 +407,7 @@ export class IpcServer {
385
407
  catch { }
386
408
  }
387
409
  }
388
- async handleCommand(cmd) {
410
+ async handleCommand(cmd, requestSignal, receivedAtMono = performance.now()) {
389
411
  if (requiresLocalControlToken(cmd) && !this.hasValidControlToken(cmd.controlToken)) {
390
412
  return { ok: false, code: 'INVALID_CONTROL_TOKEN', error: 'valid local control token is required' };
391
413
  }
@@ -405,6 +427,7 @@ export class IpcServer {
405
427
  threadId: cmd.threadId,
406
428
  toolName: cmd.toolName,
407
429
  toolInput: cmd.toolInput,
430
+ signal: requestSignal,
408
431
  });
409
432
  }
410
433
  catch (error) {
@@ -413,6 +436,23 @@ export class IpcServer {
413
436
  }
414
437
  case 'ping':
415
438
  return { pong: true, pid: process.pid, protocolVersion: 1 };
439
+ case 'shutdown': {
440
+ if (!this.shutdownExecutor)
441
+ return { ok: false, error: 'shutdown executor not configured' };
442
+ if (Number.isInteger(cmd.expectedPid) && cmd.expectedPid !== process.pid) {
443
+ return { ok: false, code: 'PID_MISMATCH', error: 'daemon pid changed before shutdown request' };
444
+ }
445
+ const reason = typeof cmd.reason === 'string' && cmd.reason.trim()
446
+ ? cmd.reason.trim().slice(0, 80)
447
+ : 'ipc';
448
+ // Let the response flush before shutdown closes the IPC server/process.
449
+ setImmediate(() => {
450
+ Promise.resolve(this.shutdownExecutor?.(reason)).catch(error => {
451
+ logger.error(`[IPC] graceful shutdown failed: ${error instanceof Error ? error.message : String(error)}`);
452
+ });
453
+ });
454
+ return { ok: true, accepted: true, pid: process.pid };
455
+ }
416
456
  case 'bind.begin': {
417
457
  if (!this.bindExecutor)
418
458
  return { ok: false, error: 'bind executor not configured' };
@@ -833,6 +873,18 @@ export class IpcServer {
833
873
  }
834
874
  return await this.fsOperationExecutor(argv, sessionId, delegationToken, typeof cmd.delegationCommandHash === 'string' ? cmd.delegationCommandHash : undefined);
835
875
  }
876
+ case 'agent.op': {
877
+ if (!this.agentOperationExecutor)
878
+ return { ok: false, code: 'NOT_CONFIGURED', error: 'agent.op not configured' };
879
+ const { argv, sessionId, delegationToken } = cmd;
880
+ if (!Array.isArray(argv) || argv.some(value => typeof value !== 'string') || !sessionId) {
881
+ return { ok: false, code: 'INVALID_REQUEST', error: 'missing argv or sessionId' };
882
+ }
883
+ if (delegationToken !== undefined && typeof delegationToken !== 'string') {
884
+ return { ok: false, code: 'INVALID_DELEGATION', error: 'delegationToken must be a string' };
885
+ }
886
+ return await this.agentOperationExecutor(argv, sessionId, delegationToken, typeof cmd.delegationCommandHash === 'string' ? cmd.delegationCommandHash : undefined);
887
+ }
836
888
  case 'trigger.list':
837
889
  case 'trigger.show':
838
890
  case 'trigger.history':
@@ -944,17 +996,36 @@ export class IpcServer {
944
996
  }
945
997
  }
946
998
  case 'menu.exec': {
947
- if (!this.menuExecutor)
999
+ const request = cmd.payload && typeof cmd.payload === 'object' && !Array.isArray(cmd.payload)
1000
+ ? cmd.payload
1001
+ : { type: 'menu.invalid' };
1002
+ const flowContext = {
1003
+ source: 'ecweb-ipc',
1004
+ };
1005
+ logMenuRequestReceived(request, flowContext);
1006
+ if (!this.menuExecutor) {
1007
+ logMenuRequestCompleted(request, undefined, flowContext, {
1008
+ delivery: 'dropped',
1009
+ reason: 'menu-executor-not-configured',
1010
+ });
948
1011
  return { ok: false, error: 'menu.exec not configured' };
1012
+ }
949
1013
  try {
950
1014
  const response = await this.menuExecutor(cmd.payload, {
951
1015
  source: 'ecweb',
952
1016
  actorAid: null,
953
1017
  localDirect: true,
954
1018
  });
955
- return { ok: true, response };
1019
+ const timedResponse = withMenuProcessingTime(response, { receivedAtMono });
1020
+ logMenuRequestCompleted(request, timedResponse, flowContext, { delivery: 'returned' });
1021
+ return { ok: true, response: timedResponse };
956
1022
  }
957
1023
  catch (e) {
1024
+ logMenuRequestCompleted(request, undefined, flowContext, {
1025
+ delivery: 'failed',
1026
+ reason: 'handler-error',
1027
+ transportError: e,
1028
+ });
958
1029
  return { ok: false, error: e?.message ?? String(e) };
959
1030
  }
960
1031
  }
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Atomic write via dual rename.
2
+ * Atomic write with a hot backup.
3
3
  *
4
4
  * 写入流程(写 foo.json):
5
- * 1. 把当前 foo.json 改名为 foo.json_ (保留旧版作为热备)
6
- * 2. 把新内容写到 foo.json__ (写入完成才有完整内容)
5
+ * 1. 把新内容写到 foo.json__ (写入完成才有完整内容)
6
+ * 2. 保留当前 foo.json 为 foo.json_ (不移走正式文件)
7
7
  * 3. 把 foo.json__ rename 为 foo.json (原子切换)
8
8
  * foo.json_ 留到下次写入时被覆盖。
9
9
  *
@@ -17,8 +17,32 @@ import fs from 'fs';
17
17
  import path from 'path';
18
18
  const HOT = '_'; // foo.json_ 旧版热备
19
19
  const TMP = '__'; // foo.json__ 写入中
20
+ const HOT_BACKUP_FALLBACK_CODES = new Set(['EACCES', 'ENOSYS', 'ENOTSUP', 'EPERM', 'EXDEV']);
21
+ /** Preserve the previous version without making the live path disappear. */
22
+ function refreshHotBackup(filePath, hotPath) {
23
+ const backupTmp = `${hotPath}.${process.pid}.tmp`;
24
+ try {
25
+ fs.rmSync(backupTmp, { force: true });
26
+ try {
27
+ // A hard link is exact and avoids copying large protected state files.
28
+ fs.linkSync(filePath, backupTmp);
29
+ }
30
+ catch (error) {
31
+ const code = error.code;
32
+ if (!HOT_BACKUP_FALLBACK_CODES.has(code ?? ''))
33
+ throw error;
34
+ fs.copyFileSync(filePath, backupTmp);
35
+ }
36
+ // The live target remains untouched until the new file is ready.
37
+ fs.rmSync(hotPath, { force: true });
38
+ fs.renameSync(backupTmp, hotPath);
39
+ }
40
+ finally {
41
+ fs.rmSync(backupTmp, { force: true });
42
+ }
43
+ }
20
44
  /**
21
- * 原子写入 JSON 文件(双 rename)。dirname 不存在时会自动创建。
45
+ * 原子写入 JSON 文件(临时文件 + 热备)。dirname 不存在时会自动创建。
22
46
  *
23
47
  * @param filePath 目标文件
24
48
  * @param content 完整内容(不含末尾换行也可,由调用方控制)
@@ -27,15 +51,25 @@ export function atomicWrite(filePath, content) {
27
51
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
28
52
  const hot = filePath + HOT;
29
53
  const tmp = filePath + TMP;
30
- // 步骤 1:当前文件 hot(保留旧版作热备)
54
+ // Write the complete new version before changing either the live target or
55
+ // its backup. This keeps the live path present throughout the update, which
56
+ // matters to readers that project protected files into a sandbox.
57
+ fs.writeFileSync(tmp, content, 'utf-8');
58
+ // Preserve the old version while leaving the live target in place. A writer
59
+ // in another process may remove the target between existsSync and linkSync;
60
+ // in that case the new version still replaces it below and the old backup is
61
+ // simply unavailable for this write.
31
62
  if (fs.existsSync(filePath)) {
32
- if (fs.existsSync(hot))
33
- fs.unlinkSync(hot);
34
- fs.renameSync(filePath, hot);
63
+ try {
64
+ refreshHotBackup(filePath, hot);
65
+ }
66
+ catch (error) {
67
+ if (error.code !== 'ENOENT')
68
+ throw error;
69
+ }
35
70
  }
36
- // 步骤 2:写到 tmp
37
- fs.writeFileSync(tmp, content, 'utf-8');
38
- // 步骤 3:tmp → 目标(原子)
71
+ // The final rename atomically replaces the old target without an absent-path
72
+ // window.
39
73
  fs.renameSync(tmp, filePath);
40
74
  }
41
75
  export function atomicWriteJson(filePath, value) {
@@ -3,6 +3,32 @@ import path from 'path';
3
3
  import { getPackageRoot, resolvePaths } from '../paths.js';
4
4
  import { logger } from './logger.js';
5
5
  import { isSandboxInitializationFailure } from '../core/permission/sandbox-runtime.js';
6
+ export const CLAUDE_STARTUP_ARGUMENTS_TOO_LONG = 'claude_startup_arguments_too_long';
7
+ /** Identify a Claude process-start failure without treating ordinary tool errors as startup failures. */
8
+ export function isClaudeStartupArgumentsTooLong(error) {
9
+ const code = error && typeof error === 'object' && 'code' in error
10
+ ? String(error.code ?? '')
11
+ : '';
12
+ const message = error && typeof error === 'object' && 'message' in error
13
+ ? String(error.message ?? '')
14
+ : String(error ?? '');
15
+ const text = `${code}\n${message}`.toLowerCase();
16
+ return text.includes(CLAUDE_STARTUP_ARGUMENTS_TOO_LONG)
17
+ || text.includes('enametoolong')
18
+ || text.includes('e2big')
19
+ || text.includes('argument list too long')
20
+ || text.includes('command line too long');
21
+ }
22
+ export function createClaudeStartupArgumentsTooLongError(error) {
23
+ const wrapped = new Error(`${CLAUDE_STARTUP_ARGUMENTS_TOO_LONG}: Claude 启动参数过长,任务未执行`);
24
+ wrapped.name = 'ClaudeStartupArgumentsTooLongError';
25
+ wrapped.code = CLAUDE_STARTUP_ARGUMENTS_TOO_LONG;
26
+ // Keep the original error available to diagnostics without echoing argv,
27
+ // settings contents, or absolute paths to users.
28
+ if (error !== undefined)
29
+ wrapped.cause = error;
30
+ return wrapped;
31
+ }
6
32
  export var ErrorType;
7
33
  (function (ErrorType) {
8
34
  ErrorType["SDK_TIMEOUT"] = "sdk_timeout";
@@ -13,6 +39,7 @@ export var ErrorType;
13
39
  ErrorType["CONTEXT_TOO_LONG"] = "context_too_long";
14
40
  ErrorType["MODEL_UNAVAILABLE"] = "model_unavailable";
15
41
  ErrorType["SANDBOX_INITIALIZATION_FAILED"] = "sandbox_initialization_failed";
42
+ ErrorType["CLAUDE_STARTUP_ARGUMENTS_TOO_LONG"] = "claude_startup_arguments_too_long";
16
43
  ErrorType["UNKNOWN"] = "unknown";
17
44
  })(ErrorType || (ErrorType = {}));
18
45
  /**
@@ -251,6 +278,11 @@ function hasRetryableHttpStatus(text) {
251
278
  }
252
279
  export function classifyError(error) {
253
280
  const msg = (error?.message || '').toLowerCase();
281
+ // Startup-size failures are deterministic local failures and must win over
282
+ // dictionary rules that would otherwise turn them into generic retries.
283
+ if (isClaudeStartupArgumentsTooLong(error)) {
284
+ return ErrorType.CLAUDE_STARTUP_ARGUMENTS_TOO_LONG;
285
+ }
254
286
  // 字典优先 — 命中则直接返回
255
287
  const rule = matchErrorRule(msg);
256
288
  if (rule) {
@@ -314,6 +346,10 @@ export function isRetryableError(error) {
314
346
  return false;
315
347
  }
316
348
  export function getErrorMessage(error, terminalReason, includeEmoji = true) {
349
+ if (isClaudeStartupArgumentsTooLong(error)) {
350
+ const prefix = includeEmoji ? '⚠️ ' : '';
351
+ return `${prefix}当前 Claude 启动参数过长,任务未执行`;
352
+ }
317
353
  // terminalReason 提供更精确的错误提示(SDK 0.2.100+)
318
354
  if (terminalReason) {
319
355
  const prefix = includeEmoji ? '❌ ' : '';
@@ -335,6 +371,8 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
335
371
  return `${prefix}权限被拒绝,操作已取消`;
336
372
  case 'sandbox_initialization_failed':
337
373
  return `${warnPrefix}只读执行沙箱启动失败(sandbox_initialization_failed),当前任务未执行;请检查 user namespace/seccomp 配置后重试`;
374
+ case 'claude_startup_arguments_too_long':
375
+ return `${warnPrefix}当前 Claude 启动参数过长,任务未执行`;
338
376
  case 'aborted_streaming':
339
377
  case 'aborted_tools':
340
378
  return `${prefix}任务已中断`;
@@ -2,6 +2,7 @@ import fs from 'fs';
2
2
  import { resolvePaths } from '../paths.js';
3
3
  import { LogWriter } from './log-writer.js';
4
4
  import { classifyToolErrorCode } from '../core/permission/tool-error-code.js';
5
+ import { buildToolLifecycleEventKey } from '../core/audit/event-key.js';
5
6
  let currentLevel = process.env.LOG_LEVEL || 'INFO';
6
7
  const LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
7
8
  const config = {
@@ -14,6 +15,7 @@ function closeWriters(current) {
14
15
  current.main.close();
15
16
  current.message?.close();
16
17
  current.event?.close();
18
+ current.menu.close();
17
19
  current.channelIn.close();
18
20
  current.channelOut.close();
19
21
  }
@@ -32,6 +34,7 @@ function getWriters() {
32
34
  event: config.eventLog
33
35
  ? new LogWriter({ baseName: 'events', logDir, rotation: 'hourly', retention: { hours: 48 } })
34
36
  : null,
37
+ menu: new LogWriter({ baseName: 'menu-protocol', logDir, rotation: 'hourly', retention: { hours: 48 } }),
35
38
  channelIn: new LogWriter({ baseName: 'channel-in', logDir, rotation: 'hourly', retention: { hours: 48 } }),
36
39
  channelOut: new LogWriter({ baseName: 'channel-out', logDir, rotation: 'hourly', retention: { hours: 48 } }),
37
40
  };
@@ -48,6 +51,8 @@ export function normalizeStructuredLog(data) {
48
51
  return data;
49
52
  const nested = data.event && typeof data.event === 'object' ? data.event : undefined;
50
53
  const eventType = data.type ?? nested?.type;
54
+ const eventKey = data.eventKey ?? data.event_key ?? nested?.eventKey ?? nested?.event_key;
55
+ const eventPhase = data.eventPhase ?? data.event_phase ?? nested?.eventPhase ?? nested?.event_phase;
51
56
  const correlationId = data.correlationId
52
57
  ?? data.correlation_id
53
58
  ?? data.callId
@@ -80,6 +85,11 @@ export function normalizeStructuredLog(data) {
80
85
  ?? (isToolResult && isError === false ? 'allow' : undefined)
81
86
  ?? (isToolUse ? 'started' : undefined)
82
87
  ?? nested?.decision;
88
+ const executed = data.executed ?? nested?.executed
89
+ ?? (isToolResult ? true : isToolUse ? false : undefined);
90
+ const executionState = data.executionState ?? data.execution_state
91
+ ?? nested?.executionState ?? nested?.execution_state
92
+ ?? (isToolResult ? (isError === true ? 'failed' : 'completed') : isToolUse ? 'requested' : undefined);
83
93
  const explicitErrorCode = data.errorCode
84
94
  ?? data.error_code
85
95
  ?? nested?.errorCode
@@ -88,17 +98,29 @@ export function normalizeStructuredLog(data) {
88
98
  ? classifyToolErrorCode({ errorCode: explicitErrorCode, error, result })
89
99
  : explicitErrorCode;
90
100
  const lifecycleRecord = isToolUse || isToolResult;
101
+ const lifecycleEventKey = lifecycleRecord
102
+ ? (eventKey ?? buildToolLifecycleEventKey({
103
+ sessionId,
104
+ callId: data.callId ?? data.call_id ?? data.toolUseId ?? data.tool_use_id ?? nested?.callId ?? nested?.call_id,
105
+ correlationId: data.correlationId ?? data.correlation_id ?? nested?.correlationId ?? nested?.correlation_id,
106
+ requestId: data.requestId ?? data.request_id ?? nested?.requestId ?? nested?.request_id,
107
+ }))
108
+ : undefined;
91
109
  const missingContextFields = lifecycleRecord
92
110
  ? ['sessionId', 'agentAid', 'permissionMode'].filter(field => ({ sessionId, agentAid, permissionMode }[field] == null))
93
111
  : [];
94
112
  return {
95
113
  ...data,
114
+ ...(lifecycleEventKey ? { eventKey: lifecycleEventKey } : {}),
115
+ ...(eventPhase ? { eventPhase } : {}),
96
116
  ...(correlationId ? { correlationId } : {}),
97
117
  ...(sessionId ? { sessionId } : lifecycleRecord ? { sessionId: 'unknown' } : {}),
98
118
  ...(agentAid ? { agentAid } : lifecycleRecord ? { agentAid: 'unknown' } : {}),
99
119
  ...(permissionMode ? { permissionMode } : lifecycleRecord ? { permissionMode: 'unknown' } : {}),
100
120
  ...(toolName ? { toolName } : {}),
101
121
  ...(decision ? { decision } : {}),
122
+ ...(executed !== undefined ? { executed } : {}),
123
+ ...(executionState ? { executionState } : {}),
102
124
  ...(errorCode ? { errorCode } : {}),
103
125
  ...(missingContextFields.length > 0 ? { contextMissing: missingContextFields } : {}),
104
126
  };
@@ -144,6 +166,11 @@ export const logger = {
144
166
  return;
145
167
  writer.write(JSON.stringify({ ts: localTimestamp(), ...normalizeStructuredLog(data) }));
146
168
  },
169
+ menu: (data) => {
170
+ // Menu has a deliberately compact domain schema. Do not expand it with
171
+ // channel/tool lifecycle aliases used by the generic structured streams.
172
+ getWriters().menu.write(JSON.stringify({ ts: localTimestamp(), ...data }));
173
+ },
147
174
  channelIn: (data) => {
148
175
  getWriters().channelIn.write(JSON.stringify({ ts: localTimestamp(), ...normalizeStructuredLog(data) }));
149
176
  },