@codemem/server 0.45.0 → 0.46.0-alpha.2

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, buildPiFlushSignalFromEvent, 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,494 @@ 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, boundary) {
2679
+ return {
2680
+ version: 1,
2681
+ request,
2682
+ flush_boundary: flushBoundary,
2683
+ ...boundary ? { boundary: {
2684
+ source: boundary.source,
2685
+ stream_id: boundary.streamId,
2686
+ ...boundary.id ? { id: boundary.id } : {}
2687
+ } } : {}
2688
+ };
2689
+ }
2690
+ function storedEntry(request, flushBoundary, boundary, enqueueOrder) {
2691
+ return {
2692
+ ...entryContent(request, flushBoundary, boundary),
2693
+ enqueue_order: enqueueOrder.toString()
2694
+ };
2695
+ }
2696
+ function entryContentId(entry) {
2697
+ return contentId(JSON.stringify(entryContent(entry.request, entry.flushBoundary, entry.boundary)));
2698
+ }
2699
+ function inboxFileName(enqueueOrder, id) {
2700
+ return `${enqueueOrder.toString().padStart(24, "0")}-${id}.json`;
2701
+ }
2702
+ function parseInboxFileName(name) {
2703
+ const match = /^(\d+)-([a-f0-9]{64})\.json$/u.exec(name);
2704
+ if (!match?.[1] || !match[2]) return null;
2705
+ const enqueueOrder = BigInt(match[1]);
2706
+ if (enqueueOrder < 1n) return null;
2707
+ return {
2708
+ id: match[2],
2709
+ enqueueOrder
2710
+ };
2711
+ }
2712
+ async function writeDurableFile(path, contents) {
2713
+ const handle = await open(path, "wx", 384);
2714
+ try {
2715
+ await handle.writeFile(contents, { encoding: "utf8" });
2716
+ await handle.sync();
2717
+ } finally {
2718
+ await handle.close();
2719
+ }
2720
+ }
2721
+ async function syncDirectory(path) {
2722
+ let handle;
2723
+ try {
2724
+ handle = await open(path, "r");
2725
+ await handle.sync();
2726
+ } catch (error) {
2727
+ const code = error.code;
2728
+ if (process.platform === "win32" && [
2729
+ "EISDIR",
2730
+ "EINVAL",
2731
+ "ENOTSUP",
2732
+ "EPERM"
2733
+ ].includes(code ?? "")) return;
2734
+ throw error;
2735
+ } finally {
2736
+ await handle?.close();
2737
+ }
2738
+ }
2739
+ async function ensurePrivateDirectory(path) {
2740
+ const firstCreated = await mkdir(path, {
2741
+ recursive: true,
2742
+ mode: 448
2743
+ });
2744
+ await chmod(path, 448);
2745
+ if (!firstCreated) return;
2746
+ const boundary = dirname(firstCreated);
2747
+ let current = path;
2748
+ while (true) {
2749
+ await syncDirectory(current);
2750
+ if (current === boundary) return;
2751
+ current = dirname(current);
2752
+ }
2753
+ }
2754
+ function parseStoredEntry(serialized) {
2755
+ const parsed = JSON.parse(serialized);
2756
+ 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");
2757
+ const storedBoundary = parsed.boundary;
2758
+ if (storedBoundary !== void 0 && (typeof storedBoundary.source !== "string" || storedBoundary.source.trim() === "" || typeof storedBoundary.stream_id !== "string" || storedBoundary.stream_id.trim() === "" || storedBoundary.id !== void 0 && (typeof storedBoundary.id !== "string" || storedBoundary.id.trim() === ""))) throw new Error("invalid raw-event inbox boundary");
2759
+ return {
2760
+ entry: {
2761
+ request: parsed.request,
2762
+ flushBoundary: parsed.flush_boundary,
2763
+ ...storedBoundary ? { boundary: {
2764
+ source: storedBoundary.source,
2765
+ streamId: storedBoundary.stream_id,
2766
+ ...storedBoundary.id ? { id: storedBoundary.id } : {}
2767
+ } } : {}
2768
+ },
2769
+ enqueueOrder: BigInt(parsed.enqueue_order)
2770
+ };
2771
+ }
2772
+ var FileRawEventInbox = class {
2773
+ batchSize;
2774
+ backlogWarningAgeMs;
2775
+ backlogWarningEntries;
2776
+ directory;
2777
+ maxEntries;
2778
+ onDrainError;
2779
+ onDrainRecovered;
2780
+ onCorruptEntries;
2781
+ onBacklog;
2782
+ onBacklogRecovered;
2783
+ processEntry;
2784
+ retryDelayMs;
2785
+ drainPromise = null;
2786
+ degraded = false;
2787
+ backlogDegraded = false;
2788
+ enqueueChain = Promise.resolve();
2789
+ notedCorruptCount = 0;
2790
+ pendingDrainDelayMs = null;
2791
+ nextEnqueueOrder = null;
2792
+ retryCount = 0;
2793
+ timer = null;
2794
+ stopped = false;
2795
+ constructor(options) {
2796
+ this.batchSize = normalizePositiveInteger(options.batchSize, DEFAULT_BATCH_SIZE);
2797
+ this.backlogWarningAgeMs = normalizePositiveInteger(options.backlogWarningAgeMs, DEFAULT_BACKLOG_WARNING_AGE_MS);
2798
+ this.backlogWarningEntries = normalizePositiveInteger(options.backlogWarningEntries, DEFAULT_BACKLOG_WARNING_ENTRIES);
2799
+ this.directory = options.directory;
2800
+ this.maxEntries = normalizePositiveInteger(options.maxEntries, DEFAULT_MAX_ENTRIES);
2801
+ this.onDrainError = options.onDrainError;
2802
+ this.onDrainRecovered = options.onDrainRecovered;
2803
+ this.onCorruptEntries = options.onCorruptEntries;
2804
+ this.onBacklog = options.onBacklog;
2805
+ this.onBacklogRecovered = options.onBacklogRecovered;
2806
+ this.processEntry = options.processEntry;
2807
+ this.retryDelayMs = normalizePositiveInteger(options.retryDelayMs, DEFAULT_RETRY_DELAY_MS);
2808
+ }
2809
+ async enqueue(request, options = {}) {
2810
+ const operation = this.enqueueChain.then(() => this.persist(request, options));
2811
+ this.enqueueChain = operation.catch(() => {});
2812
+ return operation;
2813
+ }
2814
+ async persist(request, options) {
2815
+ const flushBoundary = options.flushBoundary === true;
2816
+ const id = entryContentId({
2817
+ request,
2818
+ flushBoundary,
2819
+ boundary: options.boundary
2820
+ });
2821
+ await ensurePrivateDirectory(this.directory);
2822
+ const entries = await readdir(this.directory, { withFileTypes: true });
2823
+ const existingEntry = entries.find((entry) => entry.isFile() && parseInboxFileName(entry.name)?.id === id);
2824
+ if (existingEntry) {
2825
+ const parsedExisting = parseStoredEntry(await readFile(join(this.directory, existingEntry.name), "utf8"));
2826
+ const existingName = parseInboxFileName(existingEntry.name);
2827
+ if (entryContentId(parsedExisting.entry) !== id || existingName?.enqueueOrder !== parsedExisting.enqueueOrder) throw new Error("raw-event inbox entry conflict");
2828
+ await chmod(join(this.directory, existingEntry.name), 384);
2829
+ await syncDirectory(this.directory);
2830
+ this.scheduleDrain();
2831
+ return;
2832
+ }
2833
+ if (entries.filter((entry) => entry.isFile() && entry.name.endsWith(".json")).length >= this.maxEntries) {
2834
+ const error = /* @__PURE__ */ new Error("raw-event inbox is full");
2835
+ error.code = RAW_EVENT_INBOX_FULL_CODE;
2836
+ throw error;
2837
+ }
2838
+ const enqueueOrder = await this.allocateEnqueueOrder(entries);
2839
+ const serialized = JSON.stringify(storedEntry(request, flushBoundary, options.boundary, enqueueOrder));
2840
+ const destination = join(this.directory, inboxFileName(enqueueOrder, id));
2841
+ const temporary = join(this.directory, `.${id}.${process.pid}.${randomUUID()}.tmp`);
2842
+ try {
2843
+ await writeDurableFile(temporary, serialized);
2844
+ try {
2845
+ await link(temporary, destination);
2846
+ } catch (error) {
2847
+ if (error.code !== "EEXIST") throw error;
2848
+ const parsedExisting = parseStoredEntry(await readFile(destination, "utf8"));
2849
+ if (entryContentId(parsedExisting.entry) !== id || parsedExisting.enqueueOrder !== enqueueOrder) throw new Error("raw-event inbox entry conflict");
2850
+ }
2851
+ await chmod(destination, 384);
2852
+ } finally {
2853
+ await rm(temporary, { force: true }).catch(() => {});
2854
+ }
2855
+ await syncDirectory(this.directory);
2856
+ this.scheduleDrain();
2857
+ }
2858
+ async allocateEnqueueOrder(entries) {
2859
+ if (this.nextEnqueueOrder != null) {
2860
+ const order = this.nextEnqueueOrder;
2861
+ this.nextEnqueueOrder += 1n;
2862
+ return order;
2863
+ }
2864
+ let highestOrder = 0n;
2865
+ for (const entry of entries) {
2866
+ if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
2867
+ const parsed = parseInboxFileName(entry.name);
2868
+ if (parsed && parsed.enqueueOrder > highestOrder) highestOrder = parsed.enqueueOrder;
2869
+ }
2870
+ const order = highestOrder + 1n;
2871
+ this.nextEnqueueOrder = order + 1n;
2872
+ return order;
2873
+ }
2874
+ start() {
2875
+ this.scheduleDrain();
2876
+ }
2877
+ async status() {
2878
+ const loaded = await this.loadEntries(Number.POSITIVE_INFINITY);
2879
+ return {
2880
+ pending: loaded.entries.length,
2881
+ corrupt: loaded.corrupt,
2882
+ draining: this.drainPromise != null
2883
+ };
2884
+ }
2885
+ async stop() {
2886
+ this.stopped = true;
2887
+ if (this.timer) clearTimeout(this.timer);
2888
+ this.timer = null;
2889
+ this.pendingDrainDelayMs = null;
2890
+ await this.enqueueChain;
2891
+ await this.drainPromise;
2892
+ }
2893
+ scheduleDrain(delayMs = 0) {
2894
+ if (this.stopped || this.timer) return;
2895
+ if (this.drainPromise) {
2896
+ this.pendingDrainDelayMs = delayMs;
2897
+ return;
2898
+ }
2899
+ this.timer = setTimeout(() => {
2900
+ this.timer = null;
2901
+ if (this.stopped || this.drainPromise) return;
2902
+ this.drainPromise = this.drain().catch((error) => this.handleDrainFailure(error)).finally(() => {
2903
+ this.drainPromise = null;
2904
+ if (this.pendingDrainDelayMs != null) {
2905
+ const pendingDelayMs = this.pendingDrainDelayMs;
2906
+ this.pendingDrainDelayMs = null;
2907
+ this.scheduleDrain(pendingDelayMs);
2908
+ }
2909
+ });
2910
+ }, delayMs);
2911
+ this.timer.unref?.();
2912
+ }
2913
+ async loadEntries(limit) {
2914
+ let directoryEntries;
2915
+ try {
2916
+ directoryEntries = await readdir(this.directory, { withFileTypes: true });
2917
+ } catch (error) {
2918
+ if (error.code === "ENOENT") return {
2919
+ entries: [],
2920
+ corrupt: 0,
2921
+ oldestAgeMs: 0,
2922
+ pending: 0
2923
+ };
2924
+ throw error;
2925
+ }
2926
+ const scan = await this.scanCandidates(directoryEntries);
2927
+ scan.candidates.sort((left, right) => {
2928
+ if (left.enqueueOrder < right.enqueueOrder) return -1;
2929
+ if (left.enqueueOrder > right.enqueueOrder) return 1;
2930
+ return left.id.localeCompare(right.id);
2931
+ });
2932
+ const entries = [];
2933
+ let corrupt = scan.corrupt;
2934
+ for (const candidate of scan.candidates) {
2935
+ if (entries.length >= limit) break;
2936
+ const loadedEntry = await this.loadEntry(candidate);
2937
+ if (!loadedEntry) {
2938
+ corrupt += 1;
2939
+ continue;
2940
+ }
2941
+ entries.push(loadedEntry);
2942
+ }
2943
+ return {
2944
+ entries,
2945
+ corrupt,
2946
+ oldestAgeMs: Number.isFinite(scan.oldestMtimeMs) ? Math.max(0, Date.now() - scan.oldestMtimeMs) : 0,
2947
+ pending: scan.pending
2948
+ };
2949
+ }
2950
+ async scanCandidates(directoryEntries) {
2951
+ const candidates = [];
2952
+ let corrupt = 0;
2953
+ let oldestMtimeMs = Number.POSITIVE_INFINITY;
2954
+ let pending = 0;
2955
+ for (const directoryEntry of directoryEntries) {
2956
+ if (!directoryEntry.isFile() || !directoryEntry.name.endsWith(".json")) continue;
2957
+ const path = join(this.directory, directoryEntry.name);
2958
+ try {
2959
+ const details = await stat(path);
2960
+ pending += 1;
2961
+ oldestMtimeMs = Math.min(oldestMtimeMs, details.mtimeMs);
2962
+ const parsed = parseInboxFileName(directoryEntry.name);
2963
+ if (!parsed) {
2964
+ corrupt += 1;
2965
+ continue;
2966
+ }
2967
+ candidates.push({
2968
+ path,
2969
+ ...parsed
2970
+ });
2971
+ } catch {}
2972
+ }
2973
+ return {
2974
+ candidates,
2975
+ corrupt,
2976
+ oldestMtimeMs,
2977
+ pending
2978
+ };
2979
+ }
2980
+ async loadEntry(candidate) {
2981
+ try {
2982
+ const { entry, enqueueOrder } = parseStoredEntry(await readFile(candidate.path, "utf8"));
2983
+ const id = entryContentId(entry);
2984
+ if (candidate.id !== id || candidate.enqueueOrder !== enqueueOrder) return null;
2985
+ return {
2986
+ id,
2987
+ path: candidate.path,
2988
+ entry,
2989
+ enqueueOrder
2990
+ };
2991
+ } catch {
2992
+ return null;
2993
+ }
2994
+ }
2995
+ async drain() {
2996
+ const loaded = await this.loadEntries(this.batchSize);
2997
+ const backlogExceeded = loaded.pending >= this.backlogWarningEntries && loaded.oldestAgeMs >= this.backlogWarningAgeMs;
2998
+ if (backlogExceeded && !this.backlogDegraded) {
2999
+ this.backlogDegraded = true;
3000
+ this.onBacklog?.(loaded.pending, loaded.oldestAgeMs);
3001
+ } else if (!backlogExceeded && this.backlogDegraded) {
3002
+ this.backlogDegraded = false;
3003
+ this.onBacklogRecovered?.();
3004
+ }
3005
+ if (loaded.corrupt !== this.notedCorruptCount) {
3006
+ this.notedCorruptCount = loaded.corrupt;
3007
+ if (loaded.corrupt > 0) this.onCorruptEntries?.(loaded.corrupt);
3008
+ }
3009
+ for (const loadedEntry of loaded.entries) {
3010
+ await this.processEntry(loadedEntry.entry);
3011
+ await rm(loadedEntry.path, { force: true });
3012
+ }
3013
+ this.retryCount = 0;
3014
+ if (this.degraded) {
3015
+ this.degraded = false;
3016
+ this.onDrainRecovered?.();
3017
+ }
3018
+ if (loaded.entries.length > 0) this.scheduleDrain();
3019
+ }
3020
+ handleDrainFailure(error) {
3021
+ if (!this.degraded) {
3022
+ this.degraded = true;
3023
+ this.onDrainError?.(error);
3024
+ }
3025
+ this.retryCount += 1;
3026
+ const delay = Math.min(this.retryDelayMs * 2 ** (this.retryCount - 1), MAX_RETRY_DELAY_MS);
3027
+ this.scheduleDrain(delay);
3028
+ }
3029
+ };
3030
+ function createViewerRawEventInbox(options) {
3031
+ const store = new MemoryStore(options.dbPath);
3032
+ store.db.pragma("busy_timeout = 25");
3033
+ const inbox = new FileRawEventInbox({
3034
+ directory: resolveRawEventInboxDirectory(options.dbPath, options.homeDir),
3035
+ onDrainError: options.onDrainError,
3036
+ onDrainRecovered: options.onDrainRecovered,
3037
+ onCorruptEntries: options.onCorruptEntries,
3038
+ onBacklog: options.onBacklog,
3039
+ onBacklogRecovered: options.onBacklogRecovered,
3040
+ processEntry: async ({ request, flushBoundary, boundary }) => {
3041
+ const result = ingestRawEvents(store, request);
3042
+ nudgeRawEventSessions(options.sweeper, result.sessions);
3043
+ if (boundary) await options.sweeper?.flushBoundary(boundary.streamId, boundary.source);
3044
+ else if (flushBoundary && isClaudeBoundaryEnvelope(request)) await flushRawEventBoundarySessions(options.sweeper, result.sessions);
3045
+ }
3046
+ });
3047
+ return {
3048
+ inbox,
3049
+ stop: async () => {
3050
+ await inbox.stop();
3051
+ store.close();
3052
+ }
3053
+ };
3054
+ }
3055
+ //#endregion
3056
+ //#region src/routes/raw-events.ts
3057
+ /**
3058
+ * Raw events routes — GET & POST /api/raw-events, GET /api/raw-events/status,
3059
+ * POST /api/claude-hooks, POST /api/codex-hooks, POST /api/pi-hooks.
3060
+ */
3061
+ var DEFAULT_MAX_RAW_EVENTS_BODY_BYTES = 1048576;
3062
+ function configuredMaxRawEventsBodyBytes() {
3063
+ const parsed = Number(process.env.CODEMEM_RAW_EVENTS_MAX_BODY_BYTES?.trim() ?? "");
3064
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : DEFAULT_MAX_RAW_EVENTS_BODY_BYTES;
3065
+ }
3066
+ var MAX_RAW_EVENTS_BODY_BYTES = configuredMaxRawEventsBodyBytes();
3067
+ function emptyTranscriptOutcomeCounts() {
3068
+ return {
3069
+ ok: 0,
3070
+ not_provided: 0,
3071
+ path_rejected: 0,
3072
+ unreadable: 0,
3073
+ no_complete_record: 0,
3074
+ no_assistant_record: 0
3075
+ };
3076
+ }
3077
+ function createTranscriptDiagnostics() {
3078
+ const counts = {
3079
+ claude: emptyTranscriptOutcomeCounts(),
3080
+ codex: emptyTranscriptOutcomeCounts()
3081
+ };
3082
+ return {
3083
+ record(source, outcome) {
3084
+ counts[source][outcome] += 1;
3085
+ },
3086
+ snapshot() {
3087
+ return {
3088
+ scope: "legacy_compatibility_routes",
3089
+ counts: {
3090
+ claude: { ...counts.claude },
3091
+ codex: { ...counts.codex }
3092
+ }
3093
+ };
3094
+ }
3095
+ };
3096
+ }
3097
+ function transcriptSkipResponse(outcome) {
3098
+ if (outcome !== null && outcome !== "ok") return {
3099
+ inserted: 0,
3100
+ skipped: 1,
3101
+ skip_reason: "transcript_unavailable",
3102
+ skip_detail: outcome
3103
+ };
3104
+ return {
3105
+ inserted: 0,
3106
+ skipped: 1,
3107
+ skip_reason: "unsupported_hook"
3108
+ };
3109
+ }
3110
+ function claudeTranscriptRoot() {
3111
+ return join(process.env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude"), "projects");
3112
+ }
3113
+ function codexTranscriptRoot() {
3114
+ return join(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "sessions");
3115
+ }
3116
+ var SAFE_INGEST_VALIDATION_ERRORS = /* @__PURE__ */ new Set([
3117
+ "source must be string",
3118
+ "source is required",
3119
+ "source must use 1-64 letters, digits, dots, underscores, or hyphens",
3120
+ "session_stream_id must be string",
3121
+ "session_id must be string",
3122
+ "stream_id must be string",
3123
+ "opencode_session_id must be string",
3124
+ "conflicting session id fields",
3125
+ "invalid session id",
3126
+ "event source conflicts with request source",
3127
+ "session id required",
3128
+ "event_type must be string",
3129
+ "event_type required",
3130
+ "event_type has invalid syntax",
3131
+ "event_id must be string",
3132
+ "event_id required",
3133
+ "event_id has invalid syntax",
3134
+ "event_seq must be int",
3135
+ "ts_wall_ms must be number",
3136
+ "ts_mono_ms must be number",
3137
+ "payload must be an object",
3138
+ "cwd must be string",
3139
+ "project must be string",
3140
+ "started_at must be string",
3141
+ "events must be a list",
3142
+ "event must be an object"
3143
+ ]);
3144
+ function boundedIngestValidationMessage(error) {
3145
+ return SAFE_INGEST_VALIDATION_ERRORS.has(error.message) ? error.message : "invalid raw event request";
2639
3146
  }
2640
3147
  function boundedIngestErrorResponse(c, error) {
2641
3148
  if (error instanceof RawEventIngestValidationError) return c.json({ error: boundedIngestValidationMessage(error) }, 400);
@@ -2643,17 +3150,222 @@ function boundedIngestErrorResponse(c, error) {
2643
3150
  if (process.env.CODEMEM_VIEWER_DEBUG === "1") response.detail = error instanceof Error ? error.message : String(error);
2644
3151
  return c.json(response, 500);
2645
3152
  }
3153
+ function boundedInboxErrorResponse(c, error) {
3154
+ if (error?.code === "raw_event_inbox_full") return c.json({ error: {
3155
+ code: "raw_event_queue_full",
3156
+ message: "raw-event queue is full"
3157
+ } }, 503);
3158
+ return c.json({ error: {
3159
+ code: "raw_event_queue_write_failed",
3160
+ message: "raw-event queue write failed"
3161
+ } }, 503);
3162
+ }
2646
3163
  function untargetedPayload(payload) {
2647
3164
  const { db_path: _dbPath, identity_target: _identityTarget, ...body } = payload;
2648
3165
  return body;
2649
3166
  }
3167
+ async function enqueueRawEventRequest(options) {
3168
+ const target = validateViewerTarget(options.inboxTarget ?? options.getStore(), options.payload, { requirePairedTargets: true });
3169
+ if (!target.ok) return options.c.json(target.body, target.status);
3170
+ const validation = validateRawEvents(options.request);
3171
+ try {
3172
+ await options.inbox.enqueue(validation.request, options.boundary ? {
3173
+ flushBoundary: options.flushBoundary,
3174
+ boundary: options.boundary
3175
+ } : { flushBoundary: options.flushBoundary });
3176
+ } catch (error) {
3177
+ return boundedInboxErrorResponse(options.c, error);
3178
+ }
3179
+ const accepted = options.acceptedCount ?? validation.received;
3180
+ return options.c.json({
3181
+ accepted,
3182
+ queued: accepted
3183
+ }, 202);
3184
+ }
3185
+ function piEventName(payload) {
3186
+ for (const key of [
3187
+ "piEvent",
3188
+ "pi_event",
3189
+ "event",
3190
+ "type"
3191
+ ]) {
3192
+ const value = payload[key];
3193
+ if (typeof value === "string" && value.trim()) return value.trim();
3194
+ }
3195
+ return "";
3196
+ }
3197
+ function piInboxBoundary(payload, envelope, signal) {
3198
+ if (!["session_before_compact", "session_shutdown"].includes(piEventName(payload))) return;
3199
+ const streamId = envelope?.session_stream_id ?? signal?.session_id;
3200
+ if (!streamId) return;
3201
+ const envelopeTimestamp = envelope?.payload.timestamp;
3202
+ const id = typeof envelopeTimestamp === "string" ? envelopeTimestamp : signal?.ts;
3203
+ return {
3204
+ source: "pi",
3205
+ streamId,
3206
+ ...id ? { id } : {}
3207
+ };
3208
+ }
3209
+ async function postPiHookRequest(c, getStore, sweeper, inbox, inboxTarget) {
3210
+ const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
3211
+ if (result instanceof Response) return result;
3212
+ const payload = result;
3213
+ try {
3214
+ const target = validateViewerTarget(inbox && inboxTarget ? inboxTarget : getStore(), payload, { requirePairedTargets: true });
3215
+ if (!target.ok) return c.json(target.body, target.status);
3216
+ const untargeted = untargetedPayload(payload);
3217
+ const envelope = buildRawEventEnvelopeFromPiEvent(untargeted);
3218
+ const signal = buildPiFlushSignalFromEvent(untargeted);
3219
+ const boundary = piInboxBoundary(untargeted, envelope, signal);
3220
+ if (envelope === null && signal === null) return c.json({
3221
+ inserted: 0,
3222
+ skipped: 1
3223
+ });
3224
+ if (inbox) return await enqueueRawEventRequest({
3225
+ c,
3226
+ getStore,
3227
+ inbox,
3228
+ inboxTarget,
3229
+ payload,
3230
+ request: envelope ? {
3231
+ ...envelope,
3232
+ source: "pi"
3233
+ } : {
3234
+ source: "pi",
3235
+ session_stream_id: signal?.session_id,
3236
+ events: []
3237
+ },
3238
+ flushBoundary: false,
3239
+ ...boundary ? {
3240
+ boundary,
3241
+ acceptedCount: 1
3242
+ } : {}
3243
+ });
3244
+ if (envelope === null) return c.json({
3245
+ inserted: 0,
3246
+ skipped: 1
3247
+ });
3248
+ const ingestResult = await ingestNormalizedEnvelope(getStore(), sweeper, {
3249
+ ...envelope,
3250
+ source: "pi"
3251
+ });
3252
+ return c.json({
3253
+ inserted: ingestResult.inserted,
3254
+ skipped: ingestResult.skipped
3255
+ });
3256
+ } catch (error) {
3257
+ return boundedIngestErrorResponse(c, error);
3258
+ }
3259
+ }
3260
+ async function postRawEventRequest(c, getStore, sweeper, inbox, inboxTarget) {
3261
+ const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
3262
+ if (result instanceof Response) return result;
3263
+ try {
3264
+ const request = untargetedPayload(result);
3265
+ if (inbox) return await enqueueRawEventRequest({
3266
+ c,
3267
+ getStore,
3268
+ inbox,
3269
+ inboxTarget,
3270
+ payload: result,
3271
+ request,
3272
+ flushBoundary: c.req.header("x-codemem-boundary-flush") === "1"
3273
+ });
3274
+ const store = getStore();
3275
+ const target = validateViewerTarget(store, result, { requirePairedTargets: true });
3276
+ if (!target.ok) return c.json(target.body, target.status);
3277
+ const ingestResult = await ingestNormalizedEnvelope(store, sweeper, request, c.req.header("x-codemem-boundary-flush") === "1");
3278
+ return c.json({
3279
+ inserted: ingestResult.inserted,
3280
+ skipped: ingestResult.skipped,
3281
+ received: ingestResult.received
3282
+ });
3283
+ } catch (error) {
3284
+ return boundedIngestErrorResponse(c, error);
3285
+ }
3286
+ }
2650
3287
  async function ingestNormalizedEnvelope(store, sweeper, envelope, flushBoundary = false) {
2651
3288
  const result = ingestRawEvents(store, envelope);
2652
- nudgeSweeper(sweeper, result.sessions);
2653
- if (flushBoundary && isClaudeBoundaryEnvelope(envelope)) await flushBoundarySessions(sweeper, result.sessions);
3289
+ nudgeRawEventSessions(sweeper, result.sessions);
3290
+ if (flushBoundary && isClaudeBoundaryEnvelope(envelope)) await flushRawEventBoundarySessions(sweeper, result.sessions);
2654
3291
  return result;
2655
3292
  }
2656
- function rawEventsRoutes(getStore, sweeper) {
3293
+ async function postClaudeHookRequest(c, getStore, sweeper, inbox, inboxTarget, transcriptDiagnostics) {
3294
+ const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
3295
+ if (result instanceof Response) return result;
3296
+ const payload = result;
3297
+ try {
3298
+ const target = validateViewerTarget(inbox && inboxTarget ? inboxTarget : getStore(), payload, { requirePairedTargets: true });
3299
+ if (!target.ok) return c.json(target.body, target.status);
3300
+ let transcriptOutcome = null;
3301
+ const envelope = buildRawEventEnvelopeFromHook(untargetedPayload(payload), {
3302
+ transcriptPolicy: {
3303
+ trust: "restricted",
3304
+ approvedRoots: [claudeTranscriptRoot()]
3305
+ },
3306
+ onTranscriptOutcome: (outcome) => {
3307
+ transcriptOutcome = outcome;
3308
+ transcriptDiagnostics.record("claude", outcome);
3309
+ }
3310
+ });
3311
+ if (envelope === null) return c.json(transcriptSkipResponse(transcriptOutcome));
3312
+ const flushBoundary = c.req.header("x-codemem-boundary-flush") === "1";
3313
+ if (inbox) return await enqueueRawEventRequest({
3314
+ c,
3315
+ getStore,
3316
+ inbox,
3317
+ inboxTarget,
3318
+ payload,
3319
+ request: { ...envelope },
3320
+ flushBoundary
3321
+ });
3322
+ const ingestResult = await ingestNormalizedEnvelope(getStore(), sweeper, envelope, flushBoundary);
3323
+ return c.json({
3324
+ inserted: ingestResult.inserted,
3325
+ skipped: ingestResult.skipped
3326
+ });
3327
+ } catch (error) {
3328
+ return boundedIngestErrorResponse(c, error);
3329
+ }
3330
+ }
3331
+ async function postCodexHookRequest(c, getStore, sweeper, inbox, inboxTarget, transcriptDiagnostics) {
3332
+ const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
3333
+ if (result instanceof Response) return result;
3334
+ const payload = result;
3335
+ try {
3336
+ const target = validateViewerTarget(inbox && inboxTarget ? inboxTarget : getStore(), payload, { requirePairedTargets: true });
3337
+ if (!target.ok) return c.json(target.body, target.status);
3338
+ let transcriptOutcome = null;
3339
+ const envelope = buildRawEventEnvelopeFromCodexHook(untargetedPayload(payload), {
3340
+ transcriptPolicy: {
3341
+ trust: "restricted",
3342
+ approvedRoots: [codexTranscriptRoot()]
3343
+ },
3344
+ onTranscriptOutcome: (outcome) => {
3345
+ transcriptOutcome = outcome;
3346
+ transcriptDiagnostics.record("codex", outcome);
3347
+ }
3348
+ });
3349
+ if (envelope === null) return c.json(transcriptSkipResponse(transcriptOutcome));
3350
+ if (inbox) return await enqueueRawEventRequest({
3351
+ c,
3352
+ getStore,
3353
+ inbox,
3354
+ inboxTarget,
3355
+ payload,
3356
+ request: { ...envelope },
3357
+ flushBoundary: false
3358
+ });
3359
+ const ingestResult = await ingestNormalizedEnvelope(getStore(), sweeper, envelope);
3360
+ return c.json({
3361
+ inserted: ingestResult.inserted,
3362
+ skipped: ingestResult.skipped
3363
+ });
3364
+ } catch (error) {
3365
+ return boundedIngestErrorResponse(c, error);
3366
+ }
3367
+ }
3368
+ function rawEventsRoutes(getStore, sweeper, inbox, inboxTarget) {
2657
3369
  const app = new Hono();
2658
3370
  const transcriptDiagnostics = createTranscriptDiagnostics();
2659
3371
  app.get("/api/raw-events", (c) => {
@@ -2661,8 +3373,17 @@ function rawEventsRoutes(getStore, sweeper) {
2661
3373
  return c.json(totals);
2662
3374
  });
2663
3375
  app.get("/api/raw-events/status", (c) => {
2664
- const store = getStore();
2665
3376
  const limit = queryInt(c.req.query("limit"), 25);
3377
+ if (inbox && limit === 0) return c.json({
3378
+ items: [],
3379
+ ingest: {
3380
+ available: true,
3381
+ mode: "durable_queue",
3382
+ max_body_bytes: MAX_RAW_EVENTS_BODY_BYTES
3383
+ },
3384
+ transcript_diagnostics: transcriptDiagnostics.snapshot()
3385
+ });
3386
+ const store = getStore();
2666
3387
  const items = drizzle(store.db, { schema }).select({
2667
3388
  source: schema.rawEventSessions.source,
2668
3389
  stream_id: schema.rawEventSessions.stream_id,
@@ -2673,127 +3394,31 @@ function rawEventsRoutes(getStore, sweeper) {
2673
3394
  last_seen_ts_wall_ms: schema.rawEventSessions.last_seen_ts_wall_ms,
2674
3395
  last_received_event_seq: schema.rawEventSessions.last_received_event_seq,
2675
3396
  last_flushed_event_seq: schema.rawEventSessions.last_flushed_event_seq,
2676
- updated_at: schema.rawEventSessions.updated_at
2677
- }).from(schema.rawEventSessions).orderBy(desc(schema.rawEventSessions.updated_at)).limit(limit).all().map((row) => {
2678
- const streamId = String(row.stream_id ?? row.opencode_session_id ?? "");
2679
- return {
2680
- ...row,
2681
- session_stream_id: streamId,
2682
- session_id: streamId
2683
- };
2684
- });
2685
- const totals = store.rawEventBacklogTotals();
2686
- return c.json({
2687
- items,
2688
- totals,
2689
- ingest: {
2690
- available: true,
2691
- mode: "stream_queue",
2692
- max_body_bytes: MAX_RAW_EVENTS_BODY_BYTES
2693
- },
2694
- transcript_diagnostics: transcriptDiagnostics.snapshot()
2695
- });
2696
- });
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
- });
2714
- app.post("/api/claude-hooks", async (c) => {
2715
- const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
2716
- if (result instanceof Response) return result;
2717
- const payload = result;
2718
- try {
2719
- const store = getStore();
2720
- const target = validateViewerTarget(store, payload, { requirePairedTargets: true });
2721
- if (!target.ok) return c.json(target.body, target.status);
2722
- let transcriptOutcome = null;
2723
- const envelope = buildRawEventEnvelopeFromHook(untargetedPayload(payload), {
2724
- transcriptPolicy: {
2725
- trust: "restricted",
2726
- approvedRoots: [claudeTranscriptRoot()]
2727
- },
2728
- onTranscriptOutcome: (outcome) => {
2729
- transcriptOutcome = outcome;
2730
- transcriptDiagnostics.record("claude", outcome);
2731
- }
2732
- });
2733
- if (envelope === null) return c.json(transcriptSkipResponse(transcriptOutcome));
2734
- const ingestResult = await ingestNormalizedEnvelope(store, sweeper, envelope, c.req.header("x-codemem-boundary-flush") === "1");
2735
- return c.json({
2736
- inserted: ingestResult.inserted,
2737
- skipped: ingestResult.skipped
2738
- });
2739
- } catch (err) {
2740
- return boundedIngestErrorResponse(c, err);
2741
- }
2742
- });
2743
- app.post("/api/codex-hooks", async (c) => {
2744
- const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
2745
- if (result instanceof Response) return result;
2746
- const payload = result;
2747
- try {
2748
- const store = getStore();
2749
- const target = validateViewerTarget(store, payload, { requirePairedTargets: true });
2750
- if (!target.ok) return c.json(target.body, target.status);
2751
- let transcriptOutcome = null;
2752
- const envelope = buildRawEventEnvelopeFromCodexHook(untargetedPayload(payload), {
2753
- transcriptPolicy: {
2754
- trust: "restricted",
2755
- approvedRoots: [codexTranscriptRoot()]
2756
- },
2757
- onTranscriptOutcome: (outcome) => {
2758
- transcriptOutcome = outcome;
2759
- transcriptDiagnostics.record("codex", outcome);
2760
- }
2761
- });
2762
- if (envelope === null) return c.json(transcriptSkipResponse(transcriptOutcome));
2763
- const ingestResult = await ingestNormalizedEnvelope(store, sweeper, envelope);
2764
- return c.json({
2765
- inserted: ingestResult.inserted,
2766
- skipped: ingestResult.skipped
2767
- });
2768
- } catch (err) {
2769
- return boundedIngestErrorResponse(c, err);
2770
- }
2771
- });
2772
- app.post("/api/pi-hooks", async (c) => {
2773
- const result = await parseJsonObjectBody(c, MAX_RAW_EVENTS_BODY_BYTES);
2774
- if (result instanceof Response) return result;
2775
- const payload = result;
2776
- try {
2777
- const store = getStore();
2778
- const target = validateViewerTarget(store, payload, { requirePairedTargets: true });
2779
- if (!target.ok) return c.json(target.body, target.status);
2780
- const envelope = buildRawEventEnvelopeFromPiEvent(untargetedPayload(payload));
2781
- if (envelope === null) return c.json({
2782
- inserted: 0,
2783
- skipped: 1
2784
- });
2785
- const ingestResult = await ingestNormalizedEnvelope(store, sweeper, {
2786
- ...envelope,
2787
- source: "pi"
2788
- });
2789
- return c.json({
2790
- inserted: ingestResult.inserted,
2791
- skipped: ingestResult.skipped
2792
- });
2793
- } catch (err) {
2794
- return boundedIngestErrorResponse(c, err);
2795
- }
3397
+ updated_at: schema.rawEventSessions.updated_at
3398
+ }).from(schema.rawEventSessions).orderBy(desc(schema.rawEventSessions.updated_at)).limit(limit).all().map((row) => {
3399
+ const streamId = String(row.stream_id ?? row.opencode_session_id ?? "");
3400
+ return {
3401
+ ...row,
3402
+ session_stream_id: streamId,
3403
+ session_id: streamId
3404
+ };
3405
+ });
3406
+ const totals = store.rawEventBacklogTotals();
3407
+ return c.json({
3408
+ items,
3409
+ totals,
3410
+ ingest: {
3411
+ available: true,
3412
+ mode: inbox ? "durable_queue" : "stream_queue",
3413
+ max_body_bytes: MAX_RAW_EVENTS_BODY_BYTES
3414
+ },
3415
+ transcript_diagnostics: transcriptDiagnostics.snapshot()
3416
+ });
2796
3417
  });
3418
+ app.post("/api/raw-events", (c) => postRawEventRequest(c, getStore, sweeper, inbox, inboxTarget));
3419
+ app.post("/api/claude-hooks", (c) => postClaudeHookRequest(c, getStore, sweeper, inbox, inboxTarget, transcriptDiagnostics));
3420
+ app.post("/api/codex-hooks", (c) => postCodexHookRequest(c, getStore, sweeper, inbox, inboxTarget, transcriptDiagnostics));
3421
+ app.post("/api/pi-hooks", (c) => postPiHookRequest(c, getStore, sweeper, inbox, inboxTarget));
2797
3422
  return app;
2798
3423
  }
2799
3424
  //#endregion
@@ -2919,24 +3544,49 @@ function mapAggregateEvents(rows) {
2919
3544
  total_tokens_read: row.tokens_read,
2920
3545
  total_tokens_written: row.tokens_written,
2921
3546
  total_tokens_saved: row.tokens_saved,
2922
- count: row.count
3547
+ count: row.count,
3548
+ token_unit: row.token_unit,
3549
+ measured_count: row.measured_count,
3550
+ estimated_count: row.estimated_count,
3551
+ unavailable_count: row.unavailable_count,
3552
+ legacy_text_length_count: row.legacy_text_length_count,
3553
+ legacy_unclassified_count: row.legacy_unclassified_count
2923
3554
  })).sort((a, b) => a.event.localeCompare(b.event));
