@chendpoc/pi-memory 0.1.11 → 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.
Files changed (177) hide show
  1. package/README.md +88 -16
  2. package/dist/cache/memoryCaches.d.ts +32 -0
  3. package/dist/cache/memoryCaches.d.ts.map +1 -0
  4. package/dist/cache/memoryCaches.js +59 -0
  5. package/dist/cache/memoryCaches.js.map +1 -0
  6. package/dist/cli.js +123 -0
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.d.ts +28 -0
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +37 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/consolidation/enqueue.d.ts +33 -0
  13. package/dist/consolidation/enqueue.d.ts.map +1 -0
  14. package/dist/consolidation/enqueue.js +145 -0
  15. package/dist/consolidation/enqueue.js.map +1 -0
  16. package/dist/consolidation/index.d.ts +19 -0
  17. package/dist/consolidation/index.d.ts.map +1 -0
  18. package/dist/consolidation/index.js +15 -0
  19. package/dist/consolidation/index.js.map +1 -0
  20. package/dist/consolidation/lock.d.ts +5 -0
  21. package/dist/consolidation/lock.d.ts.map +1 -0
  22. package/dist/consolidation/lock.js +33 -0
  23. package/dist/consolidation/lock.js.map +1 -0
  24. package/dist/consolidation/log.d.ts +8 -0
  25. package/dist/consolidation/log.d.ts.map +1 -0
  26. package/dist/consolidation/log.js +32 -0
  27. package/dist/consolidation/log.js.map +1 -0
  28. package/dist/consolidation/memoryIndex.d.ts +18 -0
  29. package/dist/consolidation/memoryIndex.d.ts.map +1 -0
  30. package/dist/consolidation/memoryIndex.js +100 -0
  31. package/dist/consolidation/memoryIndex.js.map +1 -0
  32. package/dist/consolidation/phase2/runPhase2.d.ts +23 -0
  33. package/dist/consolidation/phase2/runPhase2.d.ts.map +1 -0
  34. package/dist/consolidation/phase2/runPhase2.js +196 -0
  35. package/dist/consolidation/phase2/runPhase2.js.map +1 -0
  36. package/dist/consolidation/scheduler/launchd.d.ts +5 -0
  37. package/dist/consolidation/scheduler/launchd.d.ts.map +1 -0
  38. package/dist/consolidation/scheduler/launchd.js +54 -0
  39. package/dist/consolidation/scheduler/launchd.js.map +1 -0
  40. package/dist/consolidation/scheduler/runConsolidate.d.ts +26 -0
  41. package/dist/consolidation/scheduler/runConsolidate.d.ts.map +1 -0
  42. package/dist/consolidation/scheduler/runConsolidate.js +83 -0
  43. package/dist/consolidation/scheduler/runConsolidate.js.map +1 -0
  44. package/dist/consolidation/scheduler/setupSchedule.d.ts +3 -0
  45. package/dist/consolidation/scheduler/setupSchedule.d.ts.map +1 -0
  46. package/dist/consolidation/scheduler/setupSchedule.js +79 -0
  47. package/dist/consolidation/scheduler/setupSchedule.js.map +1 -0
  48. package/dist/consolidation/scheduler/systemd.d.ts +9 -0
  49. package/dist/consolidation/scheduler/systemd.d.ts.map +1 -0
  50. package/dist/consolidation/scheduler/systemd.js +57 -0
  51. package/dist/consolidation/scheduler/systemd.js.map +1 -0
  52. package/dist/consolidation/scheduler/types.d.ts +24 -0
  53. package/dist/consolidation/scheduler/types.d.ts.map +1 -0
  54. package/dist/consolidation/scheduler/types.js +2 -0
  55. package/dist/consolidation/scheduler/types.js.map +1 -0
  56. package/dist/consolidation/scope.d.ts +12 -0
  57. package/dist/consolidation/scope.d.ts.map +1 -0
  58. package/dist/consolidation/scope.js +51 -0
  59. package/dist/consolidation/scope.js.map +1 -0
  60. package/dist/consolidation/stage1/deltaExtract.d.ts +3 -0
  61. package/dist/consolidation/stage1/deltaExtract.d.ts.map +1 -0
  62. package/dist/consolidation/stage1/deltaExtract.js +19 -0
  63. package/dist/consolidation/stage1/deltaExtract.js.map +1 -0
  64. package/dist/consolidation/stage1/drainQueue.d.ts +21 -0
  65. package/dist/consolidation/stage1/drainQueue.d.ts.map +1 -0
  66. package/dist/consolidation/stage1/drainQueue.js +113 -0
  67. package/dist/consolidation/stage1/drainQueue.js.map +1 -0
  68. package/dist/consolidation/stage1/extractSession.d.ts +6 -0
  69. package/dist/consolidation/stage1/extractSession.d.ts.map +1 -0
  70. package/dist/consolidation/stage1/extractSession.js +47 -0
  71. package/dist/consolidation/stage1/extractSession.js.map +1 -0
  72. package/dist/consolidation/stage1/store.d.ts +45 -0
  73. package/dist/consolidation/stage1/store.d.ts.map +1 -0
  74. package/dist/consolidation/stage1/store.js +297 -0
  75. package/dist/consolidation/stage1/store.js.map +1 -0
  76. package/dist/consolidation/types.d.ts +49 -0
  77. package/dist/consolidation/types.d.ts.map +1 -0
  78. package/dist/consolidation/types.js +2 -0
  79. package/dist/consolidation/types.js.map +1 -0
  80. package/dist/fallback/llmRerank.d.ts.map +1 -1
  81. package/dist/fallback/llmRerank.js +9 -1
  82. package/dist/fallback/llmRerank.js.map +1 -1
  83. package/dist/fallback/sessionIndex.d.ts.map +1 -1
  84. package/dist/fallback/sessionIndex.js +15 -63
  85. package/dist/fallback/sessionIndex.js.map +1 -1
  86. package/dist/index.d.ts +4 -3
  87. package/dist/index.d.ts.map +1 -1
  88. package/dist/index.js +4 -3
  89. package/dist/index.js.map +1 -1
  90. package/dist/local/graphQuery.d.ts +10 -0
  91. package/dist/local/graphQuery.d.ts.map +1 -1
  92. package/dist/local/graphQuery.js +25 -0
  93. package/dist/local/graphQuery.js.map +1 -1
  94. package/dist/pi-extension.d.ts.map +1 -1
  95. package/dist/pi-extension.js +356 -22
  96. package/dist/pi-extension.js.map +1 -1
  97. package/dist/preflight/detectIntents.d.ts.map +1 -1
  98. package/dist/preflight/detectIntents.js +7 -0
  99. package/dist/preflight/detectIntents.js.map +1 -1
  100. package/dist/preflight/hook.d.ts +18 -0
  101. package/dist/preflight/hook.d.ts.map +1 -1
  102. package/dist/preflight/hook.js +52 -7
  103. package/dist/preflight/hook.js.map +1 -1
  104. package/dist/preflight/render.d.ts +16 -2
  105. package/dist/preflight/render.d.ts.map +1 -1
  106. package/dist/preflight/render.js +24 -4
  107. package/dist/preflight/render.js.map +1 -1
  108. package/dist/service.d.ts +16 -0
  109. package/dist/service.d.ts.map +1 -1
  110. package/dist/service.js +57 -0
  111. package/dist/service.js.map +1 -1
  112. package/dist/session/activeBranch.d.ts +17 -0
  113. package/dist/session/activeBranch.d.ts.map +1 -0
  114. package/dist/session/activeBranch.js +251 -0
  115. package/dist/session/activeBranch.js.map +1 -0
  116. package/dist/settings.d.ts +5 -2
  117. package/dist/settings.d.ts.map +1 -1
  118. package/dist/settings.js +18 -1
  119. package/dist/settings.js.map +1 -1
  120. package/dist/tools/memoryAppend.d.ts +14 -4
  121. package/dist/tools/memoryAppend.d.ts.map +1 -1
  122. package/dist/tools/memoryAppend.js +46 -7
  123. package/dist/tools/memoryAppend.js.map +1 -1
  124. package/dist/tools/memoryRecall.d.ts +2 -2
  125. package/dist/tools/memoryRecall.d.ts.map +1 -1
  126. package/dist/tools/memoryRecall.js +35 -9
  127. package/dist/tools/memoryRecall.js.map +1 -1
  128. package/dist/trainer/extractFacts.d.ts +1 -1
  129. package/dist/trainer/index.d.ts +1 -1
  130. package/dist/trainer/index.d.ts.map +1 -1
  131. package/dist/trainer/index.js +1 -1
  132. package/dist/trainer/index.js.map +1 -1
  133. package/dist/trainer/scheduler.d.ts +2 -0
  134. package/dist/trainer/scheduler.d.ts.map +1 -1
  135. package/dist/trainer/scheduler.js +1 -0
  136. package/dist/trainer/scheduler.js.map +1 -1
  137. package/dist/trainer/sessionLoader.d.ts +3 -0
  138. package/dist/trainer/sessionLoader.d.ts.map +1 -1
  139. package/dist/trainer/sessionLoader.js +37 -74
  140. package/dist/trainer/sessionLoader.js.map +1 -1
  141. package/package.json +17 -4
  142. package/src/cache/memoryCaches.ts +72 -0
  143. package/src/cli.ts +144 -0
  144. package/src/config.ts +89 -1
  145. package/src/consolidation/enqueue.ts +205 -0
  146. package/src/consolidation/index.ts +90 -0
  147. package/src/consolidation/lock.ts +41 -0
  148. package/src/consolidation/log.ts +43 -0
  149. package/src/consolidation/memoryIndex.ts +127 -0
  150. package/src/consolidation/phase2/runPhase2.ts +255 -0
  151. package/src/consolidation/scheduler/launchd.ts +62 -0
  152. package/src/consolidation/scheduler/runConsolidate.ts +115 -0
  153. package/src/consolidation/scheduler/setupSchedule.ts +106 -0
  154. package/src/consolidation/scheduler/systemd.ts +78 -0
  155. package/src/consolidation/scheduler/types.ts +27 -0
  156. package/src/consolidation/scope.ts +58 -0
  157. package/src/consolidation/stage1/deltaExtract.ts +29 -0
  158. package/src/consolidation/stage1/drainQueue.ts +143 -0
  159. package/src/consolidation/stage1/extractSession.ts +58 -0
  160. package/src/consolidation/stage1/store.ts +411 -0
  161. package/src/consolidation/types.ts +52 -0
  162. package/src/fallback/llmRerank.ts +8 -1
  163. package/src/fallback/sessionIndex.ts +14 -51
  164. package/src/index.ts +8 -0
  165. package/src/local/graphQuery.ts +24 -0
  166. package/src/pi-extension.ts +393 -28
  167. package/src/preflight/detectIntents.ts +6 -0
  168. package/src/preflight/hook.ts +84 -7
  169. package/src/preflight/render.ts +35 -3
  170. package/src/service.ts +57 -0
  171. package/src/session/activeBranch.ts +294 -0
  172. package/src/settings.ts +37 -2
  173. package/src/tools/memoryAppend.ts +60 -7
  174. package/src/tools/memoryRecall.ts +33 -9
  175. package/src/trainer/index.ts +1 -1
  176. package/src/trainer/scheduler.ts +3 -0
  177. package/src/trainer/sessionLoader.ts +38 -87
