pi-mega-compact 0.21.2 → 0.21.3
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/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +1 -0
- package/dist/extensions/mega-compact-child.js +126 -0
- package/dist/extensions/mega-config.js +5 -0
- package/dist/src/bridge/factory.js +181 -0
- package/dist/src/bridge/types.js +10 -0
- package/dist/src/bridge.js +1 -0
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +6 -0
- package/extensions/mega-compact-child.ts +129 -0
- package/extensions/mega-config-types.ts +5 -0
- package/extensions/mega-config.ts +5 -0
- package/package.json +1 -1
- package/src/bridge/factory.ts +231 -0
- package/src/bridge/types.ts +138 -0
- package/src/bridge.ts +24 -0
|
@@ -138,6 +138,7 @@ export const SETTINGS = [
|
|
|
138
138
|
name: "Three-Way Failback",
|
|
139
139
|
settings: [
|
|
140
140
|
boolDirect("MEGACOMPACT_THREE_WAY_FAILBACK", "Three-Way Failback", "Umbrella for the 3-way failback safety system: TriggerGuard (stages a recall block even when session_start never fires), the live-window ReductionValidator + persisted ThrashGuard (stops ineffective compaction re-fire loops), the 3-source read-only recall vote + same-repo relevance floor, and InjectionConfirm (asserts the staged block reached the message list pi sends). OFF = byte-identical pre-3WF behavior (v0.20.83). Runtime reads envBool(plain key), so this uses the plain-write convention (not _DISABLED).", true),
|
|
141
|
+
boolDirect("MEGACOMPACT_ITHACUS_BRIDGE", "ithacus Bridge", "Mega↔ithacus bridge: gate the child extension + bridge usage that tie this extension to ithacus's durable compaction. Default ON; OFF (=0/`=false`) is byte-identical to pre-bridge behavior — the bridge is only consulted when this is ON. Positive sprint flag. Runtime reads envBool(plain key), so this uses the plain-write convention (not _DISABLED).", true),
|
|
141
142
|
boolDirect("MEGACOMPACT_RECALL_TAIL_INJECT", "Recall Tail Inject", "Compose the staged recall block as a trailing user message on the context event (tail inject) instead of the legacy system-prompt prepend. Tail mode keeps the cache prefix stable and is the mode InjectionConfirm verifies against ContextEvent.messages; OFF falls back to the legacy prepend path (verified by string-contains). Runtime reads envBool(plain key), so this uses the plain-write convention (not _DISABLED).", true),
|
|
142
143
|
],
|
|
143
144
|
},
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mega-compact-child.ts — minimal extension loaded ONLY into dispatched child
|
|
3
|
+
* pi subprocesses (spawned by ithacus with a second `-e` flag).
|
|
4
|
+
*
|
|
5
|
+
* Design: a child is a FRESH pi process started with `--no-extensions -e <this
|
|
6
|
+
* file>` (see ithacus-spawn.ts). It does NOT receive the parent's MegaConfig and
|
|
7
|
+
* is a separate process, so it reads its two control env vars directly and owns
|
|
8
|
+
* its own bridge. It gives children recall-at-start + compaction-on-shutdown via
|
|
9
|
+
* the mega-compact bridge, with NO tools and NO console output, so it never
|
|
10
|
+
* pollutes the child's `--mode json` JSONL stdout that ithacus-spawn parses.
|
|
11
|
+
*
|
|
12
|
+
* Per the teammate brief this mirrors ithacus-child-mailbox.ts (default export,
|
|
13
|
+
* no console, dispose on session_shutdown) but registers ZERO tools — registering
|
|
14
|
+
* any tool risks a pi duplicate-tool-name hard-fail and children need none.
|
|
15
|
+
*
|
|
16
|
+
* PREVENT-PI-004: no network. The bridge is a same-repo relative import over a
|
|
17
|
+
* local sqlite store; the only I/O is the read-only `git rev-parse` inside
|
|
18
|
+
* repoStateDir. Nothing to flag.
|
|
19
|
+
*/
|
|
20
|
+
import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { createMegaBridge } from "../src/bridge.js";
|
|
22
|
+
import { repoStateDir } from "./mega-config.js";
|
|
23
|
+
import { STATE_DIR_DEFAULT } from "../src/config.js";
|
|
24
|
+
/** Default-ON env bool: only `=false`/`=0` disables (matches mega-config envBool). */
|
|
25
|
+
function envBool(name, fallback) {
|
|
26
|
+
const v = process.env[name];
|
|
27
|
+
if (v == null || v === "")
|
|
28
|
+
return fallback;
|
|
29
|
+
return v === "true" || v === "1";
|
|
30
|
+
}
|
|
31
|
+
/** Extract a recall query from a single AgentMessage (string or content blocks). */
|
|
32
|
+
function messageToText(m) {
|
|
33
|
+
const c = m.content;
|
|
34
|
+
if (typeof c === "string")
|
|
35
|
+
return c;
|
|
36
|
+
if (Array.isArray(c))
|
|
37
|
+
return c.map((b) => b.text ?? "").join(" ");
|
|
38
|
+
return "";
|
|
39
|
+
}
|
|
40
|
+
/** Convert a session's AgentMessages into the bridge's lightweight shape. */
|
|
41
|
+
function toBridgeMessages(ctx) {
|
|
42
|
+
const out = [];
|
|
43
|
+
try {
|
|
44
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
45
|
+
for (const m of sessionEntryToContextMessages(entry)) {
|
|
46
|
+
if (m.role === "user" || m.role === "assistant") {
|
|
47
|
+
out.push({ role: m.role, text: messageToText(m) });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
/* non-fatal: a child without a session manager yields no messages */
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
export default function (pi) {
|
|
58
|
+
// Flag read at LOAD time: a flag-OFF child registers nothing and a flag-ON
|
|
59
|
+
// child that never fires a hook pays zero cost (bridge is built lazily).
|
|
60
|
+
if (!envBool("MEGACOMPACT_ITHACUS_BRIDGE", true))
|
|
61
|
+
return;
|
|
62
|
+
let bridge;
|
|
63
|
+
// Build the bridge lazily on first hook fire so cost is opt-in by usage.
|
|
64
|
+
const getBridge = () => {
|
|
65
|
+
if (!bridge) {
|
|
66
|
+
bridge = createMegaBridge({
|
|
67
|
+
stateDir: repoStateDir(process.cwd(), STATE_DIR_DEFAULT),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
return bridge;
|
|
71
|
+
};
|
|
72
|
+
// S52-style recall injection: prepend staged checkpoints + durable memories
|
|
73
|
+
// to the system prompt, mirroring the main entry's before_agent_start path.
|
|
74
|
+
// 4th-layer stability guard: an unset/empty sessionId makes recall silently
|
|
75
|
+
// useless (the openclaw Date.now() gotcha), so skip outright.
|
|
76
|
+
pi.on("before_agent_start", async (event) => {
|
|
77
|
+
try {
|
|
78
|
+
const sessionId = process.env.ITHACUS_MEGA_SESSION_ID;
|
|
79
|
+
if (!sessionId || sessionId === "")
|
|
80
|
+
return undefined;
|
|
81
|
+
// Prefer the event's raw prompt; fall back to a generic query.
|
|
82
|
+
const query = event.prompt && event.prompt.trim() ? event.prompt.trim() : "";
|
|
83
|
+
if (query === "")
|
|
84
|
+
return undefined;
|
|
85
|
+
const b = getBridge();
|
|
86
|
+
const cp = b.recallCheckpoints({ sessionId, query, limit: 3 });
|
|
87
|
+
const mem = await b.recallMemories({ query, limit: 5 });
|
|
88
|
+
const blocks = [];
|
|
89
|
+
if (!cp.empty && cp.block)
|
|
90
|
+
blocks.push(cp.block);
|
|
91
|
+
if (!mem.empty && mem.block)
|
|
92
|
+
blocks.push(mem.block);
|
|
93
|
+
if (blocks.length === 0)
|
|
94
|
+
return undefined;
|
|
95
|
+
return { systemPrompt: `${event.systemPrompt ?? ""}\n\n${blocks.join("\n\n")}` };
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// layer b: non-fatal — never break the agent loop. No injection.
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
// Compaction on shutdown: persist the session's messages as a checkpoint.
|
|
103
|
+
// Best-effort; non-fatal. Releases the sqlite handle via close().
|
|
104
|
+
// The bridge is constructed lazily here too: a child that only compacts (no
|
|
105
|
+
// recall fired) still persists its session. Best-effort; non-fatal.
|
|
106
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
107
|
+
try {
|
|
108
|
+
const sessionId = process.env.ITHACUS_MEGA_SESSION_ID;
|
|
109
|
+
if (!sessionId || sessionId === "")
|
|
110
|
+
return;
|
|
111
|
+
const messages = toBridgeMessages(ctx);
|
|
112
|
+
if (messages.length === 0)
|
|
113
|
+
return;
|
|
114
|
+
await getBridge().compact({ sessionId, messages });
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
/* non-fatal */
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
if (bridge) {
|
|
121
|
+
bridge.close();
|
|
122
|
+
bridge = undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -198,6 +198,11 @@ export function loadConfig() {
|
|
|
198
198
|
// 3WF-1: TriggerGuard — guarantee a staged recall block on every context
|
|
199
199
|
// event even when session_start never fires. Default ON; OFF = byte-identical.
|
|
200
200
|
threeWayFailback: envBool("MEGACOMPACT_THREE_WAY_FAILBACK", true),
|
|
201
|
+
// Sprint A: Mega↔ithacus bridge — gate the child extension + bridge usage.
|
|
202
|
+
// Default ON; OFF (=0/`=false`) = byte-identical pre-bridge behavior.
|
|
203
|
+
// Runtime reads envBool(plain key), mirroring threeWayFailback (plain-write
|
|
204
|
+
// convention, not _DISABLED).
|
|
205
|
+
ithacusBridge: envBool("MEGACOMPACT_ITHACUS_BRIDGE", true),
|
|
201
206
|
// 3WF-2: ThrashGuard re-arm budget as a fraction of effectiveThreshold.
|
|
202
207
|
// 0.10 default (10% of the effective threshold) — see mega-config-types.
|
|
203
208
|
// Clamped to [0.01, 0.5]: below 1% the guard is almost never armed (any
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bridge/factory.ts — `createMegaBridge(opts)` implementation.
|
|
3
|
+
*
|
|
4
|
+
* A thin, pi-agnostic wrapper over the engine's compaction / recall / memory /
|
|
5
|
+
* fork / vector APIs. Stores are constructed lazily on first use (a consumer
|
|
6
|
+
* that only calls recallMemories pays no VectorStore cost). Exceptions
|
|
7
|
+
* propagate from every method except `fork` (catches ForkError by design) and
|
|
8
|
+
* `close` (swallows best-effort cleanup), so failures surface in tests.
|
|
9
|
+
*/
|
|
10
|
+
import { compactSession } from "../engine.js";
|
|
11
|
+
import { recallAndInline, recallAndInlineAsync, recallMemoriesAndInline, } from "../recall.js";
|
|
12
|
+
import { forkFromConversation, ForkError } from "../fork.js";
|
|
13
|
+
import { createTurnStore } from "../store/turns/index.js";
|
|
14
|
+
import { addMemory } from "../store/sqlite/memories.js";
|
|
15
|
+
import { VectorStore, vectorSearch } from "../vectorStore.js";
|
|
16
|
+
import { repoKey } from "../store/repoKey.js";
|
|
17
|
+
/** Map a RecallInjectResult to the bridge's slimmer result contract. */
|
|
18
|
+
function mapRecallResult(r) {
|
|
19
|
+
return {
|
|
20
|
+
block: r.block,
|
|
21
|
+
report: r.report,
|
|
22
|
+
hitCount: r.toInject.length,
|
|
23
|
+
empty: r.empty,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** Map the memoryRecallAndInline tuple result to the bridge contract. */
|
|
27
|
+
function mapMemoryResult(r) {
|
|
28
|
+
return {
|
|
29
|
+
block: r.block,
|
|
30
|
+
report: r.report,
|
|
31
|
+
hitCount: r.report.length,
|
|
32
|
+
empty: r.empty,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/** Map vectorSearch hits to the cortex result contract. */
|
|
36
|
+
function mapCortexHits(hits, limit) {
|
|
37
|
+
const top = hits.slice(0, limit);
|
|
38
|
+
return {
|
|
39
|
+
results: top.map((h) => ({
|
|
40
|
+
checkpointId: h.checkpoint.checkpointId,
|
|
41
|
+
score: h.score,
|
|
42
|
+
summary: h.checkpoint.summary,
|
|
43
|
+
})),
|
|
44
|
+
hitCount: top.length,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Create a MegaBridge over a single stateDir.
|
|
49
|
+
*
|
|
50
|
+
* The VectorStore and TurnStore are lazy: constructed on first use and cached
|
|
51
|
+
* in closures. The stateDir is retained for memory recall, which needs it
|
|
52
|
+
* directly.
|
|
53
|
+
*/
|
|
54
|
+
export function createMegaBridge(opts) {
|
|
55
|
+
const stateDir = opts.stateDir;
|
|
56
|
+
let vectorStore;
|
|
57
|
+
let turnStore;
|
|
58
|
+
const getVectorStore = () => {
|
|
59
|
+
if (!vectorStore)
|
|
60
|
+
vectorStore = new VectorStore({ stateDir });
|
|
61
|
+
return vectorStore;
|
|
62
|
+
};
|
|
63
|
+
const getTurnStore = () => {
|
|
64
|
+
if (!turnStore)
|
|
65
|
+
turnStore = createTurnStore({ stateDir });
|
|
66
|
+
return turnStore;
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
compact(input) {
|
|
70
|
+
const result = compactSession({
|
|
71
|
+
sessionId: input.sessionId,
|
|
72
|
+
messages: input.messages,
|
|
73
|
+
keepFrom: input.keepFrom,
|
|
74
|
+
summary: input.summary,
|
|
75
|
+
keyDecisions: input.keyDecisions,
|
|
76
|
+
nextSteps: input.nextSteps,
|
|
77
|
+
filesModified: input.filesModified,
|
|
78
|
+
compressionPressure: input.compressionPressure,
|
|
79
|
+
}, getVectorStore());
|
|
80
|
+
return {
|
|
81
|
+
skipped: result.skipped,
|
|
82
|
+
deduped: result.deduped,
|
|
83
|
+
summary: result.summary,
|
|
84
|
+
checkpointId: result.checkpointId,
|
|
85
|
+
tokenEstimate: result.tokenEstimate,
|
|
86
|
+
originalTokenEstimate: result.originalTokenEstimate,
|
|
87
|
+
compactedFrom: result.compactedFrom,
|
|
88
|
+
};
|
|
89
|
+
},
|
|
90
|
+
recallCheckpoints(opts) {
|
|
91
|
+
const recallOpts = {
|
|
92
|
+
sessionId: opts.sessionId,
|
|
93
|
+
query: opts.query,
|
|
94
|
+
limit: opts.limit ?? 3,
|
|
95
|
+
source: "command",
|
|
96
|
+
skipInjected: opts.skipInjected,
|
|
97
|
+
recallMaxTokens: opts.recallMaxTokens,
|
|
98
|
+
};
|
|
99
|
+
return mapRecallResult(recallAndInline(recallOpts, getVectorStore()));
|
|
100
|
+
},
|
|
101
|
+
async recallMemories(opts) {
|
|
102
|
+
const memOpts = {
|
|
103
|
+
query: opts.query,
|
|
104
|
+
stateDir,
|
|
105
|
+
limit: opts.limit,
|
|
106
|
+
minSimilarity: opts.minSimilarity,
|
|
107
|
+
crossRepo: opts.crossRepo,
|
|
108
|
+
crossRepoCosine: opts.crossRepoCosine,
|
|
109
|
+
recallMaxTokens: opts.recallMaxTokens,
|
|
110
|
+
};
|
|
111
|
+
const r = await recallMemoriesAndInline(memOpts);
|
|
112
|
+
return mapMemoryResult(r);
|
|
113
|
+
},
|
|
114
|
+
async recallAndInlineAsync(opts) {
|
|
115
|
+
const recallOpts = {
|
|
116
|
+
sessionId: opts.sessionId,
|
|
117
|
+
query: opts.query,
|
|
118
|
+
limit: opts.limit ?? 3,
|
|
119
|
+
source: "command",
|
|
120
|
+
skipInjected: opts.skipInjected,
|
|
121
|
+
recallMaxTokens: opts.recallMaxTokens,
|
|
122
|
+
};
|
|
123
|
+
const r = await recallAndInlineAsync(recallOpts, getVectorStore());
|
|
124
|
+
return mapRecallResult(r);
|
|
125
|
+
},
|
|
126
|
+
fork(opts) {
|
|
127
|
+
try {
|
|
128
|
+
const outcome = forkFromConversation(getTurnStore(), opts.parentConversationId, opts.turnIndex);
|
|
129
|
+
return {
|
|
130
|
+
childConversationId: outcome.childConversationId,
|
|
131
|
+
checkpointIds: outcome.checkpointIds,
|
|
132
|
+
forkTurnIndex: opts.turnIndex,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
if (e instanceof ForkError) {
|
|
137
|
+
return { error: e.code };
|
|
138
|
+
}
|
|
139
|
+
throw e;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
cortexQuery(opts) {
|
|
143
|
+
const limit = opts.limit ?? 3;
|
|
144
|
+
const scope = opts.repo ?? repoKey(stateDir);
|
|
145
|
+
const hits = vectorSearch(getVectorStore(), scope, opts.query, limit);
|
|
146
|
+
return mapCortexHits(hits, limit);
|
|
147
|
+
},
|
|
148
|
+
addMemory(input) {
|
|
149
|
+
// repo === null ⇒ stateDir-scoped durable memory (matches recallMemories).
|
|
150
|
+
return addMemory({
|
|
151
|
+
kind: input.kind,
|
|
152
|
+
content: input.content,
|
|
153
|
+
tags: input.tags,
|
|
154
|
+
category: input.category,
|
|
155
|
+
}, null, stateDir);
|
|
156
|
+
},
|
|
157
|
+
recordTurn(input) {
|
|
158
|
+
const turn = {
|
|
159
|
+
conversationId: input.conversationId,
|
|
160
|
+
sessionId: input.sessionId,
|
|
161
|
+
turnIndex: input.turnIndex,
|
|
162
|
+
role: input.role ?? "assistant",
|
|
163
|
+
endedAt: input.endedAt ?? Date.now(),
|
|
164
|
+
ctxTokens: input.ctxTokens,
|
|
165
|
+
ctxPercent: input.ctxPercent,
|
|
166
|
+
model: input.model,
|
|
167
|
+
};
|
|
168
|
+
getTurnStore().asWriter().appendTurn(turn);
|
|
169
|
+
},
|
|
170
|
+
close() {
|
|
171
|
+
if (turnStore) {
|
|
172
|
+
try {
|
|
173
|
+
turnStore.close();
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
/* best-effort */
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bridge/types.ts — contract types for the bidirectional mega-compact bridge.
|
|
3
|
+
*
|
|
4
|
+
* A pi-agnostic, unit-testable adapter surface that wraps the engine's
|
|
5
|
+
* compaction / recall / memory / fork / vector APIs behind one factory so an
|
|
6
|
+
* external host (ithacus) can drive them without importing pi-runtime types.
|
|
7
|
+
* Every type here mirrors a real engine signature (see factory.ts for the
|
|
8
|
+
* wiring).
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createMegaBridge } from "./bridge/factory.js";
|
|
@@ -305,6 +305,12 @@ export const SETTINGS: ReadonlyArray<SettingGroup> = [
|
|
|
305
305
|
"Umbrella for the 3-way failback safety system: TriggerGuard (stages a recall block even when session_start never fires), the live-window ReductionValidator + persisted ThrashGuard (stops ineffective compaction re-fire loops), the 3-source read-only recall vote + same-repo relevance floor, and InjectionConfirm (asserts the staged block reached the message list pi sends). OFF = byte-identical pre-3WF behavior (v0.20.83). Runtime reads envBool(plain key), so this uses the plain-write convention (not _DISABLED).",
|
|
306
306
|
true,
|
|
307
307
|
),
|
|
308
|
+
boolDirect(
|
|
309
|
+
"MEGACOMPACT_ITHACUS_BRIDGE",
|
|
310
|
+
"ithacus Bridge",
|
|
311
|
+
"Mega↔ithacus bridge: gate the child extension + bridge usage that tie this extension to ithacus's durable compaction. Default ON; OFF (=0/`=false`) is byte-identical to pre-bridge behavior — the bridge is only consulted when this is ON. Positive sprint flag. Runtime reads envBool(plain key), so this uses the plain-write convention (not _DISABLED).",
|
|
312
|
+
true,
|
|
313
|
+
),
|
|
308
314
|
boolDirect(
|
|
309
315
|
"MEGACOMPACT_RECALL_TAIL_INJECT",
|
|
310
316
|
"Recall Tail Inject",
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mega-compact-child.ts — minimal extension loaded ONLY into dispatched child
|
|
3
|
+
* pi subprocesses (spawned by ithacus with a second `-e` flag).
|
|
4
|
+
*
|
|
5
|
+
* Design: a child is a FRESH pi process started with `--no-extensions -e <this
|
|
6
|
+
* file>` (see ithacus-spawn.ts). It does NOT receive the parent's MegaConfig and
|
|
7
|
+
* is a separate process, so it reads its two control env vars directly and owns
|
|
8
|
+
* its own bridge. It gives children recall-at-start + compaction-on-shutdown via
|
|
9
|
+
* the mega-compact bridge, with NO tools and NO console output, so it never
|
|
10
|
+
* pollutes the child's `--mode json` JSONL stdout that ithacus-spawn parses.
|
|
11
|
+
*
|
|
12
|
+
* Per the teammate brief this mirrors ithacus-child-mailbox.ts (default export,
|
|
13
|
+
* no console, dispose on session_shutdown) but registers ZERO tools — registering
|
|
14
|
+
* any tool risks a pi duplicate-tool-name hard-fail and children need none.
|
|
15
|
+
*
|
|
16
|
+
* PREVENT-PI-004: no network. The bridge is a same-repo relative import over a
|
|
17
|
+
* local sqlite store; the only I/O is the read-only `git rev-parse` inside
|
|
18
|
+
* repoStateDir. Nothing to flag.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import { sessionEntryToContextMessages } from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import { createMegaBridge } from "../src/bridge.js";
|
|
24
|
+
import type {
|
|
25
|
+
MegaBridge,
|
|
26
|
+
BridgeMessage,
|
|
27
|
+
} from "../src/bridge.js";
|
|
28
|
+
import { repoStateDir } from "./mega-config.js";
|
|
29
|
+
import { STATE_DIR_DEFAULT } from "../src/config.js";
|
|
30
|
+
|
|
31
|
+
/** Default-ON env bool: only `=false`/`=0` disables (matches mega-config envBool). */
|
|
32
|
+
function envBool(name: string, fallback: boolean): boolean {
|
|
33
|
+
const v = process.env[name];
|
|
34
|
+
if (v == null || v === "") return fallback;
|
|
35
|
+
return v === "true" || v === "1";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Extract a recall query from a single AgentMessage (string or content blocks). */
|
|
39
|
+
function messageToText(m: { content: unknown }): string {
|
|
40
|
+
const c = (m as { content: unknown }).content;
|
|
41
|
+
if (typeof c === "string") return c;
|
|
42
|
+
if (Array.isArray(c)) return c.map((b: { text?: string }) => b.text ?? "").join(" ");
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Convert a session's AgentMessages into the bridge's lightweight shape. */
|
|
47
|
+
function toBridgeMessages(ctx: ExtensionContext): BridgeMessage[] {
|
|
48
|
+
const out: BridgeMessage[] = [];
|
|
49
|
+
try {
|
|
50
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
51
|
+
for (const m of sessionEntryToContextMessages(entry as never)) {
|
|
52
|
+
if (m.role === "user" || m.role === "assistant") {
|
|
53
|
+
out.push({ role: m.role, text: messageToText(m) });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
/* non-fatal: a child without a session manager yields no messages */
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export default function (pi: ExtensionAPI): void {
|
|
64
|
+
// Flag read at LOAD time: a flag-OFF child registers nothing and a flag-ON
|
|
65
|
+
// child that never fires a hook pays zero cost (bridge is built lazily).
|
|
66
|
+
if (!envBool("MEGACOMPACT_ITHACUS_BRIDGE", true)) return;
|
|
67
|
+
|
|
68
|
+
let bridge: MegaBridge | undefined;
|
|
69
|
+
|
|
70
|
+
// Build the bridge lazily on first hook fire so cost is opt-in by usage.
|
|
71
|
+
const getBridge = (): MegaBridge => {
|
|
72
|
+
if (!bridge) {
|
|
73
|
+
bridge = createMegaBridge({
|
|
74
|
+
stateDir: repoStateDir(process.cwd(), STATE_DIR_DEFAULT),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
return bridge;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// S52-style recall injection: prepend staged checkpoints + durable memories
|
|
81
|
+
// to the system prompt, mirroring the main entry's before_agent_start path.
|
|
82
|
+
// 4th-layer stability guard: an unset/empty sessionId makes recall silently
|
|
83
|
+
// useless (the openclaw Date.now() gotcha), so skip outright.
|
|
84
|
+
pi.on("before_agent_start", async (event) => {
|
|
85
|
+
try {
|
|
86
|
+
const sessionId = process.env.ITHACUS_MEGA_SESSION_ID;
|
|
87
|
+
if (!sessionId || sessionId === "") return undefined;
|
|
88
|
+
|
|
89
|
+
// Prefer the event's raw prompt; fall back to a generic query.
|
|
90
|
+
const query = event.prompt && event.prompt.trim() ? event.prompt.trim() : "";
|
|
91
|
+
if (query === "") return undefined;
|
|
92
|
+
|
|
93
|
+
const b = getBridge();
|
|
94
|
+
const cp = b.recallCheckpoints({ sessionId, query, limit: 3 });
|
|
95
|
+
const mem = await b.recallMemories({ query, limit: 5 });
|
|
96
|
+
|
|
97
|
+
const blocks: string[] = [];
|
|
98
|
+
if (!cp.empty && cp.block) blocks.push(cp.block);
|
|
99
|
+
if (!mem.empty && mem.block) blocks.push(mem.block);
|
|
100
|
+
if (blocks.length === 0) return undefined;
|
|
101
|
+
|
|
102
|
+
return { systemPrompt: `${event.systemPrompt ?? ""}\n\n${blocks.join("\n\n")}` };
|
|
103
|
+
} catch {
|
|
104
|
+
// layer b: non-fatal — never break the agent loop. No injection.
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Compaction on shutdown: persist the session's messages as a checkpoint.
|
|
110
|
+
// Best-effort; non-fatal. Releases the sqlite handle via close().
|
|
111
|
+
// The bridge is constructed lazily here too: a child that only compacts (no
|
|
112
|
+
// recall fired) still persists its session. Best-effort; non-fatal.
|
|
113
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
114
|
+
try {
|
|
115
|
+
const sessionId = process.env.ITHACUS_MEGA_SESSION_ID;
|
|
116
|
+
if (!sessionId || sessionId === "") return;
|
|
117
|
+
const messages = toBridgeMessages(ctx);
|
|
118
|
+
if (messages.length === 0) return;
|
|
119
|
+
await getBridge().compact({ sessionId, messages });
|
|
120
|
+
} catch {
|
|
121
|
+
/* non-fatal */
|
|
122
|
+
} finally {
|
|
123
|
+
if (bridge) {
|
|
124
|
+
bridge.close();
|
|
125
|
+
bridge = undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
@@ -159,6 +159,11 @@ export interface MegaConfig {
|
|
|
159
159
|
* to the pre-change OFF state. The single gate lives at the call site
|
|
160
160
|
* (tailResult.ts, config.messageSeparation), not inside buildSeparatedPrompt. */
|
|
161
161
|
messageSeparation: boolean;
|
|
162
|
+
/** Sprint A: Mega↔ithacus bridge — gate the child extension + bridge usage
|
|
163
|
+
* that tie this extension to ithacus's durable compaction. Positive sprint
|
|
164
|
+
* flag, default ON; flag-OFF (=0/`=false`) is byte-identical to pre-bridge
|
|
165
|
+
* behavior (the bridge is only consulted when this is ON). */
|
|
166
|
+
ithacusBridge: boolean;
|
|
162
167
|
/** P3: Cache-aware striping (PLAN_V2 Phase 3). Inserts stability-ordered
|
|
163
168
|
* cache stripes between summaries and thread. Default OFF. */
|
|
164
169
|
cacheStriping: boolean;
|
|
@@ -234,6 +234,11 @@ export function loadConfig(): MegaConfig {
|
|
|
234
234
|
// 3WF-1: TriggerGuard — guarantee a staged recall block on every context
|
|
235
235
|
// event even when session_start never fires. Default ON; OFF = byte-identical.
|
|
236
236
|
threeWayFailback: envBool("MEGACOMPACT_THREE_WAY_FAILBACK", true),
|
|
237
|
+
// Sprint A: Mega↔ithacus bridge — gate the child extension + bridge usage.
|
|
238
|
+
// Default ON; OFF (=0/`=false`) = byte-identical pre-bridge behavior.
|
|
239
|
+
// Runtime reads envBool(plain key), mirroring threeWayFailback (plain-write
|
|
240
|
+
// convention, not _DISABLED).
|
|
241
|
+
ithacusBridge: envBool("MEGACOMPACT_ITHACUS_BRIDGE", true),
|
|
237
242
|
// 3WF-2: ThrashGuard re-arm budget as a fraction of effectiveThreshold.
|
|
238
243
|
// 0.10 default (10% of the effective threshold) — see mega-config-types.
|
|
239
244
|
// Clamped to [0.01, 0.5]: below 1% the guard is almost never armed (any
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-mega-compact",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.3",
|
|
4
4
|
"description": "Layered, local, vector-backed context compressor for pi — supersede/collapse/cluster compaction with deduped inline recall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-3-Clause",
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bridge/factory.ts — `createMegaBridge(opts)` implementation.
|
|
3
|
+
*
|
|
4
|
+
* A thin, pi-agnostic wrapper over the engine's compaction / recall / memory /
|
|
5
|
+
* fork / vector APIs. Stores are constructed lazily on first use (a consumer
|
|
6
|
+
* that only calls recallMemories pays no VectorStore cost). Exceptions
|
|
7
|
+
* propagate from every method except `fork` (catches ForkError by design) and
|
|
8
|
+
* `close` (swallows best-effort cleanup), so failures surface in tests.
|
|
9
|
+
*/
|
|
10
|
+
import { compactSession } from "../engine.js";
|
|
11
|
+
import {
|
|
12
|
+
recallAndInline,
|
|
13
|
+
recallAndInlineAsync,
|
|
14
|
+
recallMemoriesAndInline,
|
|
15
|
+
} from "../recall.js";
|
|
16
|
+
import type {
|
|
17
|
+
RecallInjectOptions,
|
|
18
|
+
RecallInjectResult,
|
|
19
|
+
MemoryRecallInjectOptions,
|
|
20
|
+
} from "../recall/types.js";
|
|
21
|
+
import { forkFromConversation, ForkError } from "../fork.js";
|
|
22
|
+
import { createTurnStore } from "../store/turns/index.js";
|
|
23
|
+
import type { TurnStore, TurnEntry } from "../store/turns/types.js";
|
|
24
|
+
import { addMemory } from "../store/sqlite/memories.js";
|
|
25
|
+
import { VectorStore, vectorSearch } from "../vectorStore.js";
|
|
26
|
+
import type { SearchHit } from "../vectorStore.js";
|
|
27
|
+
import { repoKey } from "../store/repoKey.js";
|
|
28
|
+
import type {
|
|
29
|
+
BridgeOptions,
|
|
30
|
+
BridgeCompactInput,
|
|
31
|
+
BridgeCompactResult,
|
|
32
|
+
BridgeRecallOptions,
|
|
33
|
+
BridgeRecallResult,
|
|
34
|
+
BridgeMemoryRecallOptions,
|
|
35
|
+
BridgeMemoryRecallResult,
|
|
36
|
+
BridgeForkOptions,
|
|
37
|
+
BridgeForkResult,
|
|
38
|
+
BridgeCortexOptions,
|
|
39
|
+
BridgeCortexResult,
|
|
40
|
+
BridgeAddMemoryInput,
|
|
41
|
+
BridgeRecordTurnInput,
|
|
42
|
+
MegaBridge,
|
|
43
|
+
} from "./types.js";
|
|
44
|
+
|
|
45
|
+
/** Map a RecallInjectResult to the bridge's slimmer result contract. */
|
|
46
|
+
function mapRecallResult(r: RecallInjectResult): BridgeRecallResult {
|
|
47
|
+
return {
|
|
48
|
+
block: r.block,
|
|
49
|
+
report: r.report,
|
|
50
|
+
hitCount: r.toInject.length,
|
|
51
|
+
empty: r.empty,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Map the memoryRecallAndInline tuple result to the bridge contract. */
|
|
56
|
+
function mapMemoryResult(
|
|
57
|
+
r: { empty: boolean; block: string; report: string[] },
|
|
58
|
+
): BridgeMemoryRecallResult {
|
|
59
|
+
return {
|
|
60
|
+
block: r.block,
|
|
61
|
+
report: r.report,
|
|
62
|
+
hitCount: r.report.length,
|
|
63
|
+
empty: r.empty,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Map vectorSearch hits to the cortex result contract. */
|
|
68
|
+
function mapCortexHits(hits: SearchHit[], limit: number): BridgeCortexResult {
|
|
69
|
+
const top = hits.slice(0, limit);
|
|
70
|
+
return {
|
|
71
|
+
results: top.map((h) => ({
|
|
72
|
+
checkpointId: h.checkpoint.checkpointId,
|
|
73
|
+
score: h.score,
|
|
74
|
+
summary: h.checkpoint.summary,
|
|
75
|
+
})),
|
|
76
|
+
hitCount: top.length,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create a MegaBridge over a single stateDir.
|
|
82
|
+
*
|
|
83
|
+
* The VectorStore and TurnStore are lazy: constructed on first use and cached
|
|
84
|
+
* in closures. The stateDir is retained for memory recall, which needs it
|
|
85
|
+
* directly.
|
|
86
|
+
*/
|
|
87
|
+
export function createMegaBridge(opts: BridgeOptions): MegaBridge {
|
|
88
|
+
const stateDir = opts.stateDir;
|
|
89
|
+
let vectorStore: VectorStore | undefined;
|
|
90
|
+
let turnStore: TurnStore | undefined;
|
|
91
|
+
|
|
92
|
+
const getVectorStore = (): VectorStore => {
|
|
93
|
+
if (!vectorStore) vectorStore = new VectorStore({ stateDir });
|
|
94
|
+
return vectorStore;
|
|
95
|
+
};
|
|
96
|
+
const getTurnStore = (): TurnStore => {
|
|
97
|
+
if (!turnStore) turnStore = createTurnStore({ stateDir });
|
|
98
|
+
return turnStore;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
compact(input: BridgeCompactInput): BridgeCompactResult {
|
|
103
|
+
const result = compactSession(
|
|
104
|
+
{
|
|
105
|
+
sessionId: input.sessionId,
|
|
106
|
+
messages: input.messages,
|
|
107
|
+
keepFrom: input.keepFrom,
|
|
108
|
+
summary: input.summary,
|
|
109
|
+
keyDecisions: input.keyDecisions,
|
|
110
|
+
nextSteps: input.nextSteps,
|
|
111
|
+
filesModified: input.filesModified,
|
|
112
|
+
compressionPressure: input.compressionPressure,
|
|
113
|
+
},
|
|
114
|
+
getVectorStore(),
|
|
115
|
+
);
|
|
116
|
+
return {
|
|
117
|
+
skipped: result.skipped,
|
|
118
|
+
deduped: result.deduped,
|
|
119
|
+
summary: result.summary,
|
|
120
|
+
checkpointId: result.checkpointId,
|
|
121
|
+
tokenEstimate: result.tokenEstimate,
|
|
122
|
+
originalTokenEstimate: result.originalTokenEstimate,
|
|
123
|
+
compactedFrom: result.compactedFrom,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
recallCheckpoints(opts: BridgeRecallOptions): BridgeRecallResult {
|
|
128
|
+
const recallOpts: RecallInjectOptions = {
|
|
129
|
+
sessionId: opts.sessionId,
|
|
130
|
+
query: opts.query,
|
|
131
|
+
limit: opts.limit ?? 3,
|
|
132
|
+
source: "command",
|
|
133
|
+
skipInjected: opts.skipInjected,
|
|
134
|
+
recallMaxTokens: opts.recallMaxTokens,
|
|
135
|
+
};
|
|
136
|
+
return mapRecallResult(recallAndInline(recallOpts, getVectorStore()));
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
async recallMemories(opts: BridgeMemoryRecallOptions): Promise<BridgeMemoryRecallResult> {
|
|
140
|
+
const memOpts: MemoryRecallInjectOptions = {
|
|
141
|
+
query: opts.query,
|
|
142
|
+
stateDir,
|
|
143
|
+
limit: opts.limit,
|
|
144
|
+
minSimilarity: opts.minSimilarity,
|
|
145
|
+
crossRepo: opts.crossRepo,
|
|
146
|
+
crossRepoCosine: opts.crossRepoCosine,
|
|
147
|
+
recallMaxTokens: opts.recallMaxTokens,
|
|
148
|
+
};
|
|
149
|
+
const r = await recallMemoriesAndInline(memOpts);
|
|
150
|
+
return mapMemoryResult(r);
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
async recallAndInlineAsync(opts: BridgeRecallOptions): Promise<BridgeRecallResult> {
|
|
154
|
+
const recallOpts: RecallInjectOptions = {
|
|
155
|
+
sessionId: opts.sessionId,
|
|
156
|
+
query: opts.query,
|
|
157
|
+
limit: opts.limit ?? 3,
|
|
158
|
+
source: "command",
|
|
159
|
+
skipInjected: opts.skipInjected,
|
|
160
|
+
recallMaxTokens: opts.recallMaxTokens,
|
|
161
|
+
};
|
|
162
|
+
const r = await recallAndInlineAsync(recallOpts, getVectorStore());
|
|
163
|
+
return mapRecallResult(r);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
fork(opts: BridgeForkOptions): BridgeForkResult {
|
|
167
|
+
try {
|
|
168
|
+
const outcome = forkFromConversation(
|
|
169
|
+
getTurnStore(),
|
|
170
|
+
opts.parentConversationId,
|
|
171
|
+
opts.turnIndex,
|
|
172
|
+
);
|
|
173
|
+
return {
|
|
174
|
+
childConversationId: outcome.childConversationId,
|
|
175
|
+
checkpointIds: outcome.checkpointIds,
|
|
176
|
+
forkTurnIndex: opts.turnIndex,
|
|
177
|
+
};
|
|
178
|
+
} catch (e) {
|
|
179
|
+
if (e instanceof ForkError) {
|
|
180
|
+
return { error: e.code };
|
|
181
|
+
}
|
|
182
|
+
throw e;
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
cortexQuery(opts: BridgeCortexOptions): BridgeCortexResult {
|
|
187
|
+
const limit = opts.limit ?? 3;
|
|
188
|
+
const scope = opts.repo ?? repoKey(stateDir);
|
|
189
|
+
const hits = vectorSearch(getVectorStore(), scope, opts.query, limit);
|
|
190
|
+
return mapCortexHits(hits, limit);
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
addMemory(input: BridgeAddMemoryInput): number | void {
|
|
194
|
+
// repo === null ⇒ stateDir-scoped durable memory (matches recallMemories).
|
|
195
|
+
return addMemory(
|
|
196
|
+
{
|
|
197
|
+
kind: input.kind,
|
|
198
|
+
content: input.content,
|
|
199
|
+
tags: input.tags,
|
|
200
|
+
category: input.category,
|
|
201
|
+
},
|
|
202
|
+
null,
|
|
203
|
+
stateDir,
|
|
204
|
+
);
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
recordTurn(input: BridgeRecordTurnInput): void {
|
|
208
|
+
const turn: TurnEntry = {
|
|
209
|
+
conversationId: input.conversationId,
|
|
210
|
+
sessionId: input.sessionId,
|
|
211
|
+
turnIndex: input.turnIndex,
|
|
212
|
+
role: (input.role as TurnEntry["role"]) ?? "assistant",
|
|
213
|
+
endedAt: input.endedAt ?? Date.now(),
|
|
214
|
+
ctxTokens: input.ctxTokens,
|
|
215
|
+
ctxPercent: input.ctxPercent,
|
|
216
|
+
model: input.model,
|
|
217
|
+
};
|
|
218
|
+
getTurnStore().asWriter().appendTurn(turn);
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
close(): void {
|
|
222
|
+
if (turnStore) {
|
|
223
|
+
try {
|
|
224
|
+
turnStore.close();
|
|
225
|
+
} catch {
|
|
226
|
+
/* best-effort */
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bridge/types.ts — contract types for the bidirectional mega-compact bridge.
|
|
3
|
+
*
|
|
4
|
+
* A pi-agnostic, unit-testable adapter surface that wraps the engine's
|
|
5
|
+
* compaction / recall / memory / fork / vector APIs behind one factory so an
|
|
6
|
+
* external host (ithacus) can drive them without importing pi-runtime types.
|
|
7
|
+
* Every type here mirrors a real engine signature (see factory.ts for the
|
|
8
|
+
* wiring).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { EngineMessage } from "../types.js";
|
|
12
|
+
|
|
13
|
+
/** Same shape as the engine's internal message. Identity re-export. */
|
|
14
|
+
export type BridgeMessage = EngineMessage;
|
|
15
|
+
|
|
16
|
+
/** Bridge construction options. */
|
|
17
|
+
export interface BridgeOptions {
|
|
18
|
+
stateDir: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Input to compact a message slice into a checkpoint. */
|
|
22
|
+
export interface BridgeCompactInput {
|
|
23
|
+
sessionId: string;
|
|
24
|
+
messages: BridgeMessage[];
|
|
25
|
+
keepFrom?: number;
|
|
26
|
+
summary?: string;
|
|
27
|
+
keyDecisions?: string[];
|
|
28
|
+
nextSteps?: string[];
|
|
29
|
+
filesModified?: string[];
|
|
30
|
+
compressionPressure?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Useful subset of CompactResult. */
|
|
34
|
+
export interface BridgeCompactResult {
|
|
35
|
+
skipped: boolean;
|
|
36
|
+
deduped: boolean;
|
|
37
|
+
summary: string;
|
|
38
|
+
checkpointId?: string;
|
|
39
|
+
tokenEstimate: number;
|
|
40
|
+
originalTokenEstimate?: number;
|
|
41
|
+
compactedFrom?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Options for checkpoint recall (per-session). */
|
|
45
|
+
export interface BridgeRecallOptions {
|
|
46
|
+
sessionId: string;
|
|
47
|
+
query: string;
|
|
48
|
+
limit?: number;
|
|
49
|
+
recallMaxTokens?: number;
|
|
50
|
+
skipInjected?: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Mapped from RecallInjectResult. */
|
|
54
|
+
export interface BridgeRecallResult {
|
|
55
|
+
block: string;
|
|
56
|
+
report: string[];
|
|
57
|
+
hitCount: number;
|
|
58
|
+
empty: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Options for durable memory recall (stateDir-scoped, no sessionId). */
|
|
62
|
+
export interface BridgeMemoryRecallOptions {
|
|
63
|
+
query: string;
|
|
64
|
+
limit?: number;
|
|
65
|
+
minSimilarity?: number;
|
|
66
|
+
crossRepo?: boolean;
|
|
67
|
+
crossRepoCosine?: number;
|
|
68
|
+
recallMaxTokens?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface BridgeMemoryRecallResult {
|
|
72
|
+
block: string;
|
|
73
|
+
report: string[];
|
|
74
|
+
hitCount: number;
|
|
75
|
+
empty: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Options to fork a child conversation off a parent turn. */
|
|
79
|
+
export interface BridgeForkOptions {
|
|
80
|
+
parentConversationId: string;
|
|
81
|
+
turnIndex: number;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Fork result: success variant OR a graceful error variant. */
|
|
85
|
+
export interface BridgeForkSuccess {
|
|
86
|
+
childConversationId: string;
|
|
87
|
+
checkpointIds: string[];
|
|
88
|
+
forkTurnIndex: number;
|
|
89
|
+
}
|
|
90
|
+
export interface BridgeForkError {
|
|
91
|
+
error: "TURN_NOT_FOUND" | "NO_RECALL";
|
|
92
|
+
}
|
|
93
|
+
export type BridgeForkResult = BridgeForkSuccess | BridgeForkError;
|
|
94
|
+
|
|
95
|
+
/** Options for a top-k corpus / vector query. */
|
|
96
|
+
export interface BridgeCortexOptions {
|
|
97
|
+
query: string;
|
|
98
|
+
limit?: number;
|
|
99
|
+
repo?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface BridgeCortexResult {
|
|
103
|
+
results: Array<{ checkpointId: string; score: number; summary?: string }>;
|
|
104
|
+
hitCount: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Input to persist a durable memory. */
|
|
108
|
+
export interface BridgeAddMemoryInput {
|
|
109
|
+
content: string;
|
|
110
|
+
kind?: string;
|
|
111
|
+
tags?: string[];
|
|
112
|
+
category?: string;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Input to record a turn fact. */
|
|
116
|
+
export interface BridgeRecordTurnInput {
|
|
117
|
+
conversationId: string;
|
|
118
|
+
sessionId: string;
|
|
119
|
+
turnIndex: number;
|
|
120
|
+
role?: string;
|
|
121
|
+
endedAt?: number;
|
|
122
|
+
ctxTokens?: number;
|
|
123
|
+
ctxPercent?: number;
|
|
124
|
+
model?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The bridge surface exposed to the host. */
|
|
128
|
+
export interface MegaBridge {
|
|
129
|
+
compact(input: BridgeCompactInput): BridgeCompactResult;
|
|
130
|
+
recallCheckpoints(opts: BridgeRecallOptions): BridgeRecallResult;
|
|
131
|
+
recallMemories(opts: BridgeMemoryRecallOptions): Promise<BridgeMemoryRecallResult>;
|
|
132
|
+
recallAndInlineAsync(opts: BridgeRecallOptions): Promise<BridgeRecallResult>;
|
|
133
|
+
fork(opts: BridgeForkOptions): BridgeForkResult;
|
|
134
|
+
cortexQuery(opts: BridgeCortexOptions): BridgeCortexResult;
|
|
135
|
+
addMemory(input: BridgeAddMemoryInput): number | void;
|
|
136
|
+
recordTurn(input: BridgeRecordTurnInput): void;
|
|
137
|
+
close(): void;
|
|
138
|
+
}
|
package/src/bridge.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* bridge.ts — public barrel for the mega-compact bidirectional bridge.
|
|
3
|
+
*
|
|
4
|
+
* Hosts import only this file. The factory and all contracts live under
|
|
5
|
+
* src/bridge/ (kept thin per the delegate-shell pattern).
|
|
6
|
+
*/
|
|
7
|
+
export type {
|
|
8
|
+
MegaBridge,
|
|
9
|
+
BridgeOptions,
|
|
10
|
+
BridgeMessage,
|
|
11
|
+
BridgeCompactInput,
|
|
12
|
+
BridgeCompactResult,
|
|
13
|
+
BridgeRecallOptions,
|
|
14
|
+
BridgeRecallResult,
|
|
15
|
+
BridgeMemoryRecallOptions,
|
|
16
|
+
BridgeMemoryRecallResult,
|
|
17
|
+
BridgeForkOptions,
|
|
18
|
+
BridgeForkResult,
|
|
19
|
+
BridgeCortexOptions,
|
|
20
|
+
BridgeCortexResult,
|
|
21
|
+
BridgeAddMemoryInput,
|
|
22
|
+
BridgeRecordTurnInput,
|
|
23
|
+
} from "./bridge/types.js";
|
|
24
|
+
export { createMegaBridge } from "./bridge/factory.js";
|