codemem 0.47.0-beta.3 → 0.47.0

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,87 @@ 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
+ let identity = decision.identity_id === null ? "none" : terminalIdentifier(decision.identity_id);
3294
+ if (decision.identity_evidence === "label") identity += " (guessed by another device)";
3295
+ yield {
3296
+ 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)}`,
3297
+ records: 0,
3298
+ decisions: 1
3299
+ };
3300
+ }
3301
+ }
3302
+ }
3303
+ /** A truncated summary must never authorize the terminal confirmation. */
3304
+ function formatControllerReviewHistory(preview, subject) {
3305
+ const lines = [
3306
+ `Proposed current owner Identity: ${terminalIdentifier(subject.identityId)}`,
3307
+ "Retained legacy sharing assignments (not ownership changes):",
3308
+ "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."
3309
+ ];
3310
+ let records = 0;
3311
+ let decisions = 0;
3312
+ let characters = lines.join("\n").length;
3313
+ let complete = true;
3314
+ for (const row of controllerHistoryRows(preview, subject)) {
3315
+ if (records + row.records > 20 || decisions + row.decisions > 40 || characters + row.text.length + 1 > 24e3) {
3316
+ complete = false;
3317
+ break;
3318
+ }
3319
+ lines.push(row.text);
3320
+ records += row.records;
3321
+ decisions += row.decisions;
3322
+ characters += row.text.length + 1;
3323
+ }
3324
+ if (preview.legacy_history.length === 0) lines.push("No relevant retained legacy assignments.");
3325
+ if (!complete) {
3326
+ const totalDecisions = preview.legacy_history.reduce((total, record) => total + record.manifest.device_decisions.length, 0);
3327
+ lines.push(`TRUNCATED: showing ${records} of ${preview.legacy_history.length} records and ${decisions} of ${totalDecisions} device decisions. Terminal confirmation is disabled.`);
3328
+ }
3329
+ 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.");
3330
+ return {
3331
+ text: lines.join("\n"),
3332
+ complete
3333
+ };
3334
+ }
3335
+ function showCoordinatorReview(preview, request, options) {
3336
+ p.log.info(`Coordinator: ${preview.coordinator_id}; reviewed invitations: ${preview.reviewed_invite_count}`, { output: process.stderr });
3337
+ const history = formatControllerReviewHistory(preview, request);
3338
+ p.log.message(history.text, { output: process.stderr });
3339
+ if (history.complete) return true;
3340
+ 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);
3341
+ return false;
3342
+ }
3343
+ function reportReviewFailure(options, error) {
3344
+ if (options.json && error instanceof CoordinatorHistoryUnavailableError) {
3345
+ process.stdout.write(`${JSON.stringify({
3346
+ error: "owner_review_unavailable",
3347
+ message: reviewErrorMessage(error),
3348
+ unchanged: true
3349
+ })}\n`);
3350
+ process.exitCode = 1;
3351
+ return;
3352
+ }
3353
+ failure$1(options, reviewErrorMessage(error));
3354
+ }
3236
3355
  async function reviewDeviceOwner(group, options) {
3237
3356
  try {
3238
3357
  if (!validId$1(group)) return failure$1(options, "Use a valid coordinator group ID.", 2);
@@ -3261,10 +3380,10 @@ async function reviewDeviceOwner(group, options) {
3261
3380
  return;
3262
3381
  }
3263
3382
  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 });
3383
+ if (!showCoordinatorReview(preview, request, options)) return;
3265
3384
  await confirmReview(group, options, evidence, request, preview);
3266
3385
  } catch (error) {
3267
- failure$1(options, reviewErrorMessage(error));
3386
+ reportReviewFailure(options, error);
3268
3387
  }
3269
3388
  }
3270
3389
  function buildCoordinatorReviewDeviceOwnerCommand() {
@@ -5909,7 +6028,7 @@ function createInjectMemoryCommand() {
5909
6028
  const pack = await store.buildMemoryPackAsync(context, limit, budget, filters);
5910
6029
  console.log(pack.pack_text ?? "");
5911
6030
  } finally {
5912
- store.close();
6031
+ await store.closeAsync();
5913
6032
  }
5914
6033
  });
5915
6034
  return cmd;
@@ -7076,7 +7195,7 @@ async function withStore(opts, errorCode, run) {
7076
7195
  p.log.error(message);
7077
7196
  process.exitCode = usageError ? 2 : 1;
7078
7197
  } finally {
7079
- store?.close();
7198
+ await store?.closeAsync();
7080
7199
  }
7081
7200
  }
7082
7201
  function ledgerAutomaticContext(payload) {
@@ -7769,7 +7888,7 @@ async function buildLocalPack(context, project, dbPath) {
7769
7888
  packTokens: Number.isFinite(Number(pack.metrics?.pack_tokens)) ? Number(pack.metrics?.pack_tokens) : 0
7770
7889
  };
7771
7890
  } finally {
7772
- store.close();
7891
+ await store.closeAsync();
7773
7892
  }
7774
7893
  }
7775
7894
  function isRecord$1(value) {
@@ -8658,6 +8777,12 @@ function readViewerPidRecord(dbPath) {
8658
8777
  };
8659
8778
  return null;
8660
8779
  }
8780
+ function removeOwnedViewerPidFile(dbPath) {
8781
+ try {
8782
+ if (readViewerPidRecord(dbPath)?.pid !== process.pid) return;
8783
+ rmSync(pidFilePath(dbPath));
8784
+ } catch {}
8785
+ }
8661
8786
  function normalizeViewerHost(host) {
8662
8787
  const normalized = host.trim().toLowerCase();
8663
8788
  if (normalized === "localhost" || normalized === "127.0.0.1" || normalized === "::1" || normalized === "[::1]") return "loopback";
@@ -9076,9 +9201,23 @@ function createRawEventTargetState(store) {
9076
9201
  stop
9077
9202
  };
9078
9203
  }
9204
+ function createOptionalOwnerRecovery(serverModule, options) {
9205
+ if (!("createAccountOwnerRecoveryService" in serverModule) || typeof serverModule.createAccountOwnerRecoveryService !== "function") return void 0;
9206
+ return serverModule.createAccountOwnerRecoveryService(options);
9207
+ }
9208
+ /** The People name Sharing shows for an Identity; absent when the local store has none. */
9209
+ function localIdentityName(store, identityId) {
9210
+ try {
9211
+ const row = store.db.prepare("SELECT display_name FROM actors WHERE actor_id = ? AND status <> 'deactivated'").get(identityId);
9212
+ return (typeof row?.display_name === "string" ? row.display_name.trim() : "") || void 0;
9213
+ } catch {
9214
+ return;
9215
+ }
9216
+ }
9079
9217
  function createViewerAccountLink(store, invocation, serverModule) {
9080
9218
  const configPath = resolveCodememConfigPath(invocation.configPath ?? void 0).resolved.path;
9081
9219
  const keysDir = dirname(resolveKeyPaths(process.env.CODEMEM_KEYS_DIR?.trim() || void 0)[0]);
9220
+ const identityName = (identityId) => localIdentityName(store, identityId);
9082
9221
  const readContext = serverModule.createAccountContextReader({
9083
9222
  getProfile: () => ({
9084
9223
  dbPath: store.dbPath,
@@ -9090,16 +9229,40 @@ function createViewerAccountLink(store, invocation, serverModule) {
9090
9229
  configPath,
9091
9230
  keysDir
9092
9231
  });
9093
- const registry = serverModule.createAccountLinkRegistry({ readContext });
9232
+ const registry = serverModule.createAccountLinkRegistry({
9233
+ readContext,
9234
+ rebind: serverModule.createAccountRebindRunner(store)
9235
+ });
9236
+ serverModule.resumeAccountRebind(store, {
9237
+ keysDir,
9238
+ configPath
9239
+ }).catch(() => {});
9240
+ const ownerRecovery = createOptionalOwnerRecovery(serverModule, {
9241
+ readContext,
9242
+ registry,
9243
+ readAdminSecret: () => {
9244
+ const envSecret = process.env.CODEMEM_SYNC_COORDINATOR_ADMIN_SECRET?.trim();
9245
+ if (envSecret) return envSecret;
9246
+ const secret = readCodememConfigFileAtPath(configPath).sync_coordinator_admin_secret;
9247
+ return typeof secret === "string" ? secret.trim() : null;
9248
+ }
9249
+ });
9094
9250
  let actualBoundPort = null;
9095
9251
  return {
9096
9252
  registry,
9253
+ ownerRecovery,
9254
+ close: async () => {
9255
+ if (ownerRecovery) await ownerRecovery.close();
9256
+ await registry.close();
9257
+ },
9097
9258
  setBoundPort: (port) => {
9098
9259
  actualBoundPort = port;
9099
9260
  },
9100
9261
  options: {
9101
9262
  readContext,
9102
9263
  registry,
9264
+ ownerRecovery,
9265
+ identityName,
9103
9266
  getListenerAddress: () => actualBoundPort === null ? null : {
9104
9267
  host: invocation.host,
9105
9268
  port: actualBoundPort
@@ -9121,7 +9284,7 @@ function announceViewerStartup(options) {
9121
9284
  }
9122
9285
  async function startForegroundViewer(invocation) {
9123
9286
  const serverModule = await import("@codemem/server");
9124
- const { createApp, createSyncApp, closeStore, createViewerRawEventInbox, getStore } = serverModule;
9287
+ const { createApp, createSyncApp, closeStoreAsync, createViewerRawEventInbox, getStore } = serverModule;
9125
9288
  const createRawEventInbox = createViewerRawEventInbox;
9126
9289
  const coordinatorMaintenanceDependencies = createServeCoordinatorMaintenanceDependencies(serverModule);
9127
9290
  const { serve } = await import("@hono/node-server");
@@ -9143,7 +9306,7 @@ async function startForegroundViewer(invocation) {
9143
9306
  p.log.warn("sqlite-vec failed to load; retrying viewer startup with embeddings disabled");
9144
9307
  for (const line of sqliteVecFailureDiagnostics(err, resolveDbPath(invocation.dbPath ?? void 0))) p.log.warn(`sqlite-vec diagnostic: ${line}`);
9145
9308
  process.env.CODEMEM_EMBEDDING_DISABLED = "1";
9146
- closeStore();
9309
+ await closeStoreAsync();
9147
9310
  store = getStore();
9148
9311
  p.log.warn("Embeddings disabled for this viewer process; raw-event ingestion remains active.");
9149
9312
  }
@@ -9237,51 +9400,72 @@ async function startForegroundViewer(invocation) {
9237
9400
  server.on("error", (err) => {
9238
9401
  if (err.code === "EADDRINUSE") p.log.warn(`Viewer already running at http://${invocation.host}:${invocation.port}`);
