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