2924
3555
  }
2925
- /**
2926
- * Sum the neutral core aggregate rows into the ApiUsageTotals wire shape.
2927
- */
2928
3556
  function totalsFromAggregate(rows) {
2929
3557
  return rows.reduce((acc, row) => ({
2930
3558
  tokens_read: acc.tokens_read + row.tokens_read,
2931
3559
  tokens_written: acc.tokens_written + row.tokens_written,
2932
3560
  tokens_saved: acc.tokens_saved + row.tokens_saved,
2933
- count: acc.count + row.count
3561
+ count: acc.count + row.count,
3562
+ token_unit: "tokens",
3563
+ measured_count: acc.measured_count + row.measured_count,
3564
+ estimated_count: acc.estimated_count + row.estimated_count,
3565
+ unavailable_count: acc.unavailable_count + row.unavailable_count,
3566
+ legacy_text_length_count: acc.legacy_text_length_count + row.legacy_text_length_count,
3567
+ legacy_unclassified_count: acc.legacy_unclassified_count + row.legacy_unclassified_count
2934
3568
  }), {
2935
3569
  tokens_read: 0,
2936
3570
  tokens_written: 0,
2937
3571
  tokens_saved: 0,
2938
- count: 0
2939
- });
3572
+ count: 0,
3573
+ token_unit: "tokens",
3574
+ measured_count: 0,
3575
+ estimated_count: 0,
3576
+ unavailable_count: 0,
3577
+ legacy_text_length_count: 0,
3578
+ legacy_unclassified_count: 0
3579
+ });
3580
+ }
3581
+ function usageAggregatePayload(store, project) {
3582
+ const globalRows = store.classifiedUsageAggregate();
3583
+ const filteredRows = project ? store.classifiedUsageAggregate(project) : null;
3584
+ return {
3585
+ eventsGlobal: mapAggregateEvents(globalRows),
3586
+ totalsGlobal: totalsFromAggregate(globalRows),
3587
+ eventsFiltered: filteredRows ? mapAggregateEvents(filteredRows) : null,
3588
+ totalsFiltered: filteredRows ? totalsFromAggregate(filteredRows) : null
3589
+ };
2940
3590
  }
