@codemem/server 0.46.0-beta.1 → 0.46.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join, posix, resolve, win32 } from "node:path";
3
- import { ACCESS_CLEANUP_OP_TYPE, 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, getCodememEnvOverrideValues, 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, resolveObserverRuntime, 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";
3
+ import { ACCESS_CLEANUP_OP_TYPE, 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, analyzeProjectScopeMappingChangesGuardrails, analyzeProjectScopeMappingDeletionGuardrails, applyLegacyTeamSetupCompletionManifest, applyLegacyTeamSetupCompletionManifestAndReturnActivation, applyReplicationOps, areLegacyTeamSetupCompletionPolicyFactsAdditivelyCompatible, automaticRecallHealth, buildAuthHeaders, buildBaseUrl, buildDirectPeerAuthHeaders, buildDistillReport, buildFilterClausesWithContext, buildPiFlushSignalFromEvent, buildRawEventEnvelopeFromCodexHook, buildRawEventEnvelopeFromHook, buildRawEventEnvelopeFromPiEvent, buildViewerIdentityTarget, canonicalCoordinatorLegacyTeamCompletionManifestJson, canonicalMemoryKind, canonicalRepositoryProjectIdentity, 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, getCodememEnvOverrideValues, 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, repositoryIdentitiesByWorkspace, repositoryIdentityForWorkspace, requestJson, requireLegacyTeamSetupAccessDeltaWithinLimit, resolveDbPath, resolveObserverRuntime, resolveProject, resolveProjectRoot, resolveRecipientPolicyReview, resolveRecipientPolicyReviewBulk, runSyncPass, schema, serializeRecipientPolicyCoordinatorGroupMutation, serializeRecipientPolicyPublicationMutation, serializeRecipientPolicyTeamMutation, setLegacyTeamSetupDeviceAssignment, setLegacyTeamSetupDeviceDecision, setLegacyTeamSetupProjectMapping, storeVectors, summarizeInboundScopeRejections, summaryLikeSqlPredicate, supportsSyncFeature, syncScopeResetRequiredPayload, toJson, tryUpdateRetrievalDelivery, updatePeerAddresses, upsertCoordinatorGroupPreference, upsertProjectScopeSettingsMappings, validateLegacyTeamSetupCompletionManifest, validateLegacyTeamSetupCompletionManifestBinding, validateRawEvents, verifyDirectPeerSignature, verifyRecipientReviewedIntent, verifySignature, wakeRecipientPoliciesForIdentities, wakeRecipientPoliciesForProjectIdentities } 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";
@@ -3847,6 +3847,8 @@ var AUTOMATIC_WAITING_ACCEPTANCE_RETRY_COOLDOWN_MS = 3e4;
3847
3847
  var AUTOMATIC_WAITING_DEVICE_RETRY_COOLDOWN_MS = 3e5;
3848
3848
  var TERMINAL_SHARE_MAINTENANCE_ERRORS = /* @__PURE__ */ new Set([
3849
3849
  "coordinator_not_configured",
3850
+ "conflicting_repository_mappings",
3851
+ "project_mapping_conflict",
3850
3852
  "team_sharing_not_configured",
3851
3853
  "team_selection_ambiguous",
3852
3854
  "initiating_device_not_reviewed",
@@ -4048,25 +4050,69 @@ async function advancePendingProjectSharesOperation(store, options) {
4048
4050
  var RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT = 10;
4049
4051
  var RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT = 3;
4050
4052
  var RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS = 6e4;
4053
+ function recipientPolicyMaintenanceCandidates(store, retryBefore, limit) {
4054
+ const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
4055
+ const canonicalize = (projectId) => canonicalRepositoryProjectIdentity(repositoryIdentities, projectId);
4056
+ const projectIds = store.db.prepare(`SELECT canonical_project_identity FROM project_recipients
4057
+ UNION SELECT canonical_project_identity FROM recipient_policy_authority_states`).all();
4058
+ const authorityRows = store.db.prepare(`SELECT authority_state, canonical_project_identity, last_attempt_at, safe_error_code
4059
+ FROM recipient_policy_authority_states`).all();
4060
+ const authorityByProject = /* @__PURE__ */ new Map();
4061
+ for (const row of authorityRows) {
4062
+ const projectId = canonicalize(row.canonical_project_identity);
4063
+ const current = authorityByProject.get(projectId);
4064
+ if (!current) {
4065
+ authorityByProject.set(projectId, row);
4066
+ continue;
4067
+ }
4068
+ if (current.canonical_project_identity === projectId) continue;
4069
+ if (row.canonical_project_identity === projectId) {
4070
+ authorityByProject.set(projectId, row);
4071
+ continue;
4072
+ }
4073
+ if (String(row.last_attempt_at ?? "") > String(current.last_attempt_at ?? "")) authorityByProject.set(projectId, row);
4074
+ }
4075
+ return [...new Set(projectIds.map((row) => canonicalize(row.canonical_project_identity)))].filter((projectId) => {
4076
+ const authority = authorityByProject.get(projectId);
4077
+ return authority == null || authority.last_attempt_at == null || authority.safe_error_code == null && authority.authority_state !== "active" || authority.last_attempt_at <= retryBefore;
4078
+ }).toSorted((left, right) => {
4079
+ const leftAttempt = authorityByProject.get(left)?.last_attempt_at;
4080
+ const rightAttempt = authorityByProject.get(right)?.last_attempt_at;
4081
+ if (leftAttempt == null && rightAttempt != null) return -1;
4082
+ if (leftAttempt != null && rightAttempt == null) return 1;
4083
+ return String(leftAttempt ?? "").localeCompare(String(rightAttempt ?? "")) || left.localeCompare(right);
4084
+ }).slice(0, limit);
4085
+ }
4086
+ function recordRecipientPolicyMaintenanceFailure(store, projectId, attemptedAt) {
4087
+ store.db.prepare(`INSERT INTO recipient_policy_authority_states(
4088
+ canonical_project_identity, state_changed_at, created_at, updated_at,
4089
+ attempt_count, last_attempt_at, safe_error_code, last_error_at
4090
+ ) VALUES (?, ?, ?, ?, 1, ?, 'recipient_policy_reconciliation_failed', ?)
4091
+ ON CONFLICT(canonical_project_identity) DO UPDATE SET
4092
+ attempt_count = attempt_count + 1, last_attempt_at = excluded.last_attempt_at,
4093
+ safe_error_code = excluded.safe_error_code, last_error_at = excluded.last_error_at,
4094
+ updated_at = excluded.updated_at`).run(projectId, attemptedAt, attemptedAt, attemptedAt, attemptedAt, attemptedAt);
4095
+ }
4096
+ function isSqliteBusy$1(error) {
4097
+ if (!error || typeof error !== "object" || !("code" in error)) return false;
4098
+ const code = error.code;
4099
+ return typeof code === "string" && (code.startsWith("SQLITE_BUSY") || code.startsWith("SQLITE_LOCKED"));
4100
+ }
4101
+ function recordMaintenanceFailureIfAvailable(store, projectId, attemptedAt, error) {
4102
+ if (isSqliteBusy$1(error)) return;
4103
+ try {
4104
+ recordRecipientPolicyMaintenanceFailure(store, projectId, attemptedAt);
4105
+ } catch (bookkeepingError) {
4106
+ if (!isSqliteBusy$1(bookkeepingError)) throw bookkeepingError;
4107
+ }
4108
+ }
4051
4109
  async function reconcileRecipientPolicyProjectsOperation(store, options) {
4052
4110
  const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? RECIPIENT_POLICY_MAINTENANCE_DEFAULT_LIMIT), RECIPIENT_POLICY_MAINTENANCE_MAX_LIMIT));
4053
4111
  const maintenanceNow = options.now ?? /* @__PURE__ */ new Date();
4054
4112
  const backoffMs = Math.max(0, Math.trunc(options.backoffMs ?? RECIPIENT_POLICY_MAINTENANCE_BACKOFF_MS));
4055
4113
  const retryBefore = new Date(maintenanceNow.getTime() - backoffMs).toISOString();
4056
- const rows = store.db.prepare(`WITH projects AS (
4057
- SELECT DISTINCT canonical_project_identity FROM project_recipients
4058
- UNION
4059
- SELECT canonical_project_identity FROM recipient_policy_authority_states
4060
- )
4061
- SELECT projects.canonical_project_identity
4062
- FROM projects
4063
- LEFT JOIN recipient_policy_authority_states authority
4064
- ON authority.canonical_project_identity = projects.canonical_project_identity
4065
- WHERE authority.safe_error_code IS NULL OR authority.last_attempt_at IS NULL
4066
- OR authority.last_attempt_at <= ?
4067
- ORDER BY CASE WHEN authority.last_attempt_at IS NULL THEN 0 ELSE 1 END,
4068
- authority.last_attempt_at, projects.canonical_project_identity
4069
- LIMIT ?`).all(retryBefore, limit);
4114
+ const attemptedAt = maintenanceNow.toISOString();
4115
+ const projectIds = recipientPolicyMaintenanceCandidates(store, retryBefore, limit);
4070
4116
  const reconcileProject = options.reconcileProject ?? reconcileRecipientPolicyProject;
4071
4117
  const result = {
4072
4118
  processed: 0,
@@ -4076,25 +4122,26 @@ async function reconcileRecipientPolicyProjectsOperation(store, options) {
4076
4122
  failed: 0,
4077
4123
  items: []
4078
4124
  };
4079
- for (const row of rows) {
4125
+ for (const projectId of projectIds) {
4080
4126
  result.processed += 1;
4081
4127
  try {
4082
4128
  const outcome = await reconcileProject(store.db, {
4083
- canonicalProjectIdentity: row.canonical_project_identity,
4129
+ canonicalProjectIdentity: projectId,
4084
4130
  leaseOwner: options.leaseOwner ?? `recipient-policy-maintenance:${store.deviceId || process.pid}`
4085
4131
  }, options.effects);
4086
4132
  if (outcome.status === "active") result.active += 1;
4087
4133
  else if (outcome.status === "needs_attention") result.attention += 1;
4088
4134
  else result.waiting += 1;
4089
4135
  result.items.push({
4090
- canonicalProjectIdentity: row.canonical_project_identity,
4136
+ canonicalProjectIdentity: projectId,
4091
4137
  status: outcome.status,
4092
4138
  safeErrorCode: outcome.safeErrorCode
4093
4139
  });
4094
- } catch {
4140
+ } catch (error) {
4141
+ recordMaintenanceFailureIfAvailable(store, projectId, attemptedAt, error);
4095
4142
  result.failed += 1;
4096
4143
  result.items.push({
4097
- canonicalProjectIdentity: row.canonical_project_identity,
4144
+ canonicalProjectIdentity: projectId,
4098
4145
  status: "failed",
4099
4146
  safeErrorCode: "recipient_policy_reconciliation_failed"
4100
4147
  });
@@ -4104,6 +4151,14 @@ async function reconcileRecipientPolicyProjectsOperation(store, options) {
4104
4151
  }
4105
4152
  //#endregion
4106
4153
  //#region src/application/sync-status.ts
4154
+ var SAFE_RUNTIME_DETAILS = {
4155
+ starting: "Running initial sync in background",
4156
+ stopping: "Stopping sync daemon",
4157
+ error: "Background sync failed. Open diagnostics for details.",
4158
+ rebootstrapping: "Restoring sync baseline",
4159
+ needs_attention: "Sync needs attention",
4160
+ disabled: "Sync is off"
4161
+ };
4107
4162
  function traceSync(label, operation) {
4108
4163
  if (process.env.CODEMEM_TRACE_SYNC_STATUS !== "1") return operation();
4109
4164
  const startedAt = Date.now();
@@ -4168,6 +4223,10 @@ function initialDaemonState(config, rows, daemonRunning) {
4168
4223
  if (!daemonRunning) return "stopped";
4169
4224
  return "ok";
4170
4225
  }
4226
+ function safeDaemonIssueCode(state, lastError) {
4227
+ if (state !== "error" || !lastError?.includes("coordinator enrollment maintenance failed")) return null;
4228
+ return lastError.includes(":request_timeout") || lastError.endsWith(" (coordinator_timeout)") ? "coordinator_timeout" : "coordinator_error";
4229
+ }
4171
4230
  function retentionPayload(config, rows, retainedFloor) {
4172
4231
  const state = rows.retention;
4173
4232
  return {
@@ -4235,8 +4294,9 @@ function readBaseStatus(input) {
4235
4294
  state = runtime.phase;
4236
4295
  statusPayload.daemon_state = state;
4237
4296
  statusPayload.daemon_running = runtime.phase === "starting" || running;
4238
- statusPayload.daemon_detail = runtime.detail ?? detail;
4297
+ statusPayload.daemon_detail = input.showDiagnostics ? runtime.detail ?? detail : SAFE_RUNTIME_DETAILS[runtime.phase] ?? null;
4239
4298
  }
4299
+ statusPayload.daemon_issue_code = safeDaemonIssueCode(state, runtime?.phase === "error" ? null : rows.daemon?.last_error);
4240
4300
  return {
4241
4301
  config,
4242
4302
  localDeviceId: rows.device?.device_id ?? null,
@@ -4293,16 +4353,35 @@ function readAttemptStatus(input) {
4293
4353
  latestError: String(rows[0]?.error || "").trim()
4294
4354
  };
4295
4355
  }
4356
+ function latestFailedPeerIds(store, activePeerIds, isRecentIso) {
4357
+ const cutoff = (/* @__PURE__ */ new Date(Date.now() - 6e5)).toISOString();
4358
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4359
+ return traceSync("recentPeerFailures", () => {
4360
+ const rows = store.db.prepare(`SELECT peer_device_id, ok, error, finished_at
4361
+ FROM sync_attempts
4362
+ WHERE (CASE WHEN finished_at IS NULL THEN started_at ELSE finished_at END) BETWEEN ? AND ?
4363
+ ORDER BY (CASE WHEN finished_at IS NULL THEN started_at ELSE finished_at END) DESC, id DESC`).iterate(cutoff, now);
4364
+ const seen = /* @__PURE__ */ new Set();
4365
+ const failed = /* @__PURE__ */ new Set();
4366
+ for (const row of rows) {
4367
+ if (!activePeerIds.has(row.peer_device_id) || seen.has(row.peer_device_id)) continue;
4368
+ if (!isRecentIso(row.finished_at)) continue;
4369
+ seen.add(row.peer_device_id);
4370
+ if (!row.ok && row.error) failed.add(row.peer_device_id);
4371
+ }
4372
+ return failed;
4373
+ });
4374
+ }
4296
4375
  function peerStates(peers) {
4297
4376
  return new Set(peers.items.map((peer) => String(peer.status?.peer_state ?? "")));
4298
4377
  }
4299
4378
  function allPeersOffline(peers) {
4300
4379
  return peers.items.length > 0 && peers.items.every((peer) => String(peer.status?.peer_state ?? "") === "offline");
4301
4380
  }
4302
- function degradedDaemonState(input, peers, attempts) {
4381
+ function degradedDaemonState(peers, failedPeerIds) {
4303
4382
  const states = peerStates(peers);
4304
4383
  const allOffline = allPeersOffline(peers);
4305
- if (Boolean(attempts.items[0] && attempts.items[0].status === "error" && input.operations.isRecentIso(attempts.items[0].finished_at))) {
4384
+ if (failedPeerIds.size > 0) {
4306
4385
  if (states.has("online") || states.has("degraded")) return "degraded";
4307
4386
  if (allOffline) return "offline-peers";
4308
4387
  if (peers.items.length > 0) return "stale";
@@ -4312,14 +4391,14 @@ function degradedDaemonState(input, peers, attempts) {
4312
4391
  if (peers.items.length > 0 && !states.has("online")) return "stale";
4313
4392
  return "ok";
4314
4393
  }
4315
- function applyDerivedDaemonState(input, base, peers, attempts, statusBlock) {
4394
+ function applyDerivedDaemonState(base, peers, attempts, failedPeerIds, statusBlock) {
4316
4395
  let state = base.daemonState;
4317
4396
  if (state === "ok" && attempts.latestError.startsWith("needs_attention:")) {
4318
4397
  state = "needs_attention";
4319
4398
  const detail = attempts.latestError.replace(/^needs_attention:/, "");
4320
4399
  base.statusPayload.daemon_detail = detail;
4321
4400
  statusBlock.daemon_detail = detail;
4322
- } else if (state === "ok") state = degradedDaemonState(input, peers, attempts);
4401
+ } else if (state === "ok") state = degradedDaemonState(peers, failedPeerIds);
4323
4402
  base.statusPayload.daemon_state = state;
4324
4403
  statusBlock.daemon_state = state;
4325
4404
  }
@@ -4337,6 +4416,12 @@ async function buildSyncStatusResponse(input) {
4337
4416
  const coordinator = traceSync("coordinator", () => input.operations.redactCoordinatorStatus(coordinatorSnapshot, input.showDiagnostics));
4338
4417
  const peers = readPeerStatus(input, base.localDeviceId);
4339
4418
  const attempts = readAttemptStatus(input);
4419
+ const activePeerIds = new Set(peers.items.map((peer) => String(peer.peer_device_id ?? "")));
4420
+ const failedPeerIds = latestFailedPeerIds(input.store, activePeerIds, input.operations.isRecentIso);
4421
+ for (const peer of peers.items) {
4422
+ const status = peer.status;
4423
+ status.recent_failed_attempt = failedPeerIds.has(String(peer.peer_device_id ?? ""));
4424
+ }
4340
4425
  const statusBlock = {
4341
4426
  ...base.statusPayload,
4342
4427
  background_maintenance: input.operations.summarizeMaintenanceJobs(listMaintenanceJobs(input.store.db), input.showDiagnostics),
@@ -4350,7 +4435,7 @@ async function buildSyncStatusResponse(input) {
4350
4435
  const sharingReview = traceSync("sharingReview", () => input.store.sharingReviewSummary(input.project));
4351
4436
  const recipientPolicyReconciliation = traceSync("recipientPolicyReconciliation", () => input.operations.listRecipientPolicyReconciliationStatus(input.store));
4352
4437
  const joinRequests = await readJoinRequests(input, base.config);
4353
- applyDerivedDaemonState(input, base, peers, attempts, statusBlock);
4438
+ applyDerivedDaemonState(base, peers, attempts, failedPeerIds, statusBlock);
4354
4439
  const response = {
4355
4440
  ...base.statusPayload,
4356
4441
  status: statusBlock,
@@ -6594,6 +6679,14 @@ function missingGuardrailConfirmations(warnings, confirmedTokens) {
6594
6679
  const confirmed = new Set(confirmedTokens);
6595
6680
  return warnings.filter((warning) => warning.requires_confirmation && (!warning.confirmation_token || !confirmed.has(warning.confirmation_token)));
6596
6681
  }
6682
+ function mappingGuardrailChallenge(warnings, missing) {
6683
+ return {
6684
+ error: "guardrail_confirmation_required",
6685
+ required_guardrails: [...new Set(missing.map((warning) => warning.code))],
6686
+ required_guardrail_tokens: [...new Set(missing.map((warning) => warning.confirmation_token))].filter((token) => typeof token === "string" && token.length > 0),
6687
+ guardrail_warnings: warnings
6688
+ };
6689
+ }
6597
6690
  function parseViewerProjectMappingInput(body) {
6598
6691
  const id = optionalViewerInteger(body, "id");
6599
6692
  const priority = optionalViewerInteger(body, "priority");
@@ -6608,6 +6701,14 @@ function parseViewerProjectMappingInput(body) {
6608
6701
  source: optionalViewerStrictString(body, "source") ?? "user"
6609
6702
  };
6610
6703
  }
6704
+ function parseViewerProjectMappingInputs(body) {
6705
+ const rawMappings = body.mappings;
6706
+ if (!Array.isArray(rawMappings) || rawMappings.length === 0) throw new Error("mappings must be a non-empty array");
6707
+ return rawMappings.map((raw) => {
6708
+ if (raw == null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("each mapping must be an object");
6709
+ return parseViewerProjectMappingInput(raw);
6710
+ });
6711
+ }
6611
6712
  function coordinatorAdminMutationStatus(message) {
6612
6713
  if (message.includes("scope_not_found") || message.includes("membership_not_found") || message.includes("group_not_found") || message.includes("Group not found") || message.includes("Scope not found") || message.includes("device_not_enrolled_for_scope_group") || message.includes("device must be enrolled")) return 404;
6613
6714
  if (message.includes("not active") || message.includes("scope_not_active") || message.includes("scope_membership_effect_conflict") || message.includes("group_archived") || message.includes("Group is archived")) return 409;
@@ -7366,6 +7467,21 @@ function recipientPolicyPresenceCapabilityExpiresAt(enrollment, observedAt) {
7366
7467
  function recipientPolicyEnrollmentValueIsValid(value) {
7367
7468
  return typeof value === "string" && value.length > 0 && value === value.trim();
7368
7469
  }
7470
+ function recipientPolicyPresenceCapabilityExpiries(enrollments, observedAt) {
7471
+ return new Map(enrollments.map((enrollment) => [enrollment.device_id, recipientPolicyPresenceCapabilityExpiresAt(enrollment, observedAt)]));
7472
+ }
7473
+ function recipientPolicySnapshotUnavailable(stage, error) {
7474
+ console.warn(`[sync] recipient policy coordinator snapshot failed: stage=${stage} code=${coordinatorEnrollmentFailureCode(error)}`);
7475
+ throw new Error("recipient_policy_snapshot_not_fresh");
7476
+ }
7477
+ async function readRecipientPolicyCoordinatorSnapshot(read) {
7478
+ for (let attempt = 0;; attempt++) try {
7479
+ return await read();
7480
+ } catch (error) {
7481
+ if (!(error instanceof RemoteCoordinatorRequestError) || error.status !== 429 || attempt >= 2) throw error;
7482
+ await new Promise((resolve) => setTimeout(resolve, (attempt + 1) * 1e3));
7483
+ }
7484
+ }
7369
7485
  function createRecipientPolicyReconcilerEffects(store, options = {}) {
7370
7486
  const config = options.config ?? readCoordinatorSyncConfig();
7371
7487
  const now = options.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
@@ -7407,15 +7523,13 @@ function createRecipientPolicyReconcilerEffects(store, options = {}) {
7407
7523
  now,
7408
7524
  snapshot: async ({ scopeId }) => {
7409
7525
  const targetOptions = coordinatorOptions(scopeId);
7410
- const snapshotMemberships = (await coordinatorListScopeMembershipsAction({
7526
+ const snapshotMemberships = (await readRecipientPolicyCoordinatorSnapshot(() => coordinatorListScopeMembershipsAction({
7411
7527
  groupId: targetOptions.groupId,
7412
7528
  scopeId,
7413
7529
  includeRevoked: true,
7414
7530
  remoteUrl: targetOptions.remoteUrl,
7415
7531
  adminSecret: targetOptions.adminSecret
7416
- }).catch(() => {
7417
- throw new Error("recipient_policy_snapshot_not_fresh");
7418
- })).map((membership) => {
7532
+ })).catch((error) => recipientPolicySnapshotUnavailable("scope_memberships", error))).map((membership) => {
7419
7533
  if (typeof membership.device_id !== "string" || membership.status !== "active" && membership.status !== "revoked") throw new Error("recipient_policy_snapshot_invalid");
7420
7534
  const status = membership.status;
7421
7535
  return {
@@ -7436,15 +7550,13 @@ function createRecipientPolicyReconcilerEffects(store, options = {}) {
7436
7550
  listBoundaryEnrollments: async ({ scopeId }) => {
7437
7551
  const targetOptions = coordinatorOptions(scopeId);
7438
7552
  const observedAt = now();
7439
- const enrollments = await listDevices({
7553
+ const enrollments = await readRecipientPolicyCoordinatorSnapshot(() => listDevices({
7440
7554
  groupId: targetOptions.groupId,
7441
7555
  includeDisabled: true,
7442
7556
  remoteUrl: targetOptions.remoteUrl,
7443
7557
  adminSecret: targetOptions.adminSecret
7444
- }).catch(() => {
7445
- throw new Error("recipient_policy_snapshot_not_fresh");
7446
- });
7447
- const presenceCapabilityExpiries = new Map(enrollments.map((enrollment) => [enrollment.device_id, recipientPolicyPresenceCapabilityExpiresAt(enrollment, observedAt)]));
7558
+ })).catch((error) => recipientPolicySnapshotUnavailable("device_enrollments", error));
7559
+ const presenceCapabilityExpiries = recipientPolicyPresenceCapabilityExpiries(enrollments, observedAt);
7448
7560
  const mapped = enrollments.map((enrollment) => {
7449
7561
  if (enrollment.group_id !== targetOptions.groupId || !recipientPolicyEnrollmentValueIsValid(enrollment.device_id) || !recipientPolicyEnrollmentValueIsValid(enrollment.public_key) || !recipientPolicyEnrollmentValueIsValid(enrollment.fingerprint) || fingerprintPublicKey(enrollment.public_key) !== enrollment.fingerprint || enrollment.enabled !== 0 && enrollment.enabled !== 1 || enrollment.identity_id !== null && !recipientPolicyEnrollmentValueIsValid(enrollment.identity_id)) throw new Error("recipient_policy_snapshot_invalid");
7450
7562
  return {
@@ -7565,13 +7677,25 @@ function recipientPolicyReadCopy(state) {
7565
7677
  explanation: "Legacy scope enforcement remains in control while reconciliation is pending."
7566
7678
  };
7567
7679
  }
7680
+ function recipientPolicyAuthorityForAliases(store, canonicalProjectIdentity, aliases) {
7681
+ const canonical = getRecipientPolicyAuthorityState(store.db, canonicalProjectIdentity);
7682
+ if (canonical) return canonical;
7683
+ return aliases.filter((identity) => identity !== canonicalProjectIdentity).map((identity) => getRecipientPolicyAuthorityState(store.db, identity)).filter((state) => state != null).toSorted((left, right) => right.updatedAt.localeCompare(left.updatedAt))[0] ?? null;
7684
+ }
7568
7685
  function listRecipientPolicyReconciliationStatus(store) {
7686
+ const projectIds = store.db.prepare(`SELECT DISTINCT canonical_project_identity FROM project_recipients WHERE status = 'active'
7687
+ UNION SELECT canonical_project_identity FROM recipient_policy_authority_states
7688
+ ORDER BY canonical_project_identity`).all().map((row) => row.canonical_project_identity);
7689
+ const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
7690
+ const aliasesByProject = /* @__PURE__ */ new Map();
7691
+ for (const projectId of projectIds) {
7692
+ const canonical = canonicalRepositoryProjectIdentity(repositoryIdentities, projectId);
7693
+ aliasesByProject.set(canonical, [...aliasesByProject.get(canonical) ?? [], projectId]);
7694
+ }
7569
7695
  return {
7570
7696
  version: 1,
7571
- items: store.db.prepare(`SELECT DISTINCT canonical_project_identity FROM project_recipients WHERE status = 'active'
7572
- UNION SELECT canonical_project_identity FROM recipient_policy_authority_states
7573
- ORDER BY canonical_project_identity`).all().map((row) => row.canonical_project_identity).map((canonicalProjectIdentity) => {
7574
- const state = recipientPolicyReadState(getRecipientPolicyAuthorityState(store.db, canonicalProjectIdentity));
7697
+ items: [...aliasesByProject.keys()].toSorted().map((canonicalProjectIdentity) => {
7698
+ const state = recipientPolicyReadState(recipientPolicyAuthorityForAliases(store, canonicalProjectIdentity, aliasesByProject.get(canonicalProjectIdentity) ?? []));
7575
7699
  return {
7576
7700
  canonicalProjectIdentity,
7577
7701
  state,
@@ -8565,6 +8689,7 @@ function addLegacySharedReviewRow(groups, row, identity, candidate, isOwnedBySel
8565
8689
  }
8566
8690
  function collectLegacySharedReviewGroups(store) {
8567
8691
  const ownedBySelf = store.buildOwnershipPredicate();
8692
+ const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
8568
8693
  const candidatesByIdentity = new Map(listProjectScopeCandidates(store.db, { limit: null }).map((candidate) => [candidate.workspace_identity, candidate]));
8569
8694
  const groups = /* @__PURE__ */ new Map();
8570
8695
  let memoryCount = 0;
@@ -8576,6 +8701,11 @@ function collectLegacySharedReviewGroups(store) {
8576
8701
  cwd: row.cwd,
8577
8702
  gitBranch: row.git_branch,
8578
8703
  gitRemote: row.git_remote,
8704
+ repositoryIdentity: repositoryIdentityForWorkspace(repositoryIdentities, {
8705
+ cwd: row.cwd,
8706
+ gitRemote: row.git_remote,
8707
+ metadataJson: row.session_metadata_json
8708
+ }),
8579
8709
  project: row.project,
8580
8710
  workspaceId: row.workspace_id
8581
8711
  });
@@ -8650,7 +8780,8 @@ function legacySharedReviewSummaryRows(store) {
8650
8780
  m.workspace_id,
8651
8781
  m.actor_id,
8652
8782
  m.origin_device_id,
8653
- m.metadata_json
8783
+ m.metadata_json,
8784
+ s.metadata_json AS session_metadata_json
8654
8785
  FROM memory_items m
8655
8786
  LEFT JOIN sessions s ON s.id = m.session_id
8656
8787
  WHERE m.scope_id = ?
@@ -8690,7 +8821,8 @@ function legacySharedReviewRows(store) {
8690
8821
  m.workspace_id,
8691
8822
  m.actor_id,
8692
8823
  m.origin_device_id,
8693
- m.metadata_json
8824
+ m.metadata_json,
8825
+ s.metadata_json AS session_metadata_json
8694
8826
  FROM memory_items m
8695
8827
  LEFT JOIN sessions s ON s.id = m.session_id
8696
8828
  WHERE m.scope_id = ?
@@ -8707,7 +8839,7 @@ function legacySharedReviewConfirmationToken(input) {
8707
8839
  return Buffer.from(`${input.workspaceIdentity}|${input.scopeId}|${payload}`, "utf8").toString("base64url");
8708
8840
  }
8709
8841
  function projectInventoryRowsForWorkspace(store, workspaceIdentity) {
8710
- return store.db.prepare(`SELECT m.id,
8842
+ const rows = store.db.prepare(`SELECT m.id,
8711
8843
  m.rev,
8712
8844
  s.project,
8713
8845
  s.cwd,
@@ -8716,15 +8848,23 @@ function projectInventoryRowsForWorkspace(store, workspaceIdentity) {
8716
8848
  m.workspace_id,
8717
8849
  m.actor_id,
8718
8850
  m.origin_device_id,
8719
- m.metadata_json
8851
+ m.metadata_json,
8852
+ s.metadata_json AS session_metadata_json
8720
8853
  FROM memory_items m
8721
8854
  LEFT JOIN sessions s ON s.id = m.session_id
8722
8855
  WHERE m.active = 1
8723
- AND m.deleted_at IS NULL`).all().filter((row) => {
8856
+ AND m.deleted_at IS NULL`).all();
8857
+ const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
8858
+ return rows.filter((row) => {
8724
8859
  return canonicalWorkspaceIdentity({
8725
8860
  cwd: row.cwd,
8726
8861
  gitBranch: row.git_branch,
8727
8862
  gitRemote: row.git_remote,
8863
+ repositoryIdentity: repositoryIdentityForWorkspace(repositoryIdentities, {
8864
+ cwd: row.cwd,
8865
+ gitRemote: row.git_remote,
8866
+ metadataJson: row.session_metadata_json
8867
+ }),
8728
8868
  project: row.project,
8729
8869
  workspaceId: row.workspace_id
8730
8870
  }).value === workspaceIdentity;
@@ -8778,11 +8918,17 @@ function assertLocalDeviceScopeMembership(store, scopeId) {
8778
8918
  LIMIT 1`).get(scopeId, store.deviceId)) throw new Error(`local device is not a member of Sharing domain ${scopeId}`);
8779
8919
  }
8780
8920
  function legacySharedReviewRowsForWorkspace(store, workspaceIdentity) {
8921
+ const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db);
8781
8922
  return legacySharedReviewRows(store).filter((row) => {
8782
8923
  return canonicalWorkspaceIdentity({
8783
8924
  cwd: row.cwd,
8784
8925
  gitBranch: row.git_branch,
8785
8926
  gitRemote: row.git_remote,
8927
+ repositoryIdentity: repositoryIdentityForWorkspace(repositoryIdentities, {
8928
+ cwd: row.cwd,
8929
+ gitRemote: row.git_remote,
8930
+ metadataJson: row.session_metadata_json
8931
+ }),
8786
8932
  project: row.project,
8787
8933
  workspaceId: row.workspace_id
8788
8934
  }).value === workspaceIdentity;
@@ -9329,6 +9475,119 @@ function serializeProjectScopeInventory(store, inventory, sharingByProject) {
9329
9475
  }))
9330
9476
  };
9331
9477
  }
9478
+ async function deleteProjectScopeMappingRoute(c, store) {
9479
+ const id = Number(c.req.param("id"));
9480
+ let releasePublicationMutation;
9481
+ try {
9482
+ const body = c.req.header("content-type")?.includes("application/json") ? await parseViewerJsonBody(c) : {};
9483
+ if (!body) return c.json({ error: "invalid json" }, 400);
9484
+ const confirmedGuardrailTokens = optionalViewerStringList(body, "confirmed_guardrail_tokens");
9485
+ releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
9486
+ const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
9487
+ const warnings = analyzeProjectScopeMappingDeletionGuardrails(store.db, id, deviceId);
9488
+ const missing = missingGuardrailConfirmations(warnings, confirmedGuardrailTokens);
9489
+ if (missing.length > 0) return c.json({
9490
+ error: "guardrail_confirmation_required",
9491
+ required_guardrails: [...new Set(missing.map((warning) => warning.code))],
9492
+ required_guardrail_tokens: missing.map((warning) => warning.confirmation_token),
9493
+ guardrail_warnings: warnings
9494
+ }, 409);
9495
+ const deleted = deleteProjectMappingAndWakePolicies(store, id, deviceId, confirmedGuardrailTokens);
9496
+ return c.json({
9497
+ ok: true,
9498
+ deleted
9499
+ });
9500
+ } catch (error) {
9501
+ return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
9502
+ } finally {
9503
+ releasePublicationMutation?.();
9504
+ }
9505
+ }
9506
+ function projectSharingByRepository(store, operations) {
9507
+ const operationById = new Map(operations.map((operation) => [operation.operation_id, operation]));
9508
+ const sharingByProject = /* @__PURE__ */ new Map();
9509
+ const mappings = store.db.prepare("SELECT workspace_identity, project_pattern FROM project_scope_mappings").all();
9510
+ const repositoryIdentities = repositoryIdentitiesByWorkspace(store.db, { knownRepositoryIdentities: mappings.flatMap((mapping) => [mapping.workspace_identity, mapping.project_pattern]) });
9511
+ const seenOperationProjects = /* @__PURE__ */ new Set();
9512
+ const reviewed = store.db.prepare(`SELECT p.operation_id, p.canonical_project_identity
9513
+ FROM share_operation_projects p
9514
+ JOIN share_operations o ON o.operation_id = p.operation_id
9515
+ WHERE o.inviter_actor_id = ? ORDER BY o.created_at, p.ordinal`).all(store.actorId);
9516
+ for (const item of reviewed) {
9517
+ const operation = operationById.get(item.operation_id);
9518
+ if (!operation) continue;
9519
+ const projectId = canonicalRepositoryProjectIdentity(repositoryIdentities, item.canonical_project_identity);
9520
+ const operationProjectKey = `${item.operation_id}\u0000${projectId}`;
9521
+ if (seenOperationProjects.has(operationProjectKey)) continue;
9522
+ seenOperationProjects.add(operationProjectKey);
9523
+ const current = sharingByProject.get(projectId) ?? [];
9524
+ current.push({
9525
+ person: operation.person,
9526
+ lifecycle: {
9527
+ state: operation.lifecycle.state,
9528
+ label: operation.lifecycle.label,
9529
+ explanation: operation.lifecycle.explanation
9530
+ }
9531
+ });
9532
+ sharingByProject.set(projectId, current);
9533
+ }
9534
+ return sharingByProject;
9535
+ }
9536
+ function deactivateActorAndWakePolicies(store, actorId, now) {
9537
+ store.db.transaction(() => {
9538
+ store.db.prepare("UPDATE actors SET status = 'deactivated', updated_at = ? WHERE actor_id = ?").run(now, actorId);
9539
+ store.db.prepare("UPDATE sync_peers SET actor_id = NULL WHERE actor_id = ?").run(actorId);
9540
+ wakeRecipientPoliciesForIdentities(store.db, [actorId], now);
9541
+ }).immediate();
9542
+ }
9543
+ function finishActorMerge(store, primaryActorId, secondaryActorId, now) {
9544
+ store.db.prepare("UPDATE actors SET status = 'merged', merged_into_actor_id = ?, updated_at = ? WHERE actor_id = ?").run(primaryActorId, now, secondaryActorId);
9545
+ wakeRecipientPoliciesForIdentities(store.db, [primaryActorId, secondaryActorId], now);
9546
+ }
9547
+ function policyWakeMappings(store) {
9548
+ return store.db.prepare("SELECT id, scope_id, workspace_identity FROM project_scope_mappings ORDER BY id").all();
9549
+ }
9550
+ function existingRequestedMappings(mappings, inputs) {
9551
+ const requestedIds = new Set(inputs.map((input) => input.id).filter((id) => id != null));
9552
+ const requestedIdentities = new Set(inputs.map((input) => input.workspace_identity).filter((identity) => identity != null).map((identity) => canonicalWorkspaceIdentity({ cwd: identity }).value));
9553
+ return mappings.filter((mapping) => {
9554
+ if (requestedIds.has(mapping.id)) return true;
9555
+ if (!mapping.workspace_identity) return false;
9556
+ return requestedIdentities.has(canonicalWorkspaceIdentity({ cwd: mapping.workspace_identity }).value);
9557
+ });
9558
+ }
9559
+ function mappedProjectsInScopes(mappings, scopeIds) {
9560
+ return mappings.filter((mapping) => scopeIds.has(mapping.scope_id)).map((mapping) => mapping.workspace_identity).filter((identity) => identity != null);
9561
+ }
9562
+ function wakeRecipientPolicyProjects(store, projectIdentities, now) {
9563
+ wakeRecipientPoliciesForProjectIdentities(store.db, [...new Set(projectIdentities)], now);
9564
+ }
9565
+ function saveProjectMappingsAndWakePolicies(store, deviceId, mappingInputs) {
9566
+ return store.db.transaction(() => {
9567
+ const before = policyWakeMappings(store);
9568
+ const previousMappings = existingRequestedMappings(before, mappingInputs);
9569
+ const mappings = upsertProjectScopeSettingsMappings(store.db, mappingInputs, { deviceId });
9570
+ const after = policyWakeMappings(store);
9571
+ const changedScopeIds = /* @__PURE__ */ new Set([...previousMappings.map((mapping) => mapping.scope_id), ...mappings.map((mapping) => mapping.scope_id)]);
9572
+ wakeRecipientPolicyProjects(store, [...mappedProjectsInScopes(before, changedScopeIds), ...mappedProjectsInScopes(after, changedScopeIds)], (/* @__PURE__ */ new Date()).toISOString());
9573
+ return mappings;
9574
+ }).immediate();
9575
+ }
9576
+ function deleteProjectMappingAndWakePolicies(store, mappingId, deviceId, confirmedGuardrailTokens) {
9577
+ return store.db.transaction(() => {
9578
+ const before = policyWakeMappings(store);
9579
+ const removed = before.find((mapping) => mapping.id === mappingId);
9580
+ const deleted = deleteProjectScopeSettingsMapping(store.db, mappingId, {
9581
+ deviceId,
9582
+ confirmedGuardrailTokens
9583
+ });
9584
+ if (deleted && removed) {
9585
+ const changedScopeIds = /* @__PURE__ */ new Set([removed.scope_id]);
9586
+ wakeRecipientPolicyProjects(store, [...mappedProjectsInScopes(before, changedScopeIds), ...mappedProjectsInScopes(policyWakeMappings(store), changedScopeIds)], (/* @__PURE__ */ new Date()).toISOString());
9587
+ }
9588
+ return deleted;
9589
+ }).immediate();
9590
+ }
9332
9591
  /**
9333
9592
  * Viewer-facing sync management routes (/api/sync/*).
9334
9593
  *
@@ -9961,27 +10220,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
9961
10220
  scopeId: c.req.query("scope_id"),
9962
10221
  status: c.req.query("status")
9963
10222
  });
9964
- const operations = await shareOperationReadModels(store, void 0, false);
9965
- const operationById = new Map(operations.map((operation) => [operation.operation_id, operation]));
9966
- const sharingByProject = /* @__PURE__ */ new Map();
9967
- const reviewed = store.db.prepare(`SELECT p.operation_id, p.canonical_project_identity
9968
- FROM share_operation_projects p
9969
- JOIN share_operations o ON o.operation_id = p.operation_id
9970
- WHERE o.inviter_actor_id = ? ORDER BY o.created_at, p.ordinal`).all(store.actorId);
9971
- for (const item of reviewed) {
9972
- const operation = operationById.get(item.operation_id);
9973
- if (!operation) continue;
9974
- const current = sharingByProject.get(item.canonical_project_identity) ?? [];
9975
- current.push({
9976
- person: operation.person,
9977
- lifecycle: {
9978
- state: operation.lifecycle.state,
9979
- label: operation.lifecycle.label,
9980
- explanation: operation.lifecycle.explanation
9981
- }
9982
- });
9983
- sharingByProject.set(item.canonical_project_identity, current);
9984
- }
10223
+ const sharingByProject = projectSharingByRepository(store, await shareOperationReadModels(store, void 0, false));
9985
10224
  return c.json(serializeProjectScopeInventory(store, inventory, sharingByProject));
9986
10225
  });
9987
10226
  app.post("/api/sync/projects/reassign-project", async (c) => {
@@ -10238,28 +10477,19 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10238
10477
  const confirmedGuardrailTokens = optionalViewerStringList(body, "confirmed_guardrail_tokens");
10239
10478
  const mappingInput = parseViewerProjectMappingInput(body);
10240
10479
  releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
10241
- const analysis = analyzeProjectScopeMappingChangeGuardrails(store.db, mappingInput);
10480
+ const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
10481
+ const analysis = analyzeProjectScopeMappingChangeGuardrails(store.db, {
10482
+ ...mappingInput,
10483
+ deviceId
10484
+ });
10242
10485
  if (analysis.requested_workspace_identity?.startsWith("unmapped:")) return c.json({
10243
10486
  error: "unmapped_project_local_only",
10244
10487
  message: "Unmapped projects need a stable path, git remote, or workspace id before assigning a Sharing domain.",
10245
10488
  guardrail_warnings: analysis.warnings
10246
10489
  }, 400);
10247
10490
  const missingConfirmations = missingGuardrailConfirmations(analysis.warnings, confirmedGuardrailTokens);
10248
- if (missingConfirmations.length > 0) {
10249
- const missingCodes = [...new Set(missingConfirmations.map((warning) => warning.code))];
10250
- const missingTokens = [...new Set(missingConfirmations.map((warning) => warning.confirmation_token))].filter((token) => typeof token === "string" && token.length > 0);
10251
- return c.json({
10252
- error: "guardrail_confirmation_required",
10253
- required_guardrails: missingCodes,
10254
- required_guardrail_tokens: missingTokens,
10255
- guardrail_warnings: analysis.warnings
10256
- }, 409);
10257
- }
10258
- const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
10259
- const mapping = upsertProjectScopeSettingsMapping(store.db, {
10260
- deviceId,
10261
- ...mappingInput
10262
- });
10491
+ if (missingConfirmations.length > 0) return c.json(mappingGuardrailChallenge(analysis.warnings, missingConfirmations), 409);
10492
+ const [mapping] = saveProjectMappingsAndWakePolicies(store, deviceId, [mappingInput]);
10263
10493
  return c.json({
10264
10494
  ok: true,
10265
10495
  mapping,
@@ -10277,14 +10507,13 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10277
10507
  if (!body) return c.json({ error: "invalid json" }, 400);
10278
10508
  let releasePublicationMutation;
10279
10509
  try {
10280
- const rawMappings = body.mappings;
10281
- if (!Array.isArray(rawMappings) || rawMappings.length === 0) return c.json({ error: "mappings must be a non-empty array" }, 400);
10282
- const mappingInputs = rawMappings.map((raw) => {
10283
- if (raw == null || typeof raw !== "object" || Array.isArray(raw)) throw new Error("each mapping must be an object");
10284
- return parseViewerProjectMappingInput(raw);
10285
- });
10510
+ const mappingInputs = parseViewerProjectMappingInputs(body);
10286
10511
  releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
10287
- const analyses = mappingInputs.map((mappingInput) => analyzeProjectScopeMappingChangeGuardrails(store.db, mappingInput));
10512
+ const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
10513
+ const analyses = analyzeProjectScopeMappingChangesGuardrails(store.db, mappingInputs.map((input) => ({
10514
+ ...input,
10515
+ deviceId
10516
+ })));
10288
10517
  const unmapped = analyses.find((analysis) => analysis.requested_workspace_identity?.startsWith("unmapped:"));
10289
10518
  if (unmapped) return c.json({
10290
10519
  error: "unmapped_project_local_only",
@@ -10292,24 +10521,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10292
10521
  guardrail_warnings: unmapped.warnings
10293
10522
  }, 400);
10294
10523
  const missingConfirmations = analyses.flatMap((analysis) => missingGuardrailConfirmations(analysis.warnings, []));
10295
- if (missingConfirmations.length > 0) {
10296
- const missingCodes = [...new Set(missingConfirmations.map((warning) => warning.code))];
10297
- const missingTokens = [...new Set(missingConfirmations.map((warning) => warning.confirmation_token))].filter((token) => typeof token === "string" && token.length > 0);
10298
- return c.json({
10299
- error: "guardrail_confirmation_required",
10300
- required_guardrails: missingCodes,
10301
- required_guardrail_tokens: missingTokens,
10302
- guardrail_warnings: analyses.flatMap((analysis) => analysis.warnings)
10303
- }, 409);
10304
- }
10305
- const [deviceId] = ensureDeviceIdentity(store.db, { keysDir: syncKeysDir() });
10306
- const saveMappings = store.db.transaction(() => mappingInputs.map((mappingInput) => upsertProjectScopeSettingsMapping(store.db, {
10307
- deviceId,
10308
- ...mappingInput
10309
- })));
10524
+ if (missingConfirmations.length > 0) return c.json(mappingGuardrailChallenge(analyses.flatMap((analysis) => analysis.warnings), missingConfirmations), 409);
10310
10525
  return c.json({
10311
10526
  ok: true,
10312
- mappings: saveMappings(),
10527
+ mappings: saveProjectMappingsAndWakePolicies(store, deviceId, mappingInputs),
10313
10528
  guardrail_warnings: analyses.flatMap((analysis) => analysis.warnings)
10314
10529
  });
10315
10530
  } catch (error) {
@@ -10318,23 +10533,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10318
10533
  releasePublicationMutation?.();
10319
10534
  }
10320
10535
  });
10321
- app.delete("/api/sync/sharing-domains/project-mappings/:id", async (c) => {
10322
- const store = getStore();
10323
- const id = Number(c.req.param("id"));
10324
- let releasePublicationMutation;
10325
- try {
10326
- releasePublicationMutation = await claimRecipientPolicyPublicationMutation(store.db);
10327
- const deleted = deleteProjectScopeSettingsMapping(store.db, id);
10328
- return c.json({
10329
- ok: true,
10330
- deleted
10331
- });
10332
- } catch (error) {
10333
- return c.json({ error: error instanceof Error ? error.message : String(error) }, 400);
10334
- } finally {
10335
- releasePublicationMutation?.();
10336
- }
10337
- });
10536
+ app.delete("/api/sync/sharing-domains/project-mappings/:id", (c) => deleteProjectScopeMappingRoute(c, getStore()));
10338
10537
  app.post("/api/sync/peers/identity", async (c) => {
10339
10538
  const store = getStore();
10340
10539
  ensureLocalActorRecord(store);
@@ -10592,11 +10791,7 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10592
10791
  WHERE inviter_actor_id = ? OR recipient_actor_id = ? OR person_id = ?`).run(secondaryActorId, primaryActorId, secondaryActorId, primaryActorId, secondaryActorId, primaryActorId, now, secondaryActorId, secondaryActorId, secondaryActorId);
10593
10792
  const peerUpdate = d.update(schema.syncPeers).set({ actor_id: primaryActorId }).where(eq(schema.syncPeers.actor_id, secondaryActorId)).run();
10594
10793
  if (primary.is_local) d.update(schema.syncPeers).set({ claimed_local_actor: 1 }).where(eq(schema.syncPeers.actor_id, primaryActorId)).run();
10595
- d.update(schema.actors).set({
10596
- status: "merged",
10597
- merged_into_actor_id: primaryActorId,
10598
- updated_at: now
10599
- }).where(eq(schema.actors.actor_id, secondaryActorId)).run();
10794
+ finishActorMerge(store, primaryActorId, secondaryActorId, now);
10600
10795
  return { mergedCount: Number(peerUpdate.changes ?? 0) };
10601
10796
  }).immediate();
10602
10797
  if ("error" in result) return c.json({ error: result.error }, result.status);
@@ -10615,16 +10810,10 @@ function syncRoutes(getStore, getSyncRuntimeStatus, options = {}) {
10615
10810
  if (actorId === store.actorId) return c.json({ error: "cannot deactivate this device's own local actor" }, 409);
10616
10811
  const releaseActorMutations = await claimRecipientPolicyActorMutations(store.db, [actorId]);
10617
10812
  queueMicrotask(releaseActorMutations);
10618
- const d = drizzle(store.db, { schema });
10619
- const actor = d.select().from(schema.actors).where(eq(schema.actors.actor_id, actorId)).get();
10813
+ const actor = drizzle(store.db, { schema }).select().from(schema.actors).where(eq(schema.actors.actor_id, actorId)).get();
10620
10814
  if (!actor) return c.json({ error: "actor not found" }, 404);
10621
10815
  if (actor.status !== "active") return c.json({ error: "actor not active" }, 409);
10622
- const now = (/* @__PURE__ */ new Date()).toISOString();
10623
- d.update(schema.actors).set({
10624
- status: "deactivated",
10625
- updated_at: now
10626
- }).where(eq(schema.actors.actor_id, actorId)).run();
10627
- d.update(schema.syncPeers).set({ actor_id: null }).where(eq(schema.syncPeers.actor_id, actorId)).run();
10816
+ deactivateActorAndWakePolicies(store, actorId, (/* @__PURE__ */ new Date()).toISOString());
10628
10817
  return c.json({ ok: true });
10629
10818
  });
10630
10819
  app.post("/api/sync/legacy-devices/claim", async (c) => {