@gitroll/tacitt-cli 0.6.8 → 0.6.10

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.
Files changed (46) hide show
  1. package/README.md +1 -1
  2. package/dist/{agent-setup-YlFIJz-h.mjs → agent-setup-LPd8WEsN.mjs} +2 -2
  3. package/dist/agent-setup-worker.mjs +5 -5
  4. package/dist/cli.mjs +19 -10
  5. package/dist/codex-title-worker.mjs +1 -1
  6. package/dist/configured-preview-worker.mjs +5 -5
  7. package/dist/configured-session-worker.mjs +5 -5
  8. package/dist/{frame-Dj5Zs6UW.mjs → frame-ByT1xayJ.mjs} +1 -1
  9. package/dist/{heap-DsE74_-n.mjs → heap-CM298Yu0.mjs} +8 -6
  10. package/dist/inspection-worker.mjs +4 -4
  11. package/dist/{job-CeSf9UCC.mjs → job-BHIwElNF.mjs} +5 -5
  12. package/dist/{locate-BWkblijS.mjs → locate-Cwv6WH8n.mjs} +25 -2
  13. package/dist/{model-f_QnDyyb.mjs → model-zvn5GUJ9.mjs} +1 -1
  14. package/dist/operation-executor.mjs +3 -3
  15. package/dist/preview-worker.mjs +1 -1
  16. package/dist/privacy-validator.mjs +1 -1
  17. package/dist/{query-CaBXQlpF.mjs → query-By41gaKm.mjs} +585 -250
  18. package/dist/{run-RA_8zZsL.mjs → run-ZQx7p4Rw.mjs} +4 -4
  19. package/dist/{src-p_uJNjV3.mjs → src-3jvMvSQn.mjs} +3 -3
  20. package/dist/{src-d7n58sbH.mjs → src-BqtdFVq7.mjs} +683 -341
  21. package/dist/{src-BBjnljUs.mjs → src-D_DfY--2.mjs} +4 -4
  22. package/dist/store-database-worker.mjs +1 -1
  23. package/dist/web/assets/index-B14e6_-z.js +174 -0
  24. package/dist/web/index.html +1 -1
  25. package/package.json +1 -1
  26. package/skills/tacitt/INDEX.md +6 -6
  27. package/skills/tacitt/SKILL.md +4 -4
  28. package/skills/tacitt/references/INDEX.md +3 -3
  29. package/skills/tacitt/references/classification.md +12 -2
  30. package/skills/tacitt/references/commands.md +60 -13
  31. package/skills/tacitt/references/journey.md +10 -2
  32. package/skills/tacitt/references/parallel-review.md +21 -0
  33. package/skills/tacitt/references/send.md +29 -8
  34. package/skills/tacitt/scripts/INDEX.md +39 -36
  35. package/skills/tacitt/scripts/journey-classification-validation.d.mts +13 -0
  36. package/skills/tacitt/scripts/journey-classification-validation.mjs +38 -0
  37. package/skills/tacitt/scripts/journey-scan-timing.d.mts +15 -0
  38. package/skills/tacitt/scripts/journey-scan-timing.mjs +153 -1
  39. package/skills/tacitt/scripts/journey-state.d.mts +6 -0
  40. package/skills/tacitt/scripts/journey-state.mjs +24 -1
  41. package/skills/tacitt/scripts/journey.d.mts +5 -0
  42. package/skills/tacitt/scripts/journey.mjs +48 -3
  43. package/skills/tacitt/scripts/privacy-validator.mjs +11 -1
  44. package/skills/tacitt/scripts/review.mjs +294 -38
  45. package/LICENSE +0 -11
  46. package/dist/web/assets/index-B6kJ6XXF.js +0 -174
@@ -1,10 +1,10 @@
1
- import { $ as createCleanup, B as parallelismLimits, C as readOriginalSearchFields, F as SessionWorkerError, G as processJourneyMetadataFields, H as privateJourneyDirectory, I as sessionWorkerFailure, K as staticMaskCategory, O as captureLogicalJourneySources, P as createSharedSessionPool, Q as removeNamedTemporarySpools, R as defaultParallelism, T as createCollectionArtifacts, U as readLogicalJourneyFinalMasks, W as readConfiguredOutputInfo, Z as createTemporary, _ as processJourneyMetadata, a as scanReviewFields, at as windowsOwnerOnly, c as readSessionScanRecords, g as readCapturedOriginalRows, i as buildWorkScanBatches, it as restrictToOwner, j as runConfiguredSessionJob, k as capturedJourneyScope, l as scanPreparedSession, m as openConfiguredUpload, n as verifyPreparedSessionSources, nt as makePrivateDirectory, o as createWorkPieces, ot as isWindowsAclFailure, p as configuredSessionUploads, q as readWorkspacePair, r as prepareSessionScans, rt as openPrivateFile, s as readSessionScanFields, st as windowsAclCodes, t as preparedScanConfiguration, v as readStaticSessionMasks, w as readConfiguredSearchFields, y as staticSessionJobs, z as gitSeedsProvider } from "./src-p_uJNjV3.mjs";
2
- import { A as createExecutionPort, C as saveRemoteState, D as createPackagingProgressPort, E as exportDelivery, F as loadDeliveryResults, L as inBackgroundStoreLane, M as withStateMutex, N as createDeliveryService, O as createStore, P as preflight, R as isBackgroundStatusPoll, S as readRemoteState, T as collectRemoteWorkers, _ as staticBaseProduction, a as writeJourneyOutput, b as workApproval, c as mergeHideIndexTokens, f as writeJourneyBase, g as staticBaseInputs, h as staticSessions, l as packRenderHideIndex, m as saveStaticSession, o as createHideIndexTokens, p as readStaticResult, s as hideIndexTexts, t as journeyOutputJob, u as journeyBaseJob, v as readSelectedCaptures, w as journeyDirectory, x as isRemoteImport, y as prepareSourcePlan, z as retrySharingViolation } from "./job-CeSf9UCC.mjs";
3
- import { $t as safeField, Bt as sessionPeriodSchema, Dn as createStaticProcessor, Fn as entityDictionaryVersion, Ft as termTypeInfo, Gt as ownerProfileSchema, Kt as passportAddressSchema, Ot as privacyTermKey, Qt as safeCode, Sn as currentDetectorVersion, T as bLiteralTerms, Un as frameRecords, Wt as mergeOwnerProfile, Xt as ApplicationError, Zt as UnfinishedJourneyError, an as observeSource, ar as admitCustomTerm, c as textDigest, ir as createEntityMatcher, nn as reviewFamilies, or as admitGitName, qt as profileHasChoices, tn as familySelected, ur as ProcessingError, zt as filterInventoryByPeriod } from "./locate-BWkblijS.mjs";
4
- import { a as mainTextChunks, c as previewInWorker, i as nearestSortingRepository, l as createNativeAdapters, n as createProjectEvidenceCollector, o as mainTextFields, r as sortingRepositoryId, s as textChunks, t as createHarnesses, u as createGitNameSeedProvider } from "./src-BBjnljUs.mjs";
5
- import { $ as settingsBatchSchema, A as deviceAuthorizationSchema, Ar as staticReviewDigest, B as inspectionStartSchema, Br as sendResolutionSchema, Bt as classificationWorks, Ct as sendPrepareSchema, Dt as currentJourneyAdapters, Fn as bTermMatcher, Fr as agentSourcesSchema, Gn as retireWorkDescription, Gr as isReadingComplete, Hn as journeyConfirmedHides, Hr as hasCompleteWorkDescription, Ht as newJourneyWork, Ir as journeySendSchema, Jn as workCorrectionBinding, Kn as workDecisionDigest, Kr as isSelectedWork, Lr as sendArtifactSchema, M as profileUrlMatchesIdentity, N as readAccountAvatarUrl, O as createAccount, Ot as obsoleteJourney, P as storedCredentialSchema, Pr as agentSourceSchema, Pt as applyClassification, R as createApplication, Rr as sendReceiptSchema, St as sendChoiceSchema, Tr as privacyDecisionDigest, Tt as sendStartSchema, Ur as isClassifying, V as validateInspectionBatch, Vr as sendSettingsSchema, Vt as retireScanGeneration, W as inspectionQuerySchema, Wr as isIncludedWork, Yr as withReadingProgress, Yt as journeySearchResultSchema, Zt as searchScopeDigest, _ as changeJourney, _r as emptyPreparationFacts, _t as emptyTermsRevision, ai as selectionSchema, ar as scanPreparationDigest, bt as replaceSensitiveTermsSchema, cr as scanGitSeedsResource, et as settingsQuerySchema, ft as manualUploadChoiceSchema, h as hideScopeDigest, ht as manualUploadStartSchema, ii as selectionBatchSchema, j as deviceProfileScopeSchema, k as accountProfileSchema, kr as hasCurrentStaticReview, lr as verifiedScanGitSeeds, lt as validateFailureLocation, m as activeJourneyTerms, mt as manualUploadSchema, ni as commandSchema, or as scanSessionResourceSchema, pt as manualUploadPrepareSchema, ri as packageCommandSchema, rt as appendSensitiveTerms, si as producerVersion, sr as scanGitSeedsId, st as artifactFailureDetailSchema, ui as recommendParallelism, ur as scanExclusionResourceSchema, ut as manualUploadActionSchema, wr as assertJourneyPrivacyReady, wt as sendRequestSchema, xt as sendActionSchema, yt as normalizeSensitiveTerms, z as inspectionCancelSchema, zn as confirmedHideKinds, zr as sendReplaceResultSchema, zt as classificationPartition } from "./src-d7n58sbH.mjs";
6
- import { _t as isCoordinationBusy } from "./heap-DsE74_-n.mjs";
7
- import { t as createAgentSetupPort } from "./agent-setup-YlFIJz-h.mjs";
1
+ import { $ as createCleanup, B as parallelismLimits, C as readOriginalSearchFields, F as SessionWorkerError, G as processJourneyMetadataFields, H as privateJourneyDirectory, I as sessionWorkerFailure, K as staticMaskCategory, O as captureLogicalJourneySources, P as createSharedSessionPool, Q as removeNamedTemporarySpools, R as defaultParallelism, T as createCollectionArtifacts, U as readLogicalJourneyFinalMasks, W as readConfiguredOutputInfo, Z as createTemporary, _ as processJourneyMetadata, a as scanReviewFields, at as windowsOwnerOnly, c as readSessionScanRecords, g as readCapturedOriginalRows, i as buildWorkScanBatches, it as restrictToOwner, j as runConfiguredSessionJob, k as capturedJourneyScope, l as scanPreparedSession, m as openConfiguredUpload, n as verifyPreparedSessionSources, nt as makePrivateDirectory, o as createWorkPieces, ot as isWindowsAclFailure, p as configuredSessionUploads, q as readWorkspacePair, r as prepareSessionScans, rt as openPrivateFile, s as readSessionScanFields, st as windowsAclCodes, t as preparedScanConfiguration, v as readStaticSessionMasks, w as readConfiguredSearchFields, y as staticSessionJobs, z as gitSeedsProvider } from "./src-3jvMvSQn.mjs";
2
+ import { A as createExecutionPort, C as saveRemoteState, D as createPackagingProgressPort, E as exportDelivery, F as loadDeliveryResults, L as inBackgroundStoreLane, M as withStateMutex, N as createDeliveryService, O as createStore, P as preflight, R as isBackgroundStatusPoll, S as readRemoteState, T as collectRemoteWorkers, _ as staticBaseProduction, a as writeJourneyOutput, b as workApproval, c as mergeHideIndexTokens, f as writeJourneyBase, g as staticBaseInputs, h as staticSessions, l as packRenderHideIndex, m as saveStaticSession, o as createHideIndexTokens, p as readStaticResult, s as hideIndexTexts, t as journeyOutputJob, u as journeyBaseJob, v as readSelectedCaptures, w as journeyDirectory, x as isRemoteImport, y as prepareSourcePlan, z as retrySharingViolation } from "./job-BHIwElNF.mjs";
3
+ import { $t as safeField, Bt as sessionPeriodSchema, Dn as createStaticProcessor, Fn as entityDictionaryVersion, Ft as termTypeInfo, Gt as ownerProfileSchema, Kt as passportAddressSchema, Ot as privacyTermKey, Qt as safeCode, Sn as currentDetectorVersion, T as bLiteralTerms, Un as frameRecords, Wt as mergeOwnerProfile, Xt as ApplicationError, Zt as UnfinishedJourneyError, an as observeSource, ar as admitCustomTerm, c as textDigest, ir as createEntityMatcher, nn as reviewFamilies, or as admitGitName, qt as profileHasChoices, tn as familySelected, ur as ProcessingError, zt as filterInventoryByPeriod } from "./locate-Cwv6WH8n.mjs";
4
+ import { a as mainTextChunks, c as previewInWorker, i as nearestSortingRepository, l as createNativeAdapters, n as createProjectEvidenceCollector, o as mainTextFields, r as sortingRepositoryId, s as textChunks, t as createHarnesses, u as createGitNameSeedProvider } from "./src-D_DfY--2.mjs";
5
+ import { $r as isClassifying, $t as searchScopeDigest, Ar as displayedScopeDigest, At as obsoleteJourney, B as createApplication, Br as staticReviewDigest, Bt as classificationPartition, Ct as sendActionSchema, Dt as sendStartSchema, Et as sendRequestSchema, F as readAccountAvatarUrl, Gr as agentSourcesSchema, H as inspectionStartSchema, Ht as applyClassification, I as storedCredentialSchema, Jn as workDecisionDigest, Jr as sendReceiptSchema, K as inspectionQuerySchema, Kr as journeySendSchema, Ln as bTermMatcher, M as deviceAuthorizationSchema, N as deviceProfileScopeSchema, Nr as assertJourneyPrivacyReady, O as createAccount, P as profileUrlMatchesIdentity, Pr as privacyDecisionDigest, Qr as hasCompleteWorkDescription, St as replaceSensitiveTermsSchema, Tt as sendPrepareSchema, U as validateInspectionBatch, Ut as retireScanGeneration, V as inspectionCancelSchema, Vn as confirmedHideKinds, Vt as classificationWorks, Wn as journeyConfirmedHides, Wr as agentSourceSchema, Wt as newJourneyWork, Xn as workCorrectionBinding, Xr as sendResolutionSchema, Yr as sendReplaceResultSchema, Zr as sendSettingsSchema, Zt as journeySearchResultSchema, _ as changeJourney, _t as manualUploadStartSchema, ai as withReadingProgress, at as appendSensitiveTerms, ci as selectionBatchSchema, cr as scanSessionResourceSchema, di as producerVersion, dr as verifiedScanGitSeeds, dt as validateFailureLocation, ei as isIncludedWork, fr as scanExclusionResourceSchema, ft as manualUploadActionSchema, gt as manualUploadSchema, h as hideScopeDigest, ht as manualUploadPrepareSchema, j as accountProfileSchema, jr as requestedScope, k as renew, kt as currentJourneyAdapters, li as selectionSchema, lr as scanGitSeedsId, lt as artifactFailureDetailSchema, m as activeJourneyTerms, mi as recommendParallelism, mt as manualUploadChoiceSchema, ni as isSelectedWork, nt as settingsQuerySchema, oi as commandSchema, qn as retireWorkDescription, qr as sendArtifactSchema, si as packageCommandSchema, sr as scanPreparationDigest, ti as isReadingComplete, tt as settingsBatchSchema, ur as scanGitSeedsResource, wt as sendChoiceSchema, xt as normalizeSensitiveTerms, yr as emptyPreparationFacts, yt as emptyTermsRevision, zr as hasCurrentStaticReview } from "./src-BqtdFVq7.mjs";
6
+ import { _t as isCoordinationBusy } from "./heap-CM298Yu0.mjs";
7
+ import { t as createAgentSetupPort } from "./agent-setup-LPd8WEsN.mjs";
8
8
  import { t as workerMessageSchema } from "./messages-D3m3bhva.mjs";
