loopforge 1.12.0 → 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 +44 -0
- package/dist/enforcement-gate.d.ts.map +1 -0
- package/dist/enforcement-gate.js +271 -0
- package/dist/enforcement-gate.js.map +1 -0
- package/dist/engine.d.ts +17 -7
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +158 -249
- 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 +19 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -10
- 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 +307 -2001
- 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 +84 -20
- package/dist/mcp/session.d.ts.map +1 -1
- package/dist/mcp/session.js +718 -316
- 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 +58 -75
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +122 -93
- 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 +117 -91
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +40 -36
- 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 +35 -17
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +357 -344
- 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 +39 -7
- package/dist/verification-gate.d.ts.map +1 -1
- package/dist/verification-gate.js +198 -9
- package/dist/verification-gate.js.map +1 -1
- package/loop_policy.json +26 -47
- package/package.json +13 -10
- package/skills/perception/SKILL.md +80 -249
- package/dist/backends/fs.d.ts +0 -36
- package/dist/backends/fs.d.ts.map +0 -1
- package/dist/backends/fs.js +0 -358
- package/dist/backends/fs.js.map +0 -1
- package/dist/builder.d.ts +0 -12
- package/dist/builder.d.ts.map +0 -1
- package/dist/builder.js +0 -259
- 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 -110
- package/dist/tests/builder.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 -371
- 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 -1051
- 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 -485
- 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 -323
- 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
|
@@ -6,25 +6,30 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { LoopForgeEngine, extractSelfEvaluation, heuristicSelfEvaluation } from "../engine.js";
|
|
9
|
-
import { checkLoopHealth
|
|
10
|
-
import { getPolicy
|
|
11
|
-
import { Mode, makeLoopCompileRequest
|
|
9
|
+
import { checkLoopHealth } from "../loop-compiler.js";
|
|
10
|
+
import { getPolicy } from "../policy.js";
|
|
11
|
+
import { Mode, makeLoopCompileRequest } from "../protocol.js";
|
|
12
12
|
import { ReplayBackend } from "../replay.js";
|
|
13
|
-
import {
|
|
14
|
-
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";
|
|
15
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";
|
|
16
21
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
17
22
|
function buildLoopRequest(session, lastEval, _lastQuality, // deprecated — kept for backward compat
|
|
18
23
|
verificationFlags) {
|
|
19
24
|
const req = {
|
|
20
25
|
task: session.task,
|
|
21
26
|
mode: Mode.LOOP_COMPILE,
|
|
22
|
-
vault_config: makeVaultConfig(),
|
|
23
27
|
feedback: null,
|
|
24
28
|
skill_name: null,
|
|
25
29
|
task_id: null,
|
|
26
30
|
loop_id: session.loopId,
|
|
27
31
|
round: session.currentRound,
|
|
32
|
+
max_rounds: session.maxRounds,
|
|
28
33
|
verification_flags: verificationFlags ?? [],
|
|
29
34
|
};
|
|
30
35
|
if (lastEval) {
|
|
@@ -55,19 +60,12 @@ verificationFlags) {
|
|
|
55
60
|
// v1.10: Checkpoint boundary
|
|
56
61
|
compression_checkpoint: lastEval.compression_checkpoint ?? false,
|
|
57
62
|
checkpoint_label: lastEval.checkpoint_label ?? "",
|
|
63
|
+
// v1.16: Agent's declared next action
|
|
64
|
+
next_action: lastEval.next_action,
|
|
58
65
|
};
|
|
59
66
|
}
|
|
60
67
|
return req;
|
|
61
68
|
}
|
|
62
|
-
function parseLevel(rationale) {
|
|
63
|
-
if (!rationale)
|
|
64
|
-
return "l2";
|
|
65
|
-
if (rationale.includes("l0"))
|
|
66
|
-
return "l0";
|
|
67
|
-
if (rationale.includes("l1"))
|
|
68
|
-
return "l1";
|
|
69
|
-
return "l2";
|
|
70
|
-
}
|
|
71
69
|
function parseWarnings(prompt) {
|
|
72
70
|
if (!prompt)
|
|
73
71
|
return [];
|
|
@@ -85,76 +83,166 @@ function parseWarnings(prompt) {
|
|
|
85
83
|
// ── SessionManager ─────────────────────────────────────────────────────────
|
|
86
84
|
export class SessionManager {
|
|
87
85
|
sessions = new Map();
|
|
86
|
+
/** Serializes state transitions for each session. */
|
|
87
|
+
sessionQueues = new Map();
|
|
88
88
|
backend;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
+
}
|
|
95
188
|
}
|
|
96
189
|
async create(input) {
|
|
97
190
|
const sessionId = randomUUID();
|
|
98
191
|
const loopId = input.loopId ?? randomUUID();
|
|
99
|
-
const engine = new LoopForgeEngine(
|
|
192
|
+
const engine = new LoopForgeEngine(this.backend);
|
|
100
193
|
const maxRounds = input.maxRounds ?? getPolicy().runtime.max_rounds;
|
|
101
194
|
// Populate extra fields for the first round
|
|
102
195
|
const request = buildLoopRequest({
|
|
103
196
|
sessionId, loopId, task: input.task, engine, currentRound: 1,
|
|
104
197
|
maxRounds, successTrajectory: [], status: "running", createdAt: Date.now(),
|
|
105
|
-
|
|
106
|
-
|
|
198
|
+
consecutiveRejections: 0,
|
|
199
|
+
lastRejectionCheck: "",
|
|
200
|
+
evidenceBaseline: [],
|
|
107
201
|
});
|
|
108
202
|
request.domain = input.domain ?? "";
|
|
109
203
|
request.plan_source = input.planSource ?? null;
|
|
110
204
|
request.constraints_from_plan = input.constraints ?? [];
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
const allowedPhases = new Set(resolveAllowedPhases(maxRounds, miPolicy.round_tiers));
|
|
114
|
-
if (miPolicy.enabled && this.memoryProvider && allowedPhases.has(1)) {
|
|
205
|
+
// Explicit context is requested by the MCP embedding; no auto-discovery.
|
|
206
|
+
if (this.contextProvider) {
|
|
115
207
|
try {
|
|
116
208
|
const ctx = {
|
|
117
209
|
loopId,
|
|
118
210
|
round: 1,
|
|
119
211
|
task: input.task,
|
|
120
212
|
domain: input.domain ?? "",
|
|
121
|
-
phase: 1,
|
|
122
|
-
progressEstimate: 0,
|
|
123
|
-
accumulatedContext: {
|
|
124
|
-
recurringIssues: [],
|
|
125
|
-
failedPatterns: [],
|
|
126
|
-
keyLessons: [],
|
|
127
|
-
remainingCriteria: [],
|
|
128
|
-
},
|
|
129
213
|
};
|
|
130
|
-
const rawContext = await this.
|
|
214
|
+
const rawContext = await this.contextProvider(ctx);
|
|
131
215
|
if (rawContext?.trim()) {
|
|
132
|
-
request.external_context = rawContext.trim()
|
|
133
|
-
logEvent("
|
|
134
|
-
loopId, round: 1,
|
|
216
|
+
request.external_context = rawContext.trim();
|
|
217
|
+
logEvent("external_context_loaded", {
|
|
218
|
+
loopId, round: 1,
|
|
135
219
|
contextLength: request.external_context.length,
|
|
136
220
|
});
|
|
137
221
|
}
|
|
138
222
|
}
|
|
139
223
|
catch {
|
|
140
|
-
//
|
|
141
|
-
logEvent("
|
|
224
|
+
// Provider failures are isolated from session creation.
|
|
225
|
+
logEvent("context_provider_error", { loopId, round: 1 });
|
|
142
226
|
}
|
|
143
227
|
}
|
|
144
|
-
const
|
|
145
|
-
const
|
|
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 ?? [];
|
|
146
232
|
const session = {
|
|
147
233
|
sessionId, loopId, task: input.task, engine,
|
|
148
234
|
currentRound: 1, maxRounds, successTrajectory: [],
|
|
149
235
|
status: "running", createdAt: Date.now(),
|
|
150
|
-
// v1.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
236
|
+
// v1.13: Enforcement gate state
|
|
237
|
+
consecutiveRejections: 0,
|
|
238
|
+
lastRejectionCheck: "",
|
|
239
|
+
evidenceBaseline,
|
|
240
|
+
roundSnapshot: prepared?.snapshot ?? prepareRoundTransaction(loopId, 1, evidenceBaseline),
|
|
241
|
+
currentPrompt: initialPrompt,
|
|
242
|
+
currentLevel: initialLevel,
|
|
156
243
|
};
|
|
157
244
|
this.sessions.set(sessionId, session);
|
|
245
|
+
policyMetrics.recordStrategy(loopId, initialLevel);
|
|
158
246
|
// Persist to vault for cross-process recovery
|
|
159
247
|
this.save(session);
|
|
160
248
|
logEvent("session_start", {
|
|
@@ -166,97 +254,382 @@ export class SessionManager {
|
|
|
166
254
|
return {
|
|
167
255
|
sessionId,
|
|
168
256
|
round: 1,
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
level:
|
|
257
|
+
roundId: session.roundSnapshot?.roundId,
|
|
258
|
+
prompt: initialPrompt,
|
|
259
|
+
level: initialLevel,
|
|
172
260
|
roundSuccess: false,
|
|
173
261
|
quality: 0,
|
|
174
|
-
warnings: parseWarnings(
|
|
262
|
+
warnings: parseWarnings(initialPrompt),
|
|
175
263
|
};
|
|
176
264
|
}
|
|
177
265
|
get(sessionId) {
|
|
178
266
|
return this.sessions.get(sessionId);
|
|
179
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
|
+
}
|
|
180
288
|
delete(sessionId) {
|
|
181
289
|
const session = this.sessions.get(sessionId);
|
|
182
290
|
if (!session)
|
|
183
291
|
return false;
|
|
184
292
|
session.status = "stopped";
|
|
185
|
-
|
|
293
|
+
this.save(session); // Persist stopped status to vault so autoResumeAll won't resurrect
|
|
294
|
+
void this.notifyTerminal(session, "cancelled");
|
|
186
295
|
this.sessions.delete(sessionId);
|
|
187
296
|
logEvent("session_end", {
|
|
188
297
|
sessionId,
|
|
189
298
|
loopId: session.loopId,
|
|
190
|
-
stopReason: "
|
|
299
|
+
stopReason: "cancelled",
|
|
191
300
|
roundsCompleted: session.currentRound,
|
|
192
301
|
});
|
|
193
302
|
return true;
|
|
194
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
|
+
}
|
|
195
514
|
/** Persist session state to vault for cross-process recovery.
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* Entire read→filter→write→append is wrapped in a file lock to prevent
|
|
199
|
-
* 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. */
|
|
200
517
|
save(session) {
|
|
201
|
-
if (!this.
|
|
518
|
+
if (!this.sessionStore)
|
|
202
519
|
return;
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
});
|
|
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
|
+
},
|
|
230
546
|
};
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
+
}
|
|
235
563
|
}
|
|
236
|
-
|
|
237
|
-
|
|
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;
|
|
238
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
|
+
};
|
|
239
612
|
}
|
|
240
613
|
/** Resume a loop from vault state.
|
|
241
614
|
* Reconstructs the session and compiles the prompt for the next round.
|
|
242
615
|
* Returns null if no session_state entry exists for this loopId. */
|
|
243
616
|
resume(loopId) {
|
|
244
|
-
|
|
617
|
+
const persistedEntry = this.findSessionEntry(loopId);
|
|
618
|
+
if (!persistedEntry)
|
|
245
619
|
return null;
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
620
|
+
const persistedLineage = (persistedEntry.loop_lineage ?? {});
|
|
621
|
+
const persistedStatus = persistedLineage.status ?? "running";
|
|
622
|
+
const sessionEntry = persistedStatus === "running"
|
|
623
|
+
? this.claimSessionEntry(loopId)
|
|
624
|
+
: persistedEntry;
|
|
250
625
|
if (!sessionEntry)
|
|
251
|
-
return
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// If the loop was already stopped or stalled, return immediately
|
|
259
|
-
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";
|
|
260
633
|
return {
|
|
261
634
|
sessionId: "",
|
|
262
635
|
round: currentRound,
|
|
@@ -264,41 +637,70 @@ export class SessionManager {
|
|
|
264
637
|
stopReason: status,
|
|
265
638
|
};
|
|
266
639
|
}
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
currentRound,
|
|
276
|
-
maxRounds,
|
|
277
|
-
successTrajectory,
|
|
278
|
-
status: "running",
|
|
279
|
-
createdAt: lineage.created_at ?? Date.now(),
|
|
280
|
-
// v1.7: Memory integration state restored from vault
|
|
281
|
-
injectionCount: lineage.injection_count ?? 0,
|
|
282
|
-
lastInjectionRound: lineage.last_injection_round ?? 0,
|
|
283
|
-
injectedContexts: [],
|
|
284
|
-
phase2Triggered: lineage.phase2_triggered ?? false,
|
|
285
|
-
phase3Triggered: lineage.phase3_triggered ?? false,
|
|
286
|
-
};
|
|
287
|
-
this.sessions.set(sessionId, session);
|
|
288
|
-
// 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.
|
|
289
648
|
const request = buildLoopRequest(session);
|
|
290
|
-
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);
|
|
291
657
|
return {
|
|
292
|
-
sessionId,
|
|
293
|
-
round: currentRound,
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
level:
|
|
658
|
+
sessionId: session.sessionId,
|
|
659
|
+
round: session.currentRound,
|
|
660
|
+
roundId: session.roundSnapshot.roundId,
|
|
661
|
+
prompt,
|
|
662
|
+
level: session.currentLevel,
|
|
297
663
|
roundSuccess: false,
|
|
298
664
|
quality: 0,
|
|
299
|
-
warnings: parseWarnings(
|
|
665
|
+
warnings: parseWarnings(prompt),
|
|
300
666
|
};
|
|
301
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
|
+
}
|
|
302
704
|
list() {
|
|
303
705
|
const seen = new Set();
|
|
304
706
|
const result = [];
|
|
@@ -312,10 +714,9 @@ export class SessionManager {
|
|
|
312
714
|
status: s.status,
|
|
313
715
|
});
|
|
314
716
|
}
|
|
315
|
-
// Merge
|
|
316
|
-
if (this.
|
|
317
|
-
const
|
|
318
|
-
const entries = vault.entries ?? [];
|
|
717
|
+
// Merge persisted sessions not already in memory
|
|
718
|
+
if (this.sessionStore) {
|
|
719
|
+
const entries = this.sessionStore.list();
|
|
319
720
|
for (const e of entries) {
|
|
320
721
|
if (e.task_type !== "session_state")
|
|
321
722
|
continue;
|
|
@@ -350,11 +751,8 @@ export class SessionManager {
|
|
|
350
751
|
}
|
|
351
752
|
}
|
|
352
753
|
// Fall back to vault for task
|
|
353
|
-
if (!task
|
|
354
|
-
const
|
|
355
|
-
prefix: `loop:${loopId}:session`,
|
|
356
|
-
});
|
|
357
|
-
const sessionEntry = entries.find((e) => e.task_type === "session_state");
|
|
754
|
+
if (!task) {
|
|
755
|
+
const sessionEntry = this.findSessionEntry(loopId);
|
|
358
756
|
if (sessionEntry) {
|
|
359
757
|
task = sessionEntry.task ?? "";
|
|
360
758
|
}
|
|
@@ -362,7 +760,7 @@ export class SessionManager {
|
|
|
362
760
|
if (!task)
|
|
363
761
|
return null;
|
|
364
762
|
// Hydrate vault context
|
|
365
|
-
const engine = new LoopForgeEngine(
|
|
763
|
+
const engine = new LoopForgeEngine(this.backend);
|
|
366
764
|
const vaultContext = engine.hydrateLoopContext(loopId);
|
|
367
765
|
// Build a minimal request for health check
|
|
368
766
|
const request = makeLoopCompileRequest({
|
|
@@ -379,6 +777,7 @@ export class SessionManager {
|
|
|
379
777
|
drift_detected: health.drift_detected,
|
|
380
778
|
strategy_stability: health.strategy_stability,
|
|
381
779
|
task_continuity: health.task_continuity,
|
|
780
|
+
policy_metrics: policyMetrics.snapshot(loopId),
|
|
382
781
|
};
|
|
383
782
|
}
|
|
384
783
|
/** Core cycle: extract self-eval → record feedback → check stop → compile next.
|
|
@@ -386,6 +785,24 @@ export class SessionManager {
|
|
|
386
785
|
* When provided (MCP path with evaluation parameter), skips regex extraction.
|
|
387
786
|
* When undefined (runtime/CLI path), falls back to regex extraction from output. */
|
|
388
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) {
|
|
389
806
|
const session = this.sessions.get(sessionId);
|
|
390
807
|
if (!session)
|
|
391
808
|
return { sessionId, round: 0, prompt: null, stopReason: "session_not_found" };
|
|
@@ -408,211 +825,196 @@ export class SessionManager {
|
|
|
408
825
|
if (!selfEval) {
|
|
409
826
|
session.status = "stalled";
|
|
410
827
|
this.save(session);
|
|
411
|
-
void this.
|
|
828
|
+
void this.notifyTerminal(session, "stalled");
|
|
412
829
|
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason: "stalled", round: session.currentRound });
|
|
413
830
|
return { sessionId, round: session.currentRound, prompt: null, stopReason: "stalled", roundSuccess: false, quality: 0 };
|
|
414
831
|
}
|
|
415
|
-
// 1.5.
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
// 3. Stop conditions (extraction-first order — see memory)
|
|
443
|
-
if (extractionFailed) {
|
|
444
|
-
session.status = "stalled";
|
|
445
|
-
this.save(session);
|
|
446
|
-
void this.doWriteback(session, "stalled");
|
|
447
|
-
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason: "stalled", round: session.currentRound });
|
|
448
|
-
return { sessionId, round: session.currentRound, prompt: null, stopReason: "stalled", roundSuccess, quality: deprecatedQuality };
|
|
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;
|
|
449
859
|
}
|
|
450
|
-
|
|
451
|
-
session.
|
|
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;
|
|
877
|
+
this.save(session);
|
|
878
|
+
return {
|
|
879
|
+
sessionId,
|
|
880
|
+
round: session.currentRound,
|
|
881
|
+
roundId: session.roundSnapshot.roundId,
|
|
882
|
+
prompt: null,
|
|
883
|
+
stopReason: "stalled",
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
session.roundSnapshot = preparedRetry.snapshot;
|
|
887
|
+
session.currentPrompt = preparedRetry.prompt;
|
|
888
|
+
session.currentLevel = preparedRetry.level;
|
|
452
889
|
this.save(session);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
+
};
|
|
456
899
|
}
|
|
457
|
-
|
|
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") {
|
|
458
904
|
session.status = "stopped";
|
|
905
|
+
session.currentPrompt = null;
|
|
459
906
|
this.save(session);
|
|
460
|
-
void this.
|
|
461
|
-
logEvent("session_end", {
|
|
462
|
-
|
|
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
|
+
};
|
|
463
921
|
}
|
|
464
|
-
if (
|
|
465
|
-
|
|
922
|
+
if (pr.action === "stop") {
|
|
923
|
+
const reason = pr.stopReason ?? "stalled";
|
|
924
|
+
session.status = reason === "stalled" ? "stalled" : "stopped";
|
|
925
|
+
session.currentPrompt = null;
|
|
466
926
|
this.save(session);
|
|
467
|
-
void this.
|
|
468
|
-
logEvent("session_end", { sessionId, loopId: session.loopId, stopReason:
|
|
469
|
-
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 };
|
|
470
930
|
}
|
|
471
|
-
|
|
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.
|
|
472
936
|
session.currentRound++;
|
|
937
|
+
session.currentPrompt = null;
|
|
473
938
|
// v1.8: Memory injection for phases 2/3 — tier-aware
|
|
474
939
|
let externalCtx = "";
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
session.
|
|
489
|
-
}
|
|
490
|
-
else if (allowedPhases.has(3) &&
|
|
491
|
-
!session.phase3Triggered &&
|
|
492
|
-
hasProgress &&
|
|
493
|
-
progress >= miPolicy.phase_thresholds.phase3.threshold) {
|
|
494
|
-
phase = 3;
|
|
495
|
-
session.phase3Triggered = true;
|
|
496
|
-
}
|
|
497
|
-
if (phase !== null) {
|
|
498
|
-
try {
|
|
499
|
-
const accCtx = {
|
|
500
|
-
recurringIssues: selfEval.constraint_violations ?? [],
|
|
501
|
-
failedPatterns: [],
|
|
502
|
-
keyLessons: selfEval.emerged_subtasks ?? [],
|
|
503
|
-
remainingCriteria: selfEval.execution_evidence?.success_criteria_remaining ?? [],
|
|
504
|
-
};
|
|
505
|
-
const ctx = {
|
|
506
|
-
loopId: session.loopId,
|
|
507
|
-
round: session.currentRound,
|
|
508
|
-
task: session.task,
|
|
509
|
-
domain: "",
|
|
510
|
-
phase,
|
|
511
|
-
progressEstimate: hasProgress ? progress : -1,
|
|
512
|
-
accumulatedContext: accCtx,
|
|
513
|
-
};
|
|
514
|
-
const rawContext = await this.memoryProvider(ctx);
|
|
515
|
-
if (rawContext?.trim()) {
|
|
516
|
-
// Dedup
|
|
517
|
-
const newTokens = tokenize(rawContext);
|
|
518
|
-
let isDuplicate = false;
|
|
519
|
-
for (const old of session.injectedContexts) {
|
|
520
|
-
if (jaccard(newTokens, tokenize(old)) > miPolicy.dedup_threshold) {
|
|
521
|
-
isDuplicate = true;
|
|
522
|
-
break;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
if (!isDuplicate) {
|
|
526
|
-
externalCtx = rawContext.trim().slice(0, miPolicy.max_context_length);
|
|
527
|
-
session.injectionCount++;
|
|
528
|
-
session.lastInjectionRound = session.currentRound;
|
|
529
|
-
session.injectedContexts.push(externalCtx);
|
|
530
|
-
logEvent("memory_injected", {
|
|
531
|
-
loopId: session.loopId, round: session.currentRound,
|
|
532
|
-
phase, injectionCount: session.injectionCount,
|
|
533
|
-
contextLength: externalCtx.length,
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
catch {
|
|
539
|
-
logEvent("memory_provider_error", {
|
|
540
|
-
loopId: session.loopId, round: session.currentRound,
|
|
541
|
-
});
|
|
542
|
-
}
|
|
543
|
-
}
|
|
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,
|
|
954
|
+
});
|
|
544
955
|
}
|
|
545
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
|
+
}
|
|
546
971
|
const request = buildLoopRequest(session, selfEval, deprecatedQuality, verificationFlags);
|
|
547
972
|
if (externalCtx) {
|
|
548
973
|
request.external_context = externalCtx;
|
|
549
974
|
}
|
|
550
|
-
const
|
|
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);
|
|
551
984
|
this.save(session);
|
|
552
985
|
return {
|
|
553
986
|
sessionId,
|
|
554
987
|
round: session.currentRound,
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
level:
|
|
988
|
+
roundId: session.roundSnapshot.roundId,
|
|
989
|
+
prompt: nextPrompt,
|
|
990
|
+
level: nextLevel,
|
|
558
991
|
roundSuccess,
|
|
559
992
|
quality: deprecatedQuality,
|
|
560
|
-
warnings: parseWarnings(
|
|
993
|
+
warnings: parseWarnings(nextPrompt),
|
|
561
994
|
};
|
|
562
995
|
}
|
|
563
996
|
/** Write back loop knowledge to long-term memory.
|
|
564
|
-
* Called when a loop terminates
|
|
565
|
-
async
|
|
566
|
-
if (
|
|
567
|
-
return;
|
|
568
|
-
const wp = getPolicy().memory_writeback;
|
|
569
|
-
if (!wp.enabled)
|
|
570
|
-
return;
|
|
571
|
-
if (wp.write_on_outcomes.length > 0 && !wp.write_on_outcomes.includes(stopReason))
|
|
997
|
+
* Uses shared base builder from policy.ts. Called when a loop terminates. */
|
|
998
|
+
async notifyTerminal(session, stopReason) {
|
|
999
|
+
if (this.terminalSinks.size === 0)
|
|
572
1000
|
return;
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
title: `${session.task.slice(0, 80)} — ${outcome}`,
|
|
584
|
-
objective: session.task.slice(0, 200),
|
|
585
|
-
keyOutcome: outcome === "completed"
|
|
586
|
-
? `Completed successfully in ${session.currentRound} rounds.`
|
|
587
|
-
: `Terminated with reason '${stopReason}' after ${session.currentRound} rounds.`,
|
|
588
|
-
keyDiscoveries: [],
|
|
589
|
-
date: new Date().toISOString().split("T")[0],
|
|
590
|
-
},
|
|
591
|
-
feedbackEntries: [],
|
|
592
|
-
referenceEntry: {
|
|
593
|
-
description: `LoopForge vault data for "${session.task.slice(0, 80)}"`,
|
|
594
|
-
vaultLocation: `.promptcraft/prompt_vault.json → loop:${session.loopId}:*`,
|
|
595
|
-
},
|
|
596
|
-
};
|
|
597
|
-
await this.memoryWriter(payload);
|
|
598
|
-
logEvent("memory_writeback", {
|
|
599
|
-
loopId: session.loopId, stopReason,
|
|
600
|
-
feedbackCount: payload.feedbackEntries.length,
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
catch {
|
|
604
|
-
logEvent("memory_writeback_error", {
|
|
605
|
-
loopId: session.loopId, stopReason,
|
|
606
|
-
});
|
|
607
|
-
}
|
|
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))));
|
|
608
1011
|
}
|
|
609
1012
|
/** Replay timeline for a session — creates ReplayBackend from the stored backend. */
|
|
610
1013
|
replayTimeline(sessionId) {
|
|
611
1014
|
const session = this.sessions.get(sessionId);
|
|
612
1015
|
if (!session)
|
|
613
1016
|
return null;
|
|
614
|
-
const
|
|
615
|
-
const replay = new ReplayBackend(vaultBackend);
|
|
1017
|
+
const replay = new ReplayBackend(this.backend);
|
|
616
1018
|
return replay.timeline(session.loopId);
|
|
617
1019
|
}
|
|
618
1020
|
}
|