evolcore 0.0.20 → 0.0.21
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.
- package/CHANGELOG.md +22 -0
- package/README.md +58 -9
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +379 -108
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +402 -135
- package/dist/agents/ecagent-runner.js +171 -61
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +59 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +72 -6
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +5 -0
- package/dist/aun/outbox.js +182 -80
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +409 -88
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +69 -115
- package/dist/cli/init.js +27 -15
- package/dist/cli/task-context.js +46 -0
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +16 -5
- package/dist/config/config-manager.js +135 -17
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +20 -9
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +151 -9
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +31 -1
- package/dist/core/auth/auth-gateway.js +33 -4
- package/dist/core/auth/authorization-audit.js +150 -2
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-service.js +6 -2
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +290 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +279 -80
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +350 -32
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +4 -0
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +10 -0
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +83 -84
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +124 -10
- package/dist/core/message/message-log.js +14 -7
- package/dist/core/message/message-queue.js +206 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +486 -68
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +410 -54
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +159 -1
- package/dist/core/permission/tool-policy.js +575 -21
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +299 -30
- package/dist/core/session/session-renew.js +19 -12
- package/dist/core/session/session-turn-coordinator.js +11 -4
- package/dist/eck/kit-renderer.js +1 -1
- package/dist/index.js +253 -46
- package/dist/ipc.js +374 -24
- package/dist/paths.js +64 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +56 -5
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +3 -2
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +21 -2
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/schemas/_meta.json +4 -2
- package/kits/schemas/agent-config.schema.11.json +13 -0
- package/kits/schemas/daemon.schema.5.json +0 -1
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +15 -3
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/package.json +6 -3
- package/skills/eclink/SKILL.md +2 -0
- package/dist/config/aun-gateway-config.js +0 -2
package/dist/paths.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import crypto from 'crypto';
|
|
5
5
|
import { DAEMON_CONFIG_FILENAME, DEFAULT_RUNTIME_HOME_DIRNAME, INSTANCE_SOCKET_FILENAME, RUNTIME_HOME_ENV, WINDOWS_PIPE_PREFIX, } from './product.js';
|
|
6
|
+
import { migrateLegacyAgentJsonlLogs } from './core/audit/log-integrity.js';
|
|
6
7
|
const isWindows = process.platform === 'win32';
|
|
7
8
|
let _root = null;
|
|
8
9
|
export function resolveRoot() {
|
|
@@ -118,9 +119,6 @@ export function agentVenuesDir(aid) {
|
|
|
118
119
|
export function agentSessionsDir(aid) {
|
|
119
120
|
return path.join(agentDir(aid), 'sessions');
|
|
120
121
|
}
|
|
121
|
-
export function agentLogsDir(aid) {
|
|
122
|
-
return path.join(agentDir(aid), 'logs');
|
|
123
|
-
}
|
|
124
122
|
export function agentDataDir(aid) {
|
|
125
123
|
return path.join(agentDir(aid), 'data');
|
|
126
124
|
}
|
|
@@ -133,6 +131,9 @@ export function agentHandoffDir(aid) {
|
|
|
133
131
|
export function agentOutboxPath(aid) {
|
|
134
132
|
return path.join(agentDataDir(aid), 'outbox.jsonl');
|
|
135
133
|
}
|
|
134
|
+
export function agentActivityOutboxPath(aid) {
|
|
135
|
+
return path.join(agentDataDir(aid), 'activity-outbox.jsonl');
|
|
136
|
+
}
|
|
136
137
|
/** Directory-safe channel instance key used by connector runtime state. */
|
|
137
138
|
function encodePathKey(value) {
|
|
138
139
|
return encodeURIComponent(value);
|
|
@@ -161,11 +162,66 @@ export function channelStatePath(channelKey, ...parts) {
|
|
|
161
162
|
export function agentTriggersDir(aid) {
|
|
162
163
|
return path.join(agentDir(aid), 'triggers');
|
|
163
164
|
}
|
|
164
|
-
function
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
function stripTrailingWindowsSeparators(value) {
|
|
166
|
+
const stripped = value.replace(/[\\/]+$/g, '');
|
|
167
|
+
// Keep a drive-root separator. `C:` is a drive-relative path on Windows.
|
|
168
|
+
if (/^[a-zA-Z]:$/.test(stripped) && /^[a-zA-Z]:[\\/]+$/.test(value)) {
|
|
169
|
+
return `${stripped}\\`;
|
|
170
|
+
}
|
|
171
|
+
return stripped;
|
|
172
|
+
}
|
|
173
|
+
function normalizeWindowsSocketRoot(root) {
|
|
174
|
+
// Use win32 semantics explicitly: EVOLCORE_HOME can come from Git Bash even
|
|
175
|
+
// when the daemon itself runs under native Windows Node.
|
|
176
|
+
return stripTrailingWindowsSeparators(path.win32.resolve(root)).toLowerCase();
|
|
177
|
+
}
|
|
178
|
+
function windowsSocketPathForRoot(root) {
|
|
179
|
+
const hash = crypto.createHash('sha1').update(root).digest('hex').slice(0, 12);
|
|
180
|
+
return `\\\\.\\pipe\\${WINDOWS_PIPE_PREFIX}-${hash}`;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Return the canonical and pre-normalization Windows daemon pipe names.
|
|
184
|
+
*
|
|
185
|
+
* The extra candidates keep a daemon started by an older EvolCore build
|
|
186
|
+
* reachable while it is still running after the CLI has been upgraded.
|
|
187
|
+
*/
|
|
188
|
+
export function getWindowsInstanceSocketPathCandidates(root) {
|
|
189
|
+
const normalized = path.win32.normalize(root);
|
|
190
|
+
const normalizedForward = stripTrailingWindowsSeparators(normalized.replace(/\\/g, '/'));
|
|
191
|
+
const canonical = normalizeWindowsSocketRoot(root);
|
|
192
|
+
const baseVariants = [
|
|
193
|
+
canonical,
|
|
194
|
+
root,
|
|
195
|
+
normalized,
|
|
196
|
+
normalizedForward,
|
|
197
|
+
canonical.replace(/\\/g, '/'),
|
|
198
|
+
root.replace(/\\/g, '/'),
|
|
199
|
+
root.replace(/\//g, '\\'),
|
|
200
|
+
];
|
|
201
|
+
const variants = new Set();
|
|
202
|
+
for (const variant of baseVariants) {
|
|
203
|
+
const stripped = stripTrailingWindowsSeparators(variant);
|
|
204
|
+
variants.add(variant);
|
|
205
|
+
variants.add(stripped);
|
|
206
|
+
if (stripped) {
|
|
207
|
+
variants.add(`${stripped}\\`);
|
|
208
|
+
variants.add(`${stripped}/`);
|
|
209
|
+
}
|
|
168
210
|
}
|
|
211
|
+
const seen = new Set();
|
|
212
|
+
const candidates = [];
|
|
213
|
+
for (const variant of variants) {
|
|
214
|
+
const socket = windowsSocketPathForRoot(variant);
|
|
215
|
+
if (seen.has(socket))
|
|
216
|
+
continue;
|
|
217
|
+
seen.add(socket);
|
|
218
|
+
candidates.push(socket);
|
|
219
|
+
}
|
|
220
|
+
return candidates;
|
|
221
|
+
}
|
|
222
|
+
function resolveInstanceSocketPath(root) {
|
|
223
|
+
if (isWindows)
|
|
224
|
+
return getWindowsInstanceSocketPathCandidates(root)[0];
|
|
169
225
|
const filesystemPath = path.join(root, 'data', 'instance', INSTANCE_SOCKET_FILENAME);
|
|
170
226
|
// Linux/macOS Unix-domain sockets have a small sockaddr path limit
|
|
171
227
|
// (typically 108 bytes on Linux). Long managed TMPDIR prefixes can make a
|
|
@@ -196,6 +252,7 @@ export function ensureDataDirs() {
|
|
|
196
252
|
fs.mkdirSync(p.outboxDir, { recursive: true });
|
|
197
253
|
fs.mkdirSync(p.eckDir, { recursive: true });
|
|
198
254
|
fs.mkdirSync(aidsDir(), { recursive: true });
|
|
255
|
+
migrateLegacyAgentJsonlLogs(p.agentsDir, p.logs);
|
|
199
256
|
migrateFromAun();
|
|
200
257
|
}
|
|
201
258
|
const MIGRATION_MARKER = '.migrated-from-aun';
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
* (Phase 6)真正需要时接入 Runner,当前提供明确的"未实现"占位,避免空壳代码。
|
|
13
13
|
*/
|
|
14
14
|
import path from 'path';
|
|
15
|
-
import fs from 'fs';
|
|
16
15
|
export class ResponseModeContextBuilder {
|
|
17
16
|
/** (sessionId::modeId) → sessionState,跨消息保持 */
|
|
18
17
|
stateCache = new Map();
|
|
@@ -22,7 +21,7 @@ export class ResponseModeContextBuilder {
|
|
|
22
21
|
*/
|
|
23
22
|
build(modeId, deps) {
|
|
24
23
|
const sessionState = this.getOrCreateState(deps.session.id, modeId);
|
|
25
|
-
const dataDir =
|
|
24
|
+
const dataDir = path.join(deps.agentDir, 'response-system', modeId);
|
|
26
25
|
return {
|
|
27
26
|
session: deps.session,
|
|
28
27
|
agentConfig: deps.agentConfig,
|
|
@@ -63,9 +62,4 @@ export class ResponseModeContextBuilder {
|
|
|
63
62
|
}
|
|
64
63
|
return state;
|
|
65
64
|
}
|
|
66
|
-
ensureDataDir(agentDir, modeId) {
|
|
67
|
-
const dir = path.join(agentDir, 'response-system', modeId);
|
|
68
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
69
|
-
return dir;
|
|
70
|
-
}
|
|
71
65
|
}
|
package/dist/trigger/feedback.js
CHANGED
|
@@ -37,8 +37,11 @@ export class TriggerFeedbackDispatcher {
|
|
|
37
37
|
const result = await this.dispatchDirect(input, renderedText, target);
|
|
38
38
|
if (result.error)
|
|
39
39
|
return { status: 'failed', reason: result.error.code, feedback, effects: result.effects, error: result.error };
|
|
40
|
+
const replyStatus = statusFromReply(input.reply);
|
|
40
41
|
return {
|
|
41
|
-
status:
|
|
42
|
+
status: result.effects.some(effect => effect.status === 'queued') && replyStatus !== 'failed'
|
|
43
|
+
? 'degraded'
|
|
44
|
+
: replyStatus,
|
|
42
45
|
reason: reasonFromReply(input.reply),
|
|
43
46
|
feedback,
|
|
44
47
|
effects: result.effects,
|
|
@@ -204,6 +207,7 @@ export class TriggerFeedbackDispatcher {
|
|
|
204
207
|
originChannelType: input.trigger.origin?.channelType
|
|
205
208
|
?? (input.trigger.origin?.channelKey ? channelTypeFromKey(input.trigger.origin.channelKey) : undefined),
|
|
206
209
|
originControl: input.trigger.origin?.channelKey === 'daemon',
|
|
210
|
+
authorizedBy: input.trigger.authorizedBy,
|
|
207
211
|
},
|
|
208
212
|
};
|
|
209
213
|
outcome = this.waitForTriggerOutcome(input.runId, input.attemptId, delivery.signal);
|
|
@@ -379,7 +383,7 @@ export class TriggerFeedbackDispatcher {
|
|
|
379
383
|
if (isAunTarget && !targetChatType(target)) {
|
|
380
384
|
throw Object.assign(new Error('AUN trigger feedback requires an explicit delivery route'), { code: 'AUN_OUTBOUND_ROUTE_REQUIRED' });
|
|
381
385
|
}
|
|
382
|
-
await raceWithAbort(binding.adapter.send(buildEnvelope({
|
|
386
|
+
const receipt = await raceWithAbort(binding.adapter.send(buildEnvelope({
|
|
383
387
|
taskId: `trigger:${input.runId}`,
|
|
384
388
|
channel: binding.adapter.channelKey,
|
|
385
389
|
channelId: target.channelId,
|
|
@@ -394,16 +398,60 @@ export class TriggerFeedbackDispatcher {
|
|
|
394
398
|
isFinal: true,
|
|
395
399
|
format: 'plain',
|
|
396
400
|
}), cancellation.signal);
|
|
397
|
-
|
|
401
|
+
const common = {
|
|
398
402
|
type: 'message.send',
|
|
399
|
-
status: 'success',
|
|
400
403
|
channelKey: target.channelKey,
|
|
401
404
|
channelType: channelTypeFromKey(target.channelKey),
|
|
402
405
|
channelId: target.channelId,
|
|
403
406
|
attempt,
|
|
404
407
|
startedAt,
|
|
405
408
|
finishedAt: Date.now(),
|
|
406
|
-
}
|
|
409
|
+
};
|
|
410
|
+
if (receipt?.status === 'sent' && receipt.messageId) {
|
|
411
|
+
effects.push({ ...common, status: 'success', operationId: receipt.operationId, messageId: receipt.messageId });
|
|
412
|
+
return { effects, error: null };
|
|
413
|
+
}
|
|
414
|
+
if (receipt?.status === 'queued') {
|
|
415
|
+
effects.push({
|
|
416
|
+
...common,
|
|
417
|
+
status: 'queued',
|
|
418
|
+
operationId: receipt.operationId,
|
|
419
|
+
outboxId: receipt.outboxId,
|
|
420
|
+
error: receipt.error,
|
|
421
|
+
code: receipt.code,
|
|
422
|
+
});
|
|
423
|
+
return { effects, error: null };
|
|
424
|
+
}
|
|
425
|
+
if (receipt?.status === 'failed') {
|
|
426
|
+
effects.push({
|
|
427
|
+
...common,
|
|
428
|
+
status: 'failed',
|
|
429
|
+
operationId: receipt.operationId,
|
|
430
|
+
outboxId: receipt.outboxId,
|
|
431
|
+
error: receipt.error,
|
|
432
|
+
code: receipt.code,
|
|
433
|
+
});
|
|
434
|
+
return {
|
|
435
|
+
effects,
|
|
436
|
+
error: {
|
|
437
|
+
code: String(receipt.code ?? 'send_failed'),
|
|
438
|
+
message: receipt.error,
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
if (receipt?.status === 'suppressed') {
|
|
443
|
+
const message = `channel suppressed trigger feedback: ${receipt.reason}`;
|
|
444
|
+
effects.push({ ...common, status: 'skipped', error: message, code: 'SEND_SUPPRESSED' });
|
|
445
|
+
return { effects, error: { code: 'send_suppressed', message } };
|
|
446
|
+
}
|
|
447
|
+
if (isAunTarget) {
|
|
448
|
+
const message = 'AUN adapter returned no delivery receipt';
|
|
449
|
+
effects.push({ ...common, status: 'failed', error: message, code: 'MISSING_SEND_RECEIPT' });
|
|
450
|
+
return { effects, error: { code: 'missing_send_receipt', message } };
|
|
451
|
+
}
|
|
452
|
+
// Non-AUN adapters are still allowed to use the legacy void result
|
|
453
|
+
// until their receipt migration is complete.
|
|
454
|
+
effects.push({ ...common, status: 'success' });
|
|
407
455
|
return { effects, error: null };
|
|
408
456
|
}
|
|
409
457
|
catch (err) {
|
|
@@ -527,6 +575,9 @@ export class TriggerFeedbackDispatcher {
|
|
|
527
575
|
const status = Number(err?.status ?? err?.statusCode ?? err?.code);
|
|
528
576
|
const rawCode = String(err?.code ?? err?.errorCode ?? '').toLowerCase();
|
|
529
577
|
const lower = `${message} ${rawCode}`.toLowerCase();
|
|
578
|
+
if (rawCode === 'outbox_full') {
|
|
579
|
+
return { retryable: false, code: 'OUTBOX_FULL', message };
|
|
580
|
+
}
|
|
530
581
|
if (status === 408 || status === 429 || status >= 500) {
|
|
531
582
|
return { retryable: true, code: 'send_retryable_error', message };
|
|
532
583
|
}
|
package/dist/trigger/history.js
CHANGED
|
@@ -2,7 +2,11 @@ import crypto from 'crypto';
|
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { isManagedSessionRuntimeDir, SESSION_RUNTIME_DIR_ENV } from '../cli/task-context.js';
|
|
5
|
+
import { findMessageLogOperationMessageId } from '../core/message/message-log.js';
|
|
6
|
+
import { chatDirPath } from '../core/session/session-fs-store.js';
|
|
7
|
+
import { resolvePaths } from '../paths.js';
|
|
5
8
|
import { definitionRevision } from './validation.js';
|
|
9
|
+
import * as aunOutbox from '../aun/outbox.js';
|
|
6
10
|
export class TriggerHistoryStore {
|
|
7
11
|
rootDir;
|
|
8
12
|
agentAid;
|
|
@@ -66,7 +70,7 @@ export class TriggerHistoryStore {
|
|
|
66
70
|
continue;
|
|
67
71
|
if (triggerId && event.triggerId !== triggerId)
|
|
68
72
|
continue;
|
|
69
|
-
out.push(event);
|
|
73
|
+
out.push(hydrateHistoryEvent(event));
|
|
70
74
|
}
|
|
71
75
|
return out;
|
|
72
76
|
}
|
|
@@ -80,7 +84,7 @@ export class TriggerHistoryStore {
|
|
|
80
84
|
if (!event || !isRunEvent(event))
|
|
81
85
|
continue;
|
|
82
86
|
if (event.agentAid === this.agentAid && event.triggerId === triggerId) {
|
|
83
|
-
out.push(event.audit);
|
|
87
|
+
out.push(hydrateTriggerAudit(event.audit));
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
return out;
|
|
@@ -95,7 +99,7 @@ export class TriggerHistoryStore {
|
|
|
95
99
|
const event = parseHistoryEvent(lines[i], this.agentAid);
|
|
96
100
|
if (!event || !isRunEvent(event) || !pending.has(event.triggerId))
|
|
97
101
|
continue;
|
|
98
|
-
latest.set(event.triggerId, event.audit);
|
|
102
|
+
latest.set(event.triggerId, hydrateTriggerAudit(event.audit));
|
|
99
103
|
pending.delete(event.triggerId);
|
|
100
104
|
}
|
|
101
105
|
return latest;
|
|
@@ -125,7 +129,7 @@ export class TriggerHistoryStore {
|
|
|
125
129
|
}
|
|
126
130
|
if (!isRunEvent(event))
|
|
127
131
|
continue;
|
|
128
|
-
const record = event.audit;
|
|
132
|
+
const record = hydrateTriggerAudit(event.audit);
|
|
129
133
|
if (record.status !== 'completed'
|
|
130
134
|
&& record.status !== 'degraded'
|
|
131
135
|
&& record.status !== 'noop'
|
|
@@ -310,6 +314,77 @@ function finiteNumber(value) {
|
|
|
310
314
|
function nonNegativeInteger(value) {
|
|
311
315
|
return typeof value === 'number' && Number.isInteger(value) && value >= 0 ? value : undefined;
|
|
312
316
|
}
|
|
317
|
+
function hydrateHistoryEvent(event) {
|
|
318
|
+
if (!isRunEvent(event))
|
|
319
|
+
return event;
|
|
320
|
+
return { ...event, audit: hydrateTriggerAudit(event.audit) };
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Resolve queued AUN effects against the durable outbox at read time. The
|
|
324
|
+
* original audit is append-only, so this projection is what lets a later
|
|
325
|
+
* expiry become visibly failed without pretending that a missing outbox row
|
|
326
|
+
* was delivered.
|
|
327
|
+
*/
|
|
328
|
+
function hydrateTriggerAudit(audit) {
|
|
329
|
+
let changed = false;
|
|
330
|
+
let deliveryFailure;
|
|
331
|
+
const effects = audit.effects.map(effect => {
|
|
332
|
+
if (effect.status !== 'queued' || !effect.outboxId)
|
|
333
|
+
return effect;
|
|
334
|
+
const state = aunOutbox.deliveryState(audit.agentAid, effect.outboxId);
|
|
335
|
+
if (state.status === 'sent') {
|
|
336
|
+
changed = true;
|
|
337
|
+
return { ...effect, status: 'success', messageId: state.messageId };
|
|
338
|
+
}
|
|
339
|
+
if (state.status === 'failed') {
|
|
340
|
+
changed = true;
|
|
341
|
+
deliveryFailure ??= { code: String(state.code ?? 'OUTBOX_DELIVERY_FAILED'), message: state.error };
|
|
342
|
+
return { ...effect, status: 'failed', error: state.error, ...(state.code !== undefined ? { code: state.code } : {}) };
|
|
343
|
+
}
|
|
344
|
+
if (state.status === 'missing' && isAunEffect(effect)) {
|
|
345
|
+
const messageId = findDeliveredEffectMessageId(audit.agentAid, effect);
|
|
346
|
+
changed = true;
|
|
347
|
+
if (messageId) {
|
|
348
|
+
return { ...effect, status: 'success', messageId };
|
|
349
|
+
}
|
|
350
|
+
const code = 'OUTBOX_RESULT_MISSING';
|
|
351
|
+
const error = 'outbox delivery result is missing and no remote message_id was recorded';
|
|
352
|
+
deliveryFailure ??= { code, message: error };
|
|
353
|
+
return { ...effect, status: 'failed', error, code };
|
|
354
|
+
}
|
|
355
|
+
return effect;
|
|
356
|
+
});
|
|
357
|
+
if (!changed)
|
|
358
|
+
return audit;
|
|
359
|
+
if (!deliveryFailure) {
|
|
360
|
+
const hasQueuedEffect = effects.some(effect => effect.status === 'queued');
|
|
361
|
+
const hasFailedEffect = effects.some(effect => effect.status === 'failed');
|
|
362
|
+
const canRestoreReplyStatus = audit.status === 'degraded'
|
|
363
|
+
&& !hasQueuedEffect
|
|
364
|
+
&& !hasFailedEffect
|
|
365
|
+
&& !audit.anomalies?.length;
|
|
366
|
+
const status = canRestoreReplyStatus
|
|
367
|
+
? audit.reply?.outcome === 'noop' ? 'noop' : 'completed'
|
|
368
|
+
: audit.status;
|
|
369
|
+
return { ...audit, status, effects };
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
...audit,
|
|
373
|
+
status: 'failed',
|
|
374
|
+
reason: deliveryFailure.code,
|
|
375
|
+
effects,
|
|
376
|
+
error: deliveryFailure,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
function isAunEffect(effect) {
|
|
380
|
+
return effect.channelType === 'aun' || effect.channelKey?.startsWith('aun#') === true;
|
|
381
|
+
}
|
|
382
|
+
function findDeliveredEffectMessageId(agentAid, effect) {
|
|
383
|
+
if (!effect.operationId || !effect.channelId)
|
|
384
|
+
return undefined;
|
|
385
|
+
const chatDir = chatDirPath(resolvePaths().sessionsDir, 'aun', effect.channelId, agentAid);
|
|
386
|
+
return findMessageLogOperationMessageId(chatDir, effect.operationId);
|
|
387
|
+
}
|
|
313
388
|
function listAuditLogFiles(logDir) {
|
|
314
389
|
let entries;
|
|
315
390
|
try {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
const HISTORY_CHANNELS = new Set(['aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom']);
|
|
2
1
|
/**
|
|
3
2
|
* Turns legacy raw session-file references into the controlled history CLI.
|
|
4
3
|
* The replacement deliberately keeps the explicit peer/channel scope but
|
|
5
4
|
* never carries a filesystem location into a Trigger prompt.
|
|
6
5
|
*/
|
|
7
6
|
export function rewriteLegacyTriggerSessionPaths(prompt, triggerAid) {
|
|
7
|
+
const historyChannels = new Set(['aun', 'feishu', 'wechat', 'dingtalk', 'qqbot', 'wecom']);
|
|
8
8
|
const legacyPath = /data\/sessions\/([A-Za-z0-9_-]+)\/([^/\s`"'<>]+)(?:\/([^/\s`"'<>]+))?\/messages\.jsonl/g;
|
|
9
9
|
return prompt.replace(legacyPath, (match, channel, first, second) => {
|
|
10
|
-
if (!
|
|
10
|
+
if (!historyChannels.has(channel))
|
|
11
11
|
return match;
|
|
12
12
|
const isAun = channel === 'aun';
|
|
13
13
|
const target = isAun ? second : first;
|
package/dist/trigger/parser.js
CHANGED
|
@@ -113,6 +113,7 @@ const PERMISSION_MODES = new Set([
|
|
|
113
113
|
'auto',
|
|
114
114
|
'request',
|
|
115
115
|
'bypass',
|
|
116
|
+
'fullaccess',
|
|
116
117
|
]);
|
|
117
118
|
const TRIGGER_EFFORTS = new Set(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
118
119
|
function parsePermissionModeFlag(flags, opts = {}) {
|
|
@@ -124,10 +125,10 @@ function parsePermissionModeFlag(flags, opts = {}) {
|
|
|
124
125
|
if (raw === 'inherit') {
|
|
125
126
|
if (opts.allowInherit)
|
|
126
127
|
return { ok: true, value: null };
|
|
127
|
-
return { ok: false, error: '--permission 创建时只能接受 readonly、auto、request 或
|
|
128
|
+
return { ok: false, error: '--permission 创建时只能接受 readonly、auto、request、bypass 或 fullaccess' };
|
|
128
129
|
}
|
|
129
130
|
if (!PERMISSION_MODES.has(raw)) {
|
|
130
|
-
return { ok: false, error: '--permission 只接受 readonly、auto、request 或
|
|
131
|
+
return { ok: false, error: '--permission 只接受 readonly、auto、request、bypass 或 fullaccess' };
|
|
131
132
|
}
|
|
132
133
|
return { ok: true, value: raw };
|
|
133
134
|
}
|
|
@@ -13,6 +13,7 @@ const PERMISSION_MODES = new Set([
|
|
|
13
13
|
'auto',
|
|
14
14
|
'request',
|
|
15
15
|
'bypass',
|
|
16
|
+
'fullaccess',
|
|
16
17
|
]);
|
|
17
18
|
export function normalizeTriggerDefinition(input, opts = {}) {
|
|
18
19
|
if (!isObject(input))
|
|
@@ -234,6 +235,7 @@ function normalizeV3Phase2(raw, opts) {
|
|
|
234
235
|
enabled: raw.enabled === undefined ? true : requiredBoolean(raw.enabled, 'enabled'),
|
|
235
236
|
name,
|
|
236
237
|
description: optionalString(raw.description),
|
|
238
|
+
authorizedBy: optionalString(raw.authorizedBy),
|
|
237
239
|
createdAt,
|
|
238
240
|
updatedAt,
|
|
239
241
|
origin: normalizeOrigin(raw.origin),
|
|
@@ -438,6 +440,9 @@ function normalizeExecution(value, _opts) {
|
|
|
438
440
|
onError: normalizeOnError(raw.onError),
|
|
439
441
|
noopSentinel: optionalString(raw.noopSentinel) ?? DEFAULT_NOOP_SENTINEL,
|
|
440
442
|
};
|
|
443
|
+
if (execution.type === 'script' && execution.permissionMode === 'fullaccess') {
|
|
444
|
+
throw new Error('script Trigger already executes as the daemon OS user and cannot declare execution.permissionMode=fullaccess');
|
|
445
|
+
}
|
|
441
446
|
return execution;
|
|
442
447
|
}
|
|
443
448
|
function normalizeExecutionBaseagent(value) {
|
|
@@ -478,7 +483,7 @@ function normalizeTriggerPermissionMode(value) {
|
|
|
478
483
|
if (mode === 'edit' || mode === 'noask' || mode === 'plan') {
|
|
479
484
|
return normalizePermissionModeContract(mode).mode;
|
|
480
485
|
}
|
|
481
|
-
throw new Error('execution.permissionMode must be one of readonly, auto, request, bypass');
|
|
486
|
+
throw new Error('execution.permissionMode must be one of readonly, auto, request, bypass, fullaccess');
|
|
482
487
|
}
|
|
483
488
|
function normalizeScript(value) {
|
|
484
489
|
if (!isObject(value))
|
|
@@ -75,6 +75,33 @@ export function atomicWrite(filePath, content) {
|
|
|
75
75
|
export function atomicWriteJson(filePath, value) {
|
|
76
76
|
atomicWrite(filePath, JSON.stringify(value, null, 2) + '\n');
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Update a small protected file without replacing its directory entry.
|
|
80
|
+
*
|
|
81
|
+
* H-class sandbox mounts bind individual files by pathname. Replacing such a
|
|
82
|
+
* file with rename(2) changes the inode while bubblewrap is constructing its
|
|
83
|
+
* mount table and can make the bind/remount operation fail. This helper keeps
|
|
84
|
+
* the inode stable; callers should use it only for best-effort, daemon-owned
|
|
85
|
+
* state where a crash during the write may leave incomplete content.
|
|
86
|
+
*/
|
|
87
|
+
export function stableWrite(filePath, content) {
|
|
88
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
89
|
+
const noFollow = fs.constants.O_NOFOLLOW ?? 0;
|
|
90
|
+
const fd = fs.openSync(filePath, fs.constants.O_RDWR | fs.constants.O_CREAT | noFollow, 0o600);
|
|
91
|
+
try {
|
|
92
|
+
const stat = fs.fstatSync(fd);
|
|
93
|
+
if (!stat.isFile())
|
|
94
|
+
throw new Error(`stable write target is not a regular file: ${filePath}`);
|
|
95
|
+
fs.ftruncateSync(fd, 0);
|
|
96
|
+
fs.writeFileSync(fd, content, 'utf8');
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
fs.closeSync(fd);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function stableWriteJson(filePath, value) {
|
|
103
|
+
stableWrite(filePath, JSON.stringify(value, null, 2) + '\n');
|
|
104
|
+
}
|
|
78
105
|
/**
|
|
79
106
|
* 原子读取——自动按状态恢复。文件不存在时返回 null。
|
|
80
107
|
*
|
|
@@ -3,6 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import { compareVersions, resolveGlobalPkg } from './npm-ops.js';
|
|
4
4
|
import { isWindows as platformIsWindows, resolveCommandPath } from './cross-platform.js';
|
|
5
5
|
import { MIN_ECWEB_VERSION, WEB_CLI_BIN, WEB_PACKAGE_NAME } from '../product.js';
|
|
6
|
+
import { resolvePaths } from '../paths.js';
|
|
6
7
|
/** EC Web owns the session-log reader, so older builds cannot safely serve a newer daemon. */
|
|
7
8
|
export function isEcwebVersionSupported(version) {
|
|
8
9
|
const normalized = version?.trim();
|
|
@@ -70,10 +71,23 @@ export function resolveEcwebLaunchCommand(ecwebArgs, opts = {}) {
|
|
|
70
71
|
* 配对码是 ecweb 进程自己生成并持有的内部状态。daemon/CLI 通过 ecweb 的
|
|
71
72
|
* localhost-only HTTP 接口 GET /api/pair-code 取当前码(远程访问被 ecweb 403 拒绝)。
|
|
72
73
|
*/
|
|
73
|
-
/** 经 localhost 拉取 ecweb 当前配对码(仅本机可取)。失败返回 null。
|
|
74
|
-
|
|
74
|
+
/** 经 localhost 拉取 ecweb 当前配对码(仅本机可取)。失败返回 null。
|
|
75
|
+
* authorizedBy 仅由 daemon 的 AUN 控制通道传入,用于把配对码换出的 token
|
|
76
|
+
* 绑定到实际批准的 daemon-owner;普通 CLI 取码不携带该字段。 */
|
|
77
|
+
export async function fetchEcwebPairCode(port, authorizedBy) {
|
|
75
78
|
try {
|
|
79
|
+
const headers = {};
|
|
80
|
+
const owner = authorizedBy?.trim();
|
|
81
|
+
if (owner)
|
|
82
|
+
headers['x-evolcore-pair-authorized-by'] = owner;
|
|
83
|
+
try {
|
|
84
|
+
const controlToken = fs.readFileSync(resolvePaths().controlToken, 'utf8').trim();
|
|
85
|
+
if (controlToken)
|
|
86
|
+
headers['x-evolcore-control-token'] = controlToken;
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
76
89
|
const resp = await fetch(`http://127.0.0.1:${port}/api/pair-code`, {
|
|
90
|
+
headers,
|
|
77
91
|
signal: AbortSignal.timeout(2000),
|
|
78
92
|
});
|
|
79
93
|
if (!resp.ok)
|
|
@@ -388,7 +388,10 @@ export function getErrorMessage(error, terminalReason, includeEmoji = true) {
|
|
|
388
388
|
const warnPrefix = includeEmoji ? '⚠️ ' : '';
|
|
389
389
|
const errPrefix = includeEmoji ? '❌ ' : '';
|
|
390
390
|
if (msg.includes('CONTEXT_COMPACT_FAILED') || isContextTooLongText(msg)) {
|
|
391
|
-
|
|
391
|
+
const detail = msg.match(/^CONTEXT_COMPACT_FAILED:\s*(.+)$/)?.[1]?.trim();
|
|
392
|
+
return detail
|
|
393
|
+
? `${warnPrefix}上下文过长,自动压缩失败:${detail}`
|
|
394
|
+
: `${warnPrefix}上下文过长,自动压缩失败,请手动输入 /compact 重试`;
|
|
392
395
|
}
|
|
393
396
|
if (isSandboxInitializationFailure(error)) {
|
|
394
397
|
return `${warnPrefix}只读执行沙箱启动失败(sandbox_initialization_failed),当前任务未执行;请检查 user namespace/seccomp 配置后重试`;
|
package/dist/utils/logger.js
CHANGED
|
@@ -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 { normalizePermissionMode } from '../core/permission/mode.js';
|
|
5
6
|
import { buildToolLifecycleEventKey } from '../core/audit/event-key.js';
|
|
6
7
|
let currentLevel = process.env.LOG_LEVEL || 'INFO';
|
|
7
8
|
const LEVELS = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
|
|
@@ -70,8 +71,25 @@ export function normalizeStructuredLog(data) {
|
|
|
70
71
|
?? nested?.toolUseId
|
|
71
72
|
?? nested?.tool_use_id;
|
|
72
73
|
const sessionId = data.sessionId ?? data.session_id ?? nested?.sessionId ?? nested?.session_id;
|
|
73
|
-
const agentAid = data.agentAid ?? data.agent_aid ?? data.selfAid ?? nested?.agentAid ?? nested?.agent_aid;
|
|
74
|
-
const
|
|
74
|
+
const agentAid = data.agentAid ?? data.agent_aid ?? data.selfAid ?? nested?.agentAid ?? nested?.agent_aid ?? nested?.selfAid;
|
|
75
|
+
const rawAgentName = data.agentName ?? data.agent_name ?? nested?.agentName ?? nested?.agent_name;
|
|
76
|
+
// Lifecycle streams should carry both the stable AID and a display label.
|
|
77
|
+
// When a producer only knows the AID, use that same value as the label so
|
|
78
|
+
// daily joins never invent a second identity for one session.
|
|
79
|
+
const normalizedAgentName = typeof rawAgentName === 'string' ? rawAgentName.trim() : '';
|
|
80
|
+
const agentName = normalizedAgentName
|
|
81
|
+
&& !['unknown', '<unknown>', '<none>', 'undefined', 'null'].includes(normalizedAgentName)
|
|
82
|
+
? normalizedAgentName
|
|
83
|
+
: typeof agentAid === 'string' && agentAid !== 'unknown'
|
|
84
|
+
? agentAid
|
|
85
|
+
: undefined;
|
|
86
|
+
const rawPermissionMode = data.permissionMode ?? data.permission_mode ?? nested?.permissionMode ?? nested?.permission_mode;
|
|
87
|
+
// Normalize legacy values at the log boundary. Preserve an explicit
|
|
88
|
+
// `unknown` marker for records that genuinely arrived without context.
|
|
89
|
+
const permissionMode = typeof rawPermissionMode === 'string'
|
|
90
|
+
&& rawPermissionMode !== 'unknown'
|
|
91
|
+
? normalizePermissionMode(rawPermissionMode).mode
|
|
92
|
+
: rawPermissionMode;
|
|
75
93
|
const toolName = data.toolName ?? data.tool ?? data.name ?? nested?.toolName ?? nested?.name;
|
|
76
94
|
const isToolResult = eventType === 'tool:result' || eventType === 'tool_result';
|
|
77
95
|
const isToolUse = eventType === 'tool:use' || eventType === 'tool_use';
|
|
@@ -116,6 +134,7 @@ export function normalizeStructuredLog(data) {
|
|
|
116
134
|
...(correlationId ? { correlationId } : {}),
|
|
117
135
|
...(sessionId ? { sessionId } : lifecycleRecord ? { sessionId: 'unknown' } : {}),
|
|
118
136
|
...(agentAid ? { agentAid } : lifecycleRecord ? { agentAid: 'unknown' } : {}),
|
|
137
|
+
...(agentName ? { agentName } : lifecycleRecord ? { agentName: 'unknown' } : {}),
|
|
119
138
|
...(permissionMode ? { permissionMode } : lifecycleRecord ? { permissionMode: 'unknown' } : {}),
|
|
120
139
|
...(toolName ? { toolName } : {}),
|
|
121
140
|
...(decision ? { decision } : {}),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import { spawnSync } from 'child_process';
|
|
3
|
+
import { parseCimDate } from './process-introspect.js';
|
|
3
4
|
function runnerKind(name) {
|
|
4
5
|
const normalized = name.toLowerCase();
|
|
5
6
|
if (normalized.includes('codex'))
|
|
@@ -164,7 +165,7 @@ function readUnixPsMetrics() {
|
|
|
164
165
|
function readWindowsMetrics() {
|
|
165
166
|
const command = [
|
|
166
167
|
'Get-CimInstance Win32_Process',
|
|
167
|
-
|
|
168
|
+
"Select-Object ProcessId,ParentProcessId,Name,WorkingSetSize,KernelModeTime,UserModeTime,@{Name='CreationDate';Expression={if ($_.CreationDate) {$_.CreationDate.ToUniversalTime().ToString('o')}}}",
|
|
168
169
|
'ConvertTo-Json -Compress',
|
|
169
170
|
].join(' | ');
|
|
170
171
|
const result = spawnSync('powershell', ['-NoProfile', '-Command', command], {
|
|
@@ -184,13 +185,32 @@ function readWindowsMetrics() {
|
|
|
184
185
|
name: String(row.Name || 'unknown'),
|
|
185
186
|
rss: Number(row.WorkingSetSize) || 0,
|
|
186
187
|
cumulativeCpuMs: ((Number(row.KernelModeTime) || 0) + (Number(row.UserModeTime) || 0)) / 10_000,
|
|
187
|
-
|
|
188
|
+
// Keep a comparable numeric start time when PowerShell serializes CIM
|
|
189
|
+
// DateTime as either CIM text or an ISO date. The raw value remains as
|
|
190
|
+
// a fallback so PID reuse protection in ProcessTreeSampler still works
|
|
191
|
+
// when a platform returns an unfamiliar representation.
|
|
192
|
+
startId: parseWindowsCreationDate(row.CreationDate),
|
|
188
193
|
})).filter((row) => row.pid > 0);
|
|
189
194
|
}
|
|
190
195
|
catch {
|
|
191
196
|
return [];
|
|
192
197
|
}
|
|
193
198
|
}
|
|
199
|
+
export function parseWindowsCreationDate(value) {
|
|
200
|
+
if (value === undefined || value === null)
|
|
201
|
+
return undefined;
|
|
202
|
+
const raw = String(value).trim();
|
|
203
|
+
if (!raw)
|
|
204
|
+
return undefined;
|
|
205
|
+
const cim = parseCimDate(raw);
|
|
206
|
+
if (cim !== null)
|
|
207
|
+
return cim;
|
|
208
|
+
const serializedDate = raw.match(/^\/Date\((-?\d+)(?:[+-]\d{4})?\)\/$/);
|
|
209
|
+
if (serializedDate)
|
|
210
|
+
return Number(serializedDate[1]);
|
|
211
|
+
const parsed = Date.parse(raw);
|
|
212
|
+
return Number.isFinite(parsed) ? parsed : raw;
|
|
213
|
+
}
|
|
194
214
|
export function readProcessMetrics() {
|
|
195
215
|
if (process.platform === 'linux')
|
|
196
216
|
return readLinuxMetrics();
|
|
@@ -209,9 +229,9 @@ export class ProcessTreeSampler {
|
|
|
209
229
|
this.readMetrics = readMetrics;
|
|
210
230
|
this.now = now;
|
|
211
231
|
}
|
|
212
|
-
sample(associatedRootPids = []) {
|
|
232
|
+
sample(associatedRootPids = [], metricsOverride) {
|
|
213
233
|
const timestamp = this.now();
|
|
214
|
-
const metrics = this.readMetrics();
|
|
234
|
+
const metrics = metricsOverride ?? this.readMetrics();
|
|
215
235
|
const byPid = new Map(metrics.map(metric => [metric.pid, metric]));
|
|
216
236
|
const children = new Map();
|
|
217
237
|
for (const metric of metrics) {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { parentPort } from 'node:worker_threads';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
3
|
+
import { readProcessMetrics } from './process-tree-stats.js';
|
|
4
|
+
const port = parentPort;
|
|
5
|
+
if (!port)
|
|
6
|
+
throw new Error('process-tree worker requires a parent port');
|
|
7
|
+
port.on('message', (message) => {
|
|
8
|
+
if (message?.type !== 'sample')
|
|
9
|
+
return;
|
|
10
|
+
const startedAt = performance.now();
|
|
11
|
+
try {
|
|
12
|
+
const metrics = readProcessMetrics();
|
|
13
|
+
if (metrics.length === 0)
|
|
14
|
+
throw new Error('process enumeration returned no metrics');
|
|
15
|
+
const response = {
|
|
16
|
+
type: 'sample',
|
|
17
|
+
requestId: message.requestId,
|
|
18
|
+
metrics,
|
|
19
|
+
durationMs: Math.round((performance.now() - startedAt) * 10) / 10,
|
|
20
|
+
};
|
|
21
|
+
port.postMessage(response);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
port.postMessage({
|
|
25
|
+
type: 'sample-error',
|
|
26
|
+
requestId: message.requestId,
|
|
27
|
+
error: error instanceof Error ? error.message : String(error),
|
|
28
|
+
durationMs: Math.round((performance.now() - startedAt) * 10) / 10,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
});
|