codemem 0.47.0-beta.3 → 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,5 +1,5 @@
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, FILE_CONTEXT_TRANSPORT_MAX_BYTES, FILE_CONTEXT_TRANSPORT_PROFILE, 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, 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, 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
5
  import { appendFileSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
@@ -1596,7 +1596,7 @@ async function buildLocalPack$2(context, project, dbPath, workingSetPaths = [])
1596
1596
  packTokens: Number.isFinite(Number(pack.metrics?.pack_tokens)) ? Number(pack.metrics.pack_tokens) : 0
1597
1597
  };
1598
1598
  } finally {
1599
- store.close();
1599
+ await store.closeAsync();
1600
1600
  }
1601
1601
  }
1602
1602
  async function buildClaudeHookInjection(payload, opts, deps = {}) {
@@ -2237,7 +2237,7 @@ async function buildLocalPack$1(context, project, dbPath) {
2237
2237
  packTokens: Number.isFinite(Number(pack.metrics?.pack_tokens)) ? Number(pack.metrics?.pack_tokens) : 0
2238
2238
  };
2239
2239
  } finally {
2240
- store.close();
2240
+ await store.closeAsync();
2241
2241
  }
2242
2242
  }
2243
2243
  async function prepareCodexHookInjection(payload, opts, deps = {}) {
@@ -2499,21 +2499,34 @@ function hasDatabaseSelection(options) {
2499
2499
  return options.dbPath !== void 0 || options.db !== void 0;
2500
2500
  }
2501
2501
  function exportAccountSchema(options) {
2502
- const statements = options.deviceUnlink ? ACCOUNT_UNLINK_MIGRATION_STATEMENTS : ACCOUNT_CONNECTION_MIGRATION_STATEMENTS;
2503
- 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`;
2504
2509
  process.stdout.write(options.json ? `${JSON.stringify({
2505
2510
  statements,
2506
2511
  applied: false
2507
2512
  })}\n` : text);
2508
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
+ }
2509
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
+ }
2510
2524
  if (options.deviceUnlink && !options.exportSql) {
2511
2525
  failure$4(options, "Use --device-unlink only with --export-sql; local upgrades detect the installed schema.", 2, "account_schema_upgrade_failed");
2512
2526
  return;
2513
2527
  }
2514
2528
  const path = resolveDbOpt(options);
2515
- const selected = hasDatabaseSelection(options);
2516
- if (options.exportSql && selected || !options.exportSql && !path) {
2529
+ if (invalidAccountSelection(options, path)) {
2517
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");
2518
2531
  return;
2519
2532
  }
@@ -2524,14 +2537,23 @@ async function upgradeAccount(options) {
2524
2537
  }
2525
2538
  if (!path) return;
2526
2539
  if (!options.json) process.stderr.write(`${accountPrerequisites}\n`);
2527
- await upgradeCoordinatorAccountSchemaFile(path);
2528
- 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));
2529
2543
  } catch (error) {
2530
- 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");
2531
2545
  }
2532
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
+ }
2533
2555
  function buildCoordinatorAccountSchemaCommand() {
2534
- 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`);
2535
2557
  addDbOption(command);
2536
2558
  addJsonOption(command);
2537
2559
  command.action(upgradeAccount);
@@ -3064,7 +3086,8 @@ var REVIEW_REASONS = [
3064
3086
  "key_mismatch",
3065
3087
  "enrollment_identity_mismatch",
3066
3088
  "invite_identity_mismatch",
3067
- "invite_evidence_invalid"
3089
+ "invite_evidence_invalid",
3090
+ "legacy_history_invalid"
3068
3091
  ];
3069
3092
  function validId$1(value) {
3070
3093
  return typeof value === "string" && value.length > 0 && value.length <= 256 && value.trim() === value && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(value);
@@ -3073,6 +3096,7 @@ function isHash(value) {
3073
3096
  return typeof value === "string" && value.length === 64 && /^[a-f0-9]{64}$/.test(value);
3074
3097
  }
3075
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.";
3076
3100
  if (error instanceof RemoteCoordinatorRequestError) {
3077
3101
  if (error.code === "review_stale") return "Evidence changed since the preview. Nothing changed; run the review again.";
3078
3102
  if (error.code === "already_reviewed_or_needs_review") return "This device already has a reviewed owner, or needs a separate review. Nothing changed.";
@@ -3140,9 +3164,27 @@ function validInviteCount(reply, invites) {
3140
3164
  function validInviteRef(invite) {
3141
3165
  return !!invite && validId$1(invite.invite_id) && (invite.kind === "team_member" || invite.kind === "add_device");
3142
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
+ }
3143
3184
  function parsePreview(payload, request) {
3144
3185
  if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("invalid_preview");
3145
3186
  const reply = payload;
3187
+ if (!Array.isArray(reply.legacy_history)) throw new CoordinatorHistoryUnavailableError();
3146
3188
  const enrollment = reply.enrollment;
3147
3189
  const invites = reply.reviewed_invites;
3148
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");
@@ -3151,14 +3193,10 @@ function parsePreview(payload, request) {
3151
3193
  return {
3152
3194
  state: reply.state,
3153
3195
  coordinator_id: reply.coordinator_id,
3196
+ legacy_history: parsePreviewHistory(reply.legacy_history, reply.coordinator_id),
3154
3197
  reasons: reply.reasons,
3155
3198
  evidence_digest: reply.state === "ready" ? reply.evidence_digest : void 0,
3156
- enrollment: enrollment ? {
3157
- device_id: enrollment.device_id,
3158
- fingerprint: enrollment.fingerprint,
3159
- display_name: null,
3160
- identity_label: enrollment.identity_label
3161
- } : null,
3199
+ enrollment: previewEnrollment(enrollment),
3162
3200
  reviewed_invites: invites.map((invite) => ({
3163
3201
  invite_id: invite.invite_id,
3164
3202
  kind: invite.kind
@@ -3233,6 +3271,86 @@ function emitReviewPreview(evidence, preview) {
3233
3271
  ...stopped
3234
3272
  })}\n`);
3235
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
+ }
3236
3354
  async function reviewDeviceOwner(group, options) {
3237
3355
  try {
3238
3356
  if (!validId$1(group)) return failure$1(options, "Use a valid coordinator group ID.", 2);
@@ -3261,10 +3379,10 @@ async function reviewDeviceOwner(group, options) {
3261
3379
  return;
3262
3380
  }
3263
3381
  if (preview.state !== "ready") return failure$1(options, `Coordinator review stopped: ${preview.reasons.join(", ")}. Nothing changed.`);
3264
- p.log.info(`Coordinator: ${preview.coordinator_id}; reviewed invitations: ${preview.reviewed_invite_count}`, { output: process.stderr });
3382
+ if (!showCoordinatorReview(preview, request, options)) return;
3265
3383
  await confirmReview(group, options, evidence, request, preview);
3266
3384
  } catch (error) {
3267
- failure$1(options, reviewErrorMessage(error));
3385
+ reportReviewFailure(options, error);
3268
3386
  }
3269
3387
  }
3270
3388
  function buildCoordinatorReviewDeviceOwnerCommand() {
@@ -5909,7 +6027,7 @@ function createInjectMemoryCommand() {
5909
6027
  const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
5910
6028
  console.log(pack.pack_text ?? "");
5911
6029
  } finally {
5912
- store.close();
6030
+ await store.closeAsync();
5913
6031
  }
5914
6032
  });
5915
6033
  return cmd;
@@ -7076,7 +7194,7 @@ async function withStore(opts, errorCode, run) {
7076
7194
  p.log.error(message);
7077
7195
  process.exitCode = usageError ? 2 : 1;
7078
7196
  } finally {
7079
- store?.close();
7197
+ await store?.closeAsync();
7080
7198
  }
7081
7199
  }
7082
7200
  function ledgerAutomaticContext(payload) {
@@ -7769,7 +7887,7 @@ async function buildLocalPack(context, project, dbPath) {
7769
7887
  packTokens: Number.isFinite(Number(pack.metrics?.pack_tokens)) ? Number(pack.metrics?.pack_tokens) : 0
7770
7888
  };
7771
7889
  } finally {
7772
- store.close();
7890
+ await store.closeAsync();
7773
7891
  }
7774
7892
  }
7775
7893
  function isRecord$1(value) {
@@ -8658,6 +8776,12 @@ function readViewerPidRecord(dbPath) {
8658
8776
  };
8659
8777
  return null;
8660
8778
  }
8779
+ function removeOwnedViewerPidFile(dbPath) {
8780
+ try {
8781
+ if (readViewerPidRecord(dbPath)?.pid !== process.pid) return;
8782
+ rmSync(pidFilePath(dbPath));
8783
+ } catch {}
8784
+ }
8661
8785
  function normalizeViewerHost(host) {
8662
8786
  const normalized = host.trim().toLowerCase();
8663
8787
  if (normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]") return "loopback";
@@ -9076,6 +9200,10 @@ function createRawEventTargetState(store) {
9076
9200
  stop
9077
9201
  };
9078
9202
  }
9203
+ function createOptionalOwnerRecovery(serverModule, options) {
9204
+ if (!("createAccountOwnerRecoveryService" in serverModule) || typeof serverModule.createAccountOwnerRecoveryService !== "function") return void 0;
9205
+ return serverModule.createAccountOwnerRecoveryService(options);
9206
+ }
9079
9207
  function createViewerAccountLink(store, invocation, serverModule) {
9080
9208
  const configPath = resolveCodememConfigPath(invocation.configPath ?? void 0).resolved.path;
9081
9209
  const keysDir = dirname(resolveKeyPaths(process.env.CODEMEM_KEYS_DIR?.trim() || void 0)[0]);
@@ -9091,15 +9219,31 @@ function createViewerAccountLink(store, invocation, serverModule) {
9091
9219
  keysDir
9092
9220
  });
9093
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
+ });
9094
9232
  let actualBoundPort = null;
9095
9233
  return {
9096
9234
  registry,
9235
+ ownerRecovery,
9236
+ close: async () => {
9237
+ if (ownerRecovery) await ownerRecovery.close();
9238
+ await registry.close();
9239
+ },
9097
9240
  setBoundPort: (port) => {
9098
9241
  actualBoundPort = port;
9099
9242
  },
9100
9243
  options: {
9101
9244
  readContext,
9102
9245
  registry,
9246
+ ownerRecovery,
9103
9247
  getListenerAddress: () => actualBoundPort === null ? null : {
9104
9248
  host: invocation.host,
9105
9249
  port: actualBoundPort
@@ -9121,7 +9265,7 @@ function announceViewerStartup(options) {
9121
9265
  }
9122
9266
  async function startForegroundViewer(invocation) {
9123
9267
  const serverModule = await import("@codemem/server");
9124
- const { createApp, createSyncApp, closeStore, createViewerRawEventInbox, getStore } = serverModule;
9268
+ const { createApp, createSyncApp, closeStoreAsync, createViewerRawEventInbox, getStore } = serverModule;
9125
9269
  const createRawEventInbox = createViewerRawEventInbox;
9126
9270
  const coordinatorMaintenanceDependencies = createServeCoordinatorMaintenanceDependencies(serverModule);
9127
9271
  const { serve } = await import("@hono/node-server");
@@ -9143,7 +9287,7 @@ async function startForegroundViewer(invocation) {
9143
9287
  p.log.warn("sqlite-vec failed to load; retrying viewer startup with embeddings disabled");
9144
9288
  for (const line of sqliteVecFailureDiagnostics(err, resolveDbPath(invocation.dbPath ?? void 0))) p.log.warn(`sqlite-vec diagnostic: ${line}`);
9145
9289
  process.env.CODEMEM_EMBEDDING_DISABLED = "1";
9146
- closeStore();
9290
+ await closeStoreAsync();
9147
9291
  store = getStore();
9148
9292
  p.log.warn("Embeddings disabled for this viewer process; raw-event ingestion remains active.");
9149
9293
  }
@@ -9237,51 +9381,72 @@ async function startForegroundViewer(invocation) {
9237
9381
  server.on("error", (err) => {
9238
9382
  if (err.code === "EADDRINUSE") p.log.warn(`Viewer already running at http://${invocation.host}:${invocation.port}`);
9239
9383
  else p.log.error(err.message);
9240
- process.exit(1);
9384
+ requestShutdown(1);
9241
9385
  });
9242
- 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 () => {
9243
9394
  p.outro("shutting down");
9244
9395
  syncAbort.abort();
9245
- await accountLink.registry.close();
9246
- await stopMaintenanceWorkerProcess(maintenanceWorker, dbPath);
9247
- await new Promise((resolve) => {
9248
- let remaining = syncServer ? 2 : 1;
9249
- const done = () => {
9250
- if (--remaining === 0) resolve();
9251
- };
9252
- syncServer?.close(done);
9253
- server.close(done);
9254
- }).catch(() => {});
9255
- await rawEventProcessing.stop();
9256
9396
  try {
9257
- rmSync(pidPath);
9258
- } catch {}
9259
- closeStore();
9260
- process.exit(0);
9261
- };
9262
- const forceShutdown = () => {
9263
- setTimeout(() => {
9264
- if (maintenanceWorker?.pid && isProcessRunning(maintenanceWorker.pid)) try {
9265
- process.kill(maintenanceWorker.pid, "SIGKILL");
9266
- } catch {}
9267
9397
  try {
9268
- rmSync(pidPath);
9269
- } 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);
9270
9409
  try {
9271
- rmSync(maintenanceWorkerPidFilePath(dbPath));
9272
- } catch {}
9273
- closeStore();
9274
- process.exit(1);
9275
- }, 3e4).unref();
9410
+ await closeStoreAsync();
9411
+ } catch (error) {
9412
+ shutdownExitCode = 1;
9413
+ logViewerShutdownError(error);
9414
+ }
9415
+ }
9416
+ process.exit(shutdownExitCode);
9276
9417
  };
9277
- process.on("SIGINT", () => {
9278
- forceShutdown();
9279
- shutdown();
9280
- });
9281
- process.on("SIGTERM", () => {
9282
- forceShutdown();
9283
- shutdown();
9284
- });
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();
9285
9450
  }
9286
9451
  async function runServeInvocation(invocation, dependencies = {
9287
9452
  nativeRuntimeAllowsServeStart,