@herbertgao/pi-extensions 2026.8.12 → 2026.8.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 (112) hide show
  1. package/README.md +8 -3
  2. package/THIRD_PARTY_NOTICES.md +4 -0
  3. package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +376 -0
  4. package/node_modules/@luxusai/pi-hindsight/README.md +92 -0
  5. package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +56 -0
  6. package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +169 -0
  7. package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +107 -0
  8. package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +89 -0
  9. package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +189 -0
  10. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
  11. package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
  12. package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +54 -0
  13. package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +75 -0
  14. package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +300 -0
  15. package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +158 -0
  16. package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +164 -0
  17. package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +139 -0
  18. package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +74 -0
  19. package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +261 -0
  20. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +151 -0
  21. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +18 -0
  22. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +46 -0
  23. package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +383 -0
  24. package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +240 -0
  25. package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +176 -0
  26. package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +178 -0
  27. package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +41 -0
  28. package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +386 -0
  29. package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +39 -0
  30. package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +34 -0
  31. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +140 -0
  32. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +61 -0
  33. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +925 -0
  34. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +247 -0
  35. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +547 -0
  36. package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +480 -0
  37. package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +182 -0
  38. package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +86 -0
  39. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +1010 -0
  40. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +175 -0
  41. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +425 -0
  42. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +210 -0
  43. package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +817 -0
  44. package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +25 -0
  45. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +319 -0
  46. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +196 -0
  47. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +189 -0
  48. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +65 -0
  49. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +260 -0
  50. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +244 -0
  51. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +59 -0
  52. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +82 -0
  53. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +45 -0
  54. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +331 -0
  55. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +354 -0
  56. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +66 -0
  57. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +132 -0
  58. package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +228 -0
  59. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +74 -0
  60. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +16 -0
  61. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +648 -0
  62. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +105 -0
  63. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +68 -0
  64. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +71 -0
  65. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +13 -0
  66. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +162 -0
  67. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +131 -0
  68. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +104 -0
  69. package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +37 -0
  70. package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +893 -0
  71. package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +35 -0
  72. package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +177 -0
  73. package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +7 -0
  74. package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +21 -0
  75. package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +112 -0
  76. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +111 -0
  77. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +214 -0
  78. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +70 -0
  79. package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +392 -0
  80. package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +45 -0
  81. package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +9 -0
  82. package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +860 -0
  83. package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +105 -0
  84. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +189 -0
  85. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +299 -0
  86. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +201 -0
  87. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +47 -0
  88. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +239 -0
  89. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +50 -0
  90. package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +259 -0
  91. package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +77 -0
  92. package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +534 -0
  93. package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +319 -0
  94. package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +325 -0
  95. package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +42 -0
  96. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +244 -0
  97. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +56 -0
  98. package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +50 -0
  99. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +167 -0
  100. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +207 -0
  101. package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +120 -0
  102. package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +8 -0
  103. package/node_modules/@luxusai/pi-hindsight/package.json +118 -0
  104. package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +75 -0
  105. package/node_modules/@narumitw/pi-btw/package.json +2 -2
  106. package/node_modules/pi-mcp-adapter/CHANGELOG.md +13 -0
  107. package/node_modules/pi-mcp-adapter/config.ts +10 -0
  108. package/node_modules/pi-mcp-adapter/dist/config.js +10 -0
  109. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  110. package/node_modules/pi-mcp-adapter/init.ts +5 -1
  111. package/node_modules/pi-mcp-adapter/package.json +1 -1
  112. package/package.json +10 -4
