thinkpool-pair 0.7.360 → 0.7.362
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/bridge.mjs +163 -55
- package/claude-session.mjs +1 -1
- package/code-event-contract.mjs +1 -0
- package/event-id.mjs +1 -1
- package/flow-conductor.mjs +11 -5
- package/flow-receipt.mjs +115 -0
- package/flow-review.mjs +3 -3
- package/flow-task-graph.mjs +290 -2
- package/package.json +2 -1
- package/recap.mjs +162 -3
- package/side-lane.mjs +1 -1
- package/thinkpool-capabilities.json +7 -4
package/bridge.mjs
CHANGED
|
@@ -77,7 +77,7 @@ import { explicitKeepAwakeChoice, keepAwakeEnabled, saveKeepAwakePreference, sta
|
|
|
77
77
|
|
|
78
78
|
const STRUCTURED_MODES = new Set(['default', 'acceptEdits', 'plan', 'review', 'bypassPermissions'])
|
|
79
79
|
import { FLOW_CONDUCTOR_PROMPT, FLOW_LANE_PROMPT, FLOW_CODEX_CONDUCTOR_PROMPT, FLOW_CODEX_LANE_PROMPT, buildConductorEnv, assembleCrossWaveContext, buildLanePrompt } from './flow-conductor.mjs'
|
|
80
|
-
import { legacyBuilderCompletionAllowed, normalizePlanOutput, validatePlanForRuntime, validReviewTargetShape } from './flow-task-graph.mjs'
|
|
80
|
+
import { legacyBuilderCompletionAllowed, normalizeBaselineEvidence, normalizePlanOutput, validatePlanForRuntime, validateTaskContractSeal, validReviewTargetShape } from './flow-task-graph.mjs'
|
|
81
81
|
import { normalizeFlowRuntime, flowLaneModelFor, flowConductorModelFor, spawnedLaneModelFor, modelCatalogValues, resolveCodexModel, resolveHermesOpenModel, assertRuntimeModelCompatible } from './flow-models.mjs'
|
|
82
82
|
// S1 (context-offload) — durable digest store. mark_flow_done digests a closed slice in;
|
|
83
83
|
// the dispatch loop reads the bounded cross-wave context back out (via assembleCrossWaveContext,
|
|
@@ -97,6 +97,7 @@ function stopFlowPreviews (flowId, laneId = null) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
import { FLOW_REVIEWER_PROMPT, FLOW_CODEX_REVIEWER_PROMPT, revertLane, parseReviewVerdict, reviewVerdictToReflection } from './flow-review.mjs'
|
|
100
|
+
import { createFlowReviewReceipt, createFlowReviewSingleFlight, isFlowReviewReceipt } from './flow-receipt.mjs'
|
|
100
101
|
import { reviewGateDecision } from './flow-review-gate.mjs'
|
|
101
102
|
import { IMMUTABLE_REVIEW_READ_TOOL_EXTRAS, UNSANDBOXED_REVIEW_CHECK_TOOL_EXTRAS, readReviewFile, runReviewCheck } from './review-check.mjs'
|
|
102
103
|
import { pairAdjudicationPrompt, reviewReflectionDecision, REVIEW_DEFAULTS } from './flow-review-reflect.mjs'
|
|
@@ -130,7 +131,7 @@ import { dispatchPendingRecap, recoverInterruptedTurn, recoverMissingResumeOnce,
|
|
|
130
131
|
import { supersedeDispatchLease } from './dispatch-lease.mjs'
|
|
131
132
|
import { realtimeRecoveryDecision, turnInFlight } from './update-gate.mjs'
|
|
132
133
|
import { saveSession, flushSession, deleteSession, loadAll, canResume, loadPtyId, savePtyId, loadNames, saveNames, appendDurableEvents, seedDurableEvents, readDurablePage, readDurableOldestSeq, hasDurableArchive, servesHistoryPage, acknowledgePendingScheduledOutcome, commitRecordedScheduledOutcome, deletePendingScheduledOutcome, loadPendingScheduledOutcome, loadPendingScheduledOutcomes, savePendingScheduledOutcome } from './session-store.mjs'
|
|
133
|
-
import { stampEvent, makeSeqCounter, maxSeq, seqable, capReplayEvents, chunkReplayEvents, boundEventForBroadcast, inlineImageBlocks, ImageEventQueue, imageQueueConfig, uploadCodeImage as uploadCodeImageRequest, usageReportLine, codexUsageReportLine, appendCurrentPersonRequest, buildRecapFromLog, RECAP_CAP, trimmedBeforeSeq, firstSeq } from './event-id.mjs'
|
|
134
|
+
import { stampEvent, makeSeqCounter, maxSeq, seqable, capReplayEvents, chunkReplayEvents, boundEventForBroadcast, inlineImageBlocks, ImageEventQueue, imageQueueConfig, uploadCodeImage as uploadCodeImageRequest, usageReportLine, codexUsageReportLine, appendCurrentPersonRequest, buildCheckpointFromLog, buildRecapFromLog, resolveCheckpointCarry, RECAP_CAP, trimmedBeforeSeq, firstSeq } from './event-id.mjs'
|
|
134
135
|
import { createLatestReplayPump, requestedReplayIds } from './replay-transport.mjs'
|
|
135
136
|
import { classifyCodeEvent, unknownCodeEventNotice } from './code-event-contract.mjs'
|
|
136
137
|
import { fetchPairControlDeliveryAuthority } from './pair-control-authority.mjs'
|
|
@@ -966,11 +967,13 @@ const MIN_TURN_MS = Number(process.env.TP_NOTIFY_MIN_TURN_MS) > 0
|
|
|
966
967
|
? Number(process.env.TP_NOTIFY_MIN_TURN_MS) : DEFAULT_MIN_TURN_MS
|
|
967
968
|
function persistAgentEvent (payload) {
|
|
968
969
|
if (AGENT_NOTIFY_OFF || !codeAuthToken || !myServeUid || !room) return
|
|
970
|
+
const { eventId, ...eventPayload } = payload || {}
|
|
971
|
+
const stableId = typeof eventId === 'string' && /^[A-Za-z0-9_-]{1,120}$/.test(eventId) ? eventId : randomUUID()
|
|
969
972
|
const row = {
|
|
970
|
-
id: `ae:${
|
|
973
|
+
id: `ae:${stableId}`,
|
|
971
974
|
session_code: room,
|
|
972
975
|
author_id: myServeUid,
|
|
973
|
-
payload: { __agent: true, ts: Date.now(), ...
|
|
976
|
+
payload: { __agent: true, ts: Date.now(), ...eventPayload },
|
|
974
977
|
}
|
|
975
978
|
fetch(`${SUPABASE_URL}/rest/v1/code_messages`, {
|
|
976
979
|
method: 'POST',
|
|
@@ -2427,6 +2430,13 @@ function emitCodexCompactionControl(entry, text, by = null) {
|
|
|
2427
2430
|
bcast('code-event', { term: entry.id, evt })
|
|
2428
2431
|
}
|
|
2429
2432
|
|
|
2433
|
+
function armCompactionCheckpoint(entry) {
|
|
2434
|
+
const checkpoint = buildCheckpointFromLog(entry?.log)
|
|
2435
|
+
if (!checkpoint) return false
|
|
2436
|
+
entry.pendingCheckpoint = checkpoint
|
|
2437
|
+
return true
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2430
2440
|
async function runCodexCompaction(entry, request = {}) {
|
|
2431
2441
|
const recap = buildRecapFromLog(entry.log, RECAP_CAP, { reason: 'compact' })
|
|
2432
2442
|
if (!recap) {
|
|
@@ -2450,6 +2460,7 @@ async function runCodexCompaction(entry, request = {}) {
|
|
|
2450
2460
|
if (!entry.session.turnActive && settleLaneControl(entry)) announce()
|
|
2451
2461
|
}
|
|
2452
2462
|
if (nativeCompacted === true) {
|
|
2463
|
+
armCompactionCheckpoint(entry)
|
|
2453
2464
|
const evt = { kind: 'compaction', trigger: 'manual', preTokens, by: request.by, native: true }
|
|
2454
2465
|
pushLog(entry, evt)
|
|
2455
2466
|
bcast('code-event', { term: entry.id, evt })
|
|
@@ -2468,7 +2479,9 @@ async function runCodexCompaction(entry, request = {}) {
|
|
|
2468
2479
|
emitCodexCompactionControl(entry, 'Codex context compaction unavailable right now', request.by)
|
|
2469
2480
|
return
|
|
2470
2481
|
}
|
|
2471
|
-
|
|
2482
|
+
const carry = resolveCheckpointCarry(recap, entry.pendingCheckpoint)
|
|
2483
|
+
entry.pendingRecap = carry.pendingRecap
|
|
2484
|
+
entry.pendingCheckpoint = carry.pendingCheckpoint
|
|
2472
2485
|
const evt = { kind: 'compaction', trigger: 'manual', preTokens, by: request.by }
|
|
2473
2486
|
pushLog(entry, evt)
|
|
2474
2487
|
bcast('code-event', { term: entry.id, evt })
|
|
@@ -2523,7 +2536,7 @@ function worktreeSnapshot(cwd) {
|
|
|
2523
2536
|
// relay STRUCTURED events. onEvent → broadcast `code-event` + print locally +
|
|
2524
2537
|
// persist to the host file; tool calls round-trip through the perm card; the
|
|
2525
2538
|
// rolling log replays to joiners and survives bridge restarts (session-store).
|
|
2526
|
-
function openStructured({ id, runtime = 'claude', model, models, effort, resume, log, commands, mode, spawnedBy, spawnDepth, cascadeRole, hop, sideParent, sideTask, pendingSideContexts, pendingWorkerCompletions, workerCompletionsInFlight, rolePrompt, sliceType, flowSessionId, flowTaskKey, flowRole, flowReviewTarget, flowReviewTargets, flowReviewSnapshots, flowReviewRound, dispatchBaseSha, revertTarget, cwd, managedWorktree, reviewSliceRoots, openedAt, defer, provider, carryRecap, lastUsage, receivedTurnCids, scheduleRunId, scheduleDeadlineAt, scheduleOutcomeRecorded, scheduleAdmissionLease }) {
|
|
2539
|
+
function openStructured({ id, runtime = 'claude', model, models, effort, resume, log, commands, mode, spawnedBy, spawnDepth, cascadeRole, hop, sideParent, sideTask, pendingSideContexts, pendingWorkerCompletions, workerCompletionsInFlight, rolePrompt, sliceType, flowSessionId, flowTaskKey, flowTaskContract, flowRole, flowReviewTarget, flowReviewTargets, flowReviewSnapshots, flowReviewRound, flowReviewConclusion, dispatchBaseSha, revertTarget, cwd, managedWorktree, reviewSliceRoots, openedAt, defer, provider, carryRecap, carryCheckpoint, lastUsage, receivedTurnCids, scheduleRunId, scheduleDeadlineAt, scheduleOutcomeRecorded, scheduleAdmissionLease }) {
|
|
2527
2540
|
if (sessions.has(id)) return
|
|
2528
2541
|
runtime = structuredRuntimeMetadata(runtime) ? runtime : 'claude'
|
|
2529
2542
|
// Fail closed before exposing a native lane if its bridge semantic contract
|
|
@@ -2596,6 +2609,9 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2596
2609
|
flowReviewTargets: Array.isArray(flowReviewTargets) && flowReviewTargets.length ? flowReviewTargets.filter(Boolean) : (flowReviewTarget ? [flowReviewTarget] : []),
|
|
2597
2610
|
flowReviewSnapshots: Array.isArray(flowReviewSnapshots) ? flowReviewSnapshots.filter((item) => item?.taskKey && item?.sha && item?.cwd) : [],
|
|
2598
2611
|
flowReviewRound: Number.isInteger(flowReviewRound) && flowReviewRound >= 0 ? flowReviewRound : 0,
|
|
2612
|
+
flowReviewConclusion: flowReviewConclusion?.taskKey && ['pass', 'reject', 'surface'].includes(flowReviewConclusion?.action) && isFlowReviewReceipt(flowReviewConclusion?.receipt)
|
|
2613
|
+
? { taskKey: flowReviewConclusion.taskKey, action: flowReviewConclusion.action, round: flowReviewConclusion.round, receipt: flowReviewConclusion.receipt }
|
|
2614
|
+
: null,
|
|
2599
2615
|
dispatchBaseSha: dispatchBaseSha || null,
|
|
2600
2616
|
revertTarget: revertTarget || null,
|
|
2601
2617
|
// Stable creation order — persisted so a bridge restart restores tabs in the SAME
|
|
@@ -2635,6 +2651,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2635
2651
|
// event-id.mjs + src/pages/code/seqDedup.js). pushLog() is the single stamp point.
|
|
2636
2652
|
entry.seq = makeSeqCounter(maxSeq(entry.log))
|
|
2637
2653
|
entry.rolePrompt = rolePrompt || null // FL-M6 — persisted so a bridge restart restores the flow role prompt
|
|
2654
|
+
entry.flowTaskContract = flowTaskContract && typeof flowTaskContract === 'object' ? flowTaskContract : null
|
|
2638
2655
|
// lastUsage — the terminal's most recent usage/ctx meter. Persisted (sessionData) +
|
|
2639
2656
|
// restored so a bridge restart can re-emit it on replay: usage is chrome (kept out of
|
|
2640
2657
|
// the replayed transcript log), so without this a (re)joiner sees "—" for context until
|
|
@@ -2649,10 +2666,17 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2649
2666
|
// MCP tool AND the FLOW_DONE sentinel-Write intercept: like the conductor's submit, the
|
|
2650
2667
|
// MCP tool is DEFERRED (needs ToolSearch, which hangs), so lanes signal done by Writing a
|
|
2651
2668
|
// FLOW_DONE file — a DIRECT tool — which the PreToolUse hook routes here. Returns a message.
|
|
2652
|
-
const markFlowDone = async ({ reviewPass = false } = {}) => {
|
|
2669
|
+
const markFlowDone = async ({ reviewPass = false, baselineEvidence = '' } = {}) => {
|
|
2653
2670
|
if (!entry.flowSessionId || !entry.flowTaskKey) return 'Not a Flow lane — nothing to mark done.'
|
|
2654
2671
|
if (entry.flowRole !== 'builder' && !(reviewPass && entry.flowRole === 'reviewer')) return 'This Flow role cannot mark a builder slice done.'
|
|
2655
2672
|
if (entry.flowDone) return 'This slice is already recorded as done.'
|
|
2673
|
+
let baselineReceipt = ''
|
|
2674
|
+
const requiresBaselineReceipt = !reviewPass && !!entry.flowTaskContract?.baseline?.gate
|
|
2675
|
+
if (requiresBaselineReceipt) {
|
|
2676
|
+
try { baselineReceipt = normalizeBaselineEvidence(baselineEvidence) } catch (e) {
|
|
2677
|
+
return `Slice "${entry.flowTaskKey}" is not done: submit the real bounded baselineEvidence receipt (${e?.message || e}). Observe the assigned pre-edit gate; do not fabricate a RED sentence.`
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2656
2680
|
let commitSha = null
|
|
2657
2681
|
try { commitSha = execFileSync('git', ['-C', entry.cwd || process.cwd(), 'rev-parse', 'HEAD'], { encoding: 'utf8' }).trim() } catch { /* no commits yet */ }
|
|
2658
2682
|
if (!reviewPass && entry.dispatchBaseSha && commitSha === entry.dispatchBaseSha) {
|
|
@@ -2681,13 +2705,16 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2681
2705
|
]
|
|
2682
2706
|
const digest = digestSlice(
|
|
2683
2707
|
{ key: entry.flowTaskKey, title: termNames[id] || entry.flowTaskKey },
|
|
2684
|
-
{ acceptanceProof: commitSha ? `committed ${commitSha.slice(0, 8)}` : 'slice done'
|
|
2708
|
+
{ acceptanceProof: `${baselineReceipt ? `baseline: ${baselineReceipt}; ` : ''}${commitSha ? `committed ${commitSha.slice(0, 8)}` : 'slice done'}`, artifacts },
|
|
2685
2709
|
)
|
|
2686
2710
|
appendDigest(entry.flowSessionId, digest, { baseDir: process.cwd() }) // idempotent per (flowId, taskKey)
|
|
2687
2711
|
} catch (e) {
|
|
2688
2712
|
process.stderr.write(`\n ${A.dim}◆ flow digest skip ${entry.flowTaskKey} — ${e?.message || e}${A.rst}\n`)
|
|
2689
2713
|
}
|
|
2690
|
-
|
|
2714
|
+
const completedContract = requiresBaselineReceipt
|
|
2715
|
+
? { ...entry.flowTaskContract, baseline: { ...entry.flowTaskContract.baseline, evidence: baselineReceipt } }
|
|
2716
|
+
: entry.flowTaskContract || null
|
|
2717
|
+
bcast('flow-task-done', { term: id, flowId: entry.flowSessionId, taskKey: entry.flowTaskKey, laneId: id, commitSha, previewUrl, contract: completedContract, ...(reviewPass ? { reviewAction: 'pass' } : {}) }, flowChannel)
|
|
2691
2718
|
process.stderr.write(`\n ${A.cyan}◆ flow slice done — ${entry.flowTaskKey} (${commitSha ? commitSha.slice(0, 8) : 'no commit'})${previewUrl ? ` · preview ${previewUrl}` : ''}${A.rst}\n`)
|
|
2692
2719
|
entry.flowDone = true // FL-B3 — retire immediately so it drops from the ≤8 lane cap
|
|
2693
2720
|
setTimeout(() => { try { endStructured(id) } catch { /* already gone */ } }, 2500)
|
|
@@ -2698,7 +2725,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2698
2725
|
// review gate was dead code: reviewers had no working emit path, so a failing slice was
|
|
2699
2726
|
// never reverted). On FAIL → broadcast flow-revert for the reviewed slice (the client flips
|
|
2700
2727
|
// it to pending → the next wave rebuilds it). Either way the review lane itself is done.
|
|
2701
|
-
const
|
|
2728
|
+
const processReviewVerdict = async (raw) => {
|
|
2702
2729
|
if (!entry.flowSessionId || !entry.flowTaskKey || entry.flowRole !== 'reviewer') return { ok: false, message: 'Not a Flow review lane.' }
|
|
2703
2730
|
let v, target = null
|
|
2704
2731
|
try {
|
|
@@ -2716,6 +2743,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2716
2743
|
? `Review verdict REJECTED: ${e?.message || e}. Re-call submit_flow_review with a valid authorized taskKey.`
|
|
2717
2744
|
: `Review verdict REJECTED: ${e?.message || e}. Re-Write FLOW_REVIEW.json with {"pass":<boolean>,"reasons":["<specific finding>"],"taskKey":"<the slice you reviewed>"}.` }
|
|
2718
2745
|
}
|
|
2746
|
+
if (entry.flowReviewConclusion) return { ok: true, message: `Review already concluded for ${entry.flowReviewConclusion.taskKey}; receipt ${entry.flowReviewConclusion.receipt.cid || 'recorded'}.` }
|
|
2719
2747
|
// E1 A1/A2 — the BOUNDED reviewer loop, live side. Each FLOW_REVIEW.json write is ONE
|
|
2720
2748
|
// hunt round; the governor decides continue-vs-stop from the round count + the lane's
|
|
2721
2749
|
// REAL budget (flowBudgets ledger). A bare pass:true (happy path held, not yet exhausted)
|
|
@@ -2757,15 +2785,51 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2757
2785
|
} else {
|
|
2758
2786
|
process.stderr.write(`\n ${A.cyan}◆ review ${decision.action.toUpperCase()} (round ${round}) — ${target || entry.flowTaskKey}${A.rst}\n`)
|
|
2759
2787
|
}
|
|
2760
|
-
// Durable room-visible verdict.
|
|
2761
|
-
//
|
|
2762
|
-
//
|
|
2763
|
-
//
|
|
2764
|
-
|
|
2765
|
-
const
|
|
2766
|
-
|
|
2767
|
-
|
|
2788
|
+
// Durable room-visible verdict. Bind a conclusive pass to the exact immutable
|
|
2789
|
+
// builder snapshot plus the acceptance digest inherited at approval time. A
|
|
2790
|
+
// missing/malformed proof can never look like a verified pass: it becomes a
|
|
2791
|
+
// held receipt and assembly stays blocked for pair adjudication.
|
|
2792
|
+
const reviewedTaskKey = target || entry.flowTaskKey
|
|
2793
|
+
const targetSnapshot = entry.flowReviewSnapshots.find((item) => item.taskKey === reviewedTaskKey) || null
|
|
2794
|
+
const acceptanceDigest = entry.flowTaskContract?.digest || null
|
|
2795
|
+
let terminalAction = decision.action
|
|
2796
|
+
let receiptReasons = v.reasons.length ? v.reasons : [decision.reason]
|
|
2797
|
+
let visibleReview
|
|
2798
|
+
try {
|
|
2799
|
+
visibleReview = createFlowReviewReceipt({
|
|
2800
|
+
taskKey: reviewedTaskKey,
|
|
2801
|
+
outcome: terminalAction === 'surface' ? 'held' : terminalAction,
|
|
2802
|
+
rounds: round,
|
|
2803
|
+
candidateSha: targetSnapshot?.sha,
|
|
2804
|
+
acceptanceDigest,
|
|
2805
|
+
reasons: receiptReasons,
|
|
2806
|
+
})
|
|
2807
|
+
} catch (error) {
|
|
2808
|
+
if (terminalAction !== 'pass') {
|
|
2809
|
+
return { ok: false, message: `Review verdict REJECTED: ${error?.message || error}.` }
|
|
2810
|
+
}
|
|
2811
|
+
terminalAction = 'surface'
|
|
2812
|
+
receiptReasons = [...receiptReasons, 'Exact candidate or sealed acceptance proof is missing; pass was held.']
|
|
2813
|
+
visibleReview = createFlowReviewReceipt({
|
|
2814
|
+
taskKey: reviewedTaskKey,
|
|
2815
|
+
outcome: 'held',
|
|
2816
|
+
rounds: round,
|
|
2817
|
+
candidateSha: targetSnapshot?.sha,
|
|
2818
|
+
acceptanceDigest,
|
|
2819
|
+
reasons: receiptReasons,
|
|
2820
|
+
})
|
|
2821
|
+
}
|
|
2822
|
+
visibleReview.cid = randomUUID()
|
|
2823
|
+
entry.flowReviewConclusion = {
|
|
2824
|
+
taskKey: reviewedTaskKey,
|
|
2825
|
+
action: terminalAction,
|
|
2826
|
+
round,
|
|
2827
|
+
receipt: visibleReview,
|
|
2768
2828
|
}
|
|
2829
|
+
// Persist the conclusion before emitting side effects. A retry or restart
|
|
2830
|
+
// can reconstruct a missing transcript row from this safe receipt, but can
|
|
2831
|
+
// never conclude the same review twice.
|
|
2832
|
+
entry.flush?.()
|
|
2769
2833
|
pushLog(entry, visibleReview)
|
|
2770
2834
|
bcast('code-event', { term: id, evt: visibleReview })
|
|
2771
2835
|
entry.flush?.()
|
|
@@ -2775,45 +2839,53 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2775
2839
|
// broadcast, does NOT change the revert/gate flow.
|
|
2776
2840
|
bcast('flow-review-verdict', {
|
|
2777
2841
|
term: id, flowId: entry.flowSessionId,
|
|
2778
|
-
taskKey:
|
|
2779
|
-
pass:
|
|
2780
|
-
reasons:
|
|
2781
|
-
action:
|
|
2842
|
+
taskKey: reviewedTaskKey,
|
|
2843
|
+
pass: terminalAction === 'pass',
|
|
2844
|
+
reasons: receiptReasons,
|
|
2845
|
+
action: terminalAction,
|
|
2782
2846
|
rounds: round,
|
|
2783
|
-
surfaceToPair: decision.surfaceToPair,
|
|
2847
|
+
surfaceToPair: terminalAction === 'surface' || decision.surfaceToPair,
|
|
2784
2848
|
prompt: pairAdjudicationPrompt({
|
|
2785
|
-
action:
|
|
2786
|
-
reason: decision.reason,
|
|
2787
|
-
taskKey:
|
|
2788
|
-
findings:
|
|
2849
|
+
action: terminalAction,
|
|
2850
|
+
reason: terminalAction === decision.action ? decision.reason : receiptReasons.at(-1),
|
|
2851
|
+
taskKey: reviewedTaskKey,
|
|
2852
|
+
findings: receiptReasons,
|
|
2789
2853
|
}),
|
|
2790
2854
|
}, flowChannel)
|
|
2791
2855
|
let doneMsg = ''
|
|
2792
|
-
if (
|
|
2856
|
+
if (terminalAction === 'pass') {
|
|
2793
2857
|
doneMsg = await markFlowDone({ reviewPass: true })
|
|
2794
2858
|
} else {
|
|
2795
|
-
if (
|
|
2796
|
-
bcast('flow-review-held', { term: id, flowId: entry.flowSessionId, reviewTaskKey: entry.flowTaskKey, taskKey: target, reasons:
|
|
2797
|
-
persistAgentEvent({ kind: 'needs-input', term: id, termName: termNames[id] || null, summary: `Flow review held ${
|
|
2859
|
+
if (terminalAction === 'surface') {
|
|
2860
|
+
bcast('flow-review-held', { term: id, flowId: entry.flowSessionId, reviewTaskKey: entry.flowTaskKey, taskKey: target, reasons: receiptReasons }, flowChannel)
|
|
2861
|
+
persistAgentEvent({ eventId: visibleReview.cid, kind: 'needs-input', term: id, termName: termNames[id] || null, summary: `Flow review held ${reviewedTaskKey}: ${receiptReasons.join('; ') || decision.reason}` })
|
|
2798
2862
|
}
|
|
2799
2863
|
// A reject must rebuild the target and then run a fresh reviewer. Retire that
|
|
2800
2864
|
// reviewer now. A surfaced inconclusive review is different: keep its lane
|
|
2801
2865
|
// alive with the durable control row above so either room member can open the
|
|
2802
2866
|
// held review, read the exact findings, and continue/adjudicate after reload.
|
|
2803
2867
|
// Neither outcome emits task-done or permits assembly.
|
|
2804
|
-
if (
|
|
2868
|
+
if (terminalAction === 'reject') {
|
|
2805
2869
|
entry.flowDone = true
|
|
2806
2870
|
entry.flush?.()
|
|
2807
2871
|
setTimeout(() => { try { endStructured(id) } catch { /* already gone */ } }, 2500)
|
|
2808
2872
|
}
|
|
2809
2873
|
}
|
|
2810
|
-
const label =
|
|
2811
|
-
? `REJECT (round ${round}) — reverting ${target || '(no target)'} (${
|
|
2812
|
-
:
|
|
2813
|
-
? `SURFACED to the pair after ${round} round${round === 1 ? '' : 's'} — ${decision.reason}`
|
|
2874
|
+
const label = terminalAction === 'reject'
|
|
2875
|
+
? `REJECT (round ${round}) — reverting ${target || '(no target)'} (${receiptReasons.join('; ').slice(0, 140)})`
|
|
2876
|
+
: terminalAction === 'surface'
|
|
2877
|
+
? `SURFACED to the pair after ${round} round${round === 1 ? '' : 's'} — ${receiptReasons.at(-1) || decision.reason}`
|
|
2814
2878
|
: `PASS (round ${round})`
|
|
2815
2879
|
return { ok: true, message: `Review verdict recorded: ${label}.${doneMsg ? ` ${doneMsg}` : ''}` }
|
|
2816
2880
|
}
|
|
2881
|
+
const runFlowReviewVerdict = createFlowReviewSingleFlight()
|
|
2882
|
+
const onReviewVerdict = async (raw) => {
|
|
2883
|
+
if (entry.flowReviewConclusion) return { ok: true, message: `Review already concluded for ${entry.flowReviewConclusion.taskKey}; receipt ${entry.flowReviewConclusion.receipt.cid || 'recorded'}.` }
|
|
2884
|
+
// One terminal verdict is one transaction. In particular, reject awaits the
|
|
2885
|
+
// host revert before it can persist its conclusion; without this single-flight
|
|
2886
|
+
// fence two simultaneous submissions can both cross that await and revert/emit.
|
|
2887
|
+
return runFlowReviewVerdict(() => processReviewVerdict(raw))
|
|
2888
|
+
}
|
|
2817
2889
|
// Identity for the durable archive — pushLog appends every new transcript event to
|
|
2818
2890
|
// <room>/<id>.events.jsonl keyed off these. Seed the archive once from the restored
|
|
2819
2891
|
// (≤2000) log so the retained window is immediately pageable; no-op if it already
|
|
@@ -2870,13 +2942,19 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
2870
2942
|
// restart. Without this, sessionData omitted it → on restart the resumed session
|
|
2871
2943
|
// re-launched on the host default (Opus) regardless of the last switch, and the
|
|
2872
2944
|
// switch looked like it "never changed the model" (Max 2026-07-02). Restored below.
|
|
2873
|
-
const sessionData = () => ({ sessionId: entry.session?.sessionId || null, runtime: entry.runtime, log: entry.log, commands: entry.commands, mode: entry.mode, effort: entry.effort, model: entry.model || null, models: entry.runtime === 'hermes' ? (entry.models || []) : undefined, provider: entry.provider || null, spawnedBy: entry.spawnedBy, spawnDepth: entry.spawnDepth || 0, cascadeRole: entry.cascadeRole || null, hop: entry.hop || 0, sideParent: entry.sideParent, sideTask: entry.sideTask, scheduleRunId: entry.scheduleRunId || null, scheduleDeadlineAt: entry.scheduleDeadlineAt || null, scheduleOutcomeRecorded: entry.scheduleOutcomeRecorded === true, scheduleAdmissionLease: entry.scheduleAdmissionLease || null, pendingSideContexts: entry.pendingSideContexts, pendingWorkerCompletions: entry.pendingWorkerCompletions, workerCompletionsInFlight: entry.workerCompletionsInFlight, sliceType: entry.sliceType, flowSessionId: entry.flowSessionId, flowTaskKey: entry.flowTaskKey, flowRole: entry.flowRole || null, flowReviewTargets: entry.flowReviewTargets || [], flowReviewSnapshots: entry.flowReviewSnapshots || [], flowReviewRound: entry.flowReviewRound || 0, dispatchBaseSha: entry.dispatchBaseSha || null, cwd: entry.cwd, managedWorktree: entry.managedWorktree, rolePrompt: entry.rolePrompt, flowReviewTarget: entry.flowReviewTarget || null, revertTarget: entry.revertTarget || null, reviewSliceRoots: entry.reviewSliceRoots || [], openedAt: entry.openedAt || null, lastUsage: entry.lastUsage || null, carryRecap: entry.pendingRecap || null, receivedTurnCids: [...entry.receivedTurnCids].slice(-1000) })
|
|
2945
|
+
const sessionData = () => ({ sessionId: entry.session?.sessionId || null, runtime: entry.runtime, log: entry.log, commands: entry.commands, mode: entry.mode, effort: entry.effort, model: entry.model || null, models: entry.runtime === 'hermes' ? (entry.models || []) : undefined, provider: entry.provider || null, spawnedBy: entry.spawnedBy, spawnDepth: entry.spawnDepth || 0, cascadeRole: entry.cascadeRole || null, hop: entry.hop || 0, sideParent: entry.sideParent, sideTask: entry.sideTask, scheduleRunId: entry.scheduleRunId || null, scheduleDeadlineAt: entry.scheduleDeadlineAt || null, scheduleOutcomeRecorded: entry.scheduleOutcomeRecorded === true, scheduleAdmissionLease: entry.scheduleAdmissionLease || null, pendingSideContexts: entry.pendingSideContexts, pendingWorkerCompletions: entry.pendingWorkerCompletions, workerCompletionsInFlight: entry.workerCompletionsInFlight, sliceType: entry.sliceType, flowSessionId: entry.flowSessionId, flowTaskKey: entry.flowTaskKey, flowTaskContract: entry.flowTaskContract, flowRole: entry.flowRole || null, flowReviewTargets: entry.flowReviewTargets || [], flowReviewSnapshots: entry.flowReviewSnapshots || [], flowReviewRound: entry.flowReviewRound || 0, flowReviewConclusion: entry.flowReviewConclusion, dispatchBaseSha: entry.dispatchBaseSha || null, cwd: entry.cwd, managedWorktree: entry.managedWorktree, rolePrompt: entry.rolePrompt, flowReviewTarget: entry.flowReviewTarget || null, revertTarget: entry.revertTarget || null, reviewSliceRoots: entry.reviewSliceRoots || [], openedAt: entry.openedAt || null, lastUsage: entry.lastUsage || null, carryRecap: entry.pendingRecap || null, carryCheckpoint: entry.pendingCheckpoint || null, receivedTurnCids: [...entry.receivedTurnCids].slice(-1000) })
|
|
2874
2946
|
const persist = () => saveSession(room, id, sessionData())
|
|
2875
2947
|
// Synchronous flush of this session's record. Used on open (so a brand-new session
|
|
2876
2948
|
// has a file under its id BEFORE its first event — surviving a restart inside the
|
|
2877
2949
|
// 1.5s saveSession debounce window) and on shutdown (so events since the last
|
|
2878
2950
|
// debounced write aren't lost). Contract #2: restart resumes, no lost messages.
|
|
2879
2951
|
entry.flush = () => flushSession(room, id, sessionData())
|
|
2952
|
+
if (entry.flowReviewConclusion?.receipt && !entry.log.some((event) => event?.cid === entry.flowReviewConclusion.receipt.cid)) {
|
|
2953
|
+
const restoredReceipt = { ...entry.flowReviewConclusion.receipt }
|
|
2954
|
+
pushLog(entry, restoredReceipt)
|
|
2955
|
+
bcast('code-event', { term: id, evt: restoredReceipt })
|
|
2956
|
+
entry.flush()
|
|
2957
|
+
}
|
|
2880
2958
|
if (entry.scheduleOutcomeRecorded && entry.scheduleAdmissionLease) {
|
|
2881
2959
|
void releaseScheduledAdmissionForEntry(entry).then((released) => {
|
|
2882
2960
|
if (released) entry.flush?.()
|
|
@@ -3450,9 +3528,9 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
3450
3528
|
// (slices whose deps just got satisfied).
|
|
3451
3529
|
...(entry.flowRole === 'builder' ? [tool(
|
|
3452
3530
|
'mark_flow_done',
|
|
3453
|
-
"ThinkPool Flow ONLY — call this ONCE when your slice is built, RUNS, and meets its acceptance criteria. Records your latest commit as the slice's atomic-revert target and signals the room that this slice is done (which unblocks slices that depended on you).
|
|
3454
|
-
{},
|
|
3455
|
-
async () => ({ content: [{ type: 'text', text: await markFlowDone() }] }),
|
|
3531
|
+
"ThinkPool Flow ONLY — call this ONCE when your slice is built, RUNS, and meets its acceptance criteria. For a gate-first task, pass `baselineEvidence`: the real, bounded pre-edit command/behavior receipt proving the assigned failure/absence. Never fabricate a RED sentence. Records your latest commit as the slice's atomic-revert target and signals the room that this slice is done (which unblocks slices that depended on you). Do not call before the slice actually runs + meets acceptance.",
|
|
3532
|
+
{ baselineEvidence: z.string().max(1400).optional().describe('real single-line pre-edit baseline command/behavior receipt; required for new gate-first tasks') },
|
|
3533
|
+
async (args) => ({ content: [{ type: 'text', text: await markFlowDone({ baselineEvidence: args?.baselineEvidence || '' }) }] }),
|
|
3456
3534
|
)] : []),
|
|
3457
3535
|
// FL-B1 — the conductor SUBMITS its decomposition through THIS tool, not the built-in
|
|
3458
3536
|
// ExitPlanMode. In the current SDK, ExitPlanMode is a deferred tool the conductor must
|
|
@@ -3462,17 +3540,17 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
3462
3540
|
// reject malformed plans back to the conductor so it re-emits.
|
|
3463
3541
|
...(entry.flowRole === 'conductor' ? [tool(
|
|
3464
3542
|
'submit_flow_plan',
|
|
3465
|
-
'ThinkPool Flow CONDUCTOR ONLY — submit your decomposition for human approval. Call this ONCE when your task-graph is ready. Pass it as `plan`: a JSON string {"summary":"<one line
|
|
3543
|
+
'ThinkPool Flow CONDUCTOR ONLY — submit your decomposition for human approval. Call this ONCE when your task-graph is ready. Pass it as `plan`: a JSON string {"summary":"<one line; assumptions: …>","tasks":[{"key":"<kebab>","title":"…","scope":"…","acceptance":"observable proof","nonGoals":["bounded exclusion"],"baseline":{"gate":"what fails/is absent before edits","evidence":"optional observed receipt"},"deps":["<key>"],"sliceType":"feature|scaffold|review|fix"}]}. Each builder/fix/scaffold needs acceptance, nonGoals, and a safe bounded baseline gate; review tasks inherit one builder contract. When uncertain, choose and record a safe reversible default; ask only when a choice materially expands scope or authority. The room validates and persists the graph, then shows approval. Do NOT use ExitPlanMode or write a plan file.',
|
|
3466
3544
|
{ plan: z.string().describe('the task-graph as a JSON string (the {summary, tasks:[…]} object)') },
|
|
3467
3545
|
async (args) => {
|
|
3468
3546
|
const okText = (t) => ({ content: [{ type: 'text', text: t }] })
|
|
3469
3547
|
if (!entry.flowSessionId || entry.flowRole !== 'conductor') return okText('Not a Flow conductor — there is no flow to submit a plan for.')
|
|
3470
3548
|
let norm
|
|
3471
3549
|
try { norm = validatePlanForRuntime(normalizePlanOutput(args?.plan || ''), entry.runtime) }
|
|
3472
|
-
catch (e) { return okText(`Plan REJECTED: ${e?.message || e}. Re-call submit_flow_plan with valid JSON
|
|
3550
|
+
catch (e) { return okText(`Plan REJECTED: ${e?.message || e}. Re-call submit_flow_plan with valid JSON: a non-empty acyclic tasks array; each builder/fix/scaffold has observable acceptance, bounded nonGoals, and a safe baseline gate that says what fails/is absent; each review targets exactly one builder contract.`) }
|
|
3473
3551
|
bcast('flow-plan', { term: id, flowId: entry.flowSessionId, plan: JSON.stringify(norm) }, flowChannel)
|
|
3474
3552
|
process.stderr.write(`\n ${A.mag}◆ flow plan submitted — flow ${String(entry.flowSessionId).slice(0, 8)} (${norm.tasks.length} slice${norm.tasks.length === 1 ? '' : 's'}).${A.rst}\n`)
|
|
3475
|
-
return okText(`Plan submitted (${norm.tasks.length} slice${norm.tasks.length === 1 ? '' : 's'})
|
|
3553
|
+
return okText(`Plan submitted (${norm.tasks.length} slice${norm.tasks.length === 1 ? '' : 's'}) with gate-first contracts. Safe reversible assumptions belong in the summary; user steering remains authoritative. The room is showing approval. You are DONE; stop here and wait for approval (lane dispatch is the room's job).`)
|
|
3476
3554
|
},
|
|
3477
3555
|
)] : []),
|
|
3478
3556
|
...(entry.flowRole === 'reviewer' ? [tool(
|
|
@@ -3523,7 +3601,13 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
3523
3601
|
// human turn cannot strand a fresh Codex thread without its recap. It is fired
|
|
3524
3602
|
// on the init `system` event (see onEvent) — a sendTurn before the input stream is consumed
|
|
3525
3603
|
// is silently lost (the 2026-07-02 auto-resume bug). Empty string → nothing to carry.
|
|
3526
|
-
|
|
3604
|
+
const carry = resolveCheckpointCarry(carryRecap, carryCheckpoint)
|
|
3605
|
+
entry.pendingRecap = carry.pendingRecap
|
|
3606
|
+
// Native compaction keeps the runtime thread alive, so this checkpoint must not
|
|
3607
|
+
// create a synthetic turn. Persist it separately and prepend it exactly once to
|
|
3608
|
+
// the next real person turn. That makes the post-compact continuation explicit
|
|
3609
|
+
// while preserving the runtime's own compacted context.
|
|
3610
|
+
entry.pendingCheckpoint = carry.pendingCheckpoint
|
|
3527
3611
|
const terminalRolePrompt = [buildTerminalRolePrompt({
|
|
3528
3612
|
spawnedBy: entry.spawnedBy,
|
|
3529
3613
|
spawnDepth: entry.spawnDepth,
|
|
@@ -3617,14 +3701,21 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
3617
3701
|
if (!entry.flowSessionId || entry.flowRole !== 'conductor') return { ok: false, message: 'Not a Flow conductor — nothing to submit.' }
|
|
3618
3702
|
let norm
|
|
3619
3703
|
try { norm = validatePlanForRuntime(normalizePlanOutput(planText || ''), entry.runtime) }
|
|
3620
|
-
catch (e) { return { ok: false, message: `Plan REJECTED: ${e?.message || e}. Re-Write FLOW_PLAN.json with valid JSON
|
|
3704
|
+
catch (e) { return { ok: false, message: `Plan REJECTED: ${e?.message || e}. Re-Write FLOW_PLAN.json with valid JSON: a non-empty acyclic tasks array; each builder/fix/scaffold has observable acceptance, bounded nonGoals, and a baseline gate that says what fails/is absent; each review targets exactly one builder contract.` } }
|
|
3621
3705
|
bcast('flow-plan', { term: id, flowId: entry.flowSessionId, plan: JSON.stringify(norm) }, flowChannel)
|
|
3622
3706
|
process.stderr.write(`\n ${A.mag}◆ flow plan submitted — flow ${String(entry.flowSessionId).slice(0, 8)} (${norm.tasks.length} slice${norm.tasks.length === 1 ? '' : 's'}).${A.rst}\n`)
|
|
3623
|
-
return { ok: true, message: `Plan SUBMITTED (${norm.tasks.length} slice${norm.tasks.length === 1 ? '' : 's'})
|
|
3707
|
+
return { ok: true, message: `Plan SUBMITTED (${norm.tasks.length} slice${norm.tasks.length === 1 ? '' : 's'}) with gate-first contracts. Record safe reversible assumptions in the summary; user steering remains authoritative. The room is showing approval. You are DONE: stop here and wait for approval.` }
|
|
3624
3708
|
},
|
|
3625
3709
|
// FL-B1 (lane side) — a flow lane signals slice-done by Writing FLOW_DONE; the hook
|
|
3626
3710
|
// routes that Write here (mark_flow_done MCP tool is deferred → ToolSearch → hangs).
|
|
3627
|
-
onLaneDone: entry.flowRole === 'builder' ? (async () =>
|
|
3711
|
+
onLaneDone: entry.flowRole === 'builder' ? (async (raw) => {
|
|
3712
|
+
let baselineEvidence = ''
|
|
3713
|
+
try {
|
|
3714
|
+
const value = JSON.parse(String(raw || '').trim())
|
|
3715
|
+
baselineEvidence = typeof value?.baselineEvidence === 'string' ? value.baselineEvidence : ''
|
|
3716
|
+
} catch { /* legacy FLOW_DONE content remains compatible with legacy tasks */ }
|
|
3717
|
+
return { ok: true, message: await markFlowDone({ baselineEvidence }) }
|
|
3718
|
+
}) : null,
|
|
3628
3719
|
// FL-B4 — a review lane records its verdict via FLOW_REVIEW.json (revert-on-fail).
|
|
3629
3720
|
onReviewVerdict: entry.flowRole === 'reviewer' ? onReviewVerdict : null,
|
|
3630
3721
|
mcpServers: { thinkpool: peekServer },
|
|
@@ -3718,12 +3809,12 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
3718
3809
|
id, runtime: entry.runtime, model: entry.model || model, effort: entry.effort,
|
|
3719
3810
|
provider: entry.provider, log: entry.log, commands: entry.commands, mode: entry.mode,
|
|
3720
3811
|
spawnedBy: entry.spawnedBy, spawnDepth: entry.spawnDepth, cascadeRole: entry.cascadeRole, hop: entry.hop, sideParent: entry.sideParent, sideTask: entry.sideTask, pendingSideContexts: entry.pendingSideContexts, pendingWorkerCompletions: entry.pendingWorkerCompletions, workerCompletionsInFlight: entry.workerCompletionsInFlight, sliceType: entry.sliceType, flowSessionId: entry.flowSessionId,
|
|
3721
|
-
flowTaskKey: entry.flowTaskKey, flowRole: entry.flowRole, flowReviewTarget: entry.flowReviewTarget,
|
|
3722
|
-
flowReviewTargets: entry.flowReviewTargets, flowReviewSnapshots: entry.flowReviewSnapshots, flowReviewRound: entry.flowReviewRound,
|
|
3812
|
+
flowTaskKey: entry.flowTaskKey, flowTaskContract: entry.flowTaskContract, flowRole: entry.flowRole, flowReviewTarget: entry.flowReviewTarget,
|
|
3813
|
+
flowReviewTargets: entry.flowReviewTargets, flowReviewSnapshots: entry.flowReviewSnapshots, flowReviewRound: entry.flowReviewRound, flowReviewConclusion: entry.flowReviewConclusion,
|
|
3723
3814
|
dispatchBaseSha: entry.dispatchBaseSha, revertTarget: entry.revertTarget, cwd: entry.cwd,
|
|
3724
3815
|
managedWorktree: entry.managedWorktree, rolePrompt: entry.rolePrompt,
|
|
3725
3816
|
reviewSliceRoots: entry.reviewSliceRoots, openedAt: entry.openedAt,
|
|
3726
|
-
lastUsage: entry.lastUsage, carryRecap,
|
|
3817
|
+
lastUsage: entry.lastUsage, carryRecap, carryCheckpoint: entry.pendingCheckpoint,
|
|
3727
3818
|
})
|
|
3728
3819
|
return sessions.get(id) || null
|
|
3729
3820
|
},
|
|
@@ -3874,6 +3965,7 @@ function openStructured({ id, runtime = 'claude', model, models, effort, resume,
|
|
|
3874
3965
|
// mid-turn) flash a brief "Compacting…" beat so it isn't silent — we only learn of it
|
|
3875
3966
|
// post-hoc, so there's no real duration to animate (Max, 2026-07-02).
|
|
3876
3967
|
if (evt.kind === 'compaction') {
|
|
3968
|
+
armCompactionCheckpoint(entry)
|
|
3877
3969
|
if (entry.compacting) {
|
|
3878
3970
|
// MANUAL /compact: attribute the recap card, then CLEAR the "Compacting…"
|
|
3879
3971
|
// indicator right here — the compact_boundary milestone IS the "done" signal.
|
|
@@ -4245,7 +4337,7 @@ function respawnStructured(id, provider) {
|
|
|
4245
4337
|
// openStructured seed from the TARGET provider's configured model, which is the
|
|
4246
4338
|
// only model this lane was ever asked for. A same-env model change never reaches
|
|
4247
4339
|
// here — that path is an in-place setModel (see provider-switch).
|
|
4248
|
-
const { runtime, log, commands, mode, spawnedBy, spawnDepth, cascadeRole, hop, sideParent, sideTask, pendingSideContexts, pendingWorkerCompletions, workerCompletionsInFlight, sliceType, flowSessionId, flowTaskKey, flowRole, flowReviewTarget, flowReviewTargets, flowReviewSnapshots, flowReviewRound, dispatchBaseSha, revertTarget, cwd, managedWorktree, rolePrompt, reviewSliceRoots, openedAt, lastUsage } = s
|
|
4340
|
+
const { runtime, log, commands, mode, spawnedBy, spawnDepth, cascadeRole, hop, sideParent, sideTask, pendingSideContexts, pendingWorkerCompletions, workerCompletionsInFlight, sliceType, flowSessionId, flowTaskKey, flowTaskContract, flowRole, flowReviewTarget, flowReviewTargets, flowReviewSnapshots, flowReviewRound, flowReviewConclusion, dispatchBaseSha, revertTarget, cwd, managedWorktree, rolePrompt, reviewSliceRoots, openedAt, lastUsage, pendingCheckpoint } = s
|
|
4249
4341
|
// Context-carry (2026-07-08): a provider switch is not an SDK resume — the new backend
|
|
4250
4342
|
// starts blank mid-conversation. Synthesize a plain-text recap from the VISIBLE log NOW
|
|
4251
4343
|
// (before teardown) and hand it to the fresh session as its first turn so the agent
|
|
@@ -4264,7 +4356,7 @@ function respawnStructured(id, provider) {
|
|
|
4264
4356
|
// sessionData() (provider included) synchronously on open, so a bridge restart
|
|
4265
4357
|
// restores the lane on its CURRENT provider, not the original — and its next
|
|
4266
4358
|
// announce carries the new provider badge (additive {id,name} projection).
|
|
4267
|
-
openStructured({ id, runtime, provider, log, commands, mode, spawnedBy, spawnDepth, cascadeRole, hop, sideParent, sideTask, pendingSideContexts, pendingWorkerCompletions, workerCompletionsInFlight, sliceType, flowSessionId, flowTaskKey, flowRole, flowReviewTarget, flowReviewTargets, flowReviewSnapshots, flowReviewRound, dispatchBaseSha, revertTarget, cwd, managedWorktree, rolePrompt, reviewSliceRoots, openedAt, lastUsage, carryRecap })
|
|
4359
|
+
openStructured({ id, runtime, provider, log, commands, mode, spawnedBy, spawnDepth, cascadeRole, hop, sideParent, sideTask, pendingSideContexts, pendingWorkerCompletions, workerCompletionsInFlight, sliceType, flowSessionId, flowTaskKey, flowTaskContract, flowRole, flowReviewTarget, flowReviewTargets, flowReviewSnapshots, flowReviewRound, flowReviewConclusion, dispatchBaseSha, revertTarget, cwd, managedWorktree, rolePrompt, reviewSliceRoots, openedAt, lastUsage, carryRecap, carryCheckpoint: pendingCheckpoint })
|
|
4268
4360
|
return true
|
|
4269
4361
|
}
|
|
4270
4362
|
|
|
@@ -4969,6 +5061,7 @@ channel
|
|
|
4969
5061
|
}
|
|
4970
5062
|
if (/^\/clear\s*$/.test(text)) {
|
|
4971
5063
|
s.pendingRecap = null // /clear means forget context — drop any un-fired carry recap
|
|
5064
|
+
s.pendingCheckpoint = null
|
|
4972
5065
|
s.pendingCodexCompaction = null // explicit clear supersedes a deferred compact
|
|
4973
5066
|
if (s.runtime === 'codex' || s.runtime === 'hermes') s.session.clearContext?.()
|
|
4974
5067
|
else s.session.sendTurn(text)
|
|
@@ -5041,6 +5134,7 @@ channel
|
|
|
5041
5134
|
let sendText = text
|
|
5042
5135
|
const carried = []
|
|
5043
5136
|
if (s.pendingRecap) { carried.push(s.pendingRecap); s.pendingRecap = null }
|
|
5137
|
+
if (s.pendingCheckpoint) { carried.push(s.pendingCheckpoint); s.pendingCheckpoint = null }
|
|
5044
5138
|
if (s.pendingSideContexts?.length) { carried.push(...s.pendingSideContexts); s.pendingSideContexts = [] }
|
|
5045
5139
|
if (carried.length) { sendText = appendCurrentPersonRequest(carried, text); s.flush?.() }
|
|
5046
5140
|
// The browser's `hostPath` is display metadata, not host authority. Rebuild
|
|
@@ -5236,7 +5330,7 @@ channel
|
|
|
5236
5330
|
// FL-M6 — restore the flow context (id/role/cwd) so an in-flight flow survives a
|
|
5237
5331
|
// bridge restart: the conductor keeps its subagent-block + plan interception, and
|
|
5238
5332
|
// lanes keep their worktree cwd + the ability to mark done.
|
|
5239
|
-
openStructured({ id: rec.id, runtime: rec.runtime || 'claude', model: rec.model || undefined, models: rec.models, effort: rec.effort, provider: rec.runtime === 'claude' ? rec.provider || undefined : undefined, resume: resumable ? rec.sessionId : undefined, log: rec.log, commands: rec.commands, mode: rec.mode, spawnedBy: rec.spawnedBy, spawnDepth: rec.spawnDepth, cascadeRole: rec.cascadeRole, hop: rec.hop, sideParent: rec.sideParent, sideTask: rec.sideTask, scheduleRunId: rec.scheduleRunId, scheduleDeadlineAt: rec.scheduleDeadlineAt, scheduleOutcomeRecorded: rec.scheduleOutcomeRecorded, scheduleAdmissionLease: rec.scheduleAdmissionLease, pendingSideContexts: rec.pendingSideContexts, pendingWorkerCompletions: rec.pendingWorkerCompletions, workerCompletionsInFlight: rec.workerCompletionsInFlight, sliceType: rec.sliceType, flowSessionId: rec.flowSessionId, flowTaskKey: rec.flowTaskKey, flowRole: rec.flowRole, flowReviewTarget: rec.flowReviewTarget, flowReviewTargets: rec.flowReviewTargets, flowReviewSnapshots: rec.flowReviewSnapshots, flowReviewRound: rec.flowReviewRound, dispatchBaseSha: rec.dispatchBaseSha, revertTarget: rec.revertTarget, cwd: rec.cwd, managedWorktree: rec.managedWorktree, rolePrompt: rec.rolePrompt, reviewSliceRoots: rec.reviewSliceRoots, openedAt: rec.openedAt, lastUsage: rec.lastUsage, receivedTurnCids: rec.receivedTurnCids, carryRecap: wasInterrupted ? recoveryRecap : rec.carryRecap,
|
|
5333
|
+
openStructured({ id: rec.id, runtime: rec.runtime || 'claude', model: rec.model || undefined, models: rec.models, effort: rec.effort, provider: rec.runtime === 'claude' ? rec.provider || undefined : undefined, resume: resumable ? rec.sessionId : undefined, log: rec.log, commands: rec.commands, mode: rec.mode, spawnedBy: rec.spawnedBy, spawnDepth: rec.spawnDepth, cascadeRole: rec.cascadeRole, hop: rec.hop, sideParent: rec.sideParent, sideTask: rec.sideTask, scheduleRunId: rec.scheduleRunId, scheduleDeadlineAt: rec.scheduleDeadlineAt, scheduleOutcomeRecorded: rec.scheduleOutcomeRecorded, scheduleAdmissionLease: rec.scheduleAdmissionLease, pendingSideContexts: rec.pendingSideContexts, pendingWorkerCompletions: rec.pendingWorkerCompletions, workerCompletionsInFlight: rec.workerCompletionsInFlight, sliceType: rec.sliceType, flowSessionId: rec.flowSessionId, flowTaskKey: rec.flowTaskKey, flowTaskContract: rec.flowTaskContract, flowRole: rec.flowRole, flowReviewTarget: rec.flowReviewTarget, flowReviewTargets: rec.flowReviewTargets, flowReviewSnapshots: rec.flowReviewSnapshots, flowReviewRound: rec.flowReviewRound, flowReviewConclusion: rec.flowReviewConclusion, dispatchBaseSha: rec.dispatchBaseSha, revertTarget: rec.revertTarget, cwd: rec.cwd, managedWorktree: rec.managedWorktree, rolePrompt: rec.rolePrompt, reviewSliceRoots: rec.reviewSliceRoots, openedAt: rec.openedAt, lastUsage: rec.lastUsage, receivedTurnCids: rec.receivedTurnCids, carryRecap: wasInterrupted ? recoveryRecap : rec.carryRecap, carryCheckpoint: rec.carryCheckpoint,
|
|
5240
5334
|
// Lazy-boot restored terminals that were IDLE + not part of a flow: their transcript
|
|
5241
5335
|
// shows immediately; the query boots on first turn. Mid-turn + flow terminals boot now
|
|
5242
5336
|
// (mid-turn needs auto-resume; flow needs its lane live).
|
|
@@ -5459,6 +5553,13 @@ flowChannel
|
|
|
5459
5553
|
// Step 4 — a `review` slice gets the adversarial reviewer prompt (try-to-break),
|
|
5460
5554
|
// every other slice gets the builder prompt.
|
|
5461
5555
|
const isReview = t.slice_type === 'review'
|
|
5556
|
+
try {
|
|
5557
|
+
validateTaskContractSeal(t, { legacy: t.contract == null })
|
|
5558
|
+
} catch (error) {
|
|
5559
|
+
process.stderr.write(`\n ${A.yel}◆ flow dispatch held — ${t.task_key || 'task'} failed its sealed acceptance contract: ${error?.message || error}${A.rst}\n`)
|
|
5560
|
+
bcast('flow-dispatch-held', { term: 'flow', flowId: payload.flowId, taskKey: t.task_key || null, reason: 'sealed acceptance contract mismatch' }, flowChannel)
|
|
5561
|
+
continue
|
|
5562
|
+
}
|
|
5462
5563
|
if (!validReviewTargetShape(t, flowRuntime)) {
|
|
5463
5564
|
process.stderr.write(`\n ${A.yel}◆ flow dispatch held — ${flowRuntime === 'hermes' ? 'Hermes' : 'Codex'} review ${t.task_key} must target exactly one dependency.${A.rst}\n`)
|
|
5464
5565
|
continue
|
|
@@ -5508,10 +5609,11 @@ flowChannel
|
|
|
5508
5609
|
? (isReview ? FLOW_CODEX_REVIEWER_PROMPT : FLOW_CODEX_LANE_PROMPT)
|
|
5509
5610
|
: (isReview ? FLOW_REVIEWER_PROMPT : FLOW_LANE_PROMPT)
|
|
5510
5611
|
const laneRolePrompt = buildLanePrompt({ base: laneBase })
|
|
5612
|
+
const sealedReviewAcceptance = t.contract?.acceptancePack?.task?.acceptance
|
|
5511
5613
|
// Model tiers (2026-07-03-flow-lane-model-tiers): pick the lane's brain by slice_type
|
|
5512
5614
|
// (scaffold→sonnet, feature/fix/review→opus; env can blanket-override or `inherit` to
|
|
5513
5615
|
// restore today's exact behavior). undefined → no model key passed (openStructured default).
|
|
5514
|
-
openStructured({ id: laneId, runtime: flowRuntime, cwd: dir, model: flowLaneModelFor({ sliceType: t.slice_type, runtime: flowRuntime, catalog: flowCatalog }), mode: structuredModeForSlice(flowRuntime, { mode: 'bypassPermissions', flowRole: isReview ? 'reviewer' : 'builder' }), rolePrompt: laneRolePrompt, flowSessionId: payload.flowId, flowTaskKey: t.task_key, flowRole: isReview ? 'reviewer' : 'builder', flowReviewTargets: isReview ? (t.deps || []) : [], flowReviewSnapshots, dispatchBaseSha, revertTarget: redispatch?.revertTarget || null, spawnedBy: `flow:${payload.flowId}`, resume: redispatch?.resumeSessionId || undefined, reviewSliceRoots })
|
|
5616
|
+
openStructured({ id: laneId, runtime: flowRuntime, cwd: dir, model: flowLaneModelFor({ sliceType: t.slice_type, runtime: flowRuntime, catalog: flowCatalog }), mode: structuredModeForSlice(flowRuntime, { mode: 'bypassPermissions', flowRole: isReview ? 'reviewer' : 'builder' }), rolePrompt: laneRolePrompt, flowSessionId: payload.flowId, flowTaskKey: t.task_key, flowTaskContract: t.contract || null, flowRole: isReview ? 'reviewer' : 'builder', flowReviewTargets: isReview ? (t.deps || []) : [], flowReviewSnapshots, dispatchBaseSha, revertTarget: redispatch?.revertTarget || null, spawnedBy: `flow:${payload.flowId}`, resume: redispatch?.resumeSessionId || undefined, reviewSliceRoots })
|
|
5515
5617
|
const le = sessions.get(laneId)
|
|
5516
5618
|
if (le) {
|
|
5517
5619
|
// S4 — stamp the surviving revert target on the resumed lane so a later reviewer still
|
|
@@ -5534,7 +5636,10 @@ flowChannel
|
|
|
5534
5636
|
? `Reviewed slice worktree(s) — check out + RUN each yourself:\n` +
|
|
5535
5637
|
t.deps.map((dep) => { const ws = worktreeSpec({ flowId: payload.flowId, taskKey: dep }); return ` - ${dep}: dir ${ws.dir} (branch ${ws.branch})` }).join('\n') + '\n'
|
|
5536
5638
|
: '') +
|
|
5537
|
-
`ACCEPTANCE to INDEPENDENTLY verify: ${t.acceptance || t.title}\n` +
|
|
5639
|
+
`ACCEPTANCE to INDEPENDENTLY verify: ${sealedReviewAcceptance || t.acceptance || t.title}\n` +
|
|
5640
|
+
`INHERITED NON-GOALS (must remain untouched): ${(t.contract?.nonGoals || []).join(' | ') || '(legacy task: none recorded)'}\n` +
|
|
5641
|
+
`INHERITED BASELINE GATE: ${t.contract?.baseline?.gate || '(legacy task: none recorded)'}\n` +
|
|
5642
|
+
`BUILDER BASELINE EVIDENCE (verify; never invent another baseline): ${t.contract?.baseline?.evidence || '(missing — reject unless this is a legacy task)'}\n` +
|
|
5538
5643
|
`\nProject (context): ${payload.flowPrompt || ''}\n\n` +
|
|
5539
5644
|
(flowRuntime === 'codex' || flowRuntime === 'hermes'
|
|
5540
5645
|
? `Review it without mutating the builder worktree. For write-producing install/build/test commands, copy its source into scratch under your own current worktree first. Submit the structured verdict with the ThinkPool submit_flow_review MCP tool; never write FLOW_REVIEW.json or call mark_flow_done.`
|
|
@@ -5543,6 +5648,9 @@ flowChannel
|
|
|
5543
5648
|
`TITLE: ${t.title}\n` +
|
|
5544
5649
|
`SCOPE (files you OWN — edit ONLY these): ${t.scope || '(none stated)'}\n` +
|
|
5545
5650
|
`ACCEPTANCE (done = this runs + proves it): ${t.acceptance || '(meet the title)'}\n` +
|
|
5651
|
+
`NON-GOALS (must remain untouched): ${(t.contract?.nonGoals || []).join(' | ') || '(legacy task: none recorded)'}\n` +
|
|
5652
|
+
`BASELINE GATE (before edits, observe this real failure/absence): ${t.contract?.baseline?.gate || '(legacy task: none recorded)'}\n` +
|
|
5653
|
+
`BASELINE EVIDENCE: run/observe the pre-edit gate, then retain one real bounded command/behavior receipt for mark_flow_done; never fabricate a RED sentence.\n` +
|
|
5546
5654
|
(t.deps && t.deps.length ? `DEPENDS ON (already built): ${t.deps.join(', ')}\n` : '') +
|
|
5547
5655
|
// S1 (context-offload) — inject the BOUNDED cross-wave context (loadDigest
|
|
5548
5656
|
// enforces CEILING) instead of accumulating full lane transcripts. Filter to
|
package/claude-session.mjs
CHANGED
|
@@ -594,7 +594,7 @@ export function startClaudeSession({ cwd, model, effort: initialEffort = 'high',
|
|
|
594
594
|
// that Write to the done logic and feed the result back; the file is never written.
|
|
595
595
|
if (onLaneDone && (toolName === 'Write' || toolName === 'Edit') && /(?:^|[/\\])FLOW_DONE(\.[a-zA-Z0-9]+)?$/.test(toolInput?.file_path || '')) {
|
|
596
596
|
let res = { ok: false, message: 'done signal failed' }
|
|
597
|
-
try { res = (await onLaneDone()) || res } catch (e) { res = { ok: false, message: `done signal failed: ${e?.message || e}` } }
|
|
597
|
+
try { res = (await onLaneDone(toolInput?.content ?? toolInput?.new_string ?? '')) || res } catch (e) { res = { ok: false, message: `done signal failed: ${e?.message || e}` } }
|
|
598
598
|
return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: res.message } }
|
|
599
599
|
}
|
|
600
600
|
// S5 (slice 1b) — REVIEW-LANE WRITE-BLOCK. A review lane is ADVERSARIAL: it reads +
|
package/code-event-contract.mjs
CHANGED
package/event-id.mjs
CHANGED
|
@@ -546,4 +546,4 @@ export function codexUsageReportLine (sessionModel, snapshot) {
|
|
|
546
546
|
* (`from './event-id.mjs'`) working untouched, including bridge/recap.test.mjs.
|
|
547
547
|
*
|
|
548
548
|
* recap.mjs must stay in bridge/package.json `files` — event-id.mjs imports it. */
|
|
549
|
-
export { CURRENT_PERSON_REQUEST_MARKER, RECAP_CAP, appendCurrentPersonRequest, buildRecapFromLog } from './recap.mjs'
|
|
549
|
+
export { CHECKPOINT_CAP, CURRENT_PERSON_REQUEST_MARKER, RECAP_CAP, appendCurrentPersonRequest, buildCheckpointFromLog, buildRecapFromLog, recapHasCheckpoint, resolveCheckpointCarry } from './recap.mjs'
|