@slicenfer/project-memory-runtime-core 0.3.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/LICENSE +674 -0
- package/README.md +12 -0
- package/dist/activation/engine.d.ts +27 -0
- package/dist/activation/engine.d.ts.map +1 -0
- package/dist/activation/engine.js +294 -0
- package/dist/activation/engine.js.map +1 -0
- package/dist/checkpoints.d.ts +12 -0
- package/dist/checkpoints.d.ts.map +1 -0
- package/dist/checkpoints.js +94 -0
- package/dist/checkpoints.js.map +1 -0
- package/dist/compiler/deterministic.d.ts +7 -0
- package/dist/compiler/deterministic.d.ts.map +1 -0
- package/dist/compiler/deterministic.js +385 -0
- package/dist/compiler/deterministic.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/ingestion/service.d.ts +7 -0
- package/dist/ingestion/service.d.ts.map +1 -0
- package/dist/ingestion/service.js +5 -0
- package/dist/ingestion/service.js.map +1 -0
- package/dist/recall/packet.d.ts +13 -0
- package/dist/recall/packet.d.ts.map +1 -0
- package/dist/recall/packet.js +155 -0
- package/dist/recall/packet.js.map +1 -0
- package/dist/runtime.d.ts +53 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +708 -0
- package/dist/runtime.js.map +1 -0
- package/dist/scope.d.ts +6 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +50 -0
- package/dist/scope.js.map +1 -0
- package/dist/storage/migrations/001_init.sql +113 -0
- package/dist/storage/migrations/002_constraints.sql +148 -0
- package/dist/storage/migrations/003_event_provenance.sql +36 -0
- package/dist/storage/migrations/004_event_capture_path.sql +37 -0
- package/dist/storage/migrations/005_activation_indices.sql +15 -0
- package/dist/storage/migrations/006_session_checkpoints.sql +54 -0
- package/dist/storage/migrations.d.ts +6 -0
- package/dist/storage/migrations.d.ts.map +1 -0
- package/dist/storage/migrations.js +19 -0
- package/dist/storage/migrations.js.map +1 -0
- package/dist/storage/sqlite.d.ts +36 -0
- package/dist/storage/sqlite.d.ts.map +1 -0
- package/dist/storage/sqlite.js +601 -0
- package/dist/storage/sqlite.js.map +1 -0
- package/dist/types.d.ts +294 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +36 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +16 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +36 -0
- package/dist/utils.js.map +1 -0
- package/dist/validation.d.ts +24 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +323 -0
- package/dist/validation.js.map +1 -0
- package/package.json +52 -0
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import { RuntimeStorage } from "./storage/sqlite.js";
|
|
2
|
+
import { nowIso, clamp, asString, daysBetween, hashId, POSITIVE_OUTCOME_TYPES, NEGATIVE_OUTCOME_TYPES } from "./utils.js";
|
|
3
|
+
import { buildRecallPacket } from "./recall/packet.js";
|
|
4
|
+
import { buildIngestionArtifacts } from "./ingestion/service.js";
|
|
5
|
+
import { scopeSignature } from "./scope.js";
|
|
6
|
+
import { checkpointPacketHash, computeCheckpointFileDigests, currentBranch, currentRepoHead, normalizeCheckpointFilePath, toRecallCheckpoint, verifyCheckpointFileDigests, } from "./checkpoints.js";
|
|
7
|
+
import { DEFAULT_ALLOWED_CAPTURE_PATHS, RuntimeInvariantError, assertClaimTransitionAllowed, assertCapturePathAllowed, assertVerificationStatus, deriveEventProvenance, validateClaimRecord, } from "./validation.js";
|
|
8
|
+
const STALE_TTL_DAYS = {
|
|
9
|
+
fact: 90,
|
|
10
|
+
decision: 60,
|
|
11
|
+
thread: 14,
|
|
12
|
+
};
|
|
13
|
+
function extractIssueId(event) {
|
|
14
|
+
const explicit = asString(event.metadata?.issue_id);
|
|
15
|
+
if (explicit)
|
|
16
|
+
return explicit;
|
|
17
|
+
const match = event.content.match(/#(\d+)/);
|
|
18
|
+
return match?.[1];
|
|
19
|
+
}
|
|
20
|
+
function isSuccessfulTestResult(event) {
|
|
21
|
+
return (event.event_type === "test_result" &&
|
|
22
|
+
typeof event.metadata?.exit_code === "number" &&
|
|
23
|
+
event.metadata.exit_code === 0);
|
|
24
|
+
}
|
|
25
|
+
function scorePositive(oldScore, strength) {
|
|
26
|
+
return clamp(oldScore + 0.1 * strength * (1 - oldScore), -1, 1);
|
|
27
|
+
}
|
|
28
|
+
function scoreNegative(oldScore, strength) {
|
|
29
|
+
return clamp(oldScore - 0.1 * strength * ((oldScore + 1) / 2), -1, 1);
|
|
30
|
+
}
|
|
31
|
+
function isPositiveOutcome(type) {
|
|
32
|
+
return POSITIVE_OUTCOME_TYPES.has(type);
|
|
33
|
+
}
|
|
34
|
+
function isNegativeOutcome(type) {
|
|
35
|
+
return NEGATIVE_OUTCOME_TYPES.has(type);
|
|
36
|
+
}
|
|
37
|
+
function buildTransition(claim, toStatus, reason, triggerType, triggerRef, actor) {
|
|
38
|
+
return {
|
|
39
|
+
id: `trn-${claim.id}-${toStatus}-${triggerRef ?? triggerType}`,
|
|
40
|
+
ts: nowIso(),
|
|
41
|
+
project_id: claim.project_id,
|
|
42
|
+
claim_id: claim.id,
|
|
43
|
+
from_status: claim.status,
|
|
44
|
+
to_status: toStatus,
|
|
45
|
+
reason,
|
|
46
|
+
trigger_type: triggerType,
|
|
47
|
+
trigger_ref: triggerRef,
|
|
48
|
+
actor,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export class ProjectMemoryRuntime {
|
|
52
|
+
storage;
|
|
53
|
+
allowedCapturePaths;
|
|
54
|
+
initialized = false;
|
|
55
|
+
adminApi;
|
|
56
|
+
constructor(config = {}) {
|
|
57
|
+
this.storage = new RuntimeStorage(config);
|
|
58
|
+
this.allowedCapturePaths = new Set(config.allowed_capture_paths ?? DEFAULT_ALLOWED_CAPTURE_PATHS);
|
|
59
|
+
this.adminApi = {
|
|
60
|
+
insertClaimRecord: (claim) => {
|
|
61
|
+
this.initialize();
|
|
62
|
+
this.storage.insertClaim(claim);
|
|
63
|
+
},
|
|
64
|
+
insertOutcomeRecord: (outcome) => {
|
|
65
|
+
this.initialize();
|
|
66
|
+
this.storage.transact(() => {
|
|
67
|
+
this.applyOutcome(outcome);
|
|
68
|
+
});
|
|
69
|
+
},
|
|
70
|
+
insertSessionCheckpointRecord: (checkpoint) => {
|
|
71
|
+
this.initialize();
|
|
72
|
+
this.storage.insertSessionCheckpoint(checkpoint);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
initialize() {
|
|
77
|
+
if (this.initialized)
|
|
78
|
+
return;
|
|
79
|
+
this.storage.applyMigrations();
|
|
80
|
+
this.initialized = true;
|
|
81
|
+
}
|
|
82
|
+
close() {
|
|
83
|
+
this.storage.close();
|
|
84
|
+
this.initialized = false;
|
|
85
|
+
}
|
|
86
|
+
getPaths() {
|
|
87
|
+
return this.storage.paths;
|
|
88
|
+
}
|
|
89
|
+
getAdminApi() {
|
|
90
|
+
return this.adminApi;
|
|
91
|
+
}
|
|
92
|
+
recordEvent(event) {
|
|
93
|
+
this.initialize();
|
|
94
|
+
this.storage.transact(() => {
|
|
95
|
+
const normalizedEvent = deriveEventProvenance(event);
|
|
96
|
+
if (normalizedEvent.capture_path) {
|
|
97
|
+
assertCapturePathAllowed(normalizedEvent.capture_path, this.allowedCapturePaths);
|
|
98
|
+
}
|
|
99
|
+
const inserted = this.storage.insertEventWithResult(normalizedEvent);
|
|
100
|
+
if (!inserted)
|
|
101
|
+
return;
|
|
102
|
+
const artifacts = buildIngestionArtifacts(normalizedEvent);
|
|
103
|
+
for (const claim of artifacts.claims) {
|
|
104
|
+
const existingClaims = this.storage.findCompatibleActiveSingletonClaims(claim.project_id, claim.canonical_key, claim.scope, claim.id);
|
|
105
|
+
for (const existing of existingClaims) {
|
|
106
|
+
this.storage.supersedeClaim(existing.id, claim.id, "replaced by deterministic ingestion", "compiler", "system");
|
|
107
|
+
}
|
|
108
|
+
this.storage.upsertClaim(claim);
|
|
109
|
+
}
|
|
110
|
+
for (const outcome of artifacts.outcomes) {
|
|
111
|
+
const inferredClaimIds = this.inferOutcomeClaimIds(normalizedEvent, outcome, artifacts.claims);
|
|
112
|
+
if (inferredClaimIds.length > 0) {
|
|
113
|
+
outcome.related_claim_ids = inferredClaimIds;
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
delete outcome.related_claim_ids;
|
|
117
|
+
}
|
|
118
|
+
this.applyOutcome(outcome);
|
|
119
|
+
}
|
|
120
|
+
this.applyThreadResolutionSignals(normalizedEvent, artifacts.outcomes);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
getStats() {
|
|
124
|
+
this.initialize();
|
|
125
|
+
return this.storage.getStats();
|
|
126
|
+
}
|
|
127
|
+
listEvents(projectId) {
|
|
128
|
+
this.initialize();
|
|
129
|
+
return this.storage.listEvents(projectId);
|
|
130
|
+
}
|
|
131
|
+
listClaims(projectId) {
|
|
132
|
+
this.initialize();
|
|
133
|
+
return this.storage.listClaims(projectId);
|
|
134
|
+
}
|
|
135
|
+
listOutcomes(projectId) {
|
|
136
|
+
this.initialize();
|
|
137
|
+
return this.storage.listOutcomes(projectId);
|
|
138
|
+
}
|
|
139
|
+
listClaimTransitions(projectId) {
|
|
140
|
+
this.initialize();
|
|
141
|
+
return this.storage.listClaimTransitions(projectId);
|
|
142
|
+
}
|
|
143
|
+
listActivationLogs(projectId) {
|
|
144
|
+
this.initialize();
|
|
145
|
+
return this.storage.listActivationLogs(projectId);
|
|
146
|
+
}
|
|
147
|
+
listSessionCheckpoints(projectId) {
|
|
148
|
+
this.initialize();
|
|
149
|
+
return this.storage.listSessionCheckpoints(projectId);
|
|
150
|
+
}
|
|
151
|
+
getClaim(claimId) {
|
|
152
|
+
this.initialize();
|
|
153
|
+
return this.storage.getClaimById(claimId);
|
|
154
|
+
}
|
|
155
|
+
buildSessionBrief(input) {
|
|
156
|
+
this.initialize();
|
|
157
|
+
const packet = this.buildRawRecallPacket({
|
|
158
|
+
projectId: input.project_id,
|
|
159
|
+
agentId: input.agent_id,
|
|
160
|
+
scope: input.scope,
|
|
161
|
+
debug: input.debug,
|
|
162
|
+
mode: "session_brief",
|
|
163
|
+
});
|
|
164
|
+
const checkpointResult = this.verifyCheckpointForSessionStart({
|
|
165
|
+
project_id: input.project_id,
|
|
166
|
+
workspace_id: input.workspace_id,
|
|
167
|
+
branch: input.scope?.branch,
|
|
168
|
+
cwd: input.cwd,
|
|
169
|
+
});
|
|
170
|
+
return this.finalizeRecallPacket(input.project_id, packet, {
|
|
171
|
+
checkpoint: checkpointResult.checkpoint,
|
|
172
|
+
warnings: checkpointResult.warnings,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
buildProjectSnapshot(input) {
|
|
176
|
+
this.initialize();
|
|
177
|
+
const packet = this.buildRawRecallPacket({
|
|
178
|
+
projectId: input.project_id,
|
|
179
|
+
agentId: input.agent_id,
|
|
180
|
+
scope: input.scope,
|
|
181
|
+
debug: input.debug,
|
|
182
|
+
mode: "project_snapshot",
|
|
183
|
+
});
|
|
184
|
+
const checkpoint = this.resolveSnapshotCheckpoint({
|
|
185
|
+
project_id: input.project_id,
|
|
186
|
+
workspace_id: input.workspace_id,
|
|
187
|
+
branch: input.scope?.branch,
|
|
188
|
+
cwd: input.cwd,
|
|
189
|
+
});
|
|
190
|
+
return this.finalizeRecallPacket(input.project_id, packet, checkpoint);
|
|
191
|
+
}
|
|
192
|
+
searchClaims(input) {
|
|
193
|
+
this.initialize();
|
|
194
|
+
const packet = this.buildRawRecallPacket({
|
|
195
|
+
projectId: input.project_id,
|
|
196
|
+
agentId: "memory.search",
|
|
197
|
+
scope: input.scope,
|
|
198
|
+
debug: input.debug,
|
|
199
|
+
query: input.query,
|
|
200
|
+
mode: "search",
|
|
201
|
+
});
|
|
202
|
+
if (typeof input.limit === "number" && input.limit >= 0) {
|
|
203
|
+
return this.finalizeRecallPacket(input.project_id, {
|
|
204
|
+
...packet,
|
|
205
|
+
active_claims: packet.active_claims.slice(0, input.limit),
|
|
206
|
+
open_threads: packet.open_threads.slice(0, input.limit),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return this.finalizeRecallPacket(input.project_id, packet);
|
|
210
|
+
}
|
|
211
|
+
touchActivatedClaims(projectId, claimIds, activatedAt = nowIso()) {
|
|
212
|
+
this.initialize();
|
|
213
|
+
return this.storage.touchClaims(projectId, claimIds, activatedAt);
|
|
214
|
+
}
|
|
215
|
+
getLatestCheckpoint(input) {
|
|
216
|
+
this.initialize();
|
|
217
|
+
return this.storage.getLatestSessionCheckpoint(input.project_id, input.workspace_id, input.status);
|
|
218
|
+
}
|
|
219
|
+
recordSessionCheckpoint(input) {
|
|
220
|
+
this.initialize();
|
|
221
|
+
const branch = input.scope?.branch ?? currentBranch(input.cwd);
|
|
222
|
+
const packet = this.buildRawRecallPacket({
|
|
223
|
+
projectId: input.project_id,
|
|
224
|
+
agentId: input.agent_id,
|
|
225
|
+
scope: input.scope,
|
|
226
|
+
mode: "session_brief",
|
|
227
|
+
});
|
|
228
|
+
const fallbackClaims = this.selectCheckpointClaims(input.project_id, input.scope);
|
|
229
|
+
const packetClaims = [...packet.open_threads, ...packet.active_claims];
|
|
230
|
+
const hotClaims = (packetClaims.length > 0 ? packetClaims : fallbackClaims).slice(0, 5);
|
|
231
|
+
const hotFiles = Array.from(new Set(hotClaims.flatMap((claim) => claim.scope?.files ?? []))).map((filePath) => normalizeCheckpointFilePath(input.cwd, filePath));
|
|
232
|
+
const repoHead = currentRepoHead(input.cwd);
|
|
233
|
+
const summary = (input.summary_hint ?? "").trim() || packet.brief;
|
|
234
|
+
const currentGoal = hotClaims.find((claim) => claim.type === "thread")?.content ??
|
|
235
|
+
hotClaims.find((claim) => claim.type === "decision")?.content ??
|
|
236
|
+
hotClaims[0]?.content;
|
|
237
|
+
const nextAction = hotClaims.find((claim) => claim.type === "thread")
|
|
238
|
+
? `继续处理:${hotClaims.find((claim) => claim.type === "thread")?.content}`
|
|
239
|
+
: undefined;
|
|
240
|
+
const blockingReason = (input.blocking_hint ?? "").trim() ||
|
|
241
|
+
packet.open_threads.find((thread) => (thread.outcome_summary?.negative_count ?? 0) > 0)?.content ||
|
|
242
|
+
fallbackClaims.find((claim) => claim.type === "thread" && claim.thread_status === "blocked")?.content ||
|
|
243
|
+
packet.warnings?.[0];
|
|
244
|
+
const packetHash = checkpointPacketHash({
|
|
245
|
+
summary,
|
|
246
|
+
currentGoal,
|
|
247
|
+
nextAction,
|
|
248
|
+
blockingReason,
|
|
249
|
+
hotClaimIds: hotClaims.map((claim) => claim.id),
|
|
250
|
+
hotFiles,
|
|
251
|
+
evidenceRefs: packet.recent_evidence_refs.length > 0
|
|
252
|
+
? packet.recent_evidence_refs
|
|
253
|
+
: hotClaims.flatMap((claim) => claim.source_event_ids).slice(0, 10),
|
|
254
|
+
branch,
|
|
255
|
+
repoHead,
|
|
256
|
+
});
|
|
257
|
+
const checkpoint = {
|
|
258
|
+
id: hashId("chk", input.project_id, input.session_id, input.source, packetHash),
|
|
259
|
+
created_at: nowIso(),
|
|
260
|
+
project_id: input.project_id,
|
|
261
|
+
session_id: input.session_id,
|
|
262
|
+
workspace_id: input.workspace_id,
|
|
263
|
+
branch,
|
|
264
|
+
repo_head: repoHead,
|
|
265
|
+
status: "active",
|
|
266
|
+
source: input.source,
|
|
267
|
+
summary,
|
|
268
|
+
current_goal: currentGoal,
|
|
269
|
+
next_action: nextAction,
|
|
270
|
+
blocking_reason: blockingReason,
|
|
271
|
+
hot_claim_ids: hotClaims.map((claim) => claim.id),
|
|
272
|
+
hot_files: hotFiles,
|
|
273
|
+
evidence_refs: packet.recent_evidence_refs.length > 0
|
|
274
|
+
? packet.recent_evidence_refs
|
|
275
|
+
: hotClaims.flatMap((claim) => claim.source_event_ids).slice(0, 10),
|
|
276
|
+
packet_hash: packetHash,
|
|
277
|
+
hot_file_digests: computeCheckpointFileDigests(input.cwd, hotFiles),
|
|
278
|
+
};
|
|
279
|
+
this.storage.upsertSessionCheckpoint(checkpoint);
|
|
280
|
+
return checkpoint;
|
|
281
|
+
}
|
|
282
|
+
verifyCheckpointForSessionStart(input) {
|
|
283
|
+
this.initialize();
|
|
284
|
+
const checkpoint = this.storage.getLatestSessionCheckpoint(input.project_id, input.workspace_id, "active");
|
|
285
|
+
if (!checkpoint) {
|
|
286
|
+
return { warnings: [] };
|
|
287
|
+
}
|
|
288
|
+
const warnings = [];
|
|
289
|
+
const currentBranchName = input.branch ?? currentBranch(input.cwd);
|
|
290
|
+
const hideNextAction = this.updateCheckpointForCurrentWorkspace(checkpoint, currentBranchName, input.cwd, warnings);
|
|
291
|
+
return {
|
|
292
|
+
checkpoint: toRecallCheckpoint(checkpoint, { hideNextAction }),
|
|
293
|
+
warnings,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
sweepStaleClaims(referenceTime = nowIso()) {
|
|
297
|
+
this.initialize();
|
|
298
|
+
const claims = this.storage.listClaims();
|
|
299
|
+
let changed = 0;
|
|
300
|
+
for (const claim of claims) {
|
|
301
|
+
if (claim.status !== "active")
|
|
302
|
+
continue;
|
|
303
|
+
if (claim.pinned)
|
|
304
|
+
continue;
|
|
305
|
+
const ttlDays = STALE_TTL_DAYS[claim.type];
|
|
306
|
+
const anchor = claim.last_verified_at ?? claim.created_at;
|
|
307
|
+
if (daysBetween(anchor, referenceTime) < ttlDays)
|
|
308
|
+
continue;
|
|
309
|
+
const updated = {
|
|
310
|
+
...claim,
|
|
311
|
+
status: "stale",
|
|
312
|
+
};
|
|
313
|
+
assertClaimTransitionAllowed(claim.status, "stale", "stale_sweep");
|
|
314
|
+
validateClaimRecord(updated);
|
|
315
|
+
this.storage.upsertClaim(updated);
|
|
316
|
+
this.storage.insertClaimTransition(buildTransition(claim, "stale", "stale TTL expired", "stale_sweep", undefined, "system"));
|
|
317
|
+
changed += 1;
|
|
318
|
+
}
|
|
319
|
+
return changed;
|
|
320
|
+
}
|
|
321
|
+
recordOutcome(input) {
|
|
322
|
+
this.initialize();
|
|
323
|
+
const outcome = {
|
|
324
|
+
id: hashId("out", input.project_id, nowIso(), input.outcome_type, ...input.related_event_ids),
|
|
325
|
+
ts: nowIso(),
|
|
326
|
+
project_id: input.project_id,
|
|
327
|
+
related_event_ids: input.related_event_ids,
|
|
328
|
+
related_claim_ids: input.related_claim_ids,
|
|
329
|
+
outcome_type: input.outcome_type,
|
|
330
|
+
strength: input.strength,
|
|
331
|
+
notes: input.notes,
|
|
332
|
+
};
|
|
333
|
+
this.storage.transact(() => {
|
|
334
|
+
this.applyOutcome(outcome);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
verifyClaim(input) {
|
|
338
|
+
this.initialize();
|
|
339
|
+
assertVerificationStatus(input.status);
|
|
340
|
+
const claim = this.storage.getClaimById(input.claim_id);
|
|
341
|
+
if (!claim)
|
|
342
|
+
return undefined;
|
|
343
|
+
const ts = input.ts ?? nowIso();
|
|
344
|
+
const updated = {
|
|
345
|
+
...claim,
|
|
346
|
+
verification_status: input.status,
|
|
347
|
+
verification_method: input.method,
|
|
348
|
+
last_verified_at: input.status === "disputed" ? claim.last_verified_at : ts,
|
|
349
|
+
};
|
|
350
|
+
if (input.status === "disputed" && claim.status === "active") {
|
|
351
|
+
assertClaimTransitionAllowed(claim.status, "stale", "verify_claim disputed");
|
|
352
|
+
updated.status = "stale";
|
|
353
|
+
}
|
|
354
|
+
else if (input.status !== "disputed" &&
|
|
355
|
+
(claim.status === "stale" || claim.status === "archived")) {
|
|
356
|
+
assertClaimTransitionAllowed(claim.status, "active", "verify_claim re-verified");
|
|
357
|
+
updated.status = "active";
|
|
358
|
+
}
|
|
359
|
+
validateClaimRecord(updated);
|
|
360
|
+
this.storage.transact(() => {
|
|
361
|
+
this.storage.upsertClaim(updated);
|
|
362
|
+
if (claim.status !== updated.status) {
|
|
363
|
+
this.storage.insertClaimTransition(buildTransition(claim, updated.status, input.status === "disputed"
|
|
364
|
+
? `marked disputed by ${input.method}`
|
|
365
|
+
: `re-verified by ${input.method}`, "verify_claim", claim.id, input.actor ?? "operator"));
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
return updated;
|
|
369
|
+
}
|
|
370
|
+
markClaimStale(input) {
|
|
371
|
+
this.initialize();
|
|
372
|
+
const claim = this.storage.getClaimById(input.claim_id);
|
|
373
|
+
if (!claim)
|
|
374
|
+
return undefined;
|
|
375
|
+
if (claim.status === "stale")
|
|
376
|
+
return claim;
|
|
377
|
+
if (claim.status === "archived" || claim.status === "superseded") {
|
|
378
|
+
throw new RuntimeInvariantError(`cannot mark ${claim.status} claim stale: ${claim.id}`);
|
|
379
|
+
}
|
|
380
|
+
assertClaimTransitionAllowed(claim.status, "stale", "mark_claim_stale");
|
|
381
|
+
const updated = {
|
|
382
|
+
...claim,
|
|
383
|
+
status: "stale",
|
|
384
|
+
};
|
|
385
|
+
validateClaimRecord(updated);
|
|
386
|
+
this.storage.transact(() => {
|
|
387
|
+
this.storage.upsertClaim(updated);
|
|
388
|
+
this.storage.insertClaimTransition(buildTransition(claim, "stale", input.reason, "mark_claim_stale", claim.id, input.actor ?? "operator"));
|
|
389
|
+
});
|
|
390
|
+
return updated;
|
|
391
|
+
}
|
|
392
|
+
explainClaim(claimId) {
|
|
393
|
+
this.initialize();
|
|
394
|
+
const claim = this.storage.getClaimById(claimId);
|
|
395
|
+
if (!claim)
|
|
396
|
+
return undefined;
|
|
397
|
+
const sourceEventIds = new Set(claim.source_event_ids);
|
|
398
|
+
const transitions = this.storage
|
|
399
|
+
.listClaimTransitions(claim.project_id)
|
|
400
|
+
.filter((entry) => entry.claim_id === claim.id);
|
|
401
|
+
const activationLogs = this.storage
|
|
402
|
+
.listActivationLogs(claim.project_id)
|
|
403
|
+
.filter((entry) => entry.claim_id === claim.id);
|
|
404
|
+
const relatedOutcomes = this.storage
|
|
405
|
+
.listOutcomes(claim.project_id)
|
|
406
|
+
.filter((outcome) => outcome.related_claim_ids?.includes(claim.id) ||
|
|
407
|
+
outcome.related_event_ids.some((eventId) => sourceEventIds.has(eventId)));
|
|
408
|
+
// Build outcome timeline
|
|
409
|
+
const timeline = [];
|
|
410
|
+
// Add creation event
|
|
411
|
+
timeline.push({
|
|
412
|
+
ts: claim.created_at,
|
|
413
|
+
event_type: "created",
|
|
414
|
+
description: `Claim created (${claim.verification_status})`,
|
|
415
|
+
score_before: undefined,
|
|
416
|
+
score_after: 0,
|
|
417
|
+
});
|
|
418
|
+
// Add transitions
|
|
419
|
+
for (const t of transitions) {
|
|
420
|
+
timeline.push({
|
|
421
|
+
ts: t.ts,
|
|
422
|
+
event_type: `transition:${t.to_status}`,
|
|
423
|
+
description: `${t.from_status ?? "—"} → ${t.to_status}: ${t.reason}`,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
// Add outcomes with score progression
|
|
427
|
+
const sortedOutcomes = [...relatedOutcomes].sort((a, b) => a.ts.localeCompare(b.ts));
|
|
428
|
+
let runningScore = 0;
|
|
429
|
+
for (const outcome of sortedOutcomes) {
|
|
430
|
+
const scoreBefore = runningScore;
|
|
431
|
+
if (isPositiveOutcome(outcome.outcome_type)) {
|
|
432
|
+
runningScore = scorePositive(runningScore, outcome.strength);
|
|
433
|
+
}
|
|
434
|
+
else if (isNegativeOutcome(outcome.outcome_type)) {
|
|
435
|
+
runningScore = scoreNegative(runningScore, outcome.strength);
|
|
436
|
+
}
|
|
437
|
+
timeline.push({
|
|
438
|
+
ts: outcome.ts,
|
|
439
|
+
event_type: outcome.outcome_type,
|
|
440
|
+
description: `${outcome.outcome_type}${outcome.notes ? `: ${outcome.notes}` : ""}`,
|
|
441
|
+
score_before: scoreBefore,
|
|
442
|
+
score_after: runningScore,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
// Sort timeline chronologically
|
|
446
|
+
timeline.sort((a, b) => a.ts.localeCompare(b.ts));
|
|
447
|
+
return {
|
|
448
|
+
claim,
|
|
449
|
+
transitions,
|
|
450
|
+
activation_logs: activationLogs,
|
|
451
|
+
related_outcomes: relatedOutcomes,
|
|
452
|
+
outcome_timeline: timeline,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
buildRawRecallPacket(input) {
|
|
456
|
+
const claims = this.storage.listClaims(input.projectId);
|
|
457
|
+
const outcomes = this.storage.listOutcomes(input.projectId);
|
|
458
|
+
return buildRecallPacket({
|
|
459
|
+
projectId: input.projectId,
|
|
460
|
+
agentId: input.agentId,
|
|
461
|
+
claims,
|
|
462
|
+
outcomes,
|
|
463
|
+
scope: input.scope,
|
|
464
|
+
debug: input.debug,
|
|
465
|
+
query: input.query,
|
|
466
|
+
mode: input.mode,
|
|
467
|
+
}, (log) => this.storage.insertActivationLog(log));
|
|
468
|
+
}
|
|
469
|
+
finalizeRecallPacket(projectId, packet, extras = {}) {
|
|
470
|
+
const activationTs = packet.generated_at;
|
|
471
|
+
const activatedClaimIds = Array.from(new Set([...packet.active_claims, ...packet.open_threads].map((claim) => claim.id)));
|
|
472
|
+
if (activatedClaimIds.length > 0) {
|
|
473
|
+
this.touchActivatedClaims(projectId, activatedClaimIds, activationTs);
|
|
474
|
+
}
|
|
475
|
+
const stampClaim = (claim) => ({
|
|
476
|
+
...claim,
|
|
477
|
+
last_activated_at: activationTs,
|
|
478
|
+
});
|
|
479
|
+
const warnings = Array.from(new Set([...(packet.warnings ?? []), ...(extras.warnings ?? [])]));
|
|
480
|
+
return {
|
|
481
|
+
...packet,
|
|
482
|
+
checkpoint: extras.checkpoint,
|
|
483
|
+
active_claims: packet.active_claims.map(stampClaim),
|
|
484
|
+
open_threads: packet.open_threads.map(stampClaim),
|
|
485
|
+
warnings: warnings.length > 0 ? warnings : undefined,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
resolveSnapshotCheckpoint(input) {
|
|
489
|
+
const latest = this.getLatestCheckpoint({
|
|
490
|
+
project_id: input.project_id,
|
|
491
|
+
workspace_id: input.workspace_id,
|
|
492
|
+
});
|
|
493
|
+
if (!latest)
|
|
494
|
+
return {};
|
|
495
|
+
if (latest.status === "active") {
|
|
496
|
+
return this.verifyCheckpointForSessionStart(input);
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
checkpoint: toRecallCheckpoint(latest, { hideNextAction: true }),
|
|
500
|
+
warnings: latest.stale_reason ? [latest.stale_reason] : undefined,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
updateCheckpointForCurrentWorkspace(checkpoint, currentBranchName, cwd, warnings) {
|
|
504
|
+
let hideNextAction = false;
|
|
505
|
+
let staleReason;
|
|
506
|
+
if (checkpoint.branch &&
|
|
507
|
+
currentBranchName &&
|
|
508
|
+
checkpoint.branch !== currentBranchName) {
|
|
509
|
+
checkpoint.status = "stale";
|
|
510
|
+
checkpoint.stale_reason = `checkpoint stale: branch changed (${checkpoint.branch} -> ${currentBranchName})`;
|
|
511
|
+
this.storage.upsertSessionCheckpoint(checkpoint);
|
|
512
|
+
warnings.push(checkpoint.stale_reason);
|
|
513
|
+
return true;
|
|
514
|
+
}
|
|
515
|
+
const currentHead = currentRepoHead(cwd);
|
|
516
|
+
const headChanged = Boolean(checkpoint.repo_head && currentHead && checkpoint.repo_head !== currentHead);
|
|
517
|
+
const changedFiles = verifyCheckpointFileDigests(cwd, checkpoint.hot_files, checkpoint.hot_file_digests);
|
|
518
|
+
if (changedFiles.length > 0) {
|
|
519
|
+
staleReason = `checkpoint stale: hot files changed (${changedFiles.join(", ")})`;
|
|
520
|
+
checkpoint.status = "stale";
|
|
521
|
+
checkpoint.stale_reason = staleReason;
|
|
522
|
+
this.storage.upsertSessionCheckpoint(checkpoint);
|
|
523
|
+
warnings.push(staleReason);
|
|
524
|
+
hideNextAction = true;
|
|
525
|
+
}
|
|
526
|
+
else if (headChanged) {
|
|
527
|
+
warnings.push("checkpoint warning: repository HEAD changed since checkpoint creation");
|
|
528
|
+
}
|
|
529
|
+
return hideNextAction;
|
|
530
|
+
}
|
|
531
|
+
selectCheckpointClaims(projectId, scope) {
|
|
532
|
+
const claims = this.storage
|
|
533
|
+
.listClaims(projectId)
|
|
534
|
+
.filter((claim) => claim.status === "active")
|
|
535
|
+
.filter((claim) => {
|
|
536
|
+
if (!scope)
|
|
537
|
+
return true;
|
|
538
|
+
if (scope.repo && claim.scope?.repo && scope.repo !== claim.scope.repo)
|
|
539
|
+
return false;
|
|
540
|
+
if (scope.branch && claim.scope?.branch && scope.branch !== claim.scope.branch)
|
|
541
|
+
return false;
|
|
542
|
+
if (scope.cwd_prefix && claim.scope?.cwd_prefix) {
|
|
543
|
+
const left = scope.cwd_prefix;
|
|
544
|
+
const right = claim.scope.cwd_prefix;
|
|
545
|
+
const compatible = left === right ||
|
|
546
|
+
left.startsWith(`${right}/`) ||
|
|
547
|
+
right.startsWith(`${left}/`);
|
|
548
|
+
if (!compatible)
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
return true;
|
|
552
|
+
});
|
|
553
|
+
return claims.sort((left, right) => {
|
|
554
|
+
const leftPriority = left.type === "thread" ? 0 : left.type === "decision" ? 1 : 2;
|
|
555
|
+
const rightPriority = right.type === "thread" ? 0 : right.type === "decision" ? 1 : 2;
|
|
556
|
+
if (leftPriority !== rightPriority)
|
|
557
|
+
return leftPriority - rightPriority;
|
|
558
|
+
if (right.importance !== left.importance)
|
|
559
|
+
return right.importance - left.importance;
|
|
560
|
+
if (right.confidence !== left.confidence)
|
|
561
|
+
return right.confidence - left.confidence;
|
|
562
|
+
return right.created_at.localeCompare(left.created_at);
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
applyOutcome(outcome) {
|
|
566
|
+
this.initialize();
|
|
567
|
+
this.storage.upsertOutcome(outcome);
|
|
568
|
+
const relatedClaims = this.resolveOutcomeClaims(outcome);
|
|
569
|
+
for (const claim of relatedClaims) {
|
|
570
|
+
const updated = { ...claim };
|
|
571
|
+
if (isPositiveOutcome(outcome.outcome_type)) {
|
|
572
|
+
updated.outcome_score = scorePositive(updated.outcome_score, outcome.strength);
|
|
573
|
+
updated.last_verified_at = outcome.ts;
|
|
574
|
+
if (updated.status === "stale") {
|
|
575
|
+
assertClaimTransitionAllowed(claim.status, "active", "positive outcome");
|
|
576
|
+
this.storage.insertClaimTransition(buildTransition(claim, "active", "reactivated by positive outcome", "outcome", outcome.id, "system"));
|
|
577
|
+
updated.status = "active";
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
else if (isNegativeOutcome(outcome.outcome_type)) {
|
|
581
|
+
updated.outcome_score = scoreNegative(updated.outcome_score, outcome.strength);
|
|
582
|
+
if (updated.status === "active") {
|
|
583
|
+
assertClaimTransitionAllowed(claim.status, "stale", "negative outcome");
|
|
584
|
+
this.storage.insertClaimTransition(buildTransition(claim, "stale", "downgraded by negative outcome", "outcome", outcome.id, "system"));
|
|
585
|
+
updated.status = "stale";
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
validateClaimRecord(updated);
|
|
589
|
+
this.storage.upsertClaim(updated);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
resolveOutcomeClaims(outcome) {
|
|
593
|
+
const seen = new Set();
|
|
594
|
+
const claims = [];
|
|
595
|
+
for (const claimId of outcome.related_claim_ids ?? []) {
|
|
596
|
+
const claim = this.storage.getClaimById(claimId);
|
|
597
|
+
if (!claim || seen.has(claim.id))
|
|
598
|
+
continue;
|
|
599
|
+
seen.add(claim.id);
|
|
600
|
+
claims.push(claim);
|
|
601
|
+
}
|
|
602
|
+
if (claims.length > 0)
|
|
603
|
+
return claims;
|
|
604
|
+
const allClaims = this.storage.listClaims(outcome.project_id);
|
|
605
|
+
for (const claim of allClaims) {
|
|
606
|
+
if (claim.source_event_ids.some((eventId) => outcome.related_event_ids.includes(eventId))) {
|
|
607
|
+
if (this.shouldSkipFallbackOutcomeLink(claim, outcome))
|
|
608
|
+
continue;
|
|
609
|
+
if (seen.has(claim.id))
|
|
610
|
+
continue;
|
|
611
|
+
seen.add(claim.id);
|
|
612
|
+
claims.push(claim);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return claims;
|
|
616
|
+
}
|
|
617
|
+
inferOutcomeClaimIds(event, outcome, newClaims) {
|
|
618
|
+
if (outcome.related_claim_ids?.length) {
|
|
619
|
+
return Array.from(new Set(outcome.related_claim_ids));
|
|
620
|
+
}
|
|
621
|
+
if (!["manual_override", "commit_reverted"].includes(outcome.outcome_type)) {
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
const targetCanonicalKey = asString(event.metadata?.overrides_canonical_key);
|
|
625
|
+
if (!targetCanonicalKey)
|
|
626
|
+
return [];
|
|
627
|
+
const avoidanceClaim = newClaims.find((claim) => claim.canonical_key === `decision.avoid.${targetCanonicalKey}`);
|
|
628
|
+
const targetScopeSignature = scopeSignature(avoidanceClaim?.scope);
|
|
629
|
+
const relatedClaims = this.storage
|
|
630
|
+
.listClaims(event.project_id)
|
|
631
|
+
.filter((claim) => claim.status === "active" &&
|
|
632
|
+
claim.canonical_key === targetCanonicalKey &&
|
|
633
|
+
(!avoidanceClaim || scopeSignature(claim.scope) === targetScopeSignature) &&
|
|
634
|
+
!claim.source_event_ids.includes(event.id))
|
|
635
|
+
.map((claim) => claim.id);
|
|
636
|
+
return Array.from(new Set(relatedClaims));
|
|
637
|
+
}
|
|
638
|
+
shouldSkipFallbackOutcomeLink(claim, outcome) {
|
|
639
|
+
if (!isNegativeOutcome(outcome.outcome_type))
|
|
640
|
+
return false;
|
|
641
|
+
if (claim.canonical_key.startsWith("decision.avoid."))
|
|
642
|
+
return true;
|
|
643
|
+
if (claim.type === "thread" && claim.thread_status === "open")
|
|
644
|
+
return true;
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
applyThreadResolutionSignals(event, outcomes) {
|
|
648
|
+
const claims = this.storage.listClaims(event.project_id).filter((claim) => claim.type === "thread" &&
|
|
649
|
+
claim.thread_status !== "resolved" &&
|
|
650
|
+
claim.status !== "archived" &&
|
|
651
|
+
claim.status !== "superseded");
|
|
652
|
+
for (const claim of claims) {
|
|
653
|
+
if (!this.threadShouldResolve(claim, event, outcomes))
|
|
654
|
+
continue;
|
|
655
|
+
this.resolveThreadClaim(claim, event.ts, outcomes[0]?.id);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
threadShouldResolve(claim, event, outcomes) {
|
|
659
|
+
const matchedOutcomeTypes = new Set(outcomes.map((outcome) => outcome.outcome_type));
|
|
660
|
+
const explicitlyRelated = outcomes.some((outcome) => outcome.related_claim_ids?.includes(claim.id));
|
|
661
|
+
const issueId = extractIssueId(event);
|
|
662
|
+
const failingTest = asString(event.metadata?.failing_test);
|
|
663
|
+
for (const rule of claim.resolution_rules ?? []) {
|
|
664
|
+
if (rule.type === "issue_closed") {
|
|
665
|
+
if (issueId === rule.issue_id && (event.event_type === "issue_closed" || matchedOutcomeTypes.has("issue_closed"))) {
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
if (rule.type === "pr_merged") {
|
|
670
|
+
const prId = asString(event.metadata?.pr_id);
|
|
671
|
+
if (prId === rule.pr_id && event.event_type === "pr_merged")
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
if (rule.type === "branch_deleted") {
|
|
675
|
+
const branch = event.scope?.branch ?? asString(event.metadata?.branch);
|
|
676
|
+
if (branch === rule.branch && asString(event.metadata?.branch_deleted) === "true")
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
if (rule.type === "commit_contains") {
|
|
680
|
+
if (event.event_type === "git_commit" && event.content.includes(rule.pattern))
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
if (rule.type === "test_pass") {
|
|
684
|
+
if (matchedOutcomeTypes.has("test_pass") && explicitlyRelated)
|
|
685
|
+
return true;
|
|
686
|
+
if (isSuccessfulTestResult(event) && failingTest === rule.test_name)
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
resolveThreadClaim(claim, resolvedAt, outcomeId) {
|
|
693
|
+
const updated = {
|
|
694
|
+
...claim,
|
|
695
|
+
thread_status: "resolved",
|
|
696
|
+
resolved_at: resolvedAt,
|
|
697
|
+
status: "archived",
|
|
698
|
+
last_verified_at: resolvedAt,
|
|
699
|
+
};
|
|
700
|
+
assertClaimTransitionAllowed(claim.status, "archived", "resolution_rule");
|
|
701
|
+
validateClaimRecord(updated);
|
|
702
|
+
if (claim.status !== "archived") {
|
|
703
|
+
this.storage.insertClaimTransition(buildTransition(claim, "archived", "resolved by lifecycle signal", "resolution_rule", outcomeId, "system"));
|
|
704
|
+
}
|
|
705
|
+
this.storage.upsertClaim(updated);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
//# sourceMappingURL=runtime.js.map
|