9239
9402
  else p.log.error(err.message);
9240
- process.exit(1);
9403
+ requestShutdown(1);
9241
9404
  });
9242
- const shutdown = async () => {
9405
+ let shutdownPromise;
9406
+ let shutdownExitCode = 0;
9407
+ const shutdown = (exitCode = 0) => {
9408
+ shutdownExitCode = Math.max(shutdownExitCode, exitCode);
9409
+ shutdownPromise ??= finishShutdown();
9410
+ return shutdownPromise;
9411
+ };
9412
+ const finishShutdown = async () => {
9243
9413
  p.outro("shutting down");
9244
9414
  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
9415
  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
9416
  try {
9268
- rmSync(pidPath);
9269
- } catch {}
9417
+ await accountLink.close();
9418
+ await stopMaintenanceWorkerProcess(maintenanceWorker, dbPath);
9419
+ } finally {
9420
+ await closeViewerListeners(syncServer ? [syncServer, server] : [server]);
9421
+ await rawEventProcessing.stop();
9422
+ }
9423
+ } catch (error) {
9424
+ shutdownExitCode = 1;
9425
+ logViewerShutdownError(error);
9426
+ } finally {
9427
+ removeOwnedViewerPidFile(dbPath);
9270
9428
  try {
9271
- rmSync(maintenanceWorkerPidFilePath(dbPath));
9272
- } catch {}
9273
- closeStore();
9274
- process.exit(1);
9275
- }, 3e4).unref();
9429
+ await closeStoreAsync();
9430
+ } catch (error) {
9431
+ shutdownExitCode = 1;
9432
+ logViewerShutdownError(error);
9433
+ }
9434
+ }
9435
+ process.exit(shutdownExitCode);
9276
9436
  };