2941
3591
  /**
2942
3592
  * Short-lived cache for the computed /api/usage payload.
@@ -3017,120 +3667,657 @@ function scopeVisibilityGeneration(store) {
3017
3667
  return `bypass-${Date.now()}-${Math.random()}`;
3018
3668
  }
3019
3669
  }
3020
- function usageCacheKey(store, projectFilter) {
3021
- const visibilityGeneration = scopeVisibilityGeneration(store);
3022
- return `${store.dbPath}|${store.actorId}|${store.deviceId}|${projectFilter ?? ""}|${visibilityGeneration}`;
3670
+ function usageCacheKey(store, projectFilter) {
3671
+ const visibilityGeneration = scopeVisibilityGeneration(store);
3672
+ return `${store.dbPath}|${store.actorId}|${store.deviceId}|${projectFilter ?? ""}|${visibilityGeneration}`;
3673
+ }
3674
+ /**
3675
+ * Create stats routes. The store factory is called per-request to get a
3676
+ * fresh connection (matching the Python viewer pattern).
3677
+ */
3678
+ function statsRoutes(getStore) {
3679
+ const app = new Hono();
3680
+ const parseMetadataJson = (value) => {
3681
+ if (typeof value !== "string" || !value.trim()) return null;
3682
+ try {
3683
+ return JSON.parse(value);
3684
+ } catch {
3685
+ return null;
3686
+ }
3687
+ };
3688
+ const RECENTLY_COMPLETED_WINDOW_MS = 12e4;
3689
+ app.get("/api/stats", (c) => {
3690
+ const store = getStore();
3691
+ const jobs = listMaintenanceJobs(store.db);
3692
+ const now = Date.now();
3693
+ const surfacedJobs = sortActiveMaintenanceJobs$1(jobs.filter((job) => {
3694
+ if (job.status === "pending" || job.status === "running" || job.status === "failed") return true;
3695
+ if (job.status === "completed" && job.finished_at) {
3696
+ const finished = Date.parse(job.finished_at);
3697
+ if (Number.isFinite(finished) && now - finished <= RECENTLY_COMPLETED_WINDOW_MS) return true;
3698
+ }
3699
+ return false;
3700
+ })).map((job) => ({
3701
+ kind: job.kind,
3702
+ title: job.title,
3703
+ status: job.status,
3704
+ message: job.message,
3705
+ progress: job.progress,
3706
+ finished_at: job.finished_at,
3707
+ error: job.error
3708
+ }));
3709
+ return c.json({
3710
+ ...healthStats(store),
3711
+ viewer_pid: process.pid,
3712
+ maintenance_jobs: surfacedJobs
3713
+ });
3714
+ });
3715
+ app.get("/api/runtime", (c) => {
3716
+ return c.json({ version: VERSION$1 });
3717
+ });
3718
+ app.get("/api/usage", (c) => {
3719
+ const store = getStore();
3720
+ {
3721
+ const projectFilter = c.req.query("project") || null;
3722
+ const cacheKey = usageCacheKey(store, projectFilter);
3723
+ const nowMs = Date.now();
3724
+ const cached = usagePayloadCache.get(cacheKey);
3725
+ if (cached && nowMs < cached.expiresAtMs) return c.json(cached.payload);
3726
+ const aggregates = usageAggregatePayload(store, projectFilter);
3727
+ const RECENT_PACK_SCAN_LIMIT = 200;
3728
+ const loadRecentPackWindow = (project) => {
3729
+ return (project ? store.db.prepare(`SELECT usage_events.id, usage_events.session_id, usage_events.event,
3730
+ usage_events.tokens_read, usage_events.tokens_written, usage_events.tokens_saved,
3731
+ usage_events.created_at, usage_events.metadata_json
3732
+ FROM usage_events
3733
+ JOIN sessions ON sessions.id = usage_events.session_id
3734
+ WHERE sessions.project = ? AND usage_events.event = 'pack'
3735
+ ORDER BY usage_events.created_at DESC
3736
+ LIMIT ?`).all(project, RECENT_PACK_SCAN_LIMIT) : store.db.prepare(`SELECT id, session_id, event, tokens_read, tokens_written, tokens_saved,
3737
+ created_at, metadata_json
3738
+ FROM usage_events
3739
+ WHERE event = 'pack'
3740
+ ORDER BY created_at DESC
3741
+ LIMIT ?`).all(RECENT_PACK_SCAN_LIMIT)).map((row) => toUsageRow(row, parseMetadataJson(row.metadata_json)));
3742
+ };
3743
+ const recentWindow = loadRecentPackWindow(projectFilter);
3744
+ const recentVisibility = buildUsageVisibility(store, recentWindow);
3745
+ const recentPacks = recentWindow.filter((row) => usageRowVisible(row, recentVisibility)).slice(0, 10).map((row) => ({
3746
+ ...row,
3747
+ metadata_json: sanitizePackUsageMetadata(row.metadata_json, recentVisibility)
3748
+ }));
3749
+ const payload = {
3750
+ project: projectFilter,
3751
+ events: projectFilter ? aggregates.eventsFiltered : aggregates.eventsGlobal,
3752
+ totals: projectFilter ? aggregates.totalsFiltered : aggregates.totalsGlobal,
3753
+ events_global: aggregates.eventsGlobal,
3754
+ totals_global: aggregates.totalsGlobal,
3755
+ events_filtered: aggregates.eventsFiltered,
3756
+ totals_filtered: aggregates.totalsFiltered,
3757
+ recent_packs: recentPacks
3758
+ };
3759
+ const setAtMs = Date.now();
3760
+ sweepUsagePayloadCache(usagePayloadCache, setAtMs);
3761
+ usagePayloadCache.set(cacheKey, {
3762
+ payload,
3763
+ expiresAtMs: setAtMs + USAGE_PAYLOAD_CACHE_MS
3764
+ });
3765
+ return c.json(payload);
3766
+ }
3767
+ });
3768
+ return app;
3769
+ }
3770
+ function healthStats(store) {
3771
+ return {
3772
+ ...store.stats(),
3773
+ automatic_recall: automaticRecallHealth(store.db, {
3774
+ actorId: store.actorId,
3775
+ deviceId: store.deviceId
3776
+ })
3777
+ };
3778
+ }
3779
+ //#endregion
3780
+ //#region src/application/coordinator-maintenance.ts
3781
+ var AUTOMATIC_SHARE_OPERATION_STATES = [
3782
+ "waiting_for_acceptance",
3783
+ "accepted",
3784
+ "provisioning",
3785
+ "initial_sync",
3786
+ "waiting_for_device"
3787
+ ];
3788
+ var AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS = 3e4;
3789
+ var AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS = 3e5;
3790
+ var TERMINAL_SHARE_MAINTENANCE_ERRORS = /* @__PURE__ */ new Set([
3791
+ "coordinator_not_configured",
3792
+ "team_sharing_not_configured",
3793
+ "team_selection_ambiguous",
3794
+ "initiating_device_not_reviewed",
3795
+ "inviter_project_access_ambiguous",
3796
+ "managed_boundary_plan_missing",
3797
+ "operation_device_binding_missing",
3798
+ "operation_intent_invalid",
3799
+ "provisioning_membership_plan_invalid",
3800
+ "device_binding_conflict",
3801
+ "intent_conflict",
3802
+ "inviter_identity_conflict"
3803
+ ]);
3804
+ var PROJECT_INVITE_OWNER_CONFLICT_ERRORS = /* @__PURE__ */ new Set([
3805
+ "operation_scope_mismatch",
3806
+ "operation_state_invalid",
3807
+ "operation_acceptance_invalid",
3808
+ "operation_trust_state_invalid",
3809
+ "operation_intent_invalid",
3810
+ "operation_intent_mismatch",
3811
+ "recipient_fingerprint_mismatch",
3812
+ "recipient_device_identity_conflict",
3813
+ "recipient_actor_conflict",
3814
+ "pending_person_identity_conflict",
3815
+ "device_binding_conflict",
3816
+ "intent_conflict",
3817
+ "inviter_identity_conflict"
3818
+ ]);
3819
+ var TERMINAL_RECONCILIATION_ERRORS = /* @__PURE__ */ new Set([
3820
+ "coordinator_not_configured",
3821
+ "team_sharing_not_configured",
3822
+ "team_selection_ambiguous",
3823
+ "operation_not_found",
3824
+ ...PROJECT_INVITE_OWNER_CONFLICT_ERRORS
3825
+ ]);
3826
+ function errorStatus$2(error) {
3827
+ const value = error && typeof error === "object" ? error.status : null;
3828
+ return typeof value === "number" && Number.isInteger(value) && value >= 400 && value <= 599 ? value : null;
3829
+ }
3830
+ function recordInviteReconciliationFailure(store, operationId, error, now) {
3831
+ const message = error instanceof Error ? error.message : String(error);
3832
+ const safeErrorCode = TERMINAL_RECONCILIATION_ERRORS.has(message) ? message : "operation_read_failed";
3833
+ const status = errorStatus$2(error);
3834
+ const retryable = !TERMINAL_RECONCILIATION_ERRORS.has(message) && (status == null || status === 408 || status === 429 || status >= 500);
3835
+ store.db.prepare(`UPDATE share_operation_steps SET
3836
+ status = CASE WHEN ? = 1 THEN 'pending' ELSE 'failed' END,
3837
+ attempt_count = attempt_count + 1, last_attempt_at = ?, safe_error_code = ?, updated_at = ?
3838
+ WHERE operation_id = ? AND step_key = 'invite_consumption'`).run(retryable ? 1 : 0, now, safeErrorCode, now, operationId);
3839
+ const outcome = retryable ? "retry_scheduled" : "needs_attention";
3840
+ store.db.prepare("UPDATE share_operations SET state = ?, updated_at = ? WHERE operation_id = ?").run(outcome === "needs_attention" ? "needs_attention" : "waiting_for_acceptance", now, operationId);
3841
+ return outcome;
3842
+ }
3843
+ function pendingShareOperationRows(store, input) {
3844
+ const placeholders = AUTOMATIC_SHARE_OPERATION_STATES.map(() => "?").join(", ");
3845
+ const waitingAcceptanceRetryBefore = (/* @__PURE__ */ new Date(input.maintenanceNow.getTime() - AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS)).toISOString();
3846
+ const waitingRetryBefore = (/* @__PURE__ */ new Date(input.maintenanceNow.getTime() - AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS)).toISOString();
3847
+ return store.db.prepare(`SELECT operation.operation_id FROM share_operations AS operation
3848
+ WHERE operation.inviter_actor_id = ?
3849
+ AND operation.state IN (${placeholders})
3850
+ AND (operation.state <> 'waiting_for_acceptance' OR operation.updated_at <= ?)
3851
+ AND (
3852
+ operation.state <> 'waiting_for_device'
3853
+ OR operation.updated_at <= ?
3854
+ OR (
3855
+ NOT EXISTS (
3856
+ SELECT 1 FROM share_operation_steps AS step
3857
+ WHERE step.operation_id = operation.operation_id
3858
+ AND step.step_key = 'capability_preflight'
3859
+ AND step.status <> 'completed'
3860
+ AND (
3861
+ step.status = 'running'
3862
+ OR step.safe_error_code IN (
3863
+ 'waiting_for_device', 'device_offline', 'recipient_offline'
3864
+ )
3865
+ )
3866
+ )
3867
+ AND EXISTS (
3868
+ SELECT 1 FROM sync_attempts AS attempt
3869
+ WHERE attempt.id = (
3870
+ SELECT latest.id FROM sync_attempts AS latest
3871
+ WHERE latest.peer_device_id = operation.recipient_device_id
3872
+ ORDER BY latest.started_at DESC, latest.id DESC
3873
+ LIMIT 1
3874
+ )
3875
+ AND attempt.ok = 1
3876
+ AND julianday(COALESCE(attempt.finished_at, attempt.started_at)) >
3877
+ julianday(operation.updated_at)
3878
+ )
3879
+ )
3880
+ )
3881
+ ORDER BY CASE WHEN operation.state IN ('accepted', 'provisioning', 'initial_sync') THEN 0 ELSE 1 END,
3882
+ CASE WHEN operation.state IN ('waiting_for_acceptance', 'waiting_for_device')
3883
+ THEN operation.updated_at ELSE operation.created_at END ASC,
3884
+ operation.created_at ASC, operation.operation_id ASC
3885
+ LIMIT ?`).all(store.actorId, ...AUTOMATIC_SHARE_OPERATION_STATES, waitingAcceptanceRetryBefore, waitingRetryBefore, input.limit);
3886
+ }
3887
+ function recordPendingShareFailure(store, result, operationId, error, now) {
3888
+ const message = error instanceof Error ? error.message : String(error);
3889
+ const state = store.db.prepare("SELECT state FROM share_operations WHERE operation_id = ?").pluck().get(operationId);
3890
+ if (state === "cancelled") {
3891
+ result.items.push({
3892
+ operationId,
3893
+ outcome: "superseded"
3894
+ });
3895
+ return;
3896
+ }
3897
+ if (message === "waiting_for_device" || state === "waiting_for_device") {
3898
+ result.waiting += 1;
3899
+ result.items.push({
3900
+ operationId,
3901
+ outcome: "waiting_for_device"
3902
+ });
3903
+ return;
3904
+ }
3905
+ if (state === "needs_attention") {
3906
+ result.attention += 1;
3907
+ result.items.push({
3908
+ operationId,
3909
+ outcome: "needs_attention",
3910
+ error: message
3911
+ });
3912
+ return;
3913
+ }
3914
+ if (state === "waiting_for_acceptance") {
3915
+ const outcome = recordInviteReconciliationFailure(store, operationId, error, now);
3916
+ if (outcome === "needs_attention") result.attention += 1;
3917
+ else result.waiting += 1;
3918
+ result.items.push({
3919
+ operationId,
3920
+ outcome,
3921
+ error: message
3922
+ });
3923
+ return;
3924
+ }
3925
+ if (TERMINAL_SHARE_MAINTENANCE_ERRORS.has(message)) {
3926
+ store.db.prepare("UPDATE share_operations SET state = 'needs_attention', updated_at = ? WHERE operation_id = ?").run(now, operationId);
3927
+ result.attention += 1;
3928
+ result.items.push({
3929
+ operationId,
3930
+ outcome: "needs_attention",
3931
+ error: message
3932
+ });
3933
+ return;
3934
+ }
3935
+ result.failed += 1;
3936
+ result.items.push({
3937
+ operationId,
3938
+ outcome: "failed",
3939
+ error: message
3940
+ });
3941
+ }
3942
+ async function processPendingShareOperation(store, result, operationId, advanceOperation, now) {
3943
+ result.processed += 1;
3944
+ try {
3945
+ const advanced = await advanceOperation(store, operationId);
3946
+ if (advanced.advanced) {
3947
+ result.advanced += 1;
3948
+ result.items.push({
3949
+ operationId,
3950
+ outcome: "advanced"
3951
+ });
3952
+ return;
3953
+ }
3954
+ if (advanced.state === "cancelled") {
3955
+ result.items.push({
3956
+ operationId,
3957
+ outcome: "superseded"
3958
+ });
3959
+ return;
3960
+ }
3961
+ store.db.prepare("UPDATE share_operations SET updated_at = ? WHERE operation_id = ?").run(now, operationId);
3962
+ result.waiting += 1;
3963
+ result.items.push({
3964
+ operationId,
3965
+ outcome: "waiting_for_acceptance"
3966
+ });
3967
+ } catch (error) {
3968
+ recordPendingShareFailure(store, result, operationId, error, now);
3969
+ }
3970
+ }
3971
+ async function advancePendingProjectSharesOperation(store, options) {
3972
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? 3), 10));
3973
+ const maintenanceNow = options.now ?? /* @__PURE__ */ new Date();
3974
+ const rows = pendingShareOperationRows(store, {
3975
+ limit,
3976
+ maintenanceNow
3977
+ });
3978
+ const result = {
3979
+ processed: 0,
3980
+ advanced: 0,
3981
+ waiting: 0,
3982
+ attention: 0,
3983
+ failed: 0,
3984
+ items: []
3985
+ };
3986
+ const now = maintenanceNow.toISOString();
3987
+ for (const row of rows) await processPendingShareOperation(store, result, row.operation_id, options.advanceOperation, now);
3988
+ return result;
3023
3989
  }
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;
3990
+ var RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT = 10;
3991
+ var RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT = 3;
3992
+ var RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS = 6e4;
3993
+ async function reconcileRecipientPolicyProjectsOperation(store, options) {
3994
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT), RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT));
3995
+ const maintenanceNow = options.now ?? /* @__PURE__ */ new Date();
3996
+ const backoffMs = Math.max(0, Math.trunc(options.backoffMs ?? RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS));
3997
+ const retryBefore = new Date(maintenanceNow.getTime() - backoffMs).toISOString();
3998
+ const rows = store.db.prepare(`WITH projects AS (
3999
+ SELECT DISTINCT canonical_project_identity FROM project_recipients
4000
+ UNION
4001
+ SELECT canonical_project_identity FROM recipient_policy_authority_states
4002
+ )
4003
+ SELECT projects.canonical_project_identity
4004
+ FROM projects
4005
+ LEFT JOIN recipient_policy_authority_states authority
4006
+ ON authority.canonical_project_identity = projects.canonical_project_identity
4007
+ WHERE authority.safe_error_code IS NULL OR authority.last_attempt_at IS NULL
4008
+ OR authority.last_attempt_at <= ?
4009
+ ORDER BY CASE WHEN authority.last_attempt_at IS NULL THEN 0 ELSE 1 END,
4010
+ authority.last_attempt_at, projects.canonical_project_identity
4011
+ LIMIT ?`).all(retryBefore, limit);
4012
+ const reconcileProject = options.reconcileProject ?? reconcileRecipientPolicyProject;
4013
+ const result = {
4014
+ processed: 0,
4015
+ active: 0,
4016
+ waiting: 0,
4017
+ attention: 0,
4018
+ failed: 0,
4019
+ items: []
4020
+ };
4021
+ for (const row of rows) {
4022
+ result.processed += 1;
3032
4023
  try {
3033
- return JSON.parse(value);
4024
+ const outcome = await reconcileProject(store.db, {
4025
+ canonicalProjectIdentity: row.canonical_project_identity,
4026
+ leaseOwner: options.leaseOwner ?? `recipient-policy-maintenance:${store.deviceId || process.pid}`
4027
+ }, options.effects);
4028
+ if (outcome.status === "active") result.active += 1;
4029
+ else if (outcome.status === "needs_attention") result.attention += 1;
4030
+ else result.waiting += 1;
4031
+ result.items.push({
4032
+ canonicalProjectIdentity: row.canonical_project_identity,
4033
+ status: outcome.status,
4034
+ safeErrorCode: outcome.safeErrorCode
4035
+ });
3034
4036
  } 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
4037
+ result.failed += 1;
4038
+ result.items.push({
4039
+ canonicalProjectIdentity: row.canonical_project_identity,
4040
+ status: "failed",
4041
+ safeErrorCode: "recipient_policy_reconciliation_failed"
3119
4042
  });
3120
- return c.json(payload);
3121
4043
  }