@@ -0,0 +1,78 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+
4
+ import type { ScheduleOptions } from "./types.js";
5
+
6
+ export const SYSTEMD_UNIT_NAME = "dev.pi.memory-consolidate";
7
+ export const SYSTEMD_SERVICE_FILE = `${SYSTEMD_UNIT_NAME}.service`;
8
+ export const SYSTEMD_TIMER_FILE = `${SYSTEMD_UNIT_NAME}.timer`;
9
+
10
+ function commandArgs(opts: ScheduleOptions): string[] {
11
+ if (opts.commandPath) {
12
+ return [opts.commandPath, "consolidate"];
13
+ }
14
+ if (opts.npxPath) {
15
+ return [opts.npxPath, "pi-memory", "consolidate"];
16
+ }
17
+ return ["pi-memory", "consolidate"];
18
+ }
19
+
20
+ function pad2(value: number, max: number): string {
21
+ return String(Math.max(0, Math.min(max, Math.trunc(value)))).padStart(2, "0");
22
+ }
23
+
24
+ function buildExecStart(opts: ScheduleOptions): string {
25
+ return commandArgs(opts).join(" ");
26
+ }
27
+
28
+ export function systemdServicePath(): string {
29
+ return path.join(
30
+ os.homedir(),
31
+ ".config",
32
+ "systemd",
33
+ "user",
34
+ SYSTEMD_SERVICE_FILE,
35
+ );
36
+ }
37
+
38
+ export function systemdTimerPath(): string {
39
+ return path.join(
40
+ os.homedir(),
41
+ ".config",
42
+ "systemd",
43
+ "user",
44
+ SYSTEMD_TIMER_FILE,
45
+ );
46
+ }
47
+
48
+ export function buildSystemdService(opts: ScheduleOptions): string {
49
+ return [
50
+ `[Unit]`,
51
+ `Description=Pi Memory Consolidation`,
52
+ ``,
53
+ `[Service]`,
54
+ `Type=oneshot`,
55
+ `ExecStart=${buildExecStart(opts)}`,
56
+ `StandardOutput=append:${opts.logPath}`,
57
+ `StandardError=append:${opts.logPath}`,
58
+ ``,
59
+ ].join("\n");
60
+ }
61
+
62
+ export function buildSystemdTimer(opts: ScheduleOptions): string {
63
+ const minute = pad2(opts.minute, 59);
64
+ const hour = pad2(opts.hour, 23);
65
+ return [
66
+ `[Unit]`,
67
+ `Description=Runs Pi memory consolidation on a daily schedule`,
68
+ ``,
69
+ `[Timer]`,
70
+ `Persistent=true`,
71
+ `OnCalendar=*-*-* ${hour}:${minute}:00`,
72
+ `Unit=${SYSTEMD_UNIT_NAME}.service`,
73
+ ``,
74
+ `[Install]`,
75
+ `WantedBy=timers.target`,
76
+ ``,
77
+ ].join("\n");
78
+ }
@@ -0,0 +1,27 @@
1
+ export type SchedulePlatform = "darwin" | "linux";
2
+
3
+ export interface ScheduleOptions {
4
+ hour: number;
5
+ minute: number;
6
+ logPath: string;
7
+ commandPath?: string;
8
+ npxPath?: string;
9
+ dryRun?: boolean;
10
+ remove?: boolean;
11
+ status?: boolean;
12
+ }
13
+
14
+ export type ScheduleAction = "write" | "remove" | "status";
15
+
16
+ export interface ScheduledFile {
17
+ path: string;
18
+ exists: boolean;
19
+ content?: string;
20
+ }
21
+
22
+ export interface SetupScheduleResult {
23
+ platform: SchedulePlatform;
24
+ action: ScheduleAction;
25
+ files: ScheduledFile[];
26
+ dryRun: boolean;
27
+ }
@@ -0,0 +1,58 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+
5
+ export type MemoryScope = "global" | `project:${string}`;
6
+
7
+ const SCOPE_RE = /<!--\s*scope:([^>\s]+)\s*-->/;
8
+
9
+ export function projectHash(input: string): string {
10
+ return createHash("sha256").update(input).digest("hex").slice(0, 12);
11
+ }
12
+
13
+ export function projectScope(projectHashValue: string | null | undefined): MemoryScope {
14
+ return projectHashValue ? `project:${projectHashValue}` : "global";
15
+ }
16
+
17
+ export function parseScopeComment(line: string): MemoryScope | null {
18
+ const raw = line.match(SCOPE_RE)?.[1]?.trim();
19
+ if (!raw) return null;
20
+ if (raw === "global") return "global";
21
+ if (/^project:[a-f0-9]{12}$/i.test(raw)) return raw as MemoryScope;
22
+ return null;
23
+ }
24
+
25
+ export function lineMatchesScope(
26
+ line: string,
27
+ allowedScopes: readonly MemoryScope[] | undefined,
28
+ ): boolean {
29
+ if (!allowedScopes || allowedScopes.length === 0) return true;
30
+ const scope = parseScopeComment(line);
31
+ if (!scope) return true;
32
+ return allowedScopes.includes(scope);
33
+ }
34
+
35
+ export function findGitRoot(startDir: string | null | undefined): string | null {
36
+ if (!startDir) return null;
37
+ let current = path.resolve(startDir);
38
+ for (;;) {
39
+ if (fs.existsSync(path.join(current, ".git"))) return current;
40
+ const parent = path.dirname(current);
41
+ if (parent === current) return null;
42
+ current = parent;
43
+ }
44
+ }
45
+
46
+ export function scopeForCwd(cwd: string | null | undefined): {
47
+ gitRoot: string | null;
48
+ projectHash: string | null;
49
+ scopes: MemoryScope[];
50
+ } {
51
+ const gitRoot = findGitRoot(cwd);
52
+ const hash = gitRoot ? projectHash(gitRoot) : null;
53
+ return {
54
+ gitRoot,
55
+ projectHash: hash,
56
+ scopes: hash ? ["global", `project:${hash}`] : ["global"],
57
+ };
58
+ }
@@ -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
+ }