@codemem/server 0.45.0 → 0.46.0-alpha.1

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,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
3
- import { ACCESS_CLEANUP_OP_TYPE, CODEMEM_CONFIG_ENV_OVERRIDES, CoordinatorReciprocalApprovalRequestChangedError, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, LOCAL_SYNC_FEATURES, LegacyTeamSetupAdditiveConvergenceError, LegacyTeamSetupRosterCapacityError, MEMORY_FILTER_FIELD_TYPES, MEMORY_FILTER_NAMES, MEMORY_KIND_DESCRIPTIONS, MemoryStore, ObserverClient, PROMPT_TRANSPORT_PROTOCOL_RANGE, REMEMBER_MEMORY_KINDS, RawEventIngestValidationError, RecipientPolicyEdgeRequestError, RecipientPolicyTeamRenameError, RemoteCoordinatorRequestError, SCOPE_MEMBERSHIP_REVOCATION_LIMITATION, SYNC_AUTHORIZATION_REFRESH_HEADER, SYNC_CAPABILITY_HEADER, SYNC_FEATURES_HEADER, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, VIEWER_IDENTITY_TARGET_KEYS, VIEWER_SERVICE_DISCRIMINATOR, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyLegacyTeamSetupCompletionManifest, applyLegacyTeamSetupCompletionManifestAndReturnActivation, applyReplicationOps, areLegacyTeamSetupCompletionPolicyFactsAdditivelyCompatible, automaticRecallHealth, buildAuthHeaders, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildFilterClausesWithContext, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildViewerIdentityTarget, canonicalCoordinatorLegacyTeamCompletionManifestJson, canonicalMemoryKind, canonicalWorkspaceIdentity, claimRecipientPolicyActorMutations, claimRecipientPolicyPublicationMutation, cleanupNonces, clearLegacyTeamSetupDeviceDecision, clonePromptPackAttempt, coerceObserverCommand, commitDeviceIdentityBindings, commitRecipientPolicyEdges, containLegacyTeamSetupCompletionConflict, coordinatorArchiveGroupAction, coordinatorCreateAddDeviceInviteAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateLegacyTeamCompletionAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGetLegacyTeamCompletionAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListConsumedTeamInvitesAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListLegacyTeamCompletionsAction, coordinatorListReviewedRecipientInviteEvidenceAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, countShareableProjectMemories, createCoordinatorReciprocalApproval, decodeInvitePayload, dedupeOrderedIds, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, deriveLegacyTeamSetupCompletionManifest, detectInstallKind, deterministicPolicyTeamId, diagnoseStalePeerReceivedRows, discoverLegacyTeamCandidates, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, friendlyDeviceName, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getLegacyTeamSetupDraft, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, getUpdateStatus, ingestRawEvents, inspectFreshLegacyTeamSetupActivation, inspectLegacyTeamSetupActivation, inviteTokenDigest, isAutomaticRecallMeasurement, isLegacyTeamCandidateSelectable, isLegacyTeamSetupProjectMappingIdentity, isPeerTrustBindingCompatible, isProjectSyncEnablementError, isScopedSyncCapability, judgeDistillReport, legacyTeamCandidateId, legacyTeamCandidateProjectInventory, legacyTeamCanonicalProjectRef, legacyTeamDeviceRef, legacyTeamResolvedProjectRef, legacyTeamSetupApiErrorCode, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listDeviceIdentityInventory, listInboundScopeRejections, listLegacyRecipientPolicyProjections, listMaintenanceJobs, listObserverProviderOptions, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listRecipientPolicyIntent, listRecipientPolicyReview, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, memoryFilterValueMatchesType, mergeAddresses, migrateRecipientPolicyIntent, negotiateSyncCapability, normalizeAddress, normalizeHumanPresentationName, normalizeSyncCapability, normalizeSyncFeatures, normalizeTeammateName, parseAcceptedProjectIntent, parsePositiveMemoryId, parseReassignScopePayload, parseRecipientPolicyEdgeCommitRequest, parseStrictInteger, parseSyncScopeRequest, persistShareOperation, personalScopeGrantStatusForPeer, planShareOperation, previewDeviceIdentityBindings, previewLegacyTeamSetupActivation, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectMatchesFilter, projectShareLifecycle, promptPackArtifactFingerprint, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientPolicyDigest, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, reconstructLegacyTeamSetupCompletionManifest, recordAutomaticRecall, recordHighestObservedDirectSignatureVersion, recordNonce, recordPromptPackArtifacts, recordPromptPackTerminal, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, refreshLegacyTeamCandidate, rejectInboundScopeFailures, renameRecipientPolicyTeam, replayLegacyTeamSetupActivation, requestJson, requireLegacyTeamSetupAccessDeltaWithinLimit, resolveDbPath, resolveProject, resolveProjectRoot, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, runSyncPass, schema, serializeRecipientPolicyCoordinatorGroupMutation, serializeRecipientPolicyPublicationMutation, serializeRecipientPolicyTeamMutation, setLegacyTeamSetupDeviceAssignment, setLegacyTeamSetupDeviceDecision, setLegacyTeamSetupProjectMapping, storeVectors, summarizeInboundScopeRejections, summaryLikeSqlPredicate, supportsSyncFeature, syncScopeResetRequiredPayload, toJson, tryUpdateRetrievalDelivery, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, validateLegacyTeamSetupCompletionManifest, validateLegacyTeamSetupCompletionManifestBinding, verifyDirectPeerSignature, verifyRecipientReviewedIntent, verifySignature, writeCodememConfigFile } from "@codemem/core";
3
+ import { ACCESS_CLEANUP_OP_TYPE, CODEMEM_CONFIG_ENV_OVERRIDES, CodememConfigMutationError, CoordinatorReciprocalApprovalRequestChangedError, DEFAULT_TIME_WINDOW_S, LEGACY_SHARED_REVIEW_SCOPE_ID, LOCAL_DEFAULT_SCOPE_ID, LOCAL_SYNC_CAPABILITY, LOCAL_SYNC_FEATURES, LegacyTeamSetupAdditiveConvergenceError, LegacyTeamSetupRosterCapacityError, MEMORY_FILTER_FIELD_TYPES, MEMORY_FILTER_NAMES, MEMORY_KIND_DESCRIPTIONS, MemoryStore, ObserverClient, PROMPT_TRANSPORT_PROTOCOL_RANGE, REMEMBER_MEMORY_KINDS, RawEventIngestValidationError, RecipientPolicyEdgeRequestError, RecipientPolicyTeamRenameError, RemoteCoordinatorRequestError, SCOPE_MEMBERSHIP_REVOCATION_LIMITATION, SYNC_AUTHORIZATION_REFRESH_HEADER, SYNC_CAPABILITY_HEADER, SYNC_FEATURES_HEADER, SYNC_SCOPE_QUERY_PARAM, VERSION, VERSION as VERSION$1, VIEWER_IDENTITY_TARGET_KEYS, VIEWER_SERVICE_DISCRIMINATOR, addSyncScopeToBoundary, analyzeProjectScopeMappingChangeGuardrails, applyLegacyTeamSetupCompletionManifest, applyLegacyTeamSetupCompletionManifestAndReturnActivation, applyReplicationOps, areLegacyTeamSetupCompletionPolicyFactsAdditivelyCompatible, automaticRecallHealth, buildAuthHeaders, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildFilterClausesWithContext, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildViewerIdentityTarget, canonicalCoordinatorLegacyTeamCompletionManifestJson, canonicalMemoryKind, canonicalWorkspaceIdentity, claimRecipientPolicyActorMutations, claimRecipientPolicyPublicationMutation, cleanupNonces, clearLegacyTeamSetupDeviceDecision, clonePromptPackAttempt, coerceObserverCommand, commitDeviceIdentityBindings, commitRecipientPolicyEdges, containLegacyTeamSetupCompletionConflict, coordinatorArchiveGroupAction, coordinatorCreateAddDeviceInviteAction, coordinatorCreateGroupAction, coordinatorCreateInviteAction, coordinatorCreateLegacyTeamCompletionAction, coordinatorCreateScopeAction, coordinatorDisableDeviceAction, coordinatorEnableDeviceAction, coordinatorGetLegacyTeamCompletionAction, coordinatorGrantScopeMembershipAction, coordinatorImportInviteAction, coordinatorListBootstrapGrantsAction, coordinatorListConsumedTeamInvitesAction, coordinatorListDevicesAction, coordinatorListGroupsAction, coordinatorListJoinRequestsAction, coordinatorListLegacyTeamCompletionsAction, coordinatorListReviewedRecipientInviteEvidenceAction, coordinatorListScopeMembershipsAction, coordinatorListScopesAction, coordinatorRemoveDeviceAction, coordinatorRenameDeviceAction, coordinatorRenameGroupAction, coordinatorReviewJoinRequestAction, coordinatorRevokeBootstrapGrantAction, coordinatorRevokeScopeMembershipAction, coordinatorStatusSnapshot, coordinatorUnarchiveGroupAction, coordinatorUpdateScopeAction, countShareableProjectMemories, createCoordinatorReciprocalApproval, decodeInvitePayload, dedupeOrderedIds, defaultSpaceScopeIdForGroup, deleteProjectScopeSettingsMapping, deriveLegacyTeamSetupCompletionManifest, detectInstallKind, deterministicPolicyTeamId, diagnoseStalePeerReceivedRows, discoverLegacyTeamCandidates, ensureDeviceIdentity, executeShareProvisioning, extractInvitePayload, extractReplicationOps, filterReplicationOpsForSyncWithStatus, fingerprintPublicKey, formatHostPort, friendlyDeviceName, fromJson, getCodememConfigPath, getCodememEnvOverrides, getCoordinatorEnrollmentReconciliationIssueSummary, getCoordinatorGroupPreference, getLegacyTeamSetupDraft, getRecipientPolicyAuthorityState, getSemanticIndexDiagnostics, getSyncResetState, getUpdateStatus, ingestRawEvents, inspectFreshLegacyTeamSetupActivation, inspectLegacyTeamSetupActivation, inviteTokenDigest, isAutomaticRecallMeasurement, isLegacyTeamCandidateSelectable, isLegacyTeamSetupProjectMappingIdentity, isPeerTrustBindingCompatible, isProjectSyncEnablementError, isScopedSyncCapability, judgeDistillReport, legacyTeamCandidateId, legacyTeamCandidateProjectInventory, legacyTeamCanonicalProjectRef, legacyTeamDeviceRef, legacyTeamResolvedProjectRef, legacyTeamSetupApiErrorCode, listAuthorizedScopesForPeer, listCoordinatorJoinRequests, listDeviceIdentityInventory, listInboundScopeRejections, listLegacyRecipientPolicyProjections, listMaintenanceJobs, listObserverProviderOptions, listPerPeerScopeSyncState, listProjectScopeCandidates, listProjectScopeInventory, listProjectScopeSettingsMappings, listRecipientPolicyIntent, listRecipientPolicyReview, listSharingDomainSettingsScopes, loadMemorySnapshotPageForPeer, loadReplicationOpsForPeer, lookupCoordinatorPeers, memoryFilterValueMatchesType, mergeAddresses, migrateRecipientPolicyIntent, mutateCodememConfigFile, negotiateSyncCapability, normalizeAddress, normalizeHumanPresentationName, normalizeSyncCapability, normalizeSyncFeatures, normalizeTeammateName, parseAcceptedProjectIntent, parsePositiveMemoryId, parseReassignScopePayload, parseRecipientPolicyEdgeCommitRequest, parseStrictInteger, parseSyncScopeRequest, persistShareOperation, personalScopeGrantStatusForPeer, planShareOperation, previewDeviceIdentityBindings, previewLegacyTeamSetupActivation, previewRecipientPolicyEdges, previewRecipientPolicyOnboarding, previewRecipientPolicyOnboardingFromReviewedIntent, probeAvailableCredentials, projectMatchesFilter, projectShareLifecycle, promptPackArtifactFingerprint, readCodememConfigFile, readCoordinatorSyncConfig, reassignProjectScopeInventoryProject, recipientInviteAuthoritativeIdentityId, recipientPolicyDigest, recipientReviewedIntentDigest, reconcileCoordinatorEnrollmentSnapshot, reconcileRecipientPolicyProject, reconcileShareOperationAcceptance, reconstructLegacyTeamSetupCompletionManifest, recordAutomaticRecall, recordHighestObservedDirectSignatureVersion, recordNonce, recordPromptPackArtifacts, recordPromptPackTerminal, refreshAuthorizedCoordinatorPeerTrust, refreshConfiguredScopeMembershipCache, refreshLegacyTeamCandidate, rejectInboundScopeFailures, renameRecipientPolicyTeam, replayLegacyTeamSetupActivation, requestJson, requireLegacyTeamSetupAccessDeltaWithinLimit, resolveDbPath, resolveProject, resolveProjectRoot, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, runSyncPass, schema, serializeRecipientPolicyCoordinatorGroupMutation, serializeRecipientPolicyPublicationMutation, serializeRecipientPolicyTeamMutation, setLegacyTeamSetupDeviceAssignment, setLegacyTeamSetupDeviceDecision, setLegacyTeamSetupProjectMapping, storeVectors, summarizeInboundScopeRejections, summaryLikeSqlPredicate, supportsSyncFeature, syncScopeResetRequiredPayload, toJson, tryUpdateRetrievalDelivery, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMapping, validateLegacyTeamSetupCompletionManifest, validateLegacyTeamSetupCompletionManifestBinding, validateRawEvents, verifyDirectPeerSignature, verifyRecipientReviewedIntent, verifySignature } from "@codemem/core";
4
4
  import { serveStatic } from "@hono/node-server/serve-static";
5
5
  import { Hono } from "hono";
6
6
  import { createMiddleware } from "hono/factory";
@@ -9,6 +9,8 @@ import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID }
9
9
  import { and, count, desc, eq, inArray, max, ne } from "drizzle-orm";
10
10
  import { drizzle } from "drizzle-orm/better-sqlite3";
11
11
  import { homedir, hostname, networkInterfaces } from "node:os";
12
+ import * as z from "zod";
13
+ import { chmod, link, mkdir, open, readFile, readdir, rm, stat } from "node:fs/promises";
12
14
  //#region src/middleware.ts
13
15
  var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
14
16
  "127.0.0.1",
@@ -180,6 +182,32 @@ var AUTH_SOURCES = /* @__PURE__ */ new Set([
180
182
  "none"
181
183
  ]);
182
184
  var HOT_RELOAD_KEYS = /* @__PURE__ */ new Set(["raw_events_sweeper_interval_s"]);
