codemem 0.47.0-beta.2 → 0.47.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
- import { ACCOUNT_CONNECTION_MIGRATION_STATEMENTS, ACCOUNT_UNLINK_MIGRATION_STATEMENTS, AUTH_LINK_AUTHORITY_CUTOVER_SQL, ContextOnlyReplayError, CoordinatorAccountLinkError, DEDUP_KEY_BACKFILL_JOB, DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, DeviceIdentityError, EXTRACTION_BENCHMARK_QUALITY_WEIGHTS, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, ObserverOutputError, ObserverOutputTransportError, OwnerProfileEnrollmentError, PROMPT_TRANSPORT_PROTOCOL_RANGE, REF_BACKFILL_JOB, RawEventIngestValidationError, RawEventSweeper, RefBackfillRunner, RemoteCoordinatorRequestError, SCOPE_BACKFILL_JOB, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, ScopeBackfillRunner, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, TRUSTED_HOOK_MAPPER_OPTIONS, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, applyDistillRule, arePromptTransportProtocolRangesCompatible, atomicReplaceConfigFile, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildTieredObserverSelection, buildViewerIdentityTarget, classifyPromptTransportFailure, clonePromptPackAttempt, collectOperationalStatus, compareMemoryRoleReports, connect, connectReadOnly, coordinatorAuthControllerReviewAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorGrantIdentityGroupAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorQualifyOwnerAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewInviteAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeDeviceAction, coordinatorRevokeScopeMembershipAction, coordinatorUpdateScopeAction, createBetterSqliteCoordinatorApp, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, describePiObserverStatus, detectInstallKind, draftDistillRule, enrollOwnerProfile, ensureDeviceIdentity, ensureSchemaBootstrapped, estimateExtractionModelCost, expandUserPath, exportMemories, extractApplyPatchPaths, extractionReplayObserverIdentity, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getAttributionDiagnostics, getCodememConfigPath, getExtractionBenchmarkProfile, getExtractionModelPricing, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryArtifactReport, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getUpdateStatus, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, importPiSessions, ingestRawEvents, initDatabase, isAutomaticRecallMeasurement, isCoordinatorControllerAttestationReference, isEmbeddingDisabled, isExplicitUpdateInstallEligible, isReleaseVersionForChannel, judgeDistillReport, linkCoordinatorAccount, listMaintenanceJobs, listPerPeerScopeSyncState, listRetentionScopeIds, loadObserverConfig, loadPublicKey, loadRuntimeSigningPublicKey, loadSqliteVec, mdnsEnabled, mutateCodememConfigFile, normalizeCoordinatorAccountLinkOrigin, normalizePromptTransportProtocolRange, observerBaseUrlForProviderOverride, planReplicationOpsAgePrune, probeCodememViewerLiveness, probeCodememViewerLiveness as probeCodememViewerLiveness$1, probeRequiredNativeRuntime, projectMatchesFilter, promptPackArtifactFingerprint, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readAccountLinkDevice, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorAccountLinkStatus, readCoordinatorOwnerReviewLocalEvidence, readCoordinatorSyncConfig, readImportPayload, recordAutomaticRecall, recordPromptPackArtifacts, recordPromptPackTerminal, recordRetrievalSurface, renderUnifiedDiff, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveComparablePath, resolveDbPath, resolveHookProject, resolveKeyPaths, resolvePiAgentDir, resolvePiObserverConfig, resolveProject, resolveProjectRoot, resolveRetrievalSession, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, scoreExtractionBenchmarkOutput, searchPiSessions, setPeerProjectFilter, syncPassPreflight, tryUpdateRetrievalDelivery, updatePeerAddresses, upgradeCoordinatorAccountSchemaFile, upgradeCoordinatorAuthSchemaFile, vacuumDatabase, viewerUrl, writeCodememConfigFile } from "@codemem/core";
2
+ import { ACCOUNT_CONNECTION_MIGRATION_STATEMENTS, ACCOUNT_UNLINK_MIGRATION_STATEMENTS, AUTH_LINK_AUTHORITY_CUTOVER_SQL, ContextOnlyReplayError, CoordinatorAccountLinkError, DEDUP_KEY_BACKFILL_JOB, DEFAULT_COORDINATOR_DB_PATH, DedupKeyBackfillRunner, DeviceIdentityError, EXTRACTION_BENCHMARK_QUALITY_WEIGHTS, FILE_CONTEXT_TRANSPORT_MAX_BYTES, FILE_CONTEXT_TRANSPORT_PROFILE, LEGACY_OWNER_RESOLUTION_SCHEMA_STATEMENTS, MUTATING_TOOL_NAMES, MemoryStore, ObserverClient, ObserverOutputError, ObserverOutputTransportError, OwnerProfileEnrollmentError, PROMPT_TRANSPORT_PROTOCOL_RANGE, REF_BACKFILL_JOB, RawEventIngestValidationError, RawEventSweeper, RefBackfillRunner, RemoteCoordinatorRequestError, SCOPE_BACKFILL_JOB, SESSION_CONTEXT_BACKFILL_JOB, SUMMARY_DEDUP_BACKFILL_JOB, ScopeBackfillRunner, SessionContextBackfillRunner, SummaryDedupBackfillRunner, SyncRetentionRunner, TRUSTED_HOOK_MAPPER_OPTIONS, VERSION, VectorModelMigrationRunner, aiBackfillStructuredContent, applyBootstrapSnapshot, applyDistillRule, arePromptTransportProtocolRangesCompatible, atomicReplaceConfigFile, backfillMemoryDedupKeys, backfillNarrativeFromBody, backfillTagsText, backfillVectors, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildTieredObserverSelection, buildViewerIdentityTarget, classifyPromptTransportFailure, clonePromptPackAttempt, collectOperationalStatus, compareMemoryRoleReports, connect, connectReadOnly, coordinatorAuthControllerReviewAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnrollDeviceAction, coordinatorGrantIdentityGroupAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorQualifyOwnerAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorReviewInviteAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeDeviceAction, coordinatorRevokeScopeMembershipAction, coordinatorUpdateScopeAction, createBetterSqliteCoordinatorApp, createFileContextRunner, deactivateLowSignalMemories, deactivateLowSignalObservations, dedupNearDuplicateMemories, describePiObserverStatus, detectInstallKind, dispatchFileContextTransport, draftDistillRule, enrollOwnerProfile, ensureDeviceIdentity, ensureSchemaBootstrapped, estimateExtractionModelCost, expandUserPath, exportMemories, extractApplyPatchPaths, extractionReplayObserverIdentity, fetchAllSnapshotPages, fingerprintPublicKey, flushRawEvents, formatHostPort, getAttributionDiagnostics, getCodememConfigPath, getExtractionBenchmarkProfile, getExtractionModelPricing, getInjectionEvalScenarioPack, getInjectionEvalScenarioPrompts, getMaintenanceJob, getMemoryArtifactReport, getMemoryRoleReport, getRawEventRelinkPlan, getRawEventRelinkReport, getRawEventStatus, getSemanticIndexDiagnostics, getSessionExtractionEval, getSessionExtractionEvalScenario, getUpdateStatus, getWorkspaceCodememConfigPath, hasPendingDedupKeyBackfill, hasPendingRefBackfill, hasPendingScopeBackfill, hasPendingSessionContextBackfill, hasPendingSummaryDedupBackfill, hasUnsyncedSharedMemoryChanges, importMemories, importPiSessions, ingestRawEvents, initDatabase, isAutomaticRecallMeasurement, isCoordinatorControllerAttestationReference, isEmbeddingDisabled, isExplicitUpdateInstallEligible, isReleaseVersionForChannel, judgeDistillReport, linkCoordinatorAccount, listMaintenanceJobs, listPerPeerScopeSyncState, listRetentionScopeIds, loadObserverConfig, loadPublicKey, loadRuntimeSigningPublicKey, loadSqliteVec, mdnsEnabled, mutateCodememConfigFile, normalizeCoordinatorAccountLinkOrigin, normalizePromptTransportProtocolRange, observerBaseUrlForProviderOverride, parseControllerHistoryEvidence, parseFileContextTransportRequest, planReplicationOpsAgePrune, probeCodememViewerLiveness, probeCodememViewerLiveness as probeCodememViewerLiveness$1, probeRequiredNativeRuntime, projectMatchesFilter, promptPackArtifactFingerprint, pruneReplicationOpsUntilCaughtUp, rawEventsGate, readAccountLinkDevice, readCodememConfigFile, readCodememConfigFileAtPath, readCoordinatorAccountLinkStatus, readCoordinatorOwnerReviewLocalEvidence, readCoordinatorSyncConfig, readImportPayload, recordAutomaticRecall, recordPromptPackArtifacts, recordPromptPackTerminal, renderUnifiedDiff, replayBatchExtraction, replayBatchExtractionWithTierRouting, requestJson, resolveCodememConfigPath, resolveComparablePath, resolveDbPath, resolveHookProject, resolveKeyPaths, resolvePiAgentDir, resolvePiObserverConfig, resolveProject, resolveProjectRoot, retryRawEventFailures, runSyncDaemon, runSyncPass, scanSecretsRetroactive, schema, scoreExtractionBenchmarkOutput, searchPiSessions, setPeerProjectFilter, syncPassPreflight, tryUpdateRetrievalDelivery, updatePeerAddresses, upgradeCoordinatorAccountSchemaFile, upgradeCoordinatorAuthSchemaFile, upgradeCoordinatorOwnerResolutionSchemaFile, vacuumDatabase, viewerUrl, writeCodememConfigFile } from "@codemem/core";
3
3
  import { Command, Option } from "commander";