9277
- process.on("SIGINT", () => {
9278
- forceShutdown();
9279
- shutdown();
9280
- });
9281
- process.on("SIGTERM", () => {
9282
- forceShutdown();
9283
- shutdown();
9284
- });
9437
+ const requestShutdown = (exitCode = 0) => {
9438
+ if (!shutdownPromise) forceViewerShutdown(maintenanceWorker, dbPath);
9439
+ shutdown(exitCode).catch((error) => {
9440
+ logViewerShutdownError(error);
9441
+ });
9442
+ };
9443
+ process.on("SIGINT", () => requestShutdown());
9444
+ process.on("SIGTERM", () => requestShutdown());
9445
+ }
9446
+ function logViewerShutdownError(error) {
9447
+ p.log.error(error instanceof Error ? error.message : String(error));
9448
+ }
9449
+ function closeViewerListeners(listeners) {
9450
+ return new Promise((resolve) => {
9451
+ let remaining = listeners.length;
9452
+ const done = () => {
9453
+ if (--remaining === 0) resolve();
9454
+ };
9455
+ for (const listener of listeners) listener.close(done);
9456
+ }).catch(() => {});
9457
+ }
9458
+ function forceViewerShutdown(maintenanceWorker, dbPath) {
9459
+ setTimeout(() => {
9460
+ if (maintenanceWorker?.pid && isProcessRunning(maintenanceWorker.pid)) try {
9461
+ process.kill(maintenanceWorker.pid, "SIGKILL");
9462
+ } catch {}
9463
+ removeOwnedViewerPidFile(dbPath);
9464
+ try {
9465
+ rmSync(maintenanceWorkerPidFilePath(dbPath));
9466
+ } catch {}
9467
+ process.exit(1);
9468
+ }, 3e4).unref();
9285
9469
  }
9286
9470
  async function runServeInvocation(invocation, dependencies = {
9287
9471
  nativeRuntimeAllowsServeStart,