9
9
  import { constants, write } from "node:fs";
10
10
  import { z } from "zod";
@@ -2603,18 +2603,24 @@ function createJourneyPreparation(options) {
2603
2603
  const scanWorks = scanRequested && state.scope.status === "confirmed" ? state.works : [];
2604
2604
  if (scanWorks.length) run = await runWorkers(options.workers);
2605
2605
  const frozen = state;
2606
- const blockWork = (work, error) => changeJourney(storage, journeyId, "snapshot", (before) => {
2607
- if (before.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
2608
- if (before.phase === "sending" || before.phase === "completed") return before;
2609
- return withReadingProgress({
2610
- ...before,
2611
- works: before.works.map((item) => item.id === work.id && item.inputRevision === work.inputRevision && !item.blockedCodes.includes("PRIVACY_CHANGE_PENDING") ? {
2612
- ...item,
2613
- status: "blocked",
2614
- blockedCodes: blockedCodes(error, safeCode(error))
2615
- } : item)
2616
- });
2617
- }, 100);
2606
+ const blockedForRetry = new Set(state.works.filter((work) => work.status === "blocked").map((work) => `${work.id}:${work.inputRevision}`));
2607
+ const deferredForHide = /* @__PURE__ */ new Set();
2608
+ const blockWork = async (work, error) => {
2609
+ const blocked = await changeJourney(storage, journeyId, "snapshot", (before) => {
2610
+ if (before.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
2611
+ if (before.phase === "sending" || before.phase === "completed") return before;
2612
+ return withReadingProgress({
2613
+ ...before,
2614
+ works: before.works.map((item) => item.id === work.id && item.inputRevision === work.inputRevision && !item.blockedCodes.includes("PRIVACY_CHANGE_PENDING") ? {
2615
+ ...item,
2616
+ status: "blocked",
2617
+ blockedCodes: blockedCodes(error, safeCode(error))
2618
+ } : item)
2619
+ });
2620
+ }, 100);
2621
+ if (blocked.works.some((item) => item.id === work.id && item.inputRevision === work.inputRevision && item.status === "blocked")) blockedForRetry.add(`${work.id}:${work.inputRevision}`);
2622
+ return blocked;
2623
+ };
2618
2624
  const retryLimit = options.retryLimit ?? workRetryDelaysMs.length;
2619
2625
  const generation = (work) => `${work.id}:${work.inputRevision}`;
2620
2626
  let serialAdmission = Promise.resolve();
@@ -2638,11 +2644,14 @@ function createJourneyPreparation(options) {
2638
2644
  await (options.retryDelay ?? waitForWorkRetry)(delay, abort.signal);
2639
2645
  abort.signal.throwIfAborted();
2640
2646
  let resumed = false;
2641
- await changeJourney(storage, journeyId, "snapshot", (before) => {
2647
+ let deferred = false;
2648
+ const committed = await changeJourney(storage, journeyId, "snapshot", (before) => {
2642
2649
  resumed = false;
2650
+ deferred = false;
2643
2651
  const target = before.works.find((item) => item.id === work.id);
2644
2652
  if (before.phase === "sending" || before.phase === "completed" || before.phase === "paused" || !target || !isSelectedWork(target) || target.inputRevision !== work.inputRevision || target.status !== "blocked" || target.blockedCodes.includes("PRIVACY_CHANGE_PENDING")) return before;
2645
2653
  resumed = !before.pendingHideId;
2654
+ deferred = !resumed;
2646
2655
  return withReadingProgress({
2647
2656
  ...before,
2648
2657
  privacy: {
@@ -2656,6 +2665,7 @@ function createJourneyPreparation(options) {
2656
2665
  } : item)
2657
2666
  });
2658
2667
  }, 100);
2668
+ if (deferred && committed.pendingHideId && committed.works.some((item) => generation(item) === generation(work) && item.status === "pending")) deferredForHide.add(generation(work));
2659
2669
  return resumed;
2660
2670
  };
2661
2671
  const readRetries = /* @__PURE__ */ new Map();
@@ -2693,6 +2703,7 @@ function createJourneyPreparation(options) {
2693
2703
  "reading",
2694
2704
  "review"
2695
2705
  ].includes(work.status)) return false;
2706
+ blockedForRetry.delete(generation(work));
2696
2707
  prepared.add(generation(work));
2697
2708
  await prepareClassificationMetadata(storage, latest, work, options.processing, abort.signal, plan);
2698
2709
  let failures = collectPreparationFailures(plan.approval.inventory, work.rootSessionIds, {
@@ -2802,7 +2813,15 @@ function createJourneyPreparation(options) {
2802
2813
  abort.abort(error);
2803
2814
  };
2804
2815
  const statics = [];
2816
+ const priorStatics = /* @__PURE__ */ new Map();
2805
2817
  const reads = [];
2818
+ const activeReads = /* @__PURE__ */ new Set();
2819
+ const activeStatics = /* @__PURE__ */ new Set();
2820
+ let workSettled = Promise.withResolvers();
2821
+ const notifySettled = () => {
2822
+ workSettled.resolve();
2823
+ workSettled = Promise.withResolvers();
2824
+ };
2806
2825
  const pass = async (works) => {
2807
2826
  let submittedWorks = 0;
2808
2827
  const submitted = Promise.withResolvers();
@@ -2826,6 +2845,7 @@ function createJourneyPreparation(options) {
2826
2845
  releaseAdmission.set(planned.id, () => released.resolve());
2827
2846
  const onSubmitted = submission();
2828
2847
  const read = async () => {
2848
+ await priorStatics.get(planned.id);
2829
2849
  if (!run.pool) await previous;
2830
2850
  if (fatal) {
2831
2851
  onSubmitted();
@@ -2840,25 +2860,45 @@ function createJourneyPreparation(options) {
2840
2860
  onSubmitted();
2841
2861
  released.resolve();
2842
2862
  }
2843
- statics.push(retryContention(() => staticWork(planned, passSubmitted), (error) => options.onFailure(error), abort.signal).catch(stop));
2863
+ activeStatics.add(planned.id);
2864
+ const staticResult = retryContention(() => staticWork(planned, passSubmitted), (error) => options.onFailure(error), abort.signal).catch(stop).finally(() => {
2865
+ activeStatics.delete(planned.id);
2866
+ notifySettled();
2867
+ });
2868
+ priorStatics.set(planned.id, staticResult);
2869
+ statics.push(staticResult);
2844
2870
  };
2845
- reads.push(read());
2871
+ activeReads.add(planned.id);
2872
+ reads.push(read().finally(() => {
2873
+ activeReads.delete(planned.id);
2874
+ notifySettled();
2875
+ }));
2846
2876
  }
2847
2877
  await Promise.all(admissions);
2848
2878
  };
2849
- /** Selected Works this run has not prepared yet, waiting while grouping continues (E2). */
2879
+ /** Admit newly grouped and explicitly retried Works until both read and static work drain. */
2850
2880
  const later = async () => {
2851
2881
  for (;;) {
2852
2882
  abort.signal.throwIfAborted();
2883
+ const settled = workSettled.promise;
2853
2884
  const latest = await storage.current(journeyId);
2854
2885
  if (!latest || latest.phase === "paused" || latest.scope.status !== "confirmed") return [];
2886
+ for (const work of latest.works) if (work.status === "blocked") blockedForRetry.add(generation(work));
2855
2887
  const ready = latest.works.filter((work) => isSelectedWork(work) && [
2856
2888
  "pending",
2857
2889
  "reading",
2858
2890
  "review"
2859
- ].includes(work.status) && !prepared.has(`${work.id}:${work.inputRevision}`));
2860
- if (ready.length || !isClassifying(latest)) return ready;
2861
- await setTimeout$1(laterWorksPollMs, void 0, { signal: abort.signal }).catch(() => abort.signal.throwIfAborted());
2891
+ ].includes(work.status) && !activeReads.has(work.id) && !activeStatics.has(work.id) && !deferredForHide.has(generation(work)) && (!prepared.has(generation(work)) && !hasCurrentStaticReview(latest, work) || work.status === "pending" && blockedForRetry.has(generation(work))));
2892
+ if (ready.length || !isClassifying(latest) && !activeReads.size && !activeStatics.size) return ready;
2893
+ const polling = new AbortController();
2894
+ try {
2895
+ await Promise.race([settled, setTimeout$1(laterWorksPollMs, void 0, { signal: AbortSignal.any([abort.signal, polling.signal]) })]);
2896
+ } catch (error) {
2897
+ abort.signal.throwIfAborted();
2898
+ throw error;
2899
+ } finally {
2900
+ polling.abort();
2901
+ }
2862
2902
  }
2863
2903
  };
2864
2904
  try {
@@ -2971,7 +3011,16 @@ async function packageBase(storage, state, work, processing, directory, signal,
2971
3011
  if (after.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
2972
3012
  assertJourneyPrivacyReady(after, true);
2973
3013
  }
2974
- await assertCurrent();
3014
+ try {
3015
+ await assertCurrent();
3016
+ } catch (error) {
3017
+ await Promise.all([
3018
+ base.primary,
3019
+ base.companion,
3020
+ base.maskLedger
3021
+ ].map((file) => rm(file.path, { force: true })));
3022
+ throw error;
3023
+ }
2975
3024
  try {
2976
3025
  await storage.saveResource(base);
2977
3026
  return base;
@@ -3533,7 +3582,7 @@ function assertScope(journey, send) {
3533
3582
  if (journey.pendingHideId) throw new ApplicationError("SCOPE_NOT_READY");
3534
3583
  if (journey.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
3535
3584
  if (scopeDigest(journey) !== send.scopeDigest) throw new ApplicationError("SEND_SCOPE_INVALIDATED");
3536
- assertJourneyPrivacyReady(journey, true);
3585
+ assertJourneyPrivacyReady(journey, true, { publicChoices: send.status !== "completed" });
3537
3586
  for (const item of send.items) {
3538
3587
  const work = journey.works.find((value) => value.id === item.workId);
3539
3588
  if (!work || work.status !== "ready" || !hasCompleteWorkDescription(work) || !work.accessConfirmed || work.access !== item.access || work.artifact?.revision !== work.revision || work.artifact.sha256 !== item.artifact.sha256 || work.artifact.archiveBytes !== item.artifact.byteLength || work.artifact.bundleId !== item.artifact.bundleId) throw new ApplicationError("SCOPE_NOT_READY");
@@ -3818,31 +3867,32 @@ function createJourneyRenderer(options) {
3818
3867
  async function renderScope(journeyId, works, signal) {
3819
3868
  const workers = options.workers;
3820
3869
  const size = workers ? workers.parallelism ?? renderWorkerCount() : 0;
3821
- if (!workers || works < 1 || size < 1) {
3822
- const retry = renderRetry(signal, Math.max(1, works));
3823
- return {
3824
- render: (workId) => render(journeyId, workId, signal, { retry }),
3825
- close: async () => void 0
3826
- };
3827
- }
3828
- const retry = renderRetry(signal, Math.min(works, size + 1));
3829
- const store = retry.store(storage);
3870
+ if (!workers || works < 1 || size < 1) return {
3871
+ render: (workId) => render(journeyId, workId, signal, { retry: renderRetry(signal, Math.max(1, Math.min(16, works))) }),
3872
+ close: async () => void 0
3873
+ };
3830
3874
  const pool = createRenderWorkers(workers, size);
3831
- const session = batchedRenderSession(store, journeyId);
3875
+ const session = batchedRenderSession(storage, journeyId);
3832
3876
  let plan;
3833
- const readPlan = () => plan ??= readJourneyPlan(store, journeyId).catch((error) => {
3877
+ const readPlan = () => plan ??= readJourneyPlan(storage, journeyId).catch((error) => {
3834
3878
  plan = void 0;
3835
3879
  throw error;
3836
3880
  });
3837
3881
  return {
3838
3882
  concurrency: size + 1,
3839
- render: (workId) => render(journeyId, workId, signal, {
3840
- write: pool.write,
3841
- session,
3842
- plan: readPlan,
3843
- writeBase: pool.writeBase,
3844
- retry
3845
- }),
3883
+ render: (workId) => {
3884
+ const retry = renderRetry(signal, Math.min(16, works, size + 1));
3885
+ return render(journeyId, workId, signal, {
3886
+ write: pool.write,
3887
+ session: {
3888
+ current: () => retry.run(() => session.current()),
3889
+ commit: (change) => retry.run(() => session.commit(change))
3890
+ },
3891
+ plan: () => retry.run(readPlan),
3892
+ writeBase: pool.writeBase,
3893
+ retry
3894
+ });
3895
+ },
3846
3896
  close: pool.close
3847
3897
  };
3848
3898
  }
@@ -4642,7 +4692,7 @@ async function resolveChunkSessions(storage, state, works, signal) {
4642
4692
  * read and applied by the store against the current state, so scan writes in between do not
4643
4693
  * refuse it (BL-216).
4644
4694
  */
4645
- function createClassificationProposal(storage) {
4695
+ function createClassificationProposal(storage, accountAccess) {
4646
4696
  return async (input, signal) => {
4647
4697
  signal.throwIfAborted();
4648
4698
  const state = await storage.current(input.journeyId);
@@ -4736,9 +4786,14 @@ function createClassificationProposal(storage) {
4736
4786
  };
4737
4787
  const { works } = classificationWorks(state, prepared);
4738
4788
  const chunkSessions = await resolveChunkSessions(storage, state, works, signal);
4789
+ const access = late ? await accountAccess?.({
4790
+ ...state,
4791
+ works
4792
+ }, signal) : void 0;
4739
4793
  return storage.commitClassification({
4740
4794
  ...prepared,
4741
- chunkSessions
4795
+ chunkSessions,
4796
+ ...access ? { accountAccess: Object.fromEntries(access) } : {}
4742
4797
  });
4743
4798
  };
4744
4799
  }
@@ -5084,11 +5139,11 @@ function createJourneyClassification(storage, _processing) {
5084
5139
  //#endregion
5085
5140
  //#region apps/cli/src/journey/works/index.ts
5086
5141
  /** The journey local port's project catalog and project-type suggestions. */
5087
- function createJourneyWorks(storage, processing, stateDir) {
5142
+ function createJourneyWorks(storage, processing, stateDir, accountAccess) {
5088
5143
  return {
5089
5144
  classificationSessions: createClassificationSessions(storage, processing, stateDir),
5090
5145
  classificationRead: createClassificationReader(storage, stateDir),
5091
- classificationSubmit: createClassificationProposal(storage),
5146
+ classificationSubmit: createClassificationProposal(storage, accountAccess),
5092
5147
  catalog: createJourneyCatalog(storage, processing, stateDir),
5093
5148
  classify: createJourneyClassification(storage, processing)
5094
5149
  };
@@ -5182,6 +5237,46 @@ function protectedSourceText(field, hidden) {
5182
5237
  }
5183
5238
  //#endregion
5184
5239
  //#region apps/cli/src/journey/send-package.ts
5240
+ /** Explicit admission refusals must never become automatic packaging retries. */
5241
+ const packagingRefusalCodes = [
5242
+ "ARTIFACT_INVALID_APPROVAL",
5243
+ "ARTIFACT_IDENTITY_MISMATCH",
5244
+ "CONFIRMATION_REQUIRED",
5245
+ "INCOMPLETE_REVIEW",
5246
+ "PRIVACY_CHANGE_PENDING",
5247
+ "PRIVACY_UNCERTAIN",
5248
+ "REVISION_CONFLICT",
5249
+ "SCOPE_NOT_READY",
5250
+ "JOURNEY_OBSOLETE",
5251
+ "UNKNOWN_ITEM",
5252
+ "UNKNOWN_WORKSPACE",
5253
+ "OPERATION_CANCELED"
5254
+ ];
5255
+ /**
5256
+ * Deterministic failures a later attempt cannot clear: the refusals above, the source refusals
5257
+ * preparation isolates per root, size limits, a missing source and a full disk. Every other code,
5258
+ * including Windows sharing or antivirus locks, retries until the saved request expires.
5259
+ */
5260
+ const finalPackagingCodes = [
5261
+ ...packagingRefusalCodes,
5262
+ ...rootScanRefusals,
5263
+ "ARTIFACT_ARCHIVE_LIMIT",
5264
+ "ARTIFACT_ENTRY_LIMIT",
5265
+ "ARTIFACT_FILESYSTEM_LIMIT",
5266
+ "ARTIFACT_INDEX_LIMIT",
5267
+ "ARTIFACT_METADATA_LIMIT",
5268
+ "ARTIFACT_PART_LIMIT",
5269
+ "ARTIFACT_SOURCE_MISSING",
5270
+ "ARTIFACT_DISK_FULL",
5271
+ "ARTIFACT_INVALID_APPROVAL",
5272
+ "ARTIFACT_INVALID_ARCHIVE",
5273
+ "ARTIFACT_INVALID_MANIFEST",
5274
+ "ARTIFACT_OUTPUT_UNSAFE",
5275
+ "ARTIFACT_PROCESSING_MISMATCH",
5276
+ "ARTIFACT_QUOTA_EXCEEDED",
5277
+ "ARTIFACT_SOURCE_UNSAFE",
5278
+ "ARTIFACT_UNSAFE_REMOTE"
5279
+ ];
5185
5280
  const packaged = (work) => work.status === "ready" && work.artifact?.revision === work.revision;
5186
5281
  /** One host-lifetime packaging batch per advance; persisted artifacts are its progress. */
5187
5282
  function createSendPackager(ports) {
@@ -5200,13 +5295,16 @@ function createSendPackager(ports) {
5200
5295
  if (!current) throw new ApplicationError("UNKNOWN_ITEM");
5201
5296
  }
5202
5297
  const ready = (works, id) => works.some((work) => work.id === id && packaged(work));
5203
- const batch = workIds.filter((id) => !ready(current.works, id)).slice(0, 16);
5298
+ const batch = workIds.filter((id) => !ready(current.works, id));
5204
5299
  if (!batch.length) return true;
5205
5300
  signal.throwIfAborted();
5206
5301
  const result = await ports.render({
5207
5302
  journeyId,
5208
5303
  workIds: batch
5209
5304
  }, signal);
5305
+ const failures = result.renders?.filter((item) => item.status === "failed") ?? [];
5306
+ const failure = failures.find((item) => packagingRefusalCodes.includes(item.code)) ?? failures.find((item) => finalPackagingCodes.includes(item.code)) ?? failures[0];
5307
+ if (failure) throw new ApplicationError(failure.code);
5210
5308
  if (batch.some((id) => !ready(result.works, id))) throw new Error("PACKAGING_FAILED");
5211
5309
  return false;
5212
5310
  };
@@ -5579,6 +5677,19 @@ async function classifyExit(child, startup, exited, busy) {
5579
5677
  }
5580
5678
  //#endregion
5581
5679
  //#region apps/cli/src/send-host.ts
5680
+ /**
5681
+ * The detached focused host (ADR 0227) drives a Send only for the store's current journey, the
5682
+ * check its timer makes, and only while `liveHost` verifies its process identity and URL.
5683
+ * Anything the CLI cannot confirm returns false, so the CLI keeps stepping as before.
5684
+ */
5685
+ async function focusedHostAlive(stateDir, journeyId, currentJourneyId) {
5686
+ try {
5687
+ if (await currentJourneyId() !== journeyId) return false;
5688
+ return (await liveHost(join(stateDir, "serve"), journeyId)).status === "alive";
5689
+ } catch {
5690
+ return false;
5691
+ }
5692
+ }
5582
5693
  const recordSchema = z.object({
5583
5694
  hostId: z.uuid(),
5584
5695
  pid: z.number().int().positive()
@@ -6042,49 +6153,72 @@ var SessionContentConflict = class extends ApplicationError {
6042
6153
  this.contentConflict = contentConflict;
6043
6154
  }
6044
6155
  };
6045
- /** Check the entire pending Send before admitting any session mutation. */
6156
+ /** Collect safe conflicts across the entire pending Send before any session mutation. */
6046
6157
  async function sessionAccessBlocked(dependencies, journeyId, items, inventory, signal) {
6047
6158
  const sessions = dependencies.artifacts.sessionFacts ?? dependencies.artifacts.sessions;
6048
- if (!sessions) return false;
6159
+ if (!sessions) return [];
6160
+ const conflicts = /* @__PURE__ */ new Map();
6161
+ const accountAccesses = /* @__PURE__ */ new Map();
6162
+ function observe(item, access) {
6163
+ const values = accountAccesses.get(item.workId) ?? /* @__PURE__ */ new Set();
6164
+ values.add(access);
6165
+ accountAccesses.set(item.workId, values);
6166
+ }
6167
+ function record(item, sessionKey, rule, account) {
6168
+ if (account) observe(item, account.access);
6169
+ const conflict = {
6170
+ workId: item.workId,
6171
+ sessionKey,
6172
+ requestedAccess: item.access,
6173
+ accountWorkId: account?.logicalWorkId ?? null,
6174
+ accountAccess: account?.access ?? null,
6175
+ rule
6176
+ };
6177
+ conflicts.set(JSON.stringify(conflict), conflict);
6178
+ }
6049
6179
  const requestedAccess = /* @__PURE__ */ new Map();
6050
6180
  const requestedDigests = /* @__PURE__ */ new Map();
6051
6181
  const contentAccess = /* @__PURE__ */ new Map();
6052
- const claimContent = (key, sha256, access) => {
6053
- const identity = JSON.stringify([key, sha256]);
6054
- const previous = contentAccess.get(identity);
6055
- if (previous !== void 0 && previous !== access) return true;
6056
- contentAccess.set(identity, access);
6057
- return false;
6058
- };
6182
+ function claim(claims, identity, key, item, rule) {
6183
+ const previous = claims.get(identity) ?? [];
6184
+ for (const other of previous) {
6185
+ if (other.access === item.access) continue;
6186
+ record(item, key, rule);
6187
+ record(other, key, rule);
6188
+ }
6189
+ if (!previous.some((other) => other.workId === item.workId)) previous.push(item);
6190
+ claims.set(identity, previous);
6191
+ }
6192
+ const claimContent = (key, sha256, item) => claim(contentAccess, JSON.stringify([key, sha256]), key, item, "content-access");
6059
6193
  for (const item of items) {
6060
6194
  if (item.status === "blocked" || item.sessionAssembly) continue;
6061
6195
  const keys = item.sessionMembership?.keys ?? (item.sessionUpload ? [item.sessionUpload.key] : []);
6062
6196
  if (!keys.length) continue;
6063
6197
  const destination = inventory.works.find((work) => work.logicalWorkId === item.workId);
6064
6198
  if (destination?.access === "removed") throw new ApplicationError("WORK_REMOVED");
6199
+ if (destination) observe(item, destination.access);
6065
6200
  for (const key of keys) {
6066
- const access = requestedAccess.get(key);
6067
- if (access !== void 0 && access !== item.access) return true;
6068
- requestedAccess.set(key, item.access);
6069
- const source = inventory.works.find((work) => work.sessions.some((entry) => entry.key === key));
6070
- if (source && source.logicalWorkId !== item.workId && source.access !== item.access) return true;
6071
- const sha256 = item.sessionMembership ? source?.sessions.find((session) => session.key === key)?.sha256 : item.sessionUpload?.sha256;
6072
- if (sha256 && claimContent(key, sha256, item.access)) return true;
6073
- if (destination && destination.access !== item.access && !destination.sessions.some((entry) => entry.key === key && (item.sessionMembership || entry.sha256 === sha256))) return true;
6201
+ claim(requestedAccess, key, key, item, "requested-access");
6202
+ const sources = inventory.works.filter((work) => work.sessions.some((entry) => entry.key === key));
6203
+ for (const source of sources) observe(item, source.access);
6204
+ for (const source of sources) if (source.logicalWorkId !== item.workId && source.access !== item.access) record(item, key, "held-elsewhere", source);
6205
+ const sha256 = item.sessionMembership ? sources[0]?.sessions.find((session) => session.key === key)?.sha256 : item.sessionUpload?.sha256;
6206
+ if (sha256) claimContent(key, sha256, item);
6207
+ if (destination && destination.access !== item.access && !destination.sessions.some((entry) => entry.key === key && (item.sessionMembership || entry.sha256 === sha256))) record(item, key, "destination-access", destination);
6074
6208
  }
6075
6209
  }
6076
6210
  for (const item of items) {
6077
6211
  if (item.sessionAssembly || item.sessionMembership || ["completed", "blocked"].includes(item.status)) continue;
6078
6212
  const destination = inventory.works.find((work) => work.logicalWorkId === item.workId);
6079
6213
  if (destination?.access === "removed") throw new ApplicationError("WORK_REMOVED");
6214
+ if (destination) observe(item, destination.access);
6080
6215
  for await (const session of sessions(journeyId, item.artifact, signal)) {
6081
6216
  const key = await sessionKeyFor(session);
6082
6217
  const { workerIdentity: _, ...original } = session;
6083
6218
  const legacyKey = session.workerIdentity ? await sessionKeyFor(original) : key;
6084
- if (claimContent(key, session.sha256, item.access) || legacyKey !== key && claimContent(legacyKey, session.sha256, item.access)) return true;
6085
- const otherAccess = requestedAccess.get(key);
6086
- if (otherAccess !== void 0 && otherAccess !== item.access) return true;
6087
- requestedAccess.set(key, item.access);
6219
+ claimContent(key, session.sha256, item);
6220
+ if (legacyKey !== key) claimContent(legacyKey, session.sha256, item);
6221
+ claim(requestedAccess, key, key, item, "requested-access");
6088
6222
  const otherDigest = requestedDigests.get(key);
6089
6223
  if (otherDigest !== void 0 && otherDigest.sha256 !== session.sha256) throw new SessionContentConflict({
6090
6224
  workIds: [.../* @__PURE__ */ new Set([...otherDigest.workIds, item.workId])],
@@ -6094,11 +6228,18 @@ async function sessionAccessBlocked(dependencies, journeyId, items, inventory, s
6094
6228
  sha256: session.sha256,
6095
6229
  workIds: [.../* @__PURE__ */ new Set([...otherDigest?.workIds ?? [], item.workId])]
6096
6230
  });
6097
- if (inventory.works.some((work) => work.logicalWorkId !== item.workId && work.access !== item.access && work.sessions.some((entry) => entry.key === key || entry.key === legacyKey && entry.sha256 === session.sha256))) return true;
6098
- if (destination && destination.access !== item.access && !destination.sessions.some((entry) => entry.key === key && entry.sha256 === session.sha256)) return true;
6231
+ for (const work of inventory.works) for (const entry of work.sessions) {
6232
+ if (entry.key !== key && !(entry.key === legacyKey && entry.sha256 === session.sha256)) continue;
6233
+ observe(item, work.access);
6234
+ if (work.logicalWorkId !== item.workId && work.access !== item.access) record(item, entry.key, "held-elsewhere", work);
6235
+ }
6236
+ if (destination && destination.access !== item.access && !destination.sessions.some((entry) => entry.key === key && entry.sha256 === session.sha256)) record(item, key, "destination-access", destination);
6099
6237
  }
6100
6238
  }
6101
- return false;
6239
+ return [...conflicts.values()].map((conflict) => (accountAccesses.get(conflict.workId)?.size ?? 0) > 1 ? {
6240
+ ...conflict,
6241
+ accountAccess: "mixed"
6242
+ } : conflict);
6102
6243
  }
6103
6244
  /** Keep overlapping session ownership serial through membership, including retained members. */
6104
6245
  async function sessionIndependentItems(dependencies, journeyId, items, inventory, signal) {
@@ -6776,8 +6917,8 @@ async function put(context, item, receiptId, upload, connection, signal, guard)
6776
6917
  await guard();
6777
6918
  await context.upload(item.artifact, upload, signal);
6778
6919
  const sent = withoutBusy(item);
6779
- await guard();
6780
6920
  try {
6921
+ await guard();
6781
6922
  return observe(sent, await connection.complete(receiptId, signal), connection, signal, context.snapshot === true);
6782
6923
  } catch (error) {
6783
6924
  if (upload.headers !== void 0 && error instanceof JourneyRemoteError && error.code === "bundle_mismatch") return {
@@ -6907,13 +7048,14 @@ function isCode(error, code) {
6907
7048
  }
6908
7049
  /**
6909
7050
  * A step that ended without a definite answer and is safe to repeat: a request timeout or abort
6910
- * that is not the Send's own cancellation, or a connection failure from `fetch`. The Work keeps
7051
+ * that is not the Send's own cancellation, or a connection failure from `fetch`: `fetch failed`
7052
+ * before the response, `terminated` when the socket resets while the body is read. The Work keeps
6911
7053
  * its state, so the next advance repeats the same idempotent step (ADR 0197).
6912
7054
  */
6913
7055
  function isInterrupted(error) {
6914
7056
  if (!(error instanceof Error)) return false;
6915
7057
  if (error.name === "TimeoutError" || error.name === "AbortError") return true;
6916
- return error instanceof TypeError && error.message === "fetch failed";
7058
+ return error instanceof TypeError && (error.message === "fetch failed" || error.message === "terminated");
6917
7059
  }
6918
7060
  const sessionBackoffMs = (attempt) => 1e3 * 2 ** (attempt - 1);
6919
7061
  const zeroByteBackoffMs = (count) => [
@@ -7201,10 +7343,10 @@ function usesSessionSend(dependencies, send) {
7201
7343
  if (send.items.some((item) => item.sessionUpload !== void 0 || item.sessionMembership || item.sessionAssembly)) return true;
7202
7344
  return !send.items.some((item) => item.receipt || item.reservation || item.previousReceipts.length > 0);
7203
7345
  }
7204
- async function saveSend(dependencies, snapshot, next, action) {
7346
+ async function saveSend(dependencies, snapshot, next, action, expectedJourneyRevision = action?.expectedJourneyRevision ?? snapshot.journey.revision) {
7205
7347
  return dependencies.storage.commitSend({
7206
7348
  journeyId: snapshot.journey.journeyId,
7207
- expectedJourneyRevision: action?.expectedJourneyRevision ?? snapshot.journey.revision,
7349
+ expectedJourneyRevision,
7208
7350
  expectedSendRevision: action?.expectedSendRevision ?? (snapshot.send?.id === next.id ? snapshot.send.revision : null),
7209
7351
  requestKey: action?.idempotencyKey ?? randomUUID(),
7210
7352
  requestHash: hash$1(action ?? next),
@@ -7231,6 +7373,10 @@ function aggregate(send) {
7231
7373
  function awaitingPassport(send) {
7232
7374
  return send.status === "completed" && send.items.some((item) => !["completed", "blocked"].includes(item.status));
7233
7375
  }
7376
+ /** The host continues confirmed uploads and unfinished Passport assessments without a panel. */
7377
+ function hostDrives(send) {
7378
+ return awaitingPassport(send) || send.confirmedAt != null && ["sending", "waiting"].includes(send.status);
7379
+ }
7234
7380
  /** Explicit resume retries recoverable items and recaptures a pending sign-in or profile. */
7235
7381
  function resumeState(send) {
7236
7382
  if (send.status !== "completed" && send.items.length && send.items.every((item) => item.status === "completed") && send.items.some((item) => !item.sessionAssembly)) return {
@@ -7255,18 +7401,23 @@ function resumeState(send) {
7255
7401
  ...send,
7256
7402
  status: "sending",
7257
7403
  error: null,
7258
- items: send.items.map((item) => item.status === "blocked" && [
7259
- "OPERATION_FAILED",
7260
- "UPLOAD_FAILED",
7261
- "SEND_REMOTE_REFUSED",
7262
- "SESSION_MOVE_ACCESS_REQUIRED",
7263
- "IDENTITY_UNAVAILABLE",
7264
- "ACCOUNT_SIGNED_OUT"
7265
- ].includes(item.error ?? "") ? {
7266
- ...item,
7267
- status: item.receipt ? item.receipt.state === "awaiting_upload" ? "uploading" : "assessing" : "ready",
7268
- error: null
7269
- } : item)
7404
+ items: send.items.map((item) => {
7405
+ const { accessConflict: _previous, accessConflictSessionCount: _count, ...current } = item;
7406
+ return item.status === "blocked" && [
7407
+ "OPERATION_FAILED",
7408
+ "COORDINATION_BUSY",
7409
+ "UPLOAD_FAILED",
7410
+ "SEND_REMOTE_REFUSED",
7411
+ "SESSION_MOVE_ACCESS_REQUIRED",
7412
+ "SESSION_MOVE_ACCESS_HELD",
7413
+ "IDENTITY_UNAVAILABLE",
7414
+ "ACCOUNT_SIGNED_OUT"
7415
+ ].includes(item.error ?? "") ? {
7416
+ ...current,
7417
+ status: item.receipt ? item.receipt.state === "awaiting_upload" ? "uploading" : "assessing" : "ready",
7418
+ error: null
7419
+ } : item;
7420
+ })
7270
7421
  });
7271
7422
  return profileRequired && !["blocked", "completed"].includes(next.status) ? {
7272
7423
  ...next,
@@ -7307,7 +7458,7 @@ async function writeProfile(connection, chosen, guard, signal, failure, captured
7307
7458
  await guard();
7308
7459
  return captured;
7309
7460
  } catch (error) {
7310
- if (signal.aborted) throw error;
7461
+ if (signal.aborted || safeCode(error) === "COORDINATION_BUSY") throw error;
7311
7462
  if (error instanceof JourneyRemoteError && error.status === 409 && addressRefusals[error.code]) return {
7312
7463
  status: "blocked",
7313
7464
  error: addressRefusals[error.code] ?? null
@@ -7333,7 +7484,7 @@ async function captureProfile(connection, guard, signal, failure) {
7333
7484
  parallelCompletes: parallelCompletes ? true : void 0
7334
7485
  };
7335
7486
  } catch (error) {
7336
- if (signal.aborted) throw error;
7487
+ if (signal.aborted || safeCode(error) === "COORDINATION_BUSY") throw error;
7337
7488
  if (error instanceof JourneyRemoteError && error.status === 401 || error instanceof ApplicationError && error.code === "ACCOUNT_SIGNED_OUT") return {
7338
7489
  status: "sign-in",
7339
7490
  error: null
@@ -7454,7 +7605,14 @@ async function advanceItems(host, signal) {
7454
7605
  });
7455
7606
  host.failure("SEND_SCOPE_BUSY", cause);
7456
7607
  error = "SEND_SCOPE_BUSY";
7457
- } else if (isInterrupted(cause) || isTransientRefusal(cause)) {
7608
+ } else if (isInterrupted(cause) || isTransientRefusal(cause) || safeCode(cause) === "COORDINATION_BUSY") {
7609
+ if (safeCode(cause) === "COORDINATION_BUSY") {
7610
+ const current = progressed ?? item;
7611
+ next.set(item.workId, {
7612
+ ...current,
7613
+ busyRefusals: (current.busyRefusals ?? 0) + 1
7614
+ });
7615
+ }
7458
7616
  host.failure("SEND_INTERRUPTED", cause);
7459
7617
  error = "SEND_INTERRUPTED";
7460
7618
  } else {
@@ -7488,7 +7646,7 @@ async function advanceItems(host, signal) {
7488
7646
  items
7489
7647
  }));
7490
7648
  } catch (error) {
7491
- if (!signal.aborted && (isInterrupted(error) || error instanceof Error && isInterrupted(error.cause) || isCode(error, "IDENTITY_UNAVAILABLE"))) {
7649
+ if (!signal.aborted && (isInterrupted(error) || error instanceof Error && isInterrupted(error.cause) || isCode(error, "IDENTITY_UNAVAILABLE") || safeCode(error) === "COORDINATION_BUSY")) {
7492
7650
  host.failure("SEND_INTERRUPTED", error);
7493
7651
  return send.error === "SEND_INTERRUPTED" ? host.applied() : host.save({
7494
7652
  ...send,
@@ -7595,7 +7753,10 @@ async function advanceSend(dependencies, address, signal, chain) {
7595
7753
  })
7596
7754
  });
7597
7755
  return blockSend({
7598
- send: snapshot.send,
7756
+ send: safeCode(error) === "SEND_SCOPE_INVALIDATED" ? {
7757
+ ...snapshot.send,
7758
+ items: snapshot.send.items.filter((item) => !["SESSION_MOVE_ACCESS_REQUIRED", "SESSION_MOVE_ACCESS_HELD"].includes(item.error ?? ""))
7759
+ } : snapshot.send,
7599
7760
  failure: dependencies.failure,
7600
7761
  save
7601
7762
  }, error, signal);
@@ -7621,18 +7782,41 @@ async function advanceSend(dependencies, address, signal, chain) {
7621
7782
  if (!connection.sessions) throw new ApplicationError("INVALID_STATE");
7622
7783
  if (!send.items.some((item) => item.status === "uploading" && !item.sessionAssembly && !item.receipt && !item.reservation)) return null;
7623
7784
  const inventory = await connection.sessions.inventory(preflightSignal);
7624
- if (!await sessionAccessBlocked(preparation.dependencies, snapshot.journey.journeyId, send.items, inventory, preflightSignal)) {
7785
+ const conflicts = await sessionAccessBlocked(preparation.dependencies, snapshot.journey.journeyId, send.items, inventory, preflightSignal);
7786
+ if (conflicts.length === 0) {
7625
7787
  independentItems = await sessionIndependentItems(preparation.dependencies, snapshot.journey.journeyId, send.items, inventory, preflightSignal);
7626
7788
  return null;
7627
7789
  }
7628
7790
  await connection.assertCurrent(preflightSignal);
7791
+ dependencies.failure("SESSION_MOVE_ACCESS_REQUIRED", {
7792
+ code: "SESSION_MOVE_ACCESS_REQUIRED",
7793
+ message: JSON.stringify({
7794
+ conflicts: conflicts.length,
7795
+ works: new Set(conflicts.map((conflict) => conflict.workId)).size,
7796
+ rules: [...new Set(conflicts.map((conflict) => conflict.rule))].sort()
7797
+ })
7798
+ });
7629
7799
  return save({
7630
7800
  ...send,
7631
7801
  status: "blocked",
7632
- items: send.items.map((item) => item.sessionAssembly || item.status === "completed" ? item : {
7633
- ...item,
7634
- status: "blocked",
7635
- error: "SESSION_MOVE_ACCESS_REQUIRED"
7802
+ items: send.items.map((item) => {
7803
+ if (item.sessionAssembly || item.status === "completed") return item;
7804
+ const { accessConflict: _previous, accessConflictSessionCount: _count, ...current } = item;
7805
+ const itemConflicts = conflicts.filter((entry) => entry.workId === item.workId);
7806
+ const conflict = itemConflicts[0];
7807
+ if (!conflict) return {
7808
+ ...current,
7809
+ status: "blocked",
7810
+ error: "SESSION_MOVE_ACCESS_HELD"
7811
+ };
7812
+ const { workId: _, ...accessConflict } = conflict;
7813
+ return {
7814
+ ...current,
7815
+ status: "blocked",
7816
+ error: "SESSION_MOVE_ACCESS_REQUIRED",
7817
+ accessConflict,
7818
+ accessConflictSessionCount: new Set(itemConflicts.map((entry) => entry.sessionKey)).size
7819
+ };
7636
7820
  })
7637
7821
  });
7638
7822
  },
@@ -7653,7 +7837,7 @@ async function advanceSend(dependencies, address, signal, chain) {
7653
7837
  });
7654
7838
  saving = pending.catch(() => void 0);
7655
7839
  return pending;
7656
- }).then((result) => {
7840
+ }).then(({ busyRefusals: _busy, ...result }) => {
7657
7841
  fulfilled.add(item.workId);
7658
7842
  return result;
7659
7843
  })
@@ -7974,20 +8158,15 @@ function createManualUploader(dependencies) {
7974
8158
  upload,
7975
8159
  status: "applied"
7976
8160
  };
7977
- const host = {
8161
+ let bindings = [];
8162
+ return advanceItems({
7978
8163
  send: upload,
7979
8164
  failure: dependencies.failure,
7980
- save: (next) => save(upload, next)
7981
- };
7982
- let bindings;
7983
- try {
7984
- bindings = await storage.bindings();
7985
- } catch (error) {
7986
- return blockSend(host, error, signal);
7987
- }
7988
- return advanceItems({
7989
- ...host,
7990
- connect: dependencies.auth.connect,
8165
+ save: (next) => save(upload, next),
8166
+ connect: async (connectSignal) => {
8167
+ bindings = await storage.bindings();
8168
+ return dependencies.auth.connect(connectSignal);
8169
+ },
7991
8170
  profile: (connection, profileSignal) => captureProfile(connection, guard(upload), profileSignal, dependencies.failure),
7992
8171
  context: (item) => ({
7993
8172
  active: guard(upload, item),
@@ -9244,89 +9423,91 @@ const sendRecoveryCodes = [
9244
9423
  ];
9245
9424
  //#endregion
9246
9425
  //#region apps/cli/src/account/journey/request.ts
9247
- /** Artifact-independent authorization: packaging progress cannot expand this scope. */
9248
- function requestedScope(journey) {
9249
- return hash$1({
9250
- privacy: privacyDecisionDigest(journey),
9251
- configurationId: journey.configurationId,
9252
- confirmedHideRevision: journey.confirmedHideRevision ?? 0,
9253
- consent: journey.consent,
9254
- profile: journey.profile,
9255
- works: journey.works.map((work) => ({
9256
- id: work.id,
9257
- inputRevision: work.inputRevision,
9258
- access: work.access,
9259
- accessConfirmed: work.accessConfirmed,
9260
- included: isIncludedWork(work),
9261
- placement: work.placement,
9262
- suggestion: work.suggestion
9263
- }))
9264
- });
9265
- }
9266
9426
  function requestMatches(send, journey) {
9267
9427
  return send.request?.scopeDigest === requestedScope(journey) && !journey.pendingHideId && journey.phase !== "paused" && !journey.abandonedAt;
9268
9428
  }
9269
9429
  async function requestSend(dependencies, raw, hostId, source) {
9270
9430
  const input = sendRequestSchema.parse(raw);
9271
- const current = await dependencies.storage.loadSend(input.journeyId);
9272
- if (!current) throw new ApplicationError("UNKNOWN_ITEM");
9273
- const recorded = current.send?.id === input.idempotencyKey ? current : await dependencies.storage.loadSend(input.journeyId, input.idempotencyKey);
9274
- if (recorded?.send) return dependencies.storage.commitSend({
9275
- journeyId: input.journeyId,
9276
- expectedJourneyRevision: input.expectedJourneyRevision,
9277
- expectedSendRevision: null,
9278
- requestKey: input.idempotencyKey,
9279
- requestHash: hash$1(input),
9280
- state: recorded.send,
9281
- at: dependencies.now()
9282
- });
9283
- if (current.journey.revision !== input.expectedJourneyRevision) return {
9284
- ...current,
9285
- status: "stale"
9286
- };
9287
- assertJourneyPrivacyReady(current.journey);
9288
- if (current.journey.pendingHideId || current.journey.phase === "paused") throw new ApplicationError("SCOPE_NOT_READY");
9289
- if (current.send && ([
9290
- "awaiting-packaging",
9291
- "sending",
9292
- "waiting",
9293
- "sign-in",
9294
- "review"
9295
- ].includes(current.send.status) || current.send.items.some((item) => item.recoveryReview) || current.send.status === "paused" && current.send.confirmedAt !== null && current.send.scopeDigest === scopeDigest(current.journey))) throw new ApplicationError("INVALID_TRANSITION");
9296
- if (!hostId) throw new ApplicationError("INVALID_TRANSITION");
9297
- const included = current.journey.works.filter(isIncludedWork);
9298
- if (!sameIds$1(input.included.map((work) => work.workId), included.map((work) => work.id)) || !input.included.every((item) => included.some((work) => work.id === item.workId && work.access === item.access && work.accessConfirmed)) || !sameIds$1(input.omittedWorkIds, current.journey.works.filter((work) => !isIncludedWork(work)).map((work) => work.id))) throw new ApplicationError("CONFIRMATION_REQUIRED");
9299
- const subject = (await dependencies.auth.connect(new AbortController().signal))?.subject ?? null;
9300
- const at = dependencies.now();
9301
- return dependencies.storage.commitSend({
9302
- journeyId: input.journeyId,
9303
- expectedJourneyRevision: input.expectedJourneyRevision,
9304
- expectedSendRevision: null,
9305
- requestKey: input.idempotencyKey,
9306
- requestHash: hash$1(input),
9307
- state: {
9308
- id: input.idempotencyKey,
9431
+ let expectedScope = input.displayedScopeDigest;
9432
+ let binding;
9433
+ let firstSendId = null;
9434
+ for (let attempt = 0;; attempt += 1) {
9435
+ const current = await dependencies.storage.loadSend(input.journeyId);
9436
+ if (!current) throw new ApplicationError("UNKNOWN_ITEM");
9437
+ if (attempt === 0) firstSendId = current.send?.id ?? null;
9438
+ const recorded = current.send?.id === input.idempotencyKey ? current : await dependencies.storage.loadSend(input.journeyId, input.idempotencyKey);
9439
+ if (recorded?.send) return dependencies.storage.commitSend({
9309
9440
  journeyId: input.journeyId,
9310
- revision: 0,
9311
- scopeDigest: requestedScope(current.journey),
9312
- subject,
9313
- confirmedAt: null,
9314
- status: "awaiting-packaging",
9315
- items: [],
9316
- omitted: [],
9317
- error: null,
9318
- request: {
9319
- requestedAt: at,
9320
- subject,
9321
- hostId,
9322
- source,
9441
+ expectedJourneyRevision: input.expectedJourneyRevision,
9442
+ expectedSendRevision: null,
9443
+ requestKey: input.idempotencyKey,
9444
+ requestHash: hash$1(input),
9445
+ state: recorded.send,
9446
+ at: dependencies.now()
9447
+ });
9448
+ if (attempt > 0 && (current.send?.id ?? null) !== firstSendId) return {
9449
+ ...current,
9450
+ status: "stale"
9451
+ };
9452
+ if (expectedScope === void 0) {
9453
+ if (current.journey.revision !== input.expectedJourneyRevision) return {
9454
+ ...current,
9455
+ status: "stale"
9456
+ };
9457
+ expectedScope = displayedScopeDigest(current.journey);
9458
+ }
9459
+ if (displayedScopeDigest(current.journey) !== expectedScope) return {
9460
+ ...current,
9461
+ status: "stale"
9462
+ };
9463
+ assertJourneyPrivacyReady(current.journey, false, { publicChoices: true });
9464
+ if (current.journey.pendingHideId || current.journey.phase === "paused") throw new ApplicationError("SCOPE_NOT_READY");
9465
+ if (current.send && ([
9466
+ "awaiting-packaging",
9467
+ "sending",
9468
+ "waiting",
9469
+ "sign-in",
9470
+ "review"
9471
+ ].includes(current.send.status) || current.send.items.some((item) => item.recoveryReview) || current.send.status === "paused" && current.send.confirmedAt !== null && scopeDigest(current.journey) === current.send.scopeDigest)) throw new ApplicationError("INVALID_TRANSITION");
9472
+ if (!hostId) throw new ApplicationError("INVALID_TRANSITION");
9473
+ const included = current.journey.works.filter(isIncludedWork);
9474
+ if (!sameIds$1(input.included.map((work) => work.workId), included.map((work) => work.id)) || !input.included.every((item) => included.some((work) => work.id === item.workId && work.access === item.access && work.accessConfirmed)) || !sameIds$1(input.omittedWorkIds, current.journey.works.filter((work) => !isIncludedWork(work)).map((work) => work.id))) throw new ApplicationError("CONFIRMATION_REQUIRED");
9475
+ if (!binding) binding = {
9476
+ subject: (await dependencies.auth.connect(new AbortController().signal))?.subject ?? null,
9477
+ at: dependencies.now()
9478
+ };
9479
+ const { subject, at } = binding;
9480
+ const result = await dependencies.storage.commitSend({
9481
+ journeyId: input.journeyId,
9482
+ expectedJourneyRevision: current.journey.revision,
9483
+ expectedSendRevision: null,
9484
+ requestKey: input.idempotencyKey,
9485
+ requestHash: hash$1(input),
9486
+ state: {
9487
+ id: input.idempotencyKey,
9488
+ journeyId: input.journeyId,
9489
+ revision: 0,
9323
9490
  scopeDigest: requestedScope(current.journey),
9324
- included: input.included,
9325
- omittedWorkIds: input.omittedWorkIds
9326
- }
9327
- },
9328
- at
9329
- });
9491
+ subject,
9492
+ confirmedAt: null,
9493
+ status: "awaiting-packaging",
9494
+ items: [],
9495
+ omitted: [],
9496
+ error: null,
9497
+ request: {
9498
+ requestedAt: at,
9499
+ subject,
9500
+ hostId,
9501
+ source,
9502
+ scopeDigest: requestedScope(current.journey),
9503
+ included: input.included,
9504
+ omittedWorkIds: input.omittedWorkIds
9505
+ }
9506
+ },
9507
+ at
9508
+ });
9509
+ if (result.status !== "stale" || attempt === 1) return result;
9510
+ }
9330
9511
  }
9331
9512
  /**
9332
9513
  * Previously sent Works with unchanged access remain part of the displayed authorization. Keep
@@ -9401,6 +9582,7 @@ function createJourneySender(dependencies, phases = 3, host = {
9401
9582
  /** The serving host owns a pending request; another process leaves it unchanged. */
9402
9583
  const pendingElsewhere = (send) => !host.serving && send?.request !== void 0 && !send.confirmedAt;
9403
9584
  const progress = /* @__PURE__ */ new Map();
9585
+ let packagingRetry;
9404
9586
  const sessionFacts = createSessionFactsCache();
9405
9587
  const running = /* @__PURE__ */ new Map();
9406
9588
  const runKey = (address) => `${address.journeyId}:${address.sendId}`;
@@ -9488,7 +9670,7 @@ function createJourneySender(dependencies, phases = 3, host = {
9488
9670
  const snapshot = await dependencies.storage.loadSend(input.journeyId);
9489
9671
  sessionFacts.retain(input.journeyId, snapshot?.send ?? null);
9490
9672
  if (!snapshot) throw new ApplicationError("UNKNOWN_ITEM");
9491
- if (snapshot.journey.revision !== input.expectedJourneyRevision) return {
9673
+ if (input.displayedScopeDigest ? displayedScopeDigest(snapshot.journey) !== input.displayedScopeDigest : snapshot.journey.revision !== input.expectedJourneyRevision) return {
9492
9674
  ...snapshot,
9493
9675
  status: "stale"
9494
9676
  };
@@ -9500,27 +9682,49 @@ function createJourneySender(dependencies, phases = 3, host = {
9500
9682
  const pendingError = pending ? await requestError(dependencies, pending, hostId, signal) : null;
9501
9683
  if (pending && (pendingError || !requestMatches(pending, snapshot.journey))) return block(snapshot, pending, pendingError ?? "REVISION_CONFLICT");
9502
9684
  const draft = await draftSend(snapshot.journey, dependencies, signal);
9685
+ let observed = false;
9503
9686
  const recover = snapshot.send && [
9504
9687
  "blocked",
9505
9688
  "paused",
9506
9689
  "completed"
9507
9690
  ].includes(snapshot.send.status) && snapshot.send.items.some((item) => sendRecoveryCodes.includes(item.error ?? ""));
9508
- if (recover && snapshot.send) {
9691
+ if (recover || dependencies.artifacts.sessions) {
9509
9692
  const connection = await dependencies.auth.connect(signal);
9510
- if (!connection) throw new ApplicationError("ACCOUNT_SIGNED_OUT");
9511
- if (snapshot.send.subject !== null && snapshot.send.subject !== connection.subject) throw new ApplicationError("IDENTITY_REJECTED");
9693
+ if (connection && draft.subject !== connection.subject) return {
9694
+ ...snapshot,
9695
+ status: "stale"
9696
+ };
9697
+ if (recover && !connection) throw new ApplicationError("ACCOUNT_SIGNED_OUT");
9698
+ if (recover && snapshot.send?.subject && snapshot.send.subject !== connection?.subject) throw new ApplicationError("IDENTITY_REJECTED");
9512
9699
  const sessions = dependencies.artifacts.sessions;
9513
- const remote = connection.sessions;
9514
- if (sessions && remote) {
9515
- const inventory = await remote.inventory(signal);
9516
- for (const item of draft.items) {
9517
- const current = inventory.works.find((work) => work.logicalWorkId === item.workId);
9700
+ const remote = connection?.sessions;
9701
+ if (sessions && remote && connection) {
9702
+ const inventory = await remote.inventory(signal).catch((error) => {
9703
+ if (signal.aborted || recover) throw error;
9704
+ dependencies.failure("SEND_INTERRUPTED", error);
9705
+ return null;
9706
+ });
9707
+ observed = inventory !== null;
9708
+ try {
9709
+ const conflicts = inventory ? await sessionAccessBlocked(dependencies, input.journeyId, draft.items, inventory, signal) : [];
9710
+ for (const item of draft.items) {
9711
+ const conflict = conflicts.find((entry) => entry.workId === item.workId && entry.accountAccess !== null) ?? conflicts.find((entry) => entry.workId === item.workId);
9712
+ if (conflict) {
9713
+ const { workId: _, ...accessConflict } = conflict;
9714
+ item.accessConflict = accessConflict;
9715
+ }
9716
+ }
9717
+ } catch (error) {
9718
+ if (!(error instanceof SessionContentConflict) && !(error instanceof ApplicationError && error.code === "WORK_REMOVED")) throw error;
9719
+ }
9720
+ for (const item of inventory && recover && snapshot.send ? draft.items : []) {
9721
+ const current = inventory?.works.find((work) => work.logicalWorkId === item.workId);
9518
9722
  const assembly = await remote.assembly(item.workId, signal);
9519
9723
  const selected = createSessionKeys();
9520
9724
  for await (const session of sessions(input.journeyId, item.artifact, signal)) await selected.add(session);
9521
9725
  const currentKeys = current?.sessions.map((session) => session.key) ?? [];
9522
9726
  item.recoveryReview = {
9523
- previousSendId: snapshot.send.id,
9727
+ previousSendId: snapshot.send?.id ?? draft.id,
9524
9728
  revision: assembly?.revision ?? 0,
9525
9729
  currentAccess: current?.access ?? null,
9526
9730
  currentKeys,
@@ -9530,7 +9734,11 @@ function createJourneySender(dependencies, phases = 3, host = {
9530
9734
  await connection.assertCurrent(signal);
9531
9735
  }
9532
9736
  }
9533
- const canReuseExistingSend = !(snapshot.send?.confirmedAt && ["blocked", "paused"].includes(snapshot.send.status)) && !recover && snapshot.send?.scopeDigest === draft.scopeDigest && snapshot.send.subject === draft.subject;
9737
+ for (const item of observed ? [] : draft.items) {
9738
+ const previous = snapshot.send?.items.find((entry) => entry.workId === item.workId && entry.access === item.access)?.accessConflict;
9739
+ if (previous && !item.accessConflict) item.accessConflict = previous;
9740
+ }
9741
+ const canReuseExistingSend = !(snapshot.send?.confirmedAt && ["blocked", "paused"].includes(snapshot.send.status)) && !recover && snapshot.send && scopeDigest(snapshot.journey) === snapshot.send.scopeDigest && snapshot.send.subject === draft.subject;
9534
9742
  const next = pending ? {
9535
9743
  ...draft,
9536
9744
  id: pending.id,
@@ -9538,11 +9746,12 @@ function createJourneySender(dependencies, phases = 3, host = {
9538
9746
  request: pending.request
9539
9747
  } : canReuseExistingSend && snapshot.send ? {
9540
9748
  ...snapshot.send,
9749
+ ...snapshot.send.confirmedAt === null ? { items: draft.items } : {},
9541
9750
  revision: snapshot.send.revision + 1
9542
9751
  } : draft;
9543
9752
  const result = await dependencies.storage.commitSend({
9544
9753
  journeyId: input.journeyId,
9545
- expectedJourneyRevision: input.expectedJourneyRevision,
9754
+ expectedJourneyRevision: snapshot.journey.revision,
9546
9755
  expectedSendRevision: pending?.revision ?? (canReuseExistingSend && snapshot.send ? snapshot.send.revision : null),
9547
9756
  requestKey: input.idempotencyKey,
9548
9757
  requestHash: hash$1(input),
@@ -9555,7 +9764,12 @@ function createJourneySender(dependencies, phases = 3, host = {
9555
9764
  async start(raw, signal) {
9556
9765
  const input = sendStartSchema.parse(raw);
9557
9766
  const snapshot = await load(input);
9558
- if (!matchesAction(snapshot, input)) return saveSend(dependencies, snapshot, snapshot.send, input);
9767
+ if (snapshot.send.revision !== input.expectedSendRevision || !input.displayedScopeDigest && snapshot.journey.revision !== input.expectedJourneyRevision) return saveSend(dependencies, snapshot, snapshot.send, input);
9768
+ if (input.displayedScopeDigest && displayedScopeDigest(snapshot.journey) !== input.displayedScopeDigest) return {
9769
+ ...snapshot,
9770
+ status: "stale"
9771
+ };
9772
+ const saveStart = (next) => saveSend(dependencies, snapshot, next, input, snapshot.journey.revision);
9559
9773
  if (pendingElsewhere(snapshot.send)) return {
9560
9774
  ...snapshot,
9561
9775
  status: "applied"
@@ -9563,8 +9777,8 @@ function createJourneySender(dependencies, phases = 3, host = {
9563
9777
  const authorizationError = await requestError(dependencies, snapshot.send, hostId, signal);
9564
9778
  if (snapshot.send.request && !snapshot.send.confirmedAt && (authorizationError || !requestMatches(snapshot.send, snapshot.journey))) return block(snapshot, snapshot.send, authorizationError ?? "REVISION_CONFLICT");
9565
9779
  assertScope(snapshot.journey, snapshot.send);
9566
- if (!sameIds$1(input.confirmedWorkIds, snapshot.send.items.map((item) => item.workId)) || !sameIds$1(input.confirmedOmittedWorkIds, snapshot.send.omitted.map((item) => item.workId)) || input.scopeDigest !== snapshot.send.scopeDigest || snapshot.send.items.length === 0) throw new ApplicationError("CONFIRMATION_REQUIRED");
9567
- if (snapshot.send.confirmedAt !== null) return saveSend(dependencies, snapshot, snapshot.send, input);
9780
+ if (!sameIds$1(input.confirmedWorkIds, snapshot.send.items.map((item) => item.workId)) || !sameIds$1(input.confirmedOmittedWorkIds, snapshot.send.omitted.map((item) => item.workId)) || input.scopeDigest !== snapshot.send.scopeDigest || snapshot.send.items.length === 0 || snapshot.send.items.some((item) => item.accessConflict !== void 0)) throw new ApplicationError("CONFIRMATION_REQUIRED");
9781
+ if (snapshot.send.confirmedAt !== null) return saveStart(snapshot.send);
9568
9782
  const connection = await dependencies.auth.connect(signal);
9569
9783
  if (snapshot.send.request && (connection?.subject ?? null) !== snapshot.send.request.subject) return saveSend(dependencies, snapshot, {
9570
9784
  ...snapshot.send,
@@ -9580,18 +9794,18 @@ function createJourneySender(dependencies, phases = 3, host = {
9580
9794
  if (connection) try {
9581
9795
  profile = await prepareProfile(dependencies, snapshot, connection, signal);
9582
9796
  } catch (error) {
9583
- if (error instanceof ApplicationError && error.code === "REVISION_CONFLICT") return saveSend(dependencies, snapshot, snapshot.send, input);
9797
+ if (error instanceof ApplicationError && error.code === "REVISION_CONFLICT") return saveStart(snapshot.send);
9584
9798
  throw error;
9585
9799
  }
9586
9800
  signal.throwIfAborted();
9587
9801
  const finalAuthorizationError = await requestError(dependencies, snapshot.send, hostId, signal);
9588
9802
  if (finalAuthorizationError) return block(snapshot, snapshot.send, finalAuthorizationError);
9589
- return saveSend(dependencies, snapshot, {
9803
+ return saveStart({
9590
9804
  ...snapshot.send,
9591
9805
  subject: connection?.subject ?? snapshot.send.subject,
9592
9806
  confirmedAt: snapshot.send.request && !connection ? null : dependencies.now(),
9593
9807
  ...profile
9594
- }, input);
9808
+ });
9595
9809
  },
9596
9810
  async respond(raw) {
9597
9811
  const input = sendChoiceSchema.parse(raw);
@@ -9672,6 +9886,10 @@ function createJourneySender(dependencies, phases = 3, host = {
9672
9886
  ...snapshot,
9673
9887
  status: "applied"
9674
9888
  };
9889
+ if (!host.serving && hostDrives(snapshot.send) && await host.focused?.(address.journeyId)) return {
9890
+ ...snapshot,
9891
+ status: "applied"
9892
+ };
9675
9893
  const authorizationError = await requestError(dependencies, snapshot.send, hostId, lifetime);
9676
9894
  if (authorizationError) return block(snapshot, snapshot.send, authorizationError);
9677
9895
  if (snapshot.send.request && !snapshot.send.confirmedAt && ["awaiting-packaging", "prepared"].includes(snapshot.send.status)) {
@@ -9686,10 +9904,52 @@ function createJourneySender(dependencies, phases = 3, host = {
9686
9904
  error: "REVISION_CONFLICT"
9687
9905
  });
9688
9906
  if (snapshot.send.status === "awaiting-packaging") {
9689
- if (await dependencies.package?.(address.journeyId, snapshot.send.request.included.map((item) => item.workId), lifetime, snapshot.send.request.source) === false) return {
9690
- ...await load(address),
9907
+ if (packagingRetry?.key !== key) packagingRetry = void 0;
9908
+ if (packagingRetry && Date.parse(dependencies.now()) < packagingRetry.after) return {
9909
+ ...snapshot,
9691
9910
  status: "applied"
9692
9911
  };
9912
+ let packaged;
9913
+ try {
9914
+ packaged = await dependencies.package?.(address.journeyId, snapshot.send.request.included.map((item) => item.workId), lifetime, snapshot.send.request.source);
9915
+ } catch (cause) {
9916
+ lifetime.throwIfAborted();
9917
+ const code = cause instanceof ApplicationError ? cause.code : safeCode(cause);
9918
+ if (packagingRefusalCodes.includes(code)) throw cause;
9919
+ dependencies.failure(code, cause);
9920
+ snapshot = await load(address);
9921
+ if (snapshot.send.status !== "awaiting-packaging") return {
9922
+ ...snapshot,
9923
+ status: "stale"
9924
+ };
9925
+ const error = await requestError(dependencies, snapshot.send, hostId, lifetime);
9926
+ if (error || !requestMatches(snapshot.send, snapshot.journey)) return block(snapshot, snapshot.send, error ?? "REVISION_CONFLICT");
9927
+ if (finalPackagingCodes.includes(code)) {
9928
+ packagingRetry = void 0;
9929
+ return block(snapshot, snapshot.send, code);
9930
+ }
9931
+ const failures = Math.min((packagingRetry?.failures ?? 0) + 1, 5);
9932
+ packagingRetry = {
9933
+ key,
9934
+ failures,
9935
+ after: Date.parse(dependencies.now()) + Math.min(2e3 * 2 ** (failures - 1), 3e4)
9936
+ };
9937
+ return saveSend(dependencies, snapshot, {
9938
+ ...snapshot.send,
9939
+ error: code
9940
+ });
9941
+ }
9942
+ packagingRetry = void 0;
9943
+ if (packaged === false) {
9944
+ snapshot = await load(address);
9945
+ return snapshot.send.error ? saveSend(dependencies, snapshot, {
9946
+ ...snapshot.send,
9947
+ error: null
9948
+ }) : {
9949
+ ...snapshot,
9950
+ status: "applied"
9951
+ };
9952
+ }
9693
9953
  snapshot = await load(address);
9694
9954
  if (snapshot.send.status !== "awaiting-packaging") return {
9695
9955
  ...snapshot,
@@ -9703,6 +9963,10 @@ function createJourneySender(dependencies, phases = 3, host = {
9703
9963
  if (prepared.status === "stale" || prepared.send?.status !== "prepared") return prepared;
9704
9964
  snapshot = requireSend(prepared);
9705
9965
  }
9966
+ if (snapshot.send.items.some((item) => item.accessConflict)) return saveSend(dependencies, snapshot, {
9967
+ ...snapshot.send,
9968
+ request: void 0
9969
+ });
9706
9970
  if (!requestMatches(snapshot.send, snapshot.journey) || !requestedItemsMatch(snapshot.send, snapshot.journey)) return saveSend(dependencies, snapshot, {
9707
9971
  ...snapshot.send,
9708
9972
  status: "blocked",
@@ -9740,10 +10004,6 @@ function createJourneySender(dependencies, phases = 3, host = {
9740
10004
  }
9741
10005
  //#endregion
9742
10006
  //#region apps/cli/src/account/journey/auth.ts
9743
- /** An access token this close to expiry is renewed first, so a step never starts on it. */
9744
- const renewBeforeMs = 12e4;
9745
- const renewTimeoutMs = 3e4;
9746
- const renewals = /* @__PURE__ */ new WeakMap();
9747
10007
  /** Reuses the existing device sign-in credential; a token is never returned to the panel or Agent. */
9748
10008
  function createJourneySendAuth(account, requestFetch) {
9749
10009
  /**
@@ -9770,39 +10030,11 @@ function createJourneySendAuth(account, requestFetch) {
9770
10030
  return value;
9771
10031
  })).credential?.refreshToken) await expire(accessToken);
9772
10032
  }
9773
- function renew(credential) {
9774
- const running = renewals.get(account);
9775
- if (running) return running;
9776
- const renewing = (async () => {
9777
- try {
9778
- const token = await account.identity.refresh(credential.refreshToken, AbortSignal.timeout(renewTimeoutMs));
9779
- if (!token) {
9780
- await expire(credential.accessToken);
9781
- return null;
9782
- }
9783
- return (await account.credentials.update((value) => {
9784
- if (value.credential?.refreshToken === credential.refreshToken) value.credential = {
9785
- ...value.credential,
9786
- accessToken: token.accessToken,
9787
- refreshToken: token.refreshToken ?? credential.refreshToken,
9788
- scope: token.scope,
9789
- issuedAt: account.now(),
9790
- expiresAt: token.expiresAt
9791
- };
9792
- return value;
9793
- })).credential;
9794
- } finally {
9795
- renewals.delete(account);
9796
- }
9797
- })();
9798
- renewals.set(account, renewing);
9799
- return renewing;
9800
- }
9801
10033
  async function current() {
9802
10034
  const credential = (await account.credentials.update((value) => value)).credential;
9803
10035
  if (!credential || credential.identityUrl !== account.configuration.identityUrl || credential.clientId !== account.configuration.clientId) return null;
9804
10036
  const left = Date.parse(credential.expiresAt) - Date.parse(account.now());
9805
- if (Number.isFinite(left) && left > renewBeforeMs) return credential;
10037
+ if (Number.isFinite(left) && left > 12e4) return credential;
9806
10038
  const { refreshToken } = credential;
9807
10039
  if (!refreshToken) {
9808
10040
  if (Number.isFinite(left) && left > 0) return credential;
@@ -9810,7 +10042,7 @@ function createJourneySendAuth(account, requestFetch) {
9810
10042
  return null;
9811
10043
  }
9812
10044
  try {
9813
- return await renew({
10045
+ return await renew(account, {
9814
10046
  ...credential,
9815
10047
  refreshToken
9816
10048
  });
@@ -9860,6 +10092,100 @@ function createJourneySendAuth(account, requestFetch) {
9860
10092
  };
9861
10093
  }
9862
10094
  //#endregion
10095
+ //#region apps/cli/src/account/journey/account-access.ts
10096
+ /** Current and legacy upload keys of every part of one local session. */
10097
+ async function keysOf(session) {
10098
+ const keys = [];
10099
+ for (const part of session.parts.length ? session.parts : [void 0]) {
10100
+ const native = part?.nativeEvidence?.identity;
10101
+ const identity = {
10102
+ harness: session.harness,
10103
+ nativeSessionId: native?.sessionId ?? session.sourceSessionId,
10104
+ ...part?.harnessRelativePath ? { relativePath: part.harnessRelativePath } : {},
10105
+ ...session.harness === "claude" && native?.role === "worker" ? { workerIdentity: { agentId: native.agentId } } : {}
10106
+ };
10107
+ const { workerIdentity: _, ...original } = identity;
10108
+ keys.push([await sessionKeyFor(identity), await sessionKeyFor(original)]);
10109
+ }
10110
+ return keys;
10111
+ }
10112
+ /**
10113
+ * Compare frozen identities with the same hashed keys used by session uploads. A Work sends each
10114
+ * root's whole family (`members`, from `reviewFamilies`), so uniform Public is `public-all` only
10115
+ * when every part of every member is held; any key without history keeps it `public` (ADR 0275).
10116
+ */
10117
+ async function accountAccessForRoots(state, roots, inventory, members) {
10118
+ const sessions = new Map(roots.map((session) => [session.id, session]));
10119
+ const held = /* @__PURE__ */ new Map();
10120
+ for (const work of inventory.works) for (const session of work.sessions) {
10121
+ const access = held.get(session.key) ?? /* @__PURE__ */ new Set();
10122
+ access.add(work.access);
10123
+ held.set(session.key, access);
10124
+ }
10125
+ const result = /* @__PURE__ */ new Map();
10126
+ for (const work of state.works) {
10127
+ const access = /* @__PURE__ */ new Set();
10128
+ let known = 0;
10129
+ let unknown = 0;
10130
+ for (const id of work.rootSessionIds) {
10131
+ const root = sessions.get(id);
10132
+ if (!root) throw new ApplicationError("CORRUPTED_STATE");
10133
+ for (const session of members.get(id) ?? [root]) for (const [key, legacyKey] of await keysOf(session)) {
10134
+ const values = /* @__PURE__ */ new Set([...held.get(key) ?? [], ...held.get(legacyKey) ?? []]);
10135
+ if (!values.size) {
10136
+ unknown++;
10137
+ continue;
10138
+ }
10139
+ known++;
10140
+ for (const value of values) access.add(value);
10141
+ }
10142
+ }
10143
+ if (!known) continue;
10144
+ const uniform = access.size === 1;
10145
+ result.set(work.id, uniform && access.has("private") ? "private" : uniform && access.has("public") ? unknown === 0 ? "public-all" : "public" : "mixed");
10146
+ }
10147
+ return result;
10148
+ }
10149
+ /** Existing authenticated owner ports supply inventory; signed-out scope confirmation stays local. */
10150
+ function createAccountAccess(storage, auth) {
10151
+ return async (state, signal) => {
10152
+ const controller = new AbortController();
10153
+ const boundedSignal = AbortSignal.any([signal, controller.signal]);
10154
+ let timer;
10155
+ let onAbort = () => {};
10156
+ const stopped = new Promise((_resolve, reject) => {
10157
+ onAbort = () => reject(/* @__PURE__ */ new Error("ACCOUNT_ACCESS_CANCELLED"));
10158
+ boundedSignal.addEventListener("abort", onAbort, { once: true });
10159
+ if (boundedSignal.aborted) onAbort();
10160
+ timer = setTimeout(() => controller.abort(), 5e3);
10161
+ });
10162
+ const read = async () => {
10163
+ boundedSignal.throwIfAborted();
10164
+ const connection = await auth.connect(boundedSignal);
10165
+ if (!connection?.sessions) return /* @__PURE__ */ new Map();
10166
+ const plan = (await storage.resources({
10167
+ journeyId: state.journeyId,
10168
+ kind: "plan"
10169
+ }))[0];
10170
+ if (plan?.kind !== "plan") throw new ApplicationError("SCOPE_NOT_READY");
10171
+ const inventory = await connection.sessions.inventory(boundedSignal);
10172
+ const access = await accountAccessForRoots(state, plan.approval.inventory.sessions, inventory, reviewFamilies(plan.approval.inventory).members);
10173
+ await connection.assertCurrent(boundedSignal);
10174
+ return access;
10175
+ };
10176
+ try {
10177
+ return await Promise.race([read(), stopped]);
10178
+ } catch {
10179
+ console.error("ACCOUNT_ACCESS_UNAVAILABLE: using model access");
10180
+ return /* @__PURE__ */ new Map();
10181
+ } finally {
10182
+ clearTimeout(timer);
10183
+ boundedSignal.removeEventListener("abort", onAbort);
10184
+ controller.abort();
10185
+ }
10186
+ };
10187
+ }
10188
+ //#endregion
9863
10189
  //#region apps/cli/src/operation/protocol.ts
9864
10190
  const sourceExclusionReasonSchema = z.enum([
9865
10191
  "guardian-excluded",
@@ -10455,6 +10781,9 @@ async function readJson(response, where) {
10455
10781
  }
10456
10782
  chunks.push(next.value);
10457
10783
  }
10784
+ } catch (error) {
10785
+ if (error instanceof ApplicationError) throw error;
10786
+ throw unavailable(`${where} body read failed`, error);
10458
10787
  } finally {
10459
10788
  reader.releaseLock();
10460
10789
  }
@@ -11489,6 +11818,8 @@ async function compose(options, callbacks, bundles) {
11489
11818
  onFatal: fatalStore.fail
11490
11819
  });
11491
11820
  const account = createAccount(accountPorts);
11821
+ const sendAuth = createJourneySendAuth(accountPorts);
11822
+ const accountAccess = createAccountAccess(journeyStorage, sendAuth);
11492
11823
  const application = createApplication({
11493
11824
  sessionPeriod: options.sessionPeriod,
11494
11825
  journeySender: createJourneySender({
@@ -11505,7 +11836,7 @@ async function compose(options, callbacks, bundles) {
11505
11836
  }
11506
11837
  }),
11507
11838
  artifacts: createJourneySendArtifacts(journeyStorage),
11508
- auth: createJourneySendAuth(accountPorts),
11839
+ auth: sendAuth,
11509
11840
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
11510
11841
  failure: (code, cause) => diagnostics.onFailure(code, cause),
11511
11842
  sessionTransfer: (workId, transfer) => diagnostics.onActivity({
@@ -11528,18 +11859,22 @@ async function compose(options, callbacks, bundles) {
11528
11859
  phases: { upload: elapsedMs },
11529
11860
  volumes: { bytes }
11530
11861
  })
11531
- }, void 0, sendHost),
11862
+ }, void 0, sendHost.serving ? sendHost : {
11863
+ ...sendHost,
11864
+ focused: (journeyId) => focusedHostAlive(stateDir, journeyId, async () => (await journeyStorage.current())?.journeyId)
11865
+ }),
11532
11866
  manualUploader: createManualUploader({
11533
11867
  storage: store.manualUpload,
11534
11868
  artifacts: createManualUploadArtifacts({ getDeliverySources: () => application.getDeliverySources() }),
11535
- auth: createJourneySendAuth(accountPorts),
11869
+ auth: sendAuth,
11536
11870
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
11537
11871
  failure: (code, cause) => diagnostics.onFailure(code, cause)
11538
11872
  }),
11539
11873
  journeyLocal: {
11874
+ accountAccess,
11540
11875
  titles: createJourneyTitles(journeyStorage, processing),
11541
11876
  describe: createJourneyDescription(journeyStorage, processing),
11542
- works: createJourneyWorks(journeyStorage, processing, stateDir),
11877
+ works: createJourneyWorks(journeyStorage, processing, stateDir, accountAccess),
11543
11878
  hide: createJourneyHide({
11544
11879
  storage: journeyStorage,
11545
11880
  execution,
@@ -12008,4 +12343,4 @@ async function cancelOperation(runtime, id) {
12008
12343
  return readOperation(runtime, id);
12009
12344
  }
12010
12345
  //#endregion
12011
- export { permissionDiagnostic as _, readAgentPresence as a, executorRequestSchema as c, guardDetachedStartup as d, createRenderActivity as f, longCommands as g, businessCommands as h, invoke as i, awaitingPassport as l, parseOptions as m, readOperation as n, createRuntime as o, helpText as p, waitOperation as r, executorEventSchema as s, cancelOperation as t, serveDetached as u, startupFailure as v, traceStartup as y };
12346
+ export { longCommands as _, readAgentPresence as a, traceStartup as b, executorRequestSchema as c, serveDetached as d, guardDetachedStartup as f, businessCommands as g, parseOptions as h, invoke as i, awaitingPassport as l, helpText as m, readOperation as n, createRuntime as o, createRenderActivity as p, waitOperation as r, executorEventSchema as s, cancelOperation as t, hostDrives as u, permissionDiagnostic as v, startupFailure as y };