brainclaw 1.28.1 → 1.28.2
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/brainclaw-vscode.vsix +0 -0
- package/dist/commands/code-map.js +2 -0
- package/dist/commands/doctor.js +1 -0
- package/dist/commands/harvest.js +32 -43
- package/dist/commands/loops-handlers.js +66 -3
- package/dist/commands/mcp-write-coordination.js +413 -137
- package/dist/commands/mcp.js +32 -4
- package/dist/core/agentrun-reconciler.js +138 -4
- package/dist/core/claims.js +4 -1
- package/dist/core/code-map/backend.js +8 -0
- package/dist/core/execution-adapters.js +15 -7
- package/dist/core/hygiene-policy.js +2 -1
- package/dist/core/loop-turn-dispatch.js +18 -1
- package/dist/core/loops/attempt-authority.js +22 -4
- package/dist/core/loops/attempt-generations.js +17 -4
- package/dist/core/loops/attempt-reservation.js +14 -1
- package/dist/core/loops/attempt-takeover.js +173 -76
- package/dist/core/loops/reconcile-turn.js +224 -26
- package/dist/core/loops/result-reducers.js +8 -8
- package/dist/core/loops/turn-execution.js +38 -19
- package/dist/core/loops/types.js +3 -0
- package/dist/core/loops/verbs.js +1 -1
- package/dist/facts.js +9 -9
- package/dist/facts.json +8 -8
- package/docs/code-map.md +10 -0
- package/docs/concepts/loop-engine.md +5 -0
- package/package.json +1 -1
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import { ensureAgentRunProjection, loadAgentRun, transitionAgentRun } from '../agentruns.js';
|
|
4
|
-
import { loadAssignment } from '../assignments.js';
|
|
4
|
+
import { convergeAssignmentToTerminal, loadAssignment, transitionAssignment } from '../assignments.js';
|
|
5
5
|
import { createRuntimeEvent } from '../events.js';
|
|
6
6
|
import { nowISO } from '../ids.js';
|
|
7
7
|
import { prepareAttemptTakeoverV2 } from './attempt-authority.js';
|
|
8
|
-
import { generationDigest } from './attempt-generations.js';
|
|
8
|
+
import { generationDigest, resolveTurnGenerationChain } from './attempt-generations.js';
|
|
9
9
|
import { commitViaIntent } from './commit-intent.js';
|
|
10
10
|
import { evidenceDigest } from './evidence.js';
|
|
11
11
|
import { withLoopLock } from './lock.js';
|
|
12
12
|
import { getReservation } from './attempt-reservation.js';
|
|
13
13
|
import { getLoop, listLoopEvents } from './store.js';
|
|
14
|
+
/**
|
|
15
|
+
* Signals that the immutable successor generation already won even though a
|
|
16
|
+
* replayable projection failed afterward. Callers must never roll claims back
|
|
17
|
+
* on this error; replaying the same takeover input repairs the projections.
|
|
18
|
+
*/
|
|
19
|
+
export class AttemptTakeoverCommittedError extends Error {
|
|
20
|
+
turn_id;
|
|
21
|
+
assignment_id;
|
|
22
|
+
run_id;
|
|
23
|
+
attempt_epoch;
|
|
24
|
+
constructor(takeover, cause) {
|
|
25
|
+
super(`takeover generation ${takeover.next_generation.attempt_epoch} committed; projection repair required: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
26
|
+
this.name = 'AttemptTakeoverCommittedError';
|
|
27
|
+
this.turn_id = takeover.next_generation.turn_id;
|
|
28
|
+
this.assignment_id = takeover.next_generation.assignment_id;
|
|
29
|
+
this.run_id = takeover.next_generation.run_id;
|
|
30
|
+
this.attempt_epoch = takeover.next_generation.attempt_epoch;
|
|
31
|
+
this.cause = cause;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
14
34
|
/**
|
|
15
35
|
* Operator/engine takeover transaction.
|
|
16
36
|
*
|
|
@@ -25,66 +45,137 @@ export function takeoverLoopAttempt(input) {
|
|
|
25
45
|
throw new Error(`takeover workspace must already exist as an isolated directory: ${input.next_workspace_path}`);
|
|
26
46
|
}
|
|
27
47
|
const authorityActor = input.actor_id ?? input.actor;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
48
|
+
let committed;
|
|
49
|
+
let transaction;
|
|
50
|
+
try {
|
|
51
|
+
transaction = withLoopLock({
|
|
52
|
+
cwd: input.cwd,
|
|
53
|
+
intent: 'attempt-takeover',
|
|
54
|
+
agentId: authorityActor,
|
|
55
|
+
scope: { kind: 'loop', loopId: input.loop_id },
|
|
56
|
+
work: () => {
|
|
57
|
+
const loop = getLoop(input.loop_id, input.cwd);
|
|
58
|
+
if (!loop || loop.status !== 'open')
|
|
59
|
+
throw new Error(`loop ${input.loop_id} is not open`);
|
|
60
|
+
if (loop.created_by !== authorityActor) {
|
|
61
|
+
throw new Error(`attempt takeover requires loop coordinator ${loop.created_by}; caller is ${authorityActor}`);
|
|
62
|
+
}
|
|
63
|
+
const slot = loop.slots.find((candidate) => candidate.slot_id === input.slot_id);
|
|
64
|
+
if (!slot)
|
|
65
|
+
throw new Error(`slot ${input.slot_id} not found in loop ${input.loop_id}`);
|
|
66
|
+
const reservation = getReservation(input.turn_id, input.cwd);
|
|
67
|
+
if (!reservation || reservation.loop_id !== loop.id || reservation.slot_id !== slot.slot_id) {
|
|
68
|
+
throw new Error(`turn ${input.turn_id} does not own ${loop.id}/${slot.slot_id}`);
|
|
69
|
+
}
|
|
70
|
+
const activeGeneration = resolveTurnGenerationChain(input.cwd, input.turn_id)?.latest_generation;
|
|
71
|
+
const activeAssignmentId = activeGeneration?.assignment_id ?? reservation.child_ids.assignment_id;
|
|
72
|
+
if (slot.current_turn_id !== input.turn_id || slot.assignment_id !== activeAssignmentId) {
|
|
73
|
+
throw new Error(`slot ${slot.slot_id} is no longer bound to turn ${input.turn_id}`);
|
|
74
|
+
}
|
|
75
|
+
const takeover = prepareAttemptTakeoverV2({ ...input, actor: authorityActor });
|
|
76
|
+
committed = takeover;
|
|
77
|
+
input.on_stage?.('authority_committed');
|
|
78
|
+
const duplicate = listLoopEvents(loop.id, input.cwd).some((event) => event.kind === 'attempt_generation_changed'
|
|
79
|
+
&& event.turn_id === input.turn_id
|
|
80
|
+
&& event.to_epoch === takeover.next_generation.attempt_epoch
|
|
81
|
+
&& event.to_run_id === takeover.next_generation.run_id);
|
|
82
|
+
if (duplicate)
|
|
83
|
+
return { loop, takeover, reservation };
|
|
84
|
+
const now = nowISO();
|
|
85
|
+
const mutationId = crypto.randomUUID();
|
|
86
|
+
const executor = takeover.next_generation.executor ?? {
|
|
87
|
+
agent: reservation.agent,
|
|
88
|
+
agent_id: reservation.agent_id,
|
|
89
|
+
claim_id: reservation.claim_id,
|
|
90
|
+
capability_snapshot: reservation.capability_snapshot,
|
|
91
|
+
};
|
|
92
|
+
const event = {
|
|
93
|
+
event_id: crypto.randomUUID(),
|
|
94
|
+
loop_id: loop.id,
|
|
95
|
+
seq: loop.version + 1,
|
|
96
|
+
at: now,
|
|
97
|
+
by: authorityActor,
|
|
98
|
+
mutation_id: mutationId,
|
|
99
|
+
kind: 'attempt_generation_changed',
|
|
100
|
+
slot_id: slot.slot_id,
|
|
101
|
+
turn_id: input.turn_id,
|
|
102
|
+
assignment_id: takeover.next_generation.assignment_id,
|
|
103
|
+
claim_id: executor.claim_id,
|
|
104
|
+
agent: executor.agent,
|
|
105
|
+
agent_id: executor.agent_id,
|
|
106
|
+
from_epoch: takeover.previous_generation.attempt_epoch,
|
|
107
|
+
to_epoch: takeover.next_generation.attempt_epoch,
|
|
108
|
+
from_run_id: takeover.previous_generation.run_id,
|
|
109
|
+
to_run_id: takeover.next_generation.run_id,
|
|
110
|
+
close_digest: evidenceDigest(takeover.close_cell),
|
|
111
|
+
cause: input.cause,
|
|
112
|
+
};
|
|
113
|
+
const next = {
|
|
114
|
+
...loop,
|
|
115
|
+
version: loop.version + 1,
|
|
116
|
+
mutation_id: mutationId,
|
|
117
|
+
slots: loop.slots.map((candidate) => candidate.slot_id === slot.slot_id
|
|
118
|
+
? {
|
|
119
|
+
...candidate,
|
|
120
|
+
agent: executor.agent,
|
|
121
|
+
agent_id: executor.agent_id,
|
|
122
|
+
assignment_id: takeover.next_generation.assignment_id,
|
|
123
|
+
claim_id: executor.claim_id,
|
|
124
|
+
current_turn_id: input.turn_id,
|
|
125
|
+
status: 'assigned',
|
|
126
|
+
}
|
|
127
|
+
: candidate),
|
|
128
|
+
updated_at: now,
|
|
129
|
+
};
|
|
130
|
+
commitViaIntent({ loop_id: loop.id, base_version: loop.version, events: [event], thread_snapshot: next }, input.cwd);
|
|
131
|
+
input.on_stage?.('loop_committed');
|
|
132
|
+
return { loop: next, takeover, reservation };
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (committed)
|
|
138
|
+
throw new AttemptTakeoverCommittedError(committed, error);
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
const { takeover, reservation } = transaction;
|
|
142
|
+
const executor = takeover.next_generation.executor ?? {
|
|
143
|
+
agent: reservation.agent,
|
|
144
|
+
agent_id: reservation.agent_id,
|
|
145
|
+
claim_id: reservation.claim_id,
|
|
146
|
+
capability_snapshot: reservation.capability_snapshot,
|
|
147
|
+
};
|
|
148
|
+
if (takeover.previous_generation.assignment_id !== takeover.next_generation.assignment_id) {
|
|
149
|
+
try {
|
|
150
|
+
const previousAssignmentId = takeover.previous_generation.assignment_id;
|
|
151
|
+
const previousAssignment = loadAssignment(previousAssignmentId, input.cwd);
|
|
152
|
+
const predecessorTerminal = input.predecessor_assignment_terminal ?? 'cancelled';
|
|
153
|
+
const statusReason = `fenced by ${input.mode ?? 'takeover'} to epoch ${takeover.next_generation.attempt_epoch}`;
|
|
154
|
+
// System convergence intentionally covers only file-worker states
|
|
155
|
+
// (offered/accepted/started). A takeover can win before launch while the
|
|
156
|
+
// predecessor is still created, or during retry setup; both states have
|
|
157
|
+
// a legal direct cancellation edge and must be terminal before the stable
|
|
158
|
+
// claim is rebound to the successor generation.
|
|
159
|
+
if (predecessorTerminal === 'rerouted' && previousAssignment && previousAssignment.status !== 'rerouted') {
|
|
160
|
+
transitionAssignment(previousAssignmentId, 'rerouted', {
|
|
161
|
+
actor: input.actor,
|
|
162
|
+
syncAgentRun: false,
|
|
163
|
+
status_reason: statusReason,
|
|
164
|
+
}, input.cwd);
|
|
39
165
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
166
|
+
else if (previousAssignment?.status === 'created' || previousAssignment?.status === 'retrying') {
|
|
167
|
+
transitionAssignment(previousAssignmentId, 'cancelled', {
|
|
168
|
+
actor: input.actor,
|
|
169
|
+
syncAgentRun: false,
|
|
170
|
+
status_reason: statusReason,
|
|
171
|
+
}, input.cwd);
|
|
46
172
|
}
|
|
47
|
-
|
|
48
|
-
|
|
173
|
+
else {
|
|
174
|
+
convergeAssignmentToTerminal(previousAssignmentId, 'cancelled', statusReason, input.cwd);
|
|
49
175
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
&& event.to_epoch === takeover.next_generation.attempt_epoch
|
|
54
|
-
&& event.to_run_id === takeover.next_generation.run_id);
|
|
55
|
-
if (duplicate)
|
|
56
|
-
return { loop, takeover, reservation };
|
|
57
|
-
const now = nowISO();
|
|
58
|
-
const mutationId = crypto.randomUUID();
|
|
59
|
-
const event = {
|
|
60
|
-
event_id: crypto.randomUUID(),
|
|
61
|
-
loop_id: loop.id,
|
|
62
|
-
seq: loop.version + 1,
|
|
63
|
-
at: now,
|
|
64
|
-
by: authorityActor,
|
|
65
|
-
mutation_id: mutationId,
|
|
66
|
-
kind: 'attempt_generation_changed',
|
|
67
|
-
slot_id: slot.slot_id,
|
|
68
|
-
turn_id: input.turn_id,
|
|
69
|
-
assignment_id: takeover.next_generation.assignment_id,
|
|
70
|
-
from_epoch: takeover.previous_generation.attempt_epoch,
|
|
71
|
-
to_epoch: takeover.next_generation.attempt_epoch,
|
|
72
|
-
from_run_id: takeover.previous_generation.run_id,
|
|
73
|
-
to_run_id: takeover.next_generation.run_id,
|
|
74
|
-
close_digest: evidenceDigest(takeover.close_cell),
|
|
75
|
-
cause: input.cause,
|
|
76
|
-
};
|
|
77
|
-
const next = {
|
|
78
|
-
...loop,
|
|
79
|
-
version: loop.version + 1,
|
|
80
|
-
mutation_id: mutationId,
|
|
81
|
-
updated_at: now,
|
|
82
|
-
};
|
|
83
|
-
commitViaIntent({ loop_id: loop.id, base_version: loop.version, events: [event], thread_snapshot: next }, input.cwd);
|
|
84
|
-
return { loop: next, takeover, reservation };
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
const { takeover, reservation } = transaction;
|
|
176
|
+
}
|
|
177
|
+
catch { /* immutable close cell already fences the old assignment */ }
|
|
178
|
+
}
|
|
88
179
|
const assignment = loadAssignment(takeover.next_generation.assignment_id, input.cwd);
|
|
89
180
|
const previousRun = loadAgentRun(takeover.previous_generation.run_id, input.cwd);
|
|
90
181
|
if (previousRun && !['completed', 'failed', 'cancelled', 'timed_out', 'interrupted'].includes(previousRun.status)) {
|
|
@@ -97,23 +188,29 @@ export function takeoverLoopAttempt(input) {
|
|
|
97
188
|
}
|
|
98
189
|
catch { /* immutable close cell already fences the old run */ }
|
|
99
190
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
191
|
+
try {
|
|
192
|
+
ensureAgentRunProjection({
|
|
193
|
+
id: takeover.next_generation.run_id,
|
|
194
|
+
short_label: takeover.next_generation.run_id,
|
|
195
|
+
assignment_id: takeover.next_generation.assignment_id,
|
|
196
|
+
claim_id: executor.claim_id,
|
|
197
|
+
attempt_index: takeover.next_generation.attempt_epoch + 1,
|
|
198
|
+
agent: executor.agent,
|
|
199
|
+
agent_id: executor.agent_id,
|
|
200
|
+
transport: 'cli_spawn',
|
|
201
|
+
status: 'created',
|
|
202
|
+
scope: assignment?.scope ?? reservation.execution_contract?.workspace_policy.scope ?? reservation.cwd,
|
|
203
|
+
description: assignment?.description ?? `Attempt generation ${takeover.next_generation.attempt_epoch} for ${input.turn_id}`,
|
|
204
|
+
worktree_path: takeover.next_generation.workspace_path,
|
|
205
|
+
execution_contract_ref: takeover.execution_contract_ref,
|
|
206
|
+
capability_snapshot: executor.capability_snapshot,
|
|
207
|
+
tags: ['turn-owned', 'loop', 'attempt-takeover', `attempt-generation:${takeover.next_generation.attempt_epoch}`],
|
|
208
|
+
}, input.cwd);
|
|
209
|
+
input.on_stage?.('run_projected');
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
throw new AttemptTakeoverCommittedError(takeover, error);
|
|
213
|
+
}
|
|
117
214
|
try {
|
|
118
215
|
createRuntimeEvent({
|
|
119
216
|
agent: input.actor,
|