imcodes 2026.4.1382-dev.1368 → 2026.4.1387-dev.1373
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/dist/shared/p2p-advanced.d.ts +88 -0
- package/dist/shared/p2p-advanced.d.ts.map +1 -0
- package/dist/shared/p2p-advanced.js +258 -0
- package/dist/shared/p2p-advanced.js.map +1 -0
- package/dist/shared/p2p-modes.d.ts +9 -0
- package/dist/shared/p2p-modes.d.ts.map +1 -1
- package/dist/shared/p2p-modes.js +0 -1
- package/dist/shared/p2p-modes.js.map +1 -1
- package/dist/shared/p2p-status.d.ts +24 -0
- package/dist/shared/p2p-status.d.ts.map +1 -1
- package/dist/shared/p2p-status.js +0 -7
- package/dist/shared/p2p-status.js.map +1 -1
- package/dist/src/agent/providers/claude-code-sdk.d.ts +4 -1
- package/dist/src/agent/providers/claude-code-sdk.d.ts.map +1 -1
- package/dist/src/agent/providers/claude-code-sdk.js +44 -0
- package/dist/src/agent/providers/claude-code-sdk.js.map +1 -1
- package/dist/src/agent/providers/codex-sdk.d.ts +5 -1
- package/dist/src/agent/providers/codex-sdk.d.ts.map +1 -1
- package/dist/src/agent/providers/codex-sdk.js +37 -0
- package/dist/src/agent/providers/codex-sdk.js.map +1 -1
- package/dist/src/agent/providers/qwen.d.ts +5 -1
- package/dist/src/agent/providers/qwen.d.ts.map +1 -1
- package/dist/src/agent/providers/qwen.js +54 -0
- package/dist/src/agent/providers/qwen.js.map +1 -1
- package/dist/src/agent/transport-provider.d.ts +12 -0
- package/dist/src/agent/transport-provider.d.ts.map +1 -1
- package/dist/src/daemon/command-handler.d.ts.map +1 -1
- package/dist/src/daemon/command-handler.js +19 -1
- package/dist/src/daemon/command-handler.js.map +1 -1
- package/dist/src/daemon/cron-executor.d.ts.map +1 -1
- package/dist/src/daemon/cron-executor.js +8 -1
- package/dist/src/daemon/cron-executor.js.map +1 -1
- package/dist/src/daemon/p2p-orchestrator.d.ts +60 -10
- package/dist/src/daemon/p2p-orchestrator.d.ts.map +1 -1
- package/dist/src/daemon/p2p-orchestrator.js +654 -79
- package/dist/src/daemon/p2p-orchestrator.js.map +1 -1
- package/dist/src/daemon/transport-relay.d.ts.map +1 -1
- package/dist/src/daemon/transport-relay.js +11 -0
- package/dist/src/daemon/transport-relay.js.map +1 -1
- package/package.json +1 -1
|
@@ -12,8 +12,9 @@ import { randomUUID } from 'node:crypto';
|
|
|
12
12
|
import { sendKeysDelayedEnter } from '../agent/tmux.js';
|
|
13
13
|
import { detectStatusAsync } from '../agent/detect.js';
|
|
14
14
|
import { getSession } from '../store/session-store.js';
|
|
15
|
-
import { getTransportRuntime } from '../agent/session-manager.js';
|
|
15
|
+
import { getTransportRuntime, launchTransportSession, stopTransportRuntimeSession } from '../agent/session-manager.js';
|
|
16
16
|
import { P2P_BASELINE_PROMPT, getP2pMode, getModeForRound, isComboMode, parseModePipeline, roundPrompt } from '../../shared/p2p-modes.js';
|
|
17
|
+
import { resolveP2pRoundPlan, } from '../../shared/p2p-advanced.js';
|
|
17
18
|
import { formatP2pParticipantIdentity, shortP2pSessionName } from '../../shared/p2p-participant.js';
|
|
18
19
|
import { P2P_TERMINAL_HOP_STATUSES, P2P_TERMINAL_RUN_STATUSES, } from '../../shared/p2p-status.js';
|
|
19
20
|
import logger from '../util/logger.js';
|
|
@@ -30,6 +31,13 @@ export function serializeP2pRun(run) {
|
|
|
30
31
|
const currentHopState = activeHopStates[0] ?? null;
|
|
31
32
|
const currentHop = currentHopState?.session ?? run.activeTargetSessions[0] ?? run.currentTargetSession;
|
|
32
33
|
const hopCounts = countHopStates(run.hopStates);
|
|
34
|
+
const routingHistory = Array.isArray(run.routingHistory) ? run.routingHistory : [];
|
|
35
|
+
const latestStepByRoundId = routingHistory.reduce((acc, entry) => {
|
|
36
|
+
if (typeof entry.toRoundId === 'string' && typeof entry.atStep === 'number') {
|
|
37
|
+
acc[entry.toRoundId] = entry.atStep;
|
|
38
|
+
}
|
|
39
|
+
return acc;
|
|
40
|
+
}, {});
|
|
33
41
|
return {
|
|
34
42
|
id: run.id,
|
|
35
43
|
discussion_id: run.discussionId,
|
|
@@ -104,6 +112,31 @@ export function serializeP2pRun(run) {
|
|
|
104
112
|
terminal_reason: run.status === 'completed' || run.status === 'timed_out' || run.status === 'failed' || run.status === 'cancelled'
|
|
105
113
|
? run.status
|
|
106
114
|
: null,
|
|
115
|
+
advanced_p2p_enabled: run.advancedP2pEnabled || undefined,
|
|
116
|
+
current_round_id: run.currentRoundId ?? null,
|
|
117
|
+
current_execution_step: run.currentExecutionStep || null,
|
|
118
|
+
current_round_attempt: run.currentRoundAttempt || null,
|
|
119
|
+
round_attempt_counts: run.advancedP2pEnabled ? { ...run.roundAttemptCounts } : undefined,
|
|
120
|
+
round_jump_counts: run.advancedP2pEnabled ? { ...run.roundJumpCounts } : undefined,
|
|
121
|
+
routing_history: run.advancedP2pEnabled ? [...routingHistory] : undefined,
|
|
122
|
+
helper_diagnostics: run.advancedP2pEnabled && run.helperDiagnostics.length > 0 ? [...run.helperDiagnostics] : undefined,
|
|
123
|
+
advanced_nodes: run.advancedP2pEnabled && run.resolvedRounds
|
|
124
|
+
? run.resolvedRounds.map((round) => ({
|
|
125
|
+
id: round.id,
|
|
126
|
+
title: round.title,
|
|
127
|
+
preset: round.preset,
|
|
128
|
+
status: (() => {
|
|
129
|
+
if (run.currentRoundId === round.id) {
|
|
130
|
+
return P2P_TERMINAL_RUN_STATUSES.has(run.status) ? (run.status === 'completed' ? 'done' : 'skipped') : 'active';
|
|
131
|
+
}
|
|
132
|
+
if ((run.roundAttemptCounts[round.id] ?? 0) > 0)
|
|
133
|
+
return 'done';
|
|
134
|
+
return 'pending';
|
|
135
|
+
})(),
|
|
136
|
+
attempt: run.roundAttemptCounts[round.id] ?? 0,
|
|
137
|
+
step: latestStepByRoundId[round.id],
|
|
138
|
+
}))
|
|
139
|
+
: undefined,
|
|
107
140
|
// Full node list for segmented progress display — compatibility projection
|
|
108
141
|
all_nodes: (() => {
|
|
109
142
|
const nodes = [];
|
|
@@ -176,6 +209,7 @@ let IDLE_POLL_MS = 3_000;
|
|
|
176
209
|
let GRACE_PERIOD_DEFAULT_MS = 180_000; // 3 min — complex analysis (subagent research + write) takes time
|
|
177
210
|
let MIN_PROCESSING_MS = 30_000; // Don't trust idle detection until 30s after dispatch
|
|
178
211
|
let FILE_SETTLE_CYCLES = 3; // File must stop growing for 3 poll cycles (9s) to be "settled"
|
|
212
|
+
let ROUND_HOP_CLEANUP_DELAY_MS = 30_000;
|
|
179
213
|
/** Override poll interval for tests. */
|
|
180
214
|
export function _setIdlePollMs(ms) { IDLE_POLL_MS = ms; }
|
|
181
215
|
/** Override grace period for tests. */
|
|
@@ -184,6 +218,8 @@ export function _setGracePeriodMs(ms) { GRACE_PERIOD_DEFAULT_MS = ms; }
|
|
|
184
218
|
export function _setMinProcessingMs(ms) { MIN_PROCESSING_MS = ms; }
|
|
185
219
|
/** Override file settle cycles for tests. */
|
|
186
220
|
export function _setFileSettleCycles(n) { FILE_SETTLE_CYCLES = n; }
|
|
221
|
+
/** Override round hop artifact cleanup delay for tests. */
|
|
222
|
+
export function _setRoundHopCleanupDelayMs(ms) { ROUND_HOP_CLEANUP_DELAY_MS = ms; }
|
|
187
223
|
const idleWaiters = new Map();
|
|
188
224
|
/**
|
|
189
225
|
* Called by lifecycle hook when a session becomes idle.
|
|
@@ -243,11 +279,42 @@ function waitForIdleEvent(session, timeoutMs) {
|
|
|
243
279
|
return { promise, cancel: cancelFn };
|
|
244
280
|
}
|
|
245
281
|
// ── Start a P2P run ───────────────────────────────────────────────────────
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
282
|
+
function buildHelperEligibleSnapshot(initiatorSession, targets) {
|
|
283
|
+
const seen = new Set();
|
|
284
|
+
const names = [initiatorSession, ...targets.map((target) => target.session)];
|
|
285
|
+
const snapshot = [];
|
|
286
|
+
for (const sessionName of names) {
|
|
287
|
+
if (seen.has(sessionName))
|
|
288
|
+
continue;
|
|
289
|
+
seen.add(sessionName);
|
|
290
|
+
const record = getSession(sessionName);
|
|
291
|
+
snapshot.push({
|
|
292
|
+
sessionName,
|
|
293
|
+
agentType: record?.agentType ?? 'unknown',
|
|
294
|
+
parentSession: record?.parentSession ?? null,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return snapshot;
|
|
298
|
+
}
|
|
299
|
+
function normalizeStartP2pRunArgs(args) {
|
|
300
|
+
if (args.length === 1 && typeof args[0] === 'object' && args[0] !== null && 'initiatorSession' in args[0]) {
|
|
301
|
+
return args[0];
|
|
302
|
+
}
|
|
303
|
+
const [initiatorSession, targets, userText, fileContents, serverLink, rounds, extraPrompt, modeOverride, hopTimeoutMs,] = args;
|
|
304
|
+
return {
|
|
305
|
+
initiatorSession,
|
|
306
|
+
targets,
|
|
307
|
+
userText,
|
|
308
|
+
fileContents,
|
|
309
|
+
serverLink,
|
|
310
|
+
rounds,
|
|
311
|
+
extraPrompt,
|
|
312
|
+
modeOverride,
|
|
313
|
+
hopTimeoutMs,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
export async function startP2pRun(...args) {
|
|
317
|
+
const { initiatorSession, targets, userText, fileContents, serverLink, rounds, extraPrompt, modeOverride, hopTimeoutMs, advancedPresetKey, advancedRounds, advancedRunTimeoutMs, contextReducer, } = normalizeStartP2pRunArgs(args);
|
|
251
318
|
// Validate same domain
|
|
252
319
|
const mainSession = extractMainSession(initiatorSession);
|
|
253
320
|
for (const t of targets) {
|
|
@@ -255,6 +322,17 @@ hopTimeoutMs) {
|
|
|
255
322
|
throw new Error(`Cross-domain P2P not supported: ${t.session} is not in ${mainSession}`);
|
|
256
323
|
}
|
|
257
324
|
}
|
|
325
|
+
const helperEligibleSnapshot = buildHelperEligibleSnapshot(initiatorSession, targets);
|
|
326
|
+
const resolvedPlan = resolveP2pRoundPlan({
|
|
327
|
+
modeOverride: modeOverride ?? targets[0]?.mode ?? 'discuss',
|
|
328
|
+
roundsOverride: rounds,
|
|
329
|
+
hopTimeoutMinutes: hopTimeoutMs != null ? Math.ceil(hopTimeoutMs / 60_000) : undefined,
|
|
330
|
+
advancedPresetKey,
|
|
331
|
+
advancedRounds,
|
|
332
|
+
advancedRunTimeoutMinutes: advancedRunTimeoutMs != null ? Math.ceil(advancedRunTimeoutMs / 60_000) : undefined,
|
|
333
|
+
contextReducer,
|
|
334
|
+
participants: helperEligibleSnapshot,
|
|
335
|
+
});
|
|
258
336
|
const mode = modeOverride ?? targets[0]?.mode ?? 'discuss';
|
|
259
337
|
const modeConfig = getP2pMode(isComboMode(mode) ? parseModePipeline(mode)[0] : mode);
|
|
260
338
|
const runId = randomUUID().slice(0, 12);
|
|
@@ -282,7 +360,9 @@ hopTimeoutMs) {
|
|
|
282
360
|
}
|
|
283
361
|
await writeFile(contextFilePath, seed, 'utf8');
|
|
284
362
|
const P2P_MAX_ROUNDS = 6;
|
|
285
|
-
const totalRounds =
|
|
363
|
+
const totalRounds = resolvedPlan.advanced
|
|
364
|
+
? resolvedPlan.rounds.length
|
|
365
|
+
: Math.min(P2P_MAX_ROUNDS, Math.max(1, rounds ?? 1));
|
|
286
366
|
const run = {
|
|
287
367
|
id: runId,
|
|
288
368
|
discussionId,
|
|
@@ -314,6 +394,23 @@ hopTimeoutMs) {
|
|
|
314
394
|
hopStartedAt: Date.now(),
|
|
315
395
|
hopStates: [],
|
|
316
396
|
activeTargetSessions: [],
|
|
397
|
+
advancedP2pEnabled: resolvedPlan.advanced,
|
|
398
|
+
resolvedRounds: resolvedPlan.advanced ? resolvedPlan.rounds : undefined,
|
|
399
|
+
helperEligibleSnapshot: resolvedPlan.helperEligibleSnapshot ?? helperEligibleSnapshot,
|
|
400
|
+
contextReducer: resolvedPlan.contextReducer,
|
|
401
|
+
advancedRunTimeoutMs: resolvedPlan.advanced && resolvedPlan.overallRunTimeoutMinutes != null
|
|
402
|
+
? resolvedPlan.overallRunTimeoutMinutes * 60_000
|
|
403
|
+
: undefined,
|
|
404
|
+
deadlineAt: resolvedPlan.advanced && resolvedPlan.overallRunTimeoutMinutes != null
|
|
405
|
+
? Date.now() + (resolvedPlan.overallRunTimeoutMinutes * 60_000)
|
|
406
|
+
: null,
|
|
407
|
+
currentRoundId: resolvedPlan.advanced ? (resolvedPlan.rounds[0]?.id ?? null) : null,
|
|
408
|
+
currentExecutionStep: 0,
|
|
409
|
+
currentRoundAttempt: 1,
|
|
410
|
+
roundAttemptCounts: {},
|
|
411
|
+
roundJumpCounts: {},
|
|
412
|
+
routingHistory: [],
|
|
413
|
+
helperDiagnostics: [],
|
|
317
414
|
_cancelled: false,
|
|
318
415
|
};
|
|
319
416
|
activeRuns.set(runId, run);
|
|
@@ -510,6 +607,11 @@ async function cleanupRoundHopArtifacts(roundHops) {
|
|
|
510
607
|
});
|
|
511
608
|
}));
|
|
512
609
|
}
|
|
610
|
+
function scheduleRoundHopArtifactCleanup(roundHops) {
|
|
611
|
+
if (roundHops.length === 0)
|
|
612
|
+
return;
|
|
613
|
+
setTimeout(() => { void cleanupRoundHopArtifacts(roundHops); }, ROUND_HOP_CLEANUP_DELAY_MS);
|
|
614
|
+
}
|
|
513
615
|
function updateHopStatus(run, hop, status, error = null) {
|
|
514
616
|
if (!hop)
|
|
515
617
|
return;
|
|
@@ -524,6 +626,10 @@ function updateHopStatus(run, hop, status, error = null) {
|
|
|
524
626
|
}
|
|
525
627
|
}
|
|
526
628
|
async function executeChain(run, modeConfig, serverLink) {
|
|
629
|
+
if (run.advancedP2pEnabled && run.resolvedRounds?.length) {
|
|
630
|
+
await executeAdvancedChain(run, serverLink);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
527
633
|
const totalHops = run.allTargets.length;
|
|
528
634
|
// ── Multi-round loop ──
|
|
529
635
|
const combo = isComboMode(run.mode);
|
|
@@ -564,81 +670,80 @@ async function executeChain(run, modeConfig, serverLink) {
|
|
|
564
670
|
// ── Phase 2: Sub-session hops ──
|
|
565
671
|
run.activePhase = 'hop';
|
|
566
672
|
const roundHops = await createRoundHopStates(run, targets, roundModeKey);
|
|
567
|
-
|
|
568
|
-
|
|
673
|
+
try {
|
|
674
|
+
run.activeTargetSessions = roundHops.map((hop) => hop.session);
|
|
675
|
+
const hopResults = await Promise.allSettled(targets.map(async (target, i) => {
|
|
676
|
+
if (run._cancelled)
|
|
677
|
+
return false;
|
|
678
|
+
const hop = roundHops[i];
|
|
679
|
+
const hopMode = combo ? roundModeKey : target.mode;
|
|
680
|
+
const hopLabel = `${discussionParticipantName(target.session)} — ${capitalize(hopMode)} (hop ${i + 1}/${totalHops}${roundLabel})`;
|
|
681
|
+
hop.section_header = hopLabel;
|
|
682
|
+
const hopModeConfig = combo ? roundModeConfig : (getP2pMode(target.mode) ?? modeConfig);
|
|
683
|
+
const hopPrompt = buildHopPrompt(run, hopModeConfig, {
|
|
684
|
+
session: target.session,
|
|
685
|
+
sectionHeader: hopLabel,
|
|
686
|
+
instruction: `Read the discussion file and provide your ${hopMode} analysis. Append your output to the file.\nIMPORTANT: This is ANALYSIS ONLY. Do NOT implement fixes, do NOT edit code files, do NOT run commands. Only write your analysis into this discussion file.`,
|
|
687
|
+
isInitial: false,
|
|
688
|
+
filePath: hop.artifact_path,
|
|
689
|
+
}, rp);
|
|
690
|
+
logger.info({ runId: run.id, target: target.session, mode: hopMode, hop: i + 1, totalHops, round: run.currentRound }, 'P2P: Phase 2 — dispatching hop');
|
|
691
|
+
return dispatchHop(run, target.session, hopPrompt, serverLink, {
|
|
692
|
+
sectionHeader: hopLabel,
|
|
693
|
+
hop,
|
|
694
|
+
filePath: hop.artifact_path,
|
|
695
|
+
});
|
|
696
|
+
}));
|
|
697
|
+
run.activeTargetSessions = [];
|
|
698
|
+
run.currentTargetSession = null;
|
|
699
|
+
if (run._cancelled || isTerminal(run.status))
|
|
700
|
+
return;
|
|
701
|
+
logger.info({
|
|
702
|
+
runId: run.id,
|
|
703
|
+
round: run.currentRound,
|
|
704
|
+
settled: hopResults.length,
|
|
705
|
+
completed: roundHops.filter((hop) => hop.status === 'completed').length,
|
|
706
|
+
}, 'P2P: Phase 2 — round barrier settled');
|
|
707
|
+
await appendRoundEvidence(run, roundHops);
|
|
708
|
+
if (run._cancelled || isTerminal(run.status))
|
|
709
|
+
return;
|
|
710
|
+
run.remainingTargets = [];
|
|
711
|
+
// ── Round summary: Initiator synthesizes this round ──
|
|
569
712
|
if (run._cancelled)
|
|
570
|
-
return
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
const
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
713
|
+
return;
|
|
714
|
+
run.runPhase = 'summarizing';
|
|
715
|
+
run.summaryPhase = 'running';
|
|
716
|
+
run.activePhase = 'summary';
|
|
717
|
+
const isLastRound = run.currentRound === run.rounds;
|
|
718
|
+
const summaryModeConfig = isLastRound && combo
|
|
719
|
+
? getModeForRound(run.mode, run.rounds) // last pipeline mode for final summary
|
|
720
|
+
: roundModeConfig;
|
|
721
|
+
const roundSummaryHeader = isLastRound
|
|
722
|
+
? `${discussionParticipantNameWithMode(run.initiatorSession, roundModeKey)} — Final Summary`
|
|
723
|
+
: `${discussionParticipantNameWithMode(run.initiatorSession, roundModeKey)} — Round ${run.currentRound}/${run.rounds} Summary`;
|
|
724
|
+
const roundSummaryInstruction = isLastRound
|
|
725
|
+
? `${summaryModeConfig?.summaryPrompt ?? 'Synthesize a final summary that captures the consensus, key decisions, and any remaining disagreements across all rounds.'}\nBefore writing the summary, use the hop evidence already appended into the discussion file for this round. If the user context clearly specifies a destination file for the final plan, write the complete plan there. Otherwise, write the complete plan at the end of the discussion file.`
|
|
726
|
+
: `Synthesize the key points, areas of agreement, and open questions from this round. Then assign specific focus areas or questions for each participant in the next round (round ${run.currentRound + 1}). Append to the file.\nIMPORTANT: This is ANALYSIS ONLY. Do NOT implement fixes, do NOT edit code files, do NOT run commands. Only write your analysis into this discussion file.`;
|
|
727
|
+
const roundSummaryPrompt = buildHopPrompt(run, summaryModeConfig, {
|
|
728
|
+
session: run.initiatorSession,
|
|
729
|
+
sectionHeader: roundSummaryHeader,
|
|
730
|
+
instruction: `${roundSummaryInstruction}\nThe orchestrator has already appended each completed hop's evidence into the discussion file. If you write the final plan to another file, still append a short completion note under the new final-summary heading in the discussion file that records the chosen output file path.`,
|
|
580
731
|
isInitial: false,
|
|
581
|
-
filePath: hop.artifact_path,
|
|
582
732
|
}, rp);
|
|
583
|
-
logger.info({ runId: run.id,
|
|
584
|
-
|
|
585
|
-
sectionHeader:
|
|
586
|
-
|
|
587
|
-
filePath: hop.artifact_path,
|
|
733
|
+
logger.info({ runId: run.id, round: run.currentRound, isLastRound, roundMode: roundModeKey }, isLastRound ? 'P2P: Final summary — initiator' : 'P2P: Round summary — initiator');
|
|
734
|
+
const summaryOk = await dispatchHop(run, run.initiatorSession, roundSummaryPrompt, serverLink, {
|
|
735
|
+
sectionHeader: roundSummaryHeader,
|
|
736
|
+
required: true,
|
|
588
737
|
});
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
logger.info({
|
|
595
|
-
runId: run.id,
|
|
596
|
-
round: run.currentRound,
|
|
597
|
-
settled: hopResults.length,
|
|
598
|
-
completed: roundHops.filter((hop) => hop.status === 'completed').length,
|
|
599
|
-
}, 'P2P: Phase 2 — round barrier settled');
|
|
600
|
-
await appendRoundEvidence(run, roundHops);
|
|
601
|
-
if (run._cancelled || isTerminal(run.status))
|
|
602
|
-
return;
|
|
603
|
-
if (run.currentRound === run.rounds) {
|
|
604
|
-
run.remainingTargets = [];
|
|
738
|
+
if (!summaryOk)
|
|
739
|
+
return;
|
|
740
|
+
run.summaryPhase = 'completed';
|
|
741
|
+
if (run._cancelled || isTerminal(run.status))
|
|
742
|
+
return;
|
|
605
743
|
}
|
|
606
|
-
|
|
607
|
-
|
|
744
|
+
finally {
|
|
745
|
+
scheduleRoundHopArtifactCleanup(roundHops);
|
|
608
746
|
}
|
|
609
|
-
// ── Round summary: Initiator synthesizes this round ──
|
|
610
|
-
if (run._cancelled)
|
|
611
|
-
return;
|
|
612
|
-
run.runPhase = 'summarizing';
|
|
613
|
-
run.summaryPhase = 'running';
|
|
614
|
-
run.activePhase = 'summary';
|
|
615
|
-
const isLastRound = run.currentRound === run.rounds;
|
|
616
|
-
const summaryModeConfig = isLastRound && combo
|
|
617
|
-
? getModeForRound(run.mode, run.rounds) // last pipeline mode for final summary
|
|
618
|
-
: roundModeConfig;
|
|
619
|
-
const roundSummaryHeader = isLastRound
|
|
620
|
-
? `${discussionParticipantNameWithMode(run.initiatorSession, roundModeKey)} — Final Summary`
|
|
621
|
-
: `${discussionParticipantNameWithMode(run.initiatorSession, roundModeKey)} — Round ${run.currentRound}/${run.rounds} Summary`;
|
|
622
|
-
const roundSummaryInstruction = isLastRound
|
|
623
|
-
? `${summaryModeConfig?.summaryPrompt ?? 'Synthesize a final summary that captures the consensus, key decisions, and any remaining disagreements across all rounds.'}\nBefore writing the summary, use the hop evidence already appended into the discussion file for this round. If the user context clearly specifies a destination file for the final plan, write the complete plan there. Otherwise, write the complete plan at the end of the discussion file.`
|
|
624
|
-
: `Synthesize the key points, areas of agreement, and open questions from this round. Then assign specific focus areas or questions for each participant in the next round (round ${run.currentRound + 1}). Append to the file.\nIMPORTANT: This is ANALYSIS ONLY. Do NOT implement fixes, do NOT edit code files, do NOT run commands. Only write your analysis into this discussion file.`;
|
|
625
|
-
const roundSummaryPrompt = buildHopPrompt(run, summaryModeConfig, {
|
|
626
|
-
session: run.initiatorSession,
|
|
627
|
-
sectionHeader: roundSummaryHeader,
|
|
628
|
-
instruction: `${roundSummaryInstruction}\nThe orchestrator has already appended each completed hop's evidence into the discussion file. If you write the final plan to another file, still append a short completion note under the new final-summary heading in the discussion file that records the chosen output file path.`,
|
|
629
|
-
isInitial: false,
|
|
630
|
-
}, rp);
|
|
631
|
-
logger.info({ runId: run.id, round: run.currentRound, isLastRound, roundMode: roundModeKey }, isLastRound ? 'P2P: Final summary — initiator' : 'P2P: Round summary — initiator');
|
|
632
|
-
const summaryOk = await dispatchHop(run, run.initiatorSession, roundSummaryPrompt, serverLink, {
|
|
633
|
-
sectionHeader: roundSummaryHeader,
|
|
634
|
-
required: true,
|
|
635
|
-
});
|
|
636
|
-
if (!summaryOk)
|
|
637
|
-
return;
|
|
638
|
-
run.summaryPhase = 'completed';
|
|
639
|
-
setTimeout(() => { void cleanupRoundHopArtifacts(roundHops); }, 30_000);
|
|
640
|
-
if (run._cancelled || isTerminal(run.status))
|
|
641
|
-
return;
|
|
642
747
|
}
|
|
643
748
|
if (run._cancelled || isTerminal(run.status))
|
|
644
749
|
return;
|
|
@@ -667,6 +772,442 @@ async function executeChain(run, modeConfig, serverLink) {
|
|
|
667
772
|
activeRuns.delete(run.id);
|
|
668
773
|
}, 60_000);
|
|
669
774
|
}
|
|
775
|
+
function addHelperDiagnostic(run, diagnostic) {
|
|
776
|
+
run.helperDiagnostics.push({ ...diagnostic, timestamp: Date.now() });
|
|
777
|
+
}
|
|
778
|
+
function parseVerdictFromContent(content) {
|
|
779
|
+
const matches = [...content.matchAll(/<!--\s*P2P_VERDICT:\s*(PASS|REWORK)\s*-->/g)];
|
|
780
|
+
const verdict = matches.at(-1)?.[1];
|
|
781
|
+
return verdict === 'PASS' || verdict === 'REWORK' ? verdict : null;
|
|
782
|
+
}
|
|
783
|
+
function helperFallbackCandidates(run, exclude) {
|
|
784
|
+
const excluded = new Set(exclude);
|
|
785
|
+
return run.helperEligibleSnapshot
|
|
786
|
+
.filter((entry) => entry.sessionName !== run.initiatorSession)
|
|
787
|
+
.filter((entry) => !!entry.parentSession || entry.sessionName.startsWith('deck_sub_'))
|
|
788
|
+
.filter((entry) => !excluded.has(entry.sessionName))
|
|
789
|
+
.map((entry) => entry.sessionName);
|
|
790
|
+
}
|
|
791
|
+
function ensureRunDeadline(run, serverLink) {
|
|
792
|
+
if (!run.advancedRunTimeoutMs || !run.deadlineAt)
|
|
793
|
+
return true;
|
|
794
|
+
if (Date.now() <= run.deadlineAt)
|
|
795
|
+
return true;
|
|
796
|
+
failRun(run, 'timed_out', 'advanced_run_timeout', serverLink);
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
async function launchClonedHelperSession(run, templateSession) {
|
|
800
|
+
const template = getSession(templateSession);
|
|
801
|
+
if (!template || !template.runtimeType || template.runtimeType !== 'transport') {
|
|
802
|
+
throw new Error(`Helper template is not an eligible SDK transport session: ${templateSession}`);
|
|
803
|
+
}
|
|
804
|
+
const helperName = `deck_p2p_helper_${run.id}_${run.currentExecutionStep}_${randomUUID().slice(0, 6)}`;
|
|
805
|
+
await launchTransportSession({
|
|
806
|
+
name: helperName,
|
|
807
|
+
projectName: template.projectName,
|
|
808
|
+
role: 'w1',
|
|
809
|
+
agentType: template.agentType,
|
|
810
|
+
projectDir: template.projectDir,
|
|
811
|
+
skipStore: true,
|
|
812
|
+
fresh: true,
|
|
813
|
+
requestedModel: template.requestedModel ?? template.activeModel ?? undefined,
|
|
814
|
+
transportConfig: template.transportConfig ?? undefined,
|
|
815
|
+
description: `P2P helper for ${run.id}`,
|
|
816
|
+
label: helperName,
|
|
817
|
+
effort: template.effort,
|
|
818
|
+
...(template.ccPreset ? { ccPreset: template.ccPreset } : {}),
|
|
819
|
+
});
|
|
820
|
+
return helperName;
|
|
821
|
+
}
|
|
822
|
+
async function teardownHelperSession(run, sessionName) {
|
|
823
|
+
try {
|
|
824
|
+
await stopTransportRuntimeSession(sessionName);
|
|
825
|
+
}
|
|
826
|
+
catch (err) {
|
|
827
|
+
addHelperDiagnostic(run, {
|
|
828
|
+
code: 'P2P_HELPER_CLEANUP_FAILED',
|
|
829
|
+
attempt: run.currentRoundAttempt,
|
|
830
|
+
sourceSession: sessionName,
|
|
831
|
+
message: err instanceof Error ? err.message : String(err),
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
async function cleanupReducerSummaryFile(run, summaryPath, sourceSession, templateSession, fallbackSession) {
|
|
836
|
+
try {
|
|
837
|
+
await unlink(summaryPath);
|
|
838
|
+
}
|
|
839
|
+
catch (err) {
|
|
840
|
+
if (err?.code === 'ENOENT')
|
|
841
|
+
return;
|
|
842
|
+
addHelperDiagnostic(run, {
|
|
843
|
+
code: 'P2P_HELPER_CLEANUP_FAILED',
|
|
844
|
+
attempt: run.currentRoundAttempt,
|
|
845
|
+
sourceSession: sourceSession ?? null,
|
|
846
|
+
templateSession: templateSession ?? null,
|
|
847
|
+
fallbackSession: fallbackSession ?? null,
|
|
848
|
+
message: err instanceof Error ? err.message : String(err),
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async function reduceAdvancedContext(run, round, serverLink) {
|
|
853
|
+
if (!run.contextReducer)
|
|
854
|
+
return null;
|
|
855
|
+
const info = await stat(run.contextFilePath).catch(() => null);
|
|
856
|
+
if (!info || info.size < 32_000)
|
|
857
|
+
return null;
|
|
858
|
+
const summaryPath = join(dirname(run.contextFilePath), `${run.id}.reducer.${run.currentExecutionStep}.md`);
|
|
859
|
+
const sectionHeader = `P2P Helper Summary — ${round.title} (step ${run.currentExecutionStep})`;
|
|
860
|
+
const reducerPrompt = [
|
|
861
|
+
`[P2P Helper Task — ${run.id}]`,
|
|
862
|
+
`Read the discussion file at ${run.contextFilePath}.`,
|
|
863
|
+
`Produce a compact context reduction for the next round.`,
|
|
864
|
+
`Focus on: latest implementation attempt, latest audit findings, declared artifact targets, and only the most relevant unresolved issues.`,
|
|
865
|
+
`Write the result to ${summaryPath}.`,
|
|
866
|
+
`Add a new heading "## ${sectionHeader}" and put the reduced context under it.`,
|
|
867
|
+
`Do not change workflow verdicts, do not route, and do not edit any code files.`,
|
|
868
|
+
`Start immediately.`,
|
|
869
|
+
].join('\n');
|
|
870
|
+
const readReducedSummary = async () => {
|
|
871
|
+
const content = await readFile(summaryPath, 'utf8').catch(() => '');
|
|
872
|
+
const section = extractHeadingSection(content, sectionHeader) ?? content;
|
|
873
|
+
return section.trim() ? section.trim() : null;
|
|
874
|
+
};
|
|
875
|
+
const attemptWithSession = async (sessionName, codeOnFailure, templateSession) => {
|
|
876
|
+
const ok = await dispatchHop(run, sessionName, reducerPrompt, serverLink, {
|
|
877
|
+
sectionHeader,
|
|
878
|
+
filePath: summaryPath,
|
|
879
|
+
required: false,
|
|
880
|
+
});
|
|
881
|
+
if (!ok) {
|
|
882
|
+
addHelperDiagnostic(run, {
|
|
883
|
+
code: codeOnFailure,
|
|
884
|
+
attempt: run.currentRoundAttempt,
|
|
885
|
+
sourceSession: sessionName,
|
|
886
|
+
templateSession: templateSession ?? null,
|
|
887
|
+
});
|
|
888
|
+
return null;
|
|
889
|
+
}
|
|
890
|
+
return readReducedSummary();
|
|
891
|
+
};
|
|
892
|
+
await writeFile(summaryPath, '# Helper Summary\n\n', 'utf8');
|
|
893
|
+
try {
|
|
894
|
+
if (run.contextReducer.mode === 'reuse_existing_session' && run.contextReducer.sessionName) {
|
|
895
|
+
const primaryResult = await attemptWithSession(run.contextReducer.sessionName, 'P2P_HELPER_PRIMARY_FAILED', run.contextReducer.sessionName);
|
|
896
|
+
if (primaryResult)
|
|
897
|
+
return primaryResult;
|
|
898
|
+
}
|
|
899
|
+
else if (run.contextReducer.mode === 'clone_sdk_session' && run.contextReducer.templateSession) {
|
|
900
|
+
let helperName = null;
|
|
901
|
+
try {
|
|
902
|
+
helperName = await launchClonedHelperSession(run, run.contextReducer.templateSession);
|
|
903
|
+
const primaryResult = await attemptWithSession(helperName, 'P2P_HELPER_PRIMARY_FAILED', run.contextReducer.templateSession);
|
|
904
|
+
if (primaryResult)
|
|
905
|
+
return primaryResult;
|
|
906
|
+
}
|
|
907
|
+
catch (err) {
|
|
908
|
+
addHelperDiagnostic(run, {
|
|
909
|
+
code: 'P2P_HELPER_PRIMARY_FAILED',
|
|
910
|
+
attempt: run.currentRoundAttempt,
|
|
911
|
+
templateSession: run.contextReducer.templateSession,
|
|
912
|
+
message: err instanceof Error ? err.message : String(err),
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
finally {
|
|
916
|
+
if (helperName)
|
|
917
|
+
await teardownHelperSession(run, helperName);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
const fallbackSession = helperFallbackCandidates(run, [
|
|
921
|
+
run.contextReducer.sessionName ?? '',
|
|
922
|
+
run.contextReducer.templateSession ?? '',
|
|
923
|
+
])[0];
|
|
924
|
+
if (!fallbackSession) {
|
|
925
|
+
addHelperDiagnostic(run, {
|
|
926
|
+
code: 'P2P_COMPRESSION_SKIPPED_NO_FALLBACK',
|
|
927
|
+
attempt: run.currentRoundAttempt,
|
|
928
|
+
templateSession: run.contextReducer.templateSession ?? null,
|
|
929
|
+
sourceSession: run.contextReducer.sessionName ?? null,
|
|
930
|
+
});
|
|
931
|
+
return null;
|
|
932
|
+
}
|
|
933
|
+
const fallbackResult = await attemptWithSession(fallbackSession, 'P2P_HELPER_FALLBACK_FAILED', fallbackSession);
|
|
934
|
+
if (fallbackResult)
|
|
935
|
+
return fallbackResult;
|
|
936
|
+
failRun(run, 'failed', `helper_fallback_failed:${fallbackSession}`, serverLink);
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
finally {
|
|
940
|
+
await cleanupReducerSummaryFile(run, summaryPath, run.contextReducer.sessionName ?? null, run.contextReducer.templateSession ?? null);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async function captureArtifactBaseline(run, round) {
|
|
944
|
+
const baseline = new Map();
|
|
945
|
+
const record = getSession(run.initiatorSession);
|
|
946
|
+
const projectDir = record?.projectDir ?? process.cwd();
|
|
947
|
+
if (round.artifactConvention === 'openspec_convention') {
|
|
948
|
+
const target = join(projectDir, 'openspec', 'changes');
|
|
949
|
+
try {
|
|
950
|
+
const entries = await readdir(target);
|
|
951
|
+
baseline.set(target, entries.join('\n'));
|
|
952
|
+
}
|
|
953
|
+
catch {
|
|
954
|
+
baseline.set(target, null);
|
|
955
|
+
}
|
|
956
|
+
return baseline;
|
|
957
|
+
}
|
|
958
|
+
for (const output of round.artifactOutputs) {
|
|
959
|
+
const absPath = join(projectDir, output);
|
|
960
|
+
try {
|
|
961
|
+
baseline.set(absPath, await readFile(absPath, 'utf8'));
|
|
962
|
+
}
|
|
963
|
+
catch {
|
|
964
|
+
baseline.set(absPath, null);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
return baseline;
|
|
968
|
+
}
|
|
969
|
+
async function validateArtifactOutputsForRound(run, round, baseline) {
|
|
970
|
+
if (round.artifactConvention === 'none')
|
|
971
|
+
return;
|
|
972
|
+
if (round.artifactConvention === 'openspec_convention') {
|
|
973
|
+
const target = [...baseline.keys()][0];
|
|
974
|
+
const before = baseline.get(target) ?? null;
|
|
975
|
+
try {
|
|
976
|
+
const afterEntries = (await readdir(target)).join('\n');
|
|
977
|
+
if (afterEntries === before)
|
|
978
|
+
throw new Error('openspec_convention artifacts were not observably updated');
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
catch (err) {
|
|
982
|
+
throw new Error(err instanceof Error ? err.message : String(err));
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
for (const [absPath, before] of baseline.entries()) {
|
|
986
|
+
let after = null;
|
|
987
|
+
try {
|
|
988
|
+
after = await readFile(absPath, 'utf8');
|
|
989
|
+
}
|
|
990
|
+
catch {
|
|
991
|
+
after = null;
|
|
992
|
+
}
|
|
993
|
+
if (after == null)
|
|
994
|
+
throw new Error(`Expected artifact missing after round: ${absPath}`);
|
|
995
|
+
if (after === before)
|
|
996
|
+
throw new Error(`Expected artifact not observably updated: ${absPath}`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
async function readAppendedContent(filePath, baselineSize) {
|
|
1000
|
+
const buffer = await readFile(filePath);
|
|
1001
|
+
return buffer.subarray(Math.min(buffer.length, baselineSize)).toString('utf8');
|
|
1002
|
+
}
|
|
1003
|
+
function buildAdvancedRoundPrefix(run, round) {
|
|
1004
|
+
return `[Advanced Round ${run.currentExecutionStep} — ${round.title} — Attempt ${run.currentRoundAttempt}]`;
|
|
1005
|
+
}
|
|
1006
|
+
function buildAdvancedPromptCommon(run, round, targetSession, filePath, sectionHeader, reducerSummary, instruction) {
|
|
1007
|
+
const parts = [];
|
|
1008
|
+
parts.push(buildAdvancedRoundPrefix(run, round));
|
|
1009
|
+
parts.push('');
|
|
1010
|
+
parts.push(P2P_BASELINE_PROMPT);
|
|
1011
|
+
if (round.presetPrompt)
|
|
1012
|
+
parts.push(round.presetPrompt);
|
|
1013
|
+
parts.push('');
|
|
1014
|
+
parts.push(`[P2P Advanced Task — run ${run.id}]`);
|
|
1015
|
+
parts.push(`Discussion file: ${filePath}`);
|
|
1016
|
+
parts.push(`Your identity for this round is "${discussionParticipantName(targetSession)}".`);
|
|
1017
|
+
parts.push(`Round id: ${round.id}`);
|
|
1018
|
+
parts.push(`Permission scope: ${round.permissionScope}`);
|
|
1019
|
+
if (round.artifactConvention === 'openspec_convention') {
|
|
1020
|
+
parts.push('Required artifact contract: write OpenSpec artifacts under repository OpenSpec conventions inside openspec/changes/.');
|
|
1021
|
+
}
|
|
1022
|
+
else if (round.artifactOutputs.length > 0) {
|
|
1023
|
+
parts.push(`Required artifact outputs: ${round.artifactOutputs.join(', ')}`);
|
|
1024
|
+
}
|
|
1025
|
+
if (reducerSummary) {
|
|
1026
|
+
parts.push('');
|
|
1027
|
+
parts.push('Reduced context for this attempt:');
|
|
1028
|
+
parts.push(reducerSummary);
|
|
1029
|
+
}
|
|
1030
|
+
if (round.promptAppend) {
|
|
1031
|
+
parts.push('');
|
|
1032
|
+
parts.push(`Additional round instructions: ${round.promptAppend}`);
|
|
1033
|
+
}
|
|
1034
|
+
parts.push('');
|
|
1035
|
+
parts.push(instruction);
|
|
1036
|
+
parts.push(`Add a new heading "## ${sectionHeader}" at the end of the discussion file and write your result below it.`);
|
|
1037
|
+
parts.push('Do not ask for confirmation. Start immediately.');
|
|
1038
|
+
if (run.extraPrompt) {
|
|
1039
|
+
parts.push('');
|
|
1040
|
+
parts.push(`Additional instructions: ${run.extraPrompt}`);
|
|
1041
|
+
}
|
|
1042
|
+
return parts.join('\n');
|
|
1043
|
+
}
|
|
1044
|
+
function buildAdvancedHopPrompt(run, round, target, filePath, sectionHeader, reducerSummary) {
|
|
1045
|
+
const instruction = round.permissionScope === 'analysis_only'
|
|
1046
|
+
? 'Read the discussion file and provide analysis only. Do not edit code or other files.'
|
|
1047
|
+
: round.permissionScope === 'artifact_generation'
|
|
1048
|
+
? 'Read the discussion file and produce the required artifacts. You may write only the round outputs and the discussion note for this round.'
|
|
1049
|
+
: 'Read the discussion file and perform the implementation work required by this round. You may edit code and tests as needed, then append a concise execution note to the discussion file.';
|
|
1050
|
+
return buildAdvancedPromptCommon(run, round, target.session, filePath, sectionHeader, reducerSummary, instruction);
|
|
1051
|
+
}
|
|
1052
|
+
function buildAdvancedSynthesisPrompt(run, round, sectionHeader, reducerSummary) {
|
|
1053
|
+
const instruction = round.summaryPrompt
|
|
1054
|
+
?? 'Synthesize the evidence appended in this round into one authoritative summary.';
|
|
1055
|
+
return buildAdvancedPromptCommon(run, round, run.initiatorSession, run.contextFilePath, sectionHeader, reducerSummary, instruction);
|
|
1056
|
+
}
|
|
1057
|
+
async function executeAdvancedChain(run, serverLink) {
|
|
1058
|
+
const rounds = run.resolvedRounds ?? [];
|
|
1059
|
+
let roundIndex = 0;
|
|
1060
|
+
while (roundIndex < rounds.length) {
|
|
1061
|
+
if (run._cancelled || isTerminal(run.status))
|
|
1062
|
+
return;
|
|
1063
|
+
if (!ensureRunDeadline(run, serverLink))
|
|
1064
|
+
return;
|
|
1065
|
+
const round = rounds[roundIndex];
|
|
1066
|
+
run.timeoutMs = round.timeoutMs;
|
|
1067
|
+
run.currentRound = roundIndex + 1;
|
|
1068
|
+
run.currentRoundId = round.id;
|
|
1069
|
+
run.currentExecutionStep += 1;
|
|
1070
|
+
run.roundAttemptCounts[round.id] = (run.roundAttemptCounts[round.id] ?? 0) + 1;
|
|
1071
|
+
run.currentRoundAttempt = run.roundAttemptCounts[round.id];
|
|
1072
|
+
run.runPhase = 'round_execution';
|
|
1073
|
+
run.summaryPhase = null;
|
|
1074
|
+
run.activePhase = round.dispatchStyle === 'initiator_only' ? 'initial' : 'hop';
|
|
1075
|
+
pushState(run, serverLink);
|
|
1076
|
+
const artifactBaseline = await captureArtifactBaseline(run, round);
|
|
1077
|
+
const reducerSummary = await reduceAdvancedContext(run, round, serverLink);
|
|
1078
|
+
if (run._cancelled || isTerminal(run.status))
|
|
1079
|
+
return;
|
|
1080
|
+
let authoritativeSegment = '';
|
|
1081
|
+
if (round.dispatchStyle === 'initiator_only') {
|
|
1082
|
+
const sectionHeader = `${discussionParticipantName(run.initiatorSession)} — ${round.title} (attempt ${run.currentRoundAttempt})`;
|
|
1083
|
+
const baselineBuffer = await readFile(run.contextFilePath).catch(() => Buffer.from(''));
|
|
1084
|
+
const prompt = buildAdvancedHopPrompt(run, round, { session: run.initiatorSession, mode: round.modeKey }, run.contextFilePath, sectionHeader, reducerSummary);
|
|
1085
|
+
const ok = await dispatchHop(run, run.initiatorSession, prompt, serverLink, {
|
|
1086
|
+
sectionHeader,
|
|
1087
|
+
required: true,
|
|
1088
|
+
});
|
|
1089
|
+
if (!ok)
|
|
1090
|
+
return;
|
|
1091
|
+
authoritativeSegment = await readAppendedContent(run.contextFilePath, baselineBuffer.length);
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
const targets = [...run.allTargets];
|
|
1095
|
+
const roundHops = await createRoundHopStates(run, targets, round.modeKey);
|
|
1096
|
+
try {
|
|
1097
|
+
run.activeTargetSessions = roundHops.map((hop) => hop.session);
|
|
1098
|
+
await Promise.allSettled(targets.map(async (target, index) => {
|
|
1099
|
+
const hop = roundHops[index];
|
|
1100
|
+
const sectionHeader = `${discussionParticipantName(target.session)} — ${round.title} (hop ${index + 1}/${targets.length}, attempt ${run.currentRoundAttempt})`;
|
|
1101
|
+
hop.section_header = sectionHeader;
|
|
1102
|
+
const prompt = buildAdvancedHopPrompt(run, round, target, hop.artifact_path, sectionHeader, reducerSummary);
|
|
1103
|
+
return dispatchHop(run, target.session, prompt, serverLink, {
|
|
1104
|
+
sectionHeader,
|
|
1105
|
+
hop,
|
|
1106
|
+
filePath: hop.artifact_path,
|
|
1107
|
+
});
|
|
1108
|
+
}));
|
|
1109
|
+
run.activeTargetSessions = [];
|
|
1110
|
+
run.currentTargetSession = null;
|
|
1111
|
+
if (run._cancelled || isTerminal(run.status))
|
|
1112
|
+
return;
|
|
1113
|
+
await appendRoundEvidence(run, roundHops);
|
|
1114
|
+
if (round.synthesisStyle === 'initiator_summary') {
|
|
1115
|
+
run.runPhase = 'summarizing';
|
|
1116
|
+
run.summaryPhase = 'running';
|
|
1117
|
+
run.activePhase = 'summary';
|
|
1118
|
+
const sectionHeader = `${discussionParticipantName(run.initiatorSession)} — ${round.title} Synthesis (attempt ${run.currentRoundAttempt})`;
|
|
1119
|
+
const baselineBuffer = await readFile(run.contextFilePath).catch(() => Buffer.from(''));
|
|
1120
|
+
const prompt = buildAdvancedSynthesisPrompt(run, round, sectionHeader, reducerSummary);
|
|
1121
|
+
const ok = await dispatchHop(run, run.initiatorSession, prompt, serverLink, {
|
|
1122
|
+
sectionHeader,
|
|
1123
|
+
required: true,
|
|
1124
|
+
});
|
|
1125
|
+
if (!ok)
|
|
1126
|
+
return;
|
|
1127
|
+
authoritativeSegment = await readAppendedContent(run.contextFilePath, baselineBuffer.length);
|
|
1128
|
+
run.summaryPhase = 'completed';
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
finally {
|
|
1132
|
+
scheduleRoundHopArtifactCleanup(roundHops);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
await validateArtifactOutputsForRound(run, round, artifactBaseline).catch((err) => {
|
|
1136
|
+
failRun(run, 'failed', err instanceof Error ? err.message : String(err), serverLink);
|
|
1137
|
+
});
|
|
1138
|
+
if (run._cancelled || isTerminal(run.status))
|
|
1139
|
+
return;
|
|
1140
|
+
const verdict = round.requiresVerdict ? parseVerdictFromContent(authoritativeSegment) : null;
|
|
1141
|
+
const effectiveVerdict = round.requiresVerdict
|
|
1142
|
+
? (verdict ?? (() => {
|
|
1143
|
+
addHelperDiagnostic(run, {
|
|
1144
|
+
code: 'P2P_VERDICT_MISSING',
|
|
1145
|
+
attempt: run.currentRoundAttempt,
|
|
1146
|
+
sourceSession: run.initiatorSession,
|
|
1147
|
+
message: `Missing verdict marker for round ${round.id}`,
|
|
1148
|
+
});
|
|
1149
|
+
return 'REWORK';
|
|
1150
|
+
})())
|
|
1151
|
+
: null;
|
|
1152
|
+
const jump = round.allowRouting && round.jumpRule
|
|
1153
|
+
? (() => {
|
|
1154
|
+
const jumpCount = run.roundJumpCounts[round.id] ?? 0;
|
|
1155
|
+
const belowMax = jumpCount < round.jumpRule.maxTriggers;
|
|
1156
|
+
if (!belowMax)
|
|
1157
|
+
return null;
|
|
1158
|
+
if (round.verdictPolicy === 'forced_rework') {
|
|
1159
|
+
if (jumpCount < round.jumpRule.minTriggers)
|
|
1160
|
+
return round.jumpRule.targetRoundId;
|
|
1161
|
+
return effectiveVerdict === (round.jumpRule.marker ?? 'REWORK') ? round.jumpRule.targetRoundId : null;
|
|
1162
|
+
}
|
|
1163
|
+
return effectiveVerdict === (round.jumpRule.marker ?? 'REWORK') ? round.jumpRule.targetRoundId : null;
|
|
1164
|
+
})()
|
|
1165
|
+
: null;
|
|
1166
|
+
if (jump) {
|
|
1167
|
+
run.roundJumpCounts[round.id] = (run.roundJumpCounts[round.id] ?? 0) + 1;
|
|
1168
|
+
run.routingHistory.push({
|
|
1169
|
+
fromRoundId: round.id,
|
|
1170
|
+
toRoundId: jump,
|
|
1171
|
+
trigger: effectiveVerdict,
|
|
1172
|
+
atStep: run.currentExecutionStep,
|
|
1173
|
+
atAttempt: run.currentRoundAttempt,
|
|
1174
|
+
timestamp: Date.now(),
|
|
1175
|
+
});
|
|
1176
|
+
roundIndex = rounds.findIndex((entry) => entry.id === jump);
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
roundIndex += 1;
|
|
1180
|
+
}
|
|
1181
|
+
if (!ensureRunDeadline(run, serverLink) || run._cancelled || isTerminal(run.status))
|
|
1182
|
+
return;
|
|
1183
|
+
run.runPhase = 'summarizing';
|
|
1184
|
+
run.summaryPhase = 'running';
|
|
1185
|
+
run.activePhase = 'summary';
|
|
1186
|
+
const finalRound = rounds[Math.max(rounds.length - 1, 0)];
|
|
1187
|
+
run.timeoutMs = finalRound?.timeoutMs ?? run.timeoutMs;
|
|
1188
|
+
const finalPrompt = buildHopPrompt(run, getP2pMode(finalRound?.modeKey ?? run.mode), {
|
|
1189
|
+
session: run.initiatorSession,
|
|
1190
|
+
sectionHeader: `${discussionParticipantNameWithMode(run.initiatorSession, finalRound?.modeKey ?? run.mode)} — Final Summary`,
|
|
1191
|
+
instruction: `${getP2pMode(finalRound?.modeKey ?? run.mode)?.summaryPrompt ?? 'Synthesize a final summary that captures the consensus, key decisions, and any remaining disagreements across all rounds.'}\nBefore writing the summary, use the hop evidence already appended into the discussion file for this round. If the user context clearly specifies a destination file for the final plan, write the complete plan there. Otherwise, write the complete plan at the end of the discussion file.`,
|
|
1192
|
+
isInitial: false,
|
|
1193
|
+
});
|
|
1194
|
+
const summaryOk = await dispatchHop(run, run.initiatorSession, finalPrompt, serverLink, {
|
|
1195
|
+
sectionHeader: `${discussionParticipantNameWithMode(run.initiatorSession, finalRound?.modeKey ?? run.mode)} — Final Summary`,
|
|
1196
|
+
required: true,
|
|
1197
|
+
});
|
|
1198
|
+
if (!summaryOk)
|
|
1199
|
+
return;
|
|
1200
|
+
run.summaryPhase = 'completed';
|
|
1201
|
+
let fullContent = '';
|
|
1202
|
+
try {
|
|
1203
|
+
fullContent = await readFile(run.contextFilePath, 'utf8');
|
|
1204
|
+
run.resultSummary = fullContent.slice(-2000);
|
|
1205
|
+
}
|
|
1206
|
+
catch { /* ignore */ }
|
|
1207
|
+
run.completedAt = new Date().toISOString();
|
|
1208
|
+
transition(run, 'completed', serverLink);
|
|
1209
|
+
setTimeout(() => { activeRuns.delete(run.id); }, 60_000);
|
|
1210
|
+
}
|
|
670
1211
|
async function dispatchHop(run, session, prompt, serverLink, options) {
|
|
671
1212
|
const { sectionHeader, hop = null, required = false } = options;
|
|
672
1213
|
run.currentTargetSession = session;
|
|
@@ -721,6 +1262,15 @@ async function dispatchHop(run, session, prompt, serverLink, options) {
|
|
|
721
1262
|
run.completedHops.push(target);
|
|
722
1263
|
}
|
|
723
1264
|
};
|
|
1265
|
+
const abortForOverallTimeout = async () => {
|
|
1266
|
+
if (hop) {
|
|
1267
|
+
await finishHop('timed_out', 'advanced_run_timeout');
|
|
1268
|
+
}
|
|
1269
|
+
else {
|
|
1270
|
+
run.currentTargetSession = null;
|
|
1271
|
+
run.activeTargetSessions = run.activeTargetSessions.filter((item) => item !== session);
|
|
1272
|
+
}
|
|
1273
|
+
};
|
|
724
1274
|
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
725
1275
|
if (run._cancelled) {
|
|
726
1276
|
await finishHop('cancelled');
|
|
@@ -769,6 +1319,11 @@ async function dispatchHop(run, session, prompt, serverLink, options) {
|
|
|
769
1319
|
let headingFound = false;
|
|
770
1320
|
let headingFoundAt = 0;
|
|
771
1321
|
while (Date.now() < deadline) {
|
|
1322
|
+
if (!ensureRunDeadline(run, serverLink)) {
|
|
1323
|
+
idleWaiter.cancel();
|
|
1324
|
+
await abortForOverallTimeout();
|
|
1325
|
+
return false;
|
|
1326
|
+
}
|
|
772
1327
|
if (Date.now() >= hardDeadline) {
|
|
773
1328
|
logger.warn({ runId: run.id, session }, 'P2P: hard deadline reached, force-skipping hop');
|
|
774
1329
|
break;
|
|
@@ -779,6 +1334,11 @@ async function dispatchHop(run, session, prompt, serverLink, options) {
|
|
|
779
1334
|
return false;
|
|
780
1335
|
}
|
|
781
1336
|
await sleep(IDLE_POLL_MS);
|
|
1337
|
+
if (!ensureRunDeadline(run, serverLink)) {
|
|
1338
|
+
idleWaiter.cancel();
|
|
1339
|
+
await abortForOverallTimeout();
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
782
1342
|
if (Date.now() >= hardDeadline) {
|
|
783
1343
|
logger.warn({ runId: run.id, session }, 'P2P: hard deadline reached, force-skipping hop');
|
|
784
1344
|
break;
|
|
@@ -979,11 +1539,17 @@ function transition(run, status, serverLink) {
|
|
|
979
1539
|
else if (status === 'cancelled') {
|
|
980
1540
|
run.runPhase = 'cancelled';
|
|
981
1541
|
}
|
|
982
|
-
else if (status === 'failed'
|
|
1542
|
+
else if (status === 'failed') {
|
|
983
1543
|
run.runPhase = 'failed';
|
|
984
1544
|
}
|
|
985
1545
|
if (P2P_TERMINAL_RUN_STATUSES.has(status)) {
|
|
986
1546
|
run.completedAt = run.completedAt ?? new Date().toISOString();
|
|
1547
|
+
if (run.advancedP2pEnabled) {
|
|
1548
|
+
void cleanupRoundHopArtifacts(run.hopStates);
|
|
1549
|
+
}
|
|
1550
|
+
else {
|
|
1551
|
+
scheduleRoundHopArtifactCleanup(run.hopStates);
|
|
1552
|
+
}
|
|
987
1553
|
}
|
|
988
1554
|
run.updatedAt = new Date().toISOString();
|
|
989
1555
|
logger.info({ runId: run.id, status }, 'P2P run state transition');
|
|
@@ -995,9 +1561,18 @@ function failRun(run, errorType, message, serverLink) {
|
|
|
995
1561
|
run.updatedAt = new Date().toISOString();
|
|
996
1562
|
const status = errorType === 'timed_out' ? 'timed_out' : 'failed';
|
|
997
1563
|
run.status = status;
|
|
998
|
-
|
|
999
|
-
|
|
1564
|
+
if (status === 'failed') {
|
|
1565
|
+
run.runPhase = 'failed';
|
|
1566
|
+
}
|
|
1567
|
+
if (run.activePhase === 'summary') {
|
|
1000
1568
|
run.summaryPhase = 'failed';
|
|
1569
|
+
}
|
|
1570
|
+
if (run.advancedP2pEnabled) {
|
|
1571
|
+
void cleanupRoundHopArtifacts(run.hopStates);
|
|
1572
|
+
}
|
|
1573
|
+
else {
|
|
1574
|
+
scheduleRoundHopArtifactCleanup(run.hopStates);
|
|
1575
|
+
}
|
|
1001
1576
|
logger.warn({ runId: run.id, errorType, message }, 'P2P run failed');
|
|
1002
1577
|
pushState(run, serverLink);
|
|
1003
1578
|
}
|