evolcore 0.0.2 → 0.0.3
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 +44 -793
- package/dist/agents/claude-runner.js +197 -17
- package/dist/agents/codex-runner.js +46 -3
- package/dist/aun/outbox.js +8 -0
- package/dist/channels/aun.js +21 -4
- package/dist/channels/contact-bind-code.js +134 -0
- package/dist/channels/dingtalk.js +979 -149
- package/dist/channels/feishu.js +130 -54
- package/dist/channels/wecom-card.js +101 -0
- package/dist/channels/wecom-onboarding.js +82 -0
- package/dist/channels/wecom-state.js +191 -0
- package/dist/channels/wecom.js +755 -163
- package/dist/cli/agent-command.js +2 -1
- package/dist/cli/aun-commands.js +88 -33
- package/dist/cli/bench.js +2 -2
- package/dist/cli/contact.js +71 -0
- package/dist/cli/ctl-command.js +2 -2
- package/dist/cli/daemon-commands.js +77 -214
- package/dist/cli/handoff-command.js +2 -2
- package/dist/cli/help.js +9 -5
- package/dist/cli/index.js +132 -116
- package/dist/cli/init-channel.js +92 -97
- package/dist/cli/init.js +63 -26
- package/dist/cli/model.js +2 -1
- package/dist/cli/net-check.js +2 -2
- package/dist/cli/queue-command.js +30 -6
- package/dist/cli/raw-key-input.js +25 -0
- package/dist/cli/response.js +5 -6
- package/dist/cli/restart-monitor.js +25 -1
- package/dist/cli/stats.js +6 -4
- package/dist/cli/trigger-command.js +55 -15
- package/dist/cli/version.js +6 -1
- package/dist/config/builtin-role-templates.js +22 -10
- package/dist/config/builtin-roles.js +7 -1
- package/dist/config/config-manager.js +221 -17
- package/dist/config/contact-alias.js +68 -0
- package/dist/config/contact-book-store.js +454 -0
- package/dist/config/contact-book-v2-startup.js +35 -0
- package/dist/config/contact-book.js +156 -303
- package/dist/config/contact-operation-service.js +110 -0
- package/dist/config/peer-role-resolver.js +133 -54
- package/dist/config/role-ranks.js +18 -0
- package/dist/config/role-service.js +16 -19
- package/dist/config/role-store.js +16 -5
- package/dist/config/roles.js +10 -1
- package/dist/config-store.js +0 -2
- package/dist/core/auth/authorization-audit.js +10 -1
- package/dist/core/auth/operation-authorizer.js +2 -0
- package/dist/core/auth/operation-catalog.js +56 -0
- package/dist/core/command/command-handler.js +105 -10
- package/dist/core/command/connect-menu.js +374 -0
- package/dist/core/command/menu-handler.js +114 -16
- package/dist/core/command/role-menu.js +128 -29
- package/dist/core/command/slash-handler.js +28 -18
- package/dist/core/daemon-file-cache.js +12 -6
- package/dist/core/event-catalog.js +70 -0
- package/dist/core/evolagent-registry.js +0 -1
- package/dist/core/evolagent.js +20 -9
- package/dist/core/message/im-renderer.js +2 -0
- package/dist/core/message/message-bridge.js +79 -8
- package/dist/core/message/message-queue.js +10 -0
- package/dist/core/message/message-utils.js +8 -2
- package/dist/core/message/response-engine.js +269 -25
- package/dist/core/message/send-receipt.js +24 -0
- package/dist/core/message/stream-debouncer.js +11 -2
- package/dist/core/permission/tool-policy.js +47 -15
- package/dist/core/protected-paths.js +2 -0
- package/dist/core/session/session-fs-store.js +44 -3
- package/dist/core/session/session-manager.js +61 -5
- package/dist/index.js +62 -6
- package/dist/ipc.js +34 -5
- package/dist/stats/billing.js +20 -8
- package/dist/trigger/manager.js +3 -0
- package/dist/trigger/parser.js +77 -2
- package/dist/trigger/patch.js +8 -1
- package/dist/trigger/scheduler.js +3 -1
- package/dist/trigger/validation.js +13 -0
- package/dist/utils/aid-bind.js +43 -29
- package/dist/utils/instance-registry.js +14 -7
- package/dist/utils/log-writer.js +46 -0
- package/dist/utils/media-cache.js +4 -1
- package/dist/utils/model-prices.jsonl +6 -3
- package/dist/utils/restart-safety.js +31 -0
- package/dist/utils/system-memory.js +62 -0
- package/kits/docs/INDEX.md +2 -1
- package/kits/docs/evolcore/INDEX.md +5 -3
- package/kits/docs/evolcore/agent.md +9 -1
- package/kits/docs/evolcore/aid.md +5 -2
- package/kits/docs/evolcore/contact.md +57 -0
- package/kits/docs/evolcore/fs.md +9 -0
- package/kits/docs/evolcore/group.md +12 -3
- package/kits/docs/evolcore/model.md +4 -1
- package/kits/docs/evolcore/msg.md +9 -3
- package/kits/docs/evolcore/response.md +16 -21
- package/kits/docs/evolcore/rpc.md +2 -0
- package/kits/docs/evolcore/stats.md +15 -2
- package/kits/docs/evolcore/storage.md +1 -0
- package/kits/docs/evolcore/trigger.md +17 -2
- package/kits/eck_manifest.json +12 -0
- package/kits/migrations/migrate-contact-book-v2.mjs +747 -0
- package/kits/schemas/_meta.json +7 -4
- package/kits/schemas/agent-config.schema.6.json +322 -0
- package/kits/schemas/contact-book.schema.2.json +43 -0
- package/kits/schemas/relation-config.schema.5.json +47 -0
- package/kits/schemas/role-config.schema.1.json +1 -0
- package/kits/schemas/role-registry.schema.1.json +2 -2
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/owner.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/kits/templates/system-fragments/commands.md +3 -1
- package/package.json +4 -4
- package/assets/brand/evolcore/README.md +0 -19
- package/assets/brand/evolcore/evolcore-app-icon.png +0 -0
- package/assets/brand/evolcore/evolcore-app-icon.svg +0 -13
- package/assets/brand/evolcore/evolcore-brand-board.png +0 -0
- package/assets/brand/evolcore/evolcore-brand-board.svg +0 -126
- package/assets/brand/evolcore/evolcore-logo-kit.zip +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.png +0 -0
- package/assets/brand/evolcore/evolcore-logo-reverse.svg +0 -14
- package/assets/brand/evolcore/evolcore-logo.png +0 -0
- package/assets/brand/evolcore/evolcore-logo.svg +0 -14
- package/assets/brand/evolcore/evolcore-mark.png +0 -0
- package/assets/brand/evolcore/evolcore-mark.svg +0 -10
|
@@ -428,14 +428,47 @@ export class ResponseEngine {
|
|
|
428
428
|
if (!session)
|
|
429
429
|
return;
|
|
430
430
|
this.turnCoordinator.hydrate(session);
|
|
431
|
-
this.turnCoordinator.invalidate(sessionId, reason);
|
|
431
|
+
const interruptedTurn = this.turnCoordinator.invalidate(sessionId, reason);
|
|
432
|
+
const interruptedTaskId = interruptedTurn?.active?.taskId;
|
|
432
433
|
this.interruptedSessions.set(sessionId, reason);
|
|
433
434
|
await this.interactionRouter?.cancelAll(sessionId, reason);
|
|
434
|
-
const
|
|
435
|
-
|
|
435
|
+
const channelKey = session.metadata?.channelKey || session.channel;
|
|
436
|
+
const agent = this.getAgent(channelKey, session.baseagent, session.selfAID);
|
|
437
|
+
const terminalStatus = (() => {
|
|
438
|
+
// TimeoutController owns timeout delivery (including user-facing timeout
|
|
439
|
+
// details) and already guarantees a single terminal status. Sending it
|
|
440
|
+
// here as well would race that barrier and duplicate status.timeout.
|
|
441
|
+
if (!interruptedTaskId || reason === 'timeout')
|
|
442
|
+
return Promise.resolve();
|
|
443
|
+
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
444
|
+
if (!channelInfo)
|
|
445
|
+
return Promise.resolve();
|
|
446
|
+
const envelope = buildEnvelope({
|
|
447
|
+
taskId: interruptedTaskId,
|
|
448
|
+
sessionId: session.id,
|
|
449
|
+
channel: channelInfo.adapter.channelName,
|
|
450
|
+
channelId: session.channelId,
|
|
451
|
+
agentName: agent.name,
|
|
452
|
+
chatmode: this.resolveEffectiveChatmodeForSession(session, channelKey, session.channelId),
|
|
453
|
+
replyContext: {
|
|
454
|
+
...(session.metadata?.replyContext ?? {}),
|
|
455
|
+
sessionId: session.id,
|
|
456
|
+
threadId: session.threadId || session.metadata?.replyContext?.threadId,
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
const payload = {
|
|
460
|
+
kind: 'status.interrupted',
|
|
461
|
+
metadata: { reason },
|
|
462
|
+
};
|
|
463
|
+
return channelInfo.adapter.send(envelope, payload).then(() => undefined).catch(error => {
|
|
464
|
+
logger.debug(`[ResponseEngine] interrupt terminal status failure: session=${sessionId} task=${interruptedTaskId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
465
|
+
});
|
|
466
|
+
})();
|
|
467
|
+
const runnerInterrupt = agent.interrupt(sessionId).catch(error => {
|
|
436
468
|
logger.debug(`[ResponseEngine] interrupt barrier runner failure: session=${sessionId} error=${error instanceof Error ? error.message : String(error)}`);
|
|
437
469
|
});
|
|
438
|
-
|
|
470
|
+
await Promise.all([terminalStatus, runnerInterrupt]);
|
|
471
|
+
const taskId = (await this.turnCoordinator.completeInterrupt(session, reason)) ?? interruptedTaskId;
|
|
439
472
|
if (taskId) {
|
|
440
473
|
this.sessionManager.clearProcessingIfTask(sessionId, taskId);
|
|
441
474
|
if (this.activeTaskRuntimeContexts.get(sessionId)?.taskId === taskId) {
|
|
@@ -986,19 +1019,34 @@ export class ResponseEngine {
|
|
|
986
1019
|
let rejectFn;
|
|
987
1020
|
let lastIdleSec = 0;
|
|
988
1021
|
let timeoutTriggered = false;
|
|
1022
|
+
const timeoutControl = {
|
|
1023
|
+
claimedByInternal: false,
|
|
1024
|
+
terminalDelivered: false,
|
|
1025
|
+
claim: () => {
|
|
1026
|
+
if (!timeoutControl.error || this.interruptedSessions.get(session.id) !== 'timeout')
|
|
1027
|
+
return undefined;
|
|
1028
|
+
timeoutControl.claimedByInternal = true;
|
|
1029
|
+
return timeoutControl.error;
|
|
1030
|
+
},
|
|
1031
|
+
};
|
|
989
1032
|
const rejectAfterInterruptBarrier = (error) => {
|
|
990
1033
|
if (timeoutTriggered)
|
|
991
1034
|
return;
|
|
992
1035
|
timeoutTriggered = true;
|
|
1036
|
+
timeoutControl.error = error;
|
|
993
1037
|
this.timeoutErrors.set(session.id, error);
|
|
994
1038
|
this.turnCoordinator.hydrate(session);
|
|
995
1039
|
this.turnCoordinator.invalidate(session.id, 'timeout');
|
|
996
1040
|
this.interruptedSessions.set(session.id, 'timeout');
|
|
997
|
-
|
|
1041
|
+
const barrier = this.interruptSession(session.id, 'timeout')
|
|
998
1042
|
.catch(interruptError => {
|
|
999
1043
|
logger.debug(`[ResponseEngine] Timeout interrupt barrier failed: ${interruptError instanceof Error ? interruptError.message : String(interruptError)}`);
|
|
1000
|
-
})
|
|
1001
|
-
|
|
1044
|
+
});
|
|
1045
|
+
timeoutControl.barrier = barrier;
|
|
1046
|
+
void barrier.finally(() => {
|
|
1047
|
+
if (!timeoutControl.claimedByInternal)
|
|
1048
|
+
rejectFn(error);
|
|
1049
|
+
});
|
|
1002
1050
|
};
|
|
1003
1051
|
const triggerAttemptId = message.replyContext?.metadata?.triggerAttemptId;
|
|
1004
1052
|
const resetTimer = (eventType, toolName) => {
|
|
@@ -1052,13 +1100,27 @@ export class ResponseEngine {
|
|
|
1052
1100
|
totalExecutionTimer.unref?.();
|
|
1053
1101
|
});
|
|
1054
1102
|
try {
|
|
1103
|
+
const processingPromise = this._processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, () => lastIdleSec, outputState, timeoutControl);
|
|
1104
|
+
const guardedProcessingPromise = processingPromise.then(async () => {
|
|
1105
|
+
if (!timeoutControl.error)
|
|
1106
|
+
return;
|
|
1107
|
+
await timeoutControl.barrier;
|
|
1108
|
+
throw timeoutControl.error;
|
|
1109
|
+
});
|
|
1055
1110
|
await Promise.race([
|
|
1056
|
-
|
|
1111
|
+
guardedProcessingPromise,
|
|
1057
1112
|
timeoutPromise,
|
|
1058
1113
|
totalExecutionPromise,
|
|
1059
1114
|
]);
|
|
1060
1115
|
}
|
|
1061
1116
|
catch (error) {
|
|
1117
|
+
if (error instanceof Error && (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT')) {
|
|
1118
|
+
await timeoutControl.barrier;
|
|
1119
|
+
if (!timeoutControl.terminalDelivered) {
|
|
1120
|
+
await timeoutControl.deliverUnhandled?.(error);
|
|
1121
|
+
}
|
|
1122
|
+
this.timeoutErrors.delete(session.id);
|
|
1123
|
+
}
|
|
1062
1124
|
// 超时错误:kill 级别已发送诊断信息,无需再发
|
|
1063
1125
|
// 非超时错误走通用处理
|
|
1064
1126
|
// 记录错误到健康状态(复用已有 session)
|
|
@@ -1112,7 +1174,7 @@ export class ResponseEngine {
|
|
|
1112
1174
|
: 60 * 60 * 1000;
|
|
1113
1175
|
}
|
|
1114
1176
|
/** 自动安全模式已禁用:仅保留错误计数,不再自动切换状态 */
|
|
1115
|
-
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec, outputState = { middleOutputMode: 'all' }) {
|
|
1177
|
+
async _processMessageInternal(message, session, absoluteProjectPath, resetTimer, shouldSuppress, getLastIdleSec, outputState = { middleOutputMode: 'all' }, timeoutControl) {
|
|
1116
1178
|
const messageId = `${message.channel}_${message.channelId}_${message.timestamp || Date.now()}`;
|
|
1117
1179
|
const channelKey = this.messageChannelKey(message, session);
|
|
1118
1180
|
const channelInfo = this.resolveChannelInfo(channelKey);
|
|
@@ -1386,6 +1448,71 @@ export class ResponseEngine {
|
|
|
1386
1448
|
replyContext: taskReplyContext(),
|
|
1387
1449
|
causation: taskCausation,
|
|
1388
1450
|
});
|
|
1451
|
+
if (timeoutControl) {
|
|
1452
|
+
timeoutControl.deliverUnhandled = async (timeoutError) => {
|
|
1453
|
+
if (timeoutControl.terminalDelivered)
|
|
1454
|
+
return;
|
|
1455
|
+
timeoutControl.terminalDelivered = true;
|
|
1456
|
+
const isTotalExecutionTimeout = timeoutError.message === 'TOTAL_EXECUTION_TIMEOUT';
|
|
1457
|
+
const totalExecutionMs = this.totalExecutionLimitMs();
|
|
1458
|
+
const statusPayload = {
|
|
1459
|
+
kind: 'status.timeout',
|
|
1460
|
+
metadata: isTotalExecutionTimeout
|
|
1461
|
+
? { totalExecutionMs }
|
|
1462
|
+
: { idleSec: getLastIdleSec?.() || undefined },
|
|
1463
|
+
};
|
|
1464
|
+
const idleSec = getLastIdleSec?.() || 0;
|
|
1465
|
+
const userMessage = isTotalExecutionTimeout
|
|
1466
|
+
? `⚠️ 任务超过总执行时限(${Math.round(totalExecutionMs / 1000)}秒),已自动中断`
|
|
1467
|
+
: idleSec > 0
|
|
1468
|
+
? `⚠️ 任务超时(${idleSec}秒无响应),已自动中断`
|
|
1469
|
+
: '⚠️ 任务超时,已自动中断';
|
|
1470
|
+
agent.cleanupStream(streamKey);
|
|
1471
|
+
this.sessionManager.clearProcessingIfTask(session.id, taskId);
|
|
1472
|
+
if (this.activeTaskRuntimeContexts.get(session.id)?.taskId === taskId) {
|
|
1473
|
+
this.activeTaskRuntimeContexts.delete(session.id);
|
|
1474
|
+
}
|
|
1475
|
+
this.agentDelegationRegistry?.revokeTask(session.id, taskId);
|
|
1476
|
+
const daemonTrigger = this.isTrustedDaemonTrigger(message);
|
|
1477
|
+
if (!daemonTrigger) {
|
|
1478
|
+
await adapter.send(envelope, statusPayload).catch(() => { });
|
|
1479
|
+
}
|
|
1480
|
+
await adapter.send(envelope, {
|
|
1481
|
+
kind: 'result.error',
|
|
1482
|
+
text: userMessage,
|
|
1483
|
+
reason: isTotalExecutionTimeout ? 'total_execution_timeout' : 'timeout',
|
|
1484
|
+
}).catch(() => { });
|
|
1485
|
+
if (daemonTrigger) {
|
|
1486
|
+
await adapter.send(envelope, statusPayload).catch(() => { });
|
|
1487
|
+
}
|
|
1488
|
+
this.touchAgentActivity(channelKey);
|
|
1489
|
+
this.publishTriggerExecutionFailure(message, timeoutError.message, {
|
|
1490
|
+
messageId,
|
|
1491
|
+
causation: taskCausation,
|
|
1492
|
+
});
|
|
1493
|
+
const errorType = prefixErrorType(ERROR_PREFIX.INFRA, classifyError(timeoutError));
|
|
1494
|
+
this.eventBus.publish({
|
|
1495
|
+
type: 'task:error',
|
|
1496
|
+
sessionId: session.id,
|
|
1497
|
+
error: timeoutError.message,
|
|
1498
|
+
errorType,
|
|
1499
|
+
agentName: agentNameForStats,
|
|
1500
|
+
causation: taskCausation,
|
|
1501
|
+
});
|
|
1502
|
+
recordCausationSpan(taskCausation, 'task.run', {
|
|
1503
|
+
status: 'failed',
|
|
1504
|
+
refs: { taskId, sessionId: session.id, messageId },
|
|
1505
|
+
reason: errorType,
|
|
1506
|
+
});
|
|
1507
|
+
logger.message({
|
|
1508
|
+
msgId: messageId,
|
|
1509
|
+
sessionId: session.id,
|
|
1510
|
+
dir: 'inbound',
|
|
1511
|
+
status: 'failed',
|
|
1512
|
+
error: timeoutError.message,
|
|
1513
|
+
});
|
|
1514
|
+
};
|
|
1515
|
+
}
|
|
1389
1516
|
try {
|
|
1390
1517
|
const isBackground = this.isBackgroundSession(session, message.channel, message.channelId);
|
|
1391
1518
|
// 记录收到消息
|
|
@@ -1474,10 +1601,24 @@ export class ResponseEngine {
|
|
|
1474
1601
|
const peerShort = peerId ? peerId.split('.')[0].split(':')[0] : '?';
|
|
1475
1602
|
const peerLabel = peerName && peerName !== peerShort ? `${peerShort}(${peerName})` : peerShort;
|
|
1476
1603
|
logger.info(`[ResponseEngine] session=${session.id} task=${taskId} peer=${peerLabel} chatType=${session.chatType} chatMode=${effectiveChatMode} baseagent=${session.baseagent} msgChatType=${message.chatType ?? 'n/a'}`);
|
|
1604
|
+
// Establish the durable turn before exposing any processing state. This
|
|
1605
|
+
// lets an immediate /stop or replacement message close the same task and
|
|
1606
|
+
// prevents a BusinessTrip reaction from becoming orphaned during setup.
|
|
1607
|
+
turnLease = await this.turnCoordinator.begin(session, taskId);
|
|
1477
1608
|
// 记录开始处理
|
|
1478
1609
|
const taskEncrypt = message.replyContext?.metadata?.encrypted != null ? !!(message.replyContext.metadata.encrypted) : undefined;
|
|
1479
1610
|
this.eventBus.publish({ type: 'task:started', sessionId: session.id, agentName: agentNameForStats, encrypt: taskEncrypt, chatmode, causation: taskCausation });
|
|
1480
1611
|
this.touchAgentActivity(channelKey);
|
|
1612
|
+
// Upgrade the channel acknowledgement at task start, before compaction and
|
|
1613
|
+
// runner setup, so processing feedback remains visible for the whole run.
|
|
1614
|
+
// Trigger messageId is an internal runId and has no channel reaction.
|
|
1615
|
+
if (message.messageId && message.source !== 'trigger') {
|
|
1616
|
+
adapter.promoteAck?.(message.messageId, {
|
|
1617
|
+
taskId,
|
|
1618
|
+
channelId: message.channelId,
|
|
1619
|
+
messageIds: message.sourceMessageIds,
|
|
1620
|
+
}).catch(() => { });
|
|
1621
|
+
}
|
|
1481
1622
|
adapter.send(envelope, { kind: 'status.started' }).catch(() => { });
|
|
1482
1623
|
await this.runPendingAutoCompactAtTaskStart(session, agent, absoluteProjectPath, adapter, envelope, false);
|
|
1483
1624
|
logger.message({
|
|
@@ -1659,7 +1800,6 @@ export class ResponseEngine {
|
|
|
1659
1800
|
recordTriggerExecutionAnomaly(triggerRunId, anomaly);
|
|
1660
1801
|
}
|
|
1661
1802
|
: undefined;
|
|
1662
|
-
turnLease = await this.turnCoordinator.begin(session, taskId);
|
|
1663
1803
|
// 设置权限审批的交互上下文(支持交互卡片)
|
|
1664
1804
|
agent.setPermissionContext?.(session.id, {
|
|
1665
1805
|
sendPrompt: permissionPromptForOrigin,
|
|
@@ -2270,11 +2410,6 @@ export class ResponseEngine {
|
|
|
2270
2410
|
logger.error('[ResponseEngine] Failed to record retry health status:', statusError);
|
|
2271
2411
|
}
|
|
2272
2412
|
};
|
|
2273
|
-
// Runner 开始执行前:将 Pin 升级为 CheckMark(表示"正在处理")。
|
|
2274
|
-
// Trigger messageId is an internal runId, so there is no channel ack to promote.
|
|
2275
|
-
if (message.messageId && message.source !== 'trigger') {
|
|
2276
|
-
adapter.promoteAck?.(message.messageId).catch(() => { });
|
|
2277
|
-
}
|
|
2278
2413
|
while (true) {
|
|
2279
2414
|
let streamRegistered = false;
|
|
2280
2415
|
try {
|
|
@@ -2383,6 +2518,10 @@ export class ResponseEngine {
|
|
|
2383
2518
|
throw error;
|
|
2384
2519
|
}
|
|
2385
2520
|
}
|
|
2521
|
+
const pendingTimeoutError = timeoutControl?.claim();
|
|
2522
|
+
if (pendingTimeoutError) {
|
|
2523
|
+
throw pendingTimeoutError;
|
|
2524
|
+
}
|
|
2386
2525
|
if (permissionTerminalFailure) {
|
|
2387
2526
|
renderer.discardPending();
|
|
2388
2527
|
streamResult = {
|
|
@@ -2734,6 +2873,12 @@ export class ResponseEngine {
|
|
|
2734
2873
|
return;
|
|
2735
2874
|
}
|
|
2736
2875
|
const durationMs = Date.now() - startTime;
|
|
2876
|
+
const statsModel = streamResult.lastModelCall?.model
|
|
2877
|
+
|| streamResult.model
|
|
2878
|
+
|| streamResult.contextUsage?.model
|
|
2879
|
+
|| modelOverride?.model
|
|
2880
|
+
|| agentModel
|
|
2881
|
+
|| 'unknown';
|
|
2737
2882
|
// ── Stats: 写入 usage_events(在 status.completed 之前,以便带上 cost) ──
|
|
2738
2883
|
// cost 含原价(official)与网关实际价(gateway)两套,由 insertUsageEvent 写库时一并算出。
|
|
2739
2884
|
let turnCost = { official: null, gateway: null };
|
|
@@ -2745,7 +2890,6 @@ export class ResponseEngine {
|
|
|
2745
2890
|
|| (configActorId
|
|
2746
2891
|
? formatPeerKey(configChannelType, configActorId)
|
|
2747
2892
|
: formatPeerKey(configChannelType, message.channelId));
|
|
2748
|
-
const statsModel = streamResult.contextUsage?.model || 'unknown';
|
|
2749
2893
|
const ctxPct = streamResult.contextUsage?.percentage;
|
|
2750
2894
|
const event = normalizeUsage(streamResult.tokenUsage, {
|
|
2751
2895
|
ts: Date.now(),
|
|
@@ -2803,7 +2947,6 @@ export class ResponseEngine {
|
|
|
2803
2947
|
try {
|
|
2804
2948
|
const { resolveModelSpec } = await import('../../stats/billing.js');
|
|
2805
2949
|
const { querySessionSummary } = await import('../../stats/query.js');
|
|
2806
|
-
const statsModel = streamResult.contextUsage?.model || 'unknown';
|
|
2807
2950
|
modelSpec = resolveModelSpec(resolveRoot(), statsModel);
|
|
2808
2951
|
const sum = querySessionSummary(resolveRoot(), session.id);
|
|
2809
2952
|
if (sum.calls > 0) {
|
|
@@ -2848,7 +2991,7 @@ export class ResponseEngine {
|
|
|
2848
2991
|
let lastModelCall = streamResult.lastModelCall;
|
|
2849
2992
|
if (lastModelCall?.tokenUsage) {
|
|
2850
2993
|
try {
|
|
2851
|
-
const lastModel = lastModelCall.model ||
|
|
2994
|
+
const lastModel = lastModelCall.model || statsModel;
|
|
2852
2995
|
const lastEvent = normalizeUsage(lastModelCall.tokenUsage, {
|
|
2853
2996
|
ts: Date.now(), agent_aid: '', peer_key: '', session_id: session.id,
|
|
2854
2997
|
model: lastModel, turns: 1,
|
|
@@ -2950,7 +3093,14 @@ export class ResponseEngine {
|
|
|
2950
3093
|
});
|
|
2951
3094
|
}
|
|
2952
3095
|
catch (error) {
|
|
2953
|
-
|
|
3096
|
+
const authoritativeTimeoutError = timeoutControl?.claim();
|
|
3097
|
+
if (authoritativeTimeoutError) {
|
|
3098
|
+
error = authoritativeTimeoutError;
|
|
3099
|
+
await timeoutControl?.barrier;
|
|
3100
|
+
}
|
|
3101
|
+
const handlingTimeout = error instanceof Error
|
|
3102
|
+
&& (error.message === 'SDK_TIMEOUT' || error.message === 'TOTAL_EXECUTION_TIMEOUT');
|
|
3103
|
+
if (handlingTimeout) {
|
|
2954
3104
|
this.timeoutErrors.delete(session.id);
|
|
2955
3105
|
}
|
|
2956
3106
|
const leaseWasCurrent = !!turnLease && this.turnCoordinator.isCurrent(turnLease);
|
|
@@ -3055,7 +3205,7 @@ export class ResponseEngine {
|
|
|
3055
3205
|
if (isUserInterrupt) {
|
|
3056
3206
|
logger.info(`[ResponseEngine] User interrupt by new_message, skip sending error message`);
|
|
3057
3207
|
}
|
|
3058
|
-
else if (error?._errorAlreadySent && !retryExhaustedCount) {
|
|
3208
|
+
else if (error?._errorAlreadySent && !retryExhaustedCount && !isTimeout && !isTotalExecutionTimeout) {
|
|
3059
3209
|
logger.info(`[ResponseEngine] Error already sent via renderer, skip sending duplicate message`);
|
|
3060
3210
|
}
|
|
3061
3211
|
else {
|
|
@@ -3103,6 +3253,9 @@ export class ResponseEngine {
|
|
|
3103
3253
|
}).catch(() => { });
|
|
3104
3254
|
}
|
|
3105
3255
|
}
|
|
3256
|
+
if (handlingTimeout && timeoutControl) {
|
|
3257
|
+
timeoutControl.terminalDelivered = true;
|
|
3258
|
+
}
|
|
3106
3259
|
}
|
|
3107
3260
|
// [迁移探针] 任务收尾:记录工具提醒最终状态并落盘(防线 1)
|
|
3108
3261
|
if (snapshot.isEnabled()) {
|
|
@@ -3374,7 +3527,7 @@ export class ResponseEngine {
|
|
|
3374
3527
|
assistantUuid = event.assistantUuid ?? assistantUuid;
|
|
3375
3528
|
protocolIncompleteReason = event.protocolIncompleteReason ?? protocolIncompleteReason;
|
|
3376
3529
|
}
|
|
3377
|
-
//
|
|
3530
|
+
// 记录事件类型:高价值事件(含子任务生命周期)INFO,
|
|
3378
3531
|
// 框架事件(session_id/state_changed/status)DEBUG
|
|
3379
3532
|
let eventDetail = '';
|
|
3380
3533
|
if (event.type === 'text' && event.text) {
|
|
@@ -3429,6 +3582,97 @@ export class ResponseEngine {
|
|
|
3429
3582
|
});
|
|
3430
3583
|
continue;
|
|
3431
3584
|
}
|
|
3585
|
+
if (event.type === 'task_started') {
|
|
3586
|
+
this.eventBus.publish({
|
|
3587
|
+
type: 'runner:task-started',
|
|
3588
|
+
sessionId: session.id,
|
|
3589
|
+
taskId: event.taskId,
|
|
3590
|
+
taskKind: event.taskKind,
|
|
3591
|
+
toolUseId: event.toolUseId,
|
|
3592
|
+
description: event.description,
|
|
3593
|
+
subagentType: event.subagentType,
|
|
3594
|
+
taskType: event.taskType,
|
|
3595
|
+
workflowName: event.workflowName,
|
|
3596
|
+
prompt: event.prompt,
|
|
3597
|
+
skipTranscript: event.skipTranscript,
|
|
3598
|
+
timestamp: Date.now(),
|
|
3599
|
+
causation,
|
|
3600
|
+
});
|
|
3601
|
+
if (!event.skipTranscript) {
|
|
3602
|
+
renderer.addLifecycle('started', {
|
|
3603
|
+
scope: 'runner_task',
|
|
3604
|
+
taskId: event.taskId,
|
|
3605
|
+
taskKind: event.taskKind,
|
|
3606
|
+
toolUseId: event.toolUseId,
|
|
3607
|
+
description: event.description,
|
|
3608
|
+
subagentType: event.subagentType,
|
|
3609
|
+
taskType: event.taskType,
|
|
3610
|
+
}, `${event.taskKind === 'agent' ? 'Agent' : 'Background'} task started: ${event.description}`);
|
|
3611
|
+
}
|
|
3612
|
+
continue;
|
|
3613
|
+
}
|
|
3614
|
+
if (event.type === 'background_tasks_changed') {
|
|
3615
|
+
this.eventBus.publish({
|
|
3616
|
+
type: 'runner:background-tasks-changed',
|
|
3617
|
+
sessionId: session.id,
|
|
3618
|
+
tasks: event.tasks,
|
|
3619
|
+
timestamp: Date.now(),
|
|
3620
|
+
causation,
|
|
3621
|
+
});
|
|
3622
|
+
continue;
|
|
3623
|
+
}
|
|
3624
|
+
if (event.type === 'task_notification') {
|
|
3625
|
+
this.eventBus.publish({
|
|
3626
|
+
type: 'runner:task-notification',
|
|
3627
|
+
sessionId: session.id,
|
|
3628
|
+
taskId: event.taskId,
|
|
3629
|
+
taskKind: event.taskKind,
|
|
3630
|
+
toolUseId: event.toolUseId,
|
|
3631
|
+
description: event.description,
|
|
3632
|
+
subagentType: event.subagentType,
|
|
3633
|
+
status: event.status,
|
|
3634
|
+
outputFile: event.outputFile,
|
|
3635
|
+
summary: event.summary,
|
|
3636
|
+
usage: event.usage,
|
|
3637
|
+
skipTranscript: event.skipTranscript,
|
|
3638
|
+
timestamp: Date.now(),
|
|
3639
|
+
causation,
|
|
3640
|
+
});
|
|
3641
|
+
if (!event.skipTranscript) {
|
|
3642
|
+
const label = event.description || event.taskId;
|
|
3643
|
+
renderer.addLifecycle('completed', {
|
|
3644
|
+
scope: 'runner_task',
|
|
3645
|
+
taskId: event.taskId,
|
|
3646
|
+
taskKind: event.taskKind,
|
|
3647
|
+
toolUseId: event.toolUseId,
|
|
3648
|
+
description: event.description,
|
|
3649
|
+
subagentType: event.subagentType,
|
|
3650
|
+
status: event.status,
|
|
3651
|
+
outputFile: event.outputFile,
|
|
3652
|
+
summary: event.summary,
|
|
3653
|
+
usage: event.usage,
|
|
3654
|
+
}, `${event.taskKind === 'agent' ? 'Agent' : 'Background'} task ${event.status}: ${label}`);
|
|
3655
|
+
}
|
|
3656
|
+
continue;
|
|
3657
|
+
}
|
|
3658
|
+
if (event.type === 'task_progress') {
|
|
3659
|
+
this.eventBus.publish({
|
|
3660
|
+
type: 'runner:task-progress',
|
|
3661
|
+
sessionId: session.id,
|
|
3662
|
+
taskId: event.taskId,
|
|
3663
|
+
taskKind: event.taskKind,
|
|
3664
|
+
toolUseId: event.toolUseId,
|
|
3665
|
+
description: event.description,
|
|
3666
|
+
subagentType: event.subagentType,
|
|
3667
|
+
summary: event.summary,
|
|
3668
|
+
lastToolName: event.lastToolName,
|
|
3669
|
+
totalTokens: event.totalTokens,
|
|
3670
|
+
toolUses: event.toolUses,
|
|
3671
|
+
durationMs: event.durationMs,
|
|
3672
|
+
timestamp: Date.now(),
|
|
3673
|
+
causation,
|
|
3674
|
+
});
|
|
3675
|
+
}
|
|
3432
3676
|
const isCurrentlyBackground = this.isBackgroundSession(session, session.channel, session.channelId);
|
|
3433
3677
|
// === 前台任务:正常处理所有事件 ===
|
|
3434
3678
|
if (!isCurrentlyBackground) {
|
|
@@ -3570,14 +3814,14 @@ export class ResponseEngine {
|
|
|
3570
3814
|
// 仅记录状态,最终 flush(true) 在流结束后统一执行
|
|
3571
3815
|
if (event.type === 'complete') {
|
|
3572
3816
|
const isAbort = event.terminalReason === 'aborted_streaming' || event.terminalReason === 'aborted_tools';
|
|
3573
|
-
logger.info(`[ResponseEngine] ${isAbort ? 'task interrupted' : 'complete event'}: isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
|
|
3817
|
+
logger.info(`[ResponseEngine] ${isAbort ? 'task interrupted' : 'complete event'}: queryFinal=${event.queryFinal !== false} isError=${event.isError} terminalReason=${event.terminalReason ?? 'none'} subtype=${event.subtype ?? 'none'} hasReceivedText=${hasReceivedText}`);
|
|
3574
3818
|
// 自动回填会话名称
|
|
3575
3819
|
if (event.sessionTitle && shouldAutoFillSessionTitle(session.name, session.threadId)) {
|
|
3576
3820
|
await this.sessionManager.renameSession(session.id, event.sessionTitle);
|
|
3577
3821
|
logger.info(`[ResponseEngine] Auto-filled session name: ${event.sessionTitle}`);
|
|
3578
3822
|
}
|
|
3579
3823
|
// 记录完成状态 + 最后一轮回复文本(后续 complete 覆盖前序)
|
|
3580
|
-
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage, lastModelCall: event.lastModelCall, modelCalls: event.modelCalls };
|
|
3824
|
+
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, model: event.model, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage, lastModelCall: event.lastModelCall, modelCalls: event.modelCalls };
|
|
3581
3825
|
if (!event.isError)
|
|
3582
3826
|
madeProgress = true;
|
|
3583
3827
|
// thought jsonl 写入已下沉到 aun.ts:sendThought 成功后,
|
|
@@ -3613,7 +3857,7 @@ export class ResponseEngine {
|
|
|
3613
3857
|
}
|
|
3614
3858
|
// 检测 proactive 标志位,设置 lastProactiveFlag 供模式切换提示使用
|
|
3615
3859
|
// [迁移点5] 标志位检查:由 ProactiveMode.onComplete 实现
|
|
3616
|
-
if (modeHooks?.mode?.onComplete && lastReplyText) {
|
|
3860
|
+
if (event.queryFinal !== false && modeHooks?.mode?.onComplete && lastReplyText) {
|
|
3617
3861
|
await modeHooks.mode.onComplete({
|
|
3618
3862
|
session,
|
|
3619
3863
|
state: modeHooks.state,
|
|
@@ -3647,7 +3891,7 @@ export class ResponseEngine {
|
|
|
3647
3891
|
logger.info(`[ResponseEngine] Auto-filled session name: ${event.sessionTitle}`);
|
|
3648
3892
|
}
|
|
3649
3893
|
// 记录完成状态
|
|
3650
|
-
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage, lastModelCall: event.lastModelCall };
|
|
3894
|
+
completeResult = { isError: !!event.isError, subtype: event.subtype, errors: event.errors, terminalReason: event.terminalReason, lastReplyText, fullText: event.result || '', hasReceivedText, numTurns: event.numTurns, ttftMs: event.ttftMs, model: event.model, tokenUsage: event.tokenUsage, contextUsage: event.contextUsage, lastModelCall: event.lastModelCall };
|
|
3651
3895
|
if (event.subtype === 'success')
|
|
3652
3896
|
madeProgress = true;
|
|
3653
3897
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export function sendOperationId(envelope) {
|
|
2
|
+
return envelope.operationId || envelope.taskId;
|
|
3
|
+
}
|
|
4
|
+
export function sentReceipt(envelope, messageIds, threadId) {
|
|
5
|
+
const messages = messageIds
|
|
6
|
+
.filter((messageId) => typeof messageId === 'string' && messageId.length > 0)
|
|
7
|
+
.map((messageId, partIndex) => ({ messageId, partIndex, threadId }));
|
|
8
|
+
if (messages.length === 0) {
|
|
9
|
+
throw new Error(`MISSING_MESSAGE_ID: operation=${sendOperationId(envelope)}`);
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
status: 'sent',
|
|
13
|
+
operationId: sendOperationId(envelope),
|
|
14
|
+
messages: messages,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function suppressedReceipt(envelope, reason) {
|
|
18
|
+
return {
|
|
19
|
+
status: 'suppressed',
|
|
20
|
+
operationId: sendOperationId(envelope),
|
|
21
|
+
messages: [],
|
|
22
|
+
reason,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -27,9 +27,9 @@ export class StreamDebouncer {
|
|
|
27
27
|
* 否则新建窗口。timer 到期后自动 flush。
|
|
28
28
|
*/
|
|
29
29
|
submit(key, message, enqueue) {
|
|
30
|
-
const { content, images, mentions, messageId, replyContext, ...rest } = message;
|
|
30
|
+
const { content, images, mentions, messageId, sourceMessageIds, replyContext, ...rest } = message;
|
|
31
31
|
return new Promise((resolve, reject) => {
|
|
32
|
-
const entry = { messageId, content, images, mentions, replyContext, rest, resolve, reject };
|
|
32
|
+
const entry = { messageId, sourceMessageIds, content, images, mentions, replyContext, rest, resolve, reject };
|
|
33
33
|
const win = this.pending.get(key);
|
|
34
34
|
if (win) {
|
|
35
35
|
clearTimeout(win.timer);
|
|
@@ -87,12 +87,20 @@ export class StreamDebouncer {
|
|
|
87
87
|
const allImages = [];
|
|
88
88
|
const allMentions = [];
|
|
89
89
|
const contents = [];
|
|
90
|
+
const sourceMessageIds = [];
|
|
91
|
+
const seenSourceMessageIds = new Set();
|
|
90
92
|
for (const e of entries) {
|
|
91
93
|
contents.push(e.content);
|
|
92
94
|
if (e.images)
|
|
93
95
|
allImages.push(...e.images);
|
|
94
96
|
if (e.mentions)
|
|
95
97
|
allMentions.push(...e.mentions);
|
|
98
|
+
for (const id of [...(e.sourceMessageIds ?? []), ...(e.messageId ? [e.messageId] : [])]) {
|
|
99
|
+
if (!seenSourceMessageIds.has(id)) {
|
|
100
|
+
seenSourceMessageIds.add(id);
|
|
101
|
+
sourceMessageIds.push(id);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
96
104
|
}
|
|
97
105
|
const last = entries[entries.length - 1];
|
|
98
106
|
// 合并后保留最新一条的 messageId(用于 thought 锚定与中断追踪)
|
|
@@ -110,6 +118,7 @@ export class StreamDebouncer {
|
|
|
110
118
|
mentions: allMentions.length > 0 ? allMentions : undefined,
|
|
111
119
|
replyContext: last.replyContext,
|
|
112
120
|
messageId: latestMessageId,
|
|
121
|
+
sourceMessageIds: sourceMessageIds.length > 0 ? sourceMessageIds : undefined,
|
|
113
122
|
};
|
|
114
123
|
const resolves = entries.map(e => e.resolve);
|
|
115
124
|
const rejects = entries.map(e => e.reject);
|
|
@@ -347,25 +347,45 @@ export function checkReadonly(toolName, input, projectPath, context) {
|
|
|
347
347
|
logger.warn(`[ReadonlyCheck] 🔒 Unknown tool blocked: tool=${toolName} session=${context?.sessionId} channel=${context?.channel} peer=${context?.peerId} role=${context?.role}`);
|
|
348
348
|
return { behavior: 'deny', message: `🔒 只读模式:未声明为只读的工具 ${toolName} 已拒绝执行` };
|
|
349
349
|
}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
350
|
+
const NON_FILESYSTEM_METADATA_TOOLS = new Set([
|
|
351
|
+
'Agent',
|
|
352
|
+
'AskUserQuestion',
|
|
353
|
+
'CronCreate',
|
|
354
|
+
'CronDelete',
|
|
355
|
+
'CronList',
|
|
356
|
+
'EnterPlanMode',
|
|
357
|
+
'ExitPlanMode',
|
|
358
|
+
'SendMessage',
|
|
359
|
+
'Skill',
|
|
360
|
+
'Task',
|
|
361
|
+
'TaskCreate',
|
|
362
|
+
'TaskGet',
|
|
363
|
+
'TaskList',
|
|
364
|
+
'TaskOutput',
|
|
365
|
+
'TaskStop',
|
|
366
|
+
'TaskUpdate',
|
|
367
|
+
'TeamCreate',
|
|
368
|
+
'TeamDelete',
|
|
369
|
+
'TodoWrite',
|
|
370
|
+
'ToolSearch',
|
|
371
|
+
'WebFetch',
|
|
372
|
+
'WebSearch',
|
|
373
|
+
]);
|
|
374
|
+
function collectOpaqueToolStrings(value, output) {
|
|
355
375
|
if (typeof value === 'string') {
|
|
356
|
-
|
|
376
|
+
if (value.trim())
|
|
377
|
+
output.push(value);
|
|
357
378
|
return;
|
|
358
379
|
}
|
|
359
380
|
if (Array.isArray(value)) {
|
|
360
381
|
for (const entry of value)
|
|
361
|
-
|
|
382
|
+
collectOpaqueToolStrings(entry, output);
|
|
362
383
|
return;
|
|
363
384
|
}
|
|
364
385
|
if (!value || typeof value !== 'object')
|
|
365
386
|
return;
|
|
366
|
-
for (const
|
|
367
|
-
output
|
|
368
|
-
collectStringValues(entry, output);
|
|
387
|
+
for (const entry of Object.values(value)) {
|
|
388
|
+
collectOpaqueToolStrings(entry, output);
|
|
369
389
|
}
|
|
370
390
|
}
|
|
371
391
|
function collectPermissionPathSpec(value, output, projectRootSubpaths) {
|
|
@@ -604,9 +624,18 @@ export function checkHClassWrite(toolName, input, context) {
|
|
|
604
624
|
collectFilesystemGrantPaths(input.additionalPermissions, grantPaths, projectRootGrantSubpaths);
|
|
605
625
|
collectFilesystemGrantPaths(input.permissions, grantPaths, projectRootGrantSubpaths);
|
|
606
626
|
}
|
|
607
|
-
else {
|
|
608
|
-
|
|
609
|
-
|
|
627
|
+
else if (!NON_FILESYSTEM_METADATA_TOOLS.has(toolName)) {
|
|
628
|
+
// Opaque external tools may run outside the Claude/Codex filesystem
|
|
629
|
+
// sandbox. Retain conservative string inspection for those tools, while
|
|
630
|
+
// excluding known orchestration tools whose payload is only metadata.
|
|
631
|
+
collectOpaqueToolStrings(input, paths);
|
|
632
|
+
}
|
|
633
|
+
// Agent/Skill/Task and other orchestration tools carry natural-language
|
|
634
|
+
// metadata, not direct filesystem operands. Treating every string in their
|
|
635
|
+
// payload as a path makes a prompt that merely mentions daemon.json or a
|
|
636
|
+
// *.json_ backup indistinguishable from an actual file access. Any nested
|
|
637
|
+
// filesystem operation is checked again when it reaches Read/Write/Bash,
|
|
638
|
+
// FileChange, or PermissionGrant above.
|
|
610
639
|
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
611
640
|
if (requestsFilesystemRoot(input.permissions) || requestsFilesystemRoot(input.additionalPermissions)) {
|
|
612
641
|
logger.warn(`[H-Class Protection] 🔒 Permission grant covers filesystem root: tool=${toolName} ` +
|
|
@@ -729,9 +758,12 @@ export function checkLClassWrite(toolName, input, context) {
|
|
|
729
758
|
collectFilesystemWriteGrantPaths(input.additionalPermissions, writeGrantPaths, projectRootWriteSubpaths);
|
|
730
759
|
collectFilesystemWriteGrantPaths(input.permissions, writeGrantPaths, projectRootWriteSubpaths);
|
|
731
760
|
}
|
|
732
|
-
else {
|
|
733
|
-
|
|
761
|
+
else if (!NON_FILESYSTEM_METADATA_TOOLS.has(toolName)) {
|
|
762
|
+
collectOpaqueToolStrings(input, paths);
|
|
734
763
|
}
|
|
764
|
+
// Do not infer mutation targets from arbitrary orchestration-tool strings.
|
|
765
|
+
// Skills commonly pass args: "" and delegation prompts may discuss
|
|
766
|
+
// protected paths; neither is itself a filesystem mutation.
|
|
735
767
|
const pathOptions = { cwd: context?.projectPath, root: context?.root };
|
|
736
768
|
for (const grantPath of writeGrantPaths) {
|
|
737
769
|
if (grantPath && lClassGrantIncludesProtectedPath(grantPath, pathOptions)) {
|
|
@@ -306,6 +306,8 @@ export function hClassGrantIncludesProtectedPath(value, options = {}) {
|
|
|
306
306
|
return true;
|
|
307
307
|
}
|
|
308
308
|
export function lClassGrantIncludesProtectedPath(value, options = {}) {
|
|
309
|
+
if (!value.trim())
|
|
310
|
+
return false;
|
|
309
311
|
const root = resolveThroughExistingAncestor(options.root ?? resolveRoot());
|
|
310
312
|
const protectedRoot = path.join(root, 'data', 'triggers');
|
|
311
313
|
if (isLClassPath(value, { cwd: options.cwd, root }))
|