3122
- });
3123
- return app;
4044
+ }
4045
+ return result;
3124
4046
  }
3125
- function healthStats(store) {
4047
+ //#endregion
4048
+ //#region src/application/sync-status.ts
4049
+ function traceSync(label, operation) {
4050
+ if (process.env.CODEMEM_TRACE_SYNC_STATUS !== "1") return operation();
4051
+ const startedAt = Date.now();
4052
+ console.warn(`[codemem sync-status] ${label} start`);
4053
+ try {
4054
+ return operation();
4055
+ } finally {
4056
+ console.warn(`[codemem sync-status] ${label} ${Date.now() - startedAt}ms`);
4057
+ }
4058
+ }
4059
+ function readRetentionState(store) {
4060
+ try {
4061
+ return traceSync("retentionState", () => drizzle(store.db, { schema }).select().from(schema.syncRetentionState).where(eq(schema.syncRetentionState.id, 1)).get());
4062
+ } catch {
4063
+ return;
4064
+ }
4065
+ }
4066
+ function readStatusRows(store) {
4067
+ const d = drizzle(store.db, { schema });
4068
+ const device = traceSync("deviceRow", () => d.select({
4069
+ device_id: schema.syncDevice.device_id,
4070
+ fingerprint: schema.syncDevice.fingerprint
4071
+ }).from(schema.syncDevice).limit(1).get());
4072
+ const daemon = traceSync("daemonState", () => d.select().from(schema.syncDaemonState).where(eq(schema.syncDaemonState.id, 1)).get());
4073
+ const peerCount = traceSync("peerCountRow", () => d.select({ total: count() }).from(schema.syncPeers).get());
4074
+ const retention = readRetentionState(store);
4075
+ const lastSync = traceSync("lastSyncRow", () => d.select({ last_sync_at: max(schema.syncPeers.last_sync_at) }).from(schema.syncPeers).get());
3126
4076
  return {
3127
- ...store.stats(),
3128
- automatic_recall: automaticRecallHealth(store.db, {
3129
- actorId: store.actorId,
3130
- deviceId: store.deviceId
3131
- })
4077
+ device,
4078
+ daemon,
4079
+ peerCount: Number(peerCount?.total ?? 0),
4080
+ retention,
4081
+ lastSyncAt: lastSync?.last_sync_at ?? null
4082
+ };
4083
+ }
4084
+ function enrollmentIssueStatus(store, showDiagnostics) {
4085
+ const summary = traceSync("coordinatorEnrollmentIssues", () => getCoordinatorEnrollmentReconciliationIssueSummary(store.db));
4086
+ if (!showDiagnostics) return { counts: summary.counts };
4087
+ return {
4088
+ counts: summary.counts,
4089
+ issues: summary.issues.map((issue) => ({
4090
+ coordinator_id: issue.coordinatorId,
4091
+ group_id: issue.groupId,
4092
+ kind: issue.kind,
4093
+ reference_id: issue.referenceId,
4094
+ code: issue.code,
4095
+ status: issue.status,
4096
+ first_seen_at: issue.firstSeenAt,
4097
+ last_seen_at: issue.lastSeenAt,
4098
+ resolved_at: issue.resolvedAt,
4099
+ occurrence_count: issue.occurrenceCount,
4100
+ updated_at: issue.updatedAt
4101
+ }))
3132
4102
  };
3133
4103
  }
4104
+ function initialDaemonState(config, rows, daemonRunning) {
4105
+ if (!config.syncEnabled) return "disabled";
4106
+ const lastError = rows.daemon?.last_error;
4107
+ const lastErrorAt = rows.daemon?.last_error_at;
4108
+ const lastOkAt = rows.daemon?.last_ok_at;
4109
+ if (lastError && (!lastOkAt || String(lastOkAt) < String(lastErrorAt ?? ""))) return "error";
4110
+ if (!daemonRunning) return "stopped";
4111
+ return "ok";
4112
+ }
4113
+ function retentionPayload(config, rows, retainedFloor) {
4114
+ const state = rows.retention;
4115
+ return {
4116
+ enabled: config.syncRetentionEnabled,
4117
+ max_age_days: config.syncRetentionMaxAgeDays,
4118
+ max_size_mb: config.syncRetentionMaxSizeMb,
4119
+ retained_floor_cursor: retainedFloor ?? state?.retained_floor_cursor ?? null,
4120
+ last_run_at: state?.last_run_at ?? null,
4121
+ last_duration_ms: typeof state?.last_duration_ms === "number" ? state.last_duration_ms : null,
4122
+ last_deleted_ops: typeof state?.last_deleted_ops === "number" ? state.last_deleted_ops : null,
4123
+ last_estimated_bytes_before: typeof state?.last_estimated_bytes_before === "number" ? state.last_estimated_bytes_before : null,
4124
+ last_estimated_bytes_after: typeof state?.last_estimated_bytes_after === "number" ? state.last_estimated_bytes_after : null,
4125
+ last_error: state?.last_error ?? null,
4126
+ last_error_at: state?.last_error_at ?? null
4127
+ };
4128
+ }
4129
+ function addDiagnosticStatusFields(payload, config, rows) {
4130
+ payload.device_id = rows.device?.device_id ?? null;
4131
+ payload.fingerprint = rows.device?.fingerprint ?? null;
4132
+ payload.bind = `${config.syncHost}:${config.syncPort}`;
4133
+ payload.daemon_last_error = rows.daemon?.last_error ?? null;
4134
+ payload.daemon_last_error_at = rows.daemon?.last_error_at ?? null;
4135
+ payload.daemon_last_ok_at = rows.daemon?.last_ok_at ?? null;
4136
+ }
4137
+ function buildBasePayload(input, config, rows, daemon, retainedFloor, semanticIndex, enrollmentIssues) {
4138
+ const payload = {
4139
+ enabled: config.syncEnabled,
4140
+ interval_s: config.syncIntervalS,
4141
+ retention: retentionPayload(config, rows, retainedFloor),
4142
+ semantic_index: semanticIndex,
4143
+ coordinator_enrollment_reconciliation_issues: enrollmentIssues,
4144
+ peer_count: rows.peerCount,
4145
+ last_sync_at: rows.lastSyncAt,
4146
+ daemon_state: daemon.state,
4147
+ daemon_running: daemon.running,
4148
+ daemon_detail: daemon.detail,
4149
+ project_filter_active: config.syncProjectsInclude.length > 0 || config.syncProjectsExclude.length > 0,
4150
+ project_filter: {
4151
+ include: config.syncProjectsInclude,
4152
+ exclude: config.syncProjectsExclude
4153
+ },
4154
+ cleanup_diagnostics: input.operations.cleanupDiagnostics(input.store, rows.device?.device_id ?? null, input.showDiagnostics),
4155
+ redacted: !input.showDiagnostics
4156
+ };
4157
+ if (input.showDiagnostics) addDiagnosticStatusFields(payload, config, rows);
4158
+ return payload;
4159
+ }
4160
+ function readBaseStatus(input) {
4161
+ const config = traceSync("readCoordinatorSyncConfig", () => readCoordinatorSyncConfig());
4162
+ const reset = traceSync("getSyncResetState", () => getSyncResetState(input.store.db));
4163
+ const rows = readStatusRows(input.store);
4164
+ const semanticIndex = traceSync("semanticIndex", () => input.operations.redactSemanticIndexDiagnostics(getSemanticIndexDiagnostics(input.store.db), input.showDiagnostics));
4165
+ const enrollmentIssues = enrollmentIssueStatus(input.store, input.showDiagnostics);
4166
+ const binding = traceSync("readViewerBinding", () => input.operations.readViewerBinding(input.store.dbPath));
4167
+ const running = Boolean(binding);
4168
+ const detail = binding ? `viewer pidfile at ${binding.host}:${binding.port}` : null;
4169
+ let state = initialDaemonState(config, rows, running);
4170
+ const statusPayload = buildBasePayload(input, config, rows, {
4171
+ state,
4172
+ running,
4173
+ detail
4174
+ }, reset.retained_floor_cursor ?? null, semanticIndex, enrollmentIssues);
4175
+ const runtime = input.getSyncRuntimeStatus?.() ?? null;
4176
+ if (runtime?.phase && runtime.phase !== "running") {
4177
+ state = runtime.phase;
4178
+ statusPayload.daemon_state = state;
4179
+ statusPayload.daemon_running = runtime.phase === "starting" || running;
4180
+ statusPayload.daemon_detail = runtime.detail ?? detail;
4181
+ }
4182
+ return {
4183
+ config,
4184
+ localDeviceId: rows.device?.device_id ?? null,
4185
+ statusPayload,
4186
+ daemonState: state,
4187
+ daemonRunning: running,
4188
+ daemonDetail: detail
4189
+ };
4190
+ }
4191
+ function readPeerStatus(input, localDeviceId) {
4192
+ const rows = traceSync("peerRows", () => input.store.db.prepare(PEERS_QUERY$1).all());
4193
+ const recentOperations = traceSync("recentPeerOps", () => input.operations.recentPeerOps(input.store));
4194
+ const scopeRejections = traceSync("recentScopeRejectionsByPeer", () => input.operations.recentScopeRejectionsByPeer(input.store));
4195
+ const items = rows.map((row) => {
4196
+ const peer = input.operations.mapPeerRow(input.store, row, input.showDiagnostics, recentOperations, scopeRejections, localDeviceId);
4197
+ peer.status = input.operations.peerStatus(peer);
4198
+ return peer;
4199
+ });
4200
+ const byId = {};
4201
+ for (const peer of items) byId[String(peer.peer_device_id)] = peer.status;
4202
+ return {
4203
+ items,
4204
+ byId
4205
+ };
4206
+ }
4207
+ function readAttemptRows(store) {
4208
+ return traceSync("attemptRows", () => drizzle(store.db, { schema }).select({
4209
+ peer_device_id: schema.syncAttempts.peer_device_id,
4210
+ ok: schema.syncAttempts.ok,
4211
+ error: schema.syncAttempts.error,
4212
+ started_at: schema.syncAttempts.started_at,
4213
+ finished_at: schema.syncAttempts.finished_at,
4214
+ ops_in: schema.syncAttempts.ops_in,
4215
+ ops_out: schema.syncAttempts.ops_out,
4216
+ local_sync_capability: schema.syncAttempts.local_sync_capability,
4217
+ peer_sync_capability: schema.syncAttempts.peer_sync_capability,
4218
+ negotiated_sync_capability: schema.syncAttempts.negotiated_sync_capability
4219
+ }).from(schema.syncAttempts).orderBy(desc(schema.syncAttempts.finished_at)).limit(25).all());
4220
+ }
4221
+ function readPeerAddressMap(store) {
4222
+ const rows = traceSync("peerAddressRows", () => store.db.prepare("SELECT peer_device_id, addresses_json FROM sync_peers").all());
4223
+ const addressesByPeer = /* @__PURE__ */ new Map();
4224
+ for (const row of rows) {
4225
+ const addresses = safeJsonList(row.addresses_json);
4226
+ if (addresses.length) addressesByPeer.set(String(row.peer_device_id ?? ""), addresses);
4227
+ }
4228
+ return addressesByPeer;
4229
+ }
4230
+ function readAttemptStatus(input) {
4231
+ const rows = readAttemptRows(input.store);
4232
+ const addressesByPeer = readPeerAddressMap(input.store);
4233
+ return {
4234
+ items: rows.map((row) => input.operations.mapSyncAttemptRow(row, input.showDiagnostics, input.showDiagnostics ? addressesByPeer.get(String(row.peer_device_id ?? "")) : void 0)),
4235
+ latestError: String(rows[0]?.error || "").trim()
4236
+ };
4237
+ }
4238
+ function peerStates(peers) {
4239
+ return new Set(peers.items.map((peer) => String(peer.status?.peer_state ?? "")));
4240
+ }
4241
+ function allPeersOffline(peers) {
4242
+ return peers.items.length > 0 && peers.items.every((peer) => String(peer.status?.peer_state ?? "") === "offline");
4243
+ }
4244
+ function degradedDaemonState(input, peers, attempts) {
4245
+ const states = peerStates(peers);
4246
+ const allOffline = allPeersOffline(peers);
4247
+ if (Boolean(attempts.items[0] && attempts.items[0].status === "error" && input.operations.isRecentIso(attempts.items[0].finished_at))) {
4248
+ if (states.has("online") || states.has("degraded")) return "degraded";
4249
+ if (allOffline) return "offline-peers";
4250
+ if (peers.items.length > 0) return "stale";
4251
+ }
4252
+ if (states.has("degraded")) return "degraded";
4253
+ if (allOffline) return "offline-peers";
4254
+ if (peers.items.length > 0 && !states.has("online")) return "stale";
4255
+ return "ok";
4256
+ }
4257
+ function applyDerivedDaemonState(input, base, peers, attempts, statusBlock) {
4258
+ let state = base.daemonState;
4259
+ if (state === "ok" && attempts.latestError.startsWith("needs_attention:")) {
4260
+ state = "needs_attention";
4261
+ const detail = attempts.latestError.replace(/^needs_attention:/, "");
4262
+ base.statusPayload.daemon_detail = detail;
4263
+ statusBlock.daemon_detail = detail;
4264
+ } else if (state === "ok") state = degradedDaemonState(input, peers, attempts);
4265
+ base.statusPayload.daemon_state = state;
4266
+ statusBlock.daemon_state = state;
4267
+ }
4268
+ async function readJoinRequests(input, config) {
4269
+ if (!input.includeJoinRequests || !input.showDiagnostics || !config.syncCoordinatorAdminSecret) return [];
4270
+ try {
4271
+ return await listCoordinatorJoinRequests(config);
4272
+ } catch {
4273
+ return [];
4274
+ }
4275
+ }
4276
+ async function buildSyncStatusResponse(input) {
4277
+ const base = readBaseStatus(input);
4278
+ const coordinatorSnapshot = await coordinatorStatusSnapshot(input.store, base.config);
4279
+ const coordinator = traceSync("coordinator", () => input.operations.redactCoordinatorStatus(coordinatorSnapshot, input.showDiagnostics));
4280
+ const peers = readPeerStatus(input, base.localDeviceId);
4281
+ const attempts = readAttemptStatus(input);
4282
+ const statusBlock = {
4283
+ ...base.statusPayload,
4284
+ background_maintenance: input.operations.summarizeMaintenanceJobs(listMaintenanceJobs(input.store.db), input.showDiagnostics),
4285
+ peers: peers.byId,
4286
+ pending: 0,
4287
+ sync: {},
4288
+ ping: {}
4289
+ };
4290
+ const legacyDevices = traceSync("legacyDevices", () => input.store.claimableLegacyDeviceIds());
4291
+ const legacyReview = traceSync("legacySharedReview", () => input.operations.legacySharedReviewSummary(input.store));
4292
+ const sharingReview = traceSync("sharingReview", () => input.store.sharingReviewSummary(input.project));
4293
+ const recipientPolicyReconciliation = traceSync("recipientPolicyReconciliation", () => input.operations.listRecipientPolicyReconciliationStatus(input.store));
4294
+ const joinRequests = await readJoinRequests(input, base.config);
4295
+ applyDerivedDaemonState(input, base, peers, attempts, statusBlock);
4296
+ const response = {
4297
+ ...base.statusPayload,
4298
+ status: statusBlock,
4299
+ peers: peers.items,
4300
+ attempts: attempts.items.slice(0, 5),
4301
+ legacy_devices: legacyDevices,
4302
+ legacy_shared_review: legacyReview,
4303
+ sharing_review: sharingReview,
4304
+ recipient_policy_reconciliation: recipientPolicyReconciliation,
4305
+ coordinator
4306
+ };
4307
+ if (input.includeJoinRequests && input.showDiagnostics) response.join_requests = joinRequests;
4308
+ return response;
4309
+ }
4310
+ var PEERS_QUERY$1 = `
4311
+ SELECT p.peer_device_id, p.name, p.pinned_fingerprint, p.addresses_json,
4312
+ p.last_seen_at, p.last_sync_at, p.last_error,
4313
+ p.runtime_version, p.runtime_version_observed_at,
4314
+ p.projects_include_json, p.projects_exclude_json, p.claimed_local_actor,
4315
+ p.actor_id, p.discovered_via_coordinator_id, p.discovered_via_group_id,
4316
+ a.display_name AS actor_display_name
4317
+ FROM sync_peers AS p
4318
+ LEFT JOIN actors AS a ON a.actor_id = p.actor_id
4319
+ ORDER BY name, peer_device_id
4320
+ `;
3134
4321
  //#endregion
3135
4322
  //#region src/routes/team-setup-view.ts
3136
4323
  var enabled = {
@@ -5165,18 +6352,31 @@ async function maybeGrantDefaultSpaceOnJoin(opts) {
5165
6352
  adminSecret: opts.config.syncCoordinatorAdminSecret || null
5166
6353
  });
5167
6354
  }