@@ -0,0 +1,817 @@
1
+ import type { HindsightLikeClient, ResolvedConfig } from "../types.js";
2
+ import { basename, dirname, extname, join, resolve } from "node:path";
3
+ import {
4
+ buildImportPlan,
5
+ createImportCheckpoint,
6
+ hashImportContent,
7
+ type ImportCheckpoint,
8
+ type ImportCheckpointDocument,
9
+ type ImportDocumentStatus,
10
+ type ImportPlan,
11
+ readImportCheckpointSafe,
12
+ readImportManifestSafe,
13
+ upsertImportManifestEntries,
14
+ writeImportCheckpoint,
15
+ } from "./import-plan.js";
16
+ import { createHash } from "node:crypto";
17
+ import {
18
+ deliverImportRetain,
19
+ type ImportDocumentPreview,
20
+ importRetainJobMatchesIdentity,
21
+ ImportRetainQueuedError,
22
+ type ImportRetainResult,
23
+ isImportRetainQueuedError,
24
+ previewImportBranch,
25
+ retainImportBranch,
26
+ } from "./import-execute.js";
27
+ import { importDocumentId } from "../utils/session.js";
28
+ import { readdir, readFile, stat } from "node:fs/promises";
29
+ import { redactError, redactSecrets } from "../utils/sanitize.js";
30
+ import { removeQueuedRetains } from "../queue/queue.js";
31
+ import { resolveOperationBank } from "../banks/bank-selection.js";
32
+ import { type ParsedSession, parseImportSessionJsonl } from "./import-parse.js";
33
+ export { parseImportSessionJsonl, parsePiSessionJsonl } from "./import-parse.js";
34
+ export { selectImportBranches } from "./import-parse.js";
35
+ export type { ImportBranch } from "./import-parse.js";
36
+ export type { ParsedMessage, ParsedSession } from "./import-parse.js";
37
+
38
+ export interface ImportSessionDocumentResult extends ImportDocumentPreview {}
39
+
40
+ export interface ImportExecutionResult {
41
+ documents: ImportSessionDocumentResult[];
42
+ messageCount: number;
43
+ retained: boolean;
44
+ }
45
+
46
+ function checkpointDocument(args: {
47
+ document: ImportDocumentPreview;
48
+ status: ImportDocumentStatus;
49
+ toolResults: ImportPlan["importConfig"]["import"]["toolResults"];
50
+ updatedAt: string;
51
+ error?: string;
52
+ }): ImportCheckpointDocument {
53
+ return {
54
+ documentId: args.document.documentId,
55
+ leafId: args.document.leafId,
56
+ contentHash: args.document.contentHash,
57
+ messageCount: args.document.messageCount,
58
+ ...(args.document.importMode ? { importMode: args.document.importMode } : {}),
59
+ toolResults: args.toolResults,
60
+ ...(args.document.importQualityProfile
61
+ ? { importQualityProfile: args.document.importQualityProfile }
62
+ : {}),
63
+ ...(args.document.projectionVersion
64
+ ? { projectionVersion: args.document.projectionVersion }
65
+ : {}),
66
+ ...(args.document.importProfile ? { importProfile: args.document.importProfile } : {}),
67
+ ...(args.document.chunkIndex !== undefined ? { chunkIndex: args.document.chunkIndex } : {}),
68
+ ...(args.document.messageRange ? { messageRange: args.document.messageRange } : {}),
69
+ status: args.status,
70
+ ...(args.document.skipReason ? { skipReason: args.document.skipReason } : {}),
71
+ updatedAt: args.updatedAt,
72
+ ...(args.error ? { error: args.error } : {}),
73
+ };
74
+ }
75
+
76
+ export async function executeImportPlan(args: {
77
+ client: HindsightLikeClient;
78
+ parsed: ParsedSession;
79
+ plan: ImportPlan;
80
+ dryRun?: boolean;
81
+ onProgress?: ImportProgressReporter;
82
+ }): Promise<ImportExecutionResult> {
83
+ const {
84
+ sessionFile,
85
+ bankId,
86
+ cwd,
87
+ sessionId,
88
+ leaves,
89
+ includeBranches,
90
+ branches,
91
+ manifestPath,
92
+ checkpointPath,
93
+ updateMode,
94
+ runId,
95
+ importConfig,
96
+ } = args.plan;
97
+ const now = new Date().toISOString();
98
+ const existingCheckpoint = importConfig.import.resume
99
+ ? (await readImportCheckpointSafe(checkpointPath)).checkpoint
100
+ : undefined;
101
+ let checkpoint: ImportCheckpoint =
102
+ existingCheckpoint?.runId === runId
103
+ ? existingCheckpoint
104
+ : createImportCheckpoint({
105
+ runId,
106
+ sourceFile: sessionFile,
107
+ bankId,
108
+ sessionId,
109
+ cwd,
110
+ includeBranches,
111
+ importMode: importConfig.import.mode,
112
+ toolResults: importConfig.import.toolResults,
113
+ importQualityProfile: importConfig.import.qualityProfile,
114
+ updateMode,
115
+ now,
116
+ });
117
+ checkpoint = {
118
+ ...checkpoint,
119
+ updatedAt: now,
120
+ toolResults: importConfig.import.toolResults,
121
+ importQualityProfile: importConfig.import.qualityProfile,
122
+ };
123
+
124
+ const results = [];
125
+ for (const branch of branches) {
126
+ const common = {
127
+ sessionFile,
128
+ bankId,
129
+ config: importConfig,
130
+ parsed: args.parsed,
131
+ cwd,
132
+ sessionId,
133
+ leaves,
134
+ branch,
135
+ };
136
+ args.onProgress?.({
137
+ phase: "previewing",
138
+ message: `Projecting branch ${branch.leafId}`,
139
+ sessionFile,
140
+ });
141
+ const previews = previewImportBranch(common);
142
+ for (const [index, preview] of previews.entries()) {
143
+ args.onProgress?.({
144
+ phase: args.dryRun ? "previewing" : "retaining",
145
+ message: `${args.dryRun ? "Previewing" : "Importing"} document ${index + 1}/${previews.length} for branch ${branch.leafId}`,
146
+ sessionFile,
147
+ current: index + 1,
148
+ total: previews.length,
149
+ });
150
+ const previous = checkpoint.documents[preview.document.documentId];
151
+ const canSkip =
152
+ !args.dryRun &&
153
+ importConfig.import.resume &&
154
+ previous?.status === "completed" &&
155
+ previous.contentHash === preview.document.contentHash;
156
+ if (args.dryRun || canSkip) {
157
+ if (canSkip && previous)
158
+ await removeQueuedRetains(cwd, importConfig, (job) =>
159
+ importRetainJobMatchesIdentity(job, {
160
+ bankId,
161
+ documentId: preview.document.documentId,
162
+ updateMode,
163
+ sourceFile: sessionFile,
164
+ cwd,
165
+ sessionId,
166
+ leafId: previous.leafId,
167
+ includeBranches,
168
+ ...(previous.importMode ? { importMode: previous.importMode } : {}),
169
+ ...(previous.toolResults ? { toolResults: previous.toolResults } : {}),
170
+ ...(previous.importQualityProfile
171
+ ? { importQualityProfile: previous.importQualityProfile }
172
+ : {}),
173
+ ...(previous.projectionVersion
174
+ ? { projectionVersion: previous.projectionVersion }
175
+ : {}),
176
+ ...(previous.importProfile ? { importProfile: previous.importProfile } : {}),
177
+ ...(previous.chunkIndex !== undefined ? { chunkIndex: previous.chunkIndex } : {}),
178
+ ...(previous.messageRange ? { messageRange: previous.messageRange } : {}),
179
+ contentHash: previous.contentHash,
180
+ }),
181
+ );
182
+ results.push({
183
+ ...preview,
184
+ document: {
185
+ ...preview.document,
186
+ wouldWrite: false,
187
+ status: canSkip ? ("skipped" as const) : preview.document.status,
188
+ ...(canSkip ? { skipReason: "already-imported" as const } : {}),
189
+ },
190
+ });
191
+ continue;
192
+ }
193
+
194
+ if (preview.document.status === "skipped") {
195
+ const skippedAt = new Date().toISOString();
196
+ checkpoint.documents[preview.document.documentId] = checkpointDocument({
197
+ document: preview.document,
198
+ status: "skipped",
199
+ toolResults: importConfig.import.toolResults,
200
+ updatedAt: skippedAt,
201
+ });
202
+ checkpoint.updatedAt = skippedAt;
203
+ await writeImportCheckpoint(checkpointPath, checkpoint);
204
+ results.push({
205
+ ...preview,
206
+ document: { ...preview.document, wouldWrite: false, status: "skipped" as const },
207
+ });
208
+ continue;
209
+ }
210
+
211
+ checkpoint.documents[preview.document.documentId] = checkpointDocument({
212
+ document: preview.document,
213
+ status: "pending",
214
+ toolResults: importConfig.import.toolResults,
215
+ updatedAt: new Date().toISOString(),
216
+ });
217
+ await writeImportCheckpoint(checkpointPath, checkpoint);
218
+
219
+ let completed: ImportRetainResult | undefined;
220
+ try {
221
+ const retained = await retainImportBranch({
222
+ ...common,
223
+ client: args.client,
224
+ documentId: preview.document.documentId,
225
+ });
226
+ const completedAt = new Date().toISOString();
227
+ checkpoint.documents[retained.document.documentId] = checkpointDocument({
228
+ document: retained.document,
229
+ status: "completed",
230
+ toolResults: importConfig.import.toolResults,
231
+ updatedAt: completedAt,
232
+ });
233
+ checkpoint.updatedAt = completedAt;
234
+ await writeImportCheckpoint(checkpointPath, checkpoint);
235
+ completed = retained;
236
+ } catch (error) {
237
+ const failedAt = new Date().toISOString();
238
+ const message = redactError(error);
239
+ const status = isImportRetainQueuedError(error) ? "queued" : "failed";
240
+ checkpoint.documents[preview.document.documentId] = checkpointDocument({
241
+ document: preview.document,
242
+ status,
243
+ toolResults: importConfig.import.toolResults,
244
+ updatedAt: failedAt,
245
+ error: message,
246
+ });
247
+ checkpoint.updatedAt = failedAt;
248
+ await writeImportCheckpoint(checkpointPath, checkpoint);
249
+ results.push({
250
+ ...preview,
251
+ document: { ...preview.document, status, error: message },
252
+ });
253
+ throw error;
254
+ }
255
+ if (!completed) throw new Error("Import retain completed without result.");
256
+ await upsertImportManifestEntries(manifestPath, [completed.manifestEntry]);
257
+ results.push({
258
+ ...completed,
259
+ document: { ...completed.document, status: "completed" as const },
260
+ });
261
+ }
262
+ }
263
+
264
+ const skippedResults = results.filter((result) => result.document.status === "skipped");
265
+ if (!args.dryRun && skippedResults.length > 0) {
266
+ const manifest = (await readImportManifestSafe(manifestPath)).manifest;
267
+ const missingSkippedEntries = skippedResults
268
+ .filter((result) => result.document.skipReason !== "empty-curated-projection")
269
+ .filter((result) => !manifest.imports[result.document.documentId])
270
+ .map((result) => result.manifestEntry);
271
+ if (missingSkippedEntries.length > 0)
272
+ await upsertImportManifestEntries(manifestPath, missingSkippedEntries);
273
+ }
274
+
275
+ const documents = results.map((result) => result.document);
276
+ return {
277
+ documents,
278
+ messageCount: documents.reduce((count, document) => count + document.messageCount, 0),
279
+ retained: !args.dryRun,
280
+ };
281
+ }
282
+
283
+ const KEPT_CHAT_TRANSCRIPT_EVENTS = new Set(["user_message", "assistant_reply", "process_end"]);
284
+
285
+ export interface ChatTranscriptEvent {
286
+ type: string;
287
+ timestamp?: string;
288
+ content?: unknown;
289
+ channel?: string;
290
+ sessionId?: string;
291
+ conversationId?: string;
292
+ data: Record<string, unknown>;
293
+ }
294
+
295
+ export interface ChatTranscriptImportResult {
296
+ sourceFile: string;
297
+ documentId: string;
298
+ retained: boolean;
299
+ skipped: boolean;
300
+ skipReason?: string;
301
+ dryRun: boolean;
302
+ bankId: string;
303
+ keptEventCount: number;
304
+ droppedEventCount: number;
305
+ malformedLineCount: number;
306
+ retainedTurnCount: number;
307
+ droppedEventTypes: Array<{ type: string; count: number }>;
308
+ contentHash: string;
309
+ contentBytes: number;
310
+ }
311
+
312
+ function isRecord(value: unknown): value is Record<string, unknown> {
313
+ return typeof value === "object" && value !== null && !Array.isArray(value);
314
+ }
315
+
316
+ function stringField(record: Record<string, unknown>, keys: string[]): string | undefined {
317
+ for (const key of keys) {
318
+ const value = record[key];
319
+ if (typeof value === "string" && value.trim()) return value;
320
+ }
321
+ return undefined;
322
+ }
323
+
324
+ function eventType(record: Record<string, unknown>): string | undefined {
325
+ return stringField(record, ["type", "event", "event_type", "kind"]);
326
+ }
327
+
328
+ function normalizeChatTranscriptEvent(
329
+ record: Record<string, unknown>,
330
+ ): ChatTranscriptEvent | undefined {
331
+ const type = eventType(record);
332
+ if (!type) return undefined;
333
+ const content = record.content ?? record.text ?? record.message ?? record.output;
334
+ const timestamp = stringField(record, ["timestamp", "created_at", "time"]);
335
+ const channel = stringField(record, ["channel", "channel_id"]);
336
+ const sessionId = stringField(record, ["session_id", "sessionId"]);
337
+ const conversationId = stringField(record, ["conversation_id", "conversationId", "thread_id"]);
338
+ return {
339
+ type,
340
+ ...(timestamp ? { timestamp } : {}),
341
+ ...(content !== undefined ? { content } : {}),
342
+ ...(channel ? { channel } : {}),
343
+ ...(sessionId ? { sessionId } : {}),
344
+ ...(conversationId ? { conversationId } : {}),
345
+ data: record,
346
+ };
347
+ }
348
+
349
+ export function parseChatTranscriptJsonl(text: string): {
350
+ kept: ChatTranscriptEvent[];
351
+ droppedEventTypes: Array<{ type: string; count: number }>;
352
+ droppedEventCount: number;
353
+ malformedLineCount: number;
354
+ } {
355
+ const kept: ChatTranscriptEvent[] = [];
356
+ const dropped = new Map<string, number>();
357
+ let malformedLineCount = 0;
358
+ for (const line of text.split("\n")) {
359
+ if (!line.trim()) continue;
360
+ let parsed: unknown;
361
+ try {
362
+ parsed = JSON.parse(line);
363
+ } catch {
364
+ malformedLineCount += 1;
365
+ continue;
366
+ }
367
+ if (!isRecord(parsed)) {
368
+ malformedLineCount += 1;
369
+ continue;
370
+ }
371
+ const event = normalizeChatTranscriptEvent(parsed);
372
+ if (!event) {
373
+ malformedLineCount += 1;
374
+ continue;
375
+ }
376
+ if (KEPT_CHAT_TRANSCRIPT_EVENTS.has(event.type)) kept.push(event);
377
+ else dropped.set(event.type, (dropped.get(event.type) ?? 0) + 1);
378
+ }
379
+ return {
380
+ kept,
381
+ droppedEventTypes: [...dropped]
382
+ .map(([type, count]) => ({ type, count }))
383
+ .sort((left, right) => right.count - left.count || left.type.localeCompare(right.type)),
384
+ droppedEventCount: [...dropped.values()].reduce((total, count) => total + count, 0),
385
+ malformedLineCount,
386
+ };
387
+ }
388
+
389
+ function firstDefined(
390
+ events: ChatTranscriptEvent[],
391
+ key: "channel" | "sessionId" | "conversationId",
392
+ ): string | undefined {
393
+ return events.find((event) => event[key])?.[key];
394
+ }
395
+
396
+ function tagValue(value: string): string {
397
+ return (
398
+ value
399
+ .trim()
400
+ .toLowerCase()
401
+ .replace(/[^a-z0-9._-]+/g, "-")
402
+ .slice(0, 80) || "unknown"
403
+ );
404
+ }
405
+
406
+ function documentId(sourceFile: string, kept: ChatTranscriptEvent[]): string {
407
+ const basis = JSON.stringify({ sourceFile, first: kept[0]?.data, last: kept.at(-1)?.data });
408
+ return `pi-gateway-import:${createHash("sha256").update(basis).digest("hex").slice(0, 16)}`;
409
+ }
410
+
411
+ export async function importChatTranscript(args: {
412
+ sourceFile: string;
413
+ cwd: string;
414
+ bankId: string;
415
+ client: HindsightLikeClient;
416
+ config: ResolvedConfig;
417
+ dryRun?: boolean;
418
+ onProgress?: ImportProgressReporter;
419
+ }): Promise<ChatTranscriptImportResult> {
420
+ args.onProgress?.({
421
+ phase: "reading",
422
+ message: `Reading chat transcript ${args.sourceFile}`,
423
+ sessionFile: args.sourceFile,
424
+ });
425
+ const parsed = parseChatTranscriptJsonl(await readFile(args.sourceFile, "utf8"));
426
+ args.onProgress?.({
427
+ phase: "planning",
428
+ message: `Planning chat transcript import for ${parsed.kept.length} kept event${parsed.kept.length === 1 ? "" : "s"}`,
429
+ sessionFile: args.sourceFile,
430
+ });
431
+ const docId = documentId(args.sourceFile, parsed.kept);
432
+ const channel = firstDefined(parsed.kept, "channel");
433
+ const sessionId = firstDefined(parsed.kept, "sessionId");
434
+ const conversationId = firstDefined(parsed.kept, "conversationId");
435
+ const contentRaw = JSON.stringify(
436
+ {
437
+ source: "chat-transcript-import",
438
+ sourceFile: args.sourceFile,
439
+ channel,
440
+ sessionId,
441
+ conversationId,
442
+ events: parsed.kept.map((event) => event.data),
443
+ droppedEventCount: parsed.droppedEventCount,
444
+ droppedEventTypes: parsed.droppedEventTypes,
445
+ },
446
+ null,
447
+ 2,
448
+ );
449
+ const content = args.config.retain.redactSecrets ? redactSecrets(contentRaw) : contentRaw;
450
+ const contentHash = createHash("sha256").update(content).digest("hex");
451
+ const result: ChatTranscriptImportResult = {
452
+ sourceFile: args.sourceFile,
453
+ documentId: docId,
454
+ retained: false,
455
+ skipped: parsed.kept.length === 0,
456
+ ...(parsed.kept.length === 0
457
+ ? { skipReason: "No high-signal chat transcript events found." }
458
+ : {}),
459
+ dryRun: Boolean(args.dryRun),
460
+ bankId: args.bankId,
461
+ keptEventCount: parsed.kept.length,
462
+ droppedEventCount: parsed.droppedEventCount,
463
+ malformedLineCount: parsed.malformedLineCount,
464
+ retainedTurnCount: parsed.kept.filter((event) => event.type === "user_message").length,
465
+ droppedEventTypes: parsed.droppedEventTypes,
466
+ contentHash,
467
+ contentBytes: Buffer.byteLength(content, "utf8"),
468
+ };
469
+ if (args.dryRun || parsed.kept.length === 0) return result;
470
+ args.onProgress?.({
471
+ phase: "retaining",
472
+ message: `Importing chat transcript document ${docId}`,
473
+ sessionFile: args.sourceFile,
474
+ current: 1,
475
+ total: 1,
476
+ });
477
+ const delivery = await deliverImportRetain({
478
+ cwd: args.cwd,
479
+ config: args.config,
480
+ client: args.client,
481
+ bankId: args.bankId,
482
+ content,
483
+ context: `Chat transcript import from ${basename(args.sourceFile)}`,
484
+ documentId: docId,
485
+ updateMode: "replace",
486
+ tags: [
487
+ "source:chat",
488
+ "import:chat",
489
+ "imported:true",
490
+ ...(channel ? [`channel:${tagValue(channel)}`] : []),
491
+ ...(sessionId ? [`session:${tagValue(sessionId)}`] : []),
492
+ ...(conversationId ? [`conversation:${tagValue(conversationId)}`] : []),
493
+ `document:${docId}`,
494
+ ],
495
+ metadata: {
496
+ source_file: args.sourceFile,
497
+ imported: "true",
498
+ source: "chat-transcript",
499
+ ...(channel ? { channel } : {}),
500
+ ...(sessionId ? { session_id: sessionId } : {}),
501
+ ...(conversationId ? { conversation_id: conversationId } : {}),
502
+ content_hash: contentHash,
503
+ },
504
+ });
505
+ if (!delivery.delivered) {
506
+ throw new ImportRetainQueuedError(
507
+ `Chat transcript import queued as ${delivery.queueJobId}; ${delivery.remaining} retain job${delivery.remaining === 1 ? "" : "s"} pending`,
508
+ );
509
+ }
510
+ return { ...result, retained: true };
511
+ }
512
+
513
+ export type ImportOperationDeps = {
514
+ getClient(): HindsightLikeClient;
515
+ getConfig(): ResolvedConfig;
516
+ getProjectBankId(): string;
517
+ };
518
+
519
+ export async function importMemorySession(
520
+ args: {
521
+ sessionFile: string;
522
+ cwd?: string;
523
+ bank?: string;
524
+ dryRun?: boolean;
525
+ includeBranches?: ResolvedConfig["import"]["includeBranches"];
526
+ onProgress?: ImportProgressReporter;
527
+ },
528
+ deps: ImportOperationDeps,
529
+ ) {
530
+ const bankId = resolveOperationBank({
531
+ requestedBank: args.bank,
532
+ config: deps.getConfig(),
533
+ projectBankId: deps.getProjectBankId(),
534
+ });
535
+ const result = await importPiSession({
536
+ sessionFile: args.sessionFile,
537
+ ...(args.cwd ? { cwd: args.cwd } : {}),
538
+ bankId,
539
+ client: deps.getClient(),
540
+ config: deps.getConfig(),
541
+ ...(args.dryRun !== undefined ? { dryRun: args.dryRun } : {}),
542
+ ...(args.includeBranches ? { includeBranches: args.includeBranches } : {}),
543
+ ...(args.onProgress ? { onProgress: args.onProgress } : {}),
544
+ });
545
+ return { bankId, ...result };
546
+ }
547
+
548
+ export async function importMemoryChatTranscript(
549
+ args: {
550
+ sourceFile: string;
551
+ cwd: string;
552
+ bank?: string;
553
+ dryRun?: boolean;
554
+ onProgress?: ImportProgressReporter;
555
+ },
556
+ deps: ImportOperationDeps,
557
+ ) {
558
+ const bankId = resolveOperationBank({
559
+ requestedBank: args.bank ?? "global",
560
+ config: deps.getConfig(),
561
+ projectBankId: deps.getProjectBankId(),
562
+ });
563
+ return importChatTranscript({
564
+ sourceFile: args.sourceFile,
565
+ cwd: args.cwd,
566
+ bankId,
567
+ client: deps.getClient(),
568
+ config: deps.getConfig(),
569
+ ...(args.dryRun !== undefined ? { dryRun: args.dryRun } : {}),
570
+ ...(args.onProgress ? { onProgress: args.onProgress } : {}),
571
+ });
572
+ }
573
+
574
+ export async function importMemoryProjectSessions(
575
+ args: {
576
+ cwd: string;
577
+ currentSessionFile?: string;
578
+ searchDir?: string;
579
+ bank?: string;
580
+ dryRun?: boolean;
581
+ includeBranches?: ResolvedConfig["import"]["includeBranches"];
582
+ onProgress?: ImportProgressReporter;
583
+ },
584
+ deps: ImportOperationDeps,
585
+ ) {
586
+ const bankId = resolveOperationBank({
587
+ requestedBank: args.bank,
588
+ config: deps.getConfig(),
589
+ projectBankId: deps.getProjectBankId(),
590
+ });
591
+ const result = await importProjectSessions({
592
+ cwd: args.cwd,
593
+ ...(args.currentSessionFile ? { currentSessionFile: args.currentSessionFile } : {}),
594
+ ...(args.searchDir ? { searchDir: args.searchDir } : {}),
595
+ bankId,
596
+ client: deps.getClient(),
597
+ config: deps.getConfig(),
598
+ ...(args.dryRun !== undefined ? { dryRun: args.dryRun } : {}),
599
+ ...(args.includeBranches ? { includeBranches: args.includeBranches } : {}),
600
+ ...(args.onProgress ? { onProgress: args.onProgress } : {}),
601
+ });
602
+ return { bankId, ...result };
603
+ }
604
+
605
+ export type ImportProgressEvent = {
606
+ phase:
607
+ | "reading"
608
+ | "planning"
609
+ | "previewing"
610
+ | "retaining"
611
+ | "discovering"
612
+ | "discovered"
613
+ | "session";
614
+ message: string;
615
+ sessionFile?: string;
616
+ current?: number;
617
+ total?: number;
618
+ };
619
+
620
+ export type ImportProgressReporter = (event: ImportProgressEvent) => void;
621
+
622
+ function sameProjectCwd(sessionCwd: string | undefined, cwd: string): boolean {
623
+ if (!sessionCwd) return false;
624
+ return resolve(sessionCwd) === resolve(cwd);
625
+ }
626
+
627
+ async function isFile(path: string): Promise<boolean> {
628
+ try {
629
+ return (await stat(path)).isFile();
630
+ } catch {
631
+ return false;
632
+ }
633
+ }
634
+
635
+ function projectSessionCheckpointPath(basePath: string, sessionFile: string): string {
636
+ return `${basePath}.${hashImportContent(sessionFile).slice(0, 12)}.json`;
637
+ }
638
+
639
+ export async function discoverProjectSessionFiles(args: {
640
+ cwd: string;
641
+ currentSessionFile?: string;
642
+ searchDir?: string;
643
+ }): Promise<ProjectSessionDiscoveryResult> {
644
+ const searchDir =
645
+ args.searchDir ?? (args.currentSessionFile ? dirname(args.currentSessionFile) : undefined);
646
+ if (!searchDir) return { sessionFiles: [], scanned: 0 };
647
+ const entries = await readdir(searchDir);
648
+ const candidates = entries
649
+ .filter((entry) => extname(entry) === ".jsonl")
650
+ .map((entry) => join(searchDir, entry));
651
+ const sessionFiles: string[] = [];
652
+ let scanned = 0;
653
+ for (const candidate of candidates) {
654
+ if (!(await isFile(candidate))) continue;
655
+ scanned += 1;
656
+ try {
657
+ const parsed = parseImportSessionJsonl(await readFile(candidate, "utf8"));
658
+ if (sameProjectCwd(parsed.cwd, args.cwd)) sessionFiles.push(candidate);
659
+ } catch {
660
+ // Ignore unrelated or malformed JSONL files during project-scoped discovery.
661
+ }
662
+ }
663
+ return { sessionFiles: sessionFiles.sort(), scanned };
664
+ }
665
+
666
+ export interface ProjectSessionDiscoveryResult {
667
+ sessionFiles: string[];
668
+ scanned: number;
669
+ }
670
+
671
+ export interface ImportProjectSessionsResult {
672
+ sessionFiles: string[];
673
+ scanned: number;
674
+ imported: ImportSessionResult[];
675
+ messageCount: number;
676
+ documentCount: number;
677
+ dryRun: boolean;
678
+ malformedLineCount: number;
679
+ }
680
+
681
+ export interface ImportSessionResult {
682
+ sessionFile: string;
683
+ documentId: string;
684
+ messageCount: number;
685
+ retained: boolean;
686
+ dryRun: boolean;
687
+ manifestPath: string;
688
+ checkpointPath: string;
689
+ runId: string;
690
+ malformedLineCount: number;
691
+ documents: ImportSessionDocumentResult[];
692
+ }
693
+
694
+ export async function importPiSession(args: {
695
+ sessionFile: string;
696
+ cwd?: string;
697
+ bankId: string;
698
+ client: HindsightLikeClient;
699
+ config: ResolvedConfig;
700
+ dryRun?: boolean;
701
+ requireMatchingCwd?: boolean;
702
+ includeBranches?: ResolvedConfig["import"]["includeBranches"];
703
+ onProgress?: ImportProgressReporter;
704
+ }): Promise<ImportSessionResult> {
705
+ args.onProgress?.({
706
+ phase: "reading",
707
+ message: `Reading session file ${args.sessionFile}`,
708
+ sessionFile: args.sessionFile,
709
+ });
710
+ const text = await readFile(args.sessionFile, "utf8");
711
+ const parsed = parseImportSessionJsonl(text);
712
+ args.onProgress?.({
713
+ phase: "planning",
714
+ message: `Planning import for ${parsed.messages.length} message${parsed.messages.length === 1 ? "" : "s"}`,
715
+ sessionFile: args.sessionFile,
716
+ });
717
+ const plan = buildImportPlan({
718
+ sessionFile: args.sessionFile,
719
+ parsed,
720
+ bankId: args.bankId,
721
+ config: args.config,
722
+ ...(args.cwd ? { cwd: args.cwd } : {}),
723
+ ...(args.requireMatchingCwd !== undefined
724
+ ? { requireMatchingCwd: args.requireMatchingCwd }
725
+ : {}),
726
+ ...(args.includeBranches ? { includeBranches: args.includeBranches } : {}),
727
+ });
728
+ const execution = await executeImportPlan({
729
+ client: args.client,
730
+ parsed,
731
+ plan,
732
+ ...(args.dryRun !== undefined ? { dryRun: args.dryRun } : {}),
733
+ ...(args.onProgress ? { onProgress: args.onProgress } : {}),
734
+ });
735
+
736
+ const first = execution.documents[0] ?? {
737
+ documentId: importDocumentId(plan.sessionId, "root"),
738
+ };
739
+ return {
740
+ sessionFile: args.sessionFile,
741
+ documentId: first.documentId,
742
+ messageCount: execution.messageCount,
743
+ retained: execution.retained,
744
+ dryRun: Boolean(args.dryRun),
745
+ manifestPath: plan.manifestPath,
746
+ checkpointPath: plan.checkpointPath,
747
+ runId: plan.runId,
748
+ malformedLineCount: parsed.malformedLineCount,
749
+ documents: execution.documents,
750
+ };
751
+ }
752
+
753
+ export async function importProjectSessions(args: {
754
+ cwd: string;
755
+ currentSessionFile?: string;
756
+ searchDir?: string;
757
+ bankId: string;
758
+ client: HindsightLikeClient;
759
+ config: ResolvedConfig;
760
+ dryRun?: boolean;
761
+ includeBranches?: ResolvedConfig["import"]["includeBranches"];
762
+ onProgress?: ImportProgressReporter;
763
+ }): Promise<ImportProjectSessionsResult> {
764
+ args.onProgress?.({ phase: "discovering", message: "Scanning project session files" });
765
+ const discovery = await discoverProjectSessionFiles({
766
+ cwd: args.cwd,
767
+ ...(args.currentSessionFile ? { currentSessionFile: args.currentSessionFile } : {}),
768
+ ...(args.searchDir ? { searchDir: args.searchDir } : {}),
769
+ });
770
+ args.onProgress?.({
771
+ phase: "discovered",
772
+ message: `Found ${discovery.sessionFiles.length} project session${discovery.sessionFiles.length === 1 ? "" : "s"} after scanning ${discovery.scanned} file${discovery.scanned === 1 ? "" : "s"}`,
773
+ current: discovery.sessionFiles.length,
774
+ total: discovery.scanned,
775
+ });
776
+ const imported: ImportSessionResult[] = [];
777
+ for (const [index, sessionFile] of discovery.sessionFiles.entries()) {
778
+ args.onProgress?.({
779
+ phase: "session",
780
+ message: `Importing project session ${index + 1}/${discovery.sessionFiles.length}: ${sessionFile}`,
781
+ sessionFile,
782
+ current: index + 1,
783
+ total: discovery.sessionFiles.length,
784
+ });
785
+ imported.push(
786
+ await importPiSession({
787
+ sessionFile,
788
+ cwd: args.cwd,
789
+ requireMatchingCwd: true,
790
+ bankId: args.bankId,
791
+ client: args.client,
792
+ config: {
793
+ ...args.config,
794
+ import: {
795
+ ...args.config.import,
796
+ checkpointPath: projectSessionCheckpointPath(
797
+ args.config.import.checkpointPath,
798
+ sessionFile,
799
+ ),
800
+ },
801
+ },
802
+ ...(args.dryRun !== undefined ? { dryRun: args.dryRun } : {}),
803
+ ...(args.includeBranches ? { includeBranches: args.includeBranches } : {}),
804
+ ...(args.onProgress ? { onProgress: args.onProgress } : {}),
805
+ }),
806
+ );
807
+ }
808
+ return {
809
+ sessionFiles: discovery.sessionFiles,
810
+ scanned: discovery.scanned,
811
+ imported,
812
+ messageCount: imported.reduce((count, result) => count + result.messageCount, 0),
813
+ documentCount: imported.reduce((count, result) => count + result.documents.length, 0),
814
+ dryRun: Boolean(args.dryRun),
815
+ malformedLineCount: imported.reduce((count, result) => count + result.malformedLineCount, 0),
816
+ };
817
+ }