loopforge 1.15.1 → 2.0.0
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 +3 -9
- package/dist/backends/interface.d.ts.map +1 -1
- package/dist/backends/interface.js +1 -5
- 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 +7 -4
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +78 -138
- package/dist/engine.js.map +1 -1
- package/dist/evidence-provider.d.ts +113 -0
- package/dist/evidence-provider.d.ts.map +1 -0
- package/dist/evidence-provider.js +419 -0
- package/dist/evidence-provider.js.map +1 -0
- package/dist/index.d.ts +18 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -9
- 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 +30 -0
- package/dist/interop.d.ts.map +1 -0
- package/dist/interop.js +29 -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 +303 -2206
- package/dist/loop-compiler.js.map +1 -1
- package/dist/loop-store.d.ts +82 -0
- package/dist/loop-store.d.ts.map +1 -0
- package/dist/loop-store.js +332 -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 +75 -19
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +707 -361
- 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 +225 -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 +53 -140
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +119 -184
- 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 +257 -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 +88 -93
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +23 -39
- 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/runtime.d.ts +33 -19
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +355 -336
- package/dist/runtime.js.map +1 -1
- package/dist/storage.d.ts +38 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +201 -0
- package/dist/storage.js.map +1 -0
- package/dist/verification-gate.d.ts +33 -7
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +195 -7
- package/dist/verification-gate.js.map +1 -1
- package/loop_policy.json +22 -48
- 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/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,25 @@
|
|
|
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 } 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
22
|
function buildLoopRequest(session, lastEval, _lastQuality, // deprecated — kept for backward compat
|
|
21
23
|
verificationFlags) {
|
|
22
24
|
const req = {
|
|
23
25
|
task: session.task,
|
|
24
26
|
mode: Mode.LOOP_COMPILE,
|
|
25
|
-
vault_config: makeVaultConfig(),
|
|
26
27
|
feedback: null,
|
|
27
28
|
skill_name: null,
|
|
28
29
|
task_id: null,
|
|
@@ -59,19 +60,12 @@ verificationFlags) {
|
|
|
59
60
|
// v1.10: Checkpoint boundary
|
|
60
61
|
compression_checkpoint: lastEval.compression_checkpoint ?? false,
|
|
61
62
|
checkpoint_label: lastEval.checkpoint_label ?? "",
|
|
63
|
+
// v1.16: Agent's declared next action
|
|
64
|
+
next_action: lastEval.next_action,
|
|
62
65
|
};
|
|
63
66
|
}
|
|
64
67
|
return req;
|
|
65
68
|
}
|
|
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
69
|
function parseWarnings(prompt) {
|
|
76
70
|
if (!prompt)
|
|
77
71
|
return [];
|
|
@@ -89,85 +83,166 @@ function parseWarnings(prompt) {
|
|
|
89
83
|
// ── SessionManager ─────────────────────────────────────────────────────────
|
|
90
84
|
export class SessionManager {
|
|
91
85
|
sessions = new Map();
|
|
86
|
+
/** Serializes state transitions for each session. */
|
|
87
|
+
sessionQueues = new Map();
|
|
92
88
|
backend;
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
89
|
+
sessionStore;
|
|
90
|
+
ownerId = `${process.pid}:${randomUUID()}`;
|
|
91
|
+
leaseMs;
|
|
92
|
+
leaseRenewIntervalMs;
|
|
93
|
+
leaseTimer = null;
|
|
94
|
+
checkpointSinks = new Set();
|
|
95
|
+
/** Explicit context provider; never auto-discovered. */
|
|
96
|
+
contextProvider;
|
|
97
|
+
terminalSinks = new Set();
|
|
98
|
+
constructor(storeOrBackend, sessionStore) {
|
|
99
|
+
const resolvedBackend = storeOrBackend
|
|
100
|
+
? "readSession" in storeOrBackend
|
|
101
|
+
? new LoopStoreBackend(storeOrBackend)
|
|
102
|
+
: storeOrBackend
|
|
103
|
+
: new LoopStoreBackend(new FileLoopStore(getPolicy().backend.root_dir));
|
|
104
|
+
this.backend = resolvedBackend;
|
|
105
|
+
this.sessionStore = sessionStore ?? new VaultSessionStateStore(resolvedBackend);
|
|
106
|
+
const mcpPolicy = getPolicy().mcp;
|
|
107
|
+
this.leaseMs = Math.max(1, mcpPolicy.session_lease_ms);
|
|
108
|
+
this.leaseRenewIntervalMs = Math.max(1, Math.min(mcpPolicy.session_lease_renew_interval_ms, this.leaseMs));
|
|
109
|
+
if (this.sessionStore?.renewLease) {
|
|
110
|
+
this.leaseTimer = setInterval(() => this.renewOwnedLeases(), this.leaseRenewIntervalMs);
|
|
111
|
+
this.leaseTimer.unref?.();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Stable process-local owner token used for cross-process session leases. */
|
|
115
|
+
getOwnerId() {
|
|
116
|
+
return this.ownerId;
|
|
117
|
+
}
|
|
118
|
+
/** Subscribe an external checkpointer; sink failures are isolated. */
|
|
119
|
+
addCheckpointSink(sink) {
|
|
120
|
+
this.checkpointSinks.add(sink);
|
|
121
|
+
return () => this.checkpointSinks.delete(sink);
|
|
122
|
+
}
|
|
123
|
+
addTerminalSink(sink) {
|
|
124
|
+
this.terminalSinks.add(sink);
|
|
125
|
+
return () => this.terminalSinks.delete(sink);
|
|
126
|
+
}
|
|
127
|
+
/** Release owned sessions and stop lease maintenance. */
|
|
128
|
+
close() {
|
|
129
|
+
if (this.leaseTimer)
|
|
130
|
+
clearInterval(this.leaseTimer);
|
|
131
|
+
this.leaseTimer = null;
|
|
132
|
+
for (const session of this.sessions.values()) {
|
|
133
|
+
this.sessionStore?.releaseLease?.(session.loopId, this.ownerId);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
findSessionEntry(loopId) {
|
|
137
|
+
return this.sessionStore?.load(loopId);
|
|
138
|
+
}
|
|
139
|
+
claimSessionEntry(loopId) {
|
|
140
|
+
if (this.sessionStore?.acquireLease) {
|
|
141
|
+
return this.sessionStore.acquireLease(loopId, this.ownerId, this.leaseMs);
|
|
142
|
+
}
|
|
143
|
+
return this.findSessionEntry(loopId);
|
|
144
|
+
}
|
|
145
|
+
renewSessionLease(loopId) {
|
|
146
|
+
if (!this.sessionStore?.renewLease)
|
|
147
|
+
return true;
|
|
148
|
+
try {
|
|
149
|
+
return this.sessionStore.renewLease(loopId, this.ownerId, this.leaseMs);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
renewOwnedLeases() {
|
|
156
|
+
for (const session of this.sessions.values()) {
|
|
157
|
+
if (session.status === "running")
|
|
158
|
+
this.renewSessionLease(session.loopId);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
leaseConflictResult(loopId, entry) {
|
|
162
|
+
const lineage = (entry?.loop_lineage ?? {});
|
|
163
|
+
return {
|
|
164
|
+
sessionId: "",
|
|
165
|
+
round: typeof lineage.current_round === "number" ? lineage.current_round : 0,
|
|
166
|
+
prompt: null,
|
|
167
|
+
stopReason: `session_owned_elsewhere:${loopId}`,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
async withSessionQueue(sessionId, work) {
|
|
171
|
+
const previous = this.sessionQueues.get(sessionId) ?? Promise.resolve();
|
|
172
|
+
let release;
|
|
173
|
+
const gate = new Promise((resolve) => {
|
|
174
|
+
release = resolve;
|
|
175
|
+
});
|
|
176
|
+
const tail = previous.then(() => gate);
|
|
177
|
+
this.sessionQueues.set(sessionId, tail);
|
|
178
|
+
await previous;
|
|
179
|
+
try {
|
|
180
|
+
return await work();
|
|
181
|
+
}
|
|
182
|
+
finally {
|
|
183
|
+
release();
|
|
184
|
+
if (this.sessionQueues.get(sessionId) === tail) {
|
|
185
|
+
this.sessionQueues.delete(sessionId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
99
188
|
}
|
|
100
189
|
async create(input) {
|
|
101
190
|
const sessionId = randomUUID();
|
|
102
191
|
const loopId = input.loopId ?? randomUUID();
|
|
103
|
-
const engine = new LoopForgeEngine(
|
|
192
|
+
const engine = new LoopForgeEngine(this.backend);
|
|
104
193
|
const maxRounds = input.maxRounds ?? getPolicy().runtime.max_rounds;
|
|
105
194
|
// Populate extra fields for the first round
|
|
106
195
|
const request = buildLoopRequest({
|
|
107
196
|
sessionId, loopId, task: input.task, engine, currentRound: 1,
|
|
108
197
|
maxRounds, successTrajectory: [], status: "running", createdAt: Date.now(),
|
|
109
|
-
|
|
110
|
-
|
|
198
|
+
consecutiveRejections: 0,
|
|
199
|
+
lastRejectionCheck: "",
|
|
200
|
+
evidenceBaseline: [],
|
|
111
201
|
});
|
|
112
202
|
request.domain = input.domain ?? "";
|
|
113
203
|
request.plan_source = input.planSource ?? null;
|
|
114
204
|
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)) {
|
|
205
|
+
// Explicit context is requested by the MCP embedding; no auto-discovery.
|
|
206
|
+
if (this.contextProvider) {
|
|
119
207
|
try {
|
|
120
208
|
const ctx = {
|
|
121
209
|
loopId,
|
|
122
210
|
round: 1,
|
|
123
211
|
task: input.task,
|
|
124
212
|
domain: input.domain ?? "",
|
|
125
|
-
phase: 1,
|
|
126
|
-
progressEstimate: 0,
|
|
127
|
-
accumulatedContext: {
|
|
128
|
-
recurringIssues: [],
|
|
129
|
-
failedPatterns: [],
|
|
130
|
-
keyLessons: [],
|
|
131
|
-
remainingCriteria: [],
|
|
132
|
-
},
|
|
133
213
|
};
|
|
134
|
-
const rawContext = await this.
|
|
214
|
+
const rawContext = await this.contextProvider(ctx);
|
|
135
215
|
if (rawContext?.trim()) {
|
|
136
|
-
request.external_context = rawContext.trim()
|
|
137
|
-
logEvent("
|
|
138
|
-
loopId, round: 1,
|
|
216
|
+
request.external_context = rawContext.trim();
|
|
217
|
+
logEvent("external_context_loaded", {
|
|
218
|
+
loopId, round: 1,
|
|
139
219
|
contextLength: request.external_context.length,
|
|
140
220
|
});
|
|
141
221
|
}
|
|
142
222
|
}
|
|
143
223
|
catch {
|
|
144
|
-
//
|
|
145
|
-
logEvent("
|
|
224
|
+
// Provider failures are isolated from session creation.
|
|
225
|
+
logEvent("context_provider_error", { loopId, round: 1 });
|
|
146
226
|
}
|
|
147
227
|
}
|
|
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;
|
|
228
|
+
const prepared = await new RoundDriver(engine, this.backend).prepare(request, loopId, 1);
|
|
229
|
+
const initialPrompt = prepared?.prompt ?? null;
|
|
230
|
+
const initialLevel = prepared?.level ?? "l2";
|
|
231
|
+
const evidenceBaseline = prepared?.evidenceBaseline ?? [];
|
|
157
232
|
const session = {
|
|
158
233
|
sessionId, loopId, task: input.task, engine,
|
|
159
234
|
currentRound: 1, maxRounds, successTrajectory: [],
|
|
160
235
|
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
236
|
// v1.13: Enforcement gate state
|
|
168
237
|
consecutiveRejections: 0,
|
|
238
|
+
lastRejectionCheck: "",
|
|
239
|
+
evidenceBaseline,
|
|
240
|
+
roundSnapshot: prepared?.snapshot ?? prepareRoundTransaction(loopId, 1, evidenceBaseline),
|
|
241
|
+
currentPrompt: initialPrompt,
|
|
242
|
+
currentLevel: initialLevel,
|
|
169
243
|
};
|
|
170
244
|
this.sessions.set(sessionId, session);
|
|
245
|
+
policyMetrics.recordStrategy(loopId, initialLevel);
|
|
171
246
|
// Persist to vault for cross-process recovery
|
|
172
247
|
this.save(session);
|
|
173
248
|
logEvent("session_start", {
|
|
@@ -179,99 +254,382 @@ export class SessionManager {
|
|
|
179
254
|
return {
|
|
180
255
|
sessionId,
|
|
181
256
|
round: 1,
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
level:
|
|
257
|
+
roundId: session.roundSnapshot?.roundId,
|
|
258
|
+
prompt: initialPrompt,
|
|
259
|
+
level: initialLevel,
|
|
185
260
|
roundSuccess: false,
|
|
186
261
|
quality: 0,
|
|
187
|
-
warnings: parseWarnings(
|
|
262
|
+
warnings: parseWarnings(initialPrompt),
|
|
188
263
|
};
|
|
189
264
|
}
|
|
190
265
|
get(sessionId) {
|
|
191
266
|
return this.sessions.get(sessionId);
|
|
192
267
|
}
|
|
268
|
+
getLeaseStatus(loopId) {
|
|
269
|
+
const entry = this.findSessionEntry(loopId);
|
|
270
|
+
if (!entry)
|
|
271
|
+
return null;
|
|
272
|
+
const lineage = (entry.loop_lineage ?? {});
|
|
273
|
+
const owner = typeof lineage.lease_owner === "string"
|
|
274
|
+
? lineage.lease_owner
|
|
275
|
+
: "";
|
|
276
|
+
const ownerPid = owner.match(/^(\d+):/)?.[1];
|
|
277
|
+
return {
|
|
278
|
+
ownedByThisProcess: owner === this.ownerId,
|
|
279
|
+
ownerPid: ownerPid ? Number(ownerPid) : null,
|
|
280
|
+
expiresAt: typeof lineage.lease_expires_at === "number"
|
|
281
|
+
? new Date(lineage.lease_expires_at).toISOString()
|
|
282
|
+
: null,
|
|
283
|
+
epoch: typeof lineage.lease_epoch === "number"
|
|
284
|
+
? lineage.lease_epoch
|
|
285
|
+
: 0,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
193
288
|
delete(sessionId) {
|
|
194
289
|
const session = this.sessions.get(sessionId);
|
|
195
290
|
if (!session)
|
|
196
291
|
return false;
|
|
197
292
|
session.status = "stopped";
|
|
198
|
-
|
|
293
|
+
this.save(session); // Persist stopped status to vault so autoResumeAll won't resurrect
|
|
294
|
+
void this.notifyTerminal(session, "cancelled");
|
|
199
295
|
this.sessions.delete(sessionId);
|
|
200
296
|
logEvent("session_end", {
|
|
201
297
|
sessionId,
|
|
202
298
|
loopId: session.loopId,
|
|
203
|
-
stopReason: "
|
|
299
|
+
stopReason: "cancelled",
|
|
204
300
|
roundsCompleted: session.currentRound,
|
|
205
301
|
});
|
|
206
302
|
return true;
|
|
207
303
|
}
|
|
304
|
+
/** v1.18: Pause a running session. The session state is persisted to
|
|
305
|
+
* vault so it survives process restarts. Returns the session status.
|
|
306
|
+
* Paused sessions cannot be advanced — they must be resumed first. */
|
|
307
|
+
pause(sessionId) {
|
|
308
|
+
const session = this.sessions.get(sessionId);
|
|
309
|
+
if (!session)
|
|
310
|
+
return { sessionId, round: 0, status: "not_found" };
|
|
311
|
+
if (session.status !== "running") {
|
|
312
|
+
return { sessionId, round: session.currentRound, status: session.status };
|
|
313
|
+
}
|
|
314
|
+
session.status = "paused";
|
|
315
|
+
this.save(session);
|
|
316
|
+
logEvent("session_paused", {
|
|
317
|
+
sessionId,
|
|
318
|
+
loopId: session.loopId,
|
|
319
|
+
round: session.currentRound,
|
|
320
|
+
});
|
|
321
|
+
return { sessionId, round: session.currentRound, status: "paused" };
|
|
322
|
+
}
|
|
323
|
+
restoredPromptResult(session) {
|
|
324
|
+
if (!session.currentPrompt)
|
|
325
|
+
return null;
|
|
326
|
+
return {
|
|
327
|
+
sessionId: session.sessionId,
|
|
328
|
+
round: session.currentRound,
|
|
329
|
+
roundId: session.roundSnapshot?.roundId,
|
|
330
|
+
prompt: session.currentPrompt,
|
|
331
|
+
level: session.currentLevel ?? "l2",
|
|
332
|
+
quality: 0,
|
|
333
|
+
roundSuccess: undefined,
|
|
334
|
+
warnings: parseWarnings(session.currentPrompt),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/** Reconcile the crash window where feedback committed but session_state
|
|
338
|
+
* still points at the old prompt. Returns null when no commit is pending. */
|
|
339
|
+
reconcileCommittedRound(session) {
|
|
340
|
+
if (!session.roundSnapshot)
|
|
341
|
+
return null;
|
|
342
|
+
const recovered = new RoundDriver(session.engine, this.backend).recover(session.roundSnapshot);
|
|
343
|
+
if (!recovered)
|
|
344
|
+
return null;
|
|
345
|
+
const pr = recovered.result;
|
|
346
|
+
session.roundSnapshot = recovered.snapshot;
|
|
347
|
+
// Replay the committed counter — but with per-rule tracking.
|
|
348
|
+
if (pr.action === "reject" && pr.rejectionCheck) {
|
|
349
|
+
session.consecutiveRejections =
|
|
350
|
+
pr.rejectionCheck === session.lastRejectionCheck
|
|
351
|
+
? pr.newConsecutiveRejections
|
|
352
|
+
: 1;
|
|
353
|
+
session.lastRejectionCheck = pr.rejectionCheck;
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
session.consecutiveRejections = pr.newConsecutiveRejections;
|
|
357
|
+
if (pr.action !== "reject")
|
|
358
|
+
session.lastRejectionCheck = "";
|
|
359
|
+
}
|
|
360
|
+
if (pr.newLastSelfEval)
|
|
361
|
+
session.lastSelfEval = pr.newLastSelfEval;
|
|
362
|
+
if (pr.shouldPushSuccessTrajectory &&
|
|
363
|
+
session.successTrajectory.length < session.currentRound) {
|
|
364
|
+
session.successTrajectory.push(pr.roundSuccess);
|
|
365
|
+
}
|
|
366
|
+
session.currentPrompt = null;
|
|
367
|
+
if (pr.action === "stop" || pr.action === "terminate") {
|
|
368
|
+
const reason = pr.action === "terminate"
|
|
369
|
+
? "enforcement_terminated"
|
|
370
|
+
: pr.stopReason ?? "stalled";
|
|
371
|
+
session.status = reason === "stalled" ? "stalled" : "stopped";
|
|
372
|
+
this.save(session);
|
|
373
|
+
void this.notifyTerminal(session, reason);
|
|
374
|
+
return {
|
|
375
|
+
sessionId: session.sessionId,
|
|
376
|
+
round: session.currentRound,
|
|
377
|
+
roundId: recovered.snapshot.roundId,
|
|
378
|
+
prompt: null,
|
|
379
|
+
stopReason: reason,
|
|
380
|
+
roundSuccess: pr.roundSuccess,
|
|
381
|
+
quality: pr.roundSuccess ? 5 : 1,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (pr.action !== "continue")
|
|
385
|
+
return null;
|
|
386
|
+
session.currentRound++;
|
|
387
|
+
const request = buildLoopRequest(session, pr.newLastSelfEval, pr.roundSuccess ? 5 : 1, pr.verificationFlags);
|
|
388
|
+
const prepared = new RoundDriver(session.engine, this.backend).prepareSync(request, session.loopId, session.currentRound);
|
|
389
|
+
if (!prepared) {
|
|
390
|
+
session.status = "stalled";
|
|
391
|
+
this.save(session);
|
|
392
|
+
return {
|
|
393
|
+
sessionId: session.sessionId,
|
|
394
|
+
round: session.currentRound,
|
|
395
|
+
prompt: null,
|
|
396
|
+
stopReason: "stalled",
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
const prompt = prepared.prompt;
|
|
400
|
+
session.evidenceBaseline = prepared.evidenceBaseline;
|
|
401
|
+
session.roundSnapshot = prepared.snapshot;
|
|
402
|
+
session.currentPrompt = prompt;
|
|
403
|
+
session.currentLevel = prepared.level;
|
|
404
|
+
policyMetrics.recordStrategy(session.loopId, session.currentLevel);
|
|
405
|
+
this.save(session);
|
|
406
|
+
return {
|
|
407
|
+
sessionId: session.sessionId,
|
|
408
|
+
round: session.currentRound,
|
|
409
|
+
roundId: session.roundSnapshot.roundId,
|
|
410
|
+
prompt,
|
|
411
|
+
level: session.currentLevel,
|
|
412
|
+
roundSuccess: pr.roundSuccess,
|
|
413
|
+
quality: pr.roundSuccess ? 5 : 1,
|
|
414
|
+
warnings: parseWarnings(prompt),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
/** v1.18: Resume a paused session. Reconstructs from vault state and
|
|
418
|
+
* compiles the next prompt. Returns null if no paused session exists
|
|
419
|
+
* for this loopId. */
|
|
420
|
+
async unpause(loopId) {
|
|
421
|
+
const persistedEntry = this.findSessionEntry(loopId);
|
|
422
|
+
if (!persistedEntry)
|
|
423
|
+
return null;
|
|
424
|
+
const lineage = (persistedEntry.loop_lineage ?? {});
|
|
425
|
+
const status = lineage.status ?? "running";
|
|
426
|
+
if (status !== "paused")
|
|
427
|
+
return null;
|
|
428
|
+
const sessionEntry = this.claimSessionEntry(loopId);
|
|
429
|
+
if (!sessionEntry)
|
|
430
|
+
return this.leaseConflictResult(loopId, persistedEntry);
|
|
431
|
+
const session = this.reconstructSession(sessionEntry, true);
|
|
432
|
+
if (!session)
|
|
433
|
+
return null;
|
|
434
|
+
// Set to running so advance() works
|
|
435
|
+
session.status = "running";
|
|
436
|
+
this.sessions.set(session.sessionId, session);
|
|
437
|
+
// Replace the sync-fallback evidence baseline with async evidence
|
|
438
|
+
// so resumed sessions don't silently drop async provider data.
|
|
439
|
+
try {
|
|
440
|
+
const asyncEvidence = await EvidenceCollector.fromPolicy().collectAsync({
|
|
441
|
+
loopId: session.loopId,
|
|
442
|
+
phase: "before",
|
|
443
|
+
});
|
|
444
|
+
if (asyncEvidence.length > 0) {
|
|
445
|
+
session.evidenceBaseline = asyncEvidence;
|
|
446
|
+
if (!session.roundSnapshot?.beforeEvidence?.length) {
|
|
447
|
+
session.roundSnapshot = prepareRoundTransaction(session.loopId, session.currentRound, asyncEvidence);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
// Async evidence is best-effort; fall back to sync baseline.
|
|
453
|
+
}
|
|
454
|
+
const reconciled = this.reconcileCommittedRound(session);
|
|
455
|
+
if (reconciled)
|
|
456
|
+
return reconciled;
|
|
457
|
+
const restored = this.restoredPromptResult(session);
|
|
458
|
+
if (restored) {
|
|
459
|
+
this.save(session);
|
|
460
|
+
return restored;
|
|
461
|
+
}
|
|
462
|
+
// Compile the next prompt from the current round state
|
|
463
|
+
const lcr = makeLoopCompileRequest({
|
|
464
|
+
loop_id: session.loopId,
|
|
465
|
+
round: session.currentRound,
|
|
466
|
+
goal_id: "",
|
|
467
|
+
task: session.task,
|
|
468
|
+
domain: undefined,
|
|
469
|
+
plan_source: null,
|
|
470
|
+
constraints_from_plan: [],
|
|
471
|
+
health_check_interval: 1,
|
|
472
|
+
});
|
|
473
|
+
const compileRequest = {
|
|
474
|
+
task: session.task,
|
|
475
|
+
mode: Mode.LOOP_COMPILE,
|
|
476
|
+
feedback: null,
|
|
477
|
+
skill_name: null,
|
|
478
|
+
task_id: null,
|
|
479
|
+
loop_id: lcr.loop_id,
|
|
480
|
+
round: lcr.round,
|
|
481
|
+
goal_id: lcr.goal_id,
|
|
482
|
+
domain: lcr.domain ?? "",
|
|
483
|
+
plan_source: lcr.plan_source ?? null,
|
|
484
|
+
constraints_from_plan: lcr.constraints_from_plan ?? [],
|
|
485
|
+
health_check_interval: lcr.health_check_interval,
|
|
486
|
+
max_rounds: session.maxRounds,
|
|
487
|
+
};
|
|
488
|
+
const prepared = await new RoundDriver(session.engine, this.backend).prepare(compileRequest, session.loopId, session.currentRound);
|
|
489
|
+
if (!prepared) {
|
|
490
|
+
session.status = "stopped";
|
|
491
|
+
this.save(session);
|
|
492
|
+
void this.notifyTerminal(session, "stalled");
|
|
493
|
+
return { sessionId: session.sessionId, round: session.currentRound, prompt: null, stopReason: "stalled" };
|
|
494
|
+
}
|
|
495
|
+
const prompt = prepared.prompt;
|
|
496
|
+
const level = prepared.level;
|
|
497
|
+
session.evidenceBaseline = prepared.evidenceBaseline;
|
|
498
|
+
session.roundSnapshot = prepared.snapshot;
|
|
499
|
+
session.currentPrompt = prompt;
|
|
500
|
+
session.currentLevel = level;
|
|
501
|
+
policyMetrics.recordStrategy(session.loopId, level);
|
|
502
|
+
this.save(session);
|
|
503
|
+
return {
|
|
504
|
+
sessionId: session.sessionId,
|
|
505
|
+
round: session.currentRound,
|
|
506
|
+
roundId: session.roundSnapshot.roundId,
|
|
507
|
+
prompt,
|
|
508
|
+
level,
|
|
509
|
+
quality: 0,
|
|
510
|
+
roundSuccess: undefined,
|
|
511
|
+
warnings: [],
|
|
512
|
+
};
|
|
513
|
+
}
|
|
208
514
|
/** 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. */
|
|
515
|
+
* The filtered vault and replacement entry are written once under the
|
|
516
|
+
* backend lock, so recovery never observes the old two-write gap. */
|
|
213
517
|
save(session) {
|
|
214
|
-
if (!this.
|
|
518
|
+
if (!this.sessionStore)
|
|
215
519
|
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
|
-
});
|
|
520
|
+
const leaseActive = session.status === "running";
|
|
521
|
+
const sessionEntry = {
|
|
522
|
+
task_id: `loop:${session.loopId}:session`,
|
|
523
|
+
task_type: "session_state",
|
|
524
|
+
timestamp: new Date().toISOString(),
|
|
525
|
+
loop_id: session.loopId,
|
|
526
|
+
task: session.task,
|
|
527
|
+
loop_lineage: {
|
|
528
|
+
session_id: session.sessionId,
|
|
529
|
+
current_round: session.currentRound,
|
|
530
|
+
max_rounds: session.maxRounds,
|
|
531
|
+
success_trajectory: session.successTrajectory,
|
|
532
|
+
status: session.status,
|
|
533
|
+
created_at: session.createdAt,
|
|
534
|
+
// v1.13: Enforcement gate state
|
|
535
|
+
consecutive_rejections: session.consecutiveRejections,
|
|
536
|
+
last_rejection_check: session.lastRejectionCheck,
|
|
537
|
+
// v1.19: durable round transaction state
|
|
538
|
+
round_snapshot: session.roundSnapshot ?? null,
|
|
539
|
+
last_self_eval: session.lastSelfEval ?? null,
|
|
540
|
+
current_prompt: session.currentPrompt ?? null,
|
|
541
|
+
current_level: session.currentLevel ?? "",
|
|
542
|
+
// v1.20: cross-process single-owner lease
|
|
543
|
+
lease_owner: leaseActive ? this.ownerId : "",
|
|
544
|
+
lease_expires_at: leaseActive ? Date.now() + this.leaseMs : 0,
|
|
545
|
+
},
|
|
245
546
|
};
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
547
|
+
this.sessionStore.save(sessionEntry, {
|
|
548
|
+
expectedLeaseOwner: this.ownerId,
|
|
549
|
+
});
|
|
550
|
+
if (this.checkpointSinks.size > 0) {
|
|
551
|
+
const checkpoint = createCognitiveCheckpoint(session, sessionEntry.timestamp);
|
|
552
|
+
for (const sink of this.checkpointSinks) {
|
|
553
|
+
try {
|
|
554
|
+
const pending = sink.save(checkpoint);
|
|
555
|
+
if (pending && typeof pending.then === "function") {
|
|
556
|
+
void pending.catch(() => undefined);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
// External adapters must not affect session durability.
|
|
561
|
+
}
|
|
562
|
+
}
|
|
250
563
|
}
|
|
251
|
-
|
|
252
|
-
|
|
564
|
+
}
|
|
565
|
+
/** Reconstruct a McpSession from a vault session_state entry.
|
|
566
|
+
* Returns null if the entry is not "running" status.
|
|
567
|
+
* Shared by resume() and autoResumeAll(). */
|
|
568
|
+
reconstructSession(entry, allowPaused = false) {
|
|
569
|
+
const lineage = (entry.loop_lineage ?? {});
|
|
570
|
+
const status = lineage.status ?? "running";
|
|
571
|
+
if (status !== "running" && !(allowPaused && status === "paused")) {
|
|
572
|
+
return null;
|
|
253
573
|
}
|
|
574
|
+
const loopId = entry.loop_id;
|
|
575
|
+
const currentRound = lineage.current_round ?? 1;
|
|
576
|
+
const successTrajectory = lineage.success_trajectory ?? lineage.quality_trajectory ?? [];
|
|
577
|
+
const task = entry.task ?? "";
|
|
578
|
+
const maxRounds = lineage.max_rounds ?? getPolicy().runtime.max_rounds;
|
|
579
|
+
const roundSnapshot = parseRoundTransactionSnapshot(lineage.round_snapshot);
|
|
580
|
+
const fallbackEvidence = EvidenceCollector.fromProviderNames(getPolicy().evidence.providers).collect();
|
|
581
|
+
const persistedEval = lineage.last_self_eval;
|
|
582
|
+
const lastSelfEval = persistedEval !== null &&
|
|
583
|
+
typeof persistedEval === "object" &&
|
|
584
|
+
!Array.isArray(persistedEval) &&
|
|
585
|
+
typeof persistedEval.success === "boolean" &&
|
|
586
|
+
typeof persistedEval.output_summary === "string"
|
|
587
|
+
? persistedEval
|
|
588
|
+
: undefined;
|
|
589
|
+
const engine = new LoopForgeEngine(this.backend);
|
|
590
|
+
return {
|
|
591
|
+
sessionId: typeof lineage.session_id === "string" && lineage.session_id
|
|
592
|
+
? lineage.session_id
|
|
593
|
+
: randomUUID(),
|
|
594
|
+
loopId, task, engine,
|
|
595
|
+
currentRound, maxRounds, successTrajectory,
|
|
596
|
+
status: status,
|
|
597
|
+
createdAt: lineage.created_at ?? Date.now(),
|
|
598
|
+
consecutiveRejections: lineage.consecutive_rejections ?? 0,
|
|
599
|
+
lastRejectionCheck: typeof lineage.last_rejection_check === "string"
|
|
600
|
+
? lineage.last_rejection_check
|
|
601
|
+
: "",
|
|
602
|
+
evidenceBaseline: roundSnapshot?.beforeEvidence ?? fallbackEvidence,
|
|
603
|
+
roundSnapshot: roundSnapshot ?? prepareRoundTransaction(loopId, currentRound, fallbackEvidence),
|
|
604
|
+
lastSelfEval,
|
|
605
|
+
currentPrompt: typeof lineage.current_prompt === "string"
|
|
606
|
+
? lineage.current_prompt
|
|
607
|
+
: null,
|
|
608
|
+
currentLevel: typeof lineage.current_level === "string"
|
|
609
|
+
? lineage.current_level
|
|
610
|
+
: undefined,
|
|
611
|
+
};
|
|
254
612
|
}
|
|
255
613
|
/** Resume a loop from vault state.
|
|
256
614
|
* Reconstructs the session and compiles the prompt for the next round.
|
|
257
615
|
* Returns null if no session_state entry exists for this loopId. */
|
|
258
616
|
resume(loopId) {
|
|
259
|
-
|
|
617
|
+
const persistedEntry = this.findSessionEntry(loopId);
|
|
618
|
+
if (!persistedEntry)
|
|
260
619
|
return null;
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
620
|
+
const persistedLineage = (persistedEntry.loop_lineage ?? {});
|
|
621
|
+
const persistedStatus = persistedLineage.status ?? "running";
|
|
622
|
+
const sessionEntry = persistedStatus === "running"
|
|
623
|
+
? this.claimSessionEntry(loopId)
|
|
624
|
+
: persistedEntry;
|
|
265
625
|
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") {
|
|
626
|
+
return this.leaseConflictResult(loopId, persistedEntry);
|
|
627
|
+
const session = this.reconstructSession(sessionEntry);
|
|
628
|
+
if (!session) {
|
|
629
|
+
// was not "running" — return stopped/stalled status
|
|
630
|
+
const lineage = (sessionEntry.loop_lineage ?? {});
|
|
631
|
+
const currentRound = lineage.current_round ?? 1;
|
|
632
|
+
const status = lineage.status ?? "stopped";
|
|
275
633
|
return {
|
|
276
634
|
sessionId: "",
|
|
277
635
|
round: currentRound,
|
|
@@ -279,43 +637,70 @@ export class SessionManager {
|
|
|
279
637
|
stopReason: status,
|
|
280
638
|
};
|
|
281
639
|
}
|
|
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
|
|
640
|
+
this.sessions.set(session.sessionId, session);
|
|
641
|
+
const reconciled = this.reconcileCommittedRound(session);
|
|
642
|
+
if (reconciled)
|
|
643
|
+
return reconciled;
|
|
644
|
+
const restored = this.restoredPromptResult(session);
|
|
645
|
+
if (restored)
|
|
646
|
+
return restored;
|
|
647
|
+
// Legacy session without a stored prompt: compile once, then persist it.
|
|
306
648
|
const request = buildLoopRequest(session);
|
|
307
|
-
const
|
|
649
|
+
const prepared = new RoundDriver(session.engine, this.backend).prepareSync(request, session.loopId, session.currentRound);
|
|
650
|
+
const prompt = prepared?.prompt ?? null;
|
|
651
|
+
session.evidenceBaseline = prepared?.evidenceBaseline ?? [];
|
|
652
|
+
session.roundSnapshot = prepared?.snapshot ?? prepareRoundTransaction(session.loopId, session.currentRound, []);
|
|
653
|
+
session.currentPrompt = prompt;
|
|
654
|
+
session.currentLevel = prepared?.level ?? "l2";
|
|
655
|
+
policyMetrics.recordStrategy(session.loopId, session.currentLevel);
|
|
656
|
+
this.save(session);
|
|
308
657
|
return {
|
|
309
|
-
sessionId,
|
|
310
|
-
round: currentRound,
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
level:
|
|
658
|
+
sessionId: session.sessionId,
|
|
659
|
+
round: session.currentRound,
|
|
660
|
+
roundId: session.roundSnapshot.roundId,
|
|
661
|
+
prompt,
|
|
662
|
+
level: session.currentLevel,
|
|
314
663
|
roundSuccess: false,
|
|
315
664
|
quality: 0,
|
|
316
|
-
warnings: parseWarnings(
|
|
665
|
+
warnings: parseWarnings(prompt),
|
|
317
666
|
};
|
|
318
667
|
}
|
|
668
|
+
/** Auto-resume all "running" sessions from vault on server startup.
|
|
669
|
+
* Scans vault for session_state entries, reconstructs each as an in-memory
|
|
670
|
+
* McpSession (without compiling — the next loopforge_next will do that).
|
|
671
|
+
* Returns the number of sessions resumed. */
|
|
672
|
+
autoResumeAll() {
|
|
673
|
+
if (!this.sessionStore)
|
|
674
|
+
return 0;
|
|
675
|
+
const entries = this.sessionStore.list();
|
|
676
|
+
// Build set of already-active loopIds
|
|
677
|
+
const activeLoopIds = new Set();
|
|
678
|
+
for (const s of this.sessions.values()) {
|
|
679
|
+
activeLoopIds.add(s.loopId);
|
|
680
|
+
}
|
|
681
|
+
let count = 0;
|
|
682
|
+
for (const entry of entries) {
|
|
683
|
+
if (entry.task_type !== "session_state")
|
|
684
|
+
continue;
|
|
685
|
+
const lid = entry.loop_id;
|
|
686
|
+
if (!lid || activeLoopIds.has(lid))
|
|
687
|
+
continue;
|
|
688
|
+
const lineage = (entry.loop_lineage ?? {});
|
|
689
|
+
const status = lineage.status ?? "running";
|
|
690
|
+
if (status !== "running" && status !== "paused")
|
|
691
|
+
continue;
|
|
692
|
+
const claimedEntry = this.claimSessionEntry(lid);
|
|
693
|
+
if (!claimedEntry)
|
|
694
|
+
continue;
|
|
695
|
+
const session = this.reconstructSession(claimedEntry, true);
|
|
696
|
+
if (session) {
|
|
697
|
+
this.sessions.set(session.sessionId, session);
|
|
698
|
+
activeLoopIds.add(lid);
|
|
699
|
+
count++;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return count;
|
|
703
|
+
}
|
|
319
704
|
list() {
|
|
320
705
|
const seen = new Set();
|
|
321
706
|
const result = [];
|
|
@@ -329,10 +714,9 @@ export class SessionManager {
|
|
|
329
714
|
status: s.status,
|
|
330
715
|
});
|
|
331
716
|
}
|
|
332
|
-
// Merge
|
|
333
|
-
if (this.
|
|
334
|
-
const
|
|
335
|
-
const entries = vault.entries ?? [];
|
|
717
|
+
// Merge persisted sessions not already in memory
|
|
718
|
+
if (this.sessionStore) {
|
|
719
|
+
const entries = this.sessionStore.list();
|
|
336
720
|
for (const e of entries) {
|
|
337
721
|
if (e.task_type !== "session_state")
|
|
338
722
|
continue;
|
|
@@ -367,11 +751,8 @@ export class SessionManager {
|
|
|
367
751
|
}
|
|
368
752
|
}
|
|
369
753
|
// 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");
|
|
754
|
+
if (!task) {
|
|
755
|
+
const sessionEntry = this.findSessionEntry(loopId);
|
|
375
756
|
if (sessionEntry) {
|
|
376
757
|
task = sessionEntry.task ?? "";
|
|
377
758
|
}
|
|
@@ -379,7 +760,7 @@ export class SessionManager {
|
|
|
379
760
|
if (!task)
|
|
380
761
|
return null;
|
|
381
762
|
// Hydrate vault context
|
|
382
|
-
const engine = new LoopForgeEngine(
|
|
763
|
+
const engine = new LoopForgeEngine(this.backend);
|
|
383
764
|
const vaultContext = engine.hydrateLoopContext(loopId);
|
|
384
765
|
// Build a minimal request for health check
|
|
385
766
|
const request = makeLoopCompileRequest({
|
|
@@ -396,6 +777,7 @@ export class SessionManager {
|
|
|
396
777
|
drift_detected: health.drift_detected,
|
|
397
778
|
strategy_stability: health.strategy_stability,
|
|
398
779
|
task_continuity: health.task_continuity,
|
|
780
|
+
policy_metrics: policyMetrics.snapshot(loopId),
|
|
399
781
|
};
|
|
400
782
|
}
|
|
401
783
|
/** Core cycle: extract self-eval → record feedback → check stop → compile next.
|
|
@@ -403,6 +785,24 @@ export class SessionManager {
|
|
|
403
785
|
* When provided (MCP path with evaluation parameter), skips regex extraction.
|
|
404
786
|
* When undefined (runtime/CLI path), falls back to regex extraction from output. */
|
|
405
787
|
async advance(sessionId, output, preExtractedEval) {
|
|
788
|
+
return this.withSessionQueue(sessionId, async () => {
|
|
789
|
+
const session = this.sessions.get(sessionId);
|
|
790
|
+
if (session?.status === "running" &&
|
|
791
|
+
!this.renewSessionLease(session.loopId)) {
|
|
792
|
+
return this.leaseConflictResult(session.loopId, this.findSessionEntry(session.loopId));
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
return await this.advanceUnlocked(sessionId, output, preExtractedEval);
|
|
796
|
+
}
|
|
797
|
+
catch (error) {
|
|
798
|
+
if (error instanceof SessionLeaseConflictError) {
|
|
799
|
+
return this.leaseConflictResult(error.loopId, this.findSessionEntry(error.loopId));
|
|
800
|
+
}
|
|
801
|
+
throw error;
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
async advanceUnlocked(sessionId, output, preExtractedEval) {
|
|
406
806
|
const session = this.sessions.get(sessionId);
|
|
407
807
|
if (!session)
|
|
408
808
|
return { sessionId, round: 0, prompt: null, stopReason: "session_not_found" };
|
|
@@ -425,250 +825,196 @@ export class SessionManager {
|
|
|
425
825
|
if (!selfEval) {
|
|
426
826
|
session.status = "stalled";
|
|
427
827
|
this.save(session);
|
|
428
|
-
void this.
|
|
828
|
+
void this.notifyTerminal(session, "stalled");
|
|
429
829
|
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason: "stalled", round: session.currentRound });
|
|
430
830
|
return { sessionId, round: session.currentRound, prompt: null, stopReason: "stalled", roundSuccess: false, quality: 0 };
|
|
431
831
|
}
|
|
432
|
-
// 1.5.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
832
|
+
// 1.5. Unified transaction: before → evaluate → verify → commit/reject.
|
|
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 actualSnapshots = completed.actualEvidence;
|
|
847
|
+
session.roundSnapshot = outcome.snapshot;
|
|
848
|
+
const pr = outcome.result;
|
|
849
|
+
policyMetrics.recordStrategyOutcome(session.loopId, session.currentLevel, pr, outcome.replayed);
|
|
850
|
+
const verificationFlags = pr.verificationFlags;
|
|
851
|
+
// Track per-rule rejections: only same-check rejections
|
|
852
|
+
// accumulate. A different rejection reason resets the counter.
|
|
853
|
+
if (pr.action === "reject" && pr.rejectionCheck) {
|
|
854
|
+
session.consecutiveRejections =
|
|
855
|
+
pr.rejectionCheck === session.lastRejectionCheck
|
|
856
|
+
? pr.newConsecutiveRejections
|
|
857
|
+
: 1;
|
|
858
|
+
session.lastRejectionCheck = pr.rejectionCheck;
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
session.consecutiveRejections = pr.newConsecutiveRejections;
|
|
862
|
+
if (pr.action !== "reject")
|
|
863
|
+
session.lastRejectionCheck = "";
|
|
864
|
+
}
|
|
865
|
+
if (pr.newLastSelfEval)
|
|
866
|
+
session.lastSelfEval = pr.newLastSelfEval;
|
|
867
|
+
if (pr.shouldPushSuccessTrajectory) {
|
|
868
|
+
session.successTrajectory.push(pr.roundSuccess);
|
|
869
|
+
}
|
|
870
|
+
// Handle enforcement actions
|
|
871
|
+
if (pr.action === "reject") {
|
|
872
|
+
const retryRequest = buildLoopRequest(session, undefined, undefined, verificationFlags);
|
|
873
|
+
const preparedRetry = await new RoundDriver(session.engine, this.backend).prepareRetry(retryRequest, session.roundSnapshot, pr.rejectionPrompt ?? "", session.consecutiveRejections);
|
|
874
|
+
if (!preparedRetry) {
|
|
875
|
+
session.status = "stalled";
|
|
876
|
+
session.currentPrompt = null;
|
|
472
877
|
this.save(session);
|
|
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
878
|
return {
|
|
479
879
|
sessionId,
|
|
480
880
|
round: session.currentRound,
|
|
881
|
+
roundId: session.roundSnapshot.roundId,
|
|
481
882
|
prompt: null,
|
|
482
|
-
stopReason: "
|
|
483
|
-
enforcementAction: "terminate",
|
|
484
|
-
enforcementReason: enforceResult.reason,
|
|
883
|
+
stopReason: "stalled",
|
|
485
884
|
};
|
|
486
885
|
}
|
|
487
|
-
|
|
488
|
-
session.
|
|
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";
|
|
886
|
+
session.roundSnapshot = preparedRetry.snapshot;
|
|
887
|
+
session.currentPrompt = preparedRetry.prompt;
|
|
888
|
+
session.currentLevel = preparedRetry.level;
|
|
509
889
|
this.save(session);
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
return { sessionId, round: session.currentRound, prompt: null, stopReason: "task_complete", roundSuccess, quality: deprecatedQuality };
|
|
890
|
+
return {
|
|
891
|
+
sessionId,
|
|
892
|
+
round: session.currentRound,
|
|
893
|
+
roundId: session.roundSnapshot.roundId,
|
|
894
|
+
prompt: preparedRetry.prompt,
|
|
895
|
+
level: preparedRetry.level,
|
|
896
|
+
enforcementAction: "reject",
|
|
897
|
+
enforcementReason: pr.enforcementReason,
|
|
898
|
+
};
|
|
520
899
|
}
|
|
521
|
-
|
|
900
|
+
// Accepted rounds advance the before-snapshot. Rejected rounds retain the
|
|
901
|
+
// original baseline so their retry is still a zero-commit transaction.
|
|
902
|
+
session.evidenceBaseline = actualSnapshots;
|
|
903
|
+
if (pr.action === "terminate") {
|
|
522
904
|
session.status = "stopped";
|
|
905
|
+
session.currentPrompt = null;
|
|
523
906
|
this.save(session);
|
|
524
|
-
void this.
|
|
525
|
-
logEvent("session_end", {
|
|
526
|
-
|
|
907
|
+
void this.notifyTerminal(session, "enforcement_terminated");
|
|
908
|
+
logEvent("session_end", {
|
|
909
|
+
sessionId, loopId: session.loopId,
|
|
910
|
+
stopReason: "enforcement_terminated", round: session.currentRound,
|
|
911
|
+
});
|
|
912
|
+
return {
|
|
913
|
+
sessionId,
|
|
914
|
+
round: session.currentRound,
|
|
915
|
+
roundId: session.roundSnapshot.roundId,
|
|
916
|
+
prompt: null,
|
|
917
|
+
stopReason: "enforcement_terminated",
|
|
918
|
+
enforcementAction: "terminate",
|
|
919
|
+
enforcementReason: pr.enforcementReason,
|
|
920
|
+
};
|
|
527
921
|
}
|
|
528
|
-
if (
|
|
529
|
-
|
|
922
|
+
if (pr.action === "stop") {
|
|
923
|
+
const reason = pr.stopReason ?? "stalled";
|
|
924
|
+
session.status = reason === "stalled" ? "stalled" : "stopped";
|
|
925
|
+
session.currentPrompt = null;
|
|
530
926
|
this.save(session);
|
|
531
|
-
void this.
|
|
532
|
-
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason:
|
|
533
|
-
return { sessionId, round: session.currentRound, prompt: null, stopReason:
|
|
927
|
+
void this.notifyTerminal(session, reason);
|
|
928
|
+
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason: reason, round: session.currentRound });
|
|
929
|
+
return { sessionId, round: session.currentRound, roundId: session.roundSnapshot.roundId, prompt: null, stopReason: reason, roundSuccess: pr.roundSuccess, quality: pr.roundSuccess ? 5 : 1 };
|
|
534
930
|
}
|
|
535
|
-
|
|
931
|
+
const roundSuccess = pr.roundSuccess;
|
|
932
|
+
// Feedback + decision metadata were committed by the transaction layer.
|
|
933
|
+
// Build deprecated quality alias for backward compat
|
|
934
|
+
const deprecatedQuality = roundSuccess ? 5 : 1;
|
|
935
|
+
// 4. Compile next round after the accepted commit.
|
|
536
936
|
session.currentRound++;
|
|
937
|
+
session.currentPrompt = null;
|
|
537
938
|
// v1.8: Memory injection for phases 2/3 — tier-aware
|
|
538
939
|
let externalCtx = "";
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
940
|
+
if (this.contextProvider) {
|
|
941
|
+
try {
|
|
942
|
+
externalCtx = (await this.contextProvider({
|
|
943
|
+
loopId: session.loopId,
|
|
944
|
+
round: session.currentRound,
|
|
945
|
+
task: session.task,
|
|
946
|
+
domain: "",
|
|
947
|
+
lastEvaluation: selfEval,
|
|
948
|
+
})).trim();
|
|
949
|
+
}
|
|
950
|
+
catch {
|
|
951
|
+
logEvent("context_provider_error", {
|
|
952
|
+
loopId: session.loopId,
|
|
953
|
+
round: session.currentRound,
|
|
548
954
|
});
|
|
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
955
|
}
|
|
596
956
|
}
|
|
957
|
+
// pause()/delete() are intentionally synchronous for API compatibility.
|
|
958
|
+
// They may run while the memory provider above is awaited. Treat the
|
|
959
|
+
// session map + status as a commit fence so an in-flight advance cannot
|
|
960
|
+
// compile/save another round after a terminal lifecycle transition.
|
|
961
|
+
if (this.sessions.get(sessionId) !== session || session.status !== "running") {
|
|
962
|
+
return {
|
|
963
|
+
sessionId,
|
|
964
|
+
round: session.currentRound,
|
|
965
|
+
prompt: null,
|
|
966
|
+
stopReason: session.status,
|
|
967
|
+
roundSuccess,
|
|
968
|
+
quality: deprecatedQuality,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
597
971
|
const request = buildLoopRequest(session, selfEval, deprecatedQuality, verificationFlags);
|
|
598
972
|
if (externalCtx) {
|
|
599
973
|
request.external_context = externalCtx;
|
|
600
974
|
}
|
|
601
|
-
const
|
|
602
|
-
|
|
603
|
-
const
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
975
|
+
const prepared = await new RoundDriver(session.engine, this.backend).prepare(request, session.loopId, session.currentRound);
|
|
976
|
+
const nextPrompt = prepared?.prompt ?? null;
|
|
977
|
+
const nextLevel = prepared?.level ?? "l2";
|
|
978
|
+
const nextBaseline = prepared?.evidenceBaseline ?? actualSnapshots;
|
|
979
|
+
session.evidenceBaseline = nextBaseline;
|
|
980
|
+
session.roundSnapshot = prepared?.snapshot ?? prepareRoundTransaction(session.loopId, session.currentRound, nextBaseline);
|
|
981
|
+
session.currentPrompt = nextPrompt;
|
|
982
|
+
session.currentLevel = nextLevel;
|
|
983
|
+
policyMetrics.recordStrategy(session.loopId, nextLevel);
|
|
609
984
|
this.save(session);
|
|
610
985
|
return {
|
|
611
986
|
sessionId,
|
|
612
987
|
round: session.currentRound,
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
level:
|
|
988
|
+
roundId: session.roundSnapshot.roundId,
|
|
989
|
+
prompt: nextPrompt,
|
|
990
|
+
level: nextLevel,
|
|
616
991
|
roundSuccess,
|
|
617
992
|
quality: deprecatedQuality,
|
|
618
|
-
warnings: parseWarnings(
|
|
993
|
+
warnings: parseWarnings(nextPrompt),
|
|
619
994
|
};
|
|
620
995
|
}
|
|
621
996
|
/** Write back loop knowledge to long-term memory.
|
|
622
997
|
* 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)
|
|
628
|
-
return;
|
|
629
|
-
if (wp.write_on_outcomes.length > 0 && !wp.write_on_outcomes.includes(stopReason))
|
|
630
|
-
return;
|
|
631
|
-
if (session.currentRound < 1)
|
|
998
|
+
async notifyTerminal(session, stopReason) {
|
|
999
|
+
if (this.terminalSinks.size === 0)
|
|
632
1000
|
return;
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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
|
-
}
|
|
1001
|
+
const event = {
|
|
1002
|
+
loopId: session.loopId,
|
|
1003
|
+
task: session.task,
|
|
1004
|
+
success: stopReason === "completed",
|
|
1005
|
+
stopReason: stopReason,
|
|
1006
|
+
roundsCompleted: session.currentRound,
|
|
1007
|
+
successTrajectory: [...session.successTrajectory],
|
|
1008
|
+
lastEvaluation: session.lastSelfEval,
|
|
1009
|
+
};
|
|
1010
|
+
await Promise.allSettled([...this.terminalSinks].map((sink) => Promise.resolve(sink(event))));
|
|
664
1011
|
}
|
|
665
1012
|
/** Replay timeline for a session — creates ReplayBackend from the stored backend. */
|
|
666
1013
|
replayTimeline(sessionId) {
|
|
667
1014
|
const session = this.sessions.get(sessionId);
|
|
668
1015
|
if (!session)
|
|
669
1016
|
return null;
|
|
670
|
-
const
|
|
671
|
-
const replay = new ReplayBackend(vaultBackend);
|
|
1017
|
+
const replay = new ReplayBackend(this.backend);
|
|
672
1018
|
return replay.timeline(session.loopId);
|
|
673
1019
|
}
|
|
674
1020
|
}
|