6355
+ function configuredGroups(config) {
6356
+ const rawGroups = config.sync_coordinator_groups;
6357
+ if (Array.isArray(rawGroups)) return rawGroups.map((group) => String(group).trim()).filter(Boolean);
6358
+ if (typeof rawGroups === "string") return rawGroups.split(",").map((group) => group.trim()).filter(Boolean);
6359
+ if (typeof config.sync_coordinator_group === "string" && config.sync_coordinator_group.trim()) return [config.sync_coordinator_group.trim()];
6360
+ return [];
6361
+ }
5168
6362
  function removeConfiguredCoordinatorGroup(groupId) {
5169
6363
  const targetGroup = groupId.trim();
5170
6364
  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);
6365
+ let nextGroups = [];
6366
+ mutateCodememConfigFile((config) => {
6367
+ const groups = configuredGroups(config);
6368
+ nextGroups = groups.filter((group) => group !== targetGroup);
6369
+ const configuredGroup = String(config.sync_coordinator_group ?? "").trim();
6370
+ const removesPluralGroup = nextGroups.length !== groups.length;
6371
+ const removesSingularGroup = configuredGroup === targetGroup;
6372
+ if (!removesPluralGroup && !removesSingularGroup) return void 0;
6373
+ config.sync_coordinator_groups = nextGroups;
6374
+ if (removesSingularGroup) {
6375
+ if (nextGroups.length) config.sync_coordinator_group = nextGroups[0];
6376
+ else delete config.sync_coordinator_group;
6377
+ }
6378
+ return config;
6379
+ });
5180
6380
  return nextGroups;
