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