agent-recall-core 3.4.31 → 3.4.36
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 +121 -441
- package/dist/digest/store.d.ts.map +1 -1
- package/dist/digest/store.js +4 -3
- package/dist/digest/store.js.map +1 -1
- package/dist/display/board-render.d.ts +61 -0
- package/dist/display/board-render.d.ts.map +1 -0
- package/dist/display/board-render.js +223 -0
- package/dist/display/board-render.js.map +1 -0
- package/dist/helpers/alignment-patterns.d.ts +10 -0
- package/dist/helpers/alignment-patterns.d.ts.map +1 -1
- package/dist/helpers/alignment-patterns.js +5 -1
- package/dist/helpers/alignment-patterns.js.map +1 -1
- package/dist/helpers/blind-spots.d.ts +86 -0
- package/dist/helpers/blind-spots.d.ts.map +1 -0
- package/dist/helpers/blind-spots.js +205 -0
- package/dist/helpers/blind-spots.js.map +1 -0
- package/dist/helpers/conflict-scan.d.ts +10 -0
- package/dist/helpers/conflict-scan.d.ts.map +1 -1
- package/dist/helpers/conflict-scan.js +3 -3
- package/dist/helpers/conflict-scan.js.map +1 -1
- package/dist/helpers/semantic-match.d.ts +82 -0
- package/dist/helpers/semantic-match.d.ts.map +1 -0
- package/dist/helpers/semantic-match.js +229 -0
- package/dist/helpers/semantic-match.js.map +1 -0
- package/dist/index.d.ts +57 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +60 -14
- package/dist/index.js.map +1 -1
- package/dist/palace/awareness.d.ts +30 -0
- package/dist/palace/awareness.d.ts.map +1 -1
- package/dist/palace/awareness.js +65 -2
- package/dist/palace/awareness.js.map +1 -1
- package/dist/palace/consolidate.d.ts.map +1 -1
- package/dist/palace/consolidate.js +17 -0
- package/dist/palace/consolidate.js.map +1 -1
- package/dist/palace/decay-pass.d.ts +46 -0
- package/dist/palace/decay-pass.d.ts.map +1 -0
- package/dist/palace/decay-pass.js +87 -0
- package/dist/palace/decay-pass.js.map +1 -0
- package/dist/palace/insights-index.d.ts.map +1 -1
- package/dist/palace/insights-index.js +4 -2
- package/dist/palace/insights-index.js.map +1 -1
- package/dist/palace/skills.d.ts +41 -2
- package/dist/palace/skills.d.ts.map +1 -1
- package/dist/palace/skills.js +106 -4
- package/dist/palace/skills.js.map +1 -1
- package/dist/prompts/consolidation-prompt.d.ts +25 -0
- package/dist/prompts/consolidation-prompt.d.ts.map +1 -0
- package/dist/prompts/consolidation-prompt.js +91 -0
- package/dist/prompts/consolidation-prompt.js.map +1 -0
- package/dist/storage/ab-experiment.d.ts +184 -0
- package/dist/storage/ab-experiment.d.ts.map +1 -0
- package/dist/storage/ab-experiment.js +294 -0
- package/dist/storage/ab-experiment.js.map +1 -0
- package/dist/storage/archive-prune.d.ts +56 -0
- package/dist/storage/archive-prune.d.ts.map +1 -0
- package/dist/storage/archive-prune.js +118 -0
- package/dist/storage/archive-prune.js.map +1 -0
- package/dist/storage/archive-write.d.ts +38 -0
- package/dist/storage/archive-write.d.ts.map +1 -0
- package/dist/storage/archive-write.js +98 -0
- package/dist/storage/archive-write.js.map +1 -0
- package/dist/storage/blind-spots-store.d.ts +30 -0
- package/dist/storage/blind-spots-store.d.ts.map +1 -0
- package/dist/storage/blind-spots-store.js +91 -0
- package/dist/storage/blind-spots-store.js.map +1 -0
- package/dist/storage/capture-router.d.ts +60 -0
- package/dist/storage/capture-router.d.ts.map +1 -0
- package/dist/storage/capture-router.js +133 -0
- package/dist/storage/capture-router.js.map +1 -0
- package/dist/storage/classification.d.ts +36 -0
- package/dist/storage/classification.d.ts.map +1 -0
- package/dist/storage/classification.js +78 -0
- package/dist/storage/classification.js.map +1 -0
- package/dist/storage/consolidation-queue.d.ts +39 -0
- package/dist/storage/consolidation-queue.d.ts.map +1 -0
- package/dist/storage/consolidation-queue.js +120 -0
- package/dist/storage/consolidation-queue.js.map +1 -0
- package/dist/storage/content-guard.d.ts +74 -0
- package/dist/storage/content-guard.d.ts.map +1 -0
- package/dist/storage/content-guard.js +179 -0
- package/dist/storage/content-guard.js.map +1 -0
- package/dist/storage/corrections.d.ts +329 -22
- package/dist/storage/corrections.d.ts.map +1 -1
- package/dist/storage/corrections.js +788 -58
- package/dist/storage/corrections.js.map +1 -1
- package/dist/storage/durable-intent.d.ts +49 -0
- package/dist/storage/durable-intent.d.ts.map +1 -0
- package/dist/storage/durable-intent.js +124 -0
- package/dist/storage/durable-intent.js.map +1 -0
- package/dist/storage/filelock.d.ts +6 -0
- package/dist/storage/filelock.d.ts.map +1 -1
- package/dist/storage/filelock.js +6 -1
- package/dist/storage/filelock.js.map +1 -1
- package/dist/storage/memory-protocol.d.ts +19 -0
- package/dist/storage/memory-protocol.d.ts.map +1 -0
- package/dist/storage/memory-protocol.js +101 -0
- package/dist/storage/memory-protocol.js.map +1 -0
- package/dist/storage/paths.d.ts +23 -0
- package/dist/storage/paths.d.ts.map +1 -1
- package/dist/storage/paths.js +39 -0
- package/dist/storage/paths.js.map +1 -1
- package/dist/storage/retention.d.ts +23 -0
- package/dist/storage/retention.d.ts.map +1 -0
- package/dist/storage/retention.js +48 -0
- package/dist/storage/retention.js.map +1 -0
- package/dist/storage/session.d.ts +2 -2
- package/dist/storage/session.d.ts.map +1 -1
- package/dist/storage/session.js +11 -1
- package/dist/storage/session.js.map +1 -1
- package/dist/supabase/config.d.ts +26 -0
- package/dist/supabase/config.d.ts.map +1 -1
- package/dist/supabase/config.js +11 -0
- package/dist/supabase/config.js.map +1 -1
- package/dist/supabase/recall-backend.d.ts +1 -0
- package/dist/supabase/recall-backend.d.ts.map +1 -1
- package/dist/supabase/recall-backend.js +13 -13
- package/dist/supabase/recall-backend.js.map +1 -1
- package/dist/supabase/sync.d.ts +1 -1
- package/dist/supabase/sync.d.ts.map +1 -1
- package/dist/supabase/sync.js +85 -3
- package/dist/supabase/sync.js.map +1 -1
- package/dist/tools-logic/bootstrap.d.ts +15 -0
- package/dist/tools-logic/bootstrap.d.ts.map +1 -1
- package/dist/tools-logic/bootstrap.js +238 -47
- package/dist/tools-logic/bootstrap.js.map +1 -1
- package/dist/tools-logic/brief.d.ts +84 -0
- package/dist/tools-logic/brief.d.ts.map +1 -0
- package/dist/tools-logic/brief.js +193 -0
- package/dist/tools-logic/brief.js.map +1 -0
- package/dist/tools-logic/check-action.d.ts +9 -0
- package/dist/tools-logic/check-action.d.ts.map +1 -1
- package/dist/tools-logic/check-action.js +61 -3
- package/dist/tools-logic/check-action.js.map +1 -1
- package/dist/tools-logic/check.d.ts +7 -0
- package/dist/tools-logic/check.d.ts.map +1 -1
- package/dist/tools-logic/check.js +24 -2
- package/dist/tools-logic/check.js.map +1 -1
- package/dist/tools-logic/confidence.d.ts +50 -0
- package/dist/tools-logic/confidence.d.ts.map +1 -0
- package/dist/tools-logic/confidence.js +70 -0
- package/dist/tools-logic/confidence.js.map +1 -0
- package/dist/tools-logic/drill-down.d.ts +37 -0
- package/dist/tools-logic/drill-down.d.ts.map +1 -0
- package/dist/tools-logic/drill-down.js +63 -0
- package/dist/tools-logic/drill-down.js.map +1 -0
- package/dist/tools-logic/export-corrections.d.ts +63 -0
- package/dist/tools-logic/export-corrections.d.ts.map +1 -0
- package/dist/tools-logic/export-corrections.js +124 -0
- package/dist/tools-logic/export-corrections.js.map +1 -0
- package/dist/tools-logic/journal-capture.d.ts.map +1 -1
- package/dist/tools-logic/journal-capture.js +4 -1
- package/dist/tools-logic/journal-capture.js.map +1 -1
- package/dist/tools-logic/journal-write.d.ts.map +1 -1
- package/dist/tools-logic/journal-write.js +3 -2
- package/dist/tools-logic/journal-write.js.map +1 -1
- package/dist/tools-logic/local-archive-backend.d.ts +33 -0
- package/dist/tools-logic/local-archive-backend.d.ts.map +1 -0
- package/dist/tools-logic/local-archive-backend.js +124 -0
- package/dist/tools-logic/local-archive-backend.js.map +1 -0
- package/dist/tools-logic/memory-backend.d.ts +74 -0
- package/dist/tools-logic/memory-backend.d.ts.map +1 -0
- package/dist/tools-logic/memory-backend.js +172 -0
- package/dist/tools-logic/memory-backend.js.map +1 -0
- package/dist/tools-logic/memory-query.d.ts +10 -3
- package/dist/tools-logic/memory-query.d.ts.map +1 -1
- package/dist/tools-logic/memory-query.js +31 -7
- package/dist/tools-logic/memory-query.js.map +1 -1
- package/dist/tools-logic/mirror-builder.d.ts +115 -0
- package/dist/tools-logic/mirror-builder.d.ts.map +1 -0
- package/dist/tools-logic/mirror-builder.js +348 -0
- package/dist/tools-logic/mirror-builder.js.map +1 -0
- package/dist/tools-logic/palace-write.d.ts.map +1 -1
- package/dist/tools-logic/palace-write.js +3 -2
- package/dist/tools-logic/palace-write.js.map +1 -1
- package/dist/tools-logic/pipeline-close.d.ts.map +1 -1
- package/dist/tools-logic/pipeline-close.js +2 -1
- package/dist/tools-logic/pipeline-close.js.map +1 -1
- package/dist/tools-logic/pipeline-open.d.ts.map +1 -1
- package/dist/tools-logic/pipeline-open.js +2 -1
- package/dist/tools-logic/pipeline-open.js.map +1 -1
- package/dist/tools-logic/predict-correction.d.ts +47 -0
- package/dist/tools-logic/predict-correction.d.ts.map +1 -0
- package/dist/tools-logic/predict-correction.js +137 -0
- package/dist/tools-logic/predict-correction.js.map +1 -0
- package/dist/tools-logic/prior-builder.d.ts +27 -0
- package/dist/tools-logic/prior-builder.d.ts.map +1 -0
- package/dist/tools-logic/prior-builder.js +90 -0
- package/dist/tools-logic/prior-builder.js.map +1 -0
- package/dist/tools-logic/recognition-builder.d.ts +122 -0
- package/dist/tools-logic/recognition-builder.d.ts.map +1 -0
- package/dist/tools-logic/recognition-builder.js +289 -0
- package/dist/tools-logic/recognition-builder.js.map +1 -0
- package/dist/tools-logic/safety-consolidation.d.ts +104 -0
- package/dist/tools-logic/safety-consolidation.d.ts.map +1 -0
- package/dist/tools-logic/safety-consolidation.js +256 -0
- package/dist/tools-logic/safety-consolidation.js.map +1 -0
- package/dist/tools-logic/session-end-reflect.d.ts +18 -0
- package/dist/tools-logic/session-end-reflect.d.ts.map +1 -1
- package/dist/tools-logic/session-end-reflect.js +69 -30
- package/dist/tools-logic/session-end-reflect.js.map +1 -1
- package/dist/tools-logic/session-end.d.ts +18 -0
- package/dist/tools-logic/session-end.d.ts.map +1 -1
- package/dist/tools-logic/session-end.js +189 -34
- package/dist/tools-logic/session-end.js.map +1 -1
- package/dist/tools-logic/session-start-lite.d.ts +2 -0
- package/dist/tools-logic/session-start-lite.d.ts.map +1 -1
- package/dist/tools-logic/session-start-lite.js +11 -2
- package/dist/tools-logic/session-start-lite.js.map +1 -1
- package/dist/tools-logic/session-start.d.ts +81 -2
- package/dist/tools-logic/session-start.d.ts.map +1 -1
- package/dist/tools-logic/session-start.js +357 -40
- package/dist/tools-logic/session-start.js.map +1 -1
- package/dist/tools-logic/skill-propose.d.ts +27 -0
- package/dist/tools-logic/skill-propose.d.ts.map +1 -0
- package/dist/tools-logic/skill-propose.js +66 -0
- package/dist/tools-logic/skill-propose.js.map +1 -0
- package/dist/tools-logic/skill-recall.d.ts +5 -0
- package/dist/tools-logic/skill-recall.d.ts.map +1 -1
- package/dist/tools-logic/skill-recall.js +14 -1
- package/dist/tools-logic/skill-recall.js.map +1 -1
- package/dist/tools-logic/smart-recall.d.ts +17 -0
- package/dist/tools-logic/smart-recall.d.ts.map +1 -1
- package/dist/tools-logic/smart-recall.js +51 -16
- package/dist/tools-logic/smart-recall.js.map +1 -1
- package/dist/tools-logic/smart-remember.js +8 -9
- package/dist/tools-logic/smart-remember.js.map +1 -1
- package/dist/tools-logic/store-doctor.d.ts +71 -0
- package/dist/tools-logic/store-doctor.d.ts.map +1 -0
- package/dist/tools-logic/store-doctor.js +387 -0
- package/dist/tools-logic/store-doctor.js.map +1 -0
- package/dist/tools-logic/store-repair.d.ts +64 -0
- package/dist/tools-logic/store-repair.d.ts.map +1 -0
- package/dist/tools-logic/store-repair.js +243 -0
- package/dist/tools-logic/store-repair.js.map +1 -0
- package/dist/tools-logic/supersession.d.ts +34 -0
- package/dist/tools-logic/supersession.d.ts.map +1 -0
- package/dist/tools-logic/supersession.js +103 -0
- package/dist/tools-logic/supersession.js.map +1 -0
- package/dist/types.d.ts +7 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -1
- package/dist/vector/embedding.d.ts +8 -0
- package/dist/vector/embedding.d.ts.map +1 -1
- package/dist/vector/embedding.js +9 -1
- package/dist/vector/embedding.js.map +1 -1
- package/dist/vector/local-vector-backend.d.ts.map +1 -1
- package/dist/vector/local-vector-backend.js +14 -17
- package/dist/vector/local-vector-backend.js.map +1 -1
- package/package.json +2 -2
- package/dist/relevance/injector.d.ts +0 -51
- package/dist/relevance/injector.d.ts.map +0 -1
- package/dist/relevance/injector.js +0 -121
- package/dist/relevance/injector.js.map +0 -1
- package/dist/relevance/precision.d.ts +0 -109
- package/dist/relevance/precision.d.ts.map +0 -1
- package/dist/relevance/precision.js +0 -279
- package/dist/relevance/precision.js.map +0 -1
- package/dist/relevance/promoter.d.ts +0 -60
- package/dist/relevance/promoter.d.ts.map +0 -1
- package/dist/relevance/promoter.js +0 -336
- package/dist/relevance/promoter.js.map +0 -1
- package/dist/relevance/stager.d.ts +0 -61
- package/dist/relevance/stager.d.ts.map +0 -1
- package/dist/relevance/stager.js +0 -251
- package/dist/relevance/stager.js.map +0 -1
- package/dist/relevance/surfacer.d.ts +0 -34
- package/dist/relevance/surfacer.d.ts.map +0 -1
- package/dist/relevance/surfacer.js +0 -333
- package/dist/relevance/surfacer.js.map +0 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ab-experiment.ts — A/B injection switch for the correction-uplift experiment.
|
|
3
|
+
*
|
|
4
|
+
* DESIGN RATIONALE (C4, 2026-07-03)
|
|
5
|
+
* ─────────────────────────────────
|
|
6
|
+
* GOAL: measure whether injecting corrections at session_start reduces the rate
|
|
7
|
+
* at which the same correction recurs. The manipulated variable is the
|
|
8
|
+
* FULL correction-derived surface of the session_start payload. Capture,
|
|
9
|
+
* journaling, and session_end outcome recording stay ON in both arms —
|
|
10
|
+
* we A/B the injection EFFECT, not the capture pipeline.
|
|
11
|
+
*
|
|
12
|
+
* OFF-arm semantics (orchestrator ruling 2026-07-03): "this agent has no
|
|
13
|
+
* correction memory today". ALL correction-derived surfaces are absent or
|
|
14
|
+
* empty in an OFF payload:
|
|
15
|
+
* corrections → [] · watch_for → [] · predicted_risks → absent ·
|
|
16
|
+
* blind_spots → [] · mirror_available → absent · alignment → null ·
|
|
17
|
+
* recognition.person → absent (tendencies derive from blind spots).
|
|
18
|
+
* Additionally NO "retrieved" outcomes are recorded in OFF sessions —
|
|
19
|
+
* recording retrieval for rules the agent never saw would corrupt the
|
|
20
|
+
* precision KPI and the experiment itself.
|
|
21
|
+
*
|
|
22
|
+
* Insights, rooms, and captures (journal lineage) are intentionally NOT
|
|
23
|
+
* manipulated in v1 and stay in BOTH arms: v1 measures corrections only.
|
|
24
|
+
* If the experiment detects uplift, a v2 can vary insights too. This
|
|
25
|
+
* choice is recorded here so it cannot be silently reversed.
|
|
26
|
+
*
|
|
27
|
+
* OPT-IN DEFAULT: the experiment is DISABLED by default (AR_AB_ENABLED is not
|
|
28
|
+
* set). All sessions get full injection as today. The experiment owner
|
|
29
|
+
* sets AR_AB_ENABLED=1 when ready to start accumulating data. This is a
|
|
30
|
+
* hard requirement: the OFF arm degrades real user sessions, so we never
|
|
31
|
+
* run it without explicit intent.
|
|
32
|
+
*
|
|
33
|
+
* ARM ASSIGNMENT (deterministic, not random):
|
|
34
|
+
* hash( project + local-date + session_ordinal ) mod 2
|
|
35
|
+
* → 0 = ON, 1 = OFF
|
|
36
|
+
* The session_ordinal is the count of existing _ab_arms.jsonl rows for this
|
|
37
|
+
* project (0-indexed). Combined with project+date, this produces a balanced,
|
|
38
|
+
* non-predictable-by-time-of-day alternating pattern. Math.random is banned
|
|
39
|
+
* (repo rule). The hash is SHA-256 truncated to the first 8 hex digits.
|
|
40
|
+
*
|
|
41
|
+
* ESCAPE HATCH: AR_AB_FORCE=on|off overrides arm assignment for demos or
|
|
42
|
+
* emergencies. Forced sessions are flagged { forced: true } in the ledger
|
|
43
|
+
* and EXCLUDED from the outcome comparison in ab-report.mjs.
|
|
44
|
+
* AR_AB_FORCE without AR_AB_ENABLED=1 is a LOUD no-op: one stderr warning,
|
|
45
|
+
* no arm, no ledger row, injection unchanged (warnForcedWithoutEnabled).
|
|
46
|
+
*
|
|
47
|
+
* LEDGER: append-only <project>/corrections/_ab_arms.jsonl — TWO row kinds:
|
|
48
|
+
* assignment row (written by assignArm, counters zeroed):
|
|
49
|
+
* { ts, project, arm, forced, session_key, injected_count: 0, payload_tokens: 0 }
|
|
50
|
+
* result row (written by logABResult after the payload is built):
|
|
51
|
+
* { ts, kind: "result", session_key, injected_count, payload_tokens }
|
|
52
|
+
* readABArms merges result rows onto their assignment rows by session_key.
|
|
53
|
+
* NOTHING is ever rewritten in place — the append-only invariant is physical,
|
|
54
|
+
* not just conventional, so two concurrent same-project sessions can never
|
|
55
|
+
* clobber each other's counter fill (each appends its own result row).
|
|
56
|
+
*
|
|
57
|
+
* PAYLOAD SIGNAL: session_start result carries ab_arm: "on"|"off" so the
|
|
58
|
+
* transcript records which arm the session ran. The terse formatter appends
|
|
59
|
+
* a quiet trailing marker rather than a banner — we must NOT prime the agent
|
|
60
|
+
* to behave differently depending on the arm (experimenter effect).
|
|
61
|
+
*/
|
|
62
|
+
import * as crypto from "node:crypto";
|
|
63
|
+
import * as fs from "node:fs";
|
|
64
|
+
import * as path from "node:path";
|
|
65
|
+
import { getRoot } from "../types.js";
|
|
66
|
+
// ── Path helper ───────────────────────────────────────────────────────────────
|
|
67
|
+
function abArmsPath(project) {
|
|
68
|
+
// Reuse the corrections dir path — share the directory for co-location.
|
|
69
|
+
const safe = (project || "unnamed")
|
|
70
|
+
.replace(/[^a-zA-Z0-9_\-]/g, "-")
|
|
71
|
+
.replace(/^-+|-+$/g, "")
|
|
72
|
+
.slice(0, 100) || "unnamed";
|
|
73
|
+
const root = getRoot();
|
|
74
|
+
const resolved = path.join(root, "projects", safe, "corrections");
|
|
75
|
+
const rootSep = root.endsWith(path.sep) ? root : root + path.sep;
|
|
76
|
+
if (!resolved.startsWith(rootSep)) {
|
|
77
|
+
throw new Error(`Invalid project (path escape): ${project}`);
|
|
78
|
+
}
|
|
79
|
+
return path.join(resolved, "_ab_arms.jsonl");
|
|
80
|
+
}
|
|
81
|
+
// ── Ordinal counter ───────────────────────────────────────────────────────────
|
|
82
|
+
/**
|
|
83
|
+
* Count existing ASSIGNMENT rows for this project (0-based ordinal for the
|
|
84
|
+
* next session). Result rows (kind:"result") are NOT counted — they are
|
|
85
|
+
* counter fills, not sessions. Returns 0 if the file does not exist or is
|
|
86
|
+
* unreadable. Never throws.
|
|
87
|
+
*/
|
|
88
|
+
function countExistingRows(project) {
|
|
89
|
+
try {
|
|
90
|
+
const p = abArmsPath(project);
|
|
91
|
+
if (!fs.existsSync(p))
|
|
92
|
+
return 0;
|
|
93
|
+
const raw = fs.readFileSync(p, "utf-8");
|
|
94
|
+
let count = 0;
|
|
95
|
+
for (const line of raw.split("\n")) {
|
|
96
|
+
const trimmed = line.trim();
|
|
97
|
+
if (!trimmed)
|
|
98
|
+
continue;
|
|
99
|
+
try {
|
|
100
|
+
const row = JSON.parse(trimmed);
|
|
101
|
+
if (row.kind !== "result")
|
|
102
|
+
count++;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Malformed line — count it as an assignment slot so ordinals stay
|
|
106
|
+
// monotonic (never reuse a possibly-taken ordinal).
|
|
107
|
+
count++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return count;
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ── Deterministic arm assignment ──────────────────────────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* computeArm(project, localDate, ordinal) → "on" | "off"
|
|
119
|
+
*
|
|
120
|
+
* SHA-256( project + "|" + localDate + "|" + ordinal ) → first 8 hex digits
|
|
121
|
+
* as uint32, mod 2 → 0 = "on", 1 = "off".
|
|
122
|
+
*
|
|
123
|
+
* Properties:
|
|
124
|
+
* - Deterministic: same inputs always yield the same arm (safe to re-run).
|
|
125
|
+
* - Balanced: over 100 synthetic sessions, yields ~50 ON / 50 OFF.
|
|
126
|
+
* - Not time-predictable: ordinal + project, not clock-parity, controls arm.
|
|
127
|
+
* - Zero Math.random.
|
|
128
|
+
*
|
|
129
|
+
* Exported for unit tests (pure function, no side effects).
|
|
130
|
+
*/
|
|
131
|
+
export function computeArm(project, localDate, ordinal) {
|
|
132
|
+
const input = `${project}|${localDate}|${ordinal}`;
|
|
133
|
+
const hex = crypto.createHash("sha256").update(input).digest("hex");
|
|
134
|
+
// Use first 8 hex chars (32 bits) to get a uint32, then mod 2.
|
|
135
|
+
const uint32 = parseInt(hex.slice(0, 8), 16);
|
|
136
|
+
return uint32 % 2 === 0 ? "on" : "off";
|
|
137
|
+
}
|
|
138
|
+
// ── Experiment enabled check ──────────────────────────────────────────────────
|
|
139
|
+
/**
|
|
140
|
+
* isExperimentEnabled() → true only when AR_AB_ENABLED=1.
|
|
141
|
+
*
|
|
142
|
+
* Default: false — all sessions get full injection (no degradation without
|
|
143
|
+
* explicit opt-in). The experiment owner sets AR_AB_ENABLED=1 when ready to
|
|
144
|
+
* start accumulating discordant-pair data.
|
|
145
|
+
*/
|
|
146
|
+
export function isExperimentEnabled() {
|
|
147
|
+
return process.env["AR_AB_ENABLED"] === "1";
|
|
148
|
+
}
|
|
149
|
+
// ── Forced override ───────────────────────────────────────────────────────────
|
|
150
|
+
/**
|
|
151
|
+
* getForcedArm() → "on" | "off" | null
|
|
152
|
+
*
|
|
153
|
+
* Reads AR_AB_FORCE env var. Validated to "on" or "off"; any other value
|
|
154
|
+
* is silently ignored (null = no override). Forced sessions are flagged in
|
|
155
|
+
* the ledger and excluded from ab-report comparisons.
|
|
156
|
+
*/
|
|
157
|
+
export function getForcedArm() {
|
|
158
|
+
const v = process.env["AR_AB_FORCE"];
|
|
159
|
+
if (v === "on" || v === "off")
|
|
160
|
+
return v;
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* warnForcedWithoutEnabled() → true if it warned.
|
|
165
|
+
*
|
|
166
|
+
* Orchestrator ruling 2026-07-03: AR_AB_FORCE without AR_AB_ENABLED=1 is a
|
|
167
|
+
* LOUD no-op — one stderr warning, no arm assignment, no ledger row, injection
|
|
168
|
+
* unchanged. Called by session_start on the disabled path so a misconfigured
|
|
169
|
+
* demo/emergency override never fails silently.
|
|
170
|
+
*/
|
|
171
|
+
export function warnForcedWithoutEnabled() {
|
|
172
|
+
if (getForcedArm() !== null && !isExperimentEnabled()) {
|
|
173
|
+
process.stderr.write("AR_AB_FORCE is set but AR_AB_ENABLED=1 is not — force ignored, experiment disabled\n");
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* assignArm(project) → { arm, forced, session_key }
|
|
180
|
+
*
|
|
181
|
+
* Side effects:
|
|
182
|
+
* - Appends an incomplete ledger row to _ab_arms.jsonl (injected_count and
|
|
183
|
+
* payload_tokens are 0 at assignment time — they are filled by logABResult
|
|
184
|
+
* after session_start resolves its payload).
|
|
185
|
+
* - Creates the corrections directory if it does not exist.
|
|
186
|
+
*
|
|
187
|
+
* Never throws: if the ledger write fails, the assignment still returns a
|
|
188
|
+
* valid arm so session_start can proceed. Errors are swallowed silently —
|
|
189
|
+
* a ledger gap is better than a broken orientation.
|
|
190
|
+
*
|
|
191
|
+
* Called only when isExperimentEnabled() is true.
|
|
192
|
+
*/
|
|
193
|
+
export function assignArm(project) {
|
|
194
|
+
const forced = getForcedArm();
|
|
195
|
+
const localDate = new Date().toLocaleDateString("sv"); // YYYY-MM-DD, local TZ
|
|
196
|
+
const ordinal = countExistingRows(project);
|
|
197
|
+
const arm = forced ?? computeArm(project, localDate, ordinal);
|
|
198
|
+
const session_key = `${project}/${localDate}/${ordinal}`;
|
|
199
|
+
// Append ledger row (with zeroed counters; logABResult fills them later).
|
|
200
|
+
try {
|
|
201
|
+
const p = abArmsPath(project);
|
|
202
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
203
|
+
const row = {
|
|
204
|
+
ts: new Date().toISOString(),
|
|
205
|
+
project,
|
|
206
|
+
arm,
|
|
207
|
+
forced: forced !== null,
|
|
208
|
+
session_key,
|
|
209
|
+
injected_count: 0, // filled by logABResult
|
|
210
|
+
payload_tokens: 0, // filled by logABResult
|
|
211
|
+
};
|
|
212
|
+
fs.appendFileSync(p, JSON.stringify(row) + "\n", "utf-8");
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
// Ledger write failure must never break session_start.
|
|
216
|
+
}
|
|
217
|
+
return { arm, forced: forced !== null, session_key };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* logABResult(project, session_key, injected_count, payload_tokens)
|
|
221
|
+
*
|
|
222
|
+
* APPENDS a result row keyed by session_key — never rewrites the ledger.
|
|
223
|
+
* The previous in-place last-row rewrite had a race: two concurrent sessions
|
|
224
|
+
* of the same project could zero each other's counter fill (review CRITICAL,
|
|
225
|
+
* fixed per orchestrator ruling 2026-07-03). With append-only result rows,
|
|
226
|
+
* each session's fill lands regardless of interleaving; readABArms merges
|
|
227
|
+
* result rows onto assignment rows by session_key (last result wins).
|
|
228
|
+
*
|
|
229
|
+
* No-ops when the ledger file does not exist (no assignment ever happened —
|
|
230
|
+
* a result row without its assignment row would be an orphan). Never throws.
|
|
231
|
+
*/
|
|
232
|
+
export function logABResult(project, session_key, injected_count, payload_tokens) {
|
|
233
|
+
try {
|
|
234
|
+
const p = abArmsPath(project);
|
|
235
|
+
if (!fs.existsSync(p))
|
|
236
|
+
return;
|
|
237
|
+
const row = {
|
|
238
|
+
ts: new Date().toISOString(),
|
|
239
|
+
kind: "result",
|
|
240
|
+
session_key,
|
|
241
|
+
injected_count,
|
|
242
|
+
payload_tokens,
|
|
243
|
+
};
|
|
244
|
+
fs.appendFileSync(p, JSON.stringify(row) + "\n", "utf-8");
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
// Silent — never break session_start.
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// ── Read ledger (for ab-report.mjs) ──────────────────────────────────────────
|
|
251
|
+
/**
|
|
252
|
+
* readABArms(project) → ABLedgerRow[]
|
|
253
|
+
*
|
|
254
|
+
* Reads _ab_arms.jsonl for a project and MERGES result rows onto their
|
|
255
|
+
* assignment rows by session_key (last result row wins when duplicates exist).
|
|
256
|
+
* Returns assignment rows only — result rows are counter fills, not sessions.
|
|
257
|
+
* Malformed lines are silently skipped (never throws). Returns [] when the
|
|
258
|
+
* file does not exist.
|
|
259
|
+
*/
|
|
260
|
+
export function readABArms(project) {
|
|
261
|
+
try {
|
|
262
|
+
const p = abArmsPath(project);
|
|
263
|
+
if (!fs.existsSync(p))
|
|
264
|
+
return [];
|
|
265
|
+
const raw = fs.readFileSync(p, "utf-8");
|
|
266
|
+
const assignments = [];
|
|
267
|
+
const results = new Map(); // session_key → last result row
|
|
268
|
+
for (const line of raw.split("\n")) {
|
|
269
|
+
const trimmed = line.trim();
|
|
270
|
+
if (!trimmed)
|
|
271
|
+
continue;
|
|
272
|
+
try {
|
|
273
|
+
const row = JSON.parse(trimmed);
|
|
274
|
+
if (row.kind === "result") {
|
|
275
|
+
results.set(row.session_key, row);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
assignments.push(row);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
catch { /* skip malformed */ }
|
|
282
|
+
}
|
|
283
|
+
return assignments.map((a) => {
|
|
284
|
+
const r = results.get(a.session_key);
|
|
285
|
+
return r
|
|
286
|
+
? { ...a, injected_count: r.injected_count, payload_tokens: r.payload_tokens }
|
|
287
|
+
: a;
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return [];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=ab-experiment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ab-experiment.js","sourceRoot":"","sources":["../../src/storage/ab-experiment.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* archive-prune.ts — retention pass for the lossless raw archive tier (Wave 2).
|
|
3
|
+
*
|
|
4
|
+
* Companion to archive-write.ts. Bounds ~/.agent-recall growth by gzipping (or
|
|
5
|
+
* removing) raw session segments under journal/archive/raw/ that are BOTH:
|
|
6
|
+
* (a) older than `olderThanDays` (by file mtime), AND
|
|
7
|
+
* (b) already CONSUMED — i.e. their mtime is at or before
|
|
8
|
+
* journal/archive/raw/.consumed.json `lastConsumedAt`.
|
|
9
|
+
*
|
|
10
|
+
* "Consumed" (single agreed definition, shared with advanceConsumeMarker in
|
|
11
|
+
* safety-consolidation.ts): a raw segment whose content has been folded into the
|
|
12
|
+
* palace by the login-free, regex journal→palace consolidation. The marker is
|
|
13
|
+
* advanced monotonically to `now − retentionWindow` by advanceConsumeMarker on
|
|
14
|
+
* the login-free safety pass (the optional dreaming loop MAY also advance it, but
|
|
15
|
+
* is no longer required to — that was the original Wave-5 plan, superseded).
|
|
16
|
+
*
|
|
17
|
+
* The consumed gate is load-bearing: while `lastConsumedAt` is null (nothing
|
|
18
|
+
* advanced the marker yet) this pass prunes NOTHING — we never discard raw bytes
|
|
19
|
+
* that have not been distilled into the palace.
|
|
20
|
+
*
|
|
21
|
+
* Mirrors journal-archive.ts's older_than_days / dry-run shape. Unlike
|
|
22
|
+
* archive-write.ts (which must never throw inside the Stop turn), this is an
|
|
23
|
+
* explicitly-invoked maintenance op: it surfaces no error to the caller but
|
|
24
|
+
* skips any single file it cannot process rather than aborting the whole pass.
|
|
25
|
+
*/
|
|
26
|
+
export interface PruneRawArchiveOptions {
|
|
27
|
+
/** Segments whose mtime is older than this many days are prune candidates. Default 30. */
|
|
28
|
+
olderThanDays?: number;
|
|
29
|
+
/** When true, report what WOULD be pruned but write/delete nothing. Default false. */
|
|
30
|
+
dryRun?: boolean;
|
|
31
|
+
/** "gzip" (default) compresses to `<file>.gz` then removes the original; "remove" deletes outright. */
|
|
32
|
+
mode?: "gzip" | "remove";
|
|
33
|
+
}
|
|
34
|
+
export interface PruneRawArchiveResult {
|
|
35
|
+
/** The journal/archive/raw directory inspected. */
|
|
36
|
+
dir: string;
|
|
37
|
+
/** Raw `.md` segments examined. */
|
|
38
|
+
scanned: number;
|
|
39
|
+
/** Segments gzipped (0 in dryRun). */
|
|
40
|
+
gzipped: number;
|
|
41
|
+
/** Segments removed outright (0 in dryRun; only when mode === "remove"). */
|
|
42
|
+
removed: number;
|
|
43
|
+
/** Candidates matching BOTH gates (old AND consumed). Equals gzipped+removed when !dryRun and no per-file error. */
|
|
44
|
+
eligible: number;
|
|
45
|
+
/** Segments kept: too new, or not yet consumed. */
|
|
46
|
+
kept: number;
|
|
47
|
+
dryRun: boolean;
|
|
48
|
+
/** `lastConsumedAt` from .consumed.json; null when nothing is consumed yet (⇒ nothing pruned). */
|
|
49
|
+
consumedThrough: string | null;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Gzip-or-remove consumed, aged raw archive segments to bound disk growth.
|
|
53
|
+
* Safe by construction: with no consume marker advanced, it prunes nothing.
|
|
54
|
+
*/
|
|
55
|
+
export declare function pruneRawArchive(project: string, options?: PruneRawArchiveOptions): PruneRawArchiveResult;
|
|
56
|
+
//# sourceMappingURL=archive-prune.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-prune.d.ts","sourceRoot":"","sources":["../../src/storage/archive-prune.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAeH,MAAM,WAAW,sBAAsB;IACrC,0FAA0F;IAC1F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sFAAsF;IACtF,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,uGAAuG;IACvG,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACpC,mDAAmD;IACnD,GAAG,EAAE,MAAM,CAAC;IACZ,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,sCAAsC;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,OAAO,EAAE,MAAM,CAAC;IAChB,oHAAoH;IACpH,QAAQ,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,kGAAkG;IAClG,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,qBAAqB,CAqFvB"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* archive-prune.ts — retention pass for the lossless raw archive tier (Wave 2).
|
|
3
|
+
*
|
|
4
|
+
* Companion to archive-write.ts. Bounds ~/.agent-recall growth by gzipping (or
|
|
5
|
+
* removing) raw session segments under journal/archive/raw/ that are BOTH:
|
|
6
|
+
* (a) older than `olderThanDays` (by file mtime), AND
|
|
7
|
+
* (b) already CONSUMED — i.e. their mtime is at or before
|
|
8
|
+
* journal/archive/raw/.consumed.json `lastConsumedAt`.
|
|
9
|
+
*
|
|
10
|
+
* "Consumed" (single agreed definition, shared with advanceConsumeMarker in
|
|
11
|
+
* safety-consolidation.ts): a raw segment whose content has been folded into the
|
|
12
|
+
* palace by the login-free, regex journal→palace consolidation. The marker is
|
|
13
|
+
* advanced monotonically to `now − retentionWindow` by advanceConsumeMarker on
|
|
14
|
+
* the login-free safety pass (the optional dreaming loop MAY also advance it, but
|
|
15
|
+
* is no longer required to — that was the original Wave-5 plan, superseded).
|
|
16
|
+
*
|
|
17
|
+
* The consumed gate is load-bearing: while `lastConsumedAt` is null (nothing
|
|
18
|
+
* advanced the marker yet) this pass prunes NOTHING — we never discard raw bytes
|
|
19
|
+
* that have not been distilled into the palace.
|
|
20
|
+
*
|
|
21
|
+
* Mirrors journal-archive.ts's older_than_days / dry-run shape. Unlike
|
|
22
|
+
* archive-write.ts (which must never throw inside the Stop turn), this is an
|
|
23
|
+
* explicitly-invoked maintenance op: it surfaces no error to the caller but
|
|
24
|
+
* skips any single file it cannot process rather than aborting the whole pass.
|
|
25
|
+
*/
|
|
26
|
+
import * as fs from "node:fs";
|
|
27
|
+
import * as path from "node:path";
|
|
28
|
+
import { gzipSync } from "node:zlib";
|
|
29
|
+
import { archiveRawDir, sanitizeSlug } from "./paths.js";
|
|
30
|
+
import { readJsonSafe } from "./fs-utils.js";
|
|
31
|
+
const DAY_MS = 86_400_000;
|
|
32
|
+
// Raw segment filenames are `<YYYY-MM-DD>--<sid>.md` (see archive-write.ts).
|
|
33
|
+
// This deliberately excludes `.consumed.json`, `index.md`, and already-gzipped
|
|
34
|
+
// `*.md.gz` segments.
|
|
35
|
+
const RAW_SEGMENT_RE = /^\d{4}-\d{2}-\d{2}--.+\.md$/;
|
|
36
|
+
/**
|
|
37
|
+
* Gzip-or-remove consumed, aged raw archive segments to bound disk growth.
|
|
38
|
+
* Safe by construction: with no consume marker advanced, it prunes nothing.
|
|
39
|
+
*/
|
|
40
|
+
export function pruneRawArchive(project, options = {}) {
|
|
41
|
+
const olderThanDays = options.olderThanDays ?? 30;
|
|
42
|
+
const dryRun = options.dryRun ?? false;
|
|
43
|
+
const mode = options.mode ?? "gzip";
|
|
44
|
+
const slug = sanitizeSlug(project);
|
|
45
|
+
const dir = archiveRawDir(slug);
|
|
46
|
+
const result = {
|
|
47
|
+
dir,
|
|
48
|
+
scanned: 0,
|
|
49
|
+
gzipped: 0,
|
|
50
|
+
removed: 0,
|
|
51
|
+
eligible: 0,
|
|
52
|
+
kept: 0,
|
|
53
|
+
dryRun,
|
|
54
|
+
consumedThrough: null,
|
|
55
|
+
};
|
|
56
|
+
if (!fs.existsSync(dir))
|
|
57
|
+
return result;
|
|
58
|
+
// Consume marker: only prune what the dreaming loop has already distilled.
|
|
59
|
+
const marker = readJsonSafe(path.join(dir, ".consumed.json"));
|
|
60
|
+
const lastConsumedAt = marker?.lastConsumedAt ?? null;
|
|
61
|
+
result.consumedThrough = lastConsumedAt;
|
|
62
|
+
// Guard: nothing consumed yet → never discard undistilled raw bytes.
|
|
63
|
+
const consumedCutoffMs = lastConsumedAt ? new Date(lastConsumedAt).getTime() : null;
|
|
64
|
+
const ageCutoffMs = Date.now() - olderThanDays * DAY_MS;
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = fs.readdirSync(dir);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
for (const name of entries) {
|
|
73
|
+
if (!RAW_SEGMENT_RE.test(name))
|
|
74
|
+
continue;
|
|
75
|
+
result.scanned++;
|
|
76
|
+
const full = path.join(dir, name);
|
|
77
|
+
let mtimeMs;
|
|
78
|
+
try {
|
|
79
|
+
mtimeMs = fs.statSync(full).mtimeMs;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
continue; // vanished mid-pass; ignore
|
|
83
|
+
}
|
|
84
|
+
// Inclusive boundaries (deterministic; the consumed gate uses the exact
|
|
85
|
+
// `mtime <= lastConsumedAt` contract from the plan).
|
|
86
|
+
const isOld = mtimeMs <= ageCutoffMs;
|
|
87
|
+
const isConsumed = consumedCutoffMs !== null && mtimeMs <= consumedCutoffMs;
|
|
88
|
+
if (!(isOld && isConsumed)) {
|
|
89
|
+
result.kept++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
result.eligible++;
|
|
93
|
+
if (dryRun)
|
|
94
|
+
continue; // report only; touch nothing
|
|
95
|
+
try {
|
|
96
|
+
if (mode === "gzip") {
|
|
97
|
+
const gzPath = full + ".gz";
|
|
98
|
+
if (!fs.existsSync(gzPath)) {
|
|
99
|
+
const raw = fs.readFileSync(full);
|
|
100
|
+
const tmp = gzPath + ".tmp." + process.pid;
|
|
101
|
+
fs.writeFileSync(tmp, gzipSync(raw));
|
|
102
|
+
fs.renameSync(tmp, gzPath);
|
|
103
|
+
}
|
|
104
|
+
fs.unlinkSync(full);
|
|
105
|
+
result.gzipped++;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
fs.unlinkSync(full);
|
|
109
|
+
result.removed++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// A single failed file must not abort the pass; it simply is not counted.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
//# sourceMappingURL=archive-prune.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-prune.js","sourceRoot":"","sources":["../../src/storage/archive-prune.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* archive-write.ts — the lossless, mechanical, judgment-free verbatim tier
|
|
3
|
+
* (Wave 2). Written on EVERY session end, BEFORE any capture/summary logic, so
|
|
4
|
+
* a session can never be "lost" just because nothing was captured.
|
|
5
|
+
*
|
|
6
|
+
* Hard invariants (privacy + safety):
|
|
7
|
+
* - LOCAL-ONLY. This module MUST NOT import journal-write or syncToSupabase.
|
|
8
|
+
* The raw tier never leaves the machine. (Verified by a structural test.)
|
|
9
|
+
* - NEVER throws to the caller. Any failure returns { path:"", bytes:0 } so a
|
|
10
|
+
* crash here can never break the Stop turn.
|
|
11
|
+
* - sessionId is UNTRUSTED (arrives from hook stdin) → MUST pass through
|
|
12
|
+
* sanitizeSlug before any path.join (MCP-security rule).
|
|
13
|
+
* - Idempotent on the session UUID: a second call for the same session is a
|
|
14
|
+
* no-op (returns bytes:0) and never overwrites the original verbatim bytes.
|
|
15
|
+
*/
|
|
16
|
+
export interface ArchiveSessionInput {
|
|
17
|
+
project: string;
|
|
18
|
+
/** UNTRUSTED — sanitized before use. The dedup key (session UUID). */
|
|
19
|
+
sessionId: string;
|
|
20
|
+
/** The transcript file path, recorded in frontmatter for provenance only. */
|
|
21
|
+
transcriptPath?: string;
|
|
22
|
+
/** The verbatim transcript bytes (head+tail). Written as-is, no truncation. */
|
|
23
|
+
rawTranscript: string;
|
|
24
|
+
/** Optional one-line human summary (first user message), for the index line. */
|
|
25
|
+
summary?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ArchiveSessionResult {
|
|
28
|
+
/** Absolute path of the written (or pre-existing) raw file; "" on failure. */
|
|
29
|
+
path: string;
|
|
30
|
+
/** Bytes of rawTranscript written; 0 if idempotent no-op or on failure. */
|
|
31
|
+
bytes: number;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Write a verbatim, lossless dump of a session under journal/archive/raw/.
|
|
35
|
+
* Mechanical and judgment-free: no min-length gate, no summarization, no sync.
|
|
36
|
+
*/
|
|
37
|
+
export declare function archiveSession(input: ArchiveSessionInput): ArchiveSessionResult;
|
|
38
|
+
//# sourceMappingURL=archive-write.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-write.d.ts","sourceRoot":"","sources":["../../src/storage/archive-write.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAQH,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;IAClB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+EAA+E;IAC/E,aAAa,EAAE,MAAM,CAAC;IACtB,gFAAgF;IAChF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;CACf;AAsCD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,mBAAmB,GAAG,oBAAoB,CAmD/E"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* archive-write.ts — the lossless, mechanical, judgment-free verbatim tier
|
|
3
|
+
* (Wave 2). Written on EVERY session end, BEFORE any capture/summary logic, so
|
|
4
|
+
* a session can never be "lost" just because nothing was captured.
|
|
5
|
+
*
|
|
6
|
+
* Hard invariants (privacy + safety):
|
|
7
|
+
* - LOCAL-ONLY. This module MUST NOT import journal-write or syncToSupabase.
|
|
8
|
+
* The raw tier never leaves the machine. (Verified by a structural test.)
|
|
9
|
+
* - NEVER throws to the caller. Any failure returns { path:"", bytes:0 } so a
|
|
10
|
+
* crash here can never break the Stop turn.
|
|
11
|
+
* - sessionId is UNTRUSTED (arrives from hook stdin) → MUST pass through
|
|
12
|
+
* sanitizeSlug before any path.join (MCP-security rule).
|
|
13
|
+
* - Idempotent on the session UUID: a second call for the same session is a
|
|
14
|
+
* no-op (returns bytes:0) and never overwrites the original verbatim bytes.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import { archiveRawDir, sanitizeSlug } from "./paths.js";
|
|
19
|
+
import { ensureDir, todayISO, writeJsonAtomic } from "./fs-utils.js";
|
|
20
|
+
import { writeMemoryProtocol } from "./memory-protocol.js";
|
|
21
|
+
function buildFrontmatter(meta) {
|
|
22
|
+
const lines = [
|
|
23
|
+
"---",
|
|
24
|
+
`project: ${meta.project}`,
|
|
25
|
+
`sessionId: ${meta.sessionId}`,
|
|
26
|
+
`savedAt: ${meta.savedAt}`,
|
|
27
|
+
`source: ${meta.source}`,
|
|
28
|
+
];
|
|
29
|
+
if (meta.transcriptPath) {
|
|
30
|
+
// JSON-encode to neutralize any newline/colon in the untrusted path.
|
|
31
|
+
lines.push(`transcriptPath: ${JSON.stringify(meta.transcriptPath)}`);
|
|
32
|
+
}
|
|
33
|
+
lines.push("---", "");
|
|
34
|
+
return lines.join("\n");
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Append a single line to journal/archive/index.md. Best-effort; swallows errors.
|
|
38
|
+
*/
|
|
39
|
+
function appendIndexLine(slug, line) {
|
|
40
|
+
try {
|
|
41
|
+
// archiveRawDir = .../journal/archive/raw ; index.md lives one level up.
|
|
42
|
+
const indexPath = path.join(path.dirname(archiveRawDir(slug)), "index.md");
|
|
43
|
+
ensureDir(path.dirname(indexPath));
|
|
44
|
+
fs.appendFileSync(indexPath, line + "\n", "utf-8");
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// index is a convenience; failure must not abort the archive write.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Write a verbatim, lossless dump of a session under journal/archive/raw/.
|
|
52
|
+
* Mechanical and judgment-free: no min-length gate, no summarization, no sync.
|
|
53
|
+
*/
|
|
54
|
+
export function archiveSession(input) {
|
|
55
|
+
try {
|
|
56
|
+
if (typeof input.rawTranscript !== "string") {
|
|
57
|
+
// Defensive: a non-string body would throw on .length / write below.
|
|
58
|
+
return { path: "", bytes: 0 };
|
|
59
|
+
}
|
|
60
|
+
const slug = sanitizeSlug(input.project); // also hardens the project name
|
|
61
|
+
const sid = sanitizeSlug(input.sessionId); // UNTRUSTED → sanitize first
|
|
62
|
+
const dir = archiveRawDir(slug);
|
|
63
|
+
ensureDir(dir);
|
|
64
|
+
const dest = path.join(dir, `${todayISO()}--${sid}.md`);
|
|
65
|
+
// Idempotent on the session UUID: never overwrite an existing dump.
|
|
66
|
+
if (fs.existsSync(dest)) {
|
|
67
|
+
return { path: dest, bytes: 0 };
|
|
68
|
+
}
|
|
69
|
+
const savedAt = new Date().toISOString();
|
|
70
|
+
const frontmatter = buildFrontmatter({
|
|
71
|
+
project: slug,
|
|
72
|
+
sessionId: sid,
|
|
73
|
+
savedAt,
|
|
74
|
+
source: "hook-archive",
|
|
75
|
+
transcriptPath: input.transcriptPath,
|
|
76
|
+
});
|
|
77
|
+
// Atomic write: tmp + rename so a partial dump never appears as complete.
|
|
78
|
+
const tmp = dest + ".tmp." + process.pid;
|
|
79
|
+
fs.writeFileSync(tmp, frontmatter + input.rawTranscript, "utf-8");
|
|
80
|
+
fs.renameSync(tmp, dest);
|
|
81
|
+
// One append-only index line.
|
|
82
|
+
const summary = (input.summary ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
83
|
+
appendIndexLine(slug, `${todayISO()} ${sid} ${summary}`.trimEnd());
|
|
84
|
+
// Seed the consume marker if absent (the dreaming loop advances it).
|
|
85
|
+
const consumed = path.join(dir, ".consumed.json");
|
|
86
|
+
if (!fs.existsSync(consumed)) {
|
|
87
|
+
writeJsonAtomic(consumed, { lastConsumedOffset: 0, lastConsumedAt: null });
|
|
88
|
+
}
|
|
89
|
+
// Write the self-describing protocol doc once.
|
|
90
|
+
writeMemoryProtocol(slug);
|
|
91
|
+
return { path: dest, bytes: input.rawTranscript.length };
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// NEVER throw into the Stop turn.
|
|
95
|
+
return { path: "", bytes: 0 };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=archive-write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive-write.js","sourceRoot":"","sources":["../../src/storage/archive-write.ts"],"names":[],"mappings":"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"}
|