5181
6381
  }
5182
6382
  function coordinatorAdminUnavailable(status) {
@@ -5892,57 +7092,12 @@ async function advanceProjectShareOperation(store, operationId) {
5892
7092
  if ((await executeProjectShareProvisioning(store, operationId)).superseded) return {
5893
7093
  advanced: false,
5894
7094
  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
- ]);
7095
+ };
7096
+ return {
7097
+ advanced: true,
7098
+ state: "active"
7099
+ };
7100
+ }
5946
7101
  function errorStatus(error) {
5947
7102
  const value = error && typeof error === "object" ? error.status : null;
5948
7103
  return typeof value === "number" && Number.isInteger(value) && value >= 400 && value <= 599 ? value : null;
@@ -5968,163 +7123,11 @@ function projectInviteOwnerErrorResponse(error, fallback) {
5968
7123
  };
5969
7124
  return fallback;
5970
7125
  }
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
7126
  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;
7127
+ return advancePendingProjectSharesOperation(store, {
7128
+ ...options,
7129
+ advanceOperation: options.advanceOperation ?? advanceProjectShareOperation
7130
+ });
6128
7131
  }
6129
7132
  function coordinatorEnrollmentFailureCode(error) {
6130
7133
  const message = (error instanceof Error ? error.message : String(error)).trim();
@@ -6216,9 +7219,6 @@ async function reconcileConfiguredCoordinatorEnrollment(store, options = {}) {
6216
7219
  }
6217
7220
  return total;
6218
7221
  }
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
7222
  var RECIPIENT_POLICY_WAITING_ERRORS = /* @__PURE__ */ new Set([
6223
7223
  "recipient_policy_capability_undetermined",
6224
7224
  "recipient_policy_parity_incomplete",
@@ -6418,59 +7418,10 @@ function createRecipientPolicyReconcilerEffects(store, options = {}) {
6418
7418
  };
6419
7419
  }
6420
7420
  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;
7421
+ return reconcileRecipientPolicyProjectsOperation(store, {
7422
+ ...options,
7423
+ effects: options.effects ?? createRecipientPolicyReconcilerEffects(store)
7424
+ });
6474
7425
  }