185
+ var EXECUTABLE_ARGV_KEYS = /* @__PURE__ */ new Set([
186
+ "claude_command",
187
+ "codex_command",
188
+ "observer_auth_command"
189
+ ]);
190
+ var BOOLEAN_KEYS = /* @__PURE__ */ new Set([
191
+ "sync_enabled",
192
+ "sync_retention_enabled",
193
+ "sync_mdns",
194
+ "observer_tier_routing_enabled"
195
+ ]);
196
+ var STRING_KEYS = /* @__PURE__ */ new Set([
197
+ "observer_base_url",
198
+ "observer_model",
199
+ "observer_simple_model",
200
+ "observer_reasoning_effort",
201
+ "observer_reasoning_summary",
202
+ "observer_rich_model",
203
+ "observer_rich_reasoning_effort",
204
+ "observer_rich_reasoning_summary",
205
+ "observer_auth_file",
206
+ "sync_host",
207
+ "sync_coordinator_url",
208
+ "sync_coordinator_group"
209
+ ]);
210
+ var TEMPERATURE_KEYS = /* @__PURE__ */ new Set(["observer_simple_temperature", "observer_rich_temperature"]);
183
211
  var PROTECTED_WRITE_KEYS = /* @__PURE__ */ new Set([
184
212
  "claude_command",
185
213
  "codex_command",
@@ -322,14 +350,18 @@ function partitionViewerUpdates(updates, beforeConfig) {
322
350
  }
323
351
  function parsePositiveInt(value, allowZero = false) {
324
352
  if (typeof value === "boolean") return null;
325
- const parsed = typeof value === "number" ? value : typeof value === "string" && /^-?\d+$/.test(value.trim()) ? Number(value.trim()) : NaN;
353
+ let parsed = NaN;
354
+ if (typeof value === "number") parsed = value;
355
+ else if (typeof value === "string" && /^-?\d+$/.test(value.trim())) parsed = Number(value.trim());
326
356
  if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) return null;
327
357
  if (allowZero) return parsed >= 0 ? parsed : null;
328
358
  return parsed > 0 ? parsed : null;
329
359
  }
330
360
  function parseFiniteNumber(value, allowZero = true) {
331
361
  if (typeof value === "boolean") return null;
332
- const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim().length > 0 ? Number(value.trim()) : NaN;
362
+ let parsed = NaN;
363
+ if (typeof value === "number") parsed = value;
364
+ else if (typeof value === "string" && value.trim().length > 0) parsed = Number(value.trim());
333
365
  if (!Number.isFinite(parsed)) return null;
334
366
  if (allowZero) return parsed >= 0 ? parsed : null;
335
367
  return parsed > 0 ? parsed : null;
@@ -356,77 +388,76 @@ function asExecutableArgv(value) {
356
388
  }
357
389
  return argv;
358
390
  }
359
- function validateAndApplyUpdate(configData, key, value, providers) {
360
- if (value == null || value === "") {
361
- delete configData[key];
362
- return null;
363
- }
364
- if (key === "observer_provider") {
365
- if (typeof value !== "string") return "observer_provider must be string";
366
- const provider = value.trim().toLowerCase();
367
- const savedBaseUrl = configData.observer_base_url;
368
- const hasSavedBaseUrl = typeof savedBaseUrl === "string" && savedBaseUrl.trim().length > 0;
369
- if (!providers.has(provider) && !hasSavedBaseUrl) return "observer_provider must match a configured provider";
370
- configData[key] = provider;
371
- return null;
372
- }
373
- if (key === "observer_runtime") {
374
- if (typeof value !== "string") return "observer_runtime must be string";
375
- const runtime = value.trim().toLowerCase();
376
- if (!RUNTIMES.has(runtime)) return "observer_runtime must be one of: api_http, claude_sidecar, codex_sidecar";
377
- configData[key] = runtime;
378
- return null;
379
- }
380
- if (key === "observer_auth_source") {
381
- if (typeof value !== "string") return "observer_auth_source must be string";
382
- const source = value.trim().toLowerCase();
383
- if (!AUTH_SOURCES.has(source)) return "observer_auth_source must be one of: auto, env, file, command, none";
384
- configData[key] = source;
385
- return null;
386
- }
387
- if (key === "claude_command" || key === "codex_command" || key === "observer_auth_command") {
388
- const argv = asExecutableArgv(value);
389
- if (argv == null) return `${key} must be string array`;
390
- if (argv.length > 0) configData[key] = argv;
391
- else delete configData[key];
392
- return null;
393
- }
394
- if (key === "observer_headers") {
395
- const headers = asStringMap(value);
396
- if (headers == null) return "observer_headers must be object of string values";
397
- if (Object.keys(headers).length > 0) configData[key] = headers;
398
- else delete configData[key];
399
- return null;
400
- }
401
- if (key === "sync_enabled" || key === "sync_retention_enabled" || key === "sync_mdns") {
402
- if (typeof value !== "boolean") return `${key} must be boolean`;
403
- configData[key] = value;
404
- return null;
405
- }
406
- if (key === "observer_tier_routing_enabled") {
407
- if (typeof value !== "boolean") return `${key} must be boolean`;
408
- configData[key] = value;
409
- return null;
410
- }
411
- if (key === "observer_base_url" || key === "observer_model" || key === "observer_simple_model" || key === "observer_reasoning_effort" || key === "observer_reasoning_summary" || key === "observer_rich_model" || key === "observer_rich_reasoning_effort" || key === "observer_rich_reasoning_summary" || key === "observer_auth_file" || key === "sync_host" || key === "sync_coordinator_url" || key === "sync_coordinator_group") {
412
- if (typeof value !== "string") return `${key} must be string`;
413
- const trimmed = value.trim();
414
- if (!trimmed) delete configData[key];
415
- else configData[key] = trimmed;
416
- return null;
417
- }
418
- if (key === "observer_simple_temperature" || key === "observer_rich_temperature") {
419
- const parsed = parseFiniteNumber(value, true);
420
- if (parsed == null) return `${key} must be non-negative number`;
421
- configData[key] = parsed;
422
- return null;
423
- }
391
+ function applyObserverProvider(configData, value, providers) {
392
+ if (typeof value !== "string") return "observer_provider must be string";
393
+ const provider = value.trim().toLowerCase();
394
+ const savedBaseUrl = configData.observer_base_url;
395
+ const hasSavedBaseUrl = typeof savedBaseUrl === "string" && savedBaseUrl.trim().length > 0;
396
+ if (!providers.has(provider) && !hasSavedBaseUrl) return "observer_provider must match a configured provider";
397
+ configData.observer_provider = provider;
398
+ return null;
399
+ }
400
+ function applyNormalizedEnum(configData, key, value, allowed) {
401
+ if (typeof value !== "string") return `${key} must be string`;
402
+ const normalized = value.trim().toLowerCase();
403
+ if (!allowed.has(normalized)) return `${key} must be one of: ${key === "observer_runtime" ? "api_http, claude_sidecar, codex_sidecar" : "auto, env, file, command, none"}`;
404
+ configData[key] = normalized;
405
+ return null;
406
+ }
407
+ function applyExecutableArgv(configData, key, value) {
408
+ const argv = asExecutableArgv(value);
409
+ if (argv == null) return `${key} must be string array`;
410
+ if (argv.length > 0) configData[key] = argv;
411
+ else delete configData[key];
412
+ return null;
413
+ }
414
+ function applyHeaders(configData, value) {
415
+ const headers = asStringMap(value);
416
+ if (headers == null) return "observer_headers must be object of string values";
417
+ if (Object.keys(headers).length > 0) configData.observer_headers = headers;
418
+ else delete configData.observer_headers;
419
+ return null;
420
+ }
421
+ function applyBoolean(configData, key, value) {
422
+ if (typeof value !== "boolean") return `${key} must be boolean`;
423
+ configData[key] = value;
424
+ return null;
425
+ }
426
+ function applyString(configData, key, value) {
427
+ if (typeof value !== "string") return `${key} must be string`;
428
+ const trimmed = value.trim();
429
+ if (trimmed) configData[key] = trimmed;
430
+ else delete configData[key];
431
+ return null;
432
+ }
433
+ function applyTemperature(configData, key, value) {
434
+ const parsed = parseFiniteNumber(value, true);
435
+ if (parsed == null) return `${key} must be non-negative number`;
436
+ configData[key] = parsed;
437
+ return null;
438
+ }
439
+ function applyPositiveInteger(configData, key, value) {
424
440
  const allowZero = key === "observer_auth_cache_ttl_s";
425
441
  const parsed = parsePositiveInt(value, allowZero);
426
442
  if (parsed == null) return `${key} must be ${allowZero ? "non-negative int" : "int"}`;
427
443
  configData[key] = parsed;
428
444
  return null;
429
445
  }
446
+ function validateAndApplyUpdate(configData, key, value, providers) {
447
+ if (value == null || value === "") {
448
+ delete configData[key];
449
+ return null;
450
+ }
451
+ if (key === "observer_provider") return applyObserverProvider(configData, value, providers);
452
+ if (key === "observer_runtime") return applyNormalizedEnum(configData, key, value, RUNTIMES);
453
+ if (key === "observer_auth_source") return applyNormalizedEnum(configData, key, value, AUTH_SOURCES);
454
+ if (EXECUTABLE_ARGV_KEYS.has(key)) return applyExecutableArgv(configData, key, value);
455
+ if (key === "observer_headers") return applyHeaders(configData, value);
456
+ if (BOOLEAN_KEYS.has(key)) return applyBoolean(configData, key, value);
457
+ if (STRING_KEYS.has(key)) return applyString(configData, key, value);
458
+ if (TEMPERATURE_KEYS.has(key)) return applyTemperature(configData, key, value);
459
+ return applyPositiveInteger(configData, key, value);
460
+ }
430
461
  function applyRuntimeEffects(changedKeys, opts) {
431
462
  const applied = [];
432
463
  if (changedKeys.includes("raw_events_sweeper_interval_s")) {
@@ -439,6 +470,88 @@ function applyRuntimeEffects(changedKeys, opts) {
439
470
  }
440
471
  return applied;
441
472
  }
473
+ var ConfigUpdateError = class extends Error {
474
+ status;
475
+ constructor(message, status) {
476
+ super(message);
477
+ this.status = status;
478
+ }
479
+ };
480
+ async function parseConfigUpdates(c) {
481
+ let payload;
482
+ try {
483
+ payload = await c.req.json();
484
+ } catch {
485
+ throw new ConfigUpdateError("invalid json", 400);
486
+ }
487
+ if (payload == null || typeof payload !== "object" || Array.isArray(payload)) throw new ConfigUpdateError("payload must be an object", 400);
488
+ const config = payload.config;
489
+ if ("config" in payload && config != null && (typeof config !== "object" || Array.isArray(config))) throw new ConfigUpdateError("config must be an object", 400);
490
+ if (config != null && typeof config === "object" && !Array.isArray(config)) return config;
491
+ return payload;
492
+ }
493
+ function saveViewerConfig(updates, configPath, providers) {
494
+ let beforeConfig = {};
495
+ let nextConfig = {};
496
+ let touchedKeys = [];
497
+ const result = mutateCodememConfigFile((currentConfig) => {
498
+ beforeConfig = withoutRemovedConfigKeys(currentConfig);
499
+ const partitioned = partitionViewerUpdates(updates, beforeConfig);
500
+ if (partitioned.error) throw new ConfigUpdateError(partitioned.error, 403);
501
+ nextConfig = { ...beforeConfig };
502
+ touchedKeys = ALLOWED_KEYS.filter((key) => key in partitioned.allowedUpdates);
503
+ for (const key of touchedKeys) {
504
+ const error = validateAndApplyUpdate(nextConfig, key, partitioned.allowedUpdates[key], providers);
505
+ if (error) throw new ConfigUpdateError(error, 400);
506
+ }
507
+ return nextConfig;
508
+ }, configPath);
509
+ return {
510
+ beforeConfig,
511
+ nextConfig,
512
+ touchedKeys,
513
+ savedPath: result.path
514
+ };
515
+ }
516
+ function viewerConfigSavePayload(saved, opts) {
517
+ const { beforeConfig, nextConfig, touchedKeys, savedPath } = saved;
518
+ const beforeEffective = getEffectiveConfig(beforeConfig);
519
+ const afterEffective = getEffectiveConfig(nextConfig);
520
+ const savedChangedKeys = ALLOWED_KEYS.filter((key) => beforeConfig[key] !== nextConfig[key]);
521
+ const effectiveChangedKeys = ALLOWED_KEYS.filter((key) => !configValuesEqual(beforeEffective[key], afterEffective[key]));
522
+ const envOverrides = getCodememEnvOverrides();
523
+ const ignoredByEnvKeys = savedChangedKeys.filter((key) => !effectiveChangedKeys.includes(key) && key in envOverrides);
524
+ const runtimeChangedKeys = [.../* @__PURE__ */ new Set([
525
+ ...touchedKeys,
526
+ ...savedChangedKeys,
527
+ ...effectiveChangedKeys
528
+ ])];
529
+ return {
530
+ path: savedPath,
531
+ config: sanitizeConfigForResponse(nextConfig),
532
+ effective: sanitizeConfigForResponse(afterEffective),
533
+ protected_keys: [...PROTECTED_WRITE_KEYS].sort(),
534
+ effects: {
535
+ saved_keys: savedChangedKeys,
536
+ effective_keys: effectiveChangedKeys,
537
+ hot_reloaded_keys: applyRuntimeEffects(runtimeChangedKeys, opts),
538
+ restart_required_keys: effectiveChangedKeys.filter((key) => !HOT_RELOAD_KEYS.has(key) && !(key in envOverrides)),
539
+ ignored_by_env_keys: ignoredByEnvKeys,
540
+ warnings: ignoredByEnvKeys.map((key) => `${key} is currently controlled by ${envOverrides[key]}; saved config will not take effect until that override is removed.`)
541
+ }
542
+ };
543
+ }
544
+ async function handleConfigPost(c, opts) {
545
+ try {
546
+ const saved = saveViewerConfig(await parseConfigUpdates(c), getCodememConfigPath(), new Set(loadProviderOptions()));
547
+ return c.json(viewerConfigSavePayload(saved, opts));
548
+ } catch (error) {
549
+ if (error instanceof ConfigUpdateError) return c.json({ error: error.message }, error.status);
550
+ if (error instanceof CodememConfigMutationError) return c.json({ error: error.message }, 409);
551
+ const message = error instanceof Error ? error.message : "failed to write config";
552
+ return c.json({ error: message }, 500);
553
+ }
554
+ }
442
555
  function configRoutes(opts = {}) {
443
556
  const app = new Hono();
444
557
  app.get("/api/config", (c) => {
@@ -455,61 +568,7 @@ function configRoutes(opts = {}) {
455
568
  providers: loadProviderOptions()
456
569
  });
457
570
  });
458
- app.post("/api/config", async (c) => {
459
- let payload;
460
- try {
461
- payload = await c.req.json();
462
- } catch {
463
- return c.json({ error: "invalid json" }, 400);
464
- }
465
- if (payload == null || typeof payload !== "object" || Array.isArray(payload)) return c.json({ error: "payload must be an object" }, 400);
466
- if ("config" in payload && payload.config != null && (typeof payload.config !== "object" || Array.isArray(payload.config))) return c.json({ error: "config must be an object" }, 400);
467
- const updates = "config" in payload && payload.config != null && typeof payload.config === "object" && !Array.isArray(payload.config) ? payload.config : payload;
468
- const configPath = getCodememConfigPath();
469
- const beforeConfig = withoutRemovedConfigKeys(readCodememConfigFile());
470
- const { allowedUpdates, error: protectedKeyError } = partitionViewerUpdates(updates, beforeConfig);
471
- if (protectedKeyError) return c.json({ error: protectedKeyError }, 403);
472
- const beforeEffective = getEffectiveConfig(beforeConfig);
473
- const nextConfig = { ...beforeConfig };
474
- const providers = new Set(loadProviderOptions());
475
- const touchedKeys = ALLOWED_KEYS.filter((key) => key in allowedUpdates);
476
- for (const key of ALLOWED_KEYS) {
477
- if (!(key in allowedUpdates)) continue;
478
- const error = validateAndApplyUpdate(nextConfig, key, allowedUpdates[key], providers);
479
- if (error) return c.json({ error }, 400);
480
- }
481
- let savedPath;
482
- try {
483
- savedPath = writeCodememConfigFile(nextConfig, configPath);
484
- } catch {
485
- return c.json({ error: "failed to write config" }, 500);
486
- }
487
- const afterEffective = getEffectiveConfig(nextConfig);
488
- const savedChangedKeys = ALLOWED_KEYS.filter((key) => beforeConfig[key] !== nextConfig[key]);
489
- const effectiveChangedKeys = ALLOWED_KEYS.filter((key) => !configValuesEqual(beforeEffective[key], afterEffective[key]));
490
- const envOverrides = getCodememEnvOverrides();
491
- const ignoredByEnvKeys = savedChangedKeys.filter((key) => !effectiveChangedKeys.includes(key) && key in envOverrides);
492
- const hotReloadedKeys = applyRuntimeEffects([.../* @__PURE__ */ new Set([
493
- ...touchedKeys,
494
- ...savedChangedKeys,
495
- ...effectiveChangedKeys
496
- ])], opts);
497
- const restartRequiredKeys = effectiveChangedKeys.filter((key) => !HOT_RELOAD_KEYS.has(key) && !(key in envOverrides));
498
- return c.json({
499
- path: savedPath,
500
- config: sanitizeConfigForResponse(nextConfig),
501
- effective: sanitizeConfigForResponse(afterEffective),
502
- protected_keys: [...PROTECTED_WRITE_KEYS].sort(),
503
- effects: {
504
- saved_keys: savedChangedKeys,
505
- effective_keys: effectiveChangedKeys,
506
- hot_reloaded_keys: hotReloadedKeys,
507
- restart_required_keys: restartRequiredKeys,
508
- ignored_by_env_keys: ignoredByEnvKeys,
509
- warnings: ignoredByEnvKeys.map((key) => `${key} is currently controlled by ${envOverrides[key]}; saved config will not take effect until that override is removed.`)
510
- }
511
- });
512
- });
571
+ app.post("/api/config", (c) => handleConfigPost(c, opts));
513
572
  return app;
514
573
  }
515
574
  //#endregion
@@ -881,6 +940,12 @@ function databaseReachable(getStore) {
881
940
  }
882
941
  function healthRoutes(getStore) {
883
942
  const app = new Hono();
943
+ app.get("/api/viewer-status", (c) => {
944
+ const store = getStore();
945
+ store.db.pragma("schema_version", { simple: true });
946
+ c.header("Cache-Control", "no-store");
947
+ return c.json({ identity: { actor_id: store.actorId } });
948
+ });
884
949
  app.get("/api/health", (c) => {
885
950
  const reachable = databaseReachable(getStore);
886
951
  c.header("Cache-Control", "no-store");
@@ -2182,49 +2247,6 @@ var MAX_METADATA_FIELD_CHARS = 512;
2182
2247
  var MAX_WORKING_SET_FILES = 50;
2183
2248
  var MAX_WORKING_SET_PATH_CHARS = 512;
2184
2249
  var INVALID_JSON = Symbol("invalid-json");
2185
- var PACK_KEYS = /* @__PURE__ */ new Set([
2186
- "context",
2187
- "limit",
2188
- "token_budget",
2189
- "project",
2190
- "cwd",
2191
- "all_projects",
2192
- "working_set_files",
2193
- "compact",
2194
- "compact_detail_count",
2195
- "db_path",
2196
- "identity_target",
2197
- "attempt",
2198
- "automatic_context"
2199
- ]);
2200
- var LEDGER_KEYS = /* @__PURE__ */ new Set([
2201
- "automatic_recall",
2202
- "evaluation_key",
2203
- "action",
2204
- "attempt_id",
2205
- "started_at",
2206
- "source",
2207
- "stream_id",
2208
- "source_session_id",
2209
- "prompt_number",
2210
- "request_id",
2211
- "retrieval_status",
2212
- "delivery_status",
2213
- "failure_code",
2214
- "failure_stage",
2215
- "original_attempt_id",
2216
- "db_path",
2217
- "identity_target"
2218
- ]);
2219
- var ATTEMPT_KEYS = /* @__PURE__ */ new Set([
2220
- "attempt_id",
2221
- "started_at",
2222
- "source",
2223
- "stream_id",
2224
- "source_session_id",
2225
- "prompt_number",
2226
- "request_id"
2227
- ]);
2228
2250
  var FORBIDDEN_LEDGER_KEYS = /* @__PURE__ */ new Set([
2229
2251
  "body",
2230
2252
  "context",
@@ -2237,6 +2259,52 @@ var FORBIDDEN_LEDGER_KEYS = /* @__PURE__ */ new Set([
2237
2259
  "raw_prompt",
2238
2260
  "title"
2239
2261
  ]);
2262
+ function optionalMetadataString(field) {
2263
+ const error = `ledger metadata field ${field} is invalid`;
2264
+ return z.string({ error }).max(MAX_METADATA_FIELD_CHARS, { error }).nullish();
2265
+ }
2266
+ var attemptMetadataShape = {
2267
+ attempt_id: z.string({ error: "ledger metadata requires attempt_id" }).min(1, { error: "ledger metadata requires attempt_id" }).max(MAX_METADATA_FIELD_CHARS, { error: "ledger metadata field attempt_id is invalid" }),
2268
+ started_at: optionalMetadataString("started_at"),
2269
+ source: optionalMetadataString("source"),
2270
+ stream_id: optionalMetadataString("stream_id"),
2271
+ source_session_id: optionalMetadataString("source_session_id"),
2272
+ prompt_number: z.number({ error: "ledger metadata prompt_number must be a non-negative integer" }).int({ error: "ledger metadata prompt_number must be a non-negative integer" }).nonnegative({ error: "ledger metadata prompt_number must be a non-negative integer" }).nullish(),
2273
+ request_id: optionalMetadataString("request_id")
2274
+ };
2275
+ var attemptPayloadSchema = z.strictObject(attemptMetadataShape, { error: "attempt must be an object" });
2276
+ var ledgerPayloadSchema = z.strictObject({
2277
+ ...attemptMetadataShape,
2278
+ automatic_recall: z.unknown().optional(),
2279
+ evaluation_key: z.unknown().optional(),
2280
+ action: z.unknown().optional(),
2281
+ retrieval_status: z.unknown().optional(),
2282
+ delivery_status: z.unknown().optional(),
2283
+ failure_code: optionalMetadataString("failure_code"),
2284
+ failure_stage: optionalMetadataString("failure_stage"),
2285
+ original_attempt_id: optionalMetadataString("original_attempt_id"),
2286
+ db_path: z.unknown().optional(),
2287
+ identity_target: z.unknown().optional()
2288
+ });
2289
+ var automaticContextPayloadSchema = z.strictObject({
2290
+ source: z.string({ error: "automatic_context source is invalid" }).max(64, { error: "automatic_context source is invalid" }).trim().min(1, { error: "automatic_context source is invalid" }),
2291
+ host_session_id: z.string({ error: "automatic_context host_session_id is invalid" }).max(MAX_METADATA_FIELD_CHARS, { error: "automatic_context host_session_id is invalid" }).trim().min(1, { error: "automatic_context host_session_id is invalid" })
2292
+ }, { error: "automatic_context must be an object or null" });
2293
+ var packRequestPayloadSchema = z.strictObject({
2294
+ context: z.string({ error: "context required" }).trim().min(1, { error: "context required" }),
2295
+ limit: z.number({ error: "limit must be a positive int" }).int({ error: "limit must be a positive int" }).min(1, { error: "limit must be a positive int" }).nullish().transform((value) => value ?? 10),
2296
+ token_budget: z.number({ error: "token_budget must be a non-negative int" }).int({ error: "token_budget must be a non-negative int" }).nonnegative({ error: "token_budget must be a non-negative int" }).nullish().transform((value) => value ?? null),
2297
+ project: z.string({ error: "project must be a non-empty string" }).refine((value) => value.trim().length > 0, { error: "project must be a non-empty string" }).nullish(),
2298
+ cwd: z.string({ error: "cwd must be an absolute path" }).refine((value) => value.trim().length > 0, { error: "cwd must be an absolute path" }).nullish(),
2299
+ all_projects: z.boolean({ error: "all_projects must be a boolean" }).nullish(),
2300
+ working_set_files: z.array(z.string({ error: "working_set_files must be an array of repository-relative strings" }), { error: "working_set_files must be an array of repository-relative strings" }).max(MAX_WORKING_SET_FILES, { error: `working_set_files must contain at most ${MAX_WORKING_SET_FILES} entries` }).nullish(),
2301
+ compact: z.boolean({ error: "compact must be a boolean" }).nullish(),
2302
+ compact_detail_count: z.number({ error: "compact_detail_count must be a non-negative int" }).int({ error: "compact_detail_count must be a non-negative int" }).nonnegative({ error: "compact_detail_count must be a non-negative int" }).nullish(),
2303
+ db_path: z.unknown().optional(),
2304
+ identity_target: z.unknown().optional(),
2305
+ attempt: attemptPayloadSchema.nullable().optional(),
2306
+ automatic_context: automaticContextPayloadSchema.nullable().optional()
2307
+ });
2240
2308
  function invalidRequest(message) {
2241
2309
  return { error: {
2242
2310
  code: "invalid_request",
@@ -2258,6 +2326,10 @@ function viewerContractUnsupported() {
2258
2326
  function isRecord(value) {
2259
2327
  return value != null && typeof value === "object" && !Array.isArray(value);
2260
2328
  }
2329
+ function firstUnknownKey(value, shape) {
2330
+ if (!isRecord(value)) return null;
2331
+ return Object.keys(value).find((key) => !Object.hasOwn(shape, key)) ?? null;
2332
+ }
2261
2333
  function isAbsolutePath(value) {
2262
2334
  return posix.isAbsolute(value) || win32.isAbsolute(value);
2263
2335
  }
@@ -2268,14 +2340,15 @@ function normalizeWorkingSetPath(value) {
2268
2340
  if (!normalized || normalized === "." || normalized === ".." || normalized.startsWith("../")) return null;
2269
2341
  return normalized;
2270
2342
  }
2271
- function validateMetadataFields(payload, allowedKeys) {
2343
+ function validateMetadataEnvelope(payload, shape) {
2272
2344
  if (Buffer.byteLength(JSON.stringify(payload), "utf8") > MAX_LEDGER_PAYLOAD_BYTES) return "ledger metadata exceeds 16384 bytes";
2273
2345
  for (const key of Object.keys(payload)) {
2274
2346
  if (FORBIDDEN_LEDGER_KEYS.has(key)) return `ledger metadata rejects sensitive field: ${key}`;
2275
- if (!allowedKeys.has(key)) return `ledger metadata contains unsupported field: ${key}`;
2347
+ if (!Object.hasOwn(shape, key)) return `ledger metadata contains unsupported field: ${key}`;
2276
2348
  }
2277
- if (typeof payload.attempt_id !== "string" || payload.attempt_id.length === 0) return "ledger metadata requires attempt_id";
2278
- if (payload.prompt_number != null && (typeof payload.prompt_number !== "number" || !Number.isInteger(payload.prompt_number) || payload.prompt_number < 0)) return "ledger metadata prompt_number must be a non-negative integer";
2349
+ return null;
2350
+ }
2351
+ function validateMetadataPaths(payload) {
2279
2352
  for (const key of [
2280
2353
  "attempt_id",
2281
2354
  "started_at",
@@ -2288,7 +2361,6 @@ function validateMetadataFields(payload, allowedKeys) {
2288
2361
  "original_attempt_id"
2289
2362
  ]) {
2290
2363
  const field = payload[key];
2291
- if (field != null && (typeof field !== "string" || field.length > MAX_METADATA_FIELD_CHARS)) return `ledger metadata field ${key} is invalid`;
2292
2364
  if (typeof field === "string" && isAbsolutePath(field)) return `ledger metadata rejects absolute paths in field: ${key}`;
2293
2365
  }
2294
2366
  return null;
@@ -2307,19 +2379,18 @@ function attemptMetadata(payload) {
2307
2379
  }
2308
2380
  function validateAutomaticContext(value) {
2309
2381
  if (value === null) return null;
2310
- if (!isRecord(value)) return "automatic_context must be an object or null";
2311
- for (const key of Object.keys(value)) if (key !== "source" && key !== "host_session_id") return `automatic_context contains unsupported field: ${key}`;
2312
- const source = value.source;
2313
- const hostSessionId = value.host_session_id;
2314
- if (typeof source !== "string" || !source.trim() || source.length > 64) return "automatic_context source is invalid";
2315
- if (typeof hostSessionId !== "string" || !hostSessionId.trim() || hostSessionId.length > MAX_METADATA_FIELD_CHARS || isAbsolutePath(hostSessionId)) return "automatic_context host_session_id is invalid";
2382
+ if (isAbsolutePath(value.host_session_id)) return "automatic_context host_session_id is invalid";
2316
2383
  return {
2317
- source: source.trim(),
2318
- hostSessionId: hostSessionId.trim()
2384
+ source: value.source,
2385
+ hostSessionId: value.host_session_id
2319
2386
  };
2320
2387
  }
2321
2388
  function resolveRequestAutomaticContext(value, attempt) {
2322
- if (Object.hasOwn(value, "automatic_context")) return validateAutomaticContext(value.automatic_context);
2389
+ if (Object.hasOwn(value, "automatic_context")) {
2390
+ const automaticContext = value.automatic_context;
2391
+ if (automaticContext === void 0) return "automatic_context must be an object or null";
2392
+ return validateAutomaticContext(automaticContext);
2393
+ }
2323
2394
  if (!attempt) return void 0;
2324
2395
  const source = typeof attempt.source === "string" ? attempt.source.trim().toLowerCase() : "";
2325
2396
  if (source && source !== "opencode") return void 0;
@@ -2347,54 +2418,60 @@ function packRequestArgs(request) {
2347
2418
  request.automaticContext
2348
2419
  ];
2349
2420
  }
2350
- function validatePackRequest(value) {
2351
- if (!isRecord(value)) return "request body must be an object";
2352
- for (const key of Object.keys(value)) if (!PACK_KEYS.has(key)) return `request body contains unsupported field: ${key}`;
2353
- const context = typeof value.context === "string" ? value.context.trim() : "";
2354
- if (!context) return "context required";
2355
- const limit = value.limit ?? 10;
2356
- if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1) return "limit must be a positive int";
2357
- const tokenBudget = value.token_budget ?? null;
2358
- if (tokenBudget !== null && (typeof tokenBudget !== "number" || !Number.isInteger(tokenBudget) || tokenBudget < 0)) return "token_budget must be a non-negative int";
2359
- if (value.all_projects != null && typeof value.all_projects !== "boolean") return "all_projects must be a boolean";
2360
- if (value.project != null && (typeof value.project !== "string" || !value.project.trim())) return "project must be a non-empty string";
2361
- if (value.cwd != null && (typeof value.cwd !== "string" || !value.cwd.trim() || !isAbsolute(value.cwd))) return "cwd must be an absolute path";
2362
- if (value.working_set_files != null && !Array.isArray(value.working_set_files)) return "working_set_files must be an array of repository-relative strings";
2363
- const workingSetValues = value.working_set_files ?? [];
2364
- if (workingSetValues.length > MAX_WORKING_SET_FILES) return `working_set_files must contain at most ${MAX_WORKING_SET_FILES} entries`;
2365
- const workingSetFiles = [];
2366
- for (const entry of workingSetValues) {
2367
- if (typeof entry !== "string") return "working_set_files must be an array of repository-relative strings";
2368
- const normalized = normalizeWorkingSetPath(entry);
2421
+ function packSchemaError(error) {
2422
+ const issue = error.issues[0];
2423
+ if (!issue) return "request body must be an object";
2424
+ if (issue.code !== "unrecognized_keys") return issue.path.length === 0 ? "request body must be an object" : issue.message;
2425
+ const unexpectedKey = issue.keys[0] ?? "unknown";
2426
+ if (issue.path[0] === "attempt") return `ledger metadata contains unsupported field: ${unexpectedKey}`;
2427
+ if (issue.path[0] === "automatic_context") return `automatic_context contains unsupported field: ${unexpectedKey}`;
2428
+ return `request body contains unsupported field: ${unexpectedKey}`;
2429
+ }
2430
+ function normalizeWorkingSetFiles(values) {
2431
+ const files = [];
2432
+ for (const value of values) {
2433
+ const normalized = normalizeWorkingSetPath(value);
2369
2434
  if (!normalized) return "working_set_files contains an invalid repository-relative path";
2370
- if (!workingSetFiles.includes(normalized)) workingSetFiles.push(normalized);
2371
- }
2372
- if (value.compact != null && typeof value.compact !== "boolean") return "compact must be a boolean";
2373
- if (value.compact_detail_count != null && (typeof value.compact_detail_count !== "number" || !Number.isInteger(value.compact_detail_count) || value.compact_detail_count < 0)) return "compact_detail_count must be a non-negative int";
2374
- let attempt;
2375
- if (value.attempt != null) {
2376
- if (!isRecord(value.attempt)) return "attempt must be an object";
2377
- const attemptError = validateMetadataFields(value.attempt, ATTEMPT_KEYS);
2378
- if (attemptError) return attemptError;
2379
- attempt = value.attempt;
2380
- }
2381
- const automaticContext = resolveRequestAutomaticContext(value, attempt);
2382
- if (typeof automaticContext === "string") return automaticContext;
2435
+ if (!files.includes(normalized)) files.push(normalized);
2436
+ }
2437
+ return files;
2438
+ }
2439
+ function packFilters(request, workingSetFiles) {
2383
2440
  const filters = {};
2384
- if (value.all_projects !== true) {
2441
+ if (request.all_projects !== true) {
2385
2442
  const envProject = process.env.CODEMEM_PROJECT?.trim() || null;
2386
- const requestCwd = value.cwd;
2387
- const requestProject = value.project;
2388
- const project = resolveProject(requestCwd ?? process.cwd(), requestProject ?? (requestCwd == null ? envProject : void 0));
2443
+ const project = resolveProject(request.cwd ?? process.cwd(), request.project ?? (request.cwd == null ? envProject : void 0));
2389
2444
  if (project) filters.project = project;
2390
2445
  }
2391
2446
  if (workingSetFiles.length > 0) filters.working_set_paths = workingSetFiles;
2447
+ return filters;
2448
+ }
2449
+ function validatePackRequest(value) {
2450
+ const requestUnknownKey = firstUnknownKey(value, packRequestPayloadSchema.shape);
2451
+ if (requestUnknownKey) return `request body contains unsupported field: ${requestUnknownKey}`;
2452
+ if (isRecord(value) && isRecord(value.attempt)) {
2453
+ const attemptEnvelopeError = validateMetadataEnvelope(value.attempt, attemptPayloadSchema.shape);
2454
+ if (attemptEnvelopeError) return attemptEnvelopeError;
2455
+ }
2456
+ const parsed = packRequestPayloadSchema.safeParse(value);
2457
+ if (!parsed.success) return packSchemaError(parsed.error);
2458
+ const request = parsed.data;
2459
+ if (request.attempt) {
2460
+ const attemptPathError = validateMetadataPaths(request.attempt);
2461
+ if (attemptPathError) return attemptPathError;
2462
+ }
2463
+ if (request.cwd != null && !isAbsolute(request.cwd)) return "cwd must be an absolute path";
2464
+ const workingSetFiles = normalizeWorkingSetFiles(request.working_set_files ?? []);
2465
+ if (typeof workingSetFiles === "string") return workingSetFiles;
2466
+ const attempt = request.attempt ?? void 0;
2467
+ const automaticContext = resolveRequestAutomaticContext(request, attempt);
2468
+ if (typeof automaticContext === "string") return automaticContext;
2392
2469
  return {
2393
- context,
2394
- limit,
2395
- tokenBudget,
2396
- filters,
2397
- renderOptions: packRenderOptions(value),
2470
+ context: request.context,
2471
+ limit: request.limit,
2472
+ tokenBudget: request.token_budget,
2473
+ filters: packFilters(request, workingSetFiles),
2474
+ renderOptions: packRenderOptions(request),
2398
2475
  attempt,
2399
2476
  automaticContext
2400
2477
  };
@@ -2439,26 +2516,81 @@ function dispatchLedger(store, payload) {
2439
2516
  if (payload.retrieval_status !== "skipped" && payload.retrieval_status !== "failed" || typeof payload.failure_code !== "string" || !payload.failure_code || typeof payload.failure_stage !== "string" || !payload.failure_stage) return "record action requires retrieval_status, failure_code, and failure_stage";
2440
2517
  return recordPromptPackTerminal(store.db, metadata, payload.retrieval_status, payload.failure_code, payload.failure_stage);
2441
2518
  }
2519
+ function validateLedgerPayload(value) {
2520
+ if (!isRecord(value)) return {
2521
+ ok: false,
2522
+ status: 400,
2523
+ body: invalidRequest("request body must be an object")
2524
+ };
2525
+ const validationError = validateMetadataEnvelope(value, ledgerPayloadSchema.shape);
2526
+ if (validationError?.startsWith("ledger metadata contains unsupported field:")) return {
2527
+ ok: false,
2528
+ status: 409,
2529
+ body: viewerContractUnsupported()
2530
+ };
2531
+ if (validationError) return {
2532
+ ok: false,
2533
+ status: 400,
2534
+ body: invalidRequest(validationError)
2535
+ };
2536
+ const parsed = ledgerPayloadSchema.safeParse(value);
2537
+ if (!parsed.success) return {
2538
+ ok: false,
2539
+ status: 400,
2540
+ body: invalidRequest(parsed.error.issues[0]?.message ?? "request body must be an object")
2541
+ };
2542
+ const pathError = validateMetadataPaths(parsed.data);
2543
+ if (pathError) return {
2544
+ ok: false,
2545
+ status: 400,
2546
+ body: invalidRequest(pathError)
2547
+ };
2548
+ return {
2549
+ ok: true,
2550
+ payload: parsed.data
2551
+ };
2552
+ }
2553
+ async function handleLedgerRequest(c, getStore) {
2554
+ const body = await c.req.json().catch(() => INVALID_JSON);
2555
+ if (body === INVALID_JSON) return c.json(invalidRequest("invalid json body"), 400);
2556
+ const validated = validateLedgerPayload(body);
2557
+ if (!validated.ok) return c.json(validated.body, validated.status);
2558
+ try {
2559
+ const store = getStore();
2560
+ const target = validateViewerTarget(store, validated.payload, { requireCurrentIdentity: true });
2561
+ if (!target.ok) return c.json(target.body, target.status);
2562
+ const outcome = dispatchLedger(store, validated.payload);
2563
+ if (typeof outcome === "string") return c.json(invalidRequest(outcome), 400);
2564
+ if (outcome.ok) return c.json(outcome);
2565
+ return c.json(outcome, ledgerFailureStatus(outcome.reason));
2566
+ } catch {
2567
+ return c.json({ error: {
2568
+ code: "ledger_failed",
2569
+ message: "prompt-pack ledger operation failed"
2570
+ } }, 500);
2571
+ }
2572
+ }
2573
+ function handleProfileRequest(c, getStore) {
2574
+ try {
2575
+ const store = getStore();
2576
+ if (!store.hasCurrentIdentity()) return c.json(viewerIdentityMismatch(), 409);
2577
+ return c.json({
2578
+ service: "codemem-viewer",
2579
+ protocol_version: PROMPT_TRANSPORT_PROTOCOL_RANGE.protocolVersion,
2580
+ min_supported_protocol_version: PROMPT_TRANSPORT_PROTOCOL_RANGE.minSupportedProtocolVersion,
2581
+ db_path: resolve(store.dbPath),
2582
+ identity_target: currentIdentityTarget()
2583
+ });
2584
+ } catch {
2585
+ return c.json({ error: {
2586
+ code: "profile_failed",
2587
+ message: "viewer profile could not be read"
2588
+ } }, 500);
2589
+ }
2590
+ }
2442
2591
  function packTransportRoutes(getStore) {
2443
2592
  const app = new Hono();
2444
- app.get("/api/prompt-pack-profile", (c) => {
2445
- try {
2446
- const store = getStore();
2447
- if (!store.hasCurrentIdentity()) return c.json(viewerIdentityMismatch(), 409);
2448
- return c.json({
2449
- service: "codemem-viewer",
2450
- protocol_version: PROMPT_TRANSPORT_PROTOCOL_RANGE.protocolVersion,
2451
- min_supported_protocol_version: PROMPT_TRANSPORT_PROTOCOL_RANGE.minSupportedProtocolVersion,
2452
- db_path: resolve(store.dbPath),
2453
- identity_target: currentIdentityTarget()
2454
- });
2455
- } catch {
2456
- return c.json({ error: {
2457
- code: "profile_failed",
2458
- message: "viewer profile could not be read"
2459
- } }, 500);
2460
- }
2461
- });
2593
+ app.get("/api/prompt-pack-profile", (c) => handleProfileRequest(c, getStore));
2462
2594
  app.post("/api/pack", async (c) => {
2463
2595
  const parsed = await c.req.json().catch(() => INVALID_JSON);
2464
2596
  if (parsed === INVALID_JSON) return c.json(invalidRequest("invalid json body"), 400);
@@ -2496,100 +2628,17 @@ function packTransportRoutes(getStore) {
2496
2628
  } }, 500);
2497
2629
  }
2498
2630
  });
2499
- app.post("/api/prompt-pack-ledger", async (c) => {
2500
- const parsed = await c.req.json().catch(() => INVALID_JSON);
2501
- if (parsed === INVALID_JSON) return c.json(invalidRequest("invalid json body"), 400);
2502
- if (!isRecord(parsed)) return c.json(invalidRequest("request body must be an object"), 400);
2503
- const validationError = validateMetadataFields(parsed, LEDGER_KEYS);
2504
- if (validationError) {
2505
- if (validationError.startsWith("ledger metadata contains unsupported field:")) return c.json(viewerContractUnsupported(), 409);
2506
- return c.json(invalidRequest(validationError), 400);
2507
- }
2508
- try {
2509
- const store = getStore();
2510
- const target = validateViewerTarget(store, parsed, { requireCurrentIdentity: true });
2511
- if (!target.ok) return c.json(target.body, target.status);
2512
- const outcome = dispatchLedger(store, parsed);
2513
- if (typeof outcome === "string") return c.json(invalidRequest(outcome), 400);
2514
- if (outcome.ok) return c.json(outcome);
2515
- return c.json(outcome, ledgerFailureStatus(outcome.reason));
2516
- } catch {
2517
- return c.json({ error: {
2518
- code: "ledger_failed",
2519
- message: "prompt-pack ledger operation failed"
2520
- } }, 500);
2521
- }
2522
- });
2631
+ app.post("/api/prompt-pack-ledger", (c) => handleLedgerRequest(c, getStore));
2523
2632
  return app;
2524
2633
  }
2525
2634
  //#endregion
2526
- //#region src/routes/raw-events.ts
2527
- /**
2528
- * Raw events routes — GET & POST /api/raw-events, GET /api/raw-events/status,
2529
- * POST /api/claude-hooks, POST /api/codex-hooks, POST /api/pi-hooks.
2530
- */
2531
- var DEFAULT_MAX_RAW_EVENTS_BODY_BYTES = 1048576;
2532
- function configuredMaxRawEventsBodyBytes() {
2533
- const parsed = Number(process.env.CODEMEM_RAW_EVENTS_MAX_BODY_BYTES?.trim() ?? "");
2534
- return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_RAW_EVENTS_BODY_BYTES;
2535
- }
2536
- var MAX_RAW_EVENTS_BODY_BYTES = configuredMaxRawEventsBodyBytes();
2537
- function emptyTranscriptOutcomeCounts() {
2538
- return {
2539
- ok: 0,
2540
- not_provided: 0,
2541
- path_rejected: 0,
2542
- unreadable: 0,
2543
- no_complete_record: 0,
2544
- no_assistant_record: 0
2545
- };
2546
- }
2547
- function createTranscriptDiagnostics() {
2548
- const counts = {
2549
- claude: emptyTranscriptOutcomeCounts(),
2550
- codex: emptyTranscriptOutcomeCounts()
2551
- };
2552
- return {
2553
- record(source, outcome) {
2554
- counts[source][outcome] += 1;
2555
- },
2556
- snapshot() {
2557
- return {
2558
- scope: "legacy_compatibility_routes",
2559
- counts: {
2560
- claude: { ...counts.claude },
2561
- codex: { ...counts.codex }
2562
- }
2563
- };
2564
- }
2565
- };
2566
- }
2567
- function transcriptSkipResponse(outcome) {
2568
- if (outcome !== null && outcome !== "ok") return {
2569
- inserted: 0,
2570
- skipped: 1,
2571
- skip_reason: "transcript_unavailable",
2572
- skip_detail: outcome
2573
- };
2574
- return {
2575
- inserted: 0,
2576
- skipped: 1,
2577
- skip_reason: "unsupported_hook"
2578
- };
2579
- }
2580
- function claudeTranscriptRoot() {
2581
- return join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude"), "projects");
2582
- }
2583
- function codexTranscriptRoot() {
2584
- return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "sessions");
2585
- }
2586
- /** Nudge the sweeper safely — never crashes the caller. */
2587
- function nudgeSweeper(sweeper, sessions) {
2635
+ //#region src/raw-event-processing.ts
2636
+ function nudgeRawEventSessions(sweeper, sessions) {
2588
2637
  for (const session of sessions) try {
2589
2638
  sweeper?.nudge(session.streamId, session.source);
2590
2639
  } catch {}
2591
2640
  }
2592
- async function flushBoundarySessions(sweeper, sessions) {
2641
+ async function flushRawEventBoundarySessions(sweeper, sessions) {
2593
2642
  for (const session of sessions) try {
2594
2643
  await sweeper?.flushBoundary(session.streamId, session.source);
2595
2644
  } catch {}
@@ -2606,36 +2655,480 @@ function isClaudeBoundaryEnvelope(envelope) {
2606
2655
  const hookEventName = meta.hook_event_name;
2607
2656
  return hookEventName === "SessionEnd" || hookEventName === "Stop";
2608
2657
  }
2609
- var SAFE_INGEST_VALIDATION_ERRORS = /* @__PURE__ */ new Set([
2610
- "source must be string",
2611
- "source is required",
2612
- "source must use 1-64 letters, digits, dots, underscores, or hyphens",
2613
- "session_stream_id must be string",
2614
- "session_id must be string",
2615
- "stream_id must be string",
2616
- "opencode_session_id must be string",
2617
- "conflicting session id fields",
2618
- "invalid session id",
2619
- "event source conflicts with request source",
2620
- "session id required",
2621
- "event_type must be string",
2622
- "event_type required",
2623
- "event_type has invalid syntax",
2624
- "event_id must be string",
2625
- "event_id required",
2626
- "event_id has invalid syntax",
2627
- "event_seq must be int",
2628
- "ts_wall_ms must be number",
2629
- "ts_mono_ms must be number",
2630
- "payload must be an object",
2631
- "cwd must be string",
2632
- "project must be string",
2633
- "started_at must be string",
2634
- "events must be a list",
2635
- "event must be an object"
2636
- ]);
2637
- function boundedIngestValidationMessage(error) {
2638
- return SAFE_INGEST_VALIDATION_ERRORS.has(error.message) ? error.message : "invalid raw event request";
2658
+ //#endregion
2659
+ //#region src/raw-event-inbox.ts
2660
+ var INBOX_DIRECTORY_NAME = "viewer-raw-event-inbox";
2661
+ var DEFAULT_BATCH_SIZE = 20;
2662
+ var DEFAULT_BACKLOG_WARNING_AGE_MS = 3e4;
2663
+ var DEFAULT_BACKLOG_WARNING_ENTRIES = 500;
2664
+ var DEFAULT_MAX_ENTRIES = 2e3;
2665
+ var DEFAULT_RETRY_DELAY_MS = 1e3;
2666
+ var MAX_RETRY_DELAY_MS = 3e4;
2667
+ var RAW_EVENT_INBOX_FULL_CODE = "raw_event_inbox_full";
2668
+ function contentId(serialized) {
2669
+ return createHash("sha256").update(serialized).digest("hex");
2670
+ }
2671
+ function normalizePositiveInteger(value, fallback) {
2672
+ return Number.isFinite(value) && Number(value) > 0 ? Math.trunc(Number(value)) : fallback;
2673
+ }
2674
+ function resolveRawEventInboxDirectory(dbPath, homeDir = homedir()) {
2675
+ const target = createHash("sha256").update(resolve(dbPath)).digest("hex").slice(0, 24);
2676
+ return join(homeDir, ".codemem", INBOX_DIRECTORY_NAME, target);
2677
+ }
2678
+ function entryContent(request, flushBoundary) {
2679
+ return {
2680
+ version: 1,
2681
+ request,
2682
+ flush_boundary: flushBoundary
2683
+ };
2684
+ }
2685
+ function storedEntry(request, flushBoundary, enqueueOrder) {
2686
+ return {
2687
+ ...entryContent(request, flushBoundary),
2688
+ enqueue_order: enqueueOrder.toString()
2689
+ };
2690
+ }
2691
+ function entryContentId(entry) {
2692
+ return contentId(JSON.stringify(entryContent(entry.request, entry.flushBoundary)));
2693
+ }
2694
+ function inboxFileName(enqueueOrder, id) {
2695
+ return `${enqueueOrder.toString().padStart(24, "0")}-${id}.json`;
2696
+ }
2697
+ function parseInboxFileName(name) {
2698
+ const match = /^(\d+)-([a-f0-9]{64})\.json$/u.exec(name);
2699
+ if (!match?.[1] || !match[2]) return null;
2700
+ const enqueueOrder = BigInt(match[1]);
2701
+ if (enqueueOrder < 1n) return null;
2702
+ return {
2703
+ id: match[2],
2704
+ enqueueOrder
2705
+ };
2706
+ }
2707
+ async function writeDurableFile(path, contents) {
2708
+ const handle = await open(path, "wx", 384);
2709
+ try {
2710
+ await handle.writeFile(contents, { encoding: "utf8" });
2711
+ await handle.sync();
2712
+ } finally {
2713
+ await handle.close();
2714
+ }
2715
+ }
2716
+ async function syncDirectory(path) {
2717
+ let handle;
2718
+ try {
2719
+ handle = await open(path, "r");
2720
+ await handle.sync();
2721
+ } catch (error) {
2722
+ const code = error.code;
2723
+ if (process.platform === "win32" && [
2724
+ "EISDIR",
2725
+ "EINVAL",
2726
+ "ENOTSUP",
2727
+ "EPERM"
2728
+ ].includes(code ?? "")) return;
2729
+ throw error;
2730
+ } finally {
2731
+ await handle?.close();
2732
+ }
2733
+ }
2734
+ async function ensurePrivateDirectory(path) {
2735
+ const firstCreated = await mkdir(path, {
2736
+ recursive: true,
2737
+ mode: 448
2738
+ });
2739
+ await chmod(path, 448);
2740
+ if (!firstCreated) return;
2741
+ const boundary = dirname(firstCreated);
2742
+ let current = path;
2743
+ while (true) {
2744
+ await syncDirectory(current);
2745
+ if (current === boundary) return;
2746
+ current = dirname(current);
2747
+ }
2748
+ }
2749
+ function parseStoredEntry(serialized) {
2750
+ const parsed = JSON.parse(serialized);
2751
+ if (parsed.version !== 1 || typeof parsed.enqueue_order !== "string" || !/^[1-9]\d*$/u.test(parsed.enqueue_order) || parsed.request == null || typeof parsed.request !== "object" || Array.isArray(parsed.request) || typeof parsed.flush_boundary !== "boolean") throw new Error("invalid raw-event inbox entry");
2752
+ return {
2753
+ entry: {
2754
+ request: parsed.request,
2755
+ flushBoundary: parsed.flush_boundary
2756
+ },
2757
+ enqueueOrder: BigInt(parsed.enqueue_order)
2758
+ };
2759
+ }
2760
+ var FileRawEventInbox = class {
2761
+ batchSize;
2762
+ backlogWarningAgeMs;
2763
+ backlogWarningEntries;
2764
+ directory;
2765
+ maxEntries;
2766
+ onDrainError;
2767
+ onDrainRecovered;
2768
+ onCorruptEntries;
2769
+ onBacklog;
2770
+ onBacklogRecovered;
2771
+ processEntry;
2772
+ retryDelayMs;
2773
+ drainPromise = null;
2774
+ degraded = false;
2775
+ backlogDegraded = false;
2776
+ enqueueChain = Promise.resolve();
2777
+ notedCorruptCount = 0;
2778
+ pendingDrainDelayMs = null;
2779
+ nextEnqueueOrder = null;
2780
+ retryCount = 0;
2781
+ timer = null;
2782
+ stopped = false;
2783
+ constructor(options) {
2784
+ this.batchSize = normalizePositiveInteger(options.batchSize, DEFAULT_BATCH_SIZE);
2785
+ this.backlogWarningAgeMs = normalizePositiveInteger(options.backlogWarningAgeMs, DEFAULT_BACKLOG_WARNING_AGE_MS);
2786
+ this.backlogWarningEntries = normalizePositiveInteger(options.backlogWarningEntries, DEFAULT_BACKLOG_WARNING_ENTRIES);
2787
+ this.directory = options.directory;
2788
+ this.maxEntries = normalizePositiveInteger(options.maxEntries, DEFAULT_MAX_ENTRIES);
2789
+ this.onDrainError = options.onDrainError;
2790
+ this.onDrainRecovered = options.onDrainRecovered;
2791
+ this.onCorruptEntries = options.onCorruptEntries;
2792
+ this.onBacklog = options.onBacklog;
2793
+ this.onBacklogRecovered = options.onBacklogRecovered;
2794
+ this.processEntry = options.processEntry;
2795
+ this.retryDelayMs = normalizePositiveInteger(options.retryDelayMs, DEFAULT_RETRY_DELAY_MS);
2796
+ }
2797
+ async enqueue(request, options = {}) {
2798
+ const operation = this.enqueueChain.then(() => this.persist(request, options));
2799
+ this.enqueueChain = operation.catch(() => {});
2800
+ return operation;
2801
+ }
2802
+ async persist(request, options) {
2803
+ const flushBoundary = options.flushBoundary === true;
2804
+ const id = entryContentId({
2805
+ request,
2806
+ flushBoundary
2807
+ });
2808
+ await ensurePrivateDirectory(this.directory);
2809
+ const entries = await readdir(this.directory, { withFileTypes: true });
2810
+ const existingEntry = entries.find((entry) => entry.isFile() && parseInboxFileName(entry.name)?.id === id);
2811
+ if (existingEntry) {
2812
+ const parsedExisting = parseStoredEntry(await readFile(join(this.directory, existingEntry.name), "utf8"));
2813
+ const existingName = parseInboxFileName(existingEntry.name);
2814
+ if (entryContentId(parsedExisting.entry) !== id || existingName?.enqueueOrder !== parsedExisting.enqueueOrder) throw new Error("raw-event inbox entry conflict");
2815
+ await chmod(join(this.directory, existingEntry.name), 384);
2816
+ await syncDirectory(this.directory);
2817
+ this.scheduleDrain();
2818
+ return;
2819
+ }
2820
+ if (entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).length >= this.maxEntries) {
2821
+ const error = /* @__PURE__ */ new Error("raw-event inbox is full");
2822
+ error.code = RAW_EVENT_INBOX_FULL_CODE;
2823
+ throw error;
2824
+ }
2825
+ const enqueueOrder = await this.allocateEnqueueOrder(entries);
2826
+ const serialized = JSON.stringify(storedEntry(request, flushBoundary, enqueueOrder));
2827
+ const destination = join(this.directory, inboxFileName(enqueueOrder, id));
2828
+ const temporary = join(this.directory, `.${id}.${process.pid}.${randomUUID()}.tmp`);
2829
+ try {
2830
+ await writeDurableFile(temporary, serialized);
2831
+ try {
2832
+ await link(temporary, destination);
2833
+ } catch (error) {
2834
+ if (error.code !== "EEXIST") throw error;
2835
+ const parsedExisting = parseStoredEntry(await readFile(destination, "utf8"));
2836
+ if (entryContentId(parsedExisting.entry) !== id || parsedExisting.enqueueOrder !== enqueueOrder) throw new Error("raw-event inbox entry conflict");
2837
+ }
2838
+ await chmod(destination, 384);
2839
+ } finally {
2840
+ await rm(temporary, { force: true }).catch(() => {});
2841
+ }
2842
+ await syncDirectory(this.directory);
2843
+ this.scheduleDrain();
2844
+ }
2845
+ async allocateEnqueueOrder(entries) {
2846
+ if (this.nextEnqueueOrder != null) {
2847
+ const order = this.nextEnqueueOrder;
2848
+ this.nextEnqueueOrder += 1n;
2849
+ return order;
2850
+ }
2851
+ let highestOrder = 0n;
2852
+ for (const entry of entries) {
2853
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
2854
+ const parsed = parseInboxFileName(entry.name);
2855
+ if (parsed && parsed.enqueueOrder > highestOrder) highestOrder = parsed.enqueueOrder;
2856
+ }
2857
+ const order = highestOrder + 1n;
2858
+ this.nextEnqueueOrder = order + 1n;
2859
+ return order;
2860
+ }
2861
+ start() {
2862
+ this.scheduleDrain();
2863
+ }
2864
+ async status() {
2865
+ const loaded = await this.loadEntries(Number.POSITIVE_INFINITY);
2866
+ return {
2867
+ pending: loaded.entries.length,
2868
+ corrupt: loaded.corrupt,
2869
+ draining: this.drainPromise != null
2870
+ };
2871
+ }
2872
+ async stop() {
2873
+ this.stopped = true;
2874
+ if (this.timer) clearTimeout(this.timer);
2875
+ this.timer = null;
2876
+ this.pendingDrainDelayMs = null;
2877
+ await this.enqueueChain;
2878
+ await this.drainPromise;
2879
+ }
2880
+ scheduleDrain(delayMs = 0) {
2881
+ if (this.stopped || this.timer) return;
2882
+ if (this.drainPromise) {
2883
+ this.pendingDrainDelayMs = delayMs;
2884
+ return;
2885
+ }
2886
+ this.timer = setTimeout(() => {
2887
+ this.timer = null;
2888
+ if (this.stopped || this.drainPromise) return;
2889
+ this.drainPromise = this.drain().catch((error) => this.handleDrainFailure(error)).finally(() => {
2890
+ this.drainPromise = null;
2891
+ if (this.pendingDrainDelayMs != null) {
2892
+ const pendingDelayMs = this.pendingDrainDelayMs;
2893
+ this.pendingDrainDelayMs = null;
2894
+ this.scheduleDrain(pendingDelayMs);
2895
+ }
2896
+ });
2897
+ }, delayMs);
2898
+ this.timer.unref?.();
2899
+ }
2900
+ async loadEntries(limit) {
2901
+ let directoryEntries;
2902
+ try {
2903
+ directoryEntries = await readdir(this.directory, { withFileTypes: true });
2904
+ } catch (error) {
2905
+ if (error.code === "ENOENT") return {
2906
+ entries: [],
2907
+ corrupt: 0,
2908
+ oldestAgeMs: 0,
2909
+ pending: 0
2910
+ };
2911
+ throw error;
2912
+ }
2913
+ const scan = await this.scanCandidates(directoryEntries);
2914
+ scan.candidates.sort((left, right) => {
2915
+ if (left.enqueueOrder < right.enqueueOrder) return -1;
2916
+ if (left.enqueueOrder > right.enqueueOrder) return 1;
2917
+ return left.id.localeCompare(right.id);
2918
+ });
2919
+ const entries = [];
2920
+ let corrupt = scan.corrupt;
2921
+ for (const candidate of scan.candidates) {
2922
+ if (entries.length >= limit) break;
2923
+ const loadedEntry = await this.loadEntry(candidate);
2924
+ if (!loadedEntry) {
2925
+ corrupt += 1;
2926
+ continue;
2927
+ }
2928
+ entries.push(loadedEntry);
2929
+ }
2930
+ return {
2931
+ entries,
2932
+ corrupt,
2933
+ oldestAgeMs: Number.isFinite(scan.oldestMtimeMs) ? Math.max(0, Date.now() - scan.oldestMtimeMs) : 0,
2934
+ pending: scan.pending
2935
+ };
2936
+ }
2937
+ async scanCandidates(directoryEntries) {
2938
+ const candidates = [];
2939
+ let corrupt = 0;
2940
+ let oldestMtimeMs = Number.POSITIVE_INFINITY;
2941
+ let pending = 0;
2942
+ for (const directoryEntry of directoryEntries) {
2943
+ if (!directoryEntry.isFile() || !directoryEntry.name.endsWith(".json")) continue;
2944
+ const path = join(this.directory, directoryEntry.name);
2945
+ try {
2946
+ const details = await stat(path);
2947
+ pending += 1;
2948
+ oldestMtimeMs = Math.min(oldestMtimeMs, details.mtimeMs);
2949
+ const parsed = parseInboxFileName(directoryEntry.name);
2950
+ if (!parsed) {
2951
+ corrupt += 1;
2952
+ continue;
2953
+ }
2954
+ candidates.push({
2955
+ path,
2956
+ ...parsed
2957
+ });
2958
+ } catch {}
2959
+ }
2960
+ return {
2961
+ candidates,
2962
+ corrupt,
2963
+ oldestMtimeMs,
2964
+ pending
2965
+ };
2966
+ }
2967
+ async loadEntry(candidate) {
2968
+ try {
2969
+ const { entry, enqueueOrder } = parseStoredEntry(await readFile(candidate.path, "utf8"));
2970
+ const id = entryContentId(entry);
2971
+ if (candidate.id !== id || candidate.enqueueOrder !== enqueueOrder) return null;
2972
+ return {
2973
+ id,
2974
+ path: candidate.path,
2975
+ entry,
2976
+ enqueueOrder
2977
+ };
2978
+ } catch {
2979
+ return null;
2980
+ }
2981
+ }
2982
+ async drain() {
2983
+ const loaded = await this.loadEntries(this.batchSize);
2984
+ const backlogExceeded = loaded.pending >= this.backlogWarningEntries && loaded.oldestAgeMs >= this.backlogWarningAgeMs;
2985
+ if (backlogExceeded && !this.backlogDegraded) {
2986
+ this.backlogDegraded = true;
2987
+ this.onBacklog?.(loaded.pending, loaded.oldestAgeMs);
2988
+ } else if (!backlogExceeded && this.backlogDegraded) {
2989
+ this.backlogDegraded = false;
2990
+ this.onBacklogRecovered?.();
2991
+ }
2992
+ if (loaded.corrupt !== this.notedCorruptCount) {
2993
+ this.notedCorruptCount = loaded.corrupt;
2994
+ if (loaded.corrupt > 0) this.onCorruptEntries?.(loaded.corrupt);
2995
+ }
2996
+ for (const loadedEntry of loaded.entries) {
2997
+ await this.processEntry(loadedEntry.entry);
2998
+ await rm(loadedEntry.path, { force: true });
2999
+ }
3000
+ this.retryCount = 0;
3001
+ if (this.degraded) {
3002
+ this.degraded = false;
3003
+ this.onDrainRecovered?.();
3004
+ }
3005
+ if (loaded.entries.length > 0) this.scheduleDrain();
3006
+ }
3007
+ handleDrainFailure(error) {
3008
+ if (!this.degraded) {
3009
+ this.degraded = true;
3010
+ this.onDrainError?.(error);
3011
+ }
3012
+ this.retryCount += 1;
3013
+ const delay = Math.min(this.retryDelayMs * 2 ** (this.retryCount - 1), MAX_RETRY_DELAY_MS);
3014
+ this.scheduleDrain(delay);
3015
+ }
3016
+ };
3017
+ function createViewerRawEventInbox(options) {
3018
+ const store = new MemoryStore(options.dbPath);
3019
+ store.db.pragma("busy_timeout = 25");
3020
+ const inbox = new FileRawEventInbox({
3021
+ directory: resolveRawEventInboxDirectory(options.dbPath, options.homeDir),
3022
+ onDrainError: options.onDrainError,
3023
+ onDrainRecovered: options.onDrainRecovered,
3024
+ onCorruptEntries: options.onCorruptEntries,
3025
+ onBacklog: options.onBacklog,
3026
+ onBacklogRecovered: options.onBacklogRecovered,
3027
+ processEntry: async ({ request, flushBoundary }) => {
3028
+ const result = ingestRawEvents(store, request);
3029
+ nudgeRawEventSessions(options.sweeper, result.sessions);
3030
+ if (flushBoundary && isClaudeBoundaryEnvelope(request)) await flushRawEventBoundarySessions(options.sweeper, result.sessions);
3031
+ }
3032
+ });
3033
+ return {
3034
+ inbox,
3035
+ stop: async () => {
3036
+ await inbox.stop();
3037
+ store.close();
3038
+ }
3039
+ };
3040
+ }
3041
+ //#endregion
3042
+ //#region src/routes/raw-events.ts
3043
+ /**
3044
+ * Raw events routes — GET & POST /api/raw-events, GET /api/raw-events/status,
3045
+ * POST /api/claude-hooks, POST /api/codex-hooks, POST /api/pi-hooks.
3046
+ */
3047
+ var DEFAULT_MAX_RAW_EVENTS_BODY_BYTES = 1048576;
3048
+ function configuredMaxRawEventsBodyBytes() {
3049
+ const parsed = Number(process.env.CODEMEM_RAW_EVENTS_MAX_BODY_BYTES?.trim() ?? "");
3050
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_RAW_EVENTS_BODY_BYTES;
3051
+ }
3052
+ var MAX_RAW_EVENTS_BODY_BYTES = configuredMaxRawEventsBodyBytes();
3053
+ function emptyTranscriptOutcomeCounts() {
3054
+ return {
3055
+ ok: 0,
3056
+ not_provided: 0,
3057
+ path_rejected: 0,
3058
+ unreadable: 0,
3059
+ no_complete_record: 0,
3060
+ no_assistant_record: 0
3061
+ };
3062
+ }
3063
+ function createTranscriptDiagnostics() {
3064
+ const counts = {
3065
+ claude: emptyTranscriptOutcomeCounts(),
3066
+ codex: emptyTranscriptOutcomeCounts()
3067
+ };
3068
+ return {
3069
+ record(source, outcome) {
3070
+ counts[source][outcome] += 1;
3071
+ },
3072
+ snapshot() {
3073
+ return {
3074
+ scope: "legacy_compatibility_routes",
3075
+ counts: {
3076
+ claude: { ...counts.claude },
3077
+ codex: { ...counts.codex }
3078
+ }
3079
+ };
3080
+ }
3081
+ };
3082
+ }
3083
+ function transcriptSkipResponse(outcome) {
3084
+ if (outcome !== null && outcome !== "ok") return {
3085
+ inserted: 0,
3086
+ skipped: 1,
3087
+ skip_reason: "transcript_unavailable",
3088
+ skip_detail: outcome
3089
+ };
3090
+ return {
3091
+ inserted: 0,
3092
+ skipped: 1,
3093
+ skip_reason: "unsupported_hook"
3094
+ };
3095
+ }
3096
+ function claudeTranscriptRoot() {
3097
+ return join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude"), "projects");
3098
+ }
3099
+ function codexTranscriptRoot() {
3100
+ return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "sessions");
3101
+ }
3102
+ var SAFE_INGEST_VALIDATION_ERRORS = /* @__PURE__ */ new Set([
3103
+ "source must be string",
3104
+ "source is required",
3105
+ "source must use 1-64 letters, digits, dots, underscores, or hyphens",
3106
+ "session_stream_id must be string",
3107
+ "session_id must be string",
3108
+ "stream_id must be string",
3109
+ "opencode_session_id must be string",
3110
+ "conflicting session id fields",
3111
+ "invalid session id",
3112
+ "event source conflicts with request source",
3113
+ "session id required",
3114
+ "event_type must be string",
3115
+ "event_type required",
3116
+ "event_type has invalid syntax",
3117
+ "event_id must be string",
3118
+ "event_id required",
3119
+ "event_id has invalid syntax",
3120
+ "event_seq must be int",
3121
+ "ts_wall_ms must be number",
3122
+ "ts_mono_ms must be number",
3123
+ "payload must be an object",
3124
+ "cwd must be string",
3125
+ "project must be string",
3126
+ "started_at must be string",
3127
+ "events must be a list",
3128
+ "event must be an object"
3129
+ ]);
3130
+ function boundedIngestValidationMessage(error) {
3131
+ return SAFE_INGEST_VALIDATION_ERRORS.has(error.message) ? error.message : "invalid raw event request";
2639
3132
  }
2640
3133
  function boundedIngestErrorResponse(c, error) {
2641
3134
  if (error instanceof RawEventIngestValidationError) return c.json({ error: boundedIngestValidationMessage(error) }, 400);
@@ -2643,17 +3136,68 @@ function boundedIngestErrorResponse(c, error) {
2643
3136
  if (process.env.CODEMEM_VIEWER_DEBUG === "1") response.detail = error instanceof Error ? error.message : String(error);
2644
3137
  return c.json(response, 500);
2645
3138
  }
3139
+ function boundedInboxErrorResponse(c, error) {
3140
+ if (error?.code === "raw_event_inbox_full") return c.json({ error: {
3141
+ code: "raw_event_queue_full",
3142
+ message: "raw-event queue is full"
3143
+ } }, 503);
3144
+ return c.json({ error: {
3145
+ code: "raw_event_queue_write_failed",
3146
+ message: "raw-event queue write failed"
3147
+ } }, 503);
3148
+ }
2646
3149
  function untargetedPayload(payload) {
2647
3150
  const { db_path: _dbPath, identity_target: _identityTarget, ...body } = payload;
2648
3151
  return body;
2649
3152
  }
3153
+ async function enqueueRawEventRequest(options) {
3154
+ const target = validateViewerTarget(options.inboxTarget ?? options.getStore(), options.payload, { requirePairedTargets: true });
3155
+ if (!target.ok) return options.c.json(target.body, target.status);
3156
+ const validation = validateRawEvents(options.request);
3157
+ try {
3158
+ await options.inbox.enqueue(validation.request, { flushBoundary: options.flushBoundary });
3159
+ } catch (error) {
3160
+ return boundedInboxErrorResponse(options.c, error);
3161
+ }
3162
+ return options.c.json({
3163
+ accepted: validation.received,
3164
+ queued: validation.received
3165
+ }, 202);
3166
+ }
3167
+ async function postRawEventRequest(c, getStore, sweeper, inbox, inboxTarget) {
3168
+ const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
3169
+ if (result instanceof Response) return result;
3170
+ try {
3171
+ const request = untargetedPayload(result);
3172
+ if (inbox) return await enqueueRawEventRequest({
3173
+ c,
3174
+ getStore,
3175
+ inbox,
3176
+ inboxTarget,
3177
+ payload: result,
3178
+ request,
3179
+ flushBoundary: c.req.header("x-codemem-boundary-flush") === "1"
3180
+ });
3181
+ const store = getStore();
3182
+ const target = validateViewerTarget(store, result, { requirePairedTargets: true });
3183
+ if (!target.ok) return c.json(target.body, target.status);
3184
+ const ingestResult = await ingestNormalizedEnvelope(store, sweeper, request, c.req.header("x-codemem-boundary-flush") === "1");
3185
+ return c.json({
3186
+ inserted: ingestResult.inserted,
3187
+ skipped: ingestResult.skipped,
3188
+ received: ingestResult.received
3189
+ });
3190
+ } catch (error) {
3191
+ return boundedIngestErrorResponse(c, error);
3192
+ }
3193
+ }
2650
3194
  async function ingestNormalizedEnvelope(store, sweeper, envelope, flushBoundary = false) {
2651
3195
  const result = ingestRawEvents(store, envelope);
2652
- nudgeSweeper(sweeper, result.sessions);
2653
- if (flushBoundary && isClaudeBoundaryEnvelope(envelope)) await flushBoundarySessions(sweeper, result.sessions);
3196
+ nudgeRawEventSessions(sweeper, result.sessions);
3197
+ if (flushBoundary && isClaudeBoundaryEnvelope(envelope)) await flushRawEventBoundarySessions(sweeper, result.sessions);
2654
3198
  return result;
2655
3199
  }
2656
- function rawEventsRoutes(getStore, sweeper) {
3200
+ function rawEventsRoutes(getStore, sweeper, inbox, inboxTarget) {
2657
3201
  const app = new Hono();
2658
3202
  const transcriptDiagnostics = createTranscriptDiagnostics();
2659
3203
  app.get("/api/raw-events", (c) => {
@@ -2661,8 +3205,17 @@ function rawEventsRoutes(getStore, sweeper) {
2661
3205
  return c.json(totals);
2662
3206
  });
2663
3207
  app.get("/api/raw-events/status", (c) => {
2664
- const store = getStore();
2665
3208
  const limit = queryInt(c.req.query("limit"), 25);
3209
+ if (inbox && limit === 0) return c.json({
3210
+ items: [],
3211
+ ingest: {
3212
+ available: true,
3213
+ mode: "durable_queue",
3214
+ max_body_bytes: MAX_RAW_EVENTS_BODY_BYTES
3215
+ },
3216
+ transcript_diagnostics: transcriptDiagnostics.snapshot()
3217
+ });
3218
+ const store = getStore();
2666
3219
  const items = drizzle(store.db, { schema }).select({
2667
3220
  source: schema.rawEventSessions.source,
2668
3221
  stream_id: schema.rawEventSessions.stream_id,
@@ -2688,29 +3241,13 @@ function rawEventsRoutes(getStore, sweeper) {
2688
3241
  totals,
2689
3242
  ingest: {
2690
3243
  available: true,
2691
- mode: "stream_queue",
3244
+ mode: inbox ? "durable_queue" : "stream_queue",
2692
3245
  max_body_bytes: MAX_RAW_EVENTS_BODY_BYTES
2693
3246
  },
2694
3247
  transcript_diagnostics: transcriptDiagnostics.snapshot()
2695
3248
  });
2696
3249
  });
2697
- app.post("/api/raw-events", async (c) => {
2698
- const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
2699
- if (result instanceof Response) return result;
2700
- try {
2701
- const store = getStore();
2702
- const target = validateViewerTarget(store, result, { requirePairedTargets: true });
2703
- if (!target.ok) return c.json(target.body, target.status);
2704
- const ingestResult = await ingestNormalizedEnvelope(store, sweeper, untargetedPayload(result), c.req.header("x-codemem-boundary-flush") === "1");
2705
- return c.json({
2706
- inserted: ingestResult.inserted,
2707
- skipped: ingestResult.skipped,
2708
- received: ingestResult.received
2709
- });
2710
- } catch (err) {
2711
- return boundedIngestErrorResponse(c, err);
2712
- }
2713
- });
3250
+ app.post("/api/raw-events", (c) => postRawEventRequest(c, getStore, sweeper, inbox, inboxTarget));
2714
3251
  app.post("/api/claude-hooks", async (c) => {
2715
3252
  const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
2716
3253
  if (result instanceof Response) return result;
@@ -2919,24 +3456,49 @@ function mapAggregateEvents(rows) {
2919
3456
  total_tokens_read: row.tokens_read,
2920
3457
  total_tokens_written: row.tokens_written,
2921
3458
  total_tokens_saved: row.tokens_saved,
2922
- count: row.count
3459
+ count: row.count,
3460
+ token_unit: row.token_unit,
3461
+ measured_count: row.measured_count,
3462
+ estimated_count: row.estimated_count,
3463
+ unavailable_count: row.unavailable_count,
3464
+ legacy_text_length_count: row.legacy_text_length_count,
3465
+ legacy_unclassified_count: row.legacy_unclassified_count
2923
3466
  })).sort((a, b) => a.event.localeCompare(b.event));
2924
3467
  }
2925
- /**
2926
- * Sum the neutral core aggregate rows into the ApiUsageTotals wire shape.
2927
- */
2928
3468
  function totalsFromAggregate(rows) {
2929
3469
  return rows.reduce((acc, row) => ({
2930
3470
  tokens_read: acc.tokens_read + row.tokens_read,
2931
3471
  tokens_written: acc.tokens_written + row.tokens_written,
2932
3472
  tokens_saved: acc.tokens_saved + row.tokens_saved,
2933
- count: acc.count + row.count
3473
+ count: acc.count + row.count,
3474
+ token_unit: "tokens",
3475
+ measured_count: acc.measured_count + row.measured_count,
3476
+ estimated_count: acc.estimated_count + row.estimated_count,
3477
+ unavailable_count: acc.unavailable_count + row.unavailable_count,
3478
+ legacy_text_length_count: acc.legacy_text_length_count + row.legacy_text_length_count,
3479
+ legacy_unclassified_count: acc.legacy_unclassified_count + row.legacy_unclassified_count
2934
3480
  }), {
2935
3481
  tokens_read: 0,
2936
3482
  tokens_written: 0,
2937
3483
  tokens_saved: 0,
2938
- count: 0
2939
- });
3484
+ count: 0,
3485
+ token_unit: "tokens",
3486
+ measured_count: 0,
3487
+ estimated_count: 0,
3488
+ unavailable_count: 0,
3489
+ legacy_text_length_count: 0,
3490
+ legacy_unclassified_count: 0
3491
+ });
3492
+ }
3493
+ function usageAggregatePayload(store, project) {
3494
+ const globalRows = store.classifiedUsageAggregate();
3495
+ const filteredRows = project ? store.classifiedUsageAggregate(project) : null;
3496
+ return {
3497
+ eventsGlobal: mapAggregateEvents(globalRows),
3498
+ totalsGlobal: totalsFromAggregate(globalRows),
3499
+ eventsFiltered: filteredRows ? mapAggregateEvents(filteredRows) : null,
3500
+ totalsFiltered: filteredRows ? totalsFromAggregate(filteredRows) : null
3501
+ };
2940
3502
  }
2941
3503
  /**
2942
3504
  * Short-lived cache for the computed /api/usage payload.
@@ -3017,120 +3579,657 @@ function scopeVisibilityGeneration(store) {
3017
3579
  return `bypass-${Date.now()}-${Math.random()}`;
3018
3580
  }
3019
3581
  }
3020
- function usageCacheKey(store, projectFilter) {
3021
- const visibilityGeneration = scopeVisibilityGeneration(store);
3022
- return `${store.dbPath}|${store.actorId}|${store.deviceId}|${projectFilter ?? ""}|${visibilityGeneration}`;
3582
+ function usageCacheKey(store, projectFilter) {
3583
+ const visibilityGeneration = scopeVisibilityGeneration(store);
3584
+ return `${store.dbPath}|${store.actorId}|${store.deviceId}|${projectFilter ?? ""}|${visibilityGeneration}`;
3585
+ }
3586
+ /**
3587
+ * Create stats routes. The store factory is called per-request to get a
3588
+ * fresh connection (matching the Python viewer pattern).
3589
+ */
3590
+ function statsRoutes(getStore) {
3591
+ const app = new Hono();
3592
+ const parseMetadataJson = (value) => {
3593
+ if (typeof value !== "string" || !value.trim()) return null;
3594
+ try {
3595
+ return JSON.parse(value);
3596
+ } catch {
3597
+ return null;
3598
+ }
3599
+ };
3600
+ const RECENTLY_COMPLETED_WINDOW_MS = 12e4;
3601
+ app.get("/api/stats", (c) => {
3602
+ const store = getStore();
3603
+ const jobs = listMaintenanceJobs(store.db);
3604
+ const now = Date.now();
3605
+ const surfacedJobs = sortActiveMaintenanceJobs$1(jobs.filter((job) => {
3606
+ if (job.status === "pending" || job.status === "running" || job.status === "failed") return true;
3607
+ if (job.status === "completed" && job.finished_at) {
3608
+ const finished = Date.parse(job.finished_at);
3609
+ if (Number.isFinite(finished) && now - finished <= RECENTLY_COMPLETED_WINDOW_MS) return true;
3610
+ }
3611
+ return false;
3612
+ })).map((job) => ({
3613
+ kind: job.kind,
3614
+ title: job.title,
3615
+ status: job.status,
3616
+ message: job.message,
3617
+ progress: job.progress,
3618
+ finished_at: job.finished_at,
3619
+ error: job.error
3620
+ }));
3621
+ return c.json({
3622
+ ...healthStats(store),
3623
+ viewer_pid: process.pid,
3624
+ maintenance_jobs: surfacedJobs
3625
+ });
3626
+ });
3627
+ app.get("/api/runtime", (c) => {
3628
+ return c.json({ version: VERSION$1 });
3629
+ });
3630
+ app.get("/api/usage", (c) => {
3631
+ const store = getStore();
3632
+ {
3633
+ const projectFilter = c.req.query("project") || null;
3634
+ const cacheKey = usageCacheKey(store, projectFilter);
3635
+ const nowMs = Date.now();
3636
+ const cached = usagePayloadCache.get(cacheKey);
3637
+ if (cached && nowMs < cached.expiresAtMs) return c.json(cached.payload);
3638
+ const aggregates = usageAggregatePayload(store, projectFilter);
3639
+ const RECENT_PACK_SCAN_LIMIT = 200;
3640
+ const loadRecentPackWindow = (project) => {
3641
+ return (project ? store.db.prepare(`SELECT usage_events.id, usage_events.session_id, usage_events.event,
3642
+ usage_events.tokens_read, usage_events.tokens_written, usage_events.tokens_saved,
3643
+ usage_events.created_at, usage_events.metadata_json
3644
+ FROM usage_events
3645
+ JOIN sessions ON sessions.id = usage_events.session_id
3646
+ WHERE sessions.project = ? AND usage_events.event = 'pack'
3647
+ ORDER BY usage_events.created_at DESC
3648
+ LIMIT ?`).all(project, RECENT_PACK_SCAN_LIMIT) : store.db.prepare(`SELECT id, session_id, event, tokens_read, tokens_written, tokens_saved,
3649
+ created_at, metadata_json
3650
+ FROM usage_events
3651
+ WHERE event = 'pack'
3652
+ ORDER BY created_at DESC
3653
+ LIMIT ?`).all(RECENT_PACK_SCAN_LIMIT)).map((row) => toUsageRow(row, parseMetadataJson(row.metadata_json)));
3654
+ };
3655
+ const recentWindow = loadRecentPackWindow(projectFilter);
3656
+ const recentVisibility = buildUsageVisibility(store, recentWindow);
3657
+ const recentPacks = recentWindow.filter((row) => usageRowVisible(row, recentVisibility)).slice(0, 10).map((row) => ({
3658
+ ...row,
3659
+ metadata_json: sanitizePackUsageMetadata(row.metadata_json, recentVisibility)
3660
+ }));
3661
+ const payload = {
3662
+ project: projectFilter,
3663
+ events: projectFilter ? aggregates.eventsFiltered : aggregates.eventsGlobal,
3664
+ totals: projectFilter ? aggregates.totalsFiltered : aggregates.totalsGlobal,
3665
+ events_global: aggregates.eventsGlobal,
3666
+ totals_global: aggregates.totalsGlobal,
3667
+ events_filtered: aggregates.eventsFiltered,
3668
+ totals_filtered: aggregates.totalsFiltered,
3669
+ recent_packs: recentPacks
3670
+ };
3671
+ const setAtMs = Date.now();
3672
+ sweepUsagePayloadCache(usagePayloadCache, setAtMs);
3673
+ usagePayloadCache.set(cacheKey, {
3674
+ payload,
3675
+ expiresAtMs: setAtMs + USAGE_PAYLOAD_CACHE_MS
3676
+ });
3677
+ return c.json(payload);
3678
+ }
3679
+ });
3680
+ return app;
3681
+ }
3682
+ function healthStats(store) {
3683
+ return {
3684
+ ...store.stats(),
3685
+ automatic_recall: automaticRecallHealth(store.db, {
3686
+ actorId: store.actorId,
3687
+ deviceId: store.deviceId
3688
+ })
3689
+ };
3690
+ }
3691
+ //#endregion
3692
+ //#region src/application/coordinator-maintenance.ts
3693
+ var AUTOMATIC_SHARE_OPERATION_STATES = [
3694
+ "waiting_for_acceptance",
3695
+ "accepted",
3696
+ "provisioning",
3697
+ "initial_sync",
3698
+ "waiting_for_device"
3699
+ ];
3700
+ var AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS = 3e4;
3701
+ var AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS = 3e5;
3702
+ var TERMINAL_SHARE_MAINTENANCE_ERRORS = /* @__PURE__ */ new Set([
3703
+ "coordinator_not_configured",
3704
+ "team_sharing_not_configured",
3705
+ "team_selection_ambiguous",
3706
+ "initiating_device_not_reviewed",
3707
+ "inviter_project_access_ambiguous",
3708
+ "managed_boundary_plan_missing",
3709
+ "operation_device_binding_missing",
3710
+ "operation_intent_invalid",
3711
+ "provisioning_membership_plan_invalid",
3712
+ "device_binding_conflict",
3713
+ "intent_conflict",
3714
+ "inviter_identity_conflict"
3715
+ ]);
3716
+ var PROJECT_INVITE_OWNER_CONFLICT_ERRORS = /* @__PURE__ */ new Set([
3717
+ "operation_scope_mismatch",
3718
+ "operation_state_invalid",
3719
+ "operation_acceptance_invalid",
3720
+ "operation_trust_state_invalid",
3721
+ "operation_intent_invalid",
3722
+ "operation_intent_mismatch",
3723
+ "recipient_fingerprint_mismatch",
3724
+ "recipient_device_identity_conflict",
3725
+ "recipient_actor_conflict",
3726
+ "pending_person_identity_conflict",
3727
+ "device_binding_conflict",
3728
+ "intent_conflict",
3729
+ "inviter_identity_conflict"
3730
+ ]);
3731
+ var TERMINAL_RECONCILIATION_ERRORS = /* @__PURE__ */ new Set([
3732
+ "coordinator_not_configured",
3733
+ "team_sharing_not_configured",
3734
+ "team_selection_ambiguous",
3735
+ "operation_not_found",
3736
+ ...PROJECT_INVITE_OWNER_CONFLICT_ERRORS
3737
+ ]);
3738
+ function errorStatus$2(error) {
3739
+ const value = error && typeof error === "object" ? error.status : null;
3740
+ return typeof value === "number" && Number.isInteger(value) && value >= 400 && value <= 599 ? value : null;
3741
+ }
3742
+ function recordInviteReconciliationFailure(store, operationId, error, now) {
3743
+ const message = error instanceof Error ? error.message : String(error);
3744
+ const safeErrorCode = TERMINAL_RECONCILIATION_ERRORS.has(message) ? message : "operation_read_failed";
3745
+ const status = errorStatus$2(error);
3746
+ const retryable = !TERMINAL_RECONCILIATION_ERRORS.has(message) && (status == null || status === 408 || status === 429 || status >= 500);
3747
+ store.db.prepare(`UPDATE share_operation_steps SET
3748
+ status = CASE WHEN ? = 1 THEN 'pending' ELSE 'failed' END,
3749
+ attempt_count = attempt_count + 1, last_attempt_at = ?, safe_error_code = ?, updated_at = ?
3750
+ WHERE operation_id = ? AND step_key = 'invite_consumption'`).run(retryable ? 1 : 0, now, safeErrorCode, now, operationId);
3751
+ const outcome = retryable ? "retry_scheduled" : "needs_attention";
3752
+ store.db.prepare("UPDATE share_operations SET state = ?, updated_at = ? WHERE operation_id = ?").run(outcome === "needs_attention" ? "needs_attention" : "waiting_for_acceptance", now, operationId);
3753
+ return outcome;
3754
+ }
3755
+ function pendingShareOperationRows(store, input) {
3756
+ const placeholders = AUTOMATIC_SHARE_OPERATION_STATES.map(() => "?").join(", ");
3757
+ const waitingAcceptanceRetryBefore = (/* @__PURE__ */ new Date(input.maintenanceNow.getTime() - AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS)).toISOString();
3758
+ const waitingRetryBefore = (/* @__PURE__ */ new Date(input.maintenanceNow.getTime() - AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS)).toISOString();
3759
+ return store.db.prepare(`SELECT operation.operation_id FROM share_operations AS operation
3760
+ WHERE operation.inviter_actor_id = ?
3761
+ AND operation.state IN (${placeholders})
3762
+ AND (operation.state <> 'waiting_for_acceptance' OR operation.updated_at <= ?)
3763
+ AND (
3764
+ operation.state <> 'waiting_for_device'
3765
+ OR operation.updated_at <= ?
3766
+ OR (
3767
+ NOT EXISTS (
3768
+ SELECT 1 FROM share_operation_steps AS step
3769
+ WHERE step.operation_id = operation.operation_id
3770
+ AND step.step_key = 'capability_preflight'
3771
+ AND step.status <> 'completed'
3772
+ AND (
3773
+ step.status = 'running'
3774
+ OR step.safe_error_code IN (
3775
+ 'waiting_for_device', 'device_offline', 'recipient_offline'
3776
+ )
3777
+ )
3778
+ )
3779
+ AND EXISTS (
3780
+ SELECT 1 FROM sync_attempts AS attempt
3781
+ WHERE attempt.id = (
3782
+ SELECT latest.id FROM sync_attempts AS latest
3783
+ WHERE latest.peer_device_id = operation.recipient_device_id
3784
+ ORDER BY latest.started_at DESC, latest.id DESC
3785
+ LIMIT 1
3786
+ )
3787
+ AND attempt.ok = 1
3788
+ AND julianday(COALESCE(attempt.finished_at, attempt.started_at)) >
3789
+ julianday(operation.updated_at)
3790
+ )
3791
+ )
3792
+ )
3793
+ ORDER BY CASE WHEN operation.state IN ('accepted', 'provisioning', 'initial_sync') THEN 0 ELSE 1 END,
3794
+ CASE WHEN operation.state IN ('waiting_for_acceptance', 'waiting_for_device')
3795
+ THEN operation.updated_at ELSE operation.created_at END ASC,
3796
+ operation.created_at ASC, operation.operation_id ASC
3797
+ LIMIT ?`).all(store.actorId, ...AUTOMATIC_SHARE_OPERATION_STATES, waitingAcceptanceRetryBefore, waitingRetryBefore, input.limit);
3798
+ }
3799
+ function recordPendingShareFailure(store, result, operationId, error, now) {
3800
+ const message = error instanceof Error ? error.message : String(error);
3801
+ const state = store.db.prepare("SELECT state FROM share_operations WHERE operation_id = ?").pluck().get(operationId);
3802
+ if (state === "cancelled") {
3803
+ result.items.push({
3804
+ operationId,
3805
+ outcome: "superseded"
3806
+ });
3807
+ return;
3808
+ }
3809
+ if (message === "waiting_for_device" || state === "waiting_for_device") {
3810
+ result.waiting += 1;
3811
+ result.items.push({
3812
+ operationId,
3813
+ outcome: "waiting_for_device"
3814
+ });
3815
+ return;
3816
+ }
3817
+ if (state === "needs_attention") {
3818
+ result.attention += 1;
3819
+ result.items.push({
3820
+ operationId,
3821
+ outcome: "needs_attention",
3822
+ error: message
3823
+ });
3824
+ return;
3825
+ }
3826
+ if (state === "waiting_for_acceptance") {
3827
+ const outcome = recordInviteReconciliationFailure(store, operationId, error, now);
3828
+ if (outcome === "needs_attention") result.attention += 1;
3829
+ else result.waiting += 1;
3830
+ result.items.push({
3831
+ operationId,
3832
+ outcome,
3833
+ error: message
3834
+ });
3835
+ return;
3836
+ }
3837
+ if (TERMINAL_SHARE_MAINTENANCE_ERRORS.has(message)) {
3838
+ store.db.prepare("UPDATE share_operations SET state = 'needs_attention', updated_at = ? WHERE operation_id = ?").run(now, operationId);
3839
+ result.attention += 1;
3840
+ result.items.push({
3841
+ operationId,
3842
+ outcome: "needs_attention",
3843
+ error: message
3844
+ });
3845
+ return;
3846
+ }
3847
+ result.failed += 1;
3848
+ result.items.push({
3849
+ operationId,
3850
+ outcome: "failed",
3851
+ error: message
3852
+ });
3853
+ }
3854
+ async function processPendingShareOperation(store, result, operationId, advanceOperation, now) {
3855
+ result.processed += 1;
3856
+ try {
3857
+ const advanced = await advanceOperation(store, operationId);
3858
+ if (advanced.advanced) {
3859
+ result.advanced += 1;
3860
+ result.items.push({
3861
+ operationId,
3862
+ outcome: "advanced"
3863
+ });
3864
+ return;
3865
+ }
3866
+ if (advanced.state === "cancelled") {
3867
+ result.items.push({
3868
+ operationId,
3869
+ outcome: "superseded"
3870
+ });
3871
+ return;
3872
+ }
3873
+ store.db.prepare("UPDATE share_operations SET updated_at = ? WHERE operation_id = ?").run(now, operationId);
3874
+ result.waiting += 1;
3875
+ result.items.push({
3876
+ operationId,
3877
+ outcome: "waiting_for_acceptance"
3878
+ });
3879
+ } catch (error) {
3880
+ recordPendingShareFailure(store, result, operationId, error, now);
3881
+ }
3882
+ }
3883
+ async function advancePendingProjectSharesOperation(store, options) {
3884
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 3), 10));
3885
+ const maintenanceNow = options.now ?? /* @__PURE__ */ new Date();
3886
+ const rows = pendingShareOperationRows(store, {
3887
+ limit,
3888
+ maintenanceNow
3889
+ });
3890
+ const result = {
3891
+ processed: 0,
3892
+ advanced: 0,
3893
+ waiting: 0,
3894
+ attention: 0,
3895
+ failed: 0,
3896
+ items: []
3897
+ };
3898
+ const now = maintenanceNow.toISOString();
3899
+ for (const row of rows) await processPendingShareOperation(store, result, row.operation_id, options.advanceOperation, now);
3900
+ return result;
3023
3901
  }
3024
- /**
3025
- * Create stats routes. The store factory is called per-request to get a
3026
- * fresh connection (matching the Python viewer pattern).
3027
- */
3028
- function statsRoutes(getStore) {
3029
- const app = new Hono();
3030
- const parseMetadataJson = (value) => {
3031
- if (typeof value !== "string" || !value.trim()) return null;
3902
+ var RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT = 10;
3903
+ var RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT = 3;
3904
+ var RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS = 6e4;
3905
+ async function reconcileRecipientPolicyProjectsOperation(store, options) {
3906
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT), RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT));
3907
+ const maintenanceNow = options.now ?? /* @__PURE__ */ new Date();
3908
+ const backoffMs = Math.max(0, Math.trunc(options.backoffMs ?? RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS));
3909
+ const retryBefore = new Date(maintenanceNow.getTime() - backoffMs).toISOString();
3910
+ const rows = store.db.prepare(`WITH projects AS (
3911
+ SELECT DISTINCT canonical_project_identity FROM project_recipients
3912
+ UNION
3913
+ SELECT canonical_project_identity FROM recipient_policy_authority_states
3914
+ )
3915
+ SELECT projects.canonical_project_identity
3916
+ FROM projects
3917
+ LEFT JOIN recipient_policy_authority_states authority
3918
+ ON authority.canonical_project_identity = projects.canonical_project_identity
3919
+ WHERE authority.safe_error_code IS NULL OR authority.last_attempt_at IS NULL
3920
+ OR authority.last_attempt_at <= ?
3921
+ ORDER BY CASE WHEN authority.last_attempt_at IS NULL THEN 0 ELSE 1 END,
3922
+ authority.last_attempt_at, projects.canonical_project_identity
3923
+ LIMIT ?`).all(retryBefore, limit);
3924
+ const reconcileProject = options.reconcileProject ?? reconcileRecipientPolicyProject;
3925
+ const result = {
3926
+ processed: 0,
3927
+ active: 0,
3928
+ waiting: 0,
3929
+ attention: 0,
3930
+ failed: 0,
3931
+ items: []
3932
+ };
3933
+ for (const row of rows) {
3934
+ result.processed += 1;
3032
3935
  try {
3033
- return JSON.parse(value);
3936
+ const outcome = await reconcileProject(store.db, {
3937
+ canonicalProjectIdentity: row.canonical_project_identity,
3938
+ leaseOwner: options.leaseOwner ?? `recipient-policy-maintenance:${store.deviceId || process.pid}`
3939
+ }, options.effects);
3940
+ if (outcome.status === "active") result.active += 1;
3941
+ else if (outcome.status === "needs_attention") result.attention += 1;
3942
+ else result.waiting += 1;
3943
+ result.items.push({
3944
+ canonicalProjectIdentity: row.canonical_project_identity,
3945
+ status: outcome.status,
3946
+ safeErrorCode: outcome.safeErrorCode
3947
+ });
3034
3948
  } catch {
3035
- return null;
3036
- }
3037
- };
3038
- const RECENTLY_COMPLETED_WINDOW_MS = 12e4;
3039
- app.get("/api/stats", (c) => {
3040
- const store = getStore();
3041
- const jobs = listMaintenanceJobs(store.db);
3042
- const now = Date.now();
3043
- const surfacedJobs = sortActiveMaintenanceJobs$1(jobs.filter((job) => {
3044
- if (job.status === "pending" || job.status === "running" || job.status === "failed") return true;
3045
- if (job.status === "completed" && job.finished_at) {
3046
- const finished = Date.parse(job.finished_at);
3047
- if (Number.isFinite(finished) && now - finished <= RECENTLY_COMPLETED_WINDOW_MS) return true;
3048
- }
3049
- return false;
3050
- })).map((job) => ({
3051
- kind: job.kind,
3052
- title: job.title,
3053
- status: job.status,
3054
- message: job.message,
3055
- progress: job.progress,
3056
- finished_at: job.finished_at,
3057
- error: job.error
3058
- }));
3059
- return c.json({
3060
- ...healthStats(store),
3061
- viewer_pid: process.pid,
3062
- maintenance_jobs: surfacedJobs
3063
- });
3064
- });
3065
- app.get("/api/runtime", (c) => {
3066
- return c.json({ version: VERSION$1 });
3067
- });
3068
- app.get("/api/usage", (c) => {
3069
- const store = getStore();
3070
- {
3071
- const projectFilter = c.req.query("project") || null;
3072
- const cacheKey = usageCacheKey(store, projectFilter);
3073
- const nowMs = Date.now();
3074
- const cached = usagePayloadCache.get(cacheKey);
3075
- if (cached && nowMs < cached.expiresAtMs) return c.json(cached.payload);
3076
- const globalAggregate = store.usageAggregate();
3077
- const eventsGlobal = mapAggregateEvents(globalAggregate);
3078
- const totalsGlobal = totalsFromAggregate(globalAggregate);
3079
- const filteredAggregate = projectFilter ? store.usageAggregate(projectFilter) : null;
3080
- const eventsFiltered = filteredAggregate ? mapAggregateEvents(filteredAggregate) : null;
3081
- const totalsFiltered = filteredAggregate ? totalsFromAggregate(filteredAggregate) : null;
3082
- const RECENT_PACK_SCAN_LIMIT = 200;
3083
- const loadRecentPackWindow = (project) => {
3084
- return (project ? store.db.prepare(`SELECT usage_events.id, usage_events.session_id, usage_events.event,
3085
- usage_events.tokens_read, usage_events.tokens_written, usage_events.tokens_saved,
3086
- usage_events.created_at, usage_events.metadata_json
3087
- FROM usage_events
3088
- JOIN sessions ON sessions.id = usage_events.session_id
3089
- WHERE sessions.project = ? AND usage_events.event = 'pack'
3090
- ORDER BY usage_events.created_at DESC
3091
- LIMIT ?`).all(project, RECENT_PACK_SCAN_LIMIT) : store.db.prepare(`SELECT id, session_id, event, tokens_read, tokens_written, tokens_saved,
3092
- created_at, metadata_json
3093
- FROM usage_events
3094
- WHERE event = 'pack'
3095
- ORDER BY created_at DESC
3096
- LIMIT ?`).all(RECENT_PACK_SCAN_LIMIT)).map((row) => toUsageRow(row, parseMetadataJson(row.metadata_json)));
3097
- };
3098
- const recentWindow = loadRecentPackWindow(projectFilter);
3099
- const recentVisibility = buildUsageVisibility(store, recentWindow);
3100
- const recentPacks = recentWindow.filter((row) => usageRowVisible(row, recentVisibility)).slice(0, 10).map((row) => ({
3101
- ...row,
3102
- metadata_json: sanitizePackUsageMetadata(row.metadata_json, recentVisibility)
3103
- }));
3104
- const payload = {
3105
- project: projectFilter,
3106
- events: projectFilter ? eventsFiltered : eventsGlobal,
3107
- totals: projectFilter ? totalsFiltered : totalsGlobal,
3108
- events_global: eventsGlobal,
3109
- totals_global: totalsGlobal,
3110
- events_filtered: eventsFiltered,
3111
- totals_filtered: totalsFiltered,
3112
- recent_packs: recentPacks
3113
- };
3114
- const setAtMs = Date.now();
3115
- sweepUsagePayloadCache(usagePayloadCache, setAtMs);
3116
- usagePayloadCache.set(cacheKey, {
3117
- payload,
3118
- expiresAtMs: setAtMs + USAGE_PAYLOAD_CACHE_MS
3949
+ result.failed += 1;
3950
+ result.items.push({
3951
+ canonicalProjectIdentity: row.canonical_project_identity,
3952
+ status: "failed",
3953
+ safeErrorCode: "recipient_policy_reconciliation_failed"
3119
3954
  });
3120
- return c.json(payload);
3121
3955
  }
3122
- });
3123
- return app;
3956
+ }
3957
+ return result;
3124
3958
  }
3125
- function healthStats(store) {
3959
+ //#endregion
3960
+ //#region src/application/sync-status.ts
3961
+ function traceSync(label, operation) {
3962
+ if (process.env.CODEMEM_TRACE_SYNC_STATUS !== "1") return operation();
3963
+ const startedAt = Date.now();
3964
+ console.warn(`[codemem sync-status] ${label} start`);
3965
+ try {
3966
+ return operation();
3967
+ } finally {
3968
+ console.warn(`[codemem sync-status] ${label} ${Date.now() - startedAt}ms`);
3969
+ }
3970
+ }
3971
+ function readRetentionState(store) {
3972
+ try {
3973
+ return traceSync("retentionState", () => drizzle(store.db, { schema }).select().from(schema.syncRetentionState).where(eq(schema.syncRetentionState.id, 1)).get());
3974
+ } catch {
3975
+ return;
3976
+ }
3977
+ }
3978
+ function readStatusRows(store) {
3979
+ const d = drizzle(store.db, { schema });
3980
+ const device = traceSync("deviceRow", () => d.select({
3981
+ device_id: schema.syncDevice.device_id,
3982
+ fingerprint: schema.syncDevice.fingerprint
3983
+ }).from(schema.syncDevice).limit(1).get());
3984
+ const daemon = traceSync("daemonState", () => d.select().from(schema.syncDaemonState).where(eq(schema.syncDaemonState.id, 1)).get());
3985
+ const peerCount = traceSync("peerCountRow", () => d.select({ total: count() }).from(schema.syncPeers).get());
3986
+ const retention = readRetentionState(store);
3987
+ const lastSync = traceSync("lastSyncRow", () => d.select({ last_sync_at: max(schema.syncPeers.last_sync_at) }).from(schema.syncPeers).get());
3126
3988
  return {
3127
- ...store.stats(),
3128
- automatic_recall: automaticRecallHealth(store.db, {
3129
- actorId: store.actorId,
3130
- deviceId: store.deviceId
3131
- })
3989
+ device,
3990
+ daemon,
3991
+ peerCount: Number(peerCount?.total ?? 0),
3992
+ retention,
3993
+ lastSyncAt: lastSync?.last_sync_at ?? null
3994
+ };
3995
+ }
3996
+ function enrollmentIssueStatus(store, showDiagnostics) {
3997
+ const summary = traceSync("coordinatorEnrollmentIssues", () => getCoordinatorEnrollmentReconciliationIssueSummary(store.db));
3998
+ if (!showDiagnostics) return { counts: summary.counts };
3999
+ return {
4000
+ counts: summary.counts,
4001
+ issues: summary.issues.map((issue) => ({
4002
+ coordinator_id: issue.coordinatorId,
4003
+ group_id: issue.groupId,
4004
+ kind: issue.kind,
4005
+ reference_id: issue.referenceId,
4006
+ code: issue.code,
4007
+ status: issue.status,
4008
+ first_seen_at: issue.firstSeenAt,
4009
+ last_seen_at: issue.lastSeenAt,
4010
+ resolved_at: issue.resolvedAt,
4011
+ occurrence_count: issue.occurrenceCount,
4012
+ updated_at: issue.updatedAt
4013
+ }))
3132
4014
  };
3133
4015
  }
4016
+ function initialDaemonState(config, rows, daemonRunning) {
4017
+ if (!config.syncEnabled) return "disabled";
4018
+ const lastError = rows.daemon?.last_error;
4019
+ const lastErrorAt = rows.daemon?.last_error_at;
4020
+ const lastOkAt = rows.daemon?.last_ok_at;
4021
+ if (lastError && (!lastOkAt || String(lastOkAt) < String(lastErrorAt ?? ""))) return "error";
4022
+ if (!daemonRunning) return "stopped";
4023
+ return "ok";
4024
+ }
4025
+ function retentionPayload(config, rows, retainedFloor) {
4026
+ const state = rows.retention;
4027
+ return {
4028
+ enabled: config.syncRetentionEnabled,
4029
+ max_age_days: config.syncRetentionMaxAgeDays,
4030
+ max_size_mb: config.syncRetentionMaxSizeMb,
4031
+ retained_floor_cursor: retainedFloor ?? state?.retained_floor_cursor ?? null,
4032
+ last_run_at: state?.last_run_at ?? null,
4033
+ last_duration_ms: typeof state?.last_duration_ms === "number" ? state.last_duration_ms : null,
4034
+ last_deleted_ops: typeof state?.last_deleted_ops === "number" ? state.last_deleted_ops : null,
4035
+ last_estimated_bytes_before: typeof state?.last_estimated_bytes_before === "number" ? state.last_estimated_bytes_before : null,
4036
+ last_estimated_bytes_after: typeof state?.last_estimated_bytes_after === "number" ? state.last_estimated_bytes_after : null,
4037
+ last_error: state?.last_error ?? null,
4038
+ last_error_at: state?.last_error_at ?? null
4039
+ };
4040
+ }
4041
+ function addDiagnosticStatusFields(payload, config, rows) {
4042
+ payload.device_id = rows.device?.device_id ?? null;
4043
+ payload.fingerprint = rows.device?.fingerprint ?? null;
4044
+ payload.bind = `${config.syncHost}:${config.syncPort}`;
4045
+ payload.daemon_last_error = rows.daemon?.last_error ?? null;
4046
+ payload.daemon_last_error_at = rows.daemon?.last_error_at ?? null;
4047
+ payload.daemon_last_ok_at = rows.daemon?.last_ok_at ?? null;
4048
+ }
4049
+ function buildBasePayload(input, config, rows, daemon, retainedFloor, semanticIndex, enrollmentIssues) {
4050
+ const payload = {
4051
+ enabled: config.syncEnabled,
4052
+ interval_s: config.syncIntervalS,
4053
+ retention: retentionPayload(config, rows, retainedFloor),
4054
+ semantic_index: semanticIndex,
4055
+ coordinator_enrollment_reconciliation_issues: enrollmentIssues,
4056
+ peer_count: rows.peerCount,
4057
+ last_sync_at: rows.lastSyncAt,
4058
+ daemon_state: daemon.state,
4059
+ daemon_running: daemon.running,
4060
+ daemon_detail: daemon.detail,
4061
+ project_filter_active: config.syncProjectsInclude.length > 0 || config.syncProjectsExclude.length > 0,
4062
+ project_filter: {
4063
+ include: config.syncProjectsInclude,
4064
+ exclude: config.syncProjectsExclude
4065
+ },
4066
+ cleanup_diagnostics: input.operations.cleanupDiagnostics(input.store, rows.device?.device_id ?? null, input.showDiagnostics),
4067
+ redacted: !input.showDiagnostics
4068
+ };
4069
+ if (input.showDiagnostics) addDiagnosticStatusFields(payload, config, rows);
4070
+ return payload;
4071
+ }
4072
+ function readBaseStatus(input) {
4073
+ const config = traceSync("readCoordinatorSyncConfig", () => readCoordinatorSyncConfig());
4074
+ const reset = traceSync("getSyncResetState", () => getSyncResetState(input.store.db));
4075
+ const rows = readStatusRows(input.store);
4076
+ const semanticIndex = traceSync("semanticIndex", () => input.operations.redactSemanticIndexDiagnostics(getSemanticIndexDiagnostics(input.store.db), input.showDiagnostics));
4077
+ const enrollmentIssues = enrollmentIssueStatus(input.store, input.showDiagnostics);
4078
+ const binding = traceSync("readViewerBinding", () => input.operations.readViewerBinding(input.store.dbPath));
4079
+ const running = Boolean(binding);
4080
+ const detail = binding ? `viewer pidfile at ${binding.host}:${binding.port}` : null;
4081
+ let state = initialDaemonState(config, rows, running);
4082
+ const statusPayload = buildBasePayload(input, config, rows, {
4083
+ state,
4084
+ running,
4085
+ detail
4086
+ }, reset.retained_floor_cursor ?? null, semanticIndex, enrollmentIssues);
4087
+ const runtime = input.getSyncRuntimeStatus?.() ?? null;
4088
+ if (runtime?.phase && runtime.phase !== "running") {
4089
+ state = runtime.phase;
4090
+ statusPayload.daemon_state = state;
4091
+ statusPayload.daemon_running = runtime.phase === "starting" || running;
4092
+ statusPayload.daemon_detail = runtime.detail ?? detail;
4093
+ }
4094
+ return {
4095
+ config,
4096
+ localDeviceId: rows.device?.device_id ?? null,
4097
+ statusPayload,
4098
+ daemonState: state,
4099
+ daemonRunning: running,
4100
+ daemonDetail: detail
4101
+ };
4102
+ }
4103
+ function readPeerStatus(input, localDeviceId) {
4104
+ const rows = traceSync("peerRows", () => input.store.db.prepare(PEERS_QUERY$1).all());
4105
+ const recentOperations = traceSync("recentPeerOps", () => input.operations.recentPeerOps(input.store));
4106
+ const scopeRejections = traceSync("recentScopeRejectionsByPeer", () => input.operations.recentScopeRejectionsByPeer(input.store));
4107
+ const items = rows.map((row) => {
4108
+ const peer = input.operations.mapPeerRow(input.store, row, input.showDiagnostics, recentOperations, scopeRejections, localDeviceId);
4109
+ peer.status = input.operations.peerStatus(peer);
4110
+ return peer;
4111
+ });
4112
+ const byId = {};
4113
+ for (const peer of items) byId[String(peer.peer_device_id)] = peer.status;
4114
+ return {
4115
+ items,
4116
+ byId
4117
+ };
4118
+ }
4119
+ function readAttemptRows(store) {
4120
+ return traceSync("attemptRows", () => drizzle(store.db, { schema }).select({
4121
+ peer_device_id: schema.syncAttempts.peer_device_id,
4122
+ ok: schema.syncAttempts.ok,
4123
+ error: schema.syncAttempts.error,
4124
+ started_at: schema.syncAttempts.started_at,
4125
+ finished_at: schema.syncAttempts.finished_at,
4126
+ ops_in: schema.syncAttempts.ops_in,
4127
+ ops_out: schema.syncAttempts.ops_out,
4128
+ local_sync_capability: schema.syncAttempts.local_sync_capability,
4129
+ peer_sync_capability: schema.syncAttempts.peer_sync_capability,
4130
+ negotiated_sync_capability: schema.syncAttempts.negotiated_sync_capability
4131
+ }).from(schema.syncAttempts).orderBy(desc(schema.syncAttempts.finished_at)).limit(25).all());
4132
+ }
4133
+ function readPeerAddressMap(store) {
4134
+ const rows = traceSync("peerAddressRows", () => store.db.prepare("SELECT peer_device_id, addresses_json FROM sync_peers").all());
4135
+ const addressesByPeer = /* @__PURE__ */ new Map();
4136
+ for (const row of rows) {
4137
+ const addresses = safeJsonList(row.addresses_json);
4138
+ if (addresses.length) addressesByPeer.set(String(row.peer_device_id ?? ""), addresses);
4139
+ }
4140
+ return addressesByPeer;
4141
+ }
4142
+ function readAttemptStatus(input) {
4143
+ const rows = readAttemptRows(input.store);
4144
+ const addressesByPeer = readPeerAddressMap(input.store);
4145
+ return {
4146
+ items: rows.map((row) => input.operations.mapSyncAttemptRow(row, input.showDiagnostics, input.showDiagnostics ? addressesByPeer.get(String(row.peer_device_id ?? "")) : void 0)),
4147
+ latestError: String(rows[0]?.error || "").trim()
4148
+ };
4149
+ }
4150
+ function peerStates(peers) {
4151
+ return new Set(peers.items.map((peer) => String(peer.status?.peer_state ?? "")));
4152
+ }
4153
+ function allPeersOffline(peers) {
4154
+ return peers.items.length > 0 && peers.items.every((peer) => String(peer.status?.peer_state ?? "") === "offline");
4155
+ }
4156
+ function degradedDaemonState(input, peers, attempts) {
4157
+ const states = peerStates(peers);
4158
+ const allOffline = allPeersOffline(peers);
4159
+ if (Boolean(attempts.items[0] && attempts.items[0].status === "error" && input.operations.isRecentIso(attempts.items[0].finished_at))) {
4160
+ if (states.has("online") || states.has("degraded")) return "degraded";
4161
+ if (allOffline) return "offline-peers";
4162
+ if (peers.items.length > 0) return "stale";
4163
+ }
4164
+ if (states.has("degraded")) return "degraded";
4165
+ if (allOffline) return "offline-peers";
4166
+ if (peers.items.length > 0 && !states.has("online")) return "stale";
4167
+ return "ok";
4168
+ }
4169
+ function applyDerivedDaemonState(input, base, peers, attempts, statusBlock) {
4170
+ let state = base.daemonState;
4171
+ if (state === "ok" && attempts.latestError.startsWith("needs_attention:")) {
4172
+ state = "needs_attention";
4173
+ const detail = attempts.latestError.replace(/^needs_attention:/, "");
4174
+ base.statusPayload.daemon_detail = detail;
4175
+ statusBlock.daemon_detail = detail;
4176
+ } else if (state === "ok") state = degradedDaemonState(input, peers, attempts);
4177
+ base.statusPayload.daemon_state = state;
4178
+ statusBlock.daemon_state = state;
4179
+ }
4180
+ async function readJoinRequests(input, config) {
4181
+ if (!input.includeJoinRequests || !input.showDiagnostics || !config.syncCoordinatorAdminSecret) return [];
4182
+ try {
4183
+ return await listCoordinatorJoinRequests(config);
4184
+ } catch {
4185
+ return [];
4186
+ }
4187
+ }
4188
+ async function buildSyncStatusResponse(input) {
4189
+ const base = readBaseStatus(input);
4190
+ const coordinatorSnapshot = await coordinatorStatusSnapshot(input.store, base.config);
4191
+ const coordinator = traceSync("coordinator", () => input.operations.redactCoordinatorStatus(coordinatorSnapshot, input.showDiagnostics));
4192
+ const peers = readPeerStatus(input, base.localDeviceId);
4193
+ const attempts = readAttemptStatus(input);
4194
+ const statusBlock = {
4195
+ ...base.statusPayload,
4196
+ background_maintenance: input.operations.summarizeMaintenanceJobs(listMaintenanceJobs(input.store.db), input.showDiagnostics),
4197
+ peers: peers.byId,
4198
+ pending: 0,
4199
+ sync: {},
4200
+ ping: {}
4201
+ };
4202
+ const legacyDevices = traceSync("legacyDevices", () => input.store.claimableLegacyDeviceIds());
4203
+ const legacyReview = traceSync("legacySharedReview", () => input.operations.legacySharedReviewSummary(input.store));
4204
+ const sharingReview = traceSync("sharingReview", () => input.store.sharingReviewSummary(input.project));
4205
+ const recipientPolicyReconciliation = traceSync("recipientPolicyReconciliation", () => input.operations.listRecipientPolicyReconciliationStatus(input.store));
4206
+ const joinRequests = await readJoinRequests(input, base.config);
4207
+ applyDerivedDaemonState(input, base, peers, attempts, statusBlock);
4208
+ const response = {
4209
+ ...base.statusPayload,
4210
+ status: statusBlock,
4211
+ peers: peers.items,
4212
+ attempts: attempts.items.slice(0, 5),
4213
+ legacy_devices: legacyDevices,
4214
+ legacy_shared_review: legacyReview,
4215
+ sharing_review: sharingReview,
4216
+ recipient_policy_reconciliation: recipientPolicyReconciliation,
4217
+ coordinator
4218
+ };
4219
+ if (input.includeJoinRequests && input.showDiagnostics) response.join_requests = joinRequests;
4220
+ return response;
4221
+ }
4222
+ var PEERS_QUERY$1 = `
4223
+ SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,
4224
+ p.last_seen_at, p.last_sync_at, p.last_error,
4225
+ p.runtime_version, p.runtime_version_observed_at,
4226
+ p.projects_include_json, p.projects_exclude_json, p.claimed_local_actor,
4227
+ p.actor_id, p.discovered_via_coordinator_id, p.discovered_via_group_id,
4228
+ a.display_name AS actor_display_name
4229
+ FROM sync_peers AS p
4230
+ LEFT JOIN actors AS a ON a.actor_id = p.actor_id
4231
+ ORDER BY name, peer_device_id
4232
+ `;
3134
4233
  //#endregion
3135
4234
  //#region src/routes/team-setup-view.ts
3136
4235
  var enabled = {
@@ -5165,18 +6264,31 @@ async function maybeGrantDefaultSpaceOnJoin(opts) {
5165
6264
  adminSecret: opts.config.syncCoordinatorAdminSecret || null
5166
6265
  });
5167
6266
  }
6267
+ function configuredGroups(config) {
6268
+ const rawGroups = config.sync_coordinator_groups;
6269
+ if (Array.isArray(rawGroups)) return rawGroups.map((group) => String(group).trim()).filter(Boolean);
6270
+ if (typeof rawGroups === "string") return rawGroups.split(",").map((group) => group.trim()).filter(Boolean);
6271
+ if (typeof config.sync_coordinator_group === "string" && config.sync_coordinator_group.trim()) return [config.sync_coordinator_group.trim()];
6272
+ return [];
6273
+ }
5168
6274
  function removeConfiguredCoordinatorGroup(groupId) {
5169
6275
  const targetGroup = groupId.trim();
5170
6276
  if (!targetGroup) return [];
5171
- const config = readCodememConfigFile();
5172
- const rawGroups = config.sync_coordinator_groups;
5173
- const groups = Array.isArray(rawGroups) ? rawGroups.map((group) => String(group).trim()).filter(Boolean) : typeof rawGroups === "string" ? rawGroups.split(",").map((group) => group.trim()).filter(Boolean) : typeof config.sync_coordinator_group === "string" && config.sync_coordinator_group.trim() ? [config.sync_coordinator_group.trim()] : [];
5174
- const nextGroups = groups.filter((group) => group !== targetGroup);
5175
- if (nextGroups.length === groups.length) return groups;
5176
- config.sync_coordinator_groups = nextGroups;
5177
- if (nextGroups.length) config.sync_coordinator_group = nextGroups[0];
5178
- else delete config.sync_coordinator_group;
5179
- writeCodememConfigFile(config);
6277
+ let nextGroups = [];
6278
+ mutateCodememConfigFile((config) => {
6279
+ const groups = configuredGroups(config);
6280
+ nextGroups = groups.filter((group) => group !== targetGroup);
6281
+ const configuredGroup = String(config.sync_coordinator_group ?? "").trim();
6282
+ const removesPluralGroup = nextGroups.length !== groups.length;
6283
+ const removesSingularGroup = configuredGroup === targetGroup;
6284
+ if (!removesPluralGroup && !removesSingularGroup) return void 0;
6285
+ config.sync_coordinator_groups = nextGroups;
6286
+ if (removesSingularGroup) {
6287
+ if (nextGroups.length) config.sync_coordinator_group = nextGroups[0];
6288
+ else delete config.sync_coordinator_group;
6289
+ }
6290
+ return config;
6291
+ });
5180
6292
  return nextGroups;
5181
6293
  }
5182
6294
  function coordinatorAdminUnavailable(status) {
@@ -5892,57 +7004,12 @@ async function advanceProjectShareOperation(store, operationId) {
5892
7004
  if ((await executeProjectShareProvisioning(store, operationId)).superseded) return {
5893
7005
  advanced: false,
5894
7006
  state: "cancelled"
5895
- };
5896
- return {
5897
- advanced: true,
5898
- state: "active"
5899
- };
5900
- }
5901
- var AUTOMATIC_SHARE_OPERATION_STATES = [
5902
- "waiting_for_acceptance",
5903
- "accepted",
5904
- "provisioning",
5905
- "initial_sync",
5906
- "waiting_for_device"
5907
- ];
5908
- var AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS = 3e4;
5909
- var AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS = 3e5;
5910
- var TERMINAL_SHARE_MAINTENANCE_ERRORS = /* @__PURE__ */ new Set([
5911
- "coordinator_not_configured",
5912
- "team_sharing_not_configured",
5913
- "team_selection_ambiguous",
5914
- "initiating_device_not_reviewed",
5915
- "inviter_project_access_ambiguous",
5916
- "managed_boundary_plan_missing",
5917
- "operation_device_binding_missing",
5918
- "operation_intent_invalid",
5919
- "provisioning_membership_plan_invalid",
5920
- "device_binding_conflict",
5921
- "intent_conflict",
5922
- "inviter_identity_conflict"
5923
- ]);
5924
- var PROJECT_INVITE_OWNER_CONFLICT_ERRORS = /* @__PURE__ */ new Set([
5925
- "operation_scope_mismatch",
5926
- "operation_state_invalid",
5927
- "operation_acceptance_invalid",
5928
- "operation_trust_state_invalid",
5929
- "operation_intent_invalid",
5930
- "operation_intent_mismatch",
5931
- "recipient_fingerprint_mismatch",
5932
- "recipient_device_identity_conflict",
5933
- "recipient_actor_conflict",
5934
- "pending_person_identity_conflict",
5935
- "device_binding_conflict",
5936
- "intent_conflict",
5937
- "inviter_identity_conflict"
5938
- ]);
5939
- var TERMINAL_RECONCILIATION_ERRORS = /* @__PURE__ */ new Set([
5940
- "coordinator_not_configured",
5941
- "team_sharing_not_configured",
5942
- "team_selection_ambiguous",
5943
- "operation_not_found",
5944
- ...PROJECT_INVITE_OWNER_CONFLICT_ERRORS
5945
- ]);
7007
+ };
7008
+ return {
7009
+ advanced: true,
7010
+ state: "active"
7011
+ };
7012
+ }
5946
7013
  function errorStatus(error) {
5947
7014
  const value = error && typeof error === "object" ? error.status : null;
5948
7015
  return typeof value === "number" && Number.isInteger(value) && value >= 400 && value <= 599 ? value : null;
@@ -5968,163 +7035,11 @@ function projectInviteOwnerErrorResponse(error, fallback) {
5968
7035
  };
5969
7036
  return fallback;
5970
7037
  }
5971
- function isRetryableReconciliationError(error) {
5972
- const message = error instanceof Error ? error.message : String(error);
5973
- if (TERMINAL_RECONCILIATION_ERRORS.has(message)) return false;
5974
- const status = errorStatus(error);
5975
- return status == null || status === 408 || status === 429 || status >= 500;
5976
- }
5977
- function safeReconciliationErrorCode(error) {
5978
- const message = error instanceof Error ? error.message : String(error);
5979
- return TERMINAL_RECONCILIATION_ERRORS.has(message) ? message : "operation_read_failed";
5980
- }
5981
- function recordInviteReconciliationFailure(store, operationId, error, now) {
5982
- const safeErrorCode = safeReconciliationErrorCode(error);
5983
- const retryable = isRetryableReconciliationError(error);
5984
- store.db.prepare(`UPDATE share_operation_steps SET
5985
- status = CASE WHEN ? = 1 THEN 'pending' ELSE 'failed' END,
5986
- attempt_count = attempt_count + 1, last_attempt_at = ?, safe_error_code = ?, updated_at = ?
5987
- WHERE operation_id = ? AND step_key = 'invite_consumption'`).run(retryable ? 1 : 0, now, safeErrorCode, now, operationId);
5988
- const outcome = retryable ? "retry_scheduled" : "needs_attention";
5989
- store.db.prepare("UPDATE share_operations SET state = ?, updated_at = ? WHERE operation_id = ?").run(outcome === "needs_attention" ? "needs_attention" : "waiting_for_acceptance", now, operationId);
5990
- return outcome;
5991
- }
5992
7038
  async function advancePendingProjectShares(store, options = {}) {
5993
- const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 3), 10));
5994
- const placeholders = AUTOMATIC_SHARE_OPERATION_STATES.map(() => "?").join(", ");
5995
- const maintenanceNow = options.now ?? /* @__PURE__ */ new Date();
5996
- const waitingAcceptanceRetryBefore = (/* @__PURE__ */ new Date(maintenanceNow.getTime() - AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS)).toISOString();
5997
- const waitingRetryBefore = (/* @__PURE__ */ new Date(maintenanceNow.getTime() - AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS)).toISOString();
5998
- const rows = store.db.prepare(`SELECT operation.operation_id FROM share_operations AS operation
5999
- WHERE operation.inviter_actor_id = ?
6000
- AND operation.state IN (${placeholders})
6001
- AND (operation.state <> 'waiting_for_acceptance' OR operation.updated_at <= ?)
6002
- AND (
6003
- operation.state <> 'waiting_for_device'
6004
- OR operation.updated_at <= ?
6005
- OR (
6006
- NOT EXISTS (
6007
- SELECT 1 FROM share_operation_steps AS step
6008
- WHERE step.operation_id = operation.operation_id
6009
- AND step.step_key = 'capability_preflight'
6010
- AND step.status <> 'completed'
6011
- AND (
6012
- step.status = 'running'
6013
- OR step.safe_error_code IN (
6014
- 'waiting_for_device', 'device_offline', 'recipient_offline'
6015
- )
6016
- )
6017
- )
6018
- AND EXISTS (
6019
- SELECT 1 FROM sync_attempts AS attempt
6020
- WHERE attempt.id = (
6021
- SELECT latest.id FROM sync_attempts AS latest
6022
- WHERE latest.peer_device_id = operation.recipient_device_id
6023
- ORDER BY latest.started_at DESC, latest.id DESC
6024
- LIMIT 1
6025
- )
6026
- AND attempt.ok = 1
6027
- AND julianday(COALESCE(attempt.finished_at, attempt.started_at)) >
6028
- julianday(operation.updated_at)
6029
- )
6030
- )
6031
- )
6032
- ORDER BY CASE WHEN operation.state IN ('accepted', 'provisioning', 'initial_sync') THEN 0 ELSE 1 END,
6033
- CASE WHEN operation.state IN ('waiting_for_acceptance', 'waiting_for_device')
6034
- THEN operation.updated_at ELSE operation.created_at END ASC,
6035
- operation.created_at ASC, operation.operation_id ASC
6036
- LIMIT ?`).all(store.actorId, ...AUTOMATIC_SHARE_OPERATION_STATES, waitingAcceptanceRetryBefore, waitingRetryBefore, limit);
6037
- const advanceOperation = options.advanceOperation ?? advanceProjectShareOperation;
6038
- const result = {
6039
- processed: 0,
6040
- advanced: 0,
6041
- waiting: 0,
6042
- attention: 0,
6043
- failed: 0,
6044
- items: []
6045
- };
6046
- for (const row of rows) {
6047
- result.processed += 1;
6048
- try {
6049
- const advanced = await advanceOperation(store, row.operation_id);
6050
- if (!advanced.advanced) {
6051
- if (advanced.state === "cancelled") {
6052
- result.items.push({
6053
- operationId: row.operation_id,
6054
- outcome: "superseded"
6055
- });
6056
- continue;
6057
- }
6058
- store.db.prepare("UPDATE share_operations SET updated_at = ? WHERE operation_id = ?").run(maintenanceNow.toISOString(), row.operation_id);
6059
- result.waiting += 1;
6060
- result.items.push({
6061
- operationId: row.operation_id,
6062
- outcome: "waiting_for_acceptance"
6063
- });
6064
- continue;
6065
- }
6066
- result.advanced += 1;
6067
- result.items.push({
6068
- operationId: row.operation_id,
6069
- outcome: "advanced"
6070
- });
6071
- } catch (error) {
6072
- const message = error instanceof Error ? error.message : String(error);
6073
- const state = store.db.prepare("SELECT state FROM share_operations WHERE operation_id = ?").pluck().get(row.operation_id);
6074
- if (state === "cancelled") {
6075
- result.items.push({
6076
- operationId: row.operation_id,
6077
- outcome: "superseded"
6078
- });
6079
- continue;
6080
- }
6081
- if (message === "waiting_for_device" || state === "waiting_for_device") {
6082
- result.waiting += 1;
6083
- result.items.push({
6084
- operationId: row.operation_id,
6085
- outcome: "waiting_for_device"
6086
- });
6087
- continue;
6088
- }
6089
- if (state === "needs_attention") {
6090
- result.attention += 1;
6091
- result.items.push({
6092
- operationId: row.operation_id,
6093
- outcome: "needs_attention",
6094
- error: message
6095
- });
6096
- continue;
6097
- }
6098
- if (state === "waiting_for_acceptance") {
6099
- const outcome = recordInviteReconciliationFailure(store, row.operation_id, error, maintenanceNow.toISOString());
6100
- if (outcome === "needs_attention") result.attention += 1;
6101
- else result.waiting += 1;
6102
- result.items.push({
6103
- operationId: row.operation_id,
6104
- outcome,
6105
- error: message
6106
- });
6107
- continue;
6108
- }
6109
- if (TERMINAL_SHARE_MAINTENANCE_ERRORS.has(message)) {
6110
- store.db.prepare("UPDATE share_operations SET state = 'needs_attention', updated_at = ? WHERE operation_id = ?").run(maintenanceNow.toISOString(), row.operation_id);
6111
- result.attention += 1;
6112
- result.items.push({
6113
- operationId: row.operation_id,
6114
- outcome: "needs_attention",
6115
- error: message
6116
- });
6117
- continue;
6118
- }
6119
- result.failed += 1;
6120
- result.items.push({
6121
- operationId: row.operation_id,
6122
- outcome: "failed",
6123
- error: message
6124
- });
6125
- }
6126
- }
6127
- return result;
7039
+ return advancePendingProjectSharesOperation(store, {
7040
+ ...options,
7041
+ advanceOperation: options.advanceOperation ?? advanceProjectShareOperation
7042
+ });
6128
7043
  }
6129
7044
  function coordinatorEnrollmentFailureCode(error) {
6130
7045
  const message = (error instanceof Error ? error.message : String(error)).trim();
@@ -6216,9 +7131,6 @@ async function reconcileConfiguredCoordinatorEnrollment(store, options = {}) {
6216
7131
  }
6217
7132
  return total;
6218
7133
  }
6219
- var RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT = 10;
6220
- var RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT = 3;
6221
- var RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS = 6e4;
6222
7134
  var RECIPIENT_POLICY_WAITING_ERRORS = /* @__PURE__ */ new Set([
6223
7135
  "recipient_policy_capability_undetermined",
6224
7136
  "recipient_policy_parity_incomplete",
@@ -6418,59 +7330,10 @@ function createRecipientPolicyReconcilerEffects(store, options = {}) {
6418
7330
  };
6419
7331
  }
6420
7332
  async function reconcileRecipientPolicyProjects(store, options = {}) {
6421
- const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT), RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT));
6422
- const maintenanceNow = options.now ?? /* @__PURE__ */ new Date();
6423
- const backoffMs = Math.max(0, Math.trunc(options.backoffMs ?? RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS));
6424
- const retryBefore = new Date(maintenanceNow.getTime() - backoffMs).toISOString();
6425
- const rows = store.db.prepare(`WITH projects AS (
6426
- SELECT DISTINCT canonical_project_identity FROM project_recipients
6427
- UNION
6428
- SELECT canonical_project_identity FROM recipient_policy_authority_states
6429
- )
6430
- SELECT projects.canonical_project_identity
6431
- FROM projects
6432
- LEFT JOIN recipient_policy_authority_states authority
6433
- ON authority.canonical_project_identity = projects.canonical_project_identity
6434
- WHERE authority.safe_error_code IS NULL OR authority.last_attempt_at IS NULL
6435
- OR authority.last_attempt_at <= ?
6436
- ORDER BY CASE WHEN authority.last_attempt_at IS NULL THEN 0 ELSE 1 END,
6437
- authority.last_attempt_at, projects.canonical_project_identity
6438
- LIMIT ?`).all(retryBefore, limit);
6439
- const effects = options.effects ?? createRecipientPolicyReconcilerEffects(store);
6440
- const reconcileProject = options.reconcileProject ?? reconcileRecipientPolicyProject;
6441
- const result = {
6442
- processed: 0,
6443
- active: 0,
6444
- waiting: 0,
6445
- attention: 0,
6446
- failed: 0,
6447
- items: []
6448
- };
6449
- for (const row of rows) {
6450
- result.processed += 1;
6451
- try {
6452
- const outcome = await reconcileProject(store.db, {
6453
- canonicalProjectIdentity: row.canonical_project_identity,
6454
- leaseOwner: options.leaseOwner ?? `recipient-policy-maintenance:${store.deviceId || process.pid}`
6455
- }, effects);
6456
- if (outcome.status === "active") result.active += 1;
6457
- else if (outcome.status === "needs_attention") result.attention += 1;
6458
- else result.waiting += 1;
6459
- result.items.push({
6460
- canonicalProjectIdentity: row.canonical_project_identity,
6461
- status: outcome.status,
6462
- safeErrorCode: outcome.safeErrorCode
6463
- });
6464
- } catch {
6465
- result.failed += 1;
6466
- result.items.push({
6467
- canonicalProjectIdentity: row.canonical_project_identity,
6468
- status: "failed",
6469
- safeErrorCode: "recipient_policy_reconciliation_failed"
6470
- });
6471
- }
6472
- }
6473
- return result;
7333
+ return reconcileRecipientPolicyProjectsOperation(store, {
7334
+ ...options,
7335
+ effects: options.effects ?? createRecipientPolicyReconcilerEffects(store)
7336
+ });
6474
7337
  }
6475
7338
  function recipientPolicyReadState(authority) {
6476
7339
  if (!authority) return "pending";
@@ -7045,10 +7908,15 @@ function ensureStableStoreIdentity(store) {
7045
7908
  return deviceId;
7046
7909
  }
7047
7910
  function ensureStableStoreDeviceIdentity(store) {
7048
- const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
7049
- store.adoptEnsuredDeviceIdentity(deviceId);
7911
+ const [deviceId] = ensureStableStoreDeviceIdentityWithFingerprint(store);
7050
7912
  return deviceId;
7051
7913
  }
7914
+ function ensureStableStoreDeviceIdentityWithFingerprint(store) {
7915
+ const identity = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
7916
+ const [deviceId] = identity;
7917
+ store.adoptEnsuredDeviceIdentity(deviceId);
7918
+ return identity;
7919
+ }
7052
7920
  function findPeerDeviceIdForAddress(store, address) {
7053
7921
  const rows = store.db.prepare("SELECT peer_device_id, addresses_json FROM sync_peers").all();
7054
7922
  for (const row of rows) if (safeJsonList(row.addresses_json).includes(address)) return String(row.peer_device_id ?? "").trim() || null;
@@ -7324,6 +8192,23 @@ function claimedLocalActorScopeStatus(store, row) {
7324
8192
  action_required: !grant.authorized
7325
8193
  };
7326
8194
  }
8195
+ function archivedCoordinatorGroupResponse(groupId, group, status, groups) {
8196
+ return {
8197
+ ok: true,
8198
+ group,
8199
+ status,
8200
+ disconnected_group_id: groupId,
8201
+ groups
8202
+ };
8203
+ }
8204
+ function missingArchivedCoordinatorGroupResponse(groupId, status, groups) {
8205
+ return {
8206
+ error: "group_not_found_or_already_archived",
8207
+ status,
8208
+ disconnected_group_id: groupId,
8209
+ groups
8210
+ };
8211
+ }
7327
8212
  /**
7328
8213
  * Map a raw sync_peers DB row to the API response shape.
7329
8214
  * When showDiag is false, sensitive fields (fingerprint, last_error, addresses)
@@ -7443,14 +8328,50 @@ function cleanupDiagnostics(store, localDeviceId, showDiag) {
7443
8328
  redacted: !showDiag
7444
8329
  };
7445
8330
  }
7446
- function legacySharedReviewSummary(store) {
7447
- const rows = legacySharedReviewRows(store);
7448
- const memoryCount = rows.length;
7449
- const lastUpdatedAt = rows.reduce((current, row) => row.updated_at && (!current || row.updated_at > current) ? row.updated_at : current, null);
8331
+ function legacySharedReviewSuggestedScope(candidate) {
8332
+ const resolvedScope = candidate?.resolved_scope_id ?? null;
8333
+ if (resolvedScope && resolvedScope !== LOCAL_DEFAULT_SCOPE_ID && resolvedScope !== LEGACY_SHARED_REVIEW_SCOPE_ID) return resolvedScope;
8334
+ return candidate?.suggested_scope_id ?? null;
8335
+ }
8336
+ function addLegacySharedReviewRow(groups, row, identity, candidate, isOwnedBySelf) {
8337
+ const existing = groups.get(identity.value);
8338
+ if (existing) {
8339
+ existing.memory_count += 1;
8340
+ if (isOwnedBySelf) existing.reassignable_memory_count += 1;
8341
+ else existing.peer_owned_memory_count += 1;
8342
+ if (row.updated_at && (!existing.last_updated_at || row.updated_at > existing.last_updated_at)) existing.last_updated_at = row.updated_at;
8343
+ if (existing.memory_sample_rows.length < LEGACY_SHARED_REVIEW_SAMPLE_LIMIT) existing.memory_sample_rows.push({
8344
+ is_owned_by_self: isOwnedBySelf,
8345
+ row
8346
+ });
8347
+ return;
8348
+ }
8349
+ const suggestedScope = legacySharedReviewSuggestedScope(candidate);
8350
+ groups.set(identity.value, {
8351
+ workspace_identity: identity.value,
8352
+ identity_source: identity.source,
8353
+ display_project: identity.displayProject ?? row.project ?? row.cwd ?? identity.value,
8354
+ memory_count: 1,
8355
+ reassignable_memory_count: isOwnedBySelf ? 1 : 0,
8356
+ peer_owned_memory_count: isOwnedBySelf ? 0 : 1,
8357
+ last_updated_at: row.updated_at ?? null,
8358
+ memory_sample_rows: [{
8359
+ is_owned_by_self: isOwnedBySelf,
8360
+ row
8361
+ }],
8362
+ suggested_scope_id: suggestedScope,
8363
+ suggestion_reason: suggestedScope ? candidate?.suggestion_reason ?? "Existing project mapping can be reviewed as a destination, but legacy data is not promoted automatically." : null
8364
+ });
8365
+ }
8366
+ function collectLegacySharedReviewGroups(store) {
7450
8367
  const ownedBySelf = store.buildOwnershipPredicate();
7451
8368
  const candidatesByIdentity = new Map(listProjectScopeCandidates(store.db, { limit: null }).map((candidate) => [candidate.workspace_identity, candidate]));
7452
- const groupsByIdentity = /* @__PURE__ */ new Map();
7453
- for (const row of rows) {
8369
+ const groups = /* @__PURE__ */ new Map();
8370
+ let memoryCount = 0;
8371
+ let lastUpdatedAt = null;
8372
+ for (const row of legacySharedReviewSummaryRows(store)) {
8373
+ memoryCount += 1;
8374
+ if (row.updated_at && (!lastUpdatedAt || row.updated_at > lastUpdatedAt)) lastUpdatedAt = row.updated_at;
7454
8375
  const identity = canonicalWorkspaceIdentity({
7455
8376
  cwd: row.cwd,
7456
8377
  gitBranch: row.git_branch,
@@ -7458,35 +8379,27 @@ function legacySharedReviewSummary(store) {
7458
8379
  project: row.project,
7459
8380
  workspaceId: row.workspace_id
7460
8381
  });
7461
- const candidate = candidatesByIdentity.get(identity.value);
7462
- const resolvedScope = candidate?.resolved_scope_id ?? null;
7463
- const suggestedScope = resolvedScope && resolvedScope !== LOCAL_DEFAULT_SCOPE_ID && resolvedScope !== LEGACY_SHARED_REVIEW_SCOPE_ID ? resolvedScope : candidate?.suggested_scope_id;
7464
- const existing = groupsByIdentity.get(identity.value);
7465
- const isOwnedBySelf = ownedBySelf(row);
7466
- const rowUpdatedAt = row.updated_at ?? null;
7467
- if (existing) {
7468
- existing.memory_count += 1;
7469
- if (isOwnedBySelf) existing.reassignable_memory_count += 1;
7470
- else existing.peer_owned_memory_count += 1;
7471
- if (rowUpdatedAt && (!existing.last_updated_at || rowUpdatedAt > existing.last_updated_at)) existing.last_updated_at = rowUpdatedAt;
7472
- if (existing.memory_samples.length < LEGACY_SHARED_REVIEW_SAMPLE_LIMIT) existing.memory_samples.push(legacySharedReviewMemorySample(row, isOwnedBySelf));
7473
- continue;
7474
- }
7475
- groupsByIdentity.set(identity.value, {
7476
- workspace_identity: identity.value,
7477
- identity_source: identity.source,
7478
- display_project: identity.displayProject ?? row.project ?? row.cwd ?? identity.value,
7479
- memory_count: 1,
7480
- reassignable_memory_count: isOwnedBySelf ? 1 : 0,
7481
- peer_owned_memory_count: isOwnedBySelf ? 0 : 1,
7482
- last_updated_at: rowUpdatedAt,
7483
- memory_samples: [legacySharedReviewMemorySample(row, isOwnedBySelf)],
7484
- suggested_scope_id: suggestedScope ?? null,
7485
- suggestion_reason: suggestedScope ? candidate?.suggestion_reason ?? "Existing project mapping can be reviewed as a destination, but legacy data is not promoted automatically." : null
7486
- });
8382
+ addLegacySharedReviewRow(groups, row, identity, candidatesByIdentity.get(identity.value), ownedBySelf(row));
7487
8383
  }
7488
- const totalGroupCount = groupsByIdentity.size;
7489
- const groups = [...groupsByIdentity.values()].sort((left, right) => right.memory_count - left.memory_count || (right.last_updated_at ?? "").localeCompare(left.last_updated_at ?? "") || left.workspace_identity.localeCompare(right.workspace_identity));
8384
+ return {
8385
+ groups,
8386
+ lastUpdatedAt,
8387
+ memoryCount
8388
+ };
8389
+ }
8390
+ function materializeLegacySharedReviewGroups(store, groupsByIdentity) {
8391
+ const sampleBodies = legacySharedReviewSampleBodies(store, [...groupsByIdentity.values()].flatMap((group) => group.memory_sample_rows.map((sample) => sample.row.id)));
8392
+ return [...groupsByIdentity.values()].map(({ memory_sample_rows: sampleRows, ...group }) => ({
8393
+ ...group,
8394
+ memory_samples: sampleRows.map(({ is_owned_by_self: isOwnedBySelf, row }) => legacySharedReviewMemorySample({
8395
+ ...row,
8396
+ body_text: sampleBodies.get(row.id) ?? null
8397
+ }, isOwnedBySelf))
8398
+ })).sort((left, right) => right.memory_count - left.memory_count || (right.last_updated_at ?? "").localeCompare(left.last_updated_at ?? "") || left.workspace_identity.localeCompare(right.workspace_identity));
8399
+ }
8400
+ function legacySharedReviewSummary(store) {
8401
+ const collected = collectLegacySharedReviewGroups(store);
8402
+ const groups = materializeLegacySharedReviewGroups(store, collected.groups);
7490
8403
  const targetScopes = listSharingDomainSettingsScopes(store.db).filter((scope) => scope.scope_id !== LOCAL_DEFAULT_SCOPE_ID && scope.scope_id !== LEGACY_SHARED_REVIEW_SCOPE_ID).map((scope) => ({
7491
8404
  authority_type: scope.authority_type,
7492
8405
  label: scope.label || scope.scope_id,
@@ -7494,16 +8407,17 @@ function legacySharedReviewSummary(store) {
7494
8407
  }));
7495
8408
  return {
7496
8409
  scope_id: LEGACY_SHARED_REVIEW_SCOPE_ID,
7497
- memory_count: memoryCount,
7498
- has_data: memoryCount > 0,
7499
- last_updated_at: lastUpdatedAt,
8410
+ memory_count: collected.memoryCount,
8411
+ has_data: collected.memoryCount > 0,
8412
+ last_updated_at: collected.lastUpdatedAt,
7500
8413
  groups,
7501
- total_group_count: totalGroupCount,
8414
+ total_group_count: collected.groups.size,
7502
8415
  target_scopes: targetScopes
7503
8416
  };
7504
8417
  }
7505
8418
  var LEGACY_SHARED_REVIEW_SAMPLE_LIMIT = 3;
7506
8419
  var LEGACY_SHARED_REVIEW_BODY_PREVIEW_SQL_LIMIT = 360;
8420
+ var LEGACY_SHARED_REVIEW_SAMPLE_BODY_BATCH_SIZE = 500;
7507
8421
  function previewText(value, limit = 180) {
7508
8422
  const normalized = value?.replace(/\s+/g, " ").trim();
7509
8423
  if (!normalized) return null;
@@ -7523,6 +8437,41 @@ function legacySharedReviewMemorySample(row, isOwnedBySelf) {
7523
8437
  git_remote: row.git_remote ?? null
7524
8438
  };
7525
8439
  }
8440
+ function legacySharedReviewSummaryRows(store) {
8441
+ return store.db.prepare(`SELECT m.id,
8442
+ m.created_at,
8443
+ m.updated_at,
8444
+ m.kind,
8445
+ m.title,
8446
+ s.project,
8447
+ s.cwd,
8448
+ s.git_remote,
8449
+ s.git_branch,
8450
+ m.workspace_id,
8451
+ m.actor_id,
8452
+ m.origin_device_id,
8453
+ m.metadata_json
8454
+ FROM memory_items m
8455
+ LEFT JOIN sessions s ON s.id = m.session_id
8456
+ WHERE m.scope_id = ?
8457
+ AND m.active = 1
8458
+ AND m.deleted_at IS NULL
8459
+ ORDER BY COALESCE(m.updated_at, m.created_at, '') DESC, m.id DESC`).iterate(LEGACY_SHARED_REVIEW_SCOPE_ID);
8460
+ }
8461
+ function legacySharedReviewSampleBodies(store, memoryIds) {
8462
+ const bodies = /* @__PURE__ */ new Map();
8463
+ for (let offset = 0; offset < memoryIds.length; offset += LEGACY_SHARED_REVIEW_SAMPLE_BODY_BATCH_SIZE) {
8464
+ const ids = memoryIds.slice(offset, offset + LEGACY_SHARED_REVIEW_SAMPLE_BODY_BATCH_SIZE);
8465
+ const rows = store.db.prepare(`SELECT id, substr(body_text, 1, ?) AS body_text
8466
+ FROM memory_items
8467
+ WHERE id IN (${ids.map(() => "?").join(",")})
8468
+ AND scope_id = ?
8469
+ AND active = 1
8470
+ AND deleted_at IS NULL`).all(LEGACY_SHARED_REVIEW_BODY_PREVIEW_SQL_LIMIT, ...ids, LEGACY_SHARED_REVIEW_SCOPE_ID);
8471
+ for (const row of rows) if (row.body_text != null) bodies.set(row.id, row.body_text);
8472
+ }
8473
+ return bodies;
8474
+ }
7526
8475
  function legacySharedReviewRows(store) {
7527
8476
  return store.db.prepare(`SELECT m.id,
7528
8477
  m.rev,
@@ -8308,198 +9257,29 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
8308
9257
  }
8309
9258
  });
8310
9259
  app.get("/api/sync/status", async (c) => {
8311
- const store = getStore();
8312
- {
8313
- const traceSync = (label, fn) => {
8314
- if (process.env.CODEMEM_TRACE_SYNC_STATUS !== "1") return fn();
8315
- const startedAt = Date.now();
8316
- console.warn(`[codemem sync-status] ${label} start`);
8317
- try {
8318
- return fn();
8319
- } finally {
8320
- console.warn(`[codemem sync-status] ${label} ${Date.now() - startedAt}ms`);
8321
- }
8322
- };
8323
- const showDiag = queryBool(c.req.query("includeDiagnostics"));
8324
- const includeJoinRequests = queryBool(c.req.query("includeJoinRequests"));
8325
- const project = c.req.query("project") || null;
8326
- const config = traceSync("readCoordinatorSyncConfig", () => readCoordinatorSyncConfig());
8327
- const syncReset = traceSync("getSyncResetState", () => getSyncResetState(store.db));
8328
- const d = drizzle(store.db, { schema });
8329
- const deviceRow = traceSync("deviceRow", () => d.select({
8330
- device_id: schema.syncDevice.device_id,
8331
- fingerprint: schema.syncDevice.fingerprint
8332
- }).from(schema.syncDevice).limit(1).get());
8333
- const daemonState = traceSync("daemonState", () => d.select().from(schema.syncDaemonState).where(eq(schema.syncDaemonState.id, 1)).get());
8334
- const peerCountRow = traceSync("peerCountRow", () => d.select({ total: count() }).from(schema.syncPeers).get());
8335
- let retentionState;
8336
- try {
8337
- retentionState = traceSync("retentionState", () => d.select().from(schema.syncRetentionState).where(eq(schema.syncRetentionState.id, 1)).get());
8338
- } catch {
8339
- retentionState = void 0;
8340
- }
8341
- const lastSyncRow = traceSync("lastSyncRow", () => d.select({ last_sync_at: max(schema.syncPeers.last_sync_at) }).from(schema.syncPeers).get());
8342
- const semanticIndex = traceSync("semanticIndex", () => redactSemanticIndexDiagnostics(getSemanticIndexDiagnostics(store.db), showDiag));
8343
- const enrollmentIssueSummary = traceSync("coordinatorEnrollmentIssues", () => getCoordinatorEnrollmentReconciliationIssueSummary(store.db));
8344
- const enrollmentIssueStatus = {
8345
- counts: enrollmentIssueSummary.counts,
8346
- ...showDiag ? { issues: enrollmentIssueSummary.issues.map((issue) => ({
8347
- coordinator_id: issue.coordinatorId,
8348
- group_id: issue.groupId,
8349
- kind: issue.kind,
8350
- reference_id: issue.referenceId,
8351
- code: issue.code,
8352
- status: issue.status,
8353
- first_seen_at: issue.firstSeenAt,
8354
- last_seen_at: issue.lastSeenAt,
8355
- resolved_at: issue.resolvedAt,
8356
- occurrence_count: issue.occurrenceCount,
8357
- updated_at: issue.updatedAt
8358
- })) } : {}
8359
- };
8360
- const lastError = daemonState?.last_error;
8361
- const lastErrorAt = daemonState?.last_error_at;
8362
- const lastOkAt = daemonState?.last_ok_at;
8363
- const viewerBinding = traceSync("readViewerBinding", () => readViewerBinding(store.dbPath));
8364
- const daemonRunning = Boolean(viewerBinding);
8365
- const daemonDetail = viewerBinding ? `viewer pidfile at ${viewerBinding.host}:${viewerBinding.port}` : null;
8366
- let daemonStateValue = "ok";
8367
- if (!config.syncEnabled) daemonStateValue = "disabled";
8368
- else if (lastError && (!lastOkAt || String(lastOkAt) < String(lastErrorAt ?? ""))) daemonStateValue = "error";
8369
- else if (!daemonRunning) daemonStateValue = "stopped";
8370
- const statusPayload = {
8371
- enabled: config.syncEnabled,
8372
- interval_s: config.syncIntervalS,
8373
- retention: {
8374
- enabled: config.syncRetentionEnabled,
8375
- max_age_days: config.syncRetentionMaxAgeDays,
8376
- max_size_mb: config.syncRetentionMaxSizeMb,
8377
- retained_floor_cursor: syncReset.retained_floor_cursor ?? retentionState?.retained_floor_cursor ?? null,
8378
- last_run_at: retentionState?.last_run_at ?? null,
8379
- last_duration_ms: typeof retentionState?.last_duration_ms === "number" ? retentionState.last_duration_ms : null,
8380
- last_deleted_ops: typeof retentionState?.last_deleted_ops === "number" ? retentionState.last_deleted_ops : null,
8381
- last_estimated_bytes_before: typeof retentionState?.last_estimated_bytes_before === "number" ? retentionState.last_estimated_bytes_before : null,
8382
- last_estimated_bytes_after: typeof retentionState?.last_estimated_bytes_after === "number" ? retentionState.last_estimated_bytes_after : null,
8383
- last_error: retentionState?.last_error ?? null,
8384
- last_error_at: retentionState?.last_error_at ?? null
8385
- },
8386
- semantic_index: semanticIndex,
8387
- coordinator_enrollment_reconciliation_issues: enrollmentIssueStatus,
8388
- peer_count: Number(peerCountRow?.total ?? 0),
8389
- last_sync_at: lastSyncRow?.last_sync_at ?? null,
8390
- daemon_state: daemonStateValue,
8391
- daemon_running: daemonRunning,
8392
- daemon_detail: daemonDetail,
8393
- project_filter_active: config.syncProjectsInclude.length > 0 || config.syncProjectsExclude.length > 0,
8394
- project_filter: {
8395
- include: config.syncProjectsInclude,
8396
- exclude: config.syncProjectsExclude
8397
- },
8398
- cleanup_diagnostics: cleanupDiagnostics(store, deviceRow?.device_id ?? null, showDiag),
8399
- redacted: !showDiag
8400
- };
8401
- if (showDiag) {
8402
- statusPayload.device_id = deviceRow?.device_id ?? null;
8403
- statusPayload.fingerprint = deviceRow?.fingerprint ?? null;
8404
- statusPayload.bind = `${config.syncHost}:${config.syncPort}`;
8405
- statusPayload.daemon_last_error = lastError;
8406
- statusPayload.daemon_last_error_at = lastErrorAt;
8407
- statusPayload.daemon_last_ok_at = lastOkAt;
8408
- }
8409
- const runtimeStatus = getSyncRuntimeStatus?.() ?? null;
8410
- if (runtimeStatus?.phase && runtimeStatus.phase !== "running") {
8411
- daemonStateValue = runtimeStatus.phase;
8412
- statusPayload.daemon_state = daemonStateValue;
8413
- statusPayload.daemon_running = runtimeStatus.phase === "starting" || daemonRunning;
8414
- statusPayload.daemon_detail = runtimeStatus.detail ?? daemonDetail;
8415
- }
8416
- const coordinatorSnapshot = await coordinatorStatusSnapshot(store, config);
8417
- const coordinator = traceSync("coordinator", () => redactCoordinatorStatus(coordinatorSnapshot, showDiag));
8418
- const peerRows = traceSync("peerRows", () => store.db.prepare(PEERS_QUERY).all());
8419
- const recentOpsByPeer = traceSync("recentPeerOps", () => recentPeerOps(store));
8420
- const scopeRejectionsByPeer = traceSync("recentScopeRejectionsByPeer", () => recentScopeRejectionsByPeer(store));
8421
- const peersItems = peerRows.map((row) => {
8422
- const peer = mapPeerRow(store, row, showDiag, recentOpsByPeer, scopeRejectionsByPeer, deviceRow?.device_id ?? null);
8423
- peer.status = peerStatus(peer);
8424
- return peer;
8425
- });
8426
- const peersMap = {};
8427
- for (const peer of peersItems) peersMap[String(peer.peer_device_id)] = peer.status;
8428
- const attemptRows = traceSync("attemptRows", () => d.select({
8429
- peer_device_id: schema.syncAttempts.peer_device_id,
8430
- ok: schema.syncAttempts.ok,
8431
- error: schema.syncAttempts.error,
8432
- started_at: schema.syncAttempts.started_at,
8433
- finished_at: schema.syncAttempts.finished_at,
8434
- ops_in: schema.syncAttempts.ops_in,
8435
- ops_out: schema.syncAttempts.ops_out,
8436
- local_sync_capability: schema.syncAttempts.local_sync_capability,
8437
- peer_sync_capability: schema.syncAttempts.peer_sync_capability,
8438
- negotiated_sync_capability: schema.syncAttempts.negotiated_sync_capability
8439
- }).from(schema.syncAttempts).orderBy(desc(schema.syncAttempts.finished_at)).limit(25).all());
8440
- const peerAddressMap = /* @__PURE__ */ new Map();
8441
- traceSync("peerAddressRows", () => store.db.prepare("SELECT peer_device_id, addresses_json FROM sync_peers").all()).forEach((peerRow) => {
8442
- const addrs = safeJsonList(peerRow.addresses_json);
8443
- if (addrs.length) peerAddressMap.set(String(peerRow.peer_device_id ?? ""), addrs);
8444
- });
8445
- const attemptsItems = attemptRows.map((row) => {
8446
- const addrs = showDiag ? peerAddressMap.get(String(row.peer_device_id ?? "")) : void 0;
8447
- return mapSyncAttemptRow(row, showDiag, addrs);
8448
- });
8449
- const latestAttemptError = String(attemptRows[0]?.error || "").trim();
8450
- const statusBlock = {
8451
- ...statusPayload,
8452
- background_maintenance: summarizeMaintenanceJobs(listMaintenanceJobs(store.db), showDiag),
8453
- peers: peersMap,
8454
- pending: 0,
8455
- sync: {},
8456
- ping: {}
8457
- };
8458
- const legacyDevices = traceSync("legacyDevices", () => store.claimableLegacyDeviceIds());
8459
- const legacyReview = traceSync("legacySharedReview", () => legacySharedReviewSummary(store));
8460
- const sharingReview = traceSync("sharingReview", () => store.sharingReviewSummary(project));
8461
- const recipientPolicyReconciliation = traceSync("recipientPolicyReconciliation", () => listRecipientPolicyReconciliationStatus(store));
8462
- let joinRequests = [];
8463
- if (includeJoinRequests && showDiag && config.syncCoordinatorAdminSecret) try {
8464
- joinRequests = await listCoordinatorJoinRequests(config);
8465
- } catch {
8466
- joinRequests = [];
9260
+ const response = await buildSyncStatusResponse({
9261
+ store: getStore(),
9262
+ showDiagnostics: queryBool(c.req.query("includeDiagnostics")),
9263
+ includeJoinRequests: queryBool(c.req.query("includeJoinRequests")),
9264
+ project: c.req.query("project") || null,
9265
+ getSyncRuntimeStatus,
9266
+ operations: {
9267
+ cleanupDiagnostics,
9268
+ isRecentIso,
9269
+ legacySharedReviewSummary,
9270
+ listRecipientPolicyReconciliationStatus,
9271
+ mapPeerRow,
9272
+ mapSyncAttemptRow,
9273
+ peerStatus,
9274
+ readViewerBinding,
9275
+ recentPeerOps,
9276
+ recentScopeRejectionsByPeer,
9277
+ redactCoordinatorStatus,
9278
+ redactSemanticIndexDiagnostics,
9279
+ summarizeMaintenanceJobs
8467
9280
  }
8468
- if (daemonStateValue === "ok" && latestAttemptError.startsWith("needs_attention:")) {
8469
- daemonStateValue = "needs_attention";
8470
- statusPayload.daemon_state = daemonStateValue;
8471
- statusPayload.daemon_detail = latestAttemptError.replace(/^needs_attention:/, "");
8472
- statusBlock.daemon_state = daemonStateValue;
8473
- statusBlock.daemon_detail = latestAttemptError.replace(/^needs_attention:/, "");
8474
- }
8475
- if (daemonStateValue === "ok") {
8476
- const peerStates = new Set(peersItems.map((peer) => String(peer.status?.peer_state ?? "")));
8477
- const latestFailedRecently = Boolean(attemptsItems[0] && attemptsItems[0].status === "error" && isRecentIso(attemptsItems[0].finished_at));
8478
- const allOffline = peersItems.length > 0 && peersItems.every((peer) => String(peer.status?.peer_state ?? "") === "offline");
8479
- if (latestFailedRecently) {
8480
- if (peerStates.has("online") || peerStates.has("degraded")) daemonStateValue = "degraded";
8481
- else if (allOffline) daemonStateValue = "offline-peers";
8482
- else if (peersItems.length > 0) daemonStateValue = "stale";
8483
- } else if (peerStates.has("degraded")) daemonStateValue = "degraded";
8484
- else if (allOffline) daemonStateValue = "offline-peers";
8485
- else if (peersItems.length > 0 && !peerStates.has("online")) daemonStateValue = "stale";
8486
- statusPayload.daemon_state = daemonStateValue;
8487
- statusBlock.daemon_state = daemonStateValue;
8488
- }
8489
- const responsePayload = {
8490
- ...statusPayload,
8491
- status: statusBlock,
8492
- peers: peersItems,
8493
- attempts: attemptsItems.slice(0, 5),
8494
- legacy_devices: legacyDevices,
8495
- legacy_shared_review: legacyReview,
8496
- sharing_review: sharingReview,
8497
- recipient_policy_reconciliation: recipientPolicyReconciliation,
8498
- coordinator
8499
- };
8500
- if (includeJoinRequests && showDiag) responsePayload.join_requests = joinRequests;
8501
- return c.json(responsePayload);
8502
- }
9281
+ });
9282
+ return c.json(response);
8503
9283
  });
8504
9284
  app.get("/api/sync/peers", (c) => {
8505
9285
  const store = getStore();
@@ -8592,7 +9372,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
8592
9372
  let publicKey;
8593
9373
  let fingerprint;
8594
9374
  try {
8595
- const [id, fp] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
9375
+ const [id, fp] = ensureStableStoreDeviceIdentityWithFingerprint(store);
8596
9376
  deviceId = id;
8597
9377
  fingerprint = fp;
8598
9378
  publicKey = d.select({ public_key: schema.syncDevice.public_key }).from(schema.syncDevice).where(eq(schema.syncDevice.device_id, id)).get()?.public_key ?? "";
@@ -10021,18 +10801,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10021
10801
  remoteUrl: config.syncCoordinatorUrl || null,
10022
10802
  adminSecret: config.syncCoordinatorAdminSecret || null
10023
10803
  });
10024
- if (!group) return c.json({
10025
- error: "group_not_found_or_already_archived",
10026
- status
10027
- }, 404);
10028
10804
  const groups = removeConfiguredCoordinatorGroup(groupId);
10029
- return c.json({
10030
- ok: true,
10031
- group,
10032
- status: coordinatorAdminStatusPayload(),
10033
- disconnected_group_id: groupId,
10034
- groups
10035
- });
10805
+ const currentStatus = coordinatorAdminStatusPayload();
10806
+ if (!group) return c.json(missingArchivedCoordinatorGroupResponse(groupId, currentStatus, groups), 404);
10807
+ return c.json(archivedCoordinatorGroupResponse(groupId, group, currentStatus, groups));
10036
10808
  } catch (error) {
10037
10809
  return c.json({
10038
10810
  error: error instanceof Error ? error.message : String(error),
@@ -11180,7 +11952,7 @@ function createApp(opts) {
11180
11952
  getObserver: () => observer
11181
11953
  }));
11182
11954
  app.route("/", configRoutes({ getSweeper: () => sweeper }));
11183
- app.route("/", rawEventsRoutes(storeFactory, sweeper));
11955
+ app.route("/", rawEventsRoutes(storeFactory, sweeper, opts?.rawEventInbox, opts?.rawEventTarget));
11184
11956
  app.route("/", syncRoutes(storeFactory, getSyncRuntimeStatus, {
11185
11957
  loadDeviceIdentityCoordinatorEvidence: opts?.loadDeviceIdentityCoordinatorEvidence,
11186
11958
  onRecipientPolicyTeamRenamed: () => invalidateTeamSetupSummary(),
@@ -11228,6 +12000,6 @@ function createSyncApp(opts) {
11228
12000
  return app;
11229
12001
  }
11230
12002
  //#endregion
11231
- export { VERSION, advancePendingProjectShares, advanceProjectShareOperation, closeStore, createApp, createRecipientPolicyReconcilerEffects, createSyncApp, getStore, listRecipientPolicyReconciliationStatus, recipientPolicyCapabilityFromStatus, reconcileConfiguredCoordinatorEnrollment, reconcileRecipientPolicyProjects };
12003
+ export { FileRawEventInbox, VERSION, advancePendingProjectShares, advancePendingProjectSharesOperation, advanceProjectShareOperation, closeStore, createApp, createRecipientPolicyReconcilerEffects, createSyncApp, createViewerRawEventInbox, getStore, listRecipientPolicyReconciliationStatus, recipientPolicyCapabilityFromStatus, reconcileConfiguredCoordinatorEnrollment, reconcileRecipientPolicyProjects, reconcileRecipientPolicyProjectsOperation, resolveRawEventInboxDirectory };
11232
12004
 
11233
12005
  //# sourceMappingURL=index.js.map