@chendpoc/pi-memory 0.1.12 → 0.1.13
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 +88 -16
- package/dist/cache/memoryCaches.d.ts +32 -0
- package/dist/cache/memoryCaches.d.ts.map +1 -0
- package/dist/cache/memoryCaches.js +59 -0
- package/dist/cache/memoryCaches.js.map +1 -0
- package/dist/cli.js +123 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +28 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +37 -1
- package/dist/config.js.map +1 -1
- package/dist/consolidation/enqueue.d.ts +33 -0
- package/dist/consolidation/enqueue.d.ts.map +1 -0
- package/dist/consolidation/enqueue.js +145 -0
- package/dist/consolidation/enqueue.js.map +1 -0
- package/dist/consolidation/index.d.ts +19 -0
- package/dist/consolidation/index.d.ts.map +1 -0
- package/dist/consolidation/index.js +15 -0
- package/dist/consolidation/index.js.map +1 -0
- package/dist/consolidation/lock.d.ts +5 -0
- package/dist/consolidation/lock.d.ts.map +1 -0
- package/dist/consolidation/lock.js +33 -0
- package/dist/consolidation/lock.js.map +1 -0
- package/dist/consolidation/log.d.ts +8 -0
- package/dist/consolidation/log.d.ts.map +1 -0
- package/dist/consolidation/log.js +32 -0
- package/dist/consolidation/log.js.map +1 -0
- package/dist/consolidation/memoryIndex.d.ts +18 -0
- package/dist/consolidation/memoryIndex.d.ts.map +1 -0
- package/dist/consolidation/memoryIndex.js +100 -0
- package/dist/consolidation/memoryIndex.js.map +1 -0
- package/dist/consolidation/phase2/runPhase2.d.ts +23 -0
- package/dist/consolidation/phase2/runPhase2.d.ts.map +1 -0
- package/dist/consolidation/phase2/runPhase2.js +196 -0
- package/dist/consolidation/phase2/runPhase2.js.map +1 -0
- package/dist/consolidation/scheduler/launchd.d.ts +5 -0
- package/dist/consolidation/scheduler/launchd.d.ts.map +1 -0
- package/dist/consolidation/scheduler/launchd.js +54 -0
- package/dist/consolidation/scheduler/launchd.js.map +1 -0
- package/dist/consolidation/scheduler/runConsolidate.d.ts +26 -0
- package/dist/consolidation/scheduler/runConsolidate.d.ts.map +1 -0
- package/dist/consolidation/scheduler/runConsolidate.js +83 -0
- package/dist/consolidation/scheduler/runConsolidate.js.map +1 -0
- package/dist/consolidation/scheduler/setupSchedule.d.ts +3 -0
- package/dist/consolidation/scheduler/setupSchedule.d.ts.map +1 -0
- package/dist/consolidation/scheduler/setupSchedule.js +79 -0
- package/dist/consolidation/scheduler/setupSchedule.js.map +1 -0
- package/dist/consolidation/scheduler/systemd.d.ts +9 -0
- package/dist/consolidation/scheduler/systemd.d.ts.map +1 -0
- package/dist/consolidation/scheduler/systemd.js +57 -0
- package/dist/consolidation/scheduler/systemd.js.map +1 -0
- package/dist/consolidation/scheduler/types.d.ts +24 -0
- package/dist/consolidation/scheduler/types.d.ts.map +1 -0
- package/dist/consolidation/scheduler/types.js +2 -0
- package/dist/consolidation/scheduler/types.js.map +1 -0
- package/dist/consolidation/scope.d.ts +12 -0
- package/dist/consolidation/scope.d.ts.map +1 -0
- package/dist/consolidation/scope.js +51 -0
- package/dist/consolidation/scope.js.map +1 -0
- package/dist/consolidation/stage1/deltaExtract.d.ts +3 -0
- package/dist/consolidation/stage1/deltaExtract.d.ts.map +1 -0
- package/dist/consolidation/stage1/deltaExtract.js +19 -0
- package/dist/consolidation/stage1/deltaExtract.js.map +1 -0
- package/dist/consolidation/stage1/drainQueue.d.ts +21 -0
- package/dist/consolidation/stage1/drainQueue.d.ts.map +1 -0
- package/dist/consolidation/stage1/drainQueue.js +113 -0
- package/dist/consolidation/stage1/drainQueue.js.map +1 -0
- package/dist/consolidation/stage1/extractSession.d.ts +6 -0
- package/dist/consolidation/stage1/extractSession.d.ts.map +1 -0
- package/dist/consolidation/stage1/extractSession.js +47 -0
- package/dist/consolidation/stage1/extractSession.js.map +1 -0
- package/dist/consolidation/stage1/store.d.ts +45 -0
- package/dist/consolidation/stage1/store.d.ts.map +1 -0
- package/dist/consolidation/stage1/store.js +297 -0
- package/dist/consolidation/stage1/store.js.map +1 -0
- package/dist/consolidation/types.d.ts +49 -0
- package/dist/consolidation/types.d.ts.map +1 -0
- package/dist/consolidation/types.js +2 -0
- package/dist/consolidation/types.js.map +1 -0
- package/dist/fallback/llmRerank.d.ts.map +1 -1
- package/dist/fallback/llmRerank.js +9 -1
- package/dist/fallback/llmRerank.js.map +1 -1
- package/dist/fallback/sessionIndex.d.ts.map +1 -1
- package/dist/fallback/sessionIndex.js +15 -63
- package/dist/fallback/sessionIndex.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/local/graphQuery.d.ts +10 -0
- package/dist/local/graphQuery.d.ts.map +1 -1
- package/dist/local/graphQuery.js +25 -0
- package/dist/local/graphQuery.js.map +1 -1
- package/dist/pi-extension.d.ts.map +1 -1
- package/dist/pi-extension.js +356 -22
- package/dist/pi-extension.js.map +1 -1
- package/dist/preflight/detectIntents.d.ts.map +1 -1
- package/dist/preflight/detectIntents.js +7 -0
- package/dist/preflight/detectIntents.js.map +1 -1
- package/dist/preflight/hook.d.ts +18 -0
- package/dist/preflight/hook.d.ts.map +1 -1
- package/dist/preflight/hook.js +52 -7
- package/dist/preflight/hook.js.map +1 -1
- package/dist/preflight/render.d.ts +16 -2
- package/dist/preflight/render.d.ts.map +1 -1
- package/dist/preflight/render.js +24 -4
- package/dist/preflight/render.js.map +1 -1
- package/dist/service.d.ts +16 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +57 -0
- package/dist/service.js.map +1 -1
- package/dist/session/activeBranch.d.ts +17 -0
- package/dist/session/activeBranch.d.ts.map +1 -0
- package/dist/session/activeBranch.js +251 -0
- package/dist/session/activeBranch.js.map +1 -0
- package/dist/settings.d.ts +5 -2
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +18 -1
- package/dist/settings.js.map +1 -1
- package/dist/tools/memoryAppend.d.ts +14 -4
- package/dist/tools/memoryAppend.d.ts.map +1 -1
- package/dist/tools/memoryAppend.js +46 -7
- package/dist/tools/memoryAppend.js.map +1 -1
- package/dist/tools/memoryRecall.d.ts +2 -2
- package/dist/tools/memoryRecall.d.ts.map +1 -1
- package/dist/tools/memoryRecall.js +35 -9
- package/dist/tools/memoryRecall.js.map +1 -1
- package/dist/trainer/extractFacts.d.ts +1 -1
- package/dist/trainer/index.d.ts +1 -1
- package/dist/trainer/index.d.ts.map +1 -1
- package/dist/trainer/index.js +1 -1
- package/dist/trainer/index.js.map +1 -1
- package/dist/trainer/scheduler.d.ts +2 -0
- package/dist/trainer/scheduler.d.ts.map +1 -1
- package/dist/trainer/scheduler.js +1 -0
- package/dist/trainer/scheduler.js.map +1 -1
- package/dist/trainer/sessionLoader.d.ts +3 -0
- package/dist/trainer/sessionLoader.d.ts.map +1 -1
- package/dist/trainer/sessionLoader.js +37 -74
- package/dist/trainer/sessionLoader.js.map +1 -1
- package/package.json +14 -4
- package/src/cli.ts +144 -0
- package/src/config.ts +89 -1
- package/src/consolidation/enqueue.ts +205 -0
- package/src/consolidation/index.ts +90 -0
- package/src/consolidation/lock.ts +41 -0
- package/src/consolidation/log.ts +43 -0
- package/src/consolidation/memoryIndex.ts +127 -0
- package/src/consolidation/phase2/runPhase2.ts +255 -0
- package/src/consolidation/scheduler/launchd.ts +62 -0
- package/src/consolidation/scheduler/runConsolidate.ts +115 -0
- package/src/consolidation/scheduler/setupSchedule.ts +106 -0
- package/src/consolidation/scheduler/systemd.ts +78 -0
- package/src/consolidation/scheduler/types.ts +27 -0
- package/src/consolidation/scope.ts +58 -0
- package/src/consolidation/stage1/deltaExtract.ts +29 -0
- package/src/consolidation/stage1/drainQueue.ts +143 -0
- package/src/consolidation/stage1/extractSession.ts +58 -0
- package/src/consolidation/stage1/store.ts +411 -0
- package/src/consolidation/types.ts +52 -0
- package/src/fallback/sessionIndex.ts +14 -51
- package/src/index.ts +6 -0
- package/src/pi-extension.ts +309 -10
- package/src/preflight/hook.ts +16 -2
- package/src/preflight/render.ts +7 -0
- package/src/session/activeBranch.ts +294 -0
- package/src/settings.ts +37 -2
- package/src/tools/memoryAppend.ts +60 -7
- package/src/trainer/index.ts +1 -1
- package/src/trainer/sessionLoader.ts +38 -87
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { SessionTurn } from "../../trainer/sessionLoader.js";
|
|
4
|
+
|
|
5
|
+
function turnHash(turn: SessionTurn): string {
|
|
6
|
+
return createHash("sha256")
|
|
7
|
+
.update(`${turn.role}\0${turn.content}`)
|
|
8
|
+
.digest("hex");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function computeDeltaTurns(
|
|
12
|
+
parentTurns: SessionTurn[],
|
|
13
|
+
childTurns: SessionTurn[],
|
|
14
|
+
): SessionTurn[] {
|
|
15
|
+
let forkIndex = 0;
|
|
16
|
+
for (
|
|
17
|
+
;
|
|
18
|
+
forkIndex < parentTurns.length && forkIndex < childTurns.length;
|
|
19
|
+
forkIndex++
|
|
20
|
+
) {
|
|
21
|
+
if (turnHash(parentTurns[forkIndex]!) !== turnHash(childTurns[forkIndex]!)) {
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return childTurns.slice(forkIndex).map((turn, idx) => ({
|
|
26
|
+
...turn,
|
|
27
|
+
turnIndex: idx,
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { ConsolidationStore } from "./store.js";
|
|
2
|
+
import { computeDeltaTurns } from "./deltaExtract.js";
|
|
3
|
+
import { extractSessionToStage1 } from "./extractSession.js";
|
|
4
|
+
import type { PendingSession } from "../types.js";
|
|
5
|
+
import { loadSessionFile, type SessionTurn } from "../../trainer/sessionLoader.js";
|
|
6
|
+
|
|
7
|
+
export interface DrainQueueOptions {
|
|
8
|
+
limit?: number;
|
|
9
|
+
dryRun?: boolean;
|
|
10
|
+
now?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface DrainQueueReport {
|
|
14
|
+
sessionsProcessed: number;
|
|
15
|
+
sessionsSkipped: number;
|
|
16
|
+
sessionsFailed: number;
|
|
17
|
+
stage1NewRows: number;
|
|
18
|
+
dryRun: boolean;
|
|
19
|
+
details: Array<{
|
|
20
|
+
session_id: string;
|
|
21
|
+
status: "processed" | "skipped" | "failed";
|
|
22
|
+
reason?: string;
|
|
23
|
+
delta_turns?: number;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function topoSortPending(sessions: PendingSession[]): PendingSession[] {
|
|
28
|
+
const byId = new Map(sessions.map((session) => [session.session_id, session]));
|
|
29
|
+
const visited = new Set<string>();
|
|
30
|
+
const visiting = new Set<string>();
|
|
31
|
+
const out: PendingSession[] = [];
|
|
32
|
+
|
|
33
|
+
function visit(session: PendingSession): void {
|
|
34
|
+
if (visited.has(session.session_id)) return;
|
|
35
|
+
if (visiting.has(session.session_id)) {
|
|
36
|
+
out.push(session);
|
|
37
|
+
visited.add(session.session_id);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
visiting.add(session.session_id);
|
|
41
|
+
const parent = session.parent_session_id
|
|
42
|
+
? byId.get(session.parent_session_id)
|
|
43
|
+
: undefined;
|
|
44
|
+
if (parent) visit(parent);
|
|
45
|
+
visiting.delete(session.session_id);
|
|
46
|
+
visited.add(session.session_id);
|
|
47
|
+
out.push(session);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const session of sessions) visit(session);
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function loadTurns(filePath: string): Promise<SessionTurn[] | null> {
|
|
55
|
+
const session = await loadSessionFile(filePath);
|
|
56
|
+
return session?.turns ?? null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function turnsForExtraction(
|
|
60
|
+
pending: PendingSession,
|
|
61
|
+
): Promise<{ turns: SessionTurn[]; delta: boolean } | null> {
|
|
62
|
+
const childTurns = await loadTurns(pending.session_file);
|
|
63
|
+
if (!childTurns) return null;
|
|
64
|
+
if (!pending.parent_session_file) {
|
|
65
|
+
return { turns: childTurns, delta: false };
|
|
66
|
+
}
|
|
67
|
+
const parentTurns = await loadTurns(pending.parent_session_file).catch(() => null);
|
|
68
|
+
if (!parentTurns) return { turns: childTurns, delta: false };
|
|
69
|
+
return { turns: computeDeltaTurns(parentTurns, childTurns), delta: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function drainQueue(
|
|
73
|
+
store: ConsolidationStore,
|
|
74
|
+
opts: DrainQueueOptions = {},
|
|
75
|
+
): Promise<DrainQueueReport> {
|
|
76
|
+
const dryRun = opts.dryRun ?? false;
|
|
77
|
+
const pending = topoSortPending(store.listPendingSessions(opts.limit ?? 100));
|
|
78
|
+
const report: DrainQueueReport = {
|
|
79
|
+
sessionsProcessed: 0,
|
|
80
|
+
sessionsSkipped: 0,
|
|
81
|
+
sessionsFailed: 0,
|
|
82
|
+
stage1NewRows: 0,
|
|
83
|
+
dryRun,
|
|
84
|
+
details: [],
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
for (const session of pending) {
|
|
88
|
+
try {
|
|
89
|
+
const loaded = await turnsForExtraction(session);
|
|
90
|
+
if (!loaded) {
|
|
91
|
+
report.sessionsSkipped++;
|
|
92
|
+
report.details.push({
|
|
93
|
+
session_id: session.session_id,
|
|
94
|
+
status: "skipped",
|
|
95
|
+
reason: "session_unreadable",
|
|
96
|
+
});
|
|
97
|
+
if (!dryRun) store.setPendingSessionStatus(session.session_id, "skipped");
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (loaded.delta && loaded.turns.length === 0) {
|
|
102
|
+
report.sessionsSkipped++;
|
|
103
|
+
report.details.push({
|
|
104
|
+
session_id: session.session_id,
|
|
105
|
+
status: "skipped",
|
|
106
|
+
reason: "clone_no_delta",
|
|
107
|
+
delta_turns: 0,
|
|
108
|
+
});
|
|
109
|
+
if (!dryRun) store.setPendingSessionStatus(session.session_id, "skipped");
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const output = await extractSessionToStage1(session, loaded.turns, {
|
|
114
|
+
now: opts.now,
|
|
115
|
+
});
|
|
116
|
+
report.sessionsProcessed++;
|
|
117
|
+
report.stage1NewRows++;
|
|
118
|
+
report.details.push({
|
|
119
|
+
session_id: session.session_id,
|
|
120
|
+
status: "processed",
|
|
121
|
+
delta_turns: loaded.turns.length,
|
|
122
|
+
});
|
|
123
|
+
if (!dryRun) {
|
|
124
|
+
store.upsertStage1Output(output);
|
|
125
|
+
store.setPendingSessionStatus(
|
|
126
|
+
session.session_id,
|
|
127
|
+
output.status === "skipped" ? "skipped" : "done",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
} catch (err) {
|
|
131
|
+
report.sessionsFailed++;
|
|
132
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
133
|
+
report.details.push({
|
|
134
|
+
session_id: session.session_id,
|
|
135
|
+
status: "failed",
|
|
136
|
+
reason: message,
|
|
137
|
+
});
|
|
138
|
+
if (!dryRun) store.setPendingSessionStatus(session.session_id, "failed", message);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return report;
|
|
143
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
import { projectScope } from "../scope.js";
|
|
4
|
+
import type { PendingSession, Stage1Output } from "../types.js";
|
|
5
|
+
import type { SessionTurn } from "../../trainer/sessionLoader.js";
|
|
6
|
+
|
|
7
|
+
const OPERATING_SIGNAL_RE =
|
|
8
|
+
/\b(prefer|preference|always|default|remember|convention|build command|test command)\b|(?:偏好|默认|以后|记住|约定|构建|测试命令)/i;
|
|
9
|
+
|
|
10
|
+
function cleanLine(text: string): string {
|
|
11
|
+
return text.replace(/\s+/g, " ").trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function summarizeTurns(turns: SessionTurn[], max = 4): string {
|
|
15
|
+
return turns
|
|
16
|
+
.filter((turn) => turn.role === "user" || turn.role === "assistant")
|
|
17
|
+
.slice(0, max)
|
|
18
|
+
.map((turn) => `- ${turn.role}: ${cleanLine(turn.content).slice(0, 240)}`)
|
|
19
|
+
.join("\n");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function extractOperatingLines(turns: SessionTurn[]): string {
|
|
23
|
+
const lines: string[] = [];
|
|
24
|
+
for (const turn of turns) {
|
|
25
|
+
if (turn.role !== "user") continue;
|
|
26
|
+
const text = cleanLine(turn.content);
|
|
27
|
+
if (!OPERATING_SIGNAL_RE.test(text)) continue;
|
|
28
|
+
lines.push(`- ${text}`);
|
|
29
|
+
}
|
|
30
|
+
return lines.join("\n");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function extractSessionToStage1(
|
|
34
|
+
pending: PendingSession,
|
|
35
|
+
turns: SessionTurn[],
|
|
36
|
+
opts: { now?: string } = {},
|
|
37
|
+
): Promise<Stage1Output> {
|
|
38
|
+
const generatedAt = opts.now ?? new Date().toISOString();
|
|
39
|
+
const stat = await fs.stat(pending.session_file);
|
|
40
|
+
const rawMemory = extractOperatingLines(turns);
|
|
41
|
+
const rolloutSummary = summarizeTurns(turns);
|
|
42
|
+
const hasOutput = rawMemory.trim() || rolloutSummary.trim();
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
session_id: pending.session_id,
|
|
46
|
+
session_file: pending.session_file,
|
|
47
|
+
source_mtime_ms: Math.trunc(stat.mtimeMs),
|
|
48
|
+
generated_at: generatedAt,
|
|
49
|
+
raw_memory: rawMemory.trim(),
|
|
50
|
+
rollout_summary: rolloutSummary.trim(),
|
|
51
|
+
scope: projectScope(pending.project_hash),
|
|
52
|
+
status: hasOutput ? "done" : "skipped",
|
|
53
|
+
selected_for_phase2: false,
|
|
54
|
+
usage_count: 0,
|
|
55
|
+
last_usage: generatedAt,
|
|
56
|
+
error_message: null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
PendingSession,
|
|
5
|
+
Stage1Output,
|
|
6
|
+
ConsolidationStatus,
|
|
7
|
+
} from "../types.js";
|
|
8
|
+
|
|
9
|
+
export interface SqliteDatabase {
|
|
10
|
+
pragma(sql: string): unknown;
|
|
11
|
+
exec(sql: string): void;
|
|
12
|
+
prepare(sql: string): {
|
|
13
|
+
run(...args: unknown[]): unknown;
|
|
14
|
+
get(...args: unknown[]): unknown;
|
|
15
|
+
all(...args: unknown[]): unknown[];
|
|
16
|
+
};
|
|
17
|
+
transaction<T>(fn: () => T): () => T;
|
|
18
|
+
close(): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type SqliteConstructor = new (path: string) => SqliteDatabase;
|
|
22
|
+
|
|
23
|
+
function loadSqlite(): SqliteConstructor | null {
|
|
24
|
+
try {
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
const mod = require("better-sqlite3") as SqliteConstructor;
|
|
27
|
+
return mod;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const SCHEMA_VERSION = 1;
|
|
34
|
+
|
|
35
|
+
const SCHEMA_SQL = `
|
|
36
|
+
CREATE TABLE IF NOT EXISTS pending_sessions (
|
|
37
|
+
session_id TEXT PRIMARY KEY,
|
|
38
|
+
session_file TEXT NOT NULL,
|
|
39
|
+
cwd TEXT NOT NULL,
|
|
40
|
+
git_root TEXT,
|
|
41
|
+
project_hash TEXT,
|
|
42
|
+
parent_session_id TEXT,
|
|
43
|
+
parent_session_file TEXT,
|
|
44
|
+
user_turn_count INTEGER NOT NULL,
|
|
45
|
+
ended_at TEXT NOT NULL,
|
|
46
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'processing', 'done', 'skipped', 'failed')),
|
|
47
|
+
error_message TEXT,
|
|
48
|
+
created_at TEXT NOT NULL,
|
|
49
|
+
updated_at TEXT NOT NULL
|
|
50
|
+
);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_pending_sessions_status ON pending_sessions(status);
|
|
52
|
+
CREATE TABLE IF NOT EXISTS stage1_outputs (
|
|
53
|
+
session_id TEXT PRIMARY KEY,
|
|
54
|
+
session_file TEXT NOT NULL,
|
|
55
|
+
source_mtime_ms INTEGER NOT NULL,
|
|
56
|
+
generated_at TEXT NOT NULL,
|
|
57
|
+
raw_memory TEXT NOT NULL,
|
|
58
|
+
rollout_summary TEXT NOT NULL,
|
|
59
|
+
scope TEXT NOT NULL,
|
|
60
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'processing', 'done', 'skipped', 'failed')),
|
|
61
|
+
selected_for_phase2 INTEGER NOT NULL DEFAULT 0,
|
|
62
|
+
usage_count INTEGER NOT NULL DEFAULT 0,
|
|
63
|
+
last_usage TEXT,
|
|
64
|
+
error_message TEXT
|
|
65
|
+
);
|
|
66
|
+
CREATE INDEX IF NOT EXISTS idx_stage1_outputs_status ON stage1_outputs(status);
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
const UPSERT_PENDING_SQL = `
|
|
70
|
+
INSERT INTO pending_sessions (
|
|
71
|
+
session_id,
|
|
72
|
+
session_file,
|
|
73
|
+
cwd,
|
|
74
|
+
git_root,
|
|
75
|
+
project_hash,
|
|
76
|
+
parent_session_id,
|
|
77
|
+
parent_session_file,
|
|
78
|
+
user_turn_count,
|
|
79
|
+
ended_at,
|
|
80
|
+
status,
|
|
81
|
+
error_message,
|
|
82
|
+
created_at,
|
|
83
|
+
updated_at
|
|
84
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
85
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
86
|
+
session_file = excluded.session_file,
|
|
87
|
+
cwd = excluded.cwd,
|
|
88
|
+
git_root = excluded.git_root,
|
|
89
|
+
project_hash = excluded.project_hash,
|
|
90
|
+
parent_session_id = excluded.parent_session_id,
|
|
91
|
+
parent_session_file = excluded.parent_session_file,
|
|
92
|
+
user_turn_count = excluded.user_turn_count,
|
|
93
|
+
ended_at = excluded.ended_at,
|
|
94
|
+
status = excluded.status,
|
|
95
|
+
error_message = excluded.error_message,
|
|
96
|
+
updated_at = excluded.updated_at;
|
|
97
|
+
`;
|
|
98
|
+
|
|
99
|
+
const UPSERT_STAGE1_SQL = `
|
|
100
|
+
INSERT INTO stage1_outputs (
|
|
101
|
+
session_id,
|
|
102
|
+
session_file,
|
|
103
|
+
source_mtime_ms,
|
|
104
|
+
generated_at,
|
|
105
|
+
raw_memory,
|
|
106
|
+
rollout_summary,
|
|
107
|
+
scope,
|
|
108
|
+
status,
|
|
109
|
+
selected_for_phase2,
|
|
110
|
+
usage_count,
|
|
111
|
+
last_usage,
|
|
112
|
+
error_message
|
|
113
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
114
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
115
|
+
session_file = excluded.session_file,
|
|
116
|
+
source_mtime_ms = excluded.source_mtime_ms,
|
|
117
|
+
generated_at = excluded.generated_at,
|
|
118
|
+
raw_memory = excluded.raw_memory,
|
|
119
|
+
rollout_summary = excluded.rollout_summary,
|
|
120
|
+
scope = excluded.scope,
|
|
121
|
+
status = excluded.status,
|
|
122
|
+
selected_for_phase2 = excluded.selected_for_phase2,
|
|
123
|
+
usage_count = excluded.usage_count,
|
|
124
|
+
last_usage = excluded.last_usage,
|
|
125
|
+
error_message = excluded.error_message;
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
export interface ConsolidationStore {
|
|
129
|
+
upsertPendingSession(session: Omit<PendingSession, "created_at" | "updated_at"> & {
|
|
130
|
+
created_at: string;
|
|
131
|
+
updated_at: string;
|
|
132
|
+
}): PendingSession;
|
|
133
|
+
upsertStage1Output(output: Stage1Output): void;
|
|
134
|
+
getPendingSession(sessionId: string): PendingSession | null;
|
|
135
|
+
listPendingSessions(limit?: number): PendingSession[];
|
|
136
|
+
setPendingSessionStatus(
|
|
137
|
+
sessionId: string,
|
|
138
|
+
status: ConsolidationStatus,
|
|
139
|
+
errorMessage?: string | null,
|
|
140
|
+
updatedAt?: string,
|
|
141
|
+
): void;
|
|
142
|
+
getStage1Output(sessionId: string): Stage1Output | null;
|
|
143
|
+
listUnselectedStage1(limit: number): Stage1Output[];
|
|
144
|
+
listStage1OlderThan(cutoffIso: string, limit?: number): Stage1Output[];
|
|
145
|
+
markStage1Selected(sessionIds: string[]): void;
|
|
146
|
+
incrementStage1Usage(sessionId: string, usedAt?: string): void;
|
|
147
|
+
countPendingSessions(): number;
|
|
148
|
+
getConsolidationStatusCounts(): {
|
|
149
|
+
pending: number;
|
|
150
|
+
processing: number;
|
|
151
|
+
done: number;
|
|
152
|
+
skipped: number;
|
|
153
|
+
failed: number;
|
|
154
|
+
};
|
|
155
|
+
getStage1Count(): number;
|
|
156
|
+
getLastGeneratedAt(): string | null;
|
|
157
|
+
getLastUpdatedAt(): string | null;
|
|
158
|
+
close(): void;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function toIsoTimestamp(value: Date | string | undefined = new Date()): string {
|
|
162
|
+
const dt = typeof value === "string" ? new Date(value) : value;
|
|
163
|
+
return dt.toISOString();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getUserVersion(db: SqliteDatabase): number {
|
|
167
|
+
const raw = db.pragma("user_version");
|
|
168
|
+
if (Array.isArray(raw) && raw[0] && typeof raw[0] === "object" && "user_version" in raw[0]) {
|
|
169
|
+
return Number((raw[0] as { user_version: number }).user_version);
|
|
170
|
+
}
|
|
171
|
+
if (typeof raw === "number") return raw;
|
|
172
|
+
return 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function setUserVersion(db: SqliteDatabase, version: number): void {
|
|
176
|
+
db.pragma(`user_version = ${version}`);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function normalizeSessionRow(row: Record<string, unknown> | undefined | null): PendingSession | null {
|
|
180
|
+
if (!row) return null;
|
|
181
|
+
return {
|
|
182
|
+
session_id: String(row.session_id ?? ""),
|
|
183
|
+
session_file: String(row.session_file ?? ""),
|
|
184
|
+
cwd: String(row.cwd ?? ""),
|
|
185
|
+
git_root: row.git_root == null ? null : String(row.git_root),
|
|
186
|
+
project_hash: row.project_hash == null ? null : String(row.project_hash),
|
|
187
|
+
parent_session_id: row.parent_session_id == null ? null : String(row.parent_session_id),
|
|
188
|
+
parent_session_file: row.parent_session_file == null ? null : String(row.parent_session_file),
|
|
189
|
+
user_turn_count: Number(row.user_turn_count ?? 0),
|
|
190
|
+
ended_at: String(row.ended_at ?? ""),
|
|
191
|
+
status: String(row.status ?? "pending") as ConsolidationStatus,
|
|
192
|
+
error_message: row.error_message == null ? null : String(row.error_message),
|
|
193
|
+
created_at: String(row.created_at ?? ""),
|
|
194
|
+
updated_at: String(row.updated_at ?? ""),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function normalizeStage1Row(row: Record<string, unknown> | undefined | null): Stage1Output | null {
|
|
199
|
+
if (!row) return null;
|
|
200
|
+
return {
|
|
201
|
+
session_id: String(row.session_id ?? ""),
|
|
202
|
+
session_file: String(row.session_file ?? ""),
|
|
203
|
+
source_mtime_ms: toNumber(row.source_mtime_ms, 0),
|
|
204
|
+
generated_at: String(row.generated_at ?? ""),
|
|
205
|
+
raw_memory: String(row.raw_memory ?? ""),
|
|
206
|
+
rollout_summary: String(row.rollout_summary ?? ""),
|
|
207
|
+
scope: String(row.scope ?? "global"),
|
|
208
|
+
status: String(row.status ?? "pending") as ConsolidationStatus,
|
|
209
|
+
selected_for_phase2: Number(row.selected_for_phase2 ?? 0) === 1,
|
|
210
|
+
usage_count: toNumber(row.usage_count, 0),
|
|
211
|
+
last_usage: row.last_usage == null ? null : String(row.last_usage),
|
|
212
|
+
error_message: row.error_message == null ? null : String(row.error_message),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function toBoolInt(value: boolean): number {
|
|
217
|
+
return value ? 1 : 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function toNumber(value: unknown, fallback = 0): number {
|
|
221
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
222
|
+
if (typeof value === "string") {
|
|
223
|
+
const parsed = Number(value);
|
|
224
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
225
|
+
}
|
|
226
|
+
return fallback;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Lazy-open a sqlite database and ensure the consolidation schema exists.
|
|
231
|
+
* Returns null when better-sqlite3 is unavailable and no injected DB is provided.
|
|
232
|
+
*/
|
|
233
|
+
export function openConsolidationStore(
|
|
234
|
+
dbPath: string,
|
|
235
|
+
injectedDb?: SqliteDatabase,
|
|
236
|
+
): ConsolidationStore | null {
|
|
237
|
+
let db: SqliteDatabase;
|
|
238
|
+
if (injectedDb) {
|
|
239
|
+
db = injectedDb;
|
|
240
|
+
} else {
|
|
241
|
+
const Sqlite = loadSqlite();
|
|
242
|
+
if (!Sqlite) return null;
|
|
243
|
+
db = new Sqlite(dbPath);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
db.pragma("journal_mode = WAL");
|
|
247
|
+
const current = getUserVersion(db);
|
|
248
|
+
if (current !== SCHEMA_VERSION) {
|
|
249
|
+
db.exec(SCHEMA_SQL);
|
|
250
|
+
setUserVersion(db, SCHEMA_VERSION);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const upsertPendingStmt = db.prepare(UPSERT_PENDING_SQL);
|
|
254
|
+
const upsertStage1Stmt = db.prepare(UPSERT_STAGE1_SQL);
|
|
255
|
+
const selectPendingByIdStmt = db.prepare("SELECT * FROM pending_sessions WHERE session_id = ?");
|
|
256
|
+
const listPendingStmt = db.prepare(
|
|
257
|
+
"SELECT * FROM pending_sessions WHERE status IN ('pending', 'failed') ORDER BY ended_at ASC LIMIT ?",
|
|
258
|
+
);
|
|
259
|
+
const updatePendingStatusStmt = db.prepare(
|
|
260
|
+
"UPDATE pending_sessions SET status = ?, error_message = ?, updated_at = ? WHERE session_id = ?",
|
|
261
|
+
);
|
|
262
|
+
const selectStage1ByIdStmt = db.prepare("SELECT * FROM stage1_outputs WHERE session_id = ?");
|
|
263
|
+
const listUnselectedStage1Stmt = db.prepare(
|
|
264
|
+
"SELECT * FROM stage1_outputs WHERE selected_for_phase2 = 0 AND status = 'done' ORDER BY generated_at DESC LIMIT ?",
|
|
265
|
+
);
|
|
266
|
+
const listStage1OlderThanStmt = db.prepare(
|
|
267
|
+
"SELECT * FROM stage1_outputs WHERE last_usage IS NOT NULL AND last_usage < ? ORDER BY last_usage ASC LIMIT ?",
|
|
268
|
+
);
|
|
269
|
+
const markStage1SelectedStmt = db.prepare(
|
|
270
|
+
"UPDATE stage1_outputs SET selected_for_phase2 = 1 WHERE session_id = ?",
|
|
271
|
+
);
|
|
272
|
+
const incrementStage1UsageStmt = db.prepare(
|
|
273
|
+
"UPDATE stage1_outputs SET usage_count = usage_count + 1, last_usage = ? WHERE session_id = ?",
|
|
274
|
+
);
|
|
275
|
+
const countPendingAllStmt = db.prepare("SELECT COUNT(*) AS count FROM pending_sessions");
|
|
276
|
+
const countPendingByStatusStmt = db.prepare(
|
|
277
|
+
"SELECT status, COUNT(*) AS count FROM pending_sessions GROUP BY status",
|
|
278
|
+
);
|
|
279
|
+
const countStage1Stmt = db.prepare("SELECT COUNT(*) AS count FROM stage1_outputs");
|
|
280
|
+
const lastGeneratedStmt = db.prepare("SELECT MAX(generated_at) AS generated_at FROM stage1_outputs");
|
|
281
|
+
const lastUpdatedStmt = db.prepare("SELECT MAX(updated_at) AS updated_at FROM pending_sessions");
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
upsertPendingSession(session): PendingSession {
|
|
285
|
+
const now = toIsoTimestamp(session.updated_at);
|
|
286
|
+
const payload = {
|
|
287
|
+
...session,
|
|
288
|
+
created_at: session.created_at ?? now,
|
|
289
|
+
updated_at: now,
|
|
290
|
+
};
|
|
291
|
+
upsertPendingStmt.run(
|
|
292
|
+
payload.session_id,
|
|
293
|
+
payload.session_file,
|
|
294
|
+
payload.cwd,
|
|
295
|
+
payload.git_root,
|
|
296
|
+
payload.project_hash,
|
|
297
|
+
payload.parent_session_id,
|
|
298
|
+
payload.parent_session_file,
|
|
299
|
+
payload.user_turn_count,
|
|
300
|
+
payload.ended_at,
|
|
301
|
+
payload.status,
|
|
302
|
+
payload.error_message,
|
|
303
|
+
payload.created_at,
|
|
304
|
+
payload.updated_at,
|
|
305
|
+
);
|
|
306
|
+
return payload;
|
|
307
|
+
},
|
|
308
|
+
upsertStage1Output(output): void {
|
|
309
|
+
upsertStage1Stmt.run(
|
|
310
|
+
output.session_id,
|
|
311
|
+
output.session_file,
|
|
312
|
+
output.source_mtime_ms,
|
|
313
|
+
output.generated_at,
|
|
314
|
+
output.raw_memory,
|
|
315
|
+
output.rollout_summary,
|
|
316
|
+
output.scope,
|
|
317
|
+
output.status,
|
|
318
|
+
toBoolInt(output.selected_for_phase2),
|
|
319
|
+
output.usage_count,
|
|
320
|
+
output.last_usage,
|
|
321
|
+
output.error_message,
|
|
322
|
+
);
|
|
323
|
+
},
|
|
324
|
+
getPendingSession(sessionId: string): PendingSession | null {
|
|
325
|
+
return normalizeSessionRow(
|
|
326
|
+
selectPendingByIdStmt.get(sessionId) as Record<string, unknown> | undefined,
|
|
327
|
+
);
|
|
328
|
+
},
|
|
329
|
+
listPendingSessions(limit = 100): PendingSession[] {
|
|
330
|
+
const rows = listPendingStmt.all(limit) as Array<Record<string, unknown>>;
|
|
331
|
+
return rows
|
|
332
|
+
.map((row) => normalizeSessionRow(row))
|
|
333
|
+
.filter((row): row is PendingSession => row != null);
|
|
334
|
+
},
|
|
335
|
+
setPendingSessionStatus(
|
|
336
|
+
sessionId: string,
|
|
337
|
+
status: ConsolidationStatus,
|
|
338
|
+
errorMessage: string | null = null,
|
|
339
|
+
updatedAt = new Date().toISOString(),
|
|
340
|
+
): void {
|
|
341
|
+
updatePendingStatusStmt.run(status, errorMessage, updatedAt, sessionId);
|
|
342
|
+
},
|
|
343
|
+
getStage1Output(sessionId: string): Stage1Output | null {
|
|
344
|
+
return normalizeStage1Row(
|
|
345
|
+
selectStage1ByIdStmt.get(sessionId) as Record<string, unknown> | undefined,
|
|
346
|
+
);
|
|
347
|
+
},
|
|
348
|
+
listUnselectedStage1(limit: number): Stage1Output[] {
|
|
349
|
+
const rows = listUnselectedStage1Stmt.all(limit) as Array<Record<string, unknown>>;
|
|
350
|
+
return rows
|
|
351
|
+
.map((row) => normalizeStage1Row(row))
|
|
352
|
+
.filter((row): row is Stage1Output => row != null);
|
|
353
|
+
},
|
|
354
|
+
listStage1OlderThan(cutoffIso: string, limit = 100): Stage1Output[] {
|
|
355
|
+
const rows = listStage1OlderThanStmt.all(cutoffIso, limit) as Array<Record<string, unknown>>;
|
|
356
|
+
return rows
|
|
357
|
+
.map((row) => normalizeStage1Row(row))
|
|
358
|
+
.filter((row): row is Stage1Output => row != null);
|
|
359
|
+
},
|
|
360
|
+
markStage1Selected(sessionIds: string[]): void {
|
|
361
|
+
const tx = db.transaction(() => {
|
|
362
|
+
for (const sessionId of sessionIds) markStage1SelectedStmt.run(sessionId);
|
|
363
|
+
});
|
|
364
|
+
tx();
|
|
365
|
+
},
|
|
366
|
+
incrementStage1Usage(sessionId: string, usedAt = new Date().toISOString()): void {
|
|
367
|
+
incrementStage1UsageStmt.run(usedAt, sessionId);
|
|
368
|
+
},
|
|
369
|
+
countPendingSessions(): number {
|
|
370
|
+
const row = countPendingAllStmt.get() as { count?: number | string } | undefined;
|
|
371
|
+
return toNumber(row?.count, 0);
|
|
372
|
+
},
|
|
373
|
+
getConsolidationStatusCounts() {
|
|
374
|
+
const rows = countPendingByStatusStmt.all() as Array<{
|
|
375
|
+
status: ConsolidationStatus;
|
|
376
|
+
count: number | string;
|
|
377
|
+
}>;
|
|
378
|
+
const counts = {
|
|
379
|
+
pending: 0,
|
|
380
|
+
processing: 0,
|
|
381
|
+
done: 0,
|
|
382
|
+
skipped: 0,
|
|
383
|
+
failed: 0,
|
|
384
|
+
};
|
|
385
|
+
for (const row of rows) {
|
|
386
|
+
const status = row.status;
|
|
387
|
+
if (status in counts) {
|
|
388
|
+
counts[status] = toNumber(row.count, 0);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return counts;
|
|
392
|
+
},
|
|
393
|
+
getStage1Count(): number {
|
|
394
|
+
const row = countStage1Stmt.get() as { count?: number | string } | undefined;
|
|
395
|
+
return toNumber(row?.count, 0);
|
|
396
|
+
},
|
|
397
|
+
getLastGeneratedAt(): string | null {
|
|
398
|
+
const row = lastGeneratedStmt.get() as { generated_at?: string } | undefined;
|
|
399
|
+
if (!row?.generated_at) return null;
|
|
400
|
+
return String(row.generated_at);
|
|
401
|
+
},
|
|
402
|
+
getLastUpdatedAt(): string | null {
|
|
403
|
+
const row = lastUpdatedStmt.get() as { updated_at?: string } | undefined;
|
|
404
|
+
if (!row?.updated_at) return null;
|
|
405
|
+
return String(row.updated_at);
|
|
406
|
+
},
|
|
407
|
+
close(): void {
|
|
408
|
+
db.close();
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type ConsolidationStatus = "pending" | "processing" | "done" | "skipped" | "failed";
|
|
2
|
+
|
|
3
|
+
export interface PendingSession {
|
|
4
|
+
session_id: string;
|
|
5
|
+
session_file: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
git_root: string | null;
|
|
8
|
+
project_hash: string | null;
|
|
9
|
+
parent_session_id: string | null;
|
|
10
|
+
parent_session_file: string | null;
|
|
11
|
+
user_turn_count: number;
|
|
12
|
+
ended_at: string;
|
|
13
|
+
status: ConsolidationStatus;
|
|
14
|
+
error_message: string | null;
|
|
15
|
+
created_at: string;
|
|
16
|
+
updated_at: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Stage1Output {
|
|
20
|
+
session_id: string;
|
|
21
|
+
session_file: string;
|
|
22
|
+
source_mtime_ms: number;
|
|
23
|
+
generated_at: string;
|
|
24
|
+
raw_memory: string;
|
|
25
|
+
rollout_summary: string;
|
|
26
|
+
scope: string;
|
|
27
|
+
status: ConsolidationStatus;
|
|
28
|
+
selected_for_phase2: boolean;
|
|
29
|
+
usage_count: number;
|
|
30
|
+
last_usage: string | null;
|
|
31
|
+
error_message: string | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface JobReport {
|
|
35
|
+
session_id: string;
|
|
36
|
+
enqueued: boolean;
|
|
37
|
+
action: "created" | "updated" | "skipped";
|
|
38
|
+
status: ConsolidationStatus;
|
|
39
|
+
reason?: string;
|
|
40
|
+
now: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ConsolidationStatusReport {
|
|
44
|
+
pending: number;
|
|
45
|
+
processing: number;
|
|
46
|
+
done: number;
|
|
47
|
+
skipped: number;
|
|
48
|
+
failed: number;
|
|
49
|
+
stage1Count: number;
|
|
50
|
+
lastGeneratedAt: string | null;
|
|
51
|
+
lastUpdatedAt: string | null;
|
|
52
|
+
}
|