6475
7426
  function recipientPolicyReadState(authority) {
6476
7427
  if (!authority) return "pending";
@@ -7045,10 +7996,15 @@ function ensureStableStoreIdentity(store) {
7045
7996
  return deviceId;
7046
7997
  }
7047
7998
  function ensureStableStoreDeviceIdentity(store) {
7048
- const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
7049
- store.adoptEnsuredDeviceIdentity(deviceId);
7999
+ const [deviceId] = ensureStableStoreDeviceIdentityWithFingerprint(store);
7050
8000
  return deviceId;
7051
8001
  }
8002
+ function ensureStableStoreDeviceIdentityWithFingerprint(store) {
8003
+ const identity = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
8004
+ const [deviceId] = identity;
8005
+ store.adoptEnsuredDeviceIdentity(deviceId);
8006
+ return identity;
8007
+ }
7052
8008
  function findPeerDeviceIdForAddress(store, address) {
7053
8009
  const rows = store.db.prepare("SELECT peer_device_id, addresses_json FROM sync_peers").all();
7054
8010
  for (const row of rows) if (safeJsonList(row.addresses_json).includes(address)) return String(row.peer_device_id ?? "").trim() || null;
@@ -7324,6 +8280,23 @@ function claimedLocalActorScopeStatus(store, row) {
7324
8280
  action_required: !grant.authorized
7325
8281
  };
7326
8282
  }
8283
+ function archivedCoordinatorGroupResponse(groupId, group, status, groups) {
8284
+ return {
8285
+ ok: true,
8286
+ group,
8287
+ status,
8288
+ disconnected_group_id: groupId,
8289
+ groups
8290
+ };
8291
+ }
8292
+ function missingArchivedCoordinatorGroupResponse(groupId, status, groups) {
8293
+ return {
8294
+ error: "group_not_found_or_already_archived",
8295
+ status,
8296
+ disconnected_group_id: groupId,
8297
+ groups
8298
+ };
8299
+ }
7327
8300
  /**
7328
8301
  * Map a raw sync_peers DB row to the API response shape.
7329
8302
  * When showDiag is false, sensitive fields (fingerprint, last_error, addresses)
@@ -7443,14 +8416,50 @@ function cleanupDiagnostics(store, localDeviceId, showDiag) {
7443
8416
  redacted: !showDiag
7444
8417
  };
7445
8418
  }
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);
8419
+ function legacySharedReviewSuggestedScope(candidate) {
8420
+ const resolvedScope = candidate?.resolved_scope_id ?? null;
8421
+ if (resolvedScope && resolvedScope !== LOCAL_DEFAULT_SCOPE_ID && resolvedScope !== LEGACY_SHARED_REVIEW_SCOPE_ID) return resolvedScope;
8422
+ return candidate?.suggested_scope_id ?? null;
8423
+ }
8424
+ function addLegacySharedReviewRow(groups, row, identity, candidate, isOwnedBySelf) {
8425
+ const existing = groups.get(identity.value);
8426
+ if (existing) {
8427
+ existing.memory_count += 1;
8428
+ if (isOwnedBySelf) existing.reassignable_memory_count += 1;
8429
+ else existing.peer_owned_memory_count += 1;
8430
+ if (row.updated_at && (!existing.last_updated_at || row.updated_at > existing.last_updated_at)) existing.last_updated_at = row.updated_at;
8431
+ if (existing.memory_sample_rows.length < LEGACY_SHARED_REVIEW_SAMPLE_LIMIT) existing.memory_sample_rows.push({
8432
+ is_owned_by_self: isOwnedBySelf,
8433
+ row
8434
+ });
8435
+ return;
8436
+ }
8437
+ const suggestedScope = legacySharedReviewSuggestedScope(candidate);
8438
+ groups.set(identity.value, {
8439
+ workspace_identity: identity.value,
8440
+ identity_source: identity.source,
8441
+ display_project: identity.displayProject ?? row.project ?? row.cwd ?? identity.value,
8442
+ memory_count: 1,
8443
+ reassignable_memory_count: isOwnedBySelf ? 1 : 0,
8444
+ peer_owned_memory_count: isOwnedBySelf ? 0 : 1,
8445
+ last_updated_at: row.updated_at ?? null,
8446
+ memory_sample_rows: [{
8447
+ is_owned_by_self: isOwnedBySelf,
8448
+ row
8449
+ }],
8450
+ suggested_scope_id: suggestedScope,
8451
+ suggestion_reason: suggestedScope ? candidate?.suggestion_reason ?? "Existing project mapping can be reviewed as a destination, but legacy data is not promoted automatically." : null
8452
+ });
8453
+ }
8454
+ function collectLegacySharedReviewGroups(store) {
7450
8455
  const ownedBySelf = store.buildOwnershipPredicate();
7451
8456
  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) {
8457
+ const groups = /* @__PURE__ */ new Map();
8458
+ let memoryCount = 0;
8459
+ let lastUpdatedAt = null;
8460
+ for (const row of legacySharedReviewSummaryRows(store)) {
8461
+ memoryCount += 1;
8462
+ if (row.updated_at && (!lastUpdatedAt || row.updated_at > lastUpdatedAt)) lastUpdatedAt = row.updated_at;
7454
8463
  const identity = canonicalWorkspaceIdentity({
7455
8464
  cwd: row.cwd,
7456
8465
  gitBranch: row.git_branch,
@@ -7458,35 +8467,27 @@ function legacySharedReviewSummary(store) {
7458
8467
  project: row.project,
7459
8468
  workspaceId: row.workspace_id
7460
8469
  });
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
- });
8470
+ addLegacySharedReviewRow(groups, row, identity, candidatesByIdentity.get(identity.value), ownedBySelf(row));
7487
8471
  }
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));
8472
+ return {
8473
+ groups,
8474
+ lastUpdatedAt,
8475
+ memoryCount
8476
+ };
8477
+ }
8478
+ function materializeLegacySharedReviewGroups(store, groupsByIdentity) {
8479
+ const sampleBodies = legacySharedReviewSampleBodies(store, [...groupsByIdentity.values()].flatMap((group) => group.memory_sample_rows.map((sample) => sample.row.id)));
8480
+ return [...groupsByIdentity.values()].map(({ memory_sample_rows: sampleRows, ...group }) => ({
8481
+ ...group,
8482
+ memory_samples: sampleRows.map(({ is_owned_by_self: isOwnedBySelf, row }) => legacySharedReviewMemorySample({
8483
+ ...row,
8484
+ body_text: sampleBodies.get(row.id) ?? null
8485
+ }, isOwnedBySelf))
8486
+ })).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));
8487
+ }
8488
+ function legacySharedReviewSummary(store) {
8489
+ const collected = collectLegacySharedReviewGroups(store);
8490
+ const groups = materializeLegacySharedReviewGroups(store, collected.groups);
7490
8491
  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
8492
  authority_type: scope.authority_type,
7492
8493
  label: scope.label || scope.scope_id,
@@ -7494,16 +8495,17 @@ function legacySharedReviewSummary(store) {
7494
8495
  }));
7495
8496
  return {
7496
8497
  scope_id: LEGACY_SHARED_REVIEW_SCOPE_ID,
7497
- memory_count: memoryCount,
7498
- has_data: memoryCount > 0,
7499
- last_updated_at: lastUpdatedAt,
8498
+ memory_count: collected.memoryCount,
8499
+ has_data: collected.memoryCount > 0,
8500
+ last_updated_at: collected.lastUpdatedAt,
7500
8501
  groups,
7501
- total_group_count: totalGroupCount,
8502
+ total_group_count: collected.groups.size,
7502
8503
  target_scopes: targetScopes
7503
8504
  };
7504
8505
  }
7505
8506
  var LEGACY_SHARED_REVIEW_SAMPLE_LIMIT = 3;
7506
8507
  var LEGACY_SHARED_REVIEW_BODY_PREVIEW_SQL_LIMIT = 360;
8508
+ var LEGACY_SHARED_REVIEW_SAMPLE_BODY_BATCH_SIZE = 500;
7507
8509
  function previewText(value, limit = 180) {
7508
8510
  const normalized = value?.replace(/\s+/g, " ").trim();
7509
8511
  if (!normalized) return null;
@@ -7523,6 +8525,41 @@ function legacySharedReviewMemorySample(row, isOwnedBySelf) {
7523
8525
  git_remote: row.git_remote ?? null
7524
8526
  };
7525
8527
  }
8528
+ function legacySharedReviewSummaryRows(store) {
8529
+ return store.db.prepare(`SELECT m.id,
8530
+ m.created_at,
8531
+ m.updated_at,
8532
+ m.kind,
8533
+ m.title,
8534
+ s.project,
8535
+ s.cwd,
8536
+ s.git_remote,
8537
+ s.git_branch,
8538
+ m.workspace_id,
8539
+ m.actor_id,
8540
+ m.origin_device_id,
8541
+ m.metadata_json
8542
+ FROM memory_items m
8543
+ LEFT JOIN sessions s ON s.id = m.session_id
8544
+ WHERE m.scope_id = ?
8545
+ AND m.active = 1
8546
+ AND m.deleted_at IS NULL
8547
+ ORDER BY COALESCE(m.updated_at, m.created_at, '') DESC, m.id DESC`).iterate(LEGACY_SHARED_REVIEW_SCOPE_ID);
8548
+ }
8549
+ function legacySharedReviewSampleBodies(store, memoryIds) {
8550
+ const bodies = /* @__PURE__ */ new Map();
8551
+ for (let offset = 0; offset < memoryIds.length; offset += LEGACY_SHARED_REVIEW_SAMPLE_BODY_BATCH_SIZE) {
8552
+ const ids = memoryIds.slice(offset, offset + LEGACY_SHARED_REVIEW_SAMPLE_BODY_BATCH_SIZE);
8553
+ const rows = store.db.prepare(`SELECT id, substr(body_text, 1, ?) AS body_text
8554
+ FROM memory_items
8555
+ WHERE id IN (${ids.map(() => "?").join(",")})
8556
+ AND scope_id = ?
8557
+ AND active = 1
8558
+ AND deleted_at IS NULL`).all(LEGACY_SHARED_REVIEW_BODY_PREVIEW_SQL_LIMIT, ...ids, LEGACY_SHARED_REVIEW_SCOPE_ID);
8559
+ for (const row of rows) if (row.body_text != null) bodies.set(row.id, row.body_text);
8560
+ }
8561
+ return bodies;
8562
+ }
7526
8563
  function legacySharedReviewRows(store) {
7527
8564
  return store.db.prepare(`SELECT m.id,
7528
8565
  m.rev,
@@ -8308,198 +9345,29 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
8308
9345
  }
8309
9346
  });
8310
9347
  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 = [];
9348
+ const response = await buildSyncStatusResponse({
9349
+ store: getStore(),
9350
+ showDiagnostics: queryBool(c.req.query("includeDiagnostics")),
9351
+ includeJoinRequests: queryBool(c.req.query("includeJoinRequests")),
9352
+ project: c.req.query("project") || null,
9353
+ getSyncRuntimeStatus,
9354
+ operations: {
9355
+ cleanupDiagnostics,
9356
+ isRecentIso,
9357
+ legacySharedReviewSummary,
9358
+ listRecipientPolicyReconciliationStatus,
9359
+ mapPeerRow,
9360
+ mapSyncAttemptRow,
9361
+ peerStatus,
9362
+ readViewerBinding,
9363
+ recentPeerOps,
9364
+ recentScopeRejectionsByPeer,
9365
+ redactCoordinatorStatus,
9366
+ redactSemanticIndexDiagnostics,
9367
+ summarizeMaintenanceJobs
8467
9368
  }
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
- }
9369
+ });
9370
+ return c.json(response);
8503
9371
  });
8504
9372
  app.get("/api/sync/peers", (c) => {
8505
9373
  const store = getStore();
@@ -8592,7 +9460,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
8592
9460
  let publicKey;
8593
9461
  let fingerprint;
8594
9462
  try {
8595
- const [id, fp] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
9463
+ const [id, fp] = ensureStableStoreDeviceIdentityWithFingerprint(store);
8596
9464
  deviceId = id;
8597
9465
  fingerprint = fp;
8598
9466
  publicKey = d.select({ public_key: schema.syncDevice.public_key }).from(schema.syncDevice).where(eq(schema.syncDevice.device_id, id)).get()?.public_key ?? "";
@@ -10021,18 +10889,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10021
10889
  remoteUrl: config.syncCoordinatorUrl || null,
10022
10890
  adminSecret: config.syncCoordinatorAdminSecret || null
10023
10891
  });
10024
- if (!group) return c.json({
10025
- error: "group_not_found_or_already_archived",
10026
- status
10027
- }, 404);
10028
10892
  const groups = removeConfiguredCoordinatorGroup(groupId);
10029
- return c.json({
10030
- ok: true,
10031
- group,
10032
- status: coordinatorAdminStatusPayload(),
10033
- disconnected_group_id: groupId,
10034
- groups
10035
- });
10893
+ const currentStatus = coordinatorAdminStatusPayload();
10894
+ if (!group) return c.json(missingArchivedCoordinatorGroupResponse(groupId, currentStatus, groups), 404);
10895
+ return c.json(archivedCoordinatorGroupResponse(groupId, group, currentStatus, groups));
10036
10896
  } catch (error) {
10037
10897
  return c.json({
10038
10898
  error: error instanceof Error ? error.message : String(error),
@@ -11180,7 +12040,7 @@ function createApp(opts) {
11180
12040
  getObserver: () => observer
11181
12041
  }));
11182
12042
  app.route("/", configRoutes({ getSweeper: () => sweeper }));
11183
- app.route("/", rawEventsRoutes(storeFactory, sweeper));
12043
+ app.route("/", rawEventsRoutes(storeFactory, sweeper, opts?.rawEventInbox, opts?.rawEventTarget));
11184
12044
  app.route("/", syncRoutes(storeFactory, getSyncRuntimeStatus, {
11185
12045
  loadDeviceIdentityCoordinatorEvidence: opts?.loadDeviceIdentityCoordinatorEvidence,
11186
12046
  onRecipientPolicyTeamRenamed: () => invalidateTeamSetupSummary(),
@@ -11228,6 +12088,6 @@ function createSyncApp(opts) {
11228
12088
  return app;
11229
12089
  }
11230
12090
  //#endregion
11231
- export { VERSION, advancePendingProjectShares, advanceProjectShareOperation, closeStore, createApp, createRecipientPolicyReconcilerEffects, createSyncApp, getStore, listRecipientPolicyReconciliationStatus, recipientPolicyCapabilityFromStatus, reconcileConfiguredCoordinatorEnrollment, reconcileRecipientPolicyProjects };
12091
+ export { FileRawEventInbox, VERSION, advancePendingProjectShares, advancePendingProjectSharesOperation, advanceProjectShareOperation, closeStore, createApp, createRecipientPolicyReconcilerEffects, createSyncApp, createViewerRawEventInbox, getStore, listRecipientPolicyReconciliationStatus, recipientPolicyCapabilityFromStatus, reconcileConfiguredCoordinatorEnrollment, reconcileRecipientPolicyProjects, reconcileRecipientPolicyProjectsOperation, resolveRawEventInboxDirectory };
11232
12092
 
11233
12093
  //# sourceMappingURL=index.js.map