@lawrenceliang-btc/atel-sdk 1.1.9 → 1.1.10
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/bin/atel.mjs +321 -16
- package/bin/notification-action-helpers.mjs +4 -7
- package/package.json +1 -1
package/bin/atel.mjs
CHANGED
|
@@ -192,6 +192,20 @@ function sanitizeAgentPrompt(promptText, meta = {}) {
|
|
|
192
192
|
return truncated;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
function isKnownUpstreamModelInputError(text) {
|
|
196
|
+
const value = String(text || '');
|
|
197
|
+
return value.includes('InternalError.Algo.InvalidParameter')
|
|
198
|
+
|| value.includes('Range of input length should be [1, 258048]');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function summarizeAgentOutput(text, maxChars = 300) {
|
|
202
|
+
const raw = String(text || '');
|
|
203
|
+
const trimmed = raw.trim();
|
|
204
|
+
if (!trimmed) return '';
|
|
205
|
+
if (isKnownUpstreamModelInputError(trimmed)) return '[suppressed upstream model input-length error]';
|
|
206
|
+
return trimmed.substring(0, maxChars);
|
|
207
|
+
}
|
|
208
|
+
|
|
195
209
|
// ═══════════════════════════════════════════════════════════════════
|
|
196
210
|
// Notification Target System — auto-discover gateway, manage targets
|
|
197
211
|
// ═══════════════════════════════════════════════════════════════════
|
|
@@ -440,6 +454,18 @@ async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey)
|
|
|
440
454
|
if (command.length === 0) {
|
|
441
455
|
return { ok: false, skipped: true, reason: 'empty_command' };
|
|
442
456
|
}
|
|
457
|
+
const actionKey = JSON.stringify(command);
|
|
458
|
+
if (globalThis.__atelActiveDirectActionKeys?.has(actionKey)) {
|
|
459
|
+
log({
|
|
460
|
+
event: 'recommended_action_direct_skip',
|
|
461
|
+
eventType,
|
|
462
|
+
dedupeKey,
|
|
463
|
+
action: action.action,
|
|
464
|
+
command,
|
|
465
|
+
reason: 'inflight_duplicate',
|
|
466
|
+
});
|
|
467
|
+
return { ok: true, skipped: true, reason: 'inflight_duplicate' };
|
|
468
|
+
}
|
|
443
469
|
|
|
444
470
|
// Idempotency guard: short-circuit duplicate milestone plan/submit/verify actions
|
|
445
471
|
// if the order or milestone has already advanced past the required state.
|
|
@@ -476,8 +502,10 @@ async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey)
|
|
|
476
502
|
}
|
|
477
503
|
const milestone = needsMilestoneIndex && Array.isArray(state?.milestones) ? state.milestones.find((m) => m.index === index) : null;
|
|
478
504
|
if (needsMilestoneIndex && milestone) {
|
|
479
|
-
const
|
|
480
|
-
|
|
505
|
+
const expectedStatuses = command[1] === 'milestone-verify'
|
|
506
|
+
? ['submitted']
|
|
507
|
+
: (eventType === 'milestone_rejected' ? ['pending', 'rejected'] : ['pending']);
|
|
508
|
+
if (!expectedStatuses.includes(milestone.status)) {
|
|
481
509
|
log({
|
|
482
510
|
event: 'recommended_action_direct_skip',
|
|
483
511
|
eventType,
|
|
@@ -509,10 +537,65 @@ async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey)
|
|
|
509
537
|
const childCwd = command[0] === 'atel' ? getAtelWorkspaceRoot() : cwd;
|
|
510
538
|
|
|
511
539
|
log({ event: 'recommended_action_direct_trigger', eventType, dedupeKey, action: action.action, command });
|
|
540
|
+
globalThis.__atelActiveDirectActionKeys ??= new Set();
|
|
541
|
+
globalThis.__atelActiveDirectActionKeys.add(actionKey);
|
|
512
542
|
|
|
513
543
|
return await new Promise((resolve) => {
|
|
514
544
|
execFile(childCmd, childArgs, { timeout: 180000, cwd: childCwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
545
|
+
globalThis.__atelActiveDirectActionKeys?.delete(actionKey);
|
|
515
546
|
if (err) {
|
|
547
|
+
const combinedErrorText = String(stderr || err.message || '');
|
|
548
|
+
if (
|
|
549
|
+
command[0] === 'atel' &&
|
|
550
|
+
command[1] === 'milestone-feedback' &&
|
|
551
|
+
command.includes('--approve') &&
|
|
552
|
+
combinedErrorText.includes('order not in milestone_review status')
|
|
553
|
+
) {
|
|
554
|
+
log({
|
|
555
|
+
event: 'recommended_action_direct_skip',
|
|
556
|
+
eventType,
|
|
557
|
+
dedupeKey,
|
|
558
|
+
action: action.action,
|
|
559
|
+
command,
|
|
560
|
+
reason: 'order_status_executing',
|
|
561
|
+
});
|
|
562
|
+
resolve({ ok: true, skipped: true, reason: 'order_status_executing' });
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (
|
|
566
|
+
command[0] === 'atel' &&
|
|
567
|
+
command[1] === 'milestone-submit' &&
|
|
568
|
+
combinedErrorText.includes('milestone cannot be submitted in status: submitted')
|
|
569
|
+
) {
|
|
570
|
+
log({
|
|
571
|
+
event: 'recommended_action_direct_skip',
|
|
572
|
+
eventType,
|
|
573
|
+
dedupeKey,
|
|
574
|
+
action: action.action,
|
|
575
|
+
command,
|
|
576
|
+
reason: 'milestone_status_submitted',
|
|
577
|
+
});
|
|
578
|
+
resolve({ ok: true, skipped: true, reason: 'milestone_status_submitted' });
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (
|
|
582
|
+
command[0] === 'atel' &&
|
|
583
|
+
command[1] === 'milestone-verify' &&
|
|
584
|
+
(combinedErrorText.includes('milestone cannot be verified in status: verified') ||
|
|
585
|
+
combinedErrorText.includes('milestone not in submitted status') ||
|
|
586
|
+
combinedErrorText.includes('milestone cannot be verified in status: settled'))
|
|
587
|
+
) {
|
|
588
|
+
log({
|
|
589
|
+
event: 'recommended_action_direct_skip',
|
|
590
|
+
eventType,
|
|
591
|
+
dedupeKey,
|
|
592
|
+
action: action.action,
|
|
593
|
+
command,
|
|
594
|
+
reason: 'milestone_already_processed',
|
|
595
|
+
});
|
|
596
|
+
resolve({ ok: true, skipped: true, reason: 'milestone_already_processed' });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
516
599
|
log({
|
|
517
600
|
event: 'recommended_action_direct_error',
|
|
518
601
|
eventType,
|
|
@@ -532,7 +615,7 @@ async function executeRecommendedActionDirect(eventType, action, cwd, dedupeKey)
|
|
|
532
615
|
dedupeKey,
|
|
533
616
|
action: action.action,
|
|
534
617
|
command,
|
|
535
|
-
stdout: (stdout
|
|
618
|
+
stdout: summarizeAgentOutput(stdout, 400),
|
|
536
619
|
});
|
|
537
620
|
resolve({ ok: true, stdout });
|
|
538
621
|
});
|
|
@@ -2904,6 +2987,178 @@ ${callbackFailed}
|
|
|
2904
2987
|
`;
|
|
2905
2988
|
}
|
|
2906
2989
|
|
|
2990
|
+
function buildLocalAgentPrompt(eventType, promptText) {
|
|
2991
|
+
if (eventType === 'milestone_submitted') {
|
|
2992
|
+
return `${promptText}
|
|
2993
|
+
|
|
2994
|
+
重要:你不是在和用户聊天。
|
|
2995
|
+
不要输出 markdown、代码块、解释、分析过程。
|
|
2996
|
+
你必须只输出一行 JSON。
|
|
2997
|
+
|
|
2998
|
+
通过时:
|
|
2999
|
+
{"decision":"pass","summary":"简短通过原因"}
|
|
3000
|
+
|
|
3001
|
+
拒绝时:
|
|
3002
|
+
{"decision":"reject","reason":"具体拒绝原因","summary":"简短审核结论"}`;
|
|
3003
|
+
}
|
|
3004
|
+
|
|
3005
|
+
if (['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected'].includes(eventType)) {
|
|
3006
|
+
return `${promptText}
|
|
3007
|
+
|
|
3008
|
+
重要:你不是在和用户聊天。
|
|
3009
|
+
不要输出 markdown、标题、项目符号、解释、分析过程。
|
|
3010
|
+
你必须只输出一行 JSON。
|
|
3011
|
+
|
|
3012
|
+
格式:
|
|
3013
|
+
{"result":"当前里程碑的真实交付内容"}`;
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
return promptText;
|
|
3017
|
+
}
|
|
3018
|
+
|
|
3019
|
+
function normalizeLocalAgentStdout(stdout) {
|
|
3020
|
+
const text = String(stdout || '').trim();
|
|
3021
|
+
if (!text) return '';
|
|
3022
|
+
try {
|
|
3023
|
+
const parsed = JSON.parse(text);
|
|
3024
|
+
if (Array.isArray(parsed?.payloads)) {
|
|
3025
|
+
for (const item of parsed.payloads) {
|
|
3026
|
+
const candidate = String(item?.text || '').trim();
|
|
3027
|
+
if (candidate) return candidate;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
if (typeof parsed?.text === 'string' && parsed.text.trim()) return parsed.text.trim();
|
|
3031
|
+
if (typeof parsed?.result === 'string' && parsed.result.trim()) return JSON.stringify({ result: parsed.result.trim() });
|
|
3032
|
+
if (typeof parsed?.decision === 'string') return JSON.stringify(parsed);
|
|
3033
|
+
} catch {}
|
|
3034
|
+
const fencedJson = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
3035
|
+
if (fencedJson?.[1]) return fencedJson[1].trim();
|
|
3036
|
+
const jsonObjects = text.match(/\{[\s\S]*\}/g);
|
|
3037
|
+
if (jsonObjects?.length) {
|
|
3038
|
+
for (let i = jsonObjects.length - 1; i >= 0; i -= 1) {
|
|
3039
|
+
const candidate = jsonObjects[i].trim();
|
|
3040
|
+
try {
|
|
3041
|
+
const parsed = JSON.parse(candidate);
|
|
3042
|
+
if (Array.isArray(parsed?.payloads)) {
|
|
3043
|
+
for (const item of parsed.payloads) {
|
|
3044
|
+
const nested = String(item?.text || '').trim();
|
|
3045
|
+
if (nested) return nested;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
if (typeof parsed?.text === 'string' && parsed.text.trim()) return parsed.text.trim();
|
|
3049
|
+
if (typeof parsed?.result === 'string' && parsed.result.trim()) return JSON.stringify({ result: parsed.result.trim() });
|
|
3050
|
+
if (typeof parsed?.decision === 'string') return JSON.stringify(parsed);
|
|
3051
|
+
return candidate;
|
|
3052
|
+
} catch {}
|
|
3053
|
+
}
|
|
3054
|
+
}
|
|
3055
|
+
const jsonLines = text.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
3056
|
+
for (let i = jsonLines.length - 1; i >= 0; i -= 1) {
|
|
3057
|
+
const candidate = jsonLines[i];
|
|
3058
|
+
if (!(candidate.startsWith('{') && candidate.endsWith('}'))) continue;
|
|
3059
|
+
try {
|
|
3060
|
+
JSON.parse(candidate);
|
|
3061
|
+
return candidate;
|
|
3062
|
+
} catch {}
|
|
3063
|
+
}
|
|
3064
|
+
return text;
|
|
3065
|
+
}
|
|
3066
|
+
|
|
3067
|
+
function normalizeResult(value) {
|
|
3068
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
function sanitizeHookSessionId(value) {
|
|
3072
|
+
const raw = String(value || '').trim();
|
|
3073
|
+
if (!raw) return `atel-hook-${Date.now()}`;
|
|
3074
|
+
const cleaned = raw.replace(/[^a-zA-Z0-9._:-]+/g, '-').replace(/^-+|-+$/g, '');
|
|
3075
|
+
return (cleaned || `atel-hook-${Date.now()}`).slice(0, 120);
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
function isOpenClawAgentInvocation(cmd, args = []) {
|
|
3079
|
+
const argv = [String(cmd || ''), ...args.map((v) => String(v || ''))];
|
|
3080
|
+
if (argv[0] === 'openclaw') return argv[1] === 'agent';
|
|
3081
|
+
if (argv[0] === 'npx') return argv[1] === 'openclaw' && argv[2] === 'agent';
|
|
3082
|
+
if (argv[0] === 'node') return argv.includes('agent') && argv.some((v) => /openclaw/i.test(v));
|
|
3083
|
+
return false;
|
|
3084
|
+
}
|
|
3085
|
+
|
|
3086
|
+
function prepareHookInvocation(cmd, args = [], hookKey, timeoutSeconds) {
|
|
3087
|
+
const nextArgs = [...args];
|
|
3088
|
+
if (!isOpenClawAgentInvocation(cmd, nextArgs)) return { cmd, args: nextArgs };
|
|
3089
|
+
|
|
3090
|
+
if (!nextArgs.includes('--json')) {
|
|
3091
|
+
const messageIndex = nextArgs.lastIndexOf('-m');
|
|
3092
|
+
const insertAt = messageIndex >= 0 ? messageIndex : nextArgs.length;
|
|
3093
|
+
nextArgs.splice(insertAt, 0, '--json');
|
|
3094
|
+
}
|
|
3095
|
+
if (!nextArgs.includes('--session-id')) {
|
|
3096
|
+
const messageIndex = nextArgs.lastIndexOf('-m');
|
|
3097
|
+
const insertAt = messageIndex >= 0 ? messageIndex : nextArgs.length;
|
|
3098
|
+
nextArgs.splice(insertAt, 0, '--session-id', sanitizeHookSessionId(hookKey));
|
|
3099
|
+
}
|
|
3100
|
+
if (!nextArgs.includes('--timeout')) {
|
|
3101
|
+
const messageIndex = nextArgs.lastIndexOf('-m');
|
|
3102
|
+
const insertAt = messageIndex >= 0 ? messageIndex : nextArgs.length;
|
|
3103
|
+
nextArgs.splice(insertAt, 0, '--timeout', String(timeoutSeconds));
|
|
3104
|
+
}
|
|
3105
|
+
if (!nextArgs.includes('--thinking')) {
|
|
3106
|
+
const messageIndex = nextArgs.lastIndexOf('-m');
|
|
3107
|
+
const insertAt = messageIndex >= 0 ? messageIndex : nextArgs.length;
|
|
3108
|
+
nextArgs.splice(insertAt, 0, '--thinking', 'minimal');
|
|
3109
|
+
}
|
|
3110
|
+
return { cmd, args: nextArgs };
|
|
3111
|
+
}
|
|
3112
|
+
|
|
3113
|
+
function buildLocalAgentActionFromStdout(eventType, payload, stdout) {
|
|
3114
|
+
const cleaned = normalizeLocalAgentStdout(stdout);
|
|
3115
|
+
if (!cleaned) return { ok: false, error: 'empty_local_agent_stdout' };
|
|
3116
|
+
|
|
3117
|
+
if (eventType === 'milestone_submitted') {
|
|
3118
|
+
try {
|
|
3119
|
+
const parsed = JSON.parse(cleaned);
|
|
3120
|
+
return buildAgentCallbackAction(eventType, payload, parsed);
|
|
3121
|
+
} catch {
|
|
3122
|
+
const lowered = cleaned.toLowerCase();
|
|
3123
|
+
if (lowered.startsWith('pass') || cleaned.includes('通过')) {
|
|
3124
|
+
return buildAgentCallbackAction(eventType, payload, { decision: 'pass', summary: cleaned });
|
|
3125
|
+
}
|
|
3126
|
+
if (lowered.startsWith('reject') || cleaned.includes('拒绝')) {
|
|
3127
|
+
return buildAgentCallbackAction(eventType, payload, { decision: 'reject', reason: cleaned, summary: cleaned });
|
|
3128
|
+
}
|
|
3129
|
+
return { ok: false, error: 'invalid_local_review_stdout' };
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
if (['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected'].includes(eventType)) {
|
|
3134
|
+
try {
|
|
3135
|
+
const parsed = JSON.parse(cleaned);
|
|
3136
|
+
return buildAgentCallbackAction(eventType, payload, parsed);
|
|
3137
|
+
} catch {
|
|
3138
|
+
return buildAgentCallbackAction(eventType, payload, { result: cleaned, summary: cleaned });
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
|
|
3142
|
+
return buildAgentCallbackAction(eventType, payload, { result: cleaned, summary: cleaned });
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
function buildMilestoneHookRecoveryKey(eventType, payload = {}) {
|
|
3146
|
+
const orderId = String(payload?.orderId || '').trim();
|
|
3147
|
+
if (!orderId) return '';
|
|
3148
|
+
if (eventType === 'milestone_submitted') {
|
|
3149
|
+
const stage = Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0;
|
|
3150
|
+
const submitCount = Number.isFinite(Number(payload?.submitCount)) ? Number(payload.submitCount) : 0;
|
|
3151
|
+
return `stage:${orderId}:requester:${stage}:${submitCount}`;
|
|
3152
|
+
}
|
|
3153
|
+
if (['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected'].includes(eventType)) {
|
|
3154
|
+
const stage = eventType === 'milestone_plan_confirmed'
|
|
3155
|
+
? (Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0)
|
|
3156
|
+
: (Number.isFinite(Number(payload?.currentMilestone)) ? Number(payload.currentMilestone) : Number.isFinite(Number(payload?.milestoneIndex)) ? Number(payload.milestoneIndex) : 0);
|
|
3157
|
+
return `stage:${orderId}:executor:${stage}`;
|
|
3158
|
+
}
|
|
3159
|
+
return '';
|
|
3160
|
+
}
|
|
3161
|
+
|
|
2907
3162
|
async function runGatewayAgentTask(eventType, dedupeKey, promptText, cwd, payload) {
|
|
2908
3163
|
const gw = discoverGateway();
|
|
2909
3164
|
const cfg = loadOpenClawConfig();
|
|
@@ -3074,9 +3329,10 @@ ${callbackFailed}
|
|
|
3074
3329
|
const promptText = currentIndex === 0
|
|
3075
3330
|
? `你是ATEL接单方Agent。双方已确认方案,开始执行。\n订单原始要求:${orderDescription || '未提供'}\n当前里程碑 M0:${currentMilestone.title || ''}\n请只围绕这个订单要求完成当前里程碑,并通过回调返回最终交付内容。`
|
|
3076
3331
|
: `你是ATEL接单方Agent。M${currentIndex - 1} 已通过审核。\n订单原始要求:${orderDescription || '未提供'}\n下一个里程碑 M${currentIndex}:${currentMilestone.title || ''}\n前面已通过的阶段结果如下:\n${previousApprovedOutputs || '无'}\n\n请严格基于这些已通过结果推进当前里程碑,不要自行假设缺失材料,也不要读取本地共享文件来补上下文。完成后通过回调返回最终交付内容。`;
|
|
3077
|
-
const recoveryKey =
|
|
3332
|
+
const recoveryKey = buildMilestoneHookRecoveryKey(eventType, payload);
|
|
3333
|
+
log({ event: 'trade_reconcile_executor', orderId, currentMilestone: currentIndex, recoveryKey });
|
|
3078
3334
|
const queued = queueAgentHook(eventType, recoveryKey, promptText, workspace.dir, payload, { recoveryKey });
|
|
3079
|
-
if (queued) log({ event: '
|
|
3335
|
+
if (queued) log({ event: 'trade_reconcile_executor_queued', orderId, currentMilestone: currentIndex, recoveryKey });
|
|
3080
3336
|
return;
|
|
3081
3337
|
}
|
|
3082
3338
|
|
|
@@ -3106,7 +3362,7 @@ ${callbackFailed}
|
|
|
3106
3362
|
previousApprovedOutputs,
|
|
3107
3363
|
};
|
|
3108
3364
|
const promptText = `你是ATEL发单方Agent,需要审核执行方提交的工作。\n订单原始要求:${orderDescription || '未提供'}\n里程碑目标:${submittedMilestone.title || ''}\n前面已通过的阶段结果如下:\n${previousApprovedOutputs || '无'}\n提交内容:${submittedMilestone.resultSummary || ''}\n请只按该订单要求和前序已通过结果审慎决定通过还是拒绝,并通过回调返回 decision=pass 或 decision=reject。`;
|
|
3109
|
-
const recoveryKey =
|
|
3365
|
+
const recoveryKey = buildMilestoneHookRecoveryKey('milestone_submitted', payload);
|
|
3110
3366
|
const queued = queueAgentHook('milestone_submitted', recoveryKey, promptText, workspace.dir, payload, { recoveryKey });
|
|
3111
3367
|
if (queued) log({ event: 'trade_reconcile_requester', orderId, milestoneIndex: submittedMilestone.index, recoveryKey });
|
|
3112
3368
|
}
|
|
@@ -3299,6 +3555,12 @@ ${callbackFailed}
|
|
|
3299
3555
|
const autoTriggerEvents = ['order_accepted', 'milestone_plan_confirmed', 'milestone_submitted', 'milestone_verified', 'milestone_rejected'];
|
|
3300
3556
|
const hasGatewayAction = Array.isArray(recommendedActions) && recommendedActions.some((action) => Array.isArray(action?.command) && action.command[0] === 'atel');
|
|
3301
3557
|
if (agentCmd && prompt && autoTriggerEvents.includes(event) && !shouldSkipAgentHook(event, directExecutionSucceeded)) {
|
|
3558
|
+
const needsActionablePayload = event !== 'milestone_submitted';
|
|
3559
|
+
if (needsActionablePayload && !hasGatewayAction) {
|
|
3560
|
+
log({ event: 'agent_hook_skip_no_action', eventType: event, dedupeKey, reason: 'informational_only_payload' });
|
|
3561
|
+
res.json({ status: 'received', eventId, eventType: event });
|
|
3562
|
+
return;
|
|
3563
|
+
}
|
|
3302
3564
|
if (shouldUseGatewaySession(event) && !hasGatewayAction) {
|
|
3303
3565
|
log({ event: 'agent_hook_skip_no_action', eventType: event, dedupeKey, reason: 'informational_only_payload' });
|
|
3304
3566
|
res.json({ status: 'received', eventId, eventType: event });
|
|
@@ -3318,13 +3580,17 @@ ${callbackFailed}
|
|
|
3318
3580
|
} else {
|
|
3319
3581
|
processedEvents.add('hook:' + dedupeKey);
|
|
3320
3582
|
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3583
|
+
const queued = queueAgentHook(
|
|
3584
|
+
event,
|
|
3585
|
+
dedupeKey,
|
|
3586
|
+
enrichedPrompt,
|
|
3587
|
+
hookCwd,
|
|
3588
|
+
payload,
|
|
3589
|
+
{ recoveryKey: buildMilestoneHookRecoveryKey(event, payload) },
|
|
3590
|
+
);
|
|
3591
|
+
if (!queued) {
|
|
3592
|
+
log({ event: 'agent_cmd_dedup_recovery_key', eventType: event, dedupeKey });
|
|
3593
|
+
}
|
|
3328
3594
|
}
|
|
3329
3595
|
}
|
|
3330
3596
|
|
|
@@ -3353,11 +3619,18 @@ ${callbackFailed}
|
|
|
3353
3619
|
return;
|
|
3354
3620
|
}
|
|
3355
3621
|
log({ event: 'agent_session_spawn_error', eventType: hookEvent, dedupeKey: hookKey, error: gatewayResult.error, fallback: 'cli' });
|
|
3622
|
+
spawnArgs[spawnArgs.length - 1] = buildLocalAgentPrompt(hookEvent, promptArg);
|
|
3623
|
+
} else if (spawnArgs.length > 0) {
|
|
3624
|
+
const promptArg = spawnArgs[spawnArgs.length - 1] || '';
|
|
3625
|
+
spawnArgs[spawnArgs.length - 1] = buildLocalAgentPrompt(hookEvent, promptArg);
|
|
3356
3626
|
}
|
|
3357
3627
|
|
|
3358
3628
|
const MAX_ATTEMPTS = 5;
|
|
3359
|
-
const
|
|
3360
|
-
|
|
3629
|
+
const isMilestoneHook = ['milestone_plan_confirmed', 'milestone_verified', 'milestone_rejected', 'milestone_submitted'].includes(hookEvent);
|
|
3630
|
+
const localHookTimeoutMs = isMilestoneHook ? 180000 : 600000;
|
|
3631
|
+
const preparedInvocation = prepareHookInvocation(spawnCmd, spawnArgs, hookKey, Math.ceil(localHookTimeoutMs / 1000));
|
|
3632
|
+
const runHook = (attempt, invocation = preparedInvocation) => {
|
|
3633
|
+
execFile(invocation.cmd, invocation.args, { timeout: localHookTimeoutMs, cwd: hookCwd, maxBuffer: 10 * 1024 * 1024 }, (err, stdout, stderr) => {
|
|
3361
3634
|
const errMsg = (err?.message || '') + (stderr || '');
|
|
3362
3635
|
const isSessionLock = errMsg.includes('session file locked') || errMsg.includes('session locked');
|
|
3363
3636
|
const isNetworkError = err && (err.killed || err.code === 'ETIMEDOUT' || err.code === 'ECONNRESET');
|
|
@@ -3373,8 +3646,40 @@ ${callbackFailed}
|
|
|
3373
3646
|
} else if (err) {
|
|
3374
3647
|
log({ event: 'agent_cmd_error', eventType: hookEvent, error: err.message, stderr: (stderr || '').substring(0, 200) });
|
|
3375
3648
|
finishHook();
|
|
3649
|
+
} else if (isKnownUpstreamModelInputError(stdout)) {
|
|
3650
|
+
log({
|
|
3651
|
+
event: 'agent_cmd_upstream_input_error',
|
|
3652
|
+
eventType: hookEvent,
|
|
3653
|
+
dedupeKey: hookKey,
|
|
3654
|
+
note: 'suppressed known upstream model input-length error',
|
|
3655
|
+
});
|
|
3656
|
+
finishHook();
|
|
3376
3657
|
} else {
|
|
3377
|
-
|
|
3658
|
+
const localAction = buildLocalAgentActionFromStdout(hookEvent, hookPayload || {}, stdout);
|
|
3659
|
+
if (!localAction.ok && localAction.error === 'empty_local_agent_stdout' && invocation.args.includes('--json')) {
|
|
3660
|
+
const retryArgs = invocation.args.filter((arg) => arg !== '--json');
|
|
3661
|
+
log({ event: 'agent_cmd_retry_without_json', eventType: hookEvent, dedupeKey: hookKey });
|
|
3662
|
+
setTimeout(() => runHook(attempt + 1, { ...invocation, args: retryArgs }), 1000);
|
|
3663
|
+
return;
|
|
3664
|
+
}
|
|
3665
|
+
if (localAction.ok && !localAction.skipped) {
|
|
3666
|
+
executeRecommendedActionDirect(hookEvent, localAction.action, hookCwd || process.cwd(), hookKey)
|
|
3667
|
+
.then((execResult) => {
|
|
3668
|
+
if (execResult.ok) {
|
|
3669
|
+
log({ event: 'agent_cmd_done', eventType: hookEvent, mode: 'local_stdout_action', dedupeKey: hookKey, stdout: summarizeAgentOutput(stdout, 200) });
|
|
3670
|
+
} else {
|
|
3671
|
+
log({ event: 'agent_cmd_local_action_error', eventType: hookEvent, dedupeKey: hookKey, error: execResult.error || 'local_action_failed', stdout: summarizeAgentOutput(stdout, 200) });
|
|
3672
|
+
}
|
|
3673
|
+
finishHook();
|
|
3674
|
+
})
|
|
3675
|
+
.catch((e) => {
|
|
3676
|
+
log({ event: 'agent_cmd_local_action_error', eventType: hookEvent, dedupeKey: hookKey, error: e.message || 'local_action_failed', stdout: summarizeAgentOutput(stdout, 200) });
|
|
3677
|
+
finishHook();
|
|
3678
|
+
});
|
|
3679
|
+
return;
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
log({ event: 'agent_cmd_done', eventType: hookEvent, stdout: summarizeAgentOutput(stdout, 300) });
|
|
3378
3683
|
finishHook();
|
|
3379
3684
|
}
|
|
3380
3685
|
});
|
|
@@ -18,13 +18,10 @@ export function shouldSkipAgentHook(eventType, directExecutionSucceeded) {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function shouldUseGatewaySession(eventType) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
'milestone_verified',
|
|
26
|
-
'milestone_rejected',
|
|
27
|
-
].includes(eventType);
|
|
21
|
+
// Keep gateway sub-sessions only for explicit P2P task execution.
|
|
22
|
+
// Milestone automation must not depend on gateway callback health; use the
|
|
23
|
+
// local structured fallback so order progression cannot stall on subagent I/O.
|
|
24
|
+
return eventType === 'p2p_task';
|
|
28
25
|
}
|
|
29
26
|
|
|
30
27
|
export function normalizeGatewayBind(bind) {
|