4
4
  import omelette from "omelette";
5
- import { randomInt, randomUUID } from "node:crypto";
6
5
  import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
7
6
  import { homedir, networkInterfaces } from "node:os";
8
7
  import { dirname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
9
- import { styleText } from "node:util";
8
+ import { isDeepStrictEqual, styleText } from "node:util";
9
+ import { randomInt, randomUUID } from "node:crypto";
10
10
  import * as p from "@clack/prompts";
11
11
  import { serve } from "@hono/node-server";
12
12
  import { execFileSync, spawn, spawnSync } from "node:child_process";
@@ -79,6 +79,54 @@ function emitJsonError(errorCode, message, exitCode = 1) {
79
79
  process.exitCode = exitCode;
80
80
  }
81
81
  //#endregion
82
+ //#region src/commands/claude-hook-file-context-transport.ts
83
+ /** Internal packaged-hook transport. Normal CLI invocation keeps its original guards/output. */
84
+ function runClaudeFileContextTransport(payload, opts) {
85
+ const parsed = parseFileContextTransportRequest(payload);
86
+ if (!parsed.success) throw new Error("invalid file-context transport request");
87
+ let request = parsed.data;
88
+ if ([
89
+ "1",
90
+ "true",
91
+ "yes",
92
+ "on"
93
+ ].includes(String(process.env.CODEMEM_PLUGIN_IGNORE ?? "").trim().toLowerCase())) return {
94
+ ok: true,
95
+ attempt_id: request.attempt_id,
96
+ request_id: request.request_id,
97
+ result: { continue: true },
98
+ delivery_pending: false
99
+ };
100
+ const disabled = (value) => [
101
+ "0",
102
+ "false",
103
+ "off"
104
+ ].includes(String(value ?? "").trim().toLowerCase());
105
+ if (disabled(process.env.CODEMEM_RETRIEVAL_LEDGER)) request = {
106
+ ...request,
107
+ ledger_enabled: false
108
+ };
109
+ if (request.action === "lookup" && disabled(process.env.CODEMEM_FILE_CONTEXT)) {
110
+ const { relative_path: _path, project: _project, file_mtime_ms: _mtime, ...base } = request;
111
+ request = {
112
+ ...base,
113
+ action: "skip",
114
+ reason: "file_context_disabled"
115
+ };
116
+ }
117
+ const dbPath = resolveDbPath(resolveDbOpt(opts));
118
+ if (resolve(dbPath) !== resolve(request.db_path) || !isDeepStrictEqual(buildViewerIdentityTarget(), request.identity_target)) throw new Error("file-context target mismatch");
119
+ const store = new MemoryStore(dbPath);
120
+ try {
121
+ if (!store.hasCurrentIdentity()) throw new Error("file-context identity changed");
122
+ return dispatchFileContextTransport(store, request);
123
+ } finally {
124
+ try {
125
+ store.close();
126
+ } catch {}
127
+ }
128
+ }
129
+ //#endregion
82
130
  //#region src/commands/claude-hook-plugin-log.ts
83
131
  /**
84
132
  * Append-only plugin event log used by `claude-hook-inject` and
@@ -130,24 +178,12 @@ function logHookEvent(message) {
130
178
  //#endregion
131
179
  //#region src/commands/claude-hook-file-context.ts
132
180
  var FILE_GATE_MIN_BYTES = 1500;
133
- var FETCH_LIMIT = 40;
134
- var DISPLAY_LIMIT = 15;
135
- var MTIME_FRESH_TOLERANCE_MS = 3e5;
136
181
  var SMALL_FILE_BYPASS_PATTERNS = [
137
182
  /\.(json|jsonc|toml|ya?ml)$/i,
138
183
  /\.env(\.|$)/i,
139
184
  /(^|\/)dockerfile(\.|$)/i,
140
185
  /\.config\.(js|ts|mjs|cjs|json)$/i
141
186
  ];
142
- var KIND_ICONS = {
143
- decision: "⚖️",
144
- bugfix: "🔴",
145
- feature: "🟢",
146
- refactor: "🔄",
147
- discovery: "🔵",
148
- change: "✅",
149
- exploration: "🔬"
150
- };
151
187
  function emitJson$2(value) {
152
188
  console.log(JSON.stringify(value));
153
189
  }
@@ -187,105 +223,6 @@ function statFile(absPath) {
187
223
  return null;
188
224
  }
189
225
  }
190
- function parseJsonArray(value) {
191
- if (!value) return [];
192
- try {
193
- const parsed = JSON.parse(value);
194
- if (!Array.isArray(parsed)) return [];
195
- return parsed.filter((item) => typeof item === "string");
196
- } catch {
197
- return [];
198
- }
199
- }
200
- function normalizePathForCompare(path) {
201
- return path.replace(/\\/g, "/");
202
- }
203
- function scoreRow(row, normalizedTarget, idx) {
204
- const filesModified = parseJsonArray(row.files_modified);
205
- const inModified = filesModified.some((f) => normalizePathForCompare(f) === normalizedTarget);
206
- let score = 0;
207
- if (inModified) score += 2;
208
- if (filesModified.length <= 1) score += 2;
209
- else if (filesModified.length <= 3) score += 1;
210
- return {
211
- row,
212
- score,
213
- idx
214
- };
215
- }
216
- function scoreAndDedupe(rows, targetPath, limit) {
217
- const normalizedTarget = normalizePathForCompare(targetPath);
218
- const scored = rows.map((row, idx) => scoreRow(row, normalizedTarget, idx));
219
- const bestPerSession = /* @__PURE__ */ new Map();
220
- for (const item of scored) {
221
- const existing = bestPerSession.get(item.row.session_id);
222
- if (!existing || item.score > existing.score || item.score === existing.score && item.idx < existing.idx) bestPerSession.set(item.row.session_id, item);
223
- }
224
- const deduped = Array.from(bestPerSession.values());
225
- deduped.sort((a, b) => b.score - a.score || a.idx - b.idx);
226
- return deduped.slice(0, limit).map((s) => s.row);
227
- }
228
- function compactTime(timeStr) {
229
- return timeStr.toLowerCase().replace(" am", "a").replace(" pm", "p");
230
- }
231
- function formatTime(epochMs) {
232
- return new Date(epochMs).toLocaleString("en-US", {
233
- hour: "numeric",
234
- minute: "2-digit",
235
- hour12: true
236
- });
237
- }
238
- function formatDate(epochMs) {
239
- return new Date(epochMs).toLocaleString("en-US", {
240
- month: "short",
241
- day: "numeric",
242
- year: "numeric"
243
- });
244
- }
245
- function formatTimeline(rows, filePath, staleness) {
246
- const safePath = filePath.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
247
- const enriched = rows.map((row) => ({
248
- row,
249
- epochMs: Date.parse(row.created_at)
250
- })).filter((item) => Number.isFinite(item.epochMs) && item.epochMs > 0);
251
- const byDay = /* @__PURE__ */ new Map();
252
- for (const item of enriched) {
253
- const day = formatDate(item.epochMs);
254
- const bucket = byDay.get(day);
255
- if (bucket) bucket.push(item);
256
- else byDay.set(day, [item]);
257
- }
258
- const sortedDays = Array.from(byDay.entries()).sort((a, b) => {
259
- return Math.min(...a[1].map((i) => i.epochMs)) - Math.min(...b[1].map((i) => i.epochMs));
260
- });
261
- const ids = rows.map((r) => r.id);
262
- const lines = [`This file (${safePath}) has prior codemem observations. The Read result below is unchanged.`, `- Fetch full bodies on demand: memory.get_observations([${ids.join(", ")}]).`];
263
- if (staleness) {
264
- const driftMinutes = Math.max(1, Math.round((staleness.fileMtimeMs - staleness.newestObservationMs) / 6e4));
265
- lines.unshift(`Heads up: this file was modified ~${driftMinutes} min after the most recent observation below. Past entries may be partially stale — verify against the Read result before relying on them.`);
266
- }
267
- for (const [day, dayItems] of sortedDays) {
268
- const chronological = [...dayItems].sort((a, b) => a.epochMs - b.epochMs);
269
- lines.push(`### ${day}`);
270
- for (const { row, epochMs } of chronological) {
271
- const title = (row.title || "Untitled").replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, 160);
272
- const icon = KIND_ICONS[row.kind] ?? "❔";
273
- const time = compactTime(formatTime(epochMs));
274
- lines.push(`${row.id} ${time} ${icon} (${row.kind}) ${title}`);
275
- }
276
- }
277
- return lines.join("\n");
278
- }
279
- function queryByFile(dbPath, relativePath, project, limit) {
280
- const store = new MemoryStore(dbPath);
281
- try {
282
- const opts = { limit };
283
- if (project) opts.project = project;
284
- return store.findByFile(relativePath, opts);
285
- } finally {
286
- store.close();
287
- }
288
- }
289
226
  function resolveProject$1(payload) {
290
227
  const cwd = typeof payload.cwd === "string" ? payload.cwd : null;
291
228
  return resolveHookProject(cwd, payload.project);
@@ -294,73 +231,45 @@ function sourceSessionId(payload) {
294
231
  const value = payload.session_id;
295
232
  return typeof value === "string" && value.trim() ? value.trim() : null;
296
233
  }
297
- async function buildClaudeFileContext(payload, opts, deps = {}) {
298
- if (envTruthy$4(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult$2();
299
- const filePath = extractFilePath(payload);
300
- if (!filePath) return continueResult$2();
301
- const startedAt = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
302
- const attemptId = (deps.createAttemptId ?? randomUUID)();
303
- const resolveDb = deps.resolveDb ?? resolveDbPath;
234
+ function fileContextStore(opts, deps) {
304
235
  let resolvedDbPath = null;
305
236
  let activeStore = null;
306
- const getStore = () => {
307
- resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
308
- activeStore ??= (deps.createStore ?? ((dbPath) => new MemoryStore(dbPath)))(resolvedDbPath);
309
- return activeStore;
237
+ const dbPath = () => {
238
+ resolvedDbPath ??= (deps.resolveDb ?? resolveDbPath)(resolveDbOpt(opts));
239
+ return resolvedDbPath;
310
240
  };
311
- const finish = (result) => {
312
- try {
313
- activeStore?.close();
314
- } catch {}
315
- activeStore = null;
316
- return result;
317
- };
318
- const record = (attemptInput) => {
319
- if (!envNotDisabled$3(process.env.CODEMEM_RETRIEVAL_LEDGER || "1")) return;
320
- try {
321
- resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
322
- const completedAt = (deps.now ?? (() => /* @__PURE__ */ new Date()))();
323
- const ledgerInput = {
324
- ...attemptInput,
325
- attemptId,
326
- surface: "file_context",
327
- trigger: "automatic",
328
- startedAt: startedAt.toISOString(),
329
- completedAt: completedAt.toISOString(),
330
- latencyMs: Math.max(0, completedAt.getTime() - startedAt.getTime()),
331
- recorderVersion: "claude-file-context-v1",
332
- source: "claude",
333
- streamId: sourceSessionId(payload),
334
- sourceSessionId: sourceSessionId(payload),
335
- mode: "claude_pre_tool_use_read"
336
- };
337
- if (deps.recordAttempt) deps.recordAttempt(resolvedDbPath, ledgerInput);
338
- else {
339
- const store = getStore();
340
- recordRetrievalSurface(store.db, {
341
- ...ledgerInput,
342
- sessionId: resolveRetrievalSession(store.db, "claude", ledgerInput.sourceSessionId)
343
- });
344
- }
345
- } catch {}
346
- };
347
- const updateDelivery = (status) => {
348
- if (!envNotDisabled$3(process.env.CODEMEM_RETRIEVAL_LEDGER || "1")) return;
349
- try {
350
- resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
351
- if (deps.updateDelivery) deps.updateDelivery(resolvedDbPath, attemptId, status);
352
- else tryUpdateRetrievalDelivery(getStore().db, attemptId, status);
353
- } catch {}
354
- };
355
- if (!envNotDisabled$3(process.env.CODEMEM_FILE_CONTEXT || "1")) {
356
- record({
357
- retrievalStatus: "skipped",
358
- deliveryStatus: "not_attempted",
359
- failureCode: "file_context_disabled",
360
- failureStage: "configuration"
361
- });
362
- return finish(continueResult$2());
363
- }
241
+ return {
242
+ dbPath,
243
+ getStore: () => {
244
+ activeStore ??= (deps.createStore ?? ((path) => new MemoryStore(path)))(dbPath());
245
+ return activeStore;
246
+ },
247
+ finish: (result) => {
248
+ try {
249
+ activeStore?.close();
250
+ } catch {}
251
+ activeStore = null;
252
+ return result;
253
+ }
254
+ };
255
+ }
256
+ function fileContextRunner(payload, store, deps) {
257
+ const findByFile = deps.queryByFile;
258
+ const recordAttempt = deps.recordAttempt;
259
+ const updateDelivery = deps.updateDelivery;
260
+ return createFileContextRunner({
261
+ getStore: store.getStore,
262
+ sourceSessionId: sourceSessionId(payload),
263
+ ledgerEnabled: () => envNotDisabled$3(process.env.CODEMEM_RETRIEVAL_LEDGER || "1"),
264
+ now: deps.now,
265
+ createAttemptId: deps.createAttemptId,
266
+ log: logHookEvent,
267
+ ...findByFile ? { findByFile: (path, options) => findByFile(store.dbPath(), path, options.project ?? null, options.limit) } : {},
268
+ ...recordAttempt ? { recordAttempt: (input) => recordAttempt(store.dbPath(), input) } : {},
269
+ ...updateDelivery ? { updateDelivery: (attemptId, status) => updateDelivery(store.dbPath(), attemptId, status) } : {}
270
+ });
271
+ }
272
+ function fileContextPath(filePath, payload) {
364
273
  const cwd = typeof payload.cwd === "string" && payload.cwd.trim() ? payload.cwd : process.cwd();
365
274
  const expandedPath = expandHome$2(filePath);
366
275
  const absolutePath = isAbsolute(expandedPath) ? expandedPath : resolve(cwd, expandedPath);
@@ -368,133 +277,78 @@ async function buildClaudeFileContext(payload, opts, deps = {}) {
368
277
  const escapesCwd = relativePath === ".." || relativePath.startsWith("../") || isAbsolute(relativePath);
369
278
  if (!relativePath || escapesCwd) {
370
279
  logHookEvent(`file_context.skip reason=outside_cwd path=${JSON.stringify(filePath)} cwd=${JSON.stringify(cwd)}`);
371
- record({
372
- retrievalStatus: "skipped",
373
- deliveryStatus: "not_attempted",
374
- failureCode: "outside_cwd",
375
- failureStage: "path_validation"
376
- });
377
- return finish(continueResult$2());
280
+ return null;
378
281
  }
282
+ return {
283
+ absolutePath,
284
+ relativePath
285
+ };
286
+ }
287
+ async function buildClaudeFileContext(payload, opts, deps = {}) {
288
+ if (envTruthy$4(process.env.CODEMEM_PLUGIN_IGNORE)) return continueResult$2();
289
+ const filePath = extractFilePath(payload);
290
+ if (!filePath) return continueResult$2();
291
+ const store = fileContextStore(opts, deps);
292
+ const runner = fileContextRunner(payload, store, deps);
293
+ if (!envNotDisabled$3(process.env.CODEMEM_FILE_CONTEXT || "1")) return store.finish(runner.skip({ reason: "file_context_disabled" }));
294
+ const path = fileContextPath(filePath, payload);
295
+ if (!path) return store.finish(runner.skip({ reason: "outside_cwd" }));
296
+ const { absolutePath, relativePath } = path;
379
297
  const minBytes = Number.parseInt(process.env.CODEMEM_FILE_CONTEXT_MIN_BYTES ?? `${FILE_GATE_MIN_BYTES}`, 10);
380
298
  const minBytesEffective = Number.isFinite(minBytes) && minBytes >= 0 ? minBytes : FILE_GATE_MIN_BYTES;
381
299
  const stat = (deps.statFile ?? statFile)(absolutePath);
382
300
  if (!stat) {
383
301
  logHookEvent(`file_context.skip reason=stat_failed path=${JSON.stringify(relativePath)}`);
384
- record({
385
- retrievalStatus: "skipped",
386
- deliveryStatus: "not_attempted",
387
- failureCode: "stat_failed",
388
- failureStage: "file_access",
302
+ return store.finish(runner.skip({
303
+ reason: "stat_failed",
389
304
  repositoryPaths: [relativePath]
390
- });
391
- return finish(continueResult$2());
305
+ }));
392
306
  }
393
307
  const bypassSizeGate = SMALL_FILE_BYPASS_PATTERNS.some((p) => p.test(relativePath));
394
308
  if (stat.sizeBytes < minBytesEffective && !bypassSizeGate) {
395
309
  logHookEvent(`file_context.skip reason=below_size_gate path=${JSON.stringify(relativePath)} size=${stat.sizeBytes} gate=${minBytesEffective}`);
396
- record({
397
- retrievalStatus: "skipped",
398
- deliveryStatus: "not_attempted",
399
- failureCode: "below_size_gate",
400
- failureStage: "size_gate",
401
- repositoryPaths: [relativePath]
402
- });
403
- return finish(continueResult$2());
404
- }
405
- const project = resolveProject$1(payload);
406
- const queryFn = deps.queryByFile ?? queryByFile;
407
- let rows = [];
408
- try {
409
- resolvedDbPath ??= resolveDb(resolveDbOpt(opts));
410
- rows = deps.queryByFile ? queryFn(resolvedDbPath, relativePath, project, FETCH_LIMIT) : getStore().findByFile(relativePath, {
411
- limit: FETCH_LIMIT,
412
- ...project ? { project } : {}
413
- });
414
- } catch (err) {
415
- logHookEvent(`codemem claude-hook-file-context query failed: ${err instanceof Error ? err.message : String(err)}`);
416
- record({
417
- retrievalStatus: "failed",
418
- deliveryStatus: "not_attempted",
419
- failureCode: "query_failed",
420
- failureStage: "retrieval",
421
- project,
422
- filters: project ? { project } : void 0,
310
+ return store.finish(runner.skip({
311
+ reason: "below_size_gate",
423
312
  repositoryPaths: [relativePath]
424
- });
425
- return finish(continueResult$2());
426
- }
427
- if (rows.length === 0) {
428
- logHookEvent(`file_context.skip reason=no_observations path=${JSON.stringify(relativePath)} project=${JSON.stringify(project ?? "")}`);
429
- record({
430
- retrievalStatus: "no_results",
431
- deliveryStatus: "not_attempted",
432
- project,
433
- filters: project ? { project } : void 0,
434
- repositoryPaths: [relativePath]
435
- });
436
- return finish(continueResult$2());
437
- }
438
- const top = scoreAndDedupe(rows, relativePath, DISPLAY_LIMIT);
439
- if (top.length === 0) {
440
- logHookEvent(`file_context.skip reason=no_top_after_dedupe path=${JSON.stringify(relativePath)} candidates=${rows.length}`);
441
- record({
442
- retrievalStatus: "succeeded",
443
- deliveryStatus: "not_attempted",
444
- candidateIds: rows.map((row) => row.id),
445
- candidateCount: rows.length,
446
- selectedIds: [],
447
- failureCode: "no_top_after_dedupe",
448
- failureStage: "selection",
449
- project,
450
- filters: project ? { project } : void 0,
451
- repositoryPaths: [relativePath]
452
- });
453
- return finish(continueResult$2());
454
- }
455
- let staleness = null;
456
- if (stat.mtimeMs > 0) {
457
- const newestObservationMs = top.reduce((max, row) => {
458
- const epoch = Date.parse(row.created_at);
459
- return Number.isFinite(epoch) && epoch > max ? epoch : max;
460
- }, 0);
461
- if (newestObservationMs > 0 && stat.mtimeMs > newestObservationMs + MTIME_FRESH_TOLERANCE_MS) staleness = {
462
- fileMtimeMs: stat.mtimeMs,
463
- newestObservationMs
464
- };
465
- }
466
- record({
467
- retrievalStatus: "succeeded",
468
- deliveryStatus: "not_attempted",
469
- candidateIds: rows.map((row) => row.id),
470
- candidateCount: rows.length,
471
- selectedIds: top.map((row) => row.id),
472
- project,
473
- filters: project ? { project } : void 0,
474
- repositoryPaths: [relativePath]
475
- });
476
- let timeline;
477
- try {
478
- timeline = formatTimeline(top, relativePath, staleness);
479
- } catch {
480
- updateDelivery("failed");
481
- return finish(continueResult$2());
313
+ }));
482
314
  }
483
- logHookEvent(`file_context.ok path=${JSON.stringify(relativePath)} candidates=${rows.length} surfaced=${top.length} project=${JSON.stringify(project ?? "")} stale=${staleness ? "true" : "false"}`);
484
- updateDelivery("handed_off");
485
- return finish({ hookSpecificOutput: {
486
- hookEventName: "PreToolUse",
487
- permissionDecision: "allow",
488
- additionalContext: timeline
489
- } });
315
+ return store.finish(runner.run({
316
+ relativePath,
317
+ project: resolveProject$1(payload),
318
+ fileMtimeMs: stat.mtimeMs
319
+ }));
490
320
  }
491
321
  var claudeHookFileContextCmd = new Command("claude-hook-file-context").configureHelp(helpStyle).description("Return Claude PreToolUse:Read additionalContext from per-file observation timeline");
492
322
  addDbOption(claudeHookFileContextCmd);
323
+ claudeHookFileContextCmd.addOption(new Option("--transport-profile").hideHelp());
324
+ claudeHookFileContextCmd.addOption(new Option("--transport").hideHelp());
493
325
  var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts) => {
326
+ if (opts.transportProfile) {
327
+ process.stdout.write(`${JSON.stringify(FILE_CONTEXT_TRANSPORT_PROFILE)}\n`);
328
+ return;
329
+ }
494
330
  let raw = "";
495
- for await (const chunk of process.stdin) raw += String(chunk);
331
+ for await (const chunk of process.stdin) {
332
+ raw += String(chunk);
333
+ if (opts.transport && Buffer.byteLength(raw) > FILE_CONTEXT_TRANSPORT_MAX_BYTES) {
334
+ emitError$2({
335
+ error: "parse_error",
336
+ message: "transport payload too large"
337
+ });
338
+ process.exitCode = 1;
339
+ return;
340
+ }
341
+ }
496
342
  const trimmed = raw.trim();
497
343
  if (!trimmed) {
344
+ if (opts.transport) {
345
+ process.stdout.write(`${JSON.stringify({
346
+ error: "parse_error",
347
+ message: "transport request required"
348
+ })}\n`);
349
+ process.exitCode = 1;
350
+ return;
351
+ }
498
352
  emitJson$2(continueResult$2());
499
353
  return;
500
354
  }
@@ -518,8 +372,23 @@ var claudeHookFileContextCommand = claudeHookFileContextCmd.action(async (opts)
518
372
  process.exitCode = 1;
519
373
  return;
520
374
  }
521
- emitJson$2(await buildClaudeFileContext(payload, opts));
375
+ await emitFileContext(payload, opts);
522
376
  });
377
+ async function emitFileContext(payload, opts) {
378
+ try {
379
+ if (opts.transport) {
380
+ process.stdout.write(`${JSON.stringify(runClaudeFileContextTransport(payload, opts))}\n`);
381
+ return;
382
+ }
383
+ emitJson$2(await buildClaudeFileContext(payload, opts));
384
+ } catch {
385
+ emitError$2({
386
+ error: "file_context_failed",
387
+ message: "file context unavailable"
388
+ });
389
+ process.exitCode = 1;
390
+ }
391
+ }
523
392
  //#endregion
524
393
  //#region src/commands/hook-ingest-live-spool.ts
525
394
  function spoolLiveHookPayload(dir, payload, logPrefix) {
@@ -1727,7 +1596,7 @@ async function buildLocalPack$2(context, project, dbPath, workingSetPaths = [])
1727
1596
  packTokens: Number.isFinite(Number(pack.metrics?.pack_tokens)) ? Number(pack.metrics.pack_tokens) : 0
1728
1597
  };
1729
1598
  } finally {
1730
- store.close();
1599
+ await store.closeAsync();
1731
1600
  }
1732
1601
  }
1733
1602
  async function buildClaudeHookInjection(payload, opts, deps = {}) {
@@ -2368,7 +2237,7 @@ async function buildLocalPack$1(context, project, dbPath) {
2368
2237
  packTokens: Number.isFinite(Number(pack.metrics?.pack_tokens)) ? Number(pack.metrics?.pack_tokens) : 0
2369
2238
  };
2370
2239
  } finally {
2371
- store.close();
2240
+ await store.closeAsync();
2372
2241
  }
2373
2242
  }
2374
2243
  async function prepareCodexHookInjection(payload, opts, deps = {}) {
@@ -2630,21 +2499,34 @@ function hasDatabaseSelection(options) {
2630
2499
  return options.dbPath !== void 0 || options.db !== void 0;
2631
2500
  }
2632
2501
  function exportAccountSchema(options) {
2633
- const statements = options.deviceUnlink ? ACCOUNT_UNLINK_MIGRATION_STATEMENTS : ACCOUNT_CONNECTION_MIGRATION_STATEMENTS;
2634
- const text = `-- ${accountPrerequisites}\n-- Execute in ONE atomic D1 batch; do not split into autocommitted requests. Nothing has been applied.\n${statements.map((s) => `${s.trim().replace(/;$/u, "")};`).join("\n")}\n`;
2502
+ let statements = options.deviceUnlink ? ACCOUNT_UNLINK_MIGRATION_STATEMENTS : ACCOUNT_CONNECTION_MIGRATION_STATEMENTS;
2503
+ let prerequisites = accountPrerequisites;
2504
+ if (options.ownerResolution) {
2505
+ statements = LEGACY_OWNER_RESOLUTION_SCHEMA_STATEMENTS;
2506
+ prerequisites += " Requires the current account-management schema first; installs inert receipt storage only, not ownership or access.";
2507
+ }
2508
+ const text = `-- ${prerequisites}\n-- Execute in ONE atomic D1 batch; do not split into autocommitted requests. Nothing has been applied.\n${statements.map((s) => `${s.trim().replace(/;$/u, "")};`).join("\n")}\n`;
2635
2509
  process.stdout.write(options.json ? `${JSON.stringify({
2636
2510
  statements,
2637
2511
  applied: false
2638
2512
  })}\n` : text);
2639
2513
  }
2514
+ function invalidAccountSelection(options, path) {
2515
+ if (options.exportSql) return hasDatabaseSelection(options);
2516
+ if (!path) return true;
2517
+ return Boolean(options.ownerResolution && !hasDatabaseSelection(options));
2518
+ }
2640
2519
  async function upgradeAccount(options) {
2520
+ if (options.ownerResolution && options.deviceUnlink) {
2521
+ failure$4(options, "Use --owner-resolution without --device-unlink.", 2, "account_schema_upgrade_failed");
2522
+ return;
2523
+ }
2641
2524
  if (options.deviceUnlink && !options.exportSql) {
2642
2525
  failure$4(options, "Use --device-unlink only with --export-sql; local upgrades detect the installed schema.", 2, "account_schema_upgrade_failed");
2643
2526
  return;
2644
2527
  }
2645
2528
  const path = resolveDbOpt(options);
2646
- const selected = hasDatabaseSelection(options);
2647
- if (options.exportSql && selected || !options.exportSql && !path) {
2529
+ if (invalidAccountSelection(options, path)) {
2648
2530
  failure$4(options, options.exportSql ? "Use --export-sql without --db-path; export opens no database." : `Select an existing coordinator database with --db-path. ${accountPrerequisites}`, 2, "account_schema_upgrade_failed");
2649
2531
  return;
2650
2532
  }
@@ -2655,14 +2537,23 @@ async function upgradeAccount(options) {
2655
2537
  }
2656
2538
  if (!path) return;
2657
2539
  if (!options.json) process.stderr.write(`${accountPrerequisites}\n`);
2658
- await upgradeCoordinatorAccountSchemaFile(path);
2659
- process.stdout.write(options.json ? `${JSON.stringify({ upgraded: true })}\n` : "Coordinator account schema upgraded.\n");
2540
+ if (options.ownerResolution) await upgradeCoordinatorOwnerResolutionSchemaFile(path);
2541
+ else await upgradeCoordinatorAccountSchemaFile(path);
2542
+ process.stdout.write(options.json ? `${JSON.stringify({ upgraded: true })}\n` : accountUpgradeSuccess(options));
2660
2543
  } catch (error) {
2661
- failure$4(options, `Account schema upgrade stopped. Inspect the selected database before retrying. ${error instanceof Error ? error.message : "Coordinator account schema upgrade failed."}`, 1, "account_schema_upgrade_failed");
2544
+ failure$4(options, `Account schema upgrade stopped. Inspect the selected database before retrying. ${accountUpgradeFailure(options, error)}`, 1, "account_schema_upgrade_failed");
2662
2545
  }
2663
2546
  }
2547
+ function accountUpgradeSuccess(options) {
2548
+ if (options.ownerResolution) return "Coordinator owner-resolution receipt storage ready; ownership and access unchanged.\n";
2549
+ return "Coordinator account schema upgraded.\n";
2550
+ }
2551
+ function accountUpgradeFailure(options, error) {
2552
+ if (options.ownerResolution) return "Owner-resolution storage requires the current account schema: first use upgrade-account-schema without --owner-resolution. Partial, unsupported or colliding receipt schemas cannot be repaired by this command. Check file access and writer locks.";
2553
+ return error instanceof Error ? error.message : "Coordinator account schema upgrade failed.";
2554
+ }
2664
2555
  function buildCoordinatorAccountSchemaCommand() {
2665
- const command = new Command("upgrade-account-schema").configureHelp(helpStyle).description("Explicitly install optional coordinator account management on an existing V2 database").option("--device-unlink", "export only the delta for the prior account-management schema (requires --export-sql)").option("-e, --export-sql", "export statement boundaries for one atomic D1 batch without opening a database").addHelpText("after", `\n${accountPrerequisites}\nD1 export does not apply SQL or verify remote execution limits.\n`);
2556
+ const command = new Command("upgrade-account-schema").configureHelp(helpStyle).description("Explicitly install optional coordinator account management on an existing V2 database").option("-o, --owner-resolution", "install inert owner-resolution receipt storage (requires current account schema)").option("--device-unlink", "export only the delta for the prior account-management schema (requires --export-sql)").option("-e, --export-sql", "export statement boundaries for one atomic D1 batch without opening a database").addHelpText("after", `\n${accountPrerequisites}\n--owner-resolution installs storage only; it does not resolve an owner or change access. It requires the current account schema and an explicit --db-path, or --export-sql without a database. It cannot be combined with --device-unlink.\nD1 export does not apply SQL or verify remote execution limits.\n`);
2666
2557
  addDbOption(command);
2667
2558
  addJsonOption(command);
2668
2559
  command.action(upgradeAccount);
@@ -3195,7 +3086,8 @@ var REVIEW_REASONS = [
3195
3086
  "key_mismatch",
3196
3087
  "enrollment_identity_mismatch",
3197
3088
  "invite_identity_mismatch",
3198
- "invite_evidence_invalid"
3089
+ "invite_evidence_invalid",
3090
+ "legacy_history_invalid"
3199
3091
  ];
3200
3092
  function validId$1(value) {
3201
3093
  return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
@@ -3204,6 +3096,7 @@ function isHash(value) {
3204
3096
  return typeof value === "string" && value.length === 64 && /^[a-f0-9]{64}$/.test(value);
3205
3097
  }
3206
3098
  function reviewErrorMessage(error) {
3099
+ if (error instanceof CoordinatorHistoryUnavailableError) return "Update the coordinator before reviewing this device owner: retained history evidence was missing or invalid in its response. Nothing changed.";
3207
3100
  if (error instanceof RemoteCoordinatorRequestError) {
3208
3101
  if (error.code === "review_stale") return "Evidence changed since the preview. Nothing changed; run the review again.";
3209
3102
  if (error.code === "already_reviewed_or_needs_review") return "This device already has a reviewed owner, or needs a separate review. Nothing changed.";
@@ -3271,9 +3164,27 @@ function validInviteCount(reply, invites) {
3271
3164
  function validInviteRef(invite) {
3272
3165
  return !!invite && validId$1(invite.invite_id) && (invite.kind === "team_member" || invite.kind === "add_device");
3273
3166
  }
3167
+ function previewEnrollment(enrollment) {
3168
+ if (!enrollment) return null;
3169
+ return {
3170
+ device_id: enrollment.device_id,
3171
+ fingerprint: enrollment.fingerprint,
3172
+ display_name: null,
3173
+ identity_label: enrollment.identity_label
3174
+ };
3175
+ }
3176
+ var CoordinatorHistoryUnavailableError = class extends Error {};
3177
+ function parsePreviewHistory(value, coordinatorId) {
3178
+ try {
3179
+ return parseControllerHistoryEvidence(value, coordinatorId);
3180
+ } catch {
3181
+ throw new CoordinatorHistoryUnavailableError();
3182
+ }
3183
+ }
3274
3184
  function parsePreview(payload, request) {
3275
3185
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("invalid_preview");
3276
3186
  const reply = payload;
3187
+ if (!Array.isArray(reply.legacy_history)) throw new CoordinatorHistoryUnavailableError();
3277
3188
  const enrollment = reply.enrollment;
3278
3189
  const invites = reply.reviewed_invites;
3279
3190
  if (reply.state !== "ready" && reply.state !== "needs_review" || !validId$1(reply.coordinator_id) || !Array.isArray(reply.reasons) || !reply.reasons.every((reason) => typeof reason === "string" && REVIEW_REASONS.includes(reason)) || !Array.isArray(invites) || !validInviteCount(reply, invites) || !invites.every(validInviteRef)) throw new Error("invalid_preview");
@@ -3282,14 +3193,10 @@ function parsePreview(payload, request) {
3282
3193
  return {
3283
3194
  state: reply.state,
3284
3195
  coordinator_id: reply.coordinator_id,
3196
+ legacy_history: parsePreviewHistory(reply.legacy_history, reply.coordinator_id),
3285
3197
  reasons: reply.reasons,
3286
3198
  evidence_digest: reply.state === "ready" ? reply.evidence_digest : void 0,
3287
- enrollment: enrollment ? {
3288
- device_id: enrollment.device_id,
3289
- fingerprint: enrollment.fingerprint,
3290
- display_name: null,
3291
- identity_label: enrollment.identity_label
3292
- } : null,
3199
+ enrollment: previewEnrollment(enrollment),
3293
3200
  reviewed_invites: invites.map((invite) => ({
3294
3201
  invite_id: invite.invite_id,
3295
3202
  kind: invite.kind
@@ -3364,6 +3271,86 @@ function emitReviewPreview(evidence, preview) {
3364
3271
  ...stopped
3365
3272
  })}\n`);
3366
3273
  }
3274
+ function terminalIdentifier(value) {
3275
+ return JSON.stringify(value).replace(/[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/gu, (character) => `\\u{${character.codePointAt(0)?.toString(16)}}`);
3276
+ }
3277
+ function historyAssociation(deviceId, fingerprint, subject) {
3278
+ if (deviceId === subject.deviceId) {
3279
+ if (fingerprint === subject.fingerprint) return "Current device; current fingerprint";
3280
+ return "Current device; different recorded key";
3281
+ }
3282
+ if (fingerprint === subject.fingerprint) return "Other device; current fingerprint";
3283
+ return "Other recorded device/key; canonical-key equivalence is not identified in this summary";
3284
+ }
3285
+ function* controllerHistoryRows(preview, subject) {
3286
+ for (const record of preview.legacy_history) {
3287
+ yield {
3288
+ text: `Historical group ${terminalIdentifier(record.group_id)}; Team ${terminalIdentifier(record.manifest.team_id)}; completed ${terminalIdentifier(record.completed_at)}\nCandidate: ${terminalIdentifier(record.candidate_ref)}; manifest version: ${record.manifest_version}; recorded: ${terminalIdentifier(record.created_at)}\nOriginal retained manifest JSON: ${terminalIdentifier(record.manifest_json)}`,
3289
+ records: 1,
3290
+ decisions: 0
3291
+ };
3292
+ for (const decision of record.manifest.device_decisions) {
3293
+ const identity = decision.identity_id === null ? "none" : terminalIdentifier(decision.identity_id);
3294
+ yield {
3295
+ text: ` ${decision.decision}; Identity: ${identity}; enabled: ${decision.enabled ? "yes" : "no"}\n Device: ${terminalIdentifier(decision.device_id)}; fingerprint: ${terminalIdentifier(decision.key_fingerprint)}\n ${historyAssociation(decision.device_id, decision.key_fingerprint, subject)}`,
3296
+ records: 0,
3297
+ decisions: 1
3298
+ };
3299
+ }
3300
+ }
3301
+ }
3302
+ /** A truncated summary must never authorize the terminal confirmation. */
3303
+ function formatControllerReviewHistory(preview, subject) {
3304
+ const lines = [
3305
+ `Proposed current owner Identity: ${terminalIdentifier(subject.identityId)}`,
3306
+ "Retained legacy sharing assignments (not ownership changes):",
3307
+ "Records may contain other devices. Different fingerprints may represent known key aliases; no row below is assumed to belong to this device unless labeled as an exact device/fingerprint match."
3308
+ ];
3309
+ let records = 0;
3310
+ let decisions = 0;
3311
+ let characters = lines.join("\n").length;
3312
+ let complete = true;
3313
+ for (const row of controllerHistoryRows(preview, subject)) {
3314
+ if (records + row.records > 20 || decisions + row.decisions > 40 || characters + row.text.length + 1 > 24e3) {
3315
+ complete = false;
3316
+ break;
3317
+ }
3318
+ lines.push(row.text);
3319
+ records += row.records;
3320
+ decisions += row.decisions;
3321
+ characters += row.text.length + 1;
3322
+ }
3323
+ if (preview.legacy_history.length === 0) lines.push("No relevant retained legacy assignments.");
3324
+ if (!complete) {
3325
+ const totalDecisions = preview.legacy_history.reduce((total, record) => total + record.manifest.device_decisions.length, 0);
3326
+ lines.push(`TRUNCATED: showing ${records} of ${preview.legacy_history.length} records and ${decisions} of ${totalDecisions} device decisions. Terminal confirmation is disabled.`);
3327
+ }
3328
+ lines.push("Previous assignments are retained. This review records current ownership; it does not migrate sharing, move memories, merge Identities, or grant Team/Project access.", "Approval grants the existing controller authority, including account management when the coordinator permits it. Google account linking remains a separate step.");
3329
+ return {
3330
+ text: lines.join("\n"),
3331
+ complete
3332
+ };
3333
+ }
3334
+ function showCoordinatorReview(preview, request, options) {
3335
+ p.log.info(`Coordinator: ${preview.coordinator_id}; reviewed invitations: ${preview.reviewed_invite_count}`, { output: process.stderr });
3336
+ const history = formatControllerReviewHistory(preview, request);
3337
+ p.log.message(history.text, { output: process.stderr });
3338
+ if (history.complete) return true;
3339
+ failure$1(options, "Nothing changed. Run the same command with --json to inspect all retained evidence. Ask the coordinator operator to use the administrator review API for evidence exceeding this terminal's display limit.", 2);
3340
+ return false;
3341
+ }
3342
+ function reportReviewFailure(options, error) {
3343
+ if (options.json && error instanceof CoordinatorHistoryUnavailableError) {
3344
+ process.stdout.write(`${JSON.stringify({
3345
+ error: "owner_review_unavailable",
3346
+ message: reviewErrorMessage(error),
3347
+ unchanged: true
3348
+ })}\n`);
3349
+ process.exitCode = 1;
3350
+ return;
3351
+ }
3352
+ failure$1(options, reviewErrorMessage(error));
3353
+ }
3367
3354
  async function reviewDeviceOwner(group, options) {
3368
3355
  try {
3369
3356
  if (!validId$1(group)) return failure$1(options, "Use a valid coordinator group ID.", 2);
@@ -3392,10 +3379,10 @@ async function reviewDeviceOwner(group, options) {
3392
3379
  return;
3393
3380
  }
3394
3381
  if (preview.state !== "ready") return failure$1(options, `Coordinator review stopped: ${preview.reasons.join(", ")}. Nothing changed.`);
3395
- p.log.info(`Coordinator: ${preview.coordinator_id}; reviewed invitations: ${preview.reviewed_invite_count}`, { output: process.stderr });
3382
+ if (!showCoordinatorReview(preview, request, options)) return;
3396
3383
  await confirmReview(group, options, evidence, request, preview);
3397
3384
  } catch (error) {
3398
- failure$1(options, reviewErrorMessage(error));
3385
+ reportReviewFailure(options, error);
3399
3386
  }
3400
3387
  }
3401
3388
  function buildCoordinatorReviewDeviceOwnerCommand() {
@@ -6040,7 +6027,7 @@ function createInjectMemoryCommand() {
6040
6027
  const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
6041
6028
  console.log(pack.pack_text ?? "");
6042
6029
  } finally {
6043
- store.close();
6030
+ await store.closeAsync();
6044
6031
  }
6045
6032
  });
6046
6033
  return cmd;
@@ -7207,7 +7194,7 @@ async function withStore(opts, errorCode, run) {
7207
7194
  p.log.error(message);
7208
7195
  process.exitCode = usageError ? 2 : 1;
7209
7196
  } finally {
7210
- store?.close();
7197
+ await store?.closeAsync();
7211
7198
  }
7212
7199
  }
7213
7200
  function ledgerAutomaticContext(payload) {
@@ -7900,7 +7887,7 @@ async function buildLocalPack(context, project, dbPath) {
7900
7887
  packTokens: Number.isFinite(Number(pack.metrics?.pack_tokens)) ? Number(pack.metrics?.pack_tokens) : 0
7901
7888
  };
7902
7889
  } finally {
7903
- store.close();
7890
+ await store.closeAsync();
7904
7891
  }
7905
7892
  }
7906
7893
  function isRecord$1(value) {
@@ -8789,6 +8776,12 @@ function readViewerPidRecord(dbPath) {
8789
8776
  };
8790
8777
  return null;
8791
8778
  }
8779
+ function removeOwnedViewerPidFile(dbPath) {
8780
+ try {
8781
+ if (readViewerPidRecord(dbPath)?.pid !== process.pid) return;
8782
+ rmSync(pidFilePath(dbPath));
8783
+ } catch {}
8784
+ }
8792
8785
  function normalizeViewerHost(host) {
8793
8786
  const normalized = host.trim().toLowerCase();
8794
8787
  if (normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]") return "loopback";
@@ -9207,6 +9200,10 @@ function createRawEventTargetState(store) {
9207
9200
  stop
9208
9201
  };
9209
9202
  }
9203
+ function createOptionalOwnerRecovery(serverModule, options) {
9204
+ if (!("createAccountOwnerRecoveryService" in serverModule) || typeof serverModule.createAccountOwnerRecoveryService !== "function") return void 0;
9205
+ return serverModule.createAccountOwnerRecoveryService(options);
9206
+ }
9210
9207
  function createViewerAccountLink(store, invocation, serverModule) {
9211
9208
  const configPath = resolveCodememConfigPath(invocation.configPath ?? void 0).resolved.path;
9212
9209
  const keysDir = dirname(resolveKeyPaths(process.env.CODEMEM_KEYS_DIR?.trim() || void 0)[0]);
@@ -9222,15 +9219,31 @@ function createViewerAccountLink(store, invocation, serverModule) {
9222
9219
  keysDir
9223
9220
  });
9224
9221
  const registry = serverModule.createAccountLinkRegistry({ readContext });
9222
+ const ownerRecovery = createOptionalOwnerRecovery(serverModule, {
9223
+ readContext,
9224
+ registry,
9225
+ readAdminSecret: () => {
9226
+ const envSecret = process.env.CODEMEM_SYNC_COORDINATOR_ADMIN_SECRET?.trim();
9227
+ if (envSecret) return envSecret;
9228
+ const secret = readCodememConfigFileAtPath(configPath).sync_coordinator_admin_secret;
9229
+ return typeof secret === "string" ? secret.trim() : null;
9230
+ }
9231
+ });
9225
9232
  let actualBoundPort = null;
9226
9233
  return {
9227
9234
  registry,
9235
+ ownerRecovery,
9236
+ close: async () => {
9237
+ if (ownerRecovery) await ownerRecovery.close();
9238
+ await registry.close();
9239
+ },
9228
9240
  setBoundPort: (port) => {
9229
9241
  actualBoundPort = port;
9230
9242
  },
9231
9243
  options: {
9232
9244
  readContext,
9233
9245
  registry,
9246
+ ownerRecovery,
9234
9247
  getListenerAddress: () => actualBoundPort === null ? null : {
9235
9248
  host: invocation.host,
9236
9249
  port: actualBoundPort
@@ -9252,7 +9265,7 @@ function announceViewerStartup(options) {
9252
9265
  }
9253
9266
  async function startForegroundViewer(invocation) {
9254
9267
  const serverModule = await import("@codemem/server");
9255
- const { createApp, createSyncApp, closeStore, createViewerRawEventInbox, getStore } = serverModule;
9268
+ const { createApp, createSyncApp, closeStoreAsync, createViewerRawEventInbox, getStore } = serverModule;
9256
9269
  const createRawEventInbox = createViewerRawEventInbox;
9257
9270
  const coordinatorMaintenanceDependencies = createServeCoordinatorMaintenanceDependencies(serverModule);
9258
9271
  const { serve } = await import("@hono/node-server");
@@ -9274,7 +9287,7 @@ async function startForegroundViewer(invocation) {
9274
9287
  p.log.warn("sqlite-vec failed to load; retrying viewer startup with embeddings disabled");
9275
9288
  for (const line of sqliteVecFailureDiagnostics(err, resolveDbPath(invocation.dbPath ?? void 0))) p.log.warn(`sqlite-vec diagnostic: ${line}`);
9276
9289
  process.env.CODEMEM_EMBEDDING_DISABLED = "1";
9277
- closeStore();
9290
+ await closeStoreAsync();
9278
9291
  store = getStore();
9279
9292
  p.log.warn("Embeddings disabled for this viewer process; raw-event ingestion remains active.");
9280
9293
  }
@@ -9368,51 +9381,72 @@ async function startForegroundViewer(invocation) {
9368
9381
  server.on("error", (err) => {
9369
9382
  if (err.code === "EADDRINUSE") p.log.warn(`Viewer already running at http://${invocation.host}:${invocation.port}`);
9370
9383
  else p.log.error(err.message);
9371
- process.exit(1);
9384
+ requestShutdown(1);
9372
9385
  });
9373
- const shutdown = async () => {
9386
+ let shutdownPromise;
9387
+ let shutdownExitCode = 0;
9388
+ const shutdown = (exitCode = 0) => {
9389
+ shutdownExitCode = Math.max(shutdownExitCode, exitCode);
9390
+ shutdownPromise ??= finishShutdown();
9391
+ return shutdownPromise;
9392
+ };
9393
+ const finishShutdown = async () => {
9374
9394
  p.outro("shutting down");
9375
9395
  syncAbort.abort();
9376
- await accountLink.registry.close();
9377
- await stopMaintenanceWorkerProcess(maintenanceWorker, dbPath);
9378
- await new Promise((resolve) => {
9379
- let remaining = syncServer ? 2 : 1;
9380
- const done = () => {
9381
- if (--remaining === 0) resolve();
9382
- };
9383
- syncServer?.close(done);
9384
- server.close(done);
9385
- }).catch(() => {});
9386
- await rawEventProcessing.stop();
9387
9396
  try {
9388
- rmSync(pidPath);
9389
- } catch {}
9390
- closeStore();
9391
- process.exit(0);
9392
- };
9393
- const forceShutdown = () => {
9394
- setTimeout(() => {
9395
- if (maintenanceWorker?.pid && isProcessRunning(maintenanceWorker.pid)) try {
9396
- process.kill(maintenanceWorker.pid, "SIGKILL");
9397
- } catch {}
9398
9397
  try {
9399
- rmSync(pidPath);
9400
- } catch {}
9398
+ await accountLink.close();
9399
+ await stopMaintenanceWorkerProcess(maintenanceWorker, dbPath);
9400
+ } finally {
9401
+ await closeViewerListeners(syncServer ? [syncServer, server] : [server]);
9402
+ await rawEventProcessing.stop();
9403
+ }
9404
+ } catch (error) {
9405
+ shutdownExitCode = 1;
9406
+ logViewerShutdownError(error);
9407
+ } finally {
9408
+ removeOwnedViewerPidFile(dbPath);
9401
9409
  try {
9402
- rmSync(maintenanceWorkerPidFilePath(dbPath));
9403
- } catch {}
9404
- closeStore();
9405
- process.exit(1);
9406
- }, 3e4).unref();
9410
+ await closeStoreAsync();
9411
+ } catch (error) {
9412
+ shutdownExitCode = 1;
9413
+ logViewerShutdownError(error);
9414
+ }
9415
+ }
9416
+ process.exit(shutdownExitCode);
9407
9417
  };
9408
- process.on("SIGINT", () => {
9409
- forceShutdown();
9410
- shutdown();
9411
- });
9412
- process.on("SIGTERM", () => {
9413
- forceShutdown();
9414
- shutdown();
9415
- });
9418
+ const requestShutdown = (exitCode = 0) => {
9419
+ if (!shutdownPromise) forceViewerShutdown(maintenanceWorker, dbPath);
9420
+ shutdown(exitCode).catch((error) => {
9421
+ logViewerShutdownError(error);
9422
+ });
9423
+ };
9424
+ process.on("SIGINT", () => requestShutdown());
9425
+ process.on("SIGTERM", () => requestShutdown());
9426
+ }
9427
+ function logViewerShutdownError(error) {
9428
+ p.log.error(error instanceof Error ? error.message : String(error));
9429
+ }
9430
+ function closeViewerListeners(listeners) {
9431
+ return new Promise((resolve) => {
9432
+ let remaining = listeners.length;
9433
+ const done = () => {
9434
+ if (--remaining === 0) resolve();
9435
+ };
9436
+ for (const listener of listeners) listener.close(done);
9437
+ }).catch(() => {});
9438
+ }
9439
+ function forceViewerShutdown(maintenanceWorker, dbPath) {
9440
+ setTimeout(() => {
9441
+ if (maintenanceWorker?.pid && isProcessRunning(maintenanceWorker.pid)) try {
9442
+ process.kill(maintenanceWorker.pid, "SIGKILL");
9443
+ } catch {}
9444
+ removeOwnedViewerPidFile(dbPath);
9445
+ try {
9446
+ rmSync(maintenanceWorkerPidFilePath(dbPath));
9447
+ } catch {}
9448
+ process.exit(1);
9449
+ }, 3e4).unref();
9416
9450
  }
9417
9451
  async function runServeInvocation(invocation, dependencies = {
9418
9452
  nativeRuntimeAllowsServeStart,