loopforge 1.15.1 → 2.0.1
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/README.md +172 -95
- package/dist/backends/interface.d.ts +5 -10
- package/dist/backends/interface.d.ts.map +1 -1
- package/dist/backends/interface.js +3 -4
- package/dist/backends/interface.js.map +1 -1
- package/dist/canonical-state.d.ts +53 -0
- package/dist/canonical-state.d.ts.map +1 -0
- package/dist/canonical-state.js +161 -0
- package/dist/canonical-state.js.map +1 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +227 -0
- package/dist/cli.js.map +1 -0
- package/dist/enforcement-gate.d.ts.map +1 -1
- package/dist/enforcement-gate.js +28 -10
- package/dist/enforcement-gate.js.map +1 -1
- package/dist/engine.d.ts +9 -28
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +135 -361
- package/dist/engine.js.map +1 -1
- package/dist/evidence-provider.d.ts +146 -0
- package/dist/evidence-provider.d.ts.map +1 -0
- package/dist/evidence-provider.js +512 -0
- package/dist/evidence-provider.js.map +1 -0
- package/dist/index.d.ts +19 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -12
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +15 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +49 -0
- package/dist/init.js.map +1 -0
- package/dist/interop.d.ts +35 -0
- package/dist/interop.d.ts.map +1 -0
- package/dist/interop.js +34 -0
- package/dist/interop.js.map +1 -0
- package/dist/loop-compiler.d.ts +14 -64
- package/dist/loop-compiler.d.ts.map +1 -1
- package/dist/loop-compiler.js +305 -2206
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-extras-parser.d.ts +82 -0
- package/dist/loop-extras-parser.d.ts.map +1 -0
- package/dist/loop-extras-parser.js +217 -0
- package/dist/loop-extras-parser.js.map +1 -0
- package/dist/loop-store.d.ts +104 -0
- package/dist/loop-store.d.ts.map +1 -0
- package/dist/loop-store.js +421 -0
- package/dist/loop-store.js.map +1 -0
- package/dist/mcp/server.d.ts +9 -6
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +109 -73
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/session.d.ts +102 -21
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +795 -400
- package/dist/mcp/session.js.map +1 -1
- package/dist/mcp/tools.d.ts +18 -268
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +226 -12
- package/dist/mcp/tools.js.map +1 -1
- package/dist/observability.d.ts +36 -16
- package/dist/observability.d.ts.map +1 -1
- package/dist/observability.js +82 -23
- package/dist/observability.js.map +1 -1
- package/dist/policy-metrics.d.ts +46 -0
- package/dist/policy-metrics.d.ts.map +1 -0
- package/dist/policy-metrics.js +146 -0
- package/dist/policy-metrics.js.map +1 -0
- package/dist/policy.d.ts +52 -147
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +118 -191
- package/dist/policy.js.map +1 -1
- package/dist/prompt-assembler.d.ts +30 -0
- package/dist/prompt-assembler.d.ts.map +1 -0
- package/dist/prompt-assembler.js +278 -0
- package/dist/prompt-assembler.js.map +1 -0
- package/dist/prompt-policy.d.ts +29 -0
- package/dist/prompt-policy.d.ts.map +1 -0
- package/dist/prompt-policy.js +51 -0
- package/dist/prompt-policy.js.map +1 -0
- package/dist/protocol.d.ts +75 -152
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +11 -53
- package/dist/protocol.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +1 -27
- package/dist/replay.js.map +1 -1
- package/dist/round-coordinator.d.ts +90 -0
- package/dist/round-coordinator.d.ts.map +1 -0
- package/dist/round-coordinator.js +186 -0
- package/dist/round-coordinator.js.map +1 -0
- package/dist/round-driver.d.ts +53 -0
- package/dist/round-driver.d.ts.map +1 -0
- package/dist/round-driver.js +101 -0
- package/dist/round-driver.js.map +1 -0
- package/dist/round-transaction.d.ts +70 -0
- package/dist/round-transaction.d.ts.map +1 -0
- package/dist/round-transaction.js +290 -0
- package/dist/round-transaction.js.map +1 -0
- package/dist/self-eval.d.ts +26 -0
- package/dist/self-eval.d.ts.map +1 -0
- package/dist/self-eval.js +154 -0
- package/dist/self-eval.js.map +1 -0
- package/dist/storage.d.ts +39 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +193 -0
- package/dist/storage.js.map +1 -0
- package/dist/verification-gate.d.ts +31 -7
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +250 -7
- package/dist/verification-gate.js.map +1 -1
- package/loop_policy.json +19 -53
- package/package.json +13 -10
- package/skills/perception/SKILL.md +80 -267
- package/dist/backends/fs.d.ts +0 -36
- package/dist/backends/fs.d.ts.map +0 -1
- package/dist/backends/fs.js +0 -362
- package/dist/backends/fs.js.map +0 -1
- package/dist/builder.d.ts +0 -19
- package/dist/builder.d.ts.map +0 -1
- package/dist/builder.js +0 -215
- package/dist/builder.js.map +0 -1
- package/dist/mcp-server.d.ts +0 -12
- package/dist/mcp-server.d.ts.map +0 -1
- package/dist/mcp-server.js +0 -14
- package/dist/mcp-server.js.map +0 -1
- package/dist/memory-bridge.d.ts +0 -47
- package/dist/memory-bridge.d.ts.map +0 -1
- package/dist/memory-bridge.js +0 -290
- package/dist/memory-bridge.js.map +0 -1
- package/dist/runtime.d.ts +0 -80
- package/dist/runtime.d.ts.map +0 -1
- package/dist/runtime.js +0 -699
- package/dist/runtime.js.map +0 -1
- package/dist/tests/_helpers.d.ts +0 -25
- package/dist/tests/_helpers.d.ts.map +0 -1
- package/dist/tests/_helpers.js +0 -47
- package/dist/tests/_helpers.js.map +0 -1
- package/dist/tests/builder.test.d.ts +0 -2
- package/dist/tests/builder.test.d.ts.map +0 -1
- package/dist/tests/builder.test.js +0 -112
- package/dist/tests/builder.test.js.map +0 -1
- package/dist/tests/enforcement-gate.test.d.ts +0 -2
- package/dist/tests/enforcement-gate.test.d.ts.map +0 -1
- package/dist/tests/enforcement-gate.test.js +0 -328
- package/dist/tests/enforcement-gate.test.js.map +0 -1
- package/dist/tests/engine.test.d.ts +0 -2
- package/dist/tests/engine.test.d.ts.map +0 -1
- package/dist/tests/engine.test.js +0 -375
- package/dist/tests/engine.test.js.map +0 -1
- package/dist/tests/generate-schema.test.d.ts +0 -8
- package/dist/tests/generate-schema.test.d.ts.map +0 -1
- package/dist/tests/generate-schema.test.js +0 -199
- package/dist/tests/generate-schema.test.js.map +0 -1
- package/dist/tests/loop-compiler.test.d.ts +0 -2
- package/dist/tests/loop-compiler.test.d.ts.map +0 -1
- package/dist/tests/loop-compiler.test.js +0 -1059
- package/dist/tests/loop-compiler.test.js.map +0 -1
- package/dist/tests/mcp.test.d.ts +0 -7
- package/dist/tests/mcp.test.d.ts.map +0 -1
- package/dist/tests/mcp.test.js +0 -505
- package/dist/tests/mcp.test.js.map +0 -1
- package/dist/tests/memory-bridge.test.d.ts +0 -8
- package/dist/tests/memory-bridge.test.d.ts.map +0 -1
- package/dist/tests/memory-bridge.test.js +0 -383
- package/dist/tests/memory-bridge.test.js.map +0 -1
- package/dist/tests/policy.test.d.ts +0 -2
- package/dist/tests/policy.test.d.ts.map +0 -1
- package/dist/tests/policy.test.js +0 -49
- package/dist/tests/policy.test.js.map +0 -1
- package/dist/tests/protocol.test.d.ts +0 -2
- package/dist/tests/protocol.test.d.ts.map +0 -1
- package/dist/tests/protocol.test.js +0 -73
- package/dist/tests/protocol.test.js.map +0 -1
- package/dist/tests/replay.test.d.ts +0 -2
- package/dist/tests/replay.test.d.ts.map +0 -1
- package/dist/tests/replay.test.js +0 -222
- package/dist/tests/replay.test.js.map +0 -1
- package/dist/tests/runtime.test.d.ts +0 -7
- package/dist/tests/runtime.test.d.ts.map +0 -1
- package/dist/tests/runtime.test.js +0 -337
- package/dist/tests/runtime.test.js.map +0 -1
- package/dist/tests/verification-gate.test.d.ts +0 -2
- package/dist/tests/verification-gate.test.d.ts.map +0 -1
- package/dist/tests/verification-gate.test.js +0 -407
- package/dist/tests/verification-gate.test.js.map +0 -1
- package/skills/prompt-techniques/SKILL.md +0 -52
- package/skills/prompt-techniques/references/chain-of-thought.md +0 -207
- package/skills/prompt-techniques/references/few-shot.md +0 -174
- package/skills/prompt-techniques/references/least-to-most.md +0 -182
- package/skills/prompt-techniques/references/step-back.md +0 -160
- package/skills/prompt-techniques/references/tree-of-thought.md +0 -191
- package/skills/prompt-techniques/references/zero-shot.md +0 -97
package/dist/mcp/session.js
CHANGED
|
@@ -5,24 +5,24 @@
|
|
|
5
5
|
* the advance() cycle: extract → feedback → check stop → compile next.
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import { writeFileSync, mkdirSync } from "node:fs";
|
|
9
|
-
import { join } from "node:path";
|
|
10
8
|
import { LoopForgeEngine, extractSelfEvaluation, heuristicSelfEvaluation } from "../engine.js";
|
|
11
|
-
import { checkLoopHealth
|
|
12
|
-
import { getPolicy
|
|
13
|
-
import { Mode, makeLoopCompileRequest
|
|
9
|
+
import { checkLoopHealth } from "../loop-compiler.js";
|
|
10
|
+
import { getPolicy } from "../policy.js";
|
|
11
|
+
import { Mode, makeLoopCompileRequest } from "../protocol.js";
|
|
14
12
|
import { ReplayBackend } from "../replay.js";
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
13
|
+
import { FileLoopStore, LoopStoreBackend, VaultBackendLoopStore } from "../loop-store.js";
|
|
14
|
+
import { EvidenceCollector } from "../evidence-provider.js";
|
|
15
|
+
import { parseRoundTransactionSnapshot, prepareRoundTransaction, } from "../round-transaction.js";
|
|
16
|
+
import { RoundDriver } from "../round-driver.js";
|
|
18
17
|
import { logEvent } from "../observability.js";
|
|
18
|
+
import { policyMetrics } from "../policy-metrics.js";
|
|
19
|
+
import { SessionLeaseConflictError, VaultSessionStateStore, } from "../storage.js";
|
|
20
|
+
import { createCognitiveCheckpoint } from "../interop.js";
|
|
19
21
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
20
|
-
function buildLoopRequest(session, lastEval,
|
|
21
|
-
verificationFlags) {
|
|
22
|
+
function buildLoopRequest(session, lastEval, verificationFlags) {
|
|
22
23
|
const req = {
|
|
23
24
|
task: session.task,
|
|
24
25
|
mode: Mode.LOOP_COMPILE,
|
|
25
|
-
vault_config: makeVaultConfig(),
|
|
26
26
|
feedback: null,
|
|
27
27
|
skill_name: null,
|
|
28
28
|
task_id: null,
|
|
@@ -59,19 +59,12 @@ verificationFlags) {
|
|
|
59
59
|
// v1.10: Checkpoint boundary
|
|
60
60
|
compression_checkpoint: lastEval.compression_checkpoint ?? false,
|
|
61
61
|
checkpoint_label: lastEval.checkpoint_label ?? "",
|
|
62
|
+
// v1.16: Agent's declared next action
|
|
63
|
+
next_action: lastEval.next_action,
|
|
62
64
|
};
|
|
63
65
|
}
|
|
64
66
|
return req;
|
|
65
67
|
}
|
|
66
|
-
function parseLevel(rationale) {
|
|
67
|
-
if (!rationale)
|
|
68
|
-
return "l2";
|
|
69
|
-
if (rationale.includes("l0"))
|
|
70
|
-
return "l0";
|
|
71
|
-
if (rationale.includes("l1"))
|
|
72
|
-
return "l1";
|
|
73
|
-
return "l2";
|
|
74
|
-
}
|
|
75
68
|
function parseWarnings(prompt) {
|
|
76
69
|
if (!prompt)
|
|
77
70
|
return [];
|
|
@@ -89,85 +82,170 @@ function parseWarnings(prompt) {
|
|
|
89
82
|
// ── SessionManager ─────────────────────────────────────────────────────────
|
|
90
83
|
export class SessionManager {
|
|
91
84
|
sessions = new Map();
|
|
85
|
+
/** Serializes state transitions for each session. */
|
|
86
|
+
sessionQueues = new Map();
|
|
92
87
|
backend;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
88
|
+
sessionStore;
|
|
89
|
+
ownerId = `${process.pid}:${randomUUID()}`;
|
|
90
|
+
leaseMs;
|
|
91
|
+
leaseRenewIntervalMs;
|
|
92
|
+
leaseTimer = null;
|
|
93
|
+
checkpointSinks = new Set();
|
|
94
|
+
/** Explicit context provider; never auto-discovered. */
|
|
95
|
+
contextProvider;
|
|
96
|
+
terminalSinks = new Set();
|
|
97
|
+
constructor(storeOrBackend, sessionStore) {
|
|
98
|
+
const isLoopStore = (v) => "readSession" in v && typeof v.readSession === "function";
|
|
99
|
+
const store = storeOrBackend
|
|
100
|
+
? isLoopStore(storeOrBackend)
|
|
101
|
+
? storeOrBackend
|
|
102
|
+
: new VaultBackendLoopStore(storeOrBackend)
|
|
103
|
+
: new FileLoopStore(getPolicy().backend.root_dir);
|
|
104
|
+
this.backend = storeOrBackend && !isLoopStore(storeOrBackend)
|
|
105
|
+
? storeOrBackend
|
|
106
|
+
: new LoopStoreBackend(store);
|
|
107
|
+
this.sessionStore = sessionStore ?? new VaultSessionStateStore(store);
|
|
108
|
+
const mcpPolicy = getPolicy().mcp;
|
|
109
|
+
this.leaseMs = Math.max(1, mcpPolicy.session_lease_ms);
|
|
110
|
+
this.leaseRenewIntervalMs = Math.max(1, Math.min(mcpPolicy.session_lease_renew_interval_ms, this.leaseMs));
|
|
111
|
+
if (this.sessionStore?.renewLease) {
|
|
112
|
+
this.leaseTimer = setInterval(() => this.renewOwnedLeases(), this.leaseRenewIntervalMs);
|
|
113
|
+
this.leaseTimer.unref?.();
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Stable process-local owner token used for cross-process session leases. */
|
|
117
|
+
getOwnerId() {
|
|
118
|
+
return this.ownerId;
|
|
119
|
+
}
|
|
120
|
+
/** Subscribe an external checkpointer; sink failures are isolated. */
|
|
121
|
+
addCheckpointSink(sink) {
|
|
122
|
+
this.checkpointSinks.add(sink);
|
|
123
|
+
return () => this.checkpointSinks.delete(sink);
|
|
124
|
+
}
|
|
125
|
+
addTerminalSink(sink) {
|
|
126
|
+
this.terminalSinks.add(sink);
|
|
127
|
+
return () => this.terminalSinks.delete(sink);
|
|
128
|
+
}
|
|
129
|
+
/** Release owned sessions and stop lease maintenance. */
|
|
130
|
+
close() {
|
|
131
|
+
if (this.leaseTimer)
|
|
132
|
+
clearInterval(this.leaseTimer);
|
|
133
|
+
this.leaseTimer = null;
|
|
134
|
+
for (const session of this.sessions.values()) {
|
|
135
|
+
this.sessionStore?.releaseLease?.(session.loopId, this.ownerId);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
findSessionEntry(loopId) {
|
|
139
|
+
return this.sessionStore?.load(loopId);
|
|
140
|
+
}
|
|
141
|
+
claimSessionEntry(loopId) {
|
|
142
|
+
if (this.sessionStore?.acquireLease) {
|
|
143
|
+
return this.sessionStore.acquireLease(loopId, this.ownerId, this.leaseMs);
|
|
144
|
+
}
|
|
145
|
+
return this.findSessionEntry(loopId);
|
|
146
|
+
}
|
|
147
|
+
renewSessionLease(loopId) {
|
|
148
|
+
if (!this.sessionStore?.renewLease)
|
|
149
|
+
return true;
|
|
150
|
+
try {
|
|
151
|
+
return this.sessionStore.renewLease(loopId, this.ownerId, this.leaseMs);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
renewOwnedLeases() {
|
|
158
|
+
for (const session of this.sessions.values()) {
|
|
159
|
+
if (session.status === "running")
|
|
160
|
+
this.renewSessionLease(session.loopId);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
leaseConflictResult(loopId, entry) {
|
|
164
|
+
const lineage = (entry?.loop_lineage ?? {});
|
|
165
|
+
return {
|
|
166
|
+
sessionId: "",
|
|
167
|
+
round: typeof lineage.current_round === "number" ? lineage.current_round : 0,
|
|
168
|
+
prompt: null,
|
|
169
|
+
stopReason: `session_owned_elsewhere:${loopId}`,
|
|
170
|
+
stopDetail: `Another process (PID ${entry?.loop_lineage ? entry.loop_lineage.lease_owner ?? "unknown" : "unknown"}) holds the lease for loop "${loopId}". Wait for the lease to expire or stop the other process.`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
async withSessionQueue(sessionId, work) {
|
|
174
|
+
const previous = this.sessionQueues.get(sessionId) ?? Promise.resolve();
|
|
175
|
+
let release;
|
|
176
|
+
const gate = new Promise((resolve) => {
|
|
177
|
+
release = resolve;
|
|
178
|
+
});
|
|
179
|
+
const tail = previous.then(() => gate);
|
|
180
|
+
this.sessionQueues.set(sessionId, tail);
|
|
181
|
+
await previous;
|
|
182
|
+
try {
|
|
183
|
+
return await work();
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
release();
|
|
187
|
+
if (this.sessionQueues.get(sessionId) === tail) {
|
|
188
|
+
this.sessionQueues.delete(sessionId);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
99
191
|
}
|
|
100
192
|
async create(input) {
|
|
101
193
|
const sessionId = randomUUID();
|
|
102
194
|
const loopId = input.loopId ?? randomUUID();
|
|
103
|
-
const engine = new LoopForgeEngine(
|
|
104
|
-
const maxRounds = input.maxRounds ?? getPolicy().
|
|
195
|
+
const engine = new LoopForgeEngine(this.backend);
|
|
196
|
+
const maxRounds = input.maxRounds ?? getPolicy().engine.max_rounds;
|
|
105
197
|
// Populate extra fields for the first round
|
|
106
198
|
const request = buildLoopRequest({
|
|
107
199
|
sessionId, loopId, task: input.task, engine, currentRound: 1,
|
|
108
200
|
maxRounds, successTrajectory: [], status: "running", createdAt: Date.now(),
|
|
109
|
-
|
|
110
|
-
|
|
201
|
+
consecutiveRejections: 0,
|
|
202
|
+
lastRejectionCheck: "",
|
|
203
|
+
evidenceBaseline: [],
|
|
111
204
|
});
|
|
112
205
|
request.domain = input.domain ?? "";
|
|
113
206
|
request.plan_source = input.planSource ?? null;
|
|
114
207
|
request.constraints_from_plan = input.constraints ?? [];
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
const allowedPhases = new Set(resolveAllowedPhases(maxRounds, miPolicy.round_tiers));
|
|
118
|
-
if (miPolicy.enabled && this.memoryProvider && allowedPhases.has(1)) {
|
|
208
|
+
// Explicit context is requested by the MCP embedding; no auto-discovery.
|
|
209
|
+
if (this.contextProvider) {
|
|
119
210
|
try {
|
|
120
211
|
const ctx = {
|
|
121
212
|
loopId,
|
|
122
213
|
round: 1,
|
|
123
214
|
task: input.task,
|
|
124
215
|
domain: input.domain ?? "",
|
|
125
|
-
phase: 1,
|
|
126
|
-
progressEstimate: 0,
|
|
127
|
-
accumulatedContext: {
|
|
128
|
-
recurringIssues: [],
|
|
129
|
-
failedPatterns: [],
|
|
130
|
-
keyLessons: [],
|
|
131
|
-
remainingCriteria: [],
|
|
132
|
-
},
|
|
133
216
|
};
|
|
134
|
-
const rawContext = await this.
|
|
217
|
+
const rawContext = await this.contextProvider(ctx);
|
|
135
218
|
if (rawContext?.trim()) {
|
|
136
|
-
request.external_context = rawContext.trim()
|
|
137
|
-
logEvent("
|
|
138
|
-
loopId, round: 1,
|
|
219
|
+
request.external_context = rawContext.trim();
|
|
220
|
+
logEvent("external_context_loaded", {
|
|
221
|
+
loopId, round: 1,
|
|
139
222
|
contextLength: request.external_context.length,
|
|
140
223
|
});
|
|
141
224
|
}
|
|
142
225
|
}
|
|
143
226
|
catch {
|
|
144
|
-
//
|
|
145
|
-
logEvent("
|
|
227
|
+
// Provider failures are isolated from session creation.
|
|
228
|
+
logEvent("context_provider_error", { loopId, round: 1 });
|
|
146
229
|
}
|
|
147
230
|
}
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
const sfDir = join(process.cwd(), getPolicy().state_file.directory);
|
|
153
|
-
mkdirSync(sfDir, { recursive: true });
|
|
154
|
-
writeFileSync(join(sfDir, `${loopId}-state.md`), sfContent, "utf-8");
|
|
155
|
-
}
|
|
156
|
-
const injectedCtx = request.external_context;
|
|
231
|
+
const prepared = await new RoundDriver(engine, this.backend).prepare(request, loopId, 1);
|
|
232
|
+
const initialPrompt = prepared?.prompt ?? null;
|
|
233
|
+
const initialLevel = prepared?.level ?? "l2";
|
|
234
|
+
const evidenceBaseline = prepared?.evidenceBaseline ?? [];
|
|
157
235
|
const session = {
|
|
158
236
|
sessionId, loopId, task: input.task, engine,
|
|
159
237
|
currentRound: 1, maxRounds, successTrajectory: [],
|
|
160
238
|
status: "running", createdAt: Date.now(),
|
|
161
|
-
// v1.7: Memory integration state
|
|
162
|
-
injectionCount: injectedCtx ? 1 : 0,
|
|
163
|
-
lastInjectionRound: injectedCtx ? 1 : 0,
|
|
164
|
-
injectedContexts: injectedCtx ? [injectedCtx] : [],
|
|
165
|
-
phase2Triggered: false,
|
|
166
|
-
phase3Triggered: false,
|
|
167
239
|
// v1.13: Enforcement gate state
|
|
168
240
|
consecutiveRejections: 0,
|
|
241
|
+
lastRejectionCheck: "",
|
|
242
|
+
evidenceBaseline,
|
|
243
|
+
roundSnapshot: prepared?.snapshot ?? prepareRoundTransaction(loopId, 1, evidenceBaseline),
|
|
244
|
+
currentPrompt: initialPrompt,
|
|
245
|
+
currentLevel: initialLevel,
|
|
169
246
|
};
|
|
170
247
|
this.sessions.set(sessionId, session);
|
|
248
|
+
policyMetrics.recordStrategy(loopId, initialLevel);
|
|
171
249
|
// Persist to vault for cross-process recovery
|
|
172
250
|
this.save(session);
|
|
173
251
|
logEvent("session_start", {
|
|
@@ -179,143 +257,454 @@ export class SessionManager {
|
|
|
179
257
|
return {
|
|
180
258
|
sessionId,
|
|
181
259
|
round: 1,
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
level:
|
|
260
|
+
roundId: session.roundSnapshot?.roundId,
|
|
261
|
+
prompt: initialPrompt,
|
|
262
|
+
level: initialLevel,
|
|
185
263
|
roundSuccess: false,
|
|
186
|
-
|
|
187
|
-
warnings: parseWarnings(result.response?.prompt ?? null),
|
|
264
|
+
warnings: parseWarnings(initialPrompt),
|
|
188
265
|
};
|
|
189
266
|
}
|
|
190
267
|
get(sessionId) {
|
|
191
268
|
return this.sessions.get(sessionId);
|
|
192
269
|
}
|
|
270
|
+
getLeaseStatus(loopId) {
|
|
271
|
+
const entry = this.findSessionEntry(loopId);
|
|
272
|
+
if (!entry)
|
|
273
|
+
return null;
|
|
274
|
+
const lineage = (entry.loop_lineage ?? {});
|
|
275
|
+
const owner = typeof lineage.lease_owner === "string"
|
|
276
|
+
? lineage.lease_owner
|
|
277
|
+
: "";
|
|
278
|
+
const ownerPid = owner.match(/^(\d+):/)?.[1];
|
|
279
|
+
return {
|
|
280
|
+
ownedByThisProcess: owner === this.ownerId,
|
|
281
|
+
ownerPid: ownerPid ? Number(ownerPid) : null,
|
|
282
|
+
expiresAt: typeof lineage.lease_expires_at === "number"
|
|
283
|
+
? new Date(lineage.lease_expires_at).toISOString()
|
|
284
|
+
: null,
|
|
285
|
+
epoch: typeof lineage.lease_epoch === "number"
|
|
286
|
+
? lineage.lease_epoch
|
|
287
|
+
: 0,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
193
290
|
delete(sessionId) {
|
|
194
291
|
const session = this.sessions.get(sessionId);
|
|
195
292
|
if (!session)
|
|
196
293
|
return false;
|
|
197
294
|
session.status = "stopped";
|
|
198
|
-
|
|
295
|
+
this.save(session);
|
|
296
|
+
void this.notifyTerminal(session, "cancelled");
|
|
199
297
|
this.sessions.delete(sessionId);
|
|
200
298
|
logEvent("session_end", {
|
|
201
299
|
sessionId,
|
|
202
300
|
loopId: session.loopId,
|
|
203
|
-
stopReason: "
|
|
301
|
+
stopReason: "cancelled",
|
|
204
302
|
roundsCompleted: session.currentRound,
|
|
205
303
|
});
|
|
206
304
|
return true;
|
|
207
305
|
}
|
|
306
|
+
/** v1.18: Pause a running session. The session state is persisted to
|
|
307
|
+
* vault so it survives process restarts. Returns the session status.
|
|
308
|
+
* Paused sessions cannot be advanced — they must be resumed first. */
|
|
309
|
+
pause(sessionId) {
|
|
310
|
+
const session = this.sessions.get(sessionId);
|
|
311
|
+
if (!session)
|
|
312
|
+
return { sessionId, round: 0, status: "not_found" };
|
|
313
|
+
if (session.status !== "running") {
|
|
314
|
+
return { sessionId, round: session.currentRound, status: session.status };
|
|
315
|
+
}
|
|
316
|
+
session.status = "paused";
|
|
317
|
+
this.save(session);
|
|
318
|
+
logEvent("session_paused", {
|
|
319
|
+
sessionId,
|
|
320
|
+
loopId: session.loopId,
|
|
321
|
+
round: session.currentRound,
|
|
322
|
+
});
|
|
323
|
+
return { sessionId, round: session.currentRound, status: "paused" };
|
|
324
|
+
}
|
|
325
|
+
restoredPromptResult(session) {
|
|
326
|
+
if (!session.currentPrompt)
|
|
327
|
+
return null;
|
|
328
|
+
return {
|
|
329
|
+
sessionId: session.sessionId,
|
|
330
|
+
round: session.currentRound,
|
|
331
|
+
roundId: session.roundSnapshot?.roundId,
|
|
332
|
+
prompt: session.currentPrompt,
|
|
333
|
+
level: session.currentLevel ?? "l2",
|
|
334
|
+
roundSuccess: undefined,
|
|
335
|
+
warnings: parseWarnings(session.currentPrompt),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
/** Reconcile the crash window where feedback committed but session_state
|
|
339
|
+
* still points at the old prompt. Returns null when no commit is pending. */
|
|
340
|
+
reconcileCommittedRound(session) {
|
|
341
|
+
if (!session.roundSnapshot)
|
|
342
|
+
return null;
|
|
343
|
+
const recovered = new RoundDriver(session.engine, this.backend).recover(session.roundSnapshot);
|
|
344
|
+
if (!recovered)
|
|
345
|
+
return null;
|
|
346
|
+
const pr = recovered.result;
|
|
347
|
+
session.roundSnapshot = recovered.snapshot;
|
|
348
|
+
// Replay the committed counter — but with per-rule tracking.
|
|
349
|
+
if (pr.action === "reject" && pr.rejectionCheck) {
|
|
350
|
+
session.consecutiveRejections =
|
|
351
|
+
pr.rejectionCheck === session.lastRejectionCheck
|
|
352
|
+
? pr.newConsecutiveRejections
|
|
353
|
+
: 1;
|
|
354
|
+
session.lastRejectionCheck = pr.rejectionCheck;
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
session.consecutiveRejections = pr.newConsecutiveRejections;
|
|
358
|
+
if (pr.action !== "reject")
|
|
359
|
+
session.lastRejectionCheck = "";
|
|
360
|
+
}
|
|
361
|
+
if (pr.newLastSelfEval)
|
|
362
|
+
session.lastSelfEval = pr.newLastSelfEval;
|
|
363
|
+
if (pr.shouldPushSuccessTrajectory &&
|
|
364
|
+
session.successTrajectory.length < session.currentRound) {
|
|
365
|
+
session.successTrajectory.push(pr.roundSuccess);
|
|
366
|
+
}
|
|
367
|
+
session.currentPrompt = null;
|
|
368
|
+
if (pr.action === "stop" || pr.action === "terminate") {
|
|
369
|
+
const reason = pr.action === "terminate"
|
|
370
|
+
? "enforcement_terminated"
|
|
371
|
+
: pr.stopReason ?? "stalled";
|
|
372
|
+
session.status = reason === "stalled" ? "stalled" : "stopped";
|
|
373
|
+
this.save(session);
|
|
374
|
+
void this.notifyTerminal(session, reason);
|
|
375
|
+
return {
|
|
376
|
+
sessionId: session.sessionId,
|
|
377
|
+
round: session.currentRound,
|
|
378
|
+
roundId: recovered.snapshot.roundId,
|
|
379
|
+
prompt: null,
|
|
380
|
+
stopReason: reason,
|
|
381
|
+
stopDetail: reason === "completed"
|
|
382
|
+
? "Task completed successfully — the agent declared should_continue=false with success=true."
|
|
383
|
+
: reason === "failed"
|
|
384
|
+
? "Agent declared should_continue=false with success=false — the task was not achieved."
|
|
385
|
+
: `Loop stopped: ${reason}.`,
|
|
386
|
+
roundSuccess: pr.roundSuccess,
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
if (pr.action !== "continue")
|
|
390
|
+
return null;
|
|
391
|
+
session.currentRound++;
|
|
392
|
+
const request = buildLoopRequest(session, pr.newLastSelfEval, pr.verificationFlags);
|
|
393
|
+
const prepared = new RoundDriver(session.engine, this.backend).prepareSync(request, session.loopId, session.currentRound);
|
|
394
|
+
if (!prepared) {
|
|
395
|
+
session.status = "stalled";
|
|
396
|
+
this.save(session);
|
|
397
|
+
return {
|
|
398
|
+
sessionId: session.sessionId,
|
|
399
|
+
round: session.currentRound,
|
|
400
|
+
prompt: null,
|
|
401
|
+
stopReason: "stalled",
|
|
402
|
+
stopDetail: "RoundDriver.prepareSync returned null — prompt compilation failed during crash recovery. The session state may be inconsistent.",
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
const prompt = prepared.prompt;
|
|
406
|
+
session.evidenceBaseline = prepared.evidenceBaseline;
|
|
407
|
+
session.roundSnapshot = prepared.snapshot;
|
|
408
|
+
session.currentPrompt = prompt;
|
|
409
|
+
session.currentLevel = prepared.level;
|
|
410
|
+
policyMetrics.recordStrategy(session.loopId, session.currentLevel);
|
|
411
|
+
this.save(session);
|
|
412
|
+
return {
|
|
413
|
+
sessionId: session.sessionId,
|
|
414
|
+
round: session.currentRound,
|
|
415
|
+
roundId: session.roundSnapshot.roundId,
|
|
416
|
+
prompt,
|
|
417
|
+
level: session.currentLevel,
|
|
418
|
+
roundSuccess: pr.roundSuccess,
|
|
419
|
+
warnings: parseWarnings(prompt),
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
/** v1.18: Resume a paused session. Reconstructs from vault state and
|
|
423
|
+
* compiles the next prompt. Returns null if no paused session exists
|
|
424
|
+
* for this loopId. */
|
|
425
|
+
async unpause(loopId) {
|
|
426
|
+
const persistedEntry = this.findSessionEntry(loopId);
|
|
427
|
+
if (!persistedEntry)
|
|
428
|
+
return null;
|
|
429
|
+
const lineage = (persistedEntry.loop_lineage ?? {});
|
|
430
|
+
const status = lineage.status ?? "running";
|
|
431
|
+
if (status !== "paused")
|
|
432
|
+
return null;
|
|
433
|
+
const sessionEntry = this.claimSessionEntry(loopId);
|
|
434
|
+
if (!sessionEntry)
|
|
435
|
+
return this.leaseConflictResult(loopId, persistedEntry);
|
|
436
|
+
const session = this.reconstructSession(sessionEntry, true);
|
|
437
|
+
if (!session)
|
|
438
|
+
return null;
|
|
439
|
+
// Set to running so advance() works
|
|
440
|
+
session.status = "running";
|
|
441
|
+
this.sessions.set(session.sessionId, session);
|
|
442
|
+
// Replace the sync-fallback evidence baseline with async evidence
|
|
443
|
+
// so resumed sessions don't silently drop async provider data.
|
|
444
|
+
try {
|
|
445
|
+
const asyncEvidence = await EvidenceCollector.fromPolicy().collectAsync({
|
|
446
|
+
loopId: session.loopId,
|
|
447
|
+
phase: "before",
|
|
448
|
+
});
|
|
449
|
+
if (asyncEvidence.length > 0) {
|
|
450
|
+
session.evidenceBaseline = asyncEvidence;
|
|
451
|
+
if (!session.roundSnapshot?.beforeEvidence?.length) {
|
|
452
|
+
session.roundSnapshot = prepareRoundTransaction(session.loopId, session.currentRound, asyncEvidence);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
catch {
|
|
457
|
+
// Async evidence is best-effort; fall back to sync baseline.
|
|
458
|
+
}
|
|
459
|
+
const reconciled = this.reconcileCommittedRound(session);
|
|
460
|
+
if (reconciled)
|
|
461
|
+
return reconciled;
|
|
462
|
+
const restored = this.restoredPromptResult(session);
|
|
463
|
+
if (restored) {
|
|
464
|
+
this.save(session);
|
|
465
|
+
return restored;
|
|
466
|
+
}
|
|
467
|
+
// Compile the next prompt from the current round state
|
|
468
|
+
const lcr = makeLoopCompileRequest({
|
|
469
|
+
loop_id: session.loopId,
|
|
470
|
+
round: session.currentRound,
|
|
471
|
+
goal_id: "",
|
|
472
|
+
task: session.task,
|
|
473
|
+
domain: undefined,
|
|
474
|
+
plan_source: null,
|
|
475
|
+
constraints_from_plan: [],
|
|
476
|
+
health_check_interval: 1,
|
|
477
|
+
});
|
|
478
|
+
const compileRequest = {
|
|
479
|
+
task: session.task,
|
|
480
|
+
mode: Mode.LOOP_COMPILE,
|
|
481
|
+
feedback: null,
|
|
482
|
+
skill_name: null,
|
|
483
|
+
task_id: null,
|
|
484
|
+
loop_id: lcr.loop_id,
|
|
485
|
+
round: lcr.round,
|
|
486
|
+
goal_id: lcr.goal_id,
|
|
487
|
+
domain: lcr.domain ?? "",
|
|
488
|
+
plan_source: lcr.plan_source ?? null,
|
|
489
|
+
constraints_from_plan: lcr.constraints_from_plan ?? [],
|
|
490
|
+
health_check_interval: lcr.health_check_interval,
|
|
491
|
+
max_rounds: session.maxRounds,
|
|
492
|
+
};
|
|
493
|
+
const prepared = await new RoundDriver(session.engine, this.backend).prepare(compileRequest, session.loopId, session.currentRound);
|
|
494
|
+
if (!prepared) {
|
|
495
|
+
session.status = "stopped";
|
|
496
|
+
this.save(session);
|
|
497
|
+
void this.notifyTerminal(session, "stalled");
|
|
498
|
+
return { sessionId: session.sessionId, round: session.currentRound, prompt: null, stopReason: "stalled", stopDetail: "RoundDriver.prepare returned null during unpause — the compiler could not produce a prompt." };
|
|
499
|
+
}
|
|
500
|
+
const prompt = prepared.prompt;
|
|
501
|
+
const level = prepared.level;
|
|
502
|
+
session.evidenceBaseline = prepared.evidenceBaseline;
|
|
503
|
+
session.roundSnapshot = prepared.snapshot;
|
|
504
|
+
session.currentPrompt = prompt;
|
|
505
|
+
session.currentLevel = level;
|
|
506
|
+
policyMetrics.recordStrategy(session.loopId, level);
|
|
507
|
+
this.save(session);
|
|
508
|
+
return {
|
|
509
|
+
sessionId: session.sessionId,
|
|
510
|
+
round: session.currentRound,
|
|
511
|
+
roundId: session.roundSnapshot.roundId,
|
|
512
|
+
prompt,
|
|
513
|
+
level,
|
|
514
|
+
roundSuccess: undefined,
|
|
515
|
+
warnings: [],
|
|
516
|
+
};
|
|
517
|
+
}
|
|
208
518
|
/** Persist session state to vault for cross-process recovery.
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* Entire read→filter→write→append is wrapped in a file lock to prevent
|
|
212
|
-
* lost updates from concurrent processes. */
|
|
519
|
+
* The filtered vault and replacement entry are written once under the
|
|
520
|
+
* backend lock, so recovery never observes the old two-write gap. */
|
|
213
521
|
save(session) {
|
|
214
|
-
if (!this.
|
|
522
|
+
if (!this.sessionStore)
|
|
215
523
|
return;
|
|
216
|
-
const
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
consecutive_rejections: session.consecutiveRejections,
|
|
243
|
-
},
|
|
244
|
-
});
|
|
524
|
+
const leaseActive = session.status === "running";
|
|
525
|
+
const sessionEntry = {
|
|
526
|
+
task_id: `loop:${session.loopId}:session`,
|
|
527
|
+
task_type: "session_state",
|
|
528
|
+
timestamp: new Date().toISOString(),
|
|
529
|
+
loop_id: session.loopId,
|
|
530
|
+
task: session.task,
|
|
531
|
+
loop_lineage: {
|
|
532
|
+
session_id: session.sessionId,
|
|
533
|
+
current_round: session.currentRound,
|
|
534
|
+
max_rounds: session.maxRounds,
|
|
535
|
+
success_trajectory: session.successTrajectory,
|
|
536
|
+
status: session.status,
|
|
537
|
+
created_at: session.createdAt,
|
|
538
|
+
// v1.13: Enforcement gate state
|
|
539
|
+
consecutive_rejections: session.consecutiveRejections,
|
|
540
|
+
last_rejection_check: session.lastRejectionCheck,
|
|
541
|
+
// v1.19: durable round transaction state
|
|
542
|
+
round_snapshot: session.roundSnapshot ?? null,
|
|
543
|
+
last_self_eval: session.lastSelfEval ?? null,
|
|
544
|
+
current_prompt: session.currentPrompt ?? null,
|
|
545
|
+
current_level: session.currentLevel ?? "",
|
|
546
|
+
// v1.20: cross-process single-owner lease
|
|
547
|
+
lease_owner: leaseActive ? this.ownerId : "",
|
|
548
|
+
lease_expires_at: leaseActive ? Date.now() + this.leaseMs : 0,
|
|
549
|
+
},
|
|
245
550
|
};
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
551
|
+
this.sessionStore.save(sessionEntry, {
|
|
552
|
+
expectedLeaseOwner: this.ownerId,
|
|
553
|
+
});
|
|
554
|
+
if (this.checkpointSinks.size > 0) {
|
|
555
|
+
const checkpoint = createCognitiveCheckpoint(session, sessionEntry.timestamp);
|
|
556
|
+
for (const sink of this.checkpointSinks) {
|
|
557
|
+
try {
|
|
558
|
+
const pending = sink.save(checkpoint);
|
|
559
|
+
if (pending && typeof pending.then === "function") {
|
|
560
|
+
void pending.catch(() => undefined);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch {
|
|
564
|
+
// External adapters must not affect session durability.
|
|
565
|
+
}
|
|
566
|
+
}
|
|
250
567
|
}
|
|
251
|
-
|
|
252
|
-
|
|
568
|
+
}
|
|
569
|
+
/** Reconstruct a McpSession from a vault session_state entry.
|
|
570
|
+
* Returns null if the entry is not "running" status.
|
|
571
|
+
* Shared by resume() and autoResumeAll(). */
|
|
572
|
+
reconstructSession(entry, allowPaused = false) {
|
|
573
|
+
const lineage = (entry.loop_lineage ?? {});
|
|
574
|
+
const status = lineage.status ?? "running";
|
|
575
|
+
if (status !== "running" && !(allowPaused && status === "paused")) {
|
|
576
|
+
return null;
|
|
253
577
|
}
|
|
578
|
+
const loopId = entry.loop_id;
|
|
579
|
+
const currentRound = lineage.current_round ?? 1;
|
|
580
|
+
const successTrajectory = lineage.success_trajectory ?? lineage.quality_trajectory ?? [];
|
|
581
|
+
const task = entry.task ?? "";
|
|
582
|
+
const maxRounds = lineage.max_rounds ?? getPolicy().engine.max_rounds;
|
|
583
|
+
const roundSnapshot = parseRoundTransactionSnapshot(lineage.round_snapshot);
|
|
584
|
+
const fallbackEvidence = EvidenceCollector.fromProviderNames(getPolicy().evidence.providers).collect();
|
|
585
|
+
const persistedEval = lineage.last_self_eval;
|
|
586
|
+
const lastSelfEval = persistedEval !== null &&
|
|
587
|
+
typeof persistedEval === "object" &&
|
|
588
|
+
!Array.isArray(persistedEval) &&
|
|
589
|
+
typeof persistedEval.success === "boolean" &&
|
|
590
|
+
typeof persistedEval.output_summary === "string"
|
|
591
|
+
? persistedEval
|
|
592
|
+
: undefined;
|
|
593
|
+
const engine = new LoopForgeEngine(this.backend);
|
|
594
|
+
return {
|
|
595
|
+
sessionId: typeof lineage.session_id === "string" && lineage.session_id
|
|
596
|
+
? lineage.session_id
|
|
597
|
+
: randomUUID(),
|
|
598
|
+
loopId, task, engine,
|
|
599
|
+
currentRound, maxRounds, successTrajectory,
|
|
600
|
+
status: status,
|
|
601
|
+
createdAt: lineage.created_at ?? Date.now(),
|
|
602
|
+
consecutiveRejections: lineage.consecutive_rejections ?? 0,
|
|
603
|
+
lastRejectionCheck: typeof lineage.last_rejection_check === "string"
|
|
604
|
+
? lineage.last_rejection_check
|
|
605
|
+
: "",
|
|
606
|
+
evidenceBaseline: roundSnapshot?.beforeEvidence ?? fallbackEvidence,
|
|
607
|
+
roundSnapshot: roundSnapshot ?? prepareRoundTransaction(loopId, currentRound, fallbackEvidence),
|
|
608
|
+
lastSelfEval,
|
|
609
|
+
currentPrompt: typeof lineage.current_prompt === "string"
|
|
610
|
+
? lineage.current_prompt
|
|
611
|
+
: null,
|
|
612
|
+
currentLevel: typeof lineage.current_level === "string"
|
|
613
|
+
? lineage.current_level
|
|
614
|
+
: undefined,
|
|
615
|
+
};
|
|
254
616
|
}
|
|
255
617
|
/** Resume a loop from vault state.
|
|
256
618
|
* Reconstructs the session and compiles the prompt for the next round.
|
|
257
619
|
* Returns null if no session_state entry exists for this loopId. */
|
|
258
620
|
resume(loopId) {
|
|
259
|
-
|
|
621
|
+
const persistedEntry = this.findSessionEntry(loopId);
|
|
622
|
+
if (!persistedEntry)
|
|
260
623
|
return null;
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
624
|
+
const persistedLineage = (persistedEntry.loop_lineage ?? {});
|
|
625
|
+
const persistedStatus = persistedLineage.status ?? "running";
|
|
626
|
+
const sessionEntry = persistedStatus === "running"
|
|
627
|
+
? this.claimSessionEntry(loopId)
|
|
628
|
+
: persistedEntry;
|
|
265
629
|
if (!sessionEntry)
|
|
266
|
-
return
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
// If the loop was already stopped or stalled, return immediately
|
|
274
|
-
if (status !== "running") {
|
|
630
|
+
return this.leaseConflictResult(loopId, persistedEntry);
|
|
631
|
+
const session = this.reconstructSession(sessionEntry);
|
|
632
|
+
if (!session) {
|
|
633
|
+
// was not "running" — return stopped/stalled status
|
|
634
|
+
const lineage = (sessionEntry.loop_lineage ?? {});
|
|
635
|
+
const currentRound = lineage.current_round ?? 1;
|
|
636
|
+
const status = lineage.status ?? "stopped";
|
|
275
637
|
return {
|
|
276
638
|
sessionId: "",
|
|
277
639
|
round: currentRound,
|
|
278
640
|
prompt: null,
|
|
279
641
|
stopReason: status,
|
|
642
|
+
stopDetail: `Loop is not running (status: ${status}). It may have already completed or been stopped.`,
|
|
280
643
|
};
|
|
281
644
|
}
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
currentRound,
|
|
291
|
-
maxRounds,
|
|
292
|
-
successTrajectory,
|
|
293
|
-
status: "running",
|
|
294
|
-
createdAt: lineage.created_at ?? Date.now(),
|
|
295
|
-
// v1.7: Memory integration state restored from vault
|
|
296
|
-
injectionCount: lineage.injection_count ?? 0,
|
|
297
|
-
lastInjectionRound: lineage.last_injection_round ?? 0,
|
|
298
|
-
injectedContexts: [],
|
|
299
|
-
phase2Triggered: lineage.phase2_triggered ?? false,
|
|
300
|
-
phase3Triggered: lineage.phase3_triggered ?? false,
|
|
301
|
-
// v1.13: Enforcement gate state restored from vault
|
|
302
|
-
consecutiveRejections: lineage.consecutive_rejections ?? 0,
|
|
303
|
-
};
|
|
304
|
-
this.sessions.set(sessionId, session);
|
|
305
|
-
// Compile the next round's prompt from vault lineage
|
|
645
|
+
this.sessions.set(session.sessionId, session);
|
|
646
|
+
const reconciled = this.reconcileCommittedRound(session);
|
|
647
|
+
if (reconciled)
|
|
648
|
+
return reconciled;
|
|
649
|
+
const restored = this.restoredPromptResult(session);
|
|
650
|
+
if (restored)
|
|
651
|
+
return restored;
|
|
652
|
+
// Legacy session without a stored prompt: compile once, then persist it.
|
|
306
653
|
const request = buildLoopRequest(session);
|
|
307
|
-
const
|
|
654
|
+
const prepared = new RoundDriver(session.engine, this.backend).prepareSync(request, session.loopId, session.currentRound);
|
|
655
|
+
const prompt = prepared?.prompt ?? null;
|
|
656
|
+
session.evidenceBaseline = prepared?.evidenceBaseline ?? [];
|
|
657
|
+
session.roundSnapshot = prepared?.snapshot ?? prepareRoundTransaction(session.loopId, session.currentRound, []);
|
|
658
|
+
session.currentPrompt = prompt;
|
|
659
|
+
session.currentLevel = prepared?.level ?? "l2";
|
|
660
|
+
policyMetrics.recordStrategy(session.loopId, session.currentLevel);
|
|
661
|
+
this.save(session);
|
|
308
662
|
return {
|
|
309
|
-
sessionId,
|
|
310
|
-
round: currentRound,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
level:
|
|
663
|
+
sessionId: session.sessionId,
|
|
664
|
+
round: session.currentRound,
|
|
665
|
+
roundId: session.roundSnapshot.roundId,
|
|
666
|
+
prompt,
|
|
667
|
+
level: session.currentLevel,
|
|
314
668
|
roundSuccess: false,
|
|
315
|
-
|
|
316
|
-
warnings: parseWarnings(result.response?.prompt ?? null),
|
|
669
|
+
warnings: parseWarnings(prompt),
|
|
317
670
|
};
|
|
318
671
|
}
|
|
672
|
+
/** Auto-resume all "running" sessions from vault on server startup.
|
|
673
|
+
* Scans vault for session_state entries, reconstructs each as an in-memory
|
|
674
|
+
* McpSession (without compiling — the next loopforge_next will do that).
|
|
675
|
+
* Returns the number of sessions resumed. */
|
|
676
|
+
autoResumeAll() {
|
|
677
|
+
if (!this.sessionStore)
|
|
678
|
+
return 0;
|
|
679
|
+
const entries = this.sessionStore.list();
|
|
680
|
+
// Build set of already-active loopIds
|
|
681
|
+
const activeLoopIds = new Set();
|
|
682
|
+
for (const s of this.sessions.values()) {
|
|
683
|
+
activeLoopIds.add(s.loopId);
|
|
684
|
+
}
|
|
685
|
+
let count = 0;
|
|
686
|
+
for (const entry of entries) {
|
|
687
|
+
if (entry.task_type !== "session_state")
|
|
688
|
+
continue;
|
|
689
|
+
const lid = entry.loop_id;
|
|
690
|
+
if (!lid || activeLoopIds.has(lid))
|
|
691
|
+
continue;
|
|
692
|
+
const lineage = (entry.loop_lineage ?? {});
|
|
693
|
+
const status = lineage.status ?? "running";
|
|
694
|
+
if (status !== "running" && status !== "paused")
|
|
695
|
+
continue;
|
|
696
|
+
const claimedEntry = this.claimSessionEntry(lid);
|
|
697
|
+
if (!claimedEntry)
|
|
698
|
+
continue;
|
|
699
|
+
const session = this.reconstructSession(claimedEntry, true);
|
|
700
|
+
if (session) {
|
|
701
|
+
this.sessions.set(session.sessionId, session);
|
|
702
|
+
activeLoopIds.add(lid);
|
|
703
|
+
count++;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return count;
|
|
707
|
+
}
|
|
319
708
|
list() {
|
|
320
709
|
const seen = new Set();
|
|
321
710
|
const result = [];
|
|
@@ -329,10 +718,9 @@ export class SessionManager {
|
|
|
329
718
|
status: s.status,
|
|
330
719
|
});
|
|
331
720
|
}
|
|
332
|
-
// Merge
|
|
333
|
-
if (this.
|
|
334
|
-
const
|
|
335
|
-
const entries = vault.entries ?? [];
|
|
721
|
+
// Merge persisted sessions not already in memory
|
|
722
|
+
if (this.sessionStore) {
|
|
723
|
+
const entries = this.sessionStore.list();
|
|
336
724
|
for (const e of entries) {
|
|
337
725
|
if (e.task_type !== "session_state")
|
|
338
726
|
continue;
|
|
@@ -367,11 +755,8 @@ export class SessionManager {
|
|
|
367
755
|
}
|
|
368
756
|
}
|
|
369
757
|
// Fall back to vault for task
|
|
370
|
-
if (!task
|
|
371
|
-
const
|
|
372
|
-
prefix: `loop:${loopId}:session`,
|
|
373
|
-
});
|
|
374
|
-
const sessionEntry = entries.find((e) => e.task_type === "session_state");
|
|
758
|
+
if (!task) {
|
|
759
|
+
const sessionEntry = this.findSessionEntry(loopId);
|
|
375
760
|
if (sessionEntry) {
|
|
376
761
|
task = sessionEntry.task ?? "";
|
|
377
762
|
}
|
|
@@ -379,7 +764,7 @@ export class SessionManager {
|
|
|
379
764
|
if (!task)
|
|
380
765
|
return null;
|
|
381
766
|
// Hydrate vault context
|
|
382
|
-
const engine = new LoopForgeEngine(
|
|
767
|
+
const engine = new LoopForgeEngine(this.backend);
|
|
383
768
|
const vaultContext = engine.hydrateLoopContext(loopId);
|
|
384
769
|
// Build a minimal request for health check
|
|
385
770
|
const request = makeLoopCompileRequest({
|
|
@@ -396,6 +781,7 @@ export class SessionManager {
|
|
|
396
781
|
drift_detected: health.drift_detected,
|
|
397
782
|
strategy_stability: health.strategy_stability,
|
|
398
783
|
task_continuity: health.task_continuity,
|
|
784
|
+
policy_metrics: policyMetrics.snapshot(loopId),
|
|
399
785
|
};
|
|
400
786
|
}
|
|
401
787
|
/** Core cycle: extract self-eval → record feedback → check stop → compile next.
|
|
@@ -403,272 +789,281 @@ export class SessionManager {
|
|
|
403
789
|
* When provided (MCP path with evaluation parameter), skips regex extraction.
|
|
404
790
|
* When undefined (runtime/CLI path), falls back to regex extraction from output. */
|
|
405
791
|
async advance(sessionId, output, preExtractedEval) {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
792
|
+
return this.withSessionQueue(sessionId, async () => {
|
|
793
|
+
const session = this.sessions.get(sessionId);
|
|
794
|
+
if (session?.status === "running" &&
|
|
795
|
+
!this.renewSessionLease(session.loopId)) {
|
|
796
|
+
return this.leaseConflictResult(session.loopId, this.findSessionEntry(session.loopId));
|
|
797
|
+
}
|
|
798
|
+
try {
|
|
799
|
+
return await this.advanceUnlocked(sessionId, output, preExtractedEval);
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
if (error instanceof SessionLeaseConflictError) {
|
|
803
|
+
return this.leaseConflictResult(error.loopId, this.findSessionEntry(error.loopId));
|
|
804
|
+
}
|
|
805
|
+
throw error;
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
// ── advanceUnlocked pipeline ───────────────────────────────────────────
|
|
810
|
+
//
|
|
811
|
+
// The method is split into focused private helpers so each phase of the
|
|
812
|
+
// round-boundary decision loop is independently readable and testable.
|
|
813
|
+
//
|
|
814
|
+
// Pipeline: validate → extract → execute transaction → route disposition
|
|
815
|
+
// → advance to next round (continue) or return terminal result.
|
|
816
|
+
/** Extract a SelfEvaluation from agent output.
|
|
817
|
+
* Structured param preferred → regex extraction → heuristic fallback.
|
|
818
|
+
* Returns null only when both regex and heuristic returned null. */
|
|
819
|
+
extractEvaluation(output, preExtractedEval) {
|
|
415
820
|
if (preExtractedEval) {
|
|
416
|
-
selfEval
|
|
417
|
-
|
|
821
|
+
return { selfEval: preExtractedEval, extractionFailed: false };
|
|
822
|
+
}
|
|
823
|
+
const structured = extractSelfEvaluation(output);
|
|
824
|
+
const extractionFailed = structured === null;
|
|
825
|
+
const selfEval = structured ?? heuristicSelfEvaluation(output);
|
|
826
|
+
return selfEval ? { selfEval, extractionFailed } : null;
|
|
827
|
+
}
|
|
828
|
+
/** Execute the round transaction and apply per-rule rejection tracking.
|
|
829
|
+
* MUTATES: session.roundSnapshot, session.consecutiveRejections,
|
|
830
|
+
* session.lastRejectionCheck, session.lastSelfEval,
|
|
831
|
+
* session.successTrajectory */
|
|
832
|
+
async executeRoundTransaction(session, selfEval, extractionFailed) {
|
|
833
|
+
const snapshot = session.roundSnapshot ?? prepareRoundTransaction(session.loopId, session.currentRound, session.evidenceBaseline ?? []);
|
|
834
|
+
const completed = await new RoundDriver(session.engine, this.backend).complete({
|
|
835
|
+
snapshot,
|
|
836
|
+
loopId: session.loopId,
|
|
837
|
+
task: session.task,
|
|
838
|
+
maxRounds: session.maxRounds,
|
|
839
|
+
selfEval,
|
|
840
|
+
extractionSucceeded: !extractionFailed,
|
|
841
|
+
lastSelfEval: session.lastSelfEval,
|
|
842
|
+
consecutiveRejections: session.consecutiveRejections,
|
|
843
|
+
successTrajectory: session.successTrajectory,
|
|
844
|
+
});
|
|
845
|
+
const outcome = completed.outcome;
|
|
846
|
+
const actualEvidence = completed.actualEvidence;
|
|
847
|
+
session.roundSnapshot = outcome.snapshot;
|
|
848
|
+
const pr = outcome.result;
|
|
849
|
+
policyMetrics.recordStrategyOutcome(session.loopId, session.currentLevel, pr, outcome.replayed);
|
|
850
|
+
// Per-rule rejection tracking: same-check rejections accumulate;
|
|
851
|
+
// a different rejection reason resets the counter.
|
|
852
|
+
if (pr.action === "reject" && pr.rejectionCheck) {
|
|
853
|
+
session.consecutiveRejections =
|
|
854
|
+
pr.rejectionCheck === session.lastRejectionCheck
|
|
855
|
+
? pr.newConsecutiveRejections
|
|
856
|
+
: 1;
|
|
857
|
+
session.lastRejectionCheck = pr.rejectionCheck;
|
|
418
858
|
}
|
|
419
859
|
else {
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
860
|
+
session.consecutiveRejections = pr.newConsecutiveRejections;
|
|
861
|
+
if (pr.action !== "reject")
|
|
862
|
+
session.lastRejectionCheck = "";
|
|
423
863
|
}
|
|
424
|
-
|
|
425
|
-
|
|
864
|
+
if (pr.newLastSelfEval)
|
|
865
|
+
session.lastSelfEval = pr.newLastSelfEval;
|
|
866
|
+
if (pr.shouldPushSuccessTrajectory) {
|
|
867
|
+
session.successTrajectory.push(pr.roundSuccess);
|
|
868
|
+
}
|
|
869
|
+
return { pr, verificationFlags: pr.verificationFlags, actualEvidence };
|
|
870
|
+
}
|
|
871
|
+
/** Build a rejection result: compile a retry prompt, persist, return.
|
|
872
|
+
* MUTATES: session.roundSnapshot, session.currentPrompt, session.currentLevel */
|
|
873
|
+
async buildRejectionResult(sessionId, session, pr, verificationFlags) {
|
|
874
|
+
const retryRequest = buildLoopRequest(session, undefined, verificationFlags);
|
|
875
|
+
const preparedRetry = await new RoundDriver(session.engine, this.backend).prepareRetry(retryRequest, session.roundSnapshot, pr.rejectionPrompt ?? "", session.consecutiveRejections);
|
|
876
|
+
if (!preparedRetry) {
|
|
426
877
|
session.status = "stalled";
|
|
878
|
+
session.currentPrompt = null;
|
|
427
879
|
this.save(session);
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
880
|
+
return {
|
|
881
|
+
sessionId,
|
|
882
|
+
round: session.currentRound,
|
|
883
|
+
roundId: session.roundSnapshot?.roundId,
|
|
884
|
+
prompt: null,
|
|
885
|
+
stopReason: "stalled",
|
|
886
|
+
stopDetail: "RoundDriver.prepareRetry returned null — the retry prompt could not be compiled.",
|
|
887
|
+
};
|
|
888
|
+
}
|
|
889
|
+
session.roundSnapshot = preparedRetry.snapshot;
|
|
890
|
+
session.currentPrompt = preparedRetry.prompt;
|
|
891
|
+
session.currentLevel = preparedRetry.level;
|
|
892
|
+
this.save(session);
|
|
893
|
+
return {
|
|
894
|
+
sessionId,
|
|
895
|
+
round: session.currentRound,
|
|
896
|
+
roundId: session.roundSnapshot.roundId,
|
|
897
|
+
prompt: preparedRetry.prompt,
|
|
898
|
+
level: preparedRetry.level,
|
|
899
|
+
enforcementAction: "reject",
|
|
900
|
+
enforcementReason: pr.enforcementReason,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
/** Build a termination result: persist stopped status, notify sinks.
|
|
904
|
+
* MUTATES: session.status, session.currentPrompt */
|
|
905
|
+
buildTerminationResult(sessionId, session, pr) {
|
|
906
|
+
session.status = "stopped";
|
|
907
|
+
session.currentPrompt = null;
|
|
908
|
+
this.save(session);
|
|
909
|
+
void this.notifyTerminal(session, "enforcement_terminated");
|
|
910
|
+
logEvent("session_end", {
|
|
911
|
+
sessionId, loopId: session.loopId,
|
|
912
|
+
stopReason: "enforcement_terminated", round: session.currentRound,
|
|
913
|
+
});
|
|
914
|
+
return {
|
|
915
|
+
sessionId,
|
|
916
|
+
round: session.currentRound,
|
|
917
|
+
roundId: session.roundSnapshot?.roundId,
|
|
918
|
+
prompt: null,
|
|
919
|
+
stopReason: "enforcement_terminated",
|
|
920
|
+
stopDetail: pr.enforcementReason ?? "The enforcement gate terminated the loop.",
|
|
921
|
+
enforcementAction: "terminate",
|
|
922
|
+
enforcementReason: pr.enforcementReason,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
/** Build a stop result: persist stopped/stalled status, notify sinks.
|
|
926
|
+
* MUTATES: session.status, session.currentPrompt */
|
|
927
|
+
buildStopResult(sessionId, session, pr) {
|
|
928
|
+
const reason = pr.stopReason ?? "stalled";
|
|
929
|
+
session.status = reason === "stalled" ? "stalled" : "stopped";
|
|
930
|
+
session.currentPrompt = null;
|
|
931
|
+
this.save(session);
|
|
932
|
+
void this.notifyTerminal(session, reason);
|
|
933
|
+
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason: reason, round: session.currentRound });
|
|
934
|
+
return {
|
|
935
|
+
sessionId, round: session.currentRound,
|
|
936
|
+
roundId: session.roundSnapshot?.roundId,
|
|
937
|
+
prompt: null, stopReason: reason,
|
|
938
|
+
stopDetail: pr.stopReason === "circuit_breaker"
|
|
939
|
+
? `${pr.roundSuccess ? "No" : "All"} recent rounds failed, triggering the circuit breaker.`
|
|
940
|
+
: pr.stopReason === "max_rounds"
|
|
941
|
+
? "The configured maximum number of rounds has been reached."
|
|
942
|
+
: `Loop stopped: ${reason}.`,
|
|
943
|
+
roundSuccess: pr.roundSuccess,
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
/** Compile the next round's prompt and advance the session.
|
|
947
|
+
* Includes the commit fence (pause/delete race guard) and context provider.
|
|
948
|
+
* MUTATES: session.currentRound, session.currentPrompt, session.currentLevel,
|
|
949
|
+
* session.evidenceBaseline, session.roundSnapshot */
|
|
950
|
+
async advanceToNextRound(sessionId, session, selfEval, pr, verificationFlags, actualEvidence) {
|
|
951
|
+
const roundSuccess = pr.roundSuccess;
|
|
952
|
+
session.currentRound++;
|
|
953
|
+
session.currentPrompt = null;
|
|
954
|
+
// v1.8: Memory injection — tier-aware external context.
|
|
955
|
+
let externalCtx = "";
|
|
956
|
+
if (this.contextProvider) {
|
|
957
|
+
try {
|
|
958
|
+
externalCtx = (await this.contextProvider({
|
|
457
959
|
loopId: session.loopId,
|
|
458
960
|
round: session.currentRound,
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
sessionId,
|
|
464
|
-
round: session.currentRound,
|
|
465
|
-
prompt: rejectionPrompt,
|
|
466
|
-
enforcementAction: "reject",
|
|
467
|
-
enforcementReason: enforceResult.reason,
|
|
468
|
-
};
|
|
961
|
+
task: session.task,
|
|
962
|
+
domain: "",
|
|
963
|
+
lastEvaluation: selfEval,
|
|
964
|
+
})).trim();
|
|
469
965
|
}
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
void this.doWriteback(session, "enforcement_terminated");
|
|
474
|
-
logEvent("session_end", {
|
|
475
|
-
sessionId, loopId: session.loopId,
|
|
476
|
-
stopReason: "enforcement_terminated", round: session.currentRound,
|
|
477
|
-
});
|
|
478
|
-
return {
|
|
479
|
-
sessionId,
|
|
966
|
+
catch {
|
|
967
|
+
logEvent("context_provider_error", {
|
|
968
|
+
loopId: session.loopId,
|
|
480
969
|
round: session.currentRound,
|
|
481
|
-
prompt: null,
|
|
482
|
-
stopReason: "enforcement_terminated",
|
|
483
|
-
enforcementAction: "terminate",
|
|
484
|
-
enforcementReason: enforceResult.reason,
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
// Accept: reset rejection counter and continue
|
|
488
|
-
session.consecutiveRejections = 0;
|
|
489
|
-
}
|
|
490
|
-
// 2. Record feedback (flushes immediately so next compile sees success flags)
|
|
491
|
-
const roundSuccess = session.engine.autoFeedback(selfEval, session.loopId, session.currentRound, session.task);
|
|
492
|
-
// Contradicted verdict: skip success trend
|
|
493
|
-
if (gateVerdict !== "contradicted") {
|
|
494
|
-
session.successTrajectory.push(roundSuccess);
|
|
495
|
-
}
|
|
496
|
-
// Note: feedback vault entry is always persisted via autoFeedback above.
|
|
497
|
-
// Only the in-memory trend is skipped — the raw data stays for audit.
|
|
498
|
-
// Store selfEval for next round's verification gate.
|
|
499
|
-
// NOTE: lastSelfEval is intentionally NOT persisted to vault (save()).
|
|
500
|
-
// A resumed session starts with lastSelfEval=undefined, which means the
|
|
501
|
-
// first round after resumption runs with degraded verification (most
|
|
502
|
-
// checks skip without prevSelfEval). The gate recovers on the next round.
|
|
503
|
-
session.lastSelfEval = selfEval;
|
|
504
|
-
// Build deprecated quality alias for backward compat
|
|
505
|
-
const deprecatedQuality = roundSuccess ? 5 : 1;
|
|
506
|
-
// 3. Stop conditions (extraction-first order — see memory)
|
|
507
|
-
if (extractionFailed) {
|
|
508
|
-
session.status = "stalled";
|
|
509
|
-
this.save(session);
|
|
510
|
-
void this.doWriteback(session, "stalled");
|
|
511
|
-
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason: "stalled", round: session.currentRound });
|
|
512
|
-
return { sessionId, round: session.currentRound, prompt: null, stopReason: "stalled", roundSuccess, quality: deprecatedQuality };
|
|
513
|
-
}
|
|
514
|
-
if (!selfEval.should_continue) {
|
|
515
|
-
session.status = "stopped";
|
|
516
|
-
this.save(session);
|
|
517
|
-
void this.doWriteback(session, "task_complete");
|
|
518
|
-
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason: "task_complete", round: session.currentRound });
|
|
519
|
-
return { sessionId, round: session.currentRound, prompt: null, stopReason: "task_complete", roundSuccess, quality: deprecatedQuality };
|
|
520
|
-
}
|
|
521
|
-
if (session.engine.shouldBreak()) {
|
|
522
|
-
session.status = "stopped";
|
|
523
|
-
this.save(session);
|
|
524
|
-
void this.doWriteback(session, "circuit_breaker");
|
|
525
|
-
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason: "circuit_breaker", round: session.currentRound });
|
|
526
|
-
return { sessionId, round: session.currentRound, prompt: null, stopReason: "circuit_breaker", roundSuccess, quality: deprecatedQuality };
|
|
527
|
-
}
|
|
528
|
-
if (session.currentRound >= session.maxRounds) {
|
|
529
|
-
session.status = "stopped";
|
|
530
|
-
this.save(session);
|
|
531
|
-
void this.doWriteback(session, "max_rounds");
|
|
532
|
-
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason: "max_rounds", round: session.currentRound });
|
|
533
|
-
return { sessionId, round: session.currentRound, prompt: null, stopReason: "max_rounds", roundSuccess, quality: deprecatedQuality };
|
|
534
|
-
}
|
|
535
|
-
// 4. Compile next round
|
|
536
|
-
session.currentRound++;
|
|
537
|
-
// v1.8: Memory injection for phases 2/3 — tier-aware
|
|
538
|
-
let externalCtx = "";
|
|
539
|
-
const miPolicy = getPolicy().memory_injection;
|
|
540
|
-
if (miPolicy.enabled && this.memoryProvider) {
|
|
541
|
-
const allowedPhases = new Set(resolveAllowedPhases(session.maxRounds, miPolicy.round_tiers));
|
|
542
|
-
if (session.injectionCount < allowedPhases.size &&
|
|
543
|
-
session.currentRound - session.lastInjectionRound >= miPolicy.min_rounds_between_injections) {
|
|
544
|
-
const progress = selfEval.execution_evidence?.progress_estimate ?? -1;
|
|
545
|
-
const phase = resolveInjectionPhase(session.currentRound, session.injectionCount, allowedPhases, typeof progress === "number" ? progress : -1, session.phase2Triggered, session.phase3Triggered, {
|
|
546
|
-
phase2: miPolicy.phase_thresholds.phase2.threshold,
|
|
547
|
-
phase3: miPolicy.phase_thresholds.phase3.threshold,
|
|
548
970
|
});
|
|
549
|
-
if (phase === 2)
|
|
550
|
-
session.phase2Triggered = true;
|
|
551
|
-
if (phase === 3)
|
|
552
|
-
session.phase3Triggered = true;
|
|
553
|
-
if (phase !== 0) {
|
|
554
|
-
try {
|
|
555
|
-
const accCtx = buildAccumulatedMemoryContext(selfEval);
|
|
556
|
-
const ctx = {
|
|
557
|
-
loopId: session.loopId,
|
|
558
|
-
round: session.currentRound,
|
|
559
|
-
task: session.task,
|
|
560
|
-
domain: "",
|
|
561
|
-
phase,
|
|
562
|
-
progressEstimate: typeof progress === "number" && progress >= 0 ? progress : -1,
|
|
563
|
-
accumulatedContext: accCtx,
|
|
564
|
-
};
|
|
565
|
-
const rawContext = await this.memoryProvider(ctx);
|
|
566
|
-
if (rawContext?.trim()) {
|
|
567
|
-
// Dedup
|
|
568
|
-
const newTokens = tokenize(rawContext);
|
|
569
|
-
let isDuplicate = false;
|
|
570
|
-
for (const old of session.injectedContexts) {
|
|
571
|
-
if (jaccard(newTokens, tokenize(old)) > miPolicy.dedup_threshold) {
|
|
572
|
-
isDuplicate = true;
|
|
573
|
-
break;
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
if (!isDuplicate) {
|
|
577
|
-
externalCtx = rawContext.trim().slice(0, miPolicy.max_context_length);
|
|
578
|
-
session.injectionCount++;
|
|
579
|
-
session.lastInjectionRound = session.currentRound;
|
|
580
|
-
session.injectedContexts.push(externalCtx);
|
|
581
|
-
logEvent("memory_injected", {
|
|
582
|
-
loopId: session.loopId, round: session.currentRound,
|
|
583
|
-
phase, injectionCount: session.injectionCount,
|
|
584
|
-
contextLength: externalCtx.length,
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
catch {
|
|
590
|
-
logEvent("memory_provider_error", {
|
|
591
|
-
loopId: session.loopId, round: session.currentRound,
|
|
592
|
-
});
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
971
|
}
|
|
596
972
|
}
|
|
597
|
-
|
|
973
|
+
// Commit fence: pause()/delete() may have run while the context provider
|
|
974
|
+
// was awaited. Do not compile if the session is no longer running.
|
|
975
|
+
if (this.sessions.get(sessionId) !== session || session.status !== "running") {
|
|
976
|
+
return {
|
|
977
|
+
sessionId,
|
|
978
|
+
round: session.currentRound,
|
|
979
|
+
prompt: null,
|
|
980
|
+
stopReason: session.status,
|
|
981
|
+
stopDetail: `Session is no longer running (status: ${session.status}). It was paused or deleted while the context provider was running.`,
|
|
982
|
+
roundSuccess,
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
const request = buildLoopRequest(session, selfEval, verificationFlags);
|
|
598
986
|
if (externalCtx) {
|
|
599
987
|
request.external_context = externalCtx;
|
|
600
988
|
}
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
989
|
+
const prepared = await new RoundDriver(session.engine, this.backend).prepare(request, session.loopId, session.currentRound);
|
|
990
|
+
const nextPrompt = prepared?.prompt ?? null;
|
|
991
|
+
const nextLevel = prepared?.level ?? "l2";
|
|
992
|
+
const nextBaseline = prepared?.evidenceBaseline ?? actualEvidence;
|
|
993
|
+
session.evidenceBaseline = nextBaseline;
|
|
994
|
+
session.roundSnapshot = prepared?.snapshot ?? prepareRoundTransaction(session.loopId, session.currentRound, nextBaseline);
|
|
995
|
+
session.currentPrompt = nextPrompt;
|
|
996
|
+
session.currentLevel = nextLevel;
|
|
997
|
+
policyMetrics.recordStrategy(session.loopId, nextLevel);
|
|
609
998
|
this.save(session);
|
|
610
999
|
return {
|
|
611
1000
|
sessionId,
|
|
612
1001
|
round: session.currentRound,
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
level:
|
|
1002
|
+
roundId: session.roundSnapshot.roundId,
|
|
1003
|
+
prompt: nextPrompt,
|
|
1004
|
+
level: nextLevel,
|
|
616
1005
|
roundSuccess,
|
|
617
|
-
|
|
618
|
-
warnings: parseWarnings(result.response?.prompt ?? null),
|
|
1006
|
+
warnings: parseWarnings(nextPrompt),
|
|
619
1007
|
};
|
|
620
1008
|
}
|
|
1009
|
+
async advanceUnlocked(sessionId, output, preExtractedEval) {
|
|
1010
|
+
// ── 1. Validate session ──────────────────────────────────────────────
|
|
1011
|
+
const session = this.sessions.get(sessionId);
|
|
1012
|
+
if (!session)
|
|
1013
|
+
return { sessionId, round: 0, prompt: null, stopReason: "session_not_found", stopDetail: "No session exists with this ID. It may have expired, been deleted, or never existed." };
|
|
1014
|
+
if (session.status !== "running") {
|
|
1015
|
+
return { sessionId, round: session.currentRound, prompt: null, stopReason: session.status, stopDetail: `Session is ${session.status}. Use loopforge_resume to restart a paused session, or loopforge_start for a new task.` };
|
|
1016
|
+
}
|
|
1017
|
+
// ── 2. Extract self-evaluation ───────────────────────────────────────
|
|
1018
|
+
const extracted = this.extractEvaluation(output, preExtractedEval);
|
|
1019
|
+
if (!extracted) {
|
|
1020
|
+
session.status = "stalled";
|
|
1021
|
+
this.save(session);
|
|
1022
|
+
void this.notifyTerminal(session, "stalled");
|
|
1023
|
+
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason: "stalled", round: session.currentRound });
|
|
1024
|
+
return { sessionId, round: session.currentRound, prompt: null, stopReason: "stalled", stopDetail: "Self-evaluation could not be extracted from agent output. Both structured regex and heuristic fallback returned null.", roundSuccess: false };
|
|
1025
|
+
}
|
|
1026
|
+
const { selfEval, extractionFailed } = extracted;
|
|
1027
|
+
// ── 3. Execute round transaction ─────────────────────────────────────
|
|
1028
|
+
const tx = await this.executeRoundTransaction(session, selfEval, extractionFailed);
|
|
1029
|
+
// ── 4. Route disposition ─────────────────────────────────────────────
|
|
1030
|
+
if (tx.pr.action === "reject") {
|
|
1031
|
+
return this.buildRejectionResult(sessionId, session, tx.pr, tx.verificationFlags);
|
|
1032
|
+
}
|
|
1033
|
+
// Accepted rounds advance the before-snapshot baseline. Rejected rounds
|
|
1034
|
+
// retain the original baseline so their retry remains zero-commit.
|
|
1035
|
+
session.evidenceBaseline = tx.actualEvidence;
|
|
1036
|
+
if (tx.pr.action === "terminate") {
|
|
1037
|
+
return this.buildTerminationResult(sessionId, session, tx.pr);
|
|
1038
|
+
}
|
|
1039
|
+
if (tx.pr.action === "stop") {
|
|
1040
|
+
return this.buildStopResult(sessionId, session, tx.pr);
|
|
1041
|
+
}
|
|
1042
|
+
// ── 5. Continue — compile next round ─────────────────────────────────
|
|
1043
|
+
return this.advanceToNextRound(sessionId, session, selfEval, tx.pr, tx.verificationFlags, tx.actualEvidence);
|
|
1044
|
+
}
|
|
621
1045
|
/** Write back loop knowledge to long-term memory.
|
|
622
1046
|
* Uses shared base builder from policy.ts. Called when a loop terminates. */
|
|
623
|
-
async
|
|
624
|
-
if (
|
|
625
|
-
return;
|
|
626
|
-
const wp = getPolicy().memory_writeback;
|
|
627
|
-
if (!wp.enabled)
|
|
1047
|
+
async notifyTerminal(session, stopReason) {
|
|
1048
|
+
if (this.terminalSinks.size === 0)
|
|
628
1049
|
return;
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
...(lastEval.discovered_constraints ?? []),
|
|
640
|
-
].slice(0, wp.max_discoveries_in_project)
|
|
641
|
-
: [];
|
|
642
|
-
const base = buildBaseMemoryWriteback({
|
|
643
|
-
loopId: session.loopId,
|
|
644
|
-
task: session.task,
|
|
645
|
-
stopReason,
|
|
646
|
-
roundsCompleted: session.currentRound,
|
|
647
|
-
discoveries,
|
|
648
|
-
});
|
|
649
|
-
const payload = {
|
|
650
|
-
...base,
|
|
651
|
-
feedbackEntries: [],
|
|
652
|
-
};
|
|
653
|
-
await this.memoryWriter(payload);
|
|
654
|
-
logEvent("memory_writeback", {
|
|
655
|
-
loopId: session.loopId, stopReason,
|
|
656
|
-
feedbackCount: 0,
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
catch {
|
|
660
|
-
logEvent("memory_writeback_error", {
|
|
661
|
-
loopId: session.loopId, stopReason,
|
|
662
|
-
});
|
|
663
|
-
}
|
|
1050
|
+
const event = {
|
|
1051
|
+
loopId: session.loopId,
|
|
1052
|
+
task: session.task,
|
|
1053
|
+
success: stopReason === "completed",
|
|
1054
|
+
stopReason: stopReason,
|
|
1055
|
+
roundsCompleted: session.currentRound,
|
|
1056
|
+
successTrajectory: [...session.successTrajectory],
|
|
1057
|
+
lastEvaluation: session.lastSelfEval,
|
|
1058
|
+
};
|
|
1059
|
+
await Promise.allSettled([...this.terminalSinks].map((sink) => Promise.resolve(sink(event))));
|
|
664
1060
|
}
|
|
665
1061
|
/** Replay timeline for a session — creates ReplayBackend from the stored backend. */
|
|
666
1062
|
replayTimeline(sessionId) {
|
|
667
1063
|
const session = this.sessions.get(sessionId);
|
|
668
1064
|
if (!session)
|
|
669
1065
|
return null;
|
|
670
|
-
const
|
|
671
|
-
const replay = new ReplayBackend(vaultBackend);
|
|
1066
|
+
const replay = new ReplayBackend(this.backend);
|
|
672
1067
|
return replay.timeline(session.loopId);
|
|
673
1068
|
}
|
|
674
1069
|
}
|