evolcore 0.0.26 → 0.0.28

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 (177) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bin/codex-managed-acl.ps1 +43 -0
  3. package/dist/agents/baseagent-availability.js +68 -0
  4. package/dist/agents/baseagent.js +10 -5
  5. package/dist/agents/claude-runner.js +123 -22
  6. package/dist/agents/codex/app-server-client.js +70 -26
  7. package/dist/agents/codex/catalog.js +7 -1
  8. package/dist/agents/codex-runner.js +363 -421
  9. package/dist/agents/ecagent-runner.js +170 -25
  10. package/dist/agents/gemini-runner.js +6 -0
  11. package/dist/agents/runner-types.js +50 -2
  12. package/dist/aun/aid/agentmd.js +73 -31
  13. package/dist/aun/aid/client.js +1 -1
  14. package/dist/aun/aid/control-aid.js +180 -1
  15. package/dist/aun/aid/identity.js +3 -3
  16. package/dist/aun/group/history.js +303 -0
  17. package/dist/aun/group/identity.js +43 -0
  18. package/dist/aun/group/index.js +5 -0
  19. package/dist/aun/{msg/group.js → group/operations.js} +161 -38
  20. package/dist/aun/{menu-client.js → menu/client.js} +13 -7
  21. package/dist/aun/menu/index.js +2 -0
  22. package/dist/aun/menu/transport.js +49 -0
  23. package/dist/aun/msg/index.js +2 -1
  24. package/dist/aun/msg/managed-operation.js +84 -18
  25. package/dist/aun/msg/mention-schema.js +51 -0
  26. package/dist/aun/msg/p2p.js +13 -8
  27. package/dist/aun/msg/upload.js +43 -88
  28. package/dist/aun/outbox.js +29 -7
  29. package/dist/aun/rpc/client.js +1 -0
  30. package/dist/aun/storage/client.js +39 -50
  31. package/dist/channels/aun.js +544 -239
  32. package/dist/channels/daemon.js +10 -6
  33. package/dist/cli/agent-command.js +108 -6
  34. package/dist/cli/agent-create.js +515 -2
  35. package/dist/cli/agent.js +109 -8
  36. package/dist/cli/aun-commands.js +191 -42
  37. package/dist/cli/config.js +8 -5
  38. package/dist/cli/contact.js +2 -2
  39. package/dist/cli/ctl-command.js +2 -2
  40. package/dist/cli/daemon-commands.js +111 -162
  41. package/dist/cli/diag.js +3 -3
  42. package/dist/cli/fs-command.js +3 -3
  43. package/dist/cli/handoff-command.js +6 -6
  44. package/dist/cli/index.js +8 -2
  45. package/dist/cli/init.js +34 -1
  46. package/dist/cli/managed-command-guard.js +2 -1
  47. package/dist/cli/menu-client.js +13 -5
  48. package/dist/cli/model.js +3 -3
  49. package/dist/cli/queue-command.js +2 -2
  50. package/dist/cli/response.js +2 -2
  51. package/dist/cli/restart-monitor.js +81 -19
  52. package/dist/cli/task-command.js +59 -0
  53. package/dist/cli/task-context.js +97 -49
  54. package/dist/cli/trigger-command.js +20 -10
  55. package/dist/config/builtin-roles.js +10 -0
  56. package/dist/config/config-field-policy.js +11 -0
  57. package/dist/config/config-manager.js +32 -4
  58. package/dist/config/config-operation-service.js +20 -5
  59. package/dist/config/contact-book-store.js +1 -1
  60. package/dist/config/contact-book.js +1 -1
  61. package/dist/config/contact-operation-service.js +1 -1
  62. package/dist/config/peer-role-resolver.js +1 -1
  63. package/dist/config/resolved-config-op.js +3 -0
  64. package/dist/config-store.js +1 -1
  65. package/dist/core/agent-application-service.js +5 -0
  66. package/dist/core/audit/event-key.js +17 -2
  67. package/dist/core/audit/log-integrity.js +34 -8
  68. package/dist/core/auth/agent-delegation.js +48 -14
  69. package/dist/core/auth/auth-gateway.js +2 -0
  70. package/dist/core/auth/authorization-audit.js +34 -38
  71. package/dist/core/auth/operation-authorizer.js +67 -5
  72. package/dist/core/auth/operation-catalog.js +110 -0
  73. package/dist/core/auth/remote-access-store.js +115 -9
  74. package/dist/core/bootstrap-messages.js +66 -7
  75. package/dist/core/bootstrap-service.js +110 -19
  76. package/dist/core/command/agent-control.js +88 -7
  77. package/dist/core/command/agent-storage-menu.js +443 -0
  78. package/dist/core/command/cli-intent-parser.js +19 -0
  79. package/dist/core/command/command-handler.js +105 -40
  80. package/dist/core/command/data-root-resolver.js +273 -0
  81. package/dist/core/command/diag-service.js +292 -27
  82. package/dist/core/command/group-menu.js +7 -2
  83. package/dist/core/command/menu-catalog.js +34 -4
  84. package/dist/core/command/menu-handler.js +364 -96
  85. package/dist/core/command/menu-protocol.js +43 -6
  86. package/dist/core/command/role-menu.js +2 -2
  87. package/dist/core/command/slash-handler.js +61 -37
  88. package/dist/core/event-catalog.js +4 -0
  89. package/dist/core/evolagent-registry.js +28 -2
  90. package/dist/core/group/invite-service.js +924 -0
  91. package/dist/core/interaction-router.js +22 -8
  92. package/dist/core/message/im-renderer.js +94 -16
  93. package/dist/core/message/message-bridge.js +140 -13
  94. package/dist/core/message/message-log.js +5 -3
  95. package/dist/core/message/message-queue.js +73 -41
  96. package/dist/core/message/response-engine.js +729 -118
  97. package/dist/core/permission/approval-gateway.js +36 -3
  98. package/dist/core/permission/ec-command-parser.js +76 -36
  99. package/dist/core/permission/protected-paths.js +134 -16
  100. package/dist/core/permission/sandbox-runtime.js +72 -3
  101. package/dist/core/permission/tool-error-code.js +15 -0
  102. package/dist/core/permission/tool-policy.js +8 -8
  103. package/dist/core/relation/peer-identity.js +57 -6
  104. package/dist/core/relation/shared-data.js +106 -0
  105. package/dist/core/role/runtime-policy.js +64 -0
  106. package/dist/core/runtime-lock.js +10 -26
  107. package/dist/core/session/runner-task-history.js +170 -0
  108. package/dist/core/session/session-manager.js +38 -6
  109. package/dist/core/session/session-mapper.js +41 -0
  110. package/dist/core/session/session-turn-coordinator.js +37 -7
  111. package/dist/core/session/session-turns.js +48 -7
  112. package/dist/core/task/task-recovery-service.js +176 -0
  113. package/dist/core/task/task-resume-operation.js +32 -0
  114. package/dist/eck/group-rules-sync.js +5 -3
  115. package/dist/eck/kit-renderer.js +1 -0
  116. package/dist/index.js +322 -207
  117. package/dist/ipc.js +141 -15
  118. package/dist/paths.js +17 -4
  119. package/dist/product.js +3 -0
  120. package/dist/trigger/event-source.js +19 -0
  121. package/dist/trigger/feedback.js +233 -40
  122. package/dist/trigger/parser.js +55 -5
  123. package/dist/trigger/patch.js +9 -0
  124. package/dist/trigger/scheduler.js +30 -4
  125. package/dist/trigger/validation.js +20 -3
  126. package/dist/utils/atomic-write.js +98 -0
  127. package/dist/utils/cross-platform.js +87 -16
  128. package/dist/utils/ecweb-utils.js +1 -0
  129. package/dist/utils/error-utils.js +40 -2
  130. package/dist/utils/logger.js +23 -13
  131. package/dist/utils/npm-ops.js +119 -4
  132. package/dist/utils/package-version-info.js +244 -0
  133. package/dist/utils/tool-summary.js +59 -3
  134. package/dist/utils/welcome.js +174 -1
  135. package/dist/utils/windows-shell-trust.js +37 -0
  136. package/ecagent/dist/harness/agent-harness.js +3 -0
  137. package/ecagent/dist/harness/session/jsonl-storage.js +21 -2
  138. package/kits/docs/INDEX.md +0 -6
  139. package/kits/docs/channels/aun.md +23 -2
  140. package/kits/docs/channels/feishu.md +1 -1
  141. package/kits/docs/context-assembly.md +29 -17
  142. package/kits/docs/evolcore/INDEX.md +8 -3
  143. package/kits/docs/evolcore/agent-create.md +39 -7
  144. package/kits/docs/evolcore/agent.md +33 -1
  145. package/kits/docs/evolcore/aid.md +9 -1
  146. package/kits/docs/evolcore/config.md +40 -1
  147. package/kits/docs/evolcore/group-collaboration.md +1 -1
  148. package/kits/docs/evolcore/group.md +117 -2
  149. package/kits/docs/evolcore/menu-client.md +16 -0
  150. package/kits/docs/evolcore/msg.md +3 -0
  151. package/kits/docs/evolcore/storage.md +3 -0
  152. package/kits/docs/evolcore/task.md +14 -0
  153. package/kits/docs/evolcore/trigger.md +51 -3
  154. package/kits/docs/venues/group.md +5 -0
  155. package/kits/eck_manifest.json +27 -0
  156. package/kits/rules/01-overview.md +11 -7
  157. package/kits/rules/04-relation.md +2 -1
  158. package/kits/rules/05-venue.md +1 -1
  159. package/kits/rules/06-channel.md +6 -4
  160. package/kits/schemas/agent-config.schema.12.json +25 -1
  161. package/kits/schemas/daemon.schema.6.json +2 -2
  162. package/kits/schemas/defaults.schema.5.json +1 -1
  163. package/kits/schemas/relation-config.schema.1.json +1 -1
  164. package/kits/schemas/relation-config.schema.5.json +1 -1
  165. package/kits/schemas/role-config.schema.1.json +14 -0
  166. package/kits/templates/roles/member.json +5 -0
  167. package/kits/templates/roles/visitor.json +5 -0
  168. package/kits/templates/system-fragments/baseagent.md +1 -1
  169. package/kits/templates/system-fragments/bootstrap.md +26 -9
  170. package/kits/templates/system-fragments/channel.md +8 -44
  171. package/kits/templates/system-fragments/commands.md +10 -2
  172. package/kits/templates/system-fragments/group-unread-history.md +7 -0
  173. package/kits/templates/system-fragments/reply.md +56 -0
  174. package/package.json +7 -3
  175. package/dist/aun/group-identity.js +0 -9
  176. /package/dist/aun/{group-fs-download.js → group/fs-download.js} +0 -0
  177. /package/dist/aun/{msg/group-index.js → group/index-rpc.js} +0 -0
@@ -3,7 +3,7 @@ import fs from 'fs';
3
3
  import { CronExpressionParser } from 'cron-parser';
4
4
  import { logger } from '../utils/logger.js';
5
5
  import { matchTriggerEvent, TriggerEventSource } from './event-source.js';
6
- import { definitionRevision, parseDurationMs, previewText, renderTemplate, resolveScriptPath, sha256, validateConcurrencyCombination, } from './validation.js';
6
+ import { definitionRevision, executionTimeoutMs, parseDurationMs, previewText, renderTemplate, resolveScriptPath, sha256, validateConcurrencyCombination, } from './validation.js';
7
7
  import { TargetSessionLockRegistry } from './session-lock.js';
8
8
  import { enterTrigger, formatTriggerPath } from '../core/causation/context.js';
9
9
  import { createRootCausation } from '../core/causation/context.js';
@@ -13,6 +13,13 @@ const MAX_TIMER_MS = 2_147_483_647;
13
13
  const RUN_ABORT_SETTLE_GRACE_MS = 1_000;
14
14
  const RUN_COMPLETION_BARRIER_MS = 1_500;
15
15
  const TRIGGER_RESTART_RESUME_PROMPT = 'EvolCore 服务已重启。请基于当前会话记录继续完成刚才被中断的 Trigger 任务;不要重新从头执行已经完成的步骤。';
16
+ function eventSessionKey(payload, fallbackAgentAid) {
17
+ const agentAid = typeof payload.agentAid === 'string' && payload.agentAid.trim()
18
+ ? payload.agentAid.trim()
19
+ : fallbackAgentAid;
20
+ const sessionId = typeof payload.sessionId === 'string' ? payload.sessionId : '';
21
+ return `${agentAid}:${sessionId}`;
22
+ }
16
23
  export class TriggerRuntimeScheduler {
17
24
  manager;
18
25
  state;
@@ -818,6 +825,7 @@ export class TriggerRuntimeScheduler {
818
825
  attemptId: `${runId}:attempt:${attempt}`,
819
826
  firedAt,
820
827
  prompt,
828
+ sourcePayload,
821
829
  dryRun,
822
830
  causation,
823
831
  signal,
@@ -869,7 +877,17 @@ export class TriggerRuntimeScheduler {
869
877
  + ` model=${definition.execution.model ?? '<agent-default>'}`
870
878
  + ` effort=${definition.execution.effort ?? '<agent-default>'}`);
871
879
  const attemptId = `${runId}:attempt:${attempt}`;
872
- const executionDeadlineAt = this.daemonExecutionDeadlineAt(Date.now());
880
+ const executionStartedAt = Date.now();
881
+ const configuredExecutionTimeout = executionTimeoutMs(definition.execution);
882
+ const daemonExecutionDeadlineAt = this.daemonExecutionDeadlineAt(executionStartedAt);
883
+ const configuredExecutionDeadlineAt = configuredExecutionTimeout === undefined
884
+ ? undefined
885
+ : executionStartedAt + configuredExecutionTimeout;
886
+ const executionDeadlineAt = daemonExecutionDeadlineAt === undefined
887
+ ? configuredExecutionDeadlineAt
888
+ : configuredExecutionDeadlineAt === undefined
889
+ ? daemonExecutionDeadlineAt
890
+ : Math.min(daemonExecutionDeadlineAt, configuredExecutionDeadlineAt);
873
891
  const executionSession = await this.daemonChannel.getOrCreateConversationSession(definition, this.runtime.projectPath, runtimeBaseagent, runId, attemptId);
874
892
  const activeRun = this.state.startExecutionAttempt?.(definition.id, runId, {
875
893
  attempt,
@@ -963,7 +981,13 @@ export class TriggerRuntimeScheduler {
963
981
  });
964
982
  }
965
983
  async checkConcurrency(definition, nextRunId, source) {
966
- const runs = this.running.get(definition.id);
984
+ const allRuns = this.running.get(definition.id);
985
+ // An event-session trigger owns many independent sessions. Forbid/replace
986
+ // applies within the source session, so one failure cannot suppress another.
987
+ const runs = definition.feedback.strategy === 'event_session' && allRuns
988
+ ? new Map([...allRuns].filter(([, runtime]) => eventSessionKey(runtime.source.payload, definition.agentAid)
989
+ === eventSessionKey(source.payload, definition.agentAid)))
990
+ : allRuns;
967
991
  if (!runs || runs.size === 0 || definition.reliability.concurrency === 'allow')
968
992
  return undefined;
969
993
  const first = runs.values().next().value;
@@ -1753,7 +1777,7 @@ export class TriggerRuntimeScheduler {
1753
1777
  publishTriggerRunOutcome(definition, audit) {
1754
1778
  if (!this.eventBus || audit.status === 'dry-run')
1755
1779
  return;
1756
- const target = primaryTarget(definition);
1780
+ const target = audit.feedback?.target ?? primaryTarget(definition);
1757
1781
  const targetChannel = target?.channelKey ?? 'daemon';
1758
1782
  const targetChannelId = target?.channelId ?? definition.agentAid;
1759
1783
  const base = {
@@ -2027,6 +2051,8 @@ function summarizeReply(reply, opts = {}) {
2027
2051
  function primaryTarget(definition) {
2028
2052
  if (definition.feedback.strategy === 'target')
2029
2053
  return definition.feedback.target;
2054
+ if (definition.feedback.strategy === 'event_session')
2055
+ return undefined;
2030
2056
  if (!definition.origin)
2031
2057
  return undefined;
2032
2058
  return {
@@ -3,6 +3,8 @@ import crypto from 'crypto';
3
3
  import { normalizePermissionMode as normalizePermissionModeContract } from '../core/permission/mode.js';
4
4
  import { formatChannelKey } from '../core/channel-loader.js';
5
5
  const MAX_SCRIPT_TIMEOUT_MS = 900_000;
6
+ const TRIGGER_SESSION_TIMEOUT_MS = 30 * 60_000;
7
+ const MAX_EXECUTION_TIMEOUT_MS = 2_147_483_647;
6
8
  export const TRIGGER_SCHEMA_VERSION = 3.1;
7
9
  const LEGACY_TRIGGER_SCHEMA_VERSION = 4;
8
10
  const DEFAULT_NOOP_SENTINEL = '[[NOOP]]';
@@ -35,7 +37,7 @@ export function normalizeTriggerDefinition(input, opts = {}) {
35
37
  * channelId shape inference at runtime.
36
38
  */
37
39
  export function assertExplicitAunDeliveryRoutes(definition) {
38
- if (definition.feedback.strategy === 'silent')
40
+ if (definition.feedback.strategy === 'silent' || definition.feedback.strategy === 'event_session')
39
41
  return;
40
42
  const target = definition.feedback.strategy === 'target'
41
43
  ? definition.feedback.target
@@ -58,6 +60,11 @@ export function assertExplicitAunDeliveryRoutes(definition) {
58
60
  export function validateTriggerDefinition(definition) {
59
61
  normalizeTriggerDefinition(definition);
60
62
  }
63
+ /** Resolve a model-backed attempt's deadline; target sessions are unlimited by default. */
64
+ export function executionTimeoutMs(execution) {
65
+ return execution.timeoutMs ?? (execution.type === 'target_session'
66
+ ? undefined : TRIGGER_SESSION_TIMEOUT_MS);
67
+ }
61
68
  /**
62
69
  * Keep legacy `per_run` definitions byte-stable while giving runtime code one
63
70
  * canonical representation for session selection and validation.
@@ -428,8 +435,14 @@ function normalizeExecution(value, _opts) {
428
435
  throw new Error('execution.type must be script, trigger_session, or target_session');
429
436
  }
430
437
  const thread = normalizeExecutionThread(raw.thread);
438
+ if (raw.timeoutMs !== undefined && (type === 'script'
439
+ || typeof raw.timeoutMs !== 'number' || !Number.isSafeInteger(raw.timeoutMs)
440
+ || raw.timeoutMs <= 0 || raw.timeoutMs > MAX_EXECUTION_TIMEOUT_MS)) {
441
+ throw new Error('execution.timeoutMs must be an integer between 1 and 2147483647 for a model-backed Trigger');
442
+ }
431
443
  const execution = {
432
444
  type,
445
+ ...(raw.timeoutMs !== undefined ? { timeoutMs: raw.timeoutMs } : {}),
433
446
  prompt: type === 'script' ? optionalString(raw.prompt) : requiredString(raw.prompt, 'execution.prompt'),
434
447
  script: type === 'script' ? normalizeScript(raw.script) : undefined,
435
448
  thread: type === 'trigger_session' ? (thread ?? 'by_trigger') : thread,
@@ -511,8 +524,8 @@ function normalizeFeedback(value, agentAid) {
511
524
  throw new Error('feedback must be an object');
512
525
  const raw = value;
513
526
  const strategy = requiredString(raw.strategy, 'feedback.strategy');
514
- if (strategy !== 'origin' && strategy !== 'target' && strategy !== 'silent') {
515
- throw new Error('feedback.strategy must be origin, target, or silent');
527
+ if (strategy !== 'origin' && strategy !== 'target' && strategy !== 'event_session' && strategy !== 'silent') {
528
+ throw new Error('feedback.strategy must be origin, target, event_session, or silent');
516
529
  }
517
530
  return {
518
531
  strategy,
@@ -630,6 +643,10 @@ function normalizeOnError(value) {
630
643
  }
631
644
  function validateTriggerSemantics(definition) {
632
645
  const execution = definition.execution;
646
+ if (definition.feedback.strategy === 'event_session'
647
+ && (definition.source.type !== 'event' || execution.type !== 'target_session')) {
648
+ throw new Error('feedback.strategy=event_session requires an event source and target_session execution');
649
+ }
633
650
  if (execution.type === 'script') {
634
651
  if (!execution.script)
635
652
  throw new Error('execution.script is required when execution.type=script');
@@ -15,6 +15,9 @@
15
15
  */
16
16
  import fs from 'fs';
17
17
  import path from 'path';
18
+ import os from 'os';
19
+ import { randomUUID } from 'crypto';
20
+ import { isProcessRunning } from './cross-platform.js';
18
21
  const HOT = '_'; // foo.json_ 旧版热备
19
22
  const TMP = '__'; // foo.json__ 写入中
20
23
  const HOT_BACKUP_FALLBACK_CODES = new Set(['EACCES', 'ENOSYS', 'ENOTSUP', 'EPERM', 'EXDEV']);
@@ -178,3 +181,98 @@ export function atomicWriteBytes(filePath, content) {
178
181
  fs.writeFileSync(tmp, content);
179
182
  fs.renameSync(tmp, filePath);
180
183
  }
184
+ const pauseFileWrite = (ms) => new Promise(resolve => setTimeout(resolve, ms));
185
+ /** Cross-process lock for package state. A complete owner file is linked into place atomically. */
186
+ export async function withFileLock(file, action, timeoutMs = 30000) {
187
+ fs.mkdirSync(path.dirname(file), { recursive: true });
188
+ const token = randomUUID();
189
+ const candidate = `${file}.${process.pid}.${token}.candidate`;
190
+ const owner = JSON.stringify({ token, pid: process.pid, host: os.hostname() });
191
+ fs.writeFileSync(candidate, owner, { flag: 'wx', mode: 0o600 });
192
+ const deadline = Date.now() + timeoutMs;
193
+ try {
194
+ while (true) {
195
+ try {
196
+ fs.linkSync(candidate, file);
197
+ break;
198
+ }
199
+ catch (error) {
200
+ if (error.code !== 'EEXIST')
201
+ throw error;
202
+ }
203
+ try {
204
+ const raw = fs.readFileSync(file, 'utf8');
205
+ const existing = JSON.parse(raw);
206
+ if (typeof existing.token === 'string' && /^[a-f0-9-]{36}$/.test(existing.token)
207
+ && existing.host === os.hostname() && existing.pid !== process.pid
208
+ && Number.isSafeInteger(existing.pid) && existing.pid > 0 && !isProcessRunning(existing.pid)
209
+ && fs.readFileSync(file, 'utf8') === raw) {
210
+ fs.unlinkSync(file);
211
+ continue;
212
+ }
213
+ }
214
+ catch { /* Unknown or concurrently released owner: wait. */ }
215
+ if (Date.now() >= deadline)
216
+ throw new Error('Package state lock timed out');
217
+ await pauseFileWrite(40 + Math.floor(Math.random() * 20));
218
+ }
219
+ const lockIdentity = fs.statSync(file);
220
+ try {
221
+ return await action();
222
+ }
223
+ finally {
224
+ const releaseDeadline = Date.now() + 5000;
225
+ while (true) {
226
+ try {
227
+ const current = fs.statSync(file);
228
+ if (current.dev !== lockIdentity.dev || current.ino !== lockIdentity.ino) {
229
+ throw new Error('Package lock ownership changed');
230
+ }
231
+ fs.unlinkSync(file);
232
+ break;
233
+ }
234
+ catch (error) {
235
+ if (error.code === 'ENOENT')
236
+ break;
237
+ if (!['EPERM', 'EACCES', 'EBUSY'].includes(error.code ?? '')
238
+ || Date.now() >= releaseDeadline)
239
+ throw error;
240
+ await pauseFileWrite(50);
241
+ }
242
+ }
243
+ }
244
+ }
245
+ finally {
246
+ fs.rmSync(candidate, { force: true });
247
+ }
248
+ }
249
+ /** Atomic replacement without the in-place Windows fallback used by protected config files. */
250
+ export async function atomicReplaceJson(file, value) {
251
+ const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
252
+ try {
253
+ const fd = fs.openSync(temporary, 'wx', 0o600);
254
+ try {
255
+ fs.writeFileSync(fd, JSON.stringify(value, null, 2) + '\n', 'utf8');
256
+ fs.fsyncSync(fd);
257
+ }
258
+ finally {
259
+ fs.closeSync(fd);
260
+ }
261
+ const deadline = Date.now() + 5000;
262
+ for (let attempt = 0;; attempt++) {
263
+ try {
264
+ fs.renameSync(temporary, file);
265
+ return;
266
+ }
267
+ catch (error) {
268
+ if (!['EPERM', 'EACCES', 'EBUSY'].includes(error.code ?? '')
269
+ || Date.now() >= deadline)
270
+ throw error;
271
+ await pauseFileWrite(Math.min(200, 25 * (attempt + 1)) + Math.floor(Math.random() * 25));
272
+ }
273
+ }
274
+ }
275
+ finally {
276
+ fs.rmSync(temporary, { force: true });
277
+ }
278
+ }
@@ -6,6 +6,7 @@ import { promisify } from 'util';
6
6
  import fs from 'fs';
7
7
  import { getProcessStartTime, parseCimDate } from './process-introspect.js';
8
8
  import { decodeWindowsOutput } from './windows-output.js';
9
+ import { resolveTrustedWindowsCmdPath } from './windows-shell-trust.js';
9
10
  const execFileAsync = promisify(execFile);
10
11
  const CLI_PROBE_FAILURE_BACKOFF_MS = 5_000;
11
12
  const CLI_PROBE_PACKAGES = {
@@ -17,9 +18,70 @@ const cliPathCache = new Map();
17
18
  const cliInstallationCache = new Map();
18
19
  const cliProbeCache = new Map();
19
20
  export const isWindows = process.platform === 'win32';
21
+ // Keep this in sync with the Windows command-line metacharacters. The whole
22
+ // `/c` expression is passed as one verbatim argument so paths with spaces and
23
+ // shim-forwarded values such as `%`, `!`, `&`, and quotes stay data.
24
+ const WINDOWS_CMD_META_RE = /([()[\]%!^"`<>&|;, *?])/g;
25
+ function escapeWindowsCmdCommand(value) {
26
+ return value.replace(WINDOWS_CMD_META_RE, '^$1');
27
+ }
28
+ function escapeWindowsCmdArgument(value) {
29
+ let escaped = String(value);
30
+ escaped = escaped.replace(/(?=(\\+?)?)\1"/g, '$1$1\\"');
31
+ escaped = escaped.replace(/(?=(\\+?)?)\1$/g, '$1$1');
32
+ escaped = `"${escaped}"`;
33
+ return escaped.replace(WINDOWS_CMD_META_RE, '^$1');
34
+ }
35
+ /** Build one argv-based CLI launch without consulting the parent terminal. */
36
+ export function buildCliLaunchCommand(executable, args, options = {}) {
37
+ const windows = options.isWindows ?? isWindows;
38
+ const extension = windows ? path.win32.extname(executable).toLowerCase() : '';
39
+ const shim = extension === '.cmd' || extension === '.bat';
40
+ if (!shim) {
41
+ return {
42
+ command: executable,
43
+ args,
44
+ resolvedPath: executable,
45
+ launcherKind: 'native-exe',
46
+ launcherPath: executable,
47
+ };
48
+ }
49
+ const resolveWindowsCmd = options.resolveWindowsCmd ?? resolveTrustedWindowsCmdPath;
50
+ const launcherPath = resolveWindowsCmd(options.comSpec);
51
+ if (!launcherPath) {
52
+ throw Object.assign(new Error('未找到受信任的系统 cmd.exe,已拒绝启动 Windows CLI shim'), {
53
+ code: 'EACCES',
54
+ path: executable,
55
+ });
56
+ }
57
+ const shellCommand = [
58
+ escapeWindowsCmdCommand(executable),
59
+ ...args.map(escapeWindowsCmdArgument),
60
+ ].join(' ');
61
+ return {
62
+ command: launcherPath,
63
+ args: ['/d', '/s', '/v:off', '/c', `"${shellCommand}"`],
64
+ resolvedPath: executable,
65
+ launcherKind: extension === '.cmd' ? 'cmd-shim' : 'bat-shim',
66
+ launcherPath,
67
+ windowsVerbatimArguments: true,
68
+ };
69
+ }
20
70
  /** Resolve a CLI executable without spawning where/which. */
21
71
  export function resolveProbePath(command) {
22
- const pathKey = `${command}\0${process.cwd()}\0${process.env.PATH ?? ''}\0${process.env.PATHEXT ?? ''}`;
72
+ const extensions = isWindows
73
+ ? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(ext => /\.(com|exe|bat|cmd)$/i.test(ext))
74
+ : [''];
75
+ return resolveProbePathWithExtensions(command, extensions, 'executable');
76
+ }
77
+ /** Diagnose an incomplete npm installation without ever executing its PS1 shim. */
78
+ export function resolvePowerShellShimPath(command) {
79
+ if (!isWindows)
80
+ return null;
81
+ return resolveProbePathWithExtensions(command, ['.ps1'], 'powershell-shim');
82
+ }
83
+ function resolveProbePathWithExtensions(command, extensions, cacheKind) {
84
+ const pathKey = `${cacheKind}\0${command}\0${process.cwd()}\0${process.env.PATH ?? ''}\0${extensions.join(';')}`;
23
85
  const cachedPath = cliPathCache.get(pathKey);
24
86
  if (cachedPath && fs.existsSync(cachedPath))
25
87
  return cachedPath;
@@ -27,9 +89,6 @@ export function resolveProbePath(command) {
27
89
  const directories = (process.env.PATH ?? '').split(path.delimiter);
28
90
  if (isWindows)
29
91
  directories.unshift(process.cwd());
30
- const extensions = isWindows
31
- ? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(ext => /\.(com|exe|bat|cmd)$/i.test(ext))
32
- : [''];
33
92
  for (const directory of directories) {
34
93
  for (const extension of extensions) {
35
94
  const candidate = path.resolve(directory.replace(/^"|"$/g, ''), command + extension);
@@ -98,20 +157,25 @@ export function invalidateCliProbes() {
98
157
  function cliProbeTimeouts(command, options) {
99
158
  return options.timeouts?.length ? options.timeouts : command === 'codex' ? [3_000, 15_000] : [3_000];
100
159
  }
101
- function prepareCliProbe(command, args) {
102
- const executable = resolveProbePath(command);
160
+ function prepareCliProbe(command, args, options = {}) {
161
+ const executable = options.executable ?? resolveProbePath(command);
103
162
  if (!executable)
104
163
  throw Object.assign(new Error(`${command} CLI not found`), { code: 'ENOENT' });
105
- const identity = `${probeInstallationIdentity(command, executable)}|${process.env.ComSpec ?? ''}`;
164
+ const launchCommand = buildCliLaunchCommand(executable, args);
165
+ const launch = launchCommand;
166
+ const identity = `${probeInstallationIdentity(command, executable)}|${cliFileIdentity(launch.launcherPath)}`;
106
167
  const key = JSON.stringify([command, args]);
107
168
  let entry = cliProbeCache.get(key);
108
169
  if (!entry || entry.identity !== identity || (entry.retryAt !== undefined && Date.now() >= entry.retryAt)) {
109
170
  entry = { identity };
110
171
  cliProbeCache.set(key, entry);
111
172
  }
112
- const shim = isWindows && /\.(cmd|bat)$/i.test(executable);
113
- return { entry, command: shim ? process.env.ComSpec ?? 'cmd.exe' : executable,
114
- args: shim ? ['/d', '/s', '/c', executable, ...args] : args };
173
+ return {
174
+ entry,
175
+ command: launchCommand.command,
176
+ args: launchCommand.args,
177
+ launch,
178
+ };
115
179
  }
116
180
  function failCliProbe(entry, error) {
117
181
  entry.error = error;
@@ -119,7 +183,7 @@ function failCliProbe(entry, error) {
119
183
  throw error;
120
184
  }
121
185
  export function probeCliSync(command, args, options = {}) {
122
- const request = prepareCliProbe(command, args);
186
+ const request = prepareCliProbe(command, args, options);
123
187
  if (request.entry.output !== undefined)
124
188
  return request.entry.output;
125
189
  if (request.entry.error !== undefined)
@@ -127,10 +191,14 @@ export function probeCliSync(command, args, options = {}) {
127
191
  for (const [index, timeout] of cliProbeTimeouts(command, options).entries()) {
128
192
  const startedAt = Date.now();
129
193
  try {
130
- return request.entry.output = execFileSync(request.command, request.args, { encoding: 'utf8', timeout, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
194
+ return request.entry.output = execFileSync(request.command, request.args, {
195
+ encoding: 'utf8', timeout, windowsHide: true,
196
+ windowsVerbatimArguments: request.launch.windowsVerbatimArguments,
197
+ stdio: ['ignore', 'pipe', 'pipe'],
198
+ });
131
199
  }
132
200
  catch (error) {
133
- options.onFailure?.(args, startedAt, error, timeout);
201
+ options.onFailure?.(args, startedAt, error, timeout, request.launch);
134
202
  if (error.code !== 'ETIMEDOUT' || index === cliProbeTimeouts(command, options).length - 1)
135
203
  return failCliProbe(request.entry, error);
136
204
  }
@@ -138,7 +206,7 @@ export function probeCliSync(command, args, options = {}) {
138
206
  throw new Error('CLI probe exhausted');
139
207
  }
140
208
  export async function probeCli(command, args, options = {}) {
141
- const request = prepareCliProbe(command, args);
209
+ const request = prepareCliProbe(command, args, options);
142
210
  if (request.entry.output !== undefined)
143
211
  return request.entry.output;
144
212
  if (request.entry.error !== undefined)
@@ -151,7 +219,10 @@ export async function probeCli(command, args, options = {}) {
151
219
  const startedAt = Date.now();
152
220
  try {
153
221
  const output = await new Promise((resolve, reject) => {
154
- const child = execFile(request.command, request.args, { encoding: 'utf8', timeout, windowsHide: true }, (error, stdout, stderr) => {
222
+ const child = execFile(request.command, request.args, {
223
+ encoding: 'utf8', timeout, windowsHide: true,
224
+ windowsVerbatimArguments: request.launch.windowsVerbatimArguments,
225
+ }, (error, stdout, stderr) => {
155
226
  if (error) {
156
227
  if (error.killed && error.signal === 'SIGTERM' && error.code == null)
157
228
  error.code = 'ETIMEDOUT';
@@ -166,7 +237,7 @@ export async function probeCli(command, args, options = {}) {
166
237
  return request.entry.output = output;
167
238
  }
168
239
  catch (error) {
169
- options.onFailure?.(args, startedAt, error, timeout);
240
+ options.onFailure?.(args, startedAt, error, timeout, request.launch);
170
241
  if (error.code !== 'ETIMEDOUT' || index === timeouts.length - 1)
171
242
  return failCliProbe(request.entry, error);
172
243
  }
@@ -70,6 +70,7 @@ export async function fetchRemotePairingCode(authorizedBy) {
70
70
  try {
71
71
  const response = await ipcQuery(resolvePaths().socket, {
72
72
  type: 'remote-auth.pair-code',
73
+ source: 'host-cli',
73
74
  ...(authorizedBy?.trim() ? { authorizedBy: authorizedBy.trim() } : {}),
74
75
  }, 2_000);
75
76
  if (!response?.ok || typeof response.code !== 'string' || typeof response.expiresAt !== 'number')
@@ -3,6 +3,27 @@ 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
+ /** A deadline imposed by an outer executor, handled by the ordinary task timeout path. */
7
+ export class ExecutionTimeoutError extends Error {
8
+ reason;
9
+ timeoutMs;
10
+ constructor(reason, timeoutMs) {
11
+ super('TOTAL_EXECUTION_TIMEOUT');
12
+ this.reason = reason;
13
+ this.timeoutMs = timeoutMs;
14
+ this.name = 'ExecutionTimeoutError';
15
+ }
16
+ }
17
+ export class OrphanedTargetSessionError extends Error {
18
+ reason = 'orphaned_target_session';
19
+ constructor() {
20
+ super('ORPHANED_TARGET_SESSION');
21
+ this.name = 'OrphanedTargetSessionError';
22
+ }
23
+ }
24
+ export function isExecutionFailure(error) {
25
+ return error instanceof ExecutionTimeoutError || error instanceof OrphanedTargetSessionError;
26
+ }
6
27
  export const CLAUDE_STARTUP_ARGUMENTS_TOO_LONG = 'claude_startup_arguments_too_long';
7
28
  /** A completed provider response exhausted its output budget, rather than a transport failure. */
8
29
  export function isOutputTokenLimitError(error) {
@@ -288,7 +309,15 @@ function hasRetryableHttpStatus(text) {
288
309
  return RETRYABLE_HTTP_STATUS_PATTERN.test(text);
289
310
  }
290
311
  export function classifyError(error) {
312
+ if (error?.code === 'RUNNER_INTERRUPT_UNCONFIRMED')
313
+ return ErrorType.SESSION_UNAVAILABLE;
291
314
  const msg = (error?.message || '').toLowerCase();
315
+ // A damaged ecagent JSONL transcript is deterministic local state. Retrying
316
+ // the provider request cannot repair it and can append more entries to the
317
+ // same unusable file.
318
+ if (msg.includes('invalid jsonl session file') || msg.includes('invalid session file')) {
319
+ return ErrorType.SESSION_UNAVAILABLE;
320
+ }
292
321
  // The broad dictionary rule for "stream" must not hide a provider budget limit.
293
322
  if (isOutputTokenLimitError(error))
294
323
  return ErrorType.MAX_OUTPUT_TOKENS;
@@ -346,11 +375,15 @@ export function classifyError(error) {
346
375
  * 注意:401 认证错误不可重试(API Key 无效不会因重试恢复)
347
376
  */
348
377
  export function isRetryableError(error) {
378
+ if (error?.code === 'RUNNER_INTERRUPT_UNCONFIRMED')
379
+ return false;
349
380
  // Replaying the same request does not increase its output budget.
350
381
  if (isOutputTokenLimitError(error))
351
382
  return false;
352
383
  const msg = error?.message || String(error);
353
384
  const lower = msg.toLowerCase();
385
+ if (lower.includes('invalid jsonl session file') || lower.includes('invalid session file'))
386
+ return false;
354
387
  // 字典优先 — 命中则直接返回
355
388
  const rule = matchErrorRule(lower);
356
389
  if (rule)
@@ -366,6 +399,8 @@ export function isRetryableError(error) {
366
399
  return false;
367
400
  }
368
401
  export function getErrorMessage(error, terminalReason, includeEmoji = true) {
402
+ if (error?.code === 'RUNNER_INTERRUPT_UNCONFIRMED')
403
+ return `${includeEmoji ? '⚠️ ' : ''}${error.message}`;
369
404
  if (terminalReason === 'max_output_tokens' || isOutputTokenLimitError(error)) {
370
405
  const prefix = includeEmoji ? '⚠️ ' : '';
371
406
  return `${prefix}模型响应达到输出上限(max_output_tokens),本轮未完成。`
@@ -395,7 +430,7 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
395
430
  case 'permission_denied':
396
431
  return `${prefix}权限被拒绝,操作已取消`;
397
432
  case 'sandbox_initialization_failed':
398
- return `${warnPrefix}只读执行沙箱启动失败(sandbox_initialization_failed),当前任务未执行;请检查 user namespace/seccomp 配置后重试`;
433
+ return `${warnPrefix}执行沙箱启动失败(sandbox_initialization_failed),当前操作未执行;请检查文件系统挂载规则及 user namespace/seccomp 配置`;
399
434
  case 'claude_startup_arguments_too_long':
400
435
  return `${warnPrefix}当前 Claude 启动参数过长,任务未执行`;
401
436
  case 'aborted_streaming':
@@ -407,6 +442,9 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
407
442
  const msg = error?.message || String(error);
408
443
  const warnPrefix = includeEmoji ? '⚠️ ' : '';
409
444
  const errPrefix = includeEmoji ? '❌ ' : '';
445
+ if (/invalid (?:jsonl )?session file/i.test(msg)) {
446
+ return `${errPrefix}ecagent 会话文件损坏,已停止自动重试;请使用 /repair 或 /new 恢复会话`;
447
+ }
410
448
  // Codex app-server reports this when another app-server still owns the
411
449
  // thread writer. Surface the handoff condition instead of hiding it behind
412
450
  // the generic "processing failed" message.
@@ -429,7 +467,7 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
429
467
  : `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
430
468
  }
431
469
  if (isSandboxInitializationFailure(error)) {
432
- return `${warnPrefix}只读执行沙箱启动失败(sandbox_initialization_failed),当前任务未执行;请检查 user namespace/seccomp 配置后重试`;
470
+ return `${warnPrefix}执行沙箱启动失败(sandbox_initialization_failed),当前操作未执行;请检查文件系统挂载规则及 user namespace/seccomp 配置`;
433
471
  }
434
472
  if (msg.includes('401') || msg.includes('authentication_error') || msg.toLowerCase().includes('unauthorized')) {
435
473
  return `${errPrefix}API Key 无效,请检查密钥配置。使用 /status 查看当前配置`;
@@ -3,7 +3,7 @@ import { resolvePaths } from '../paths.js';
3
3
  import { LogWriter } from './log-writer.js';
4
4
  import { classifyToolErrorCode, normalizeToolErrorCode } from '../core/permission/tool-error-code.js';
5
5
  import { normalizePermissionMode } from '../core/permission/mode.js';
6
- import { buildToolLifecycleEventKey } from '../core/audit/event-key.js';
6
+ import { buildToolLifecycleEventKey, normalizeToolCorrelation } from '../core/audit/event-key.js';
7
7
  let currentLevel = process.env.LOG_LEVEL || 'INFO';
8
8
  const LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
9
9
  const config = {
@@ -53,8 +53,18 @@ export function normalizeStructuredLog(data) {
53
53
  const nested = data.event && typeof data.event === 'object' ? data.event : undefined;
54
54
  const eventType = data.type ?? nested?.type;
55
55
  const eventKey = data.eventKey ?? data.event_key ?? nested?.eventKey ?? nested?.event_key;
56
- const eventPhase = data.eventPhase ?? data.event_phase ?? nested?.eventPhase ?? nested?.event_phase;
57
- const correlationId = data.correlationId
56
+ const isToolResult = eventType === 'tool:result' || eventType === 'tool_result';
57
+ const isToolUse = eventType === 'tool:use' || eventType === 'tool_use';
58
+ const lifecycleRecord = isToolUse || isToolResult;
59
+ const eventPhase = data.eventPhase ?? data.event_phase ?? nested?.eventPhase ?? nested?.event_phase
60
+ ?? (isToolUse ? 'use' : isToolResult ? 'result' : undefined);
61
+ const toolCorrelation = normalizeToolCorrelation({
62
+ callId: data.callId ?? data.call_id ?? data.toolUseId ?? data.tool_use_id
63
+ ?? nested?.callId ?? nested?.call_id ?? nested?.toolUseId ?? nested?.tool_use_id,
64
+ correlationId: data.correlationId ?? data.correlation_id ?? nested?.correlationId ?? nested?.correlation_id,
65
+ requestId: data.requestId ?? data.request_id ?? nested?.requestId ?? nested?.request_id,
66
+ });
67
+ const correlationId = lifecycleRecord ? toolCorrelation.correlationId : data.correlationId
58
68
  ?? data.correlation_id
59
69
  ?? data.callId
60
70
  ?? data.call_id
@@ -69,7 +79,9 @@ export function normalizeStructuredLog(data) {
69
79
  ?? nested?.callId
70
80
  ?? nested?.call_id
71
81
  ?? nested?.toolUseId
72
- ?? nested?.tool_use_id;
82
+ ?? nested?.tool_use_id
83
+ ?? nested?.requestId
84
+ ?? nested?.request_id;
73
85
  const sessionId = data.sessionId ?? data.session_id ?? nested?.sessionId ?? nested?.session_id;
74
86
  const agentAid = data.agentAid ?? data.agent_aid ?? data.selfAid ?? nested?.agentAid ?? nested?.agent_aid ?? nested?.selfAid;
75
87
  const rawAgentName = data.agentName ?? data.agent_name ?? nested?.agentName ?? nested?.agent_name;
@@ -91,18 +103,17 @@ export function normalizeStructuredLog(data) {
91
103
  ? normalizePermissionMode(rawPermissionMode).mode
92
104
  : rawPermissionMode;
93
105
  const toolName = data.toolName ?? data.tool ?? data.name ?? nested?.toolName ?? nested?.name;
94
- const isToolResult = eventType === 'tool:result' || eventType === 'tool_result';
95
- const isToolUse = eventType === 'tool:use' || eventType === 'tool_use';
96
106
  const isError = data.isError ?? data.is_error ?? (data.ok === false ? true : undefined)
97
107
  ?? nested?.isError ?? nested?.is_error ?? (nested?.ok === false ? true : undefined);
98
108
  const decisionSource = data.decisionSource ?? data.decision_source
99
- ?? nested?.decisionSource ?? nested?.decision_source;
109
+ ?? nested?.decisionSource ?? nested?.decision_source
110
+ ?? (lifecycleRecord ? 'runner' : undefined);
100
111
  const decision = data.decision
112
+ ?? nested?.decision
101
113
  ?? data.status
102
114
  ?? (isToolResult && isError === true ? 'error' : undefined)
103
115
  ?? (isToolResult && isError === false ? 'allow' : undefined)
104
- ?? (isToolUse ? 'started' : undefined)
105
- ?? nested?.decision;
116
+ ?? (isToolUse ? 'pending' : undefined);
106
117
  const executed = data.executed ?? nested?.executed
107
118
  ?? (isToolResult ? true : isToolUse ? false : undefined);
108
119
  const executionState = data.executionState ?? data.execution_state
@@ -118,13 +129,10 @@ export function normalizeStructuredLog(data) {
118
129
  const classificationMissing = isToolResult && isError === true
119
130
  && !normalizeToolErrorCode(explicitErrorCode)
120
131
  && decisionSource !== 'policy' && decisionSource !== 'approval';
121
- const lifecycleRecord = isToolUse || isToolResult;
122
132
  const lifecycleEventKey = lifecycleRecord
123
133
  ? (eventKey ?? buildToolLifecycleEventKey({
124
134
  sessionId,
125
- callId: data.callId ?? data.call_id ?? data.toolUseId ?? data.tool_use_id ?? nested?.callId ?? nested?.call_id,
126
- correlationId: data.correlationId ?? data.correlation_id ?? nested?.correlationId ?? nested?.correlation_id,
127
- requestId: data.requestId ?? data.request_id ?? nested?.requestId ?? nested?.request_id,
135
+ ...toolCorrelation,
128
136
  }))
129
137
  : undefined;
130
138
  const missingContextFields = lifecycleRecord
@@ -134,6 +142,8 @@ export function normalizeStructuredLog(data) {
134
142
  ...data,
135
143
  ...(lifecycleEventKey ? { eventKey: lifecycleEventKey } : {}),
136
144
  ...(eventPhase ? { eventPhase } : {}),
145
+ ...(lifecycleRecord ? toolCorrelation : {}),
146
+ ...(lifecycleRecord && !toolCorrelation.callId ? { correlationMissing: true } : {}),
137
147
  ...(correlationId ? { correlationId } : {}),
138
148
  ...(sessionId ? { sessionId } : lifecycleRecord ? { sessionId: 'unknown' } : {}),
139
149
  ...(agentAid ? { agentAid } : lifecycleRecord ? { agentAid: 'unknown' } : {}),