@gitroll/tacitt-cli 0.6.9 → 0.6.11

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 (57) hide show
  1. package/README.md +43 -4
  2. package/dist/{agent-setup-CODlN6zl.mjs → agent-setup-DdfF3zch.mjs} +2 -2
  3. package/dist/agent-setup-worker.mjs +5 -5
  4. package/dist/cli.mjs +35 -12
  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 +9 -7
  8. package/dist/{frame-Dj5Zs6UW.mjs → frame-Bq2zRh-i.mjs} +1 -1
  9. package/dist/{heap-Dt3-d27v.mjs → heap-CV8Eca8V.mjs} +217 -13
  10. package/dist/inspection-worker.mjs +4 -4
  11. package/dist/{job-BQXssW46.mjs → job-BKsifhyv.mjs} +38 -18
  12. package/dist/{locate-BWkblijS.mjs → locate-Cx52Zqxv.mjs} +204 -20
  13. package/dist/{model-CSx2dP_P.mjs → model-C1UXkHh9.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 +29 -2
  17. package/dist/{query-CfhcS4gd.mjs → query-AMZerUh8.mjs} +712 -215
  18. package/dist/{run-xlOaKCk8.mjs → run-CaPktPhH.mjs} +4 -4
  19. package/dist/{src-DzKuzGVH.mjs → src-D1iONla3.mjs} +2215 -1613
  20. package/dist/{src-V9GHLZQs.mjs → src-DlVbGB32.mjs} +74 -37
  21. package/dist/{src-BgsEnMsy.mjs → src-wZx_v0Ih.mjs} +297 -207
  22. package/dist/store-database-worker.mjs +1 -1
  23. package/dist/web/assets/index-CYl7eDUu.js +172 -0
  24. package/dist/web/assets/index-DzOozqms.css +1 -0
  25. package/dist/web/index.html +2 -2
  26. package/package.json +1 -1
  27. package/skills/tacitt/INDEX.md +6 -6
  28. package/skills/tacitt/SKILL.md +11 -11
  29. package/skills/tacitt/references/INDEX.md +3 -3
  30. package/skills/tacitt/references/b-method.md +2 -2
  31. package/skills/tacitt/references/bootstrap.md +2 -2
  32. package/skills/tacitt/references/classification.md +15 -5
  33. package/skills/tacitt/references/commands.md +37 -30
  34. package/skills/tacitt/references/journey.md +14 -6
  35. package/skills/tacitt/references/parallel-review.md +27 -33
  36. package/skills/tacitt/references/send.md +35 -9
  37. package/skills/tacitt/scripts/INDEX.md +37 -37
  38. package/skills/tacitt/scripts/answer.mjs +73 -15
  39. package/skills/tacitt/scripts/journey-classification-metadata.mjs +75 -26
  40. package/skills/tacitt/scripts/journey-classification-progress.mjs +1 -2
  41. package/skills/tacitt/scripts/journey-classification-validation.d.mts +13 -0
  42. package/skills/tacitt/scripts/journey-classification-validation.mjs +38 -0
  43. package/skills/tacitt/scripts/journey-classification.mjs +6 -1
  44. package/skills/tacitt/scripts/journey-consolidation.mjs +1 -0
  45. package/skills/tacitt/scripts/journey-scan-timing.d.mts +1 -0
  46. package/skills/tacitt/scripts/journey-scan-timing.mjs +19 -4
  47. package/skills/tacitt/scripts/journey-state.d.mts +15 -1
  48. package/skills/tacitt/scripts/journey-state.mjs +104 -5
  49. package/skills/tacitt/scripts/journey-tasks.d.mts +1 -0
  50. package/skills/tacitt/scripts/journey-tasks.mjs +13 -5
  51. package/skills/tacitt/scripts/journey.d.mts +46 -28
  52. package/skills/tacitt/scripts/journey.mjs +57 -8
  53. package/skills/tacitt/scripts/privacy-validator.mjs +178 -12
  54. package/skills/tacitt/scripts/review.d.mts +7 -6
  55. package/skills/tacitt/scripts/review.mjs +576 -147
  56. package/dist/web/assets/index-9UfCT_2C.js +0 -174
  57. package/dist/web/assets/index-CYJBq1px.css +0 -1
@@ -1,10 +1,11 @@
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-V9GHLZQs.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-BQXssW46.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-BgsEnMsy.mjs";
5
- import { $ as settingsBatchSchema, $r as isReadingComplete, A as deviceAuthorizationSchema, B as inspectionStartSchema, Bt as classificationWorks, Ct as sendPrepareSchema, Dt as currentJourneyAdapters, Fn as bTermMatcher, Gn as retireWorkDescription, Gr as sendArtifactSchema, Hn as journeyConfirmedHides, Hr as agentSourceSchema, Ht as newJourneyWork, Jn as workCorrectionBinding, Jr as sendResolutionSchema, Kn as workDecisionDigest, Kr as sendReceiptSchema, Lr as hasCurrentStaticReview, M as profileUrlMatchesIdentity, Mr as privacyDecisionDigest, N as readAccountAvatarUrl, O as createAccount, Or as displayedScopeDigest, Ot as obsoleteJourney, P as storedCredentialSchema, Pt as applyClassification, Qr as isIncludedWork, R as createApplication, Rr as staticReviewDigest, St as sendChoiceSchema, Tt as sendStartSchema, Ur as agentSourcesSchema, V as validateInspectionBatch, Vt as retireScanGeneration, W as inspectionQuerySchema, Wr as journeySendSchema, Xr as hasCompleteWorkDescription, Yr as sendSettingsSchema, Yt as journeySearchResultSchema, Zr as isClassifying, Zt as searchScopeDigest, _ as changeJourney, _r as emptyPreparationFacts, _t as emptyTermsRevision, ai as packageCommandSchema, ar as scanPreparationDigest, bt as replaceSensitiveTermsSchema, cr as scanGitSeedsResource, ei as isSelectedWork, et as settingsQuerySchema, fi as recommendParallelism, ft as manualUploadChoiceSchema, h as hideScopeDigest, ht as manualUploadStartSchema, ii as commandSchema, j as deviceProfileScopeSchema, jr as assertJourneyPrivacyReady, k as accountProfileSchema, kr as requestedScope, li as producerVersion, lr as verifiedScanGitSeeds, lt as validateFailureLocation, m as activeJourneyTerms, mt as manualUploadSchema, oi as selectionBatchSchema, or as scanSessionResourceSchema, pt as manualUploadPrepareSchema, qr as sendReplaceResultSchema, ri as withReadingProgress, rt as appendSensitiveTerms, si as selectionSchema, sr as scanGitSeedsId, st as artifactFailureDetailSchema, ur as scanExclusionResourceSchema, ut as manualUploadActionSchema, wt as sendRequestSchema, xt as sendActionSchema, yt as normalizeSensitiveTerms, z as inspectionCancelSchema, zn as confirmedHideKinds, zt as classificationPartition } from "./src-DzKuzGVH.mjs";
6
- import { _t as isCoordinationBusy } from "./heap-Dt3-d27v.mjs";
7
- import { t as createAgentSetupPort } from "./agent-setup-CODlN6zl.mjs";
1
+ import { $ as removeNamedTemporarySpools, 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 createTemporary, R as defaultParallelism, T as createCollectionArtifacts, U as readLogicalJourneyFinalMasks, W as readConfiguredOutputInfo, Z as decodeZstd, _ as processJourneyMetadata, a as scanReviewFields, at as restrictToOwner, c as readSessionScanRecords, ct as windowsAclCodes, et as createCleanup, g as readCapturedOriginalRows, i as buildWorkScanBatches, it as openPrivateFile, j as runConfiguredSessionJob, k as capturedJourneyScope, l as scanPreparedSession, m as openConfiguredUpload, n as verifyPreparedSessionSources, o as createWorkPieces, ot as windowsOwnerOnly, p as configuredSessionUploads, q as readWorkspacePair, r as prepareSessionScans, rt as makePrivateDirectory, s as readSessionScanFields, st as isWindowsAclFailure, t as preparedScanConfiguration, v as readStaticSessionMasks, w as readConfiguredSearchFields, y as staticSessionJobs, z as gitSeedsProvider } from "./src-DlVbGB32.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-BKsifhyv.mjs";
3
+ import { $t as profileHasChoices, D as bLiteralTerms, Gt as filterInventoryByPeriod, Kt as sessionPeriodSchema, Mt as privacyTermKey, Nn as createStaticProcessor, On as currentDetectorVersion, Qt as passportAddressSchema, Vn as entityDictionaryVersion, Vt as termTypeInfo, Xt as mergeOwnerProfile, Yn as frameRecords, Zt as ownerProfileSchema, an as safeField, br as processingSite, cn as reviewFamilies, dn as observeSource, dr as admitCustomTerm, fr as admitGitName, gr as ProcessingError, in as safeCode, nn as ApplicationError, rn as UnfinishedJourneyError, sn as familySelected, u as textDigest, ur as createEntityMatcher, xr as siteFields } from "./locate-Cx52Zqxv.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-wZx_v0Ih.mjs";
5
+ import { $r as sendReplaceResultSchema, An as scanGitSeedsResource, At as obsoleteJourney, B as createApplication, Bn as journeySearchResultSchema, Bt as classificationPartition, Ct as sendActionSchema, Dn as scanPreparationDigest, Dt as sendStartSchema, Et as sendRequestSchema, F as readAccountAvatarUrl, H as inspectionStartSchema, Hn as searchScopeDigest, Ht as retireScanGeneration, I as storedCredentialSchema, Ir as assertJourneyPrivacyReady, Jr as agentSourceSchema, K as inspectionQuerySchema, Lr as privacyDecisionDigest, M as deviceAuthorizationSchema, Mn as scanExclusionResourceSchema, N as deviceProfileScopeSchema, Nr as displayedScopeDigest, O as createAccount, On as scanSessionResourceSchema, P as profileUrlMatchesIdentity, Pr as requestedScope, Qr as sendReceiptSchema, St as replaceSensitiveTermsSchema, Tt as sendPrepareSchema, U as validateInspectionBatch, Ur as hasCurrentStaticReview, Ut as newJourneyWork, V as inspectionCancelSchema, Vt as classificationWorks, Wr as staticReviewDigest, Xr as journeySendSchema, Yr as agentSourcesSchema, Zr as sendArtifactSchema, _ as changeJourney, _t as manualUploadStartSchema, ai as isReadingComplete, ar as bTermMatcher, at as appendSensitiveTerms, di as packageCommandSchema, dt as validateFailureLocation, ei as sendResolutionSchema, fi as selectionBatchSchema, fr as journeyConfirmedHides, ft as manualUploadActionSchema, gr as workDecisionDigest, gt as manualUploadSchema, h as hideScopeDigest, hi as producerVersion, hr as retireWorkDescription, ht as manualUploadPrepareSchema, ii as isIncludedWork, j as accountProfileSchema, jn as verifiedScanGitSeeds, k as renew, kn as scanGitSeedsId, kt as currentJourneyAdapters, li as withReadingProgress, lr as confirmedHideKinds, lt as artifactFailureDetailSchema, m as activeJourneyTerms, mt as manualUploadChoiceSchema, ni as hasCompleteWorkDescription, nt as settingsQuerySchema, oi as isSelectedWork, pi as selectionSchema, ri as isClassifying, ti as sendSettingsSchema, tt as settingsBatchSchema, ui as commandSchema, vi as recommendParallelism, vr as workCorrectionBinding, wt as sendChoiceSchema, xt as normalizeSensitiveTerms, yr as emptyPreparationFacts, yt as emptyTermsRevision, zn as applyClassification } from "./src-D1iONla3.mjs";
6
+ import { vt as isCoordinationBusy } from "./heap-CV8Eca8V.mjs";
7
+ import { d as checkOpenSource, h as openSource } from "./run-CaPktPhH.mjs";
8
+ import { t as createAgentSetupPort } from "./agent-setup-DdfF3zch.mjs";
8
9
  import { t as workerMessageSchema } from "./messages-D3m3bhva.mjs";
9
10
  import { constants, write } from "node:fs";
10
11
  import { z } from "zod";
@@ -92,7 +93,7 @@ function permissionDiagnostic(error) {
92
93
  function startupFailure(error, platform = process.platform) {
93
94
  const code = safeCode(error);
94
95
  const permission = permissionDiagnostic(error);
95
- const advice = code === "STATE_CLOSE_FAILED" ? " Keep the state directory intact and stop this CLI before retrying. Closing the state database could not be confirmed; state was not reset." : permission ? " Windows permissions could not be verified. Check that system Windows PowerShell can run and access the entry; retry after resolving the reported helper failure. Permissions were not established as unsafe." : code === "OBSOLETE_SCHEMA" ? " This control-plane database uses an older format. --overwrite-corrupted explicitly resets workflow progress, approvals and scan counts; source logs, completed ZIP files and private sensitive terms are retained." : code === "UNSAFE_STATE_PATH" ? platform === "win32" ? " State requires current-user ownership and a private Windows access control list. Repair permissions explicitly or use a private --state-dir." : " State requires current-user ownership, directory mode 0700 and file mode 0600. Repair permissions explicitly or use a private --state-dir." : ["CORRUPTED_STATE", "RESET_INCOMPLETE"].includes(code) ? " Inspect state first; --overwrite-corrupted explicitly resets only control-plane state." : code === "UNSUPPORTED_SCHEMA" ? " Stop this CLI, then run `npx --yes @gitroll/tacitt-cli@latest` with your original options to start the latest release. If you use the Skill, run `npx --yes @gitroll/tacitt-cli@latest skill.install`, then start a fresh conversation and invoke $tacitt in Codex or /tacitt in Claude Code. State was not reset." : code === "STATE_LAYOUT_INVALID" ? " Keep the state directory intact for inspection. Its storage layout cannot be recovered automatically; --overwrite-corrupted does not bypass this check." : code === "DEV_REQUIRES_SOURCE_CHECKOUT" ? " --dev serves the repository UI through Vite and only works from a source checkout; run without --dev." : code === "ARTIFACT_OUTPUT_UNSAFE" ? " Use a writable private --bundle-dir or repair the configured bundle library. The saved directory was not changed." : code === "STATE_IN_USE" ? " Another tacitt process still has this state open. Stop the other tacitt process and retry; state was not reset." : error instanceof UnfinishedJourneyError ? unfinishedJourneyAdvice(error) : " Run --help for usage.";
96
+ const advice = code === "STATE_CLOSE_FAILED" ? " Keep the state directory intact and stop this CLI before retrying. Closing the state database could not be confirmed; state was not reset." : permission ? " Windows permissions could not be verified. Check that system Windows PowerShell can run and access the entry; retry after resolving the reported helper failure. Permissions were not established as unsafe." : code === "OBSOLETE_SCHEMA" ? " This control-plane database uses an older format. --overwrite-corrupted explicitly resets workflow progress, approvals and scan counts; source logs, completed ZIP files and private sensitive terms are retained." : code === "UNSAFE_STATE_PATH" ? platform === "win32" ? " State requires current-user ownership and a private Windows access control list. Repair permissions explicitly or use a private --state-dir." : " State requires current-user ownership, directory mode 0700 and file mode 0600. Repair permissions explicitly or use a private --state-dir." : ["CORRUPTED_STATE", "RESET_INCOMPLETE"].includes(code) ? " Inspect state first; --overwrite-corrupted explicitly resets only control-plane state." : code === "UNSUPPORTED_SCHEMA" ? " Stop this CLI, then run the same command through `npx --yes @gitroll/tacitt-cli@latest` to use the latest release. If you use the Skill, run `npx --yes @gitroll/tacitt-cli@latest skill.install`, then start a fresh conversation and invoke $tacitt in Codex or /tacitt in Claude Code. State was not reset." : code === "STATE_LAYOUT_INVALID" ? " Keep the state directory intact for inspection. Its storage layout cannot be recovered automatically; --overwrite-corrupted does not bypass this check." : code === "DEV_REQUIRES_SOURCE_CHECKOUT" ? " --dev serves the repository UI through Vite and only works from a source checkout; run without --dev." : code === "ARTIFACT_OUTPUT_UNSAFE" ? " Use a writable private --bundle-dir or repair the configured bundle library. The saved directory was not changed." : code === "STATE_IN_USE" ? " Another tacitt process still has this state open. Stop the other tacitt process and retry; state was not reset." : error instanceof UnfinishedJourneyError ? unfinishedJourneyAdvice(error) : " Run --help for usage.";
96
97
  return `${code}.${code === "OPERATION_FAILED" ? failureCause(error) : ""}${permission}${advice}\n`;
97
98
  }
98
99
  const safeJourneyId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
@@ -141,6 +142,7 @@ const businessCommands = [
141
142
  "journey.findingImpact",
142
143
  "journey.files",
143
144
  "journey.prepare",
145
+ "review.reopenEmpty",
144
146
  "review.terms",
145
147
  "review.status",
146
148
  "review.guidance",
@@ -342,13 +344,14 @@ function parseOptions(args, source = sourceCheckout) {
342
344
  }
343
345
  const fullHelp = `tacitt — review and package local session logs
344
346
 
345
- Usage: tacitt [--no-open] [--state-dir PATH] [--bundle-dir PATH] [--parallelism N]
346
- [--since DATE] [--until DATE]
347
- tacitt state dump [--state-dir PATH] [--output PATH]
348
- tacitt status [--state-dir PATH]
347
+ Usage: tacitt skill.install [--agent codex|claude|all] [--scope user|project] [--cli-path PATH]
349
348
  tacitt PROCEDURE [--input FILE|-] [--async] [--timeout-ms N]
350
349
  tacitt operation.get|operation.wait|operation.cancel ID [--timeout-ms N]
351
- tacitt skill.install [--agent codex|claude|all] [--scope user|project] [--cli-path PATH]
350
+ tacitt state dump [--state-dir PATH] [--output PATH]
351
+ tacitt status [--state-dir PATH]
352
+
353
+ Tacitt has three surfaces: the Skill (/tacitt in Claude Code, $tacitt in Codex), the Skill
354
+ panel that the Skill opens, and the CLI commands below. Bare tacitt prints how to start the Skill.
352
355
 
353
356
  Business procedures (JSON output):
354
357
  ${Array.from(businessCommands).join("\n")}
@@ -372,12 +375,11 @@ Workspace ZIPs and collection indexes use the saved bundle library directory.
372
375
  Native Skill Send uploads only the confirmed primary artifacts to the configured Tacitt profile API.
373
376
  status prints safe workspace outcomes from the latest saved packaging operation.
374
377
  Private packaging error details stay in <state-dir>/diagnostics/ (up to 4 MiB).
375
- Sign in from the Web UI to link this tool to your GitRoll Identity account; the CLI contacts
378
+ Sign in from the Skill panel to link this tool to your GitRoll Identity account; the CLI contacts
376
379
  only the Identity and profile API origins below, only when you ask, and keeps the issued
377
380
  token in <state-dir>/account/ (owner-only).
378
381
 
379
382
  Options:
380
- --no-open Print the URL without opening a browser
381
383
  --agent CLIENT skill.install only: codex, claude or all (default: all)
382
384
  --scope SCOPE skill.install only: user or project (default: user)
383
385
  --cli-path PATH skill.install only: bind an absolute installed package executable
@@ -388,10 +390,7 @@ Options:
388
390
  --claude-home PATH Override the Claude configuration root
389
391
  --codex-home PATH Override the Codex configuration root
390
392
  --overwrite-corrupted Explicitly reset unusable control-plane state
391
- --parallelism N Configured packaging worker count (default: the Package dialog recommendation)
392
- --since DATE Manual: preselect only sessions last active on or after DATE
393
- --until DATE Manual: preselect only sessions last active on or before DATE
394
- (DATE: YYYY-MM-DD local day, inclusive, or an ISO timestamp)
393
+ --parallelism N Preparation and packaging worker count (default: recommended for this computer)
395
394
  --identity-url URL Identity origin for sign-in (default: ${accountDefaults.identityUrl})
396
395
  --identity-client-id ID Public device client ID registered for this tool
397
396
  --profile-api-url URL Profile API origin the token is checked against
@@ -617,12 +616,13 @@ async function scanRemoteDispatches(sessions, signal) {
617
616
  const pending = /* @__PURE__ */ new Map();
618
617
  for (const part of session.parts) {
619
618
  signal.throwIfAborted();
620
- if (part.representation !== "jsonl") throw new Error("REMOTE_SOURCE_FORMAT_UNSUPPORTED");
621
- const file = await open(part.path, constants.O_RDONLY | constants.O_NOFOLLOW);
619
+ const file = await openSource(part);
622
620
  try {
623
- const before = await file.stat({ bigint: true });
624
- if (String(before.dev) !== part.snapshot.dev || String(before.ino) !== part.snapshot.ino || before.size !== BigInt(part.snapshot.size) || String(before.mtimeNs) !== part.snapshot.mtimeNs || String(before.ctimeNs) !== part.snapshot.ctimeNs) throw new Error("REMOTE_SOURCE_CHANGED");
625
- if (!part.snapshot.size) continue;
621
+ if (!part.snapshot.size) {
622
+ if (part.representation === "jsonl.zst") throw new Error("ARTIFACT_PROCESSING_DECODE_FAILED");
623
+ await checkOpenSource(part, file);
624
+ continue;
625
+ }
626
626
  const stream = file.createReadStream({
627
627
  autoClose: false,
628
628
  start: 0,
@@ -637,7 +637,7 @@ async function scanRemoteDispatches(sessions, signal) {
637
637
  unreadableLines++;
638
638
  cause ??= /* @__PURE__ */ new Error(`line ${location.line}, invalid UTF-8`);
639
639
  };
640
- const lines = frameRecords(numberedChunks(stream, location), "jsonl", 16777216, signal);
640
+ const lines = frameRecords(numberedChunks(part.representation === "jsonl.zst" ? decodeZstd(stream, signal) : stream, location), "jsonl", 16777216, signal);
641
641
  try {
642
642
  for await (const framed of skipLiveTail(lines, location, skipped)) {
643
643
  signal.throwIfAborted();
@@ -671,11 +671,10 @@ async function scanRemoteDispatches(sessions, signal) {
671
671
  end: end && end >= at ? end : at
672
672
  }));
673
673
  windows.push(...found);
674
- pending.set(command.id, found);
674
+ if (found.length) pending.set(command.id, found);
675
675
  }
676
676
  }
677
- const after = await file.stat({ bigint: true });
678
- if (after.mtimeNs !== before.mtimeNs || after.size !== before.size) throw new Error("REMOTE_SOURCE_CHANGED");
677
+ await checkOpenSource(part, file);
679
678
  } finally {
680
679
  stream.destroy();
681
680
  }
@@ -743,7 +742,10 @@ function createRemotePreflight(options) {
743
742
  } catch (error) {
744
743
  signal.throwIfAborted();
745
744
  options.reportFailure(error);
746
- state.failures.push("source-scan");
745
+ if (!state.failures.includes("source-scan")) state.failures.push("source-scan");
746
+ const reason = sourceFailureReason(error);
747
+ const counts = state.sourceFailureCounts ??= {};
748
+ counts[reason] = (counts[reason] ?? 0) + 1;
747
749
  }
748
750
  await saveRemoteState(options.stateDir, input.journeyId, state);
749
751
  }
@@ -780,9 +782,23 @@ function result(state) {
780
782
  })),
781
783
  copied: state.copied.length,
782
784
  failures: state.failures,
785
+ ...state.sourceFailureCounts ? { sourceFailureCounts: state.sourceFailureCounts } : {},
783
786
  ...state.failures.some((failure) => ["source-lines-unreadable", "source-scan"].includes(failure)) ? { warning: state.failures.includes("source-scan") ? "Some remote sources could not be scanned; remote worker discovery may be incomplete." : "Some remote dispatch lines were unreadable; remote worker discovery may be incomplete." } : {}
784
787
  };
785
788
  }
789
+ function sourceFailureReason(error) {
790
+ if (!(error instanceof Error)) return "other";
791
+ if (error instanceof SyntaxError) return "invalid-json";
792
+ if (error.message === "SOURCE_CHANGED") return "source-changed";
793
+ if (error.message === "UNSAFE_SOURCE_PATH") return "source-inaccessible";
794
+ if (error.message === "ARTIFACT_PROCESSING_DECODE_FAILED") return "compressed-decode";
795
+ const code = "code" in error ? error.code : null;
796
+ if (code === "PROCESSING_LIMIT") return "record-limit";
797
+ if (code === "PROCESSING_INVALID_RECORD") return "invalid-utf8";
798
+ if (code === "ENOENT" || code === "ENOTDIR") return "source-missing";
799
+ if (code === "EACCES" || code === "EPERM" || code === "ELOOP") return "source-inaccessible";
800
+ return "other";
801
+ }
786
802
  //#endregion
787
803
  //#region apps/cli/src/journey/confirmed-hides.ts
788
804
  /** The Work's explicit exception values; an occurrence inside one stays visible (ADR 0069). */
@@ -1144,7 +1160,10 @@ function bindJourneyDestination(storage, destination, options = {}) {
1144
1160
  return state;
1145
1161
  };
1146
1162
  const guard = async (id) => {
1147
- if (!verified.has(id)) await current(id);
1163
+ if (verified.has(id)) return;
1164
+ const permission = await storage.preparationPermission(id);
1165
+ if (permission && permission.destination !== destination) throw new ApplicationError("JOURNEY_DESTINATION_CHANGED");
1166
+ if (permission) verified.add(id);
1148
1167
  };
1149
1168
  const readStatus = async (input) => {
1150
1169
  await guard(input.journeyId);
@@ -1152,6 +1171,18 @@ function bindJourneyDestination(storage, destination, options = {}) {
1152
1171
  };
1153
1172
  return {
1154
1173
  current,
1174
+ async preparationState(id) {
1175
+ await guard(id);
1176
+ return storage.preparationState(id);
1177
+ },
1178
+ async preparationPermission(id) {
1179
+ await guard(id);
1180
+ return storage.preparationPermission(id);
1181
+ },
1182
+ async updatePreparation(input) {
1183
+ await guard(input.journeyId);
1184
+ return storage.updatePreparation(input);
1185
+ },
1155
1186
  readInventory: (input) => storage.readInventory({
1156
1187
  ...input,
1157
1188
  expectedDestination: destination
@@ -1270,6 +1301,10 @@ function bindJourneyDestination(storage, destination, options = {}) {
1270
1301
  await guard(input.journeyId);
1271
1302
  return storage.submitReview(input);
1272
1303
  },
1304
+ async reopenEmpty(input) {
1305
+ await guard(input.journeyId);
1306
+ return storage.reopenEmpty(input);
1307
+ },
1273
1308
  async recordTerms(input) {
1274
1309
  await guard(input.journeyId);
1275
1310
  return storage.recordTerms(input);
@@ -1321,6 +1356,31 @@ function bindSendDestination(storage, destination) {
1321
1356
  };
1322
1357
  }
1323
1358
  //#endregion
1359
+ //#region apps/cli/src/journey/prepare/update.ts
1360
+ /** Preparation owns only the changed Work rows, progress and package invalidation. */
1361
+ async function changePreparation(storage, journeyId, kind, change, attempts = Number.POSITIVE_INFINITY, clearPackageApproval = false) {
1362
+ for (let attempt = 0; attempt < attempts; attempt++) {
1363
+ const before = await storage.preparationState(journeyId);
1364
+ if (!before?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
1365
+ const after = change(before);
1366
+ const previous = new Map(before.works.map((work) => [work.id, work]));
1367
+ const result = await storage.updatePreparation({
1368
+ journeyId,
1369
+ expectedRevision: before.revision,
1370
+ requestKey: randomUUID(),
1371
+ at: (/* @__PURE__ */ new Date()).toISOString(),
1372
+ kind,
1373
+ works: after.works.filter((work) => work !== previous.get(work.id)),
1374
+ phase: after.phase,
1375
+ readingComplete: after.readingComplete,
1376
+ readingStartedAt: after.readingStartedAt,
1377
+ clearPackageApproval: clearPackageApproval && before !== after
1378
+ });
1379
+ if (result.status !== "stale") return result.state;
1380
+ }
1381
+ throw new ApplicationError("REVISION_CONFLICT");
1382
+ }
1383
+ //#endregion
1324
1384
  //#region apps/cli/src/journey/works/metadata.ts
1325
1385
  /** Original classification fields remain durable; only the derived display cache is replaced. */
1326
1386
  const classificationMetadataInput = (work) => ({
@@ -1368,7 +1428,7 @@ async function prepareClassificationMetadata(storage, state, work, processing, s
1368
1428
  fields: Object.values(raw).map((text) => ({ text }))
1369
1429
  }, signal);
1370
1430
  if (title === void 0 || projectName === void 0 || description === void 0 || reason === void 0) throw new ApplicationError("CORRUPTED_STATE");
1371
- await changeJourney(storage, state.journeyId, "snapshot", (before) => {
1431
+ await changePreparation(storage, state.journeyId, "snapshot", (before) => {
1372
1432
  const active = before.works.find((item) => item.id === work.id);
1373
1433
  if (before.scope.status !== "confirmed" || !active || textDigest(JSON.stringify([
1374
1434
  active.inputRevision,
@@ -1699,7 +1759,7 @@ async function isolateFailingRoots(storage, journeyId, work, error) {
1699
1759
  });
1700
1760
  if (roots.length === 0 || roots.length === work.rootSessionIds.length) return false;
1701
1761
  for (;;) {
1702
- const state = await storage.current(journeyId);
1762
+ const state = await storage.preparationState(journeyId);
1703
1763
  const current = state?.works.find((item) => item.id === work.id);
1704
1764
  if (!state || state.phase === "paused" || state.pendingHideId || current?.inputRevision !== work.inputRevision || current.status !== "pending") return false;
1705
1765
  if ((await storage.isolateRoots({
@@ -1801,7 +1861,7 @@ function scanFailureCode(reason) {
1801
1861
  }
1802
1862
  /** Resolve only the frozen inspection settings and captures; classification stays unchanged. */
1803
1863
  async function prepareScanContext(storage, journeyId, work, selected, processing, signal, workers) {
1804
- const current = await storage.current(journeyId);
1864
+ const current = await storage.preparationState(journeyId);
1805
1865
  if (!current?.consent || !current.snapshotId) throw new ApplicationError("CONFIRMATION_REQUIRED");
1806
1866
  const active = current.works.find((item) => item.id === work.id);
1807
1867
  if (current.scope.status !== "confirmed" || current.phase === "paused" || active?.access === "drop") throw new ApplicationError("OPERATION_CANCELED");
@@ -2127,7 +2187,7 @@ async function prepareScannedWork(storage, plan, work, directory, hooks, observa
2127
2187
  workspaceId: session.workspaceId,
2128
2188
  sessionId: session.id,
2129
2189
  ...failure.sourcePartId ? { sourcePartId: failure.sourcePartId } : {}
2130
- });
2190
+ }, void 0, failure.cause ?? new ApplicationError(scanFailureCode(failure.reason)));
2131
2191
  const root = families.owners.get(failure.sessionId);
2132
2192
  if (root) failures.set(root.id, failure.cause === void 0 ? scanFailureCode(failure.reason) : safeCode(failure.cause));
2133
2193
  }
@@ -2153,7 +2213,7 @@ async function prepareScannedWork(storage, plan, work, directory, hooks, observa
2153
2213
  ...proof,
2154
2214
  rootSessionIds: excludedRoots
2155
2215
  } : void 0;
2156
- await changeJourney(storage, plan.journeyId, "batch", (state) => {
2216
+ await changePreparation(storage, plan.journeyId, "batch", (state) => {
2157
2217
  const target = state.works.find((item) => item.id === work.id);
2158
2218
  if (state.scope.status !== "confirmed" || state.phase === "paused" || target?.access === "drop") throw new ApplicationError("OPERATION_CANCELED");
2159
2219
  if (target?.inputRevision !== work.inputRevision) throw new ApplicationError("REVISION_CONFLICT");
@@ -2212,7 +2272,7 @@ async function observePreparation(storage, input, observation, operation) {
2212
2272
  //#endregion
2213
2273
  //#region apps/cli/src/journey/prepare/work.ts
2214
2274
  async function prepareJourneyWork(storage, plan, work, directory, processing, hooks, workers, onGeneration, onSubmitted) {
2215
- let state = await storage.current(plan.journeyId);
2275
+ let state = await storage.preparationState(plan.journeyId);
2216
2276
  if (!state?.consent || !state.snapshotId) throw new ApplicationError("CONFIRMATION_REQUIRED");
2217
2277
  if (state.scope.status !== "confirmed" || state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
2218
2278
  const selected = workApproval(plan.approval, work.rootSessionIds);
@@ -2270,9 +2330,9 @@ async function prepareJourneyWork(storage, plan, work, directory, processing, ho
2270
2330
  context ??= await resolveContext();
2271
2331
  return prepareScannedWork(storage, plan, work, directory, {
2272
2332
  ...hooks,
2273
- onFailureLocation(location, detail) {
2333
+ onFailureLocation(location, detail, cause) {
2274
2334
  observation.location(location);
2275
- hooks.onFailureLocation?.(location, detail);
2335
+ hooks.onFailureLocation?.(location, detail, cause);
2276
2336
  }
2277
2337
  }, observation, context, onSubmitted);
2278
2338
  });
@@ -2294,7 +2354,7 @@ async function prepareJourneyWork(storage, plan, work, directory, processing, ho
2294
2354
  */
2295
2355
  async function prepareStaticWork(options) {
2296
2356
  const { storage, journeyId, signal } = options;
2297
- const state = await storage.current(journeyId);
2357
+ const state = await storage.preparationState(journeyId);
2298
2358
  const work = state?.works.find((item) => item.id === options.workId);
2299
2359
  if (!state?.snapshotId || !work || !isSelectedWork(work) || !work.rootSessionIds.length) return;
2300
2360
  if (![
@@ -2366,7 +2426,7 @@ async function prepareStaticWork(options) {
2366
2426
  if (failure) throw failure.error;
2367
2427
  signal.throwIfAborted();
2368
2428
  if (resources.length !== jobs.length) throw new ApplicationError("SCOPE_NOT_READY");
2369
- await changeJourney(storage, journeyId, "batch", (current) => {
2429
+ await changePreparation(storage, journeyId, "batch", (current) => {
2370
2430
  const target = current.works.find((item) => item.id === work.id);
2371
2431
  if (current.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
2372
2432
  if (current.snapshotId !== snapshotId || target?.inputRevision !== work.inputRevision) throw new ApplicationError("REVISION_CONFLICT");
@@ -2403,7 +2463,7 @@ function observeJourneyPause(storage, journeyId, processing, onFailure, requireC
2403
2463
  const closing = new AbortController();
2404
2464
  async function readPermission() {
2405
2465
  for (let attempt = 1;; attempt++) try {
2406
- return await storage.current(journeyId);
2466
+ return await storage.preparationPermission(journeyId);
2407
2467
  } catch (error) {
2408
2468
  if (safeCode(error) !== "COORDINATION_BUSY" || attempt === 4) throw error;
2409
2469
  onFailure(error);
@@ -2476,6 +2536,16 @@ const preparationFailureBindingsSchema = z.object({
2476
2536
  workId: z.uuid(),
2477
2537
  inputRevision: z.number().int().positive().max(Number.MAX_SAFE_INTEGER)
2478
2538
  }).strict();
2539
+ /** Issue #59: the named fields are exactly what siteFields keeps, so no other text passes. */
2540
+ const siteShape = {
2541
+ site: z.string().optional(),
2542
+ harness: z.string().optional(),
2543
+ harnessVersion: z.string().optional()
2544
+ };
2545
+ const onlySiteFields = (value) => {
2546
+ const kept = siteFields(value);
2547
+ return kept?.site === value.site && kept?.harness === value.harness && kept?.harnessVersion === value.harnessVersion;
2548
+ };
2479
2549
  const preparationSourceFailureSchema = z.object({
2480
2550
  location: z.object({
2481
2551
  workspaceId: z.uuid(),
@@ -2483,10 +2553,19 @@ const preparationSourceFailureSchema = z.object({
2483
2553
  sourcePartId: z.uuid().optional()
2484
2554
  }).strict(),
2485
2555
  rootSessionId: z.uuid().nullable(),
2486
- detail: artifactFailureDetailSchema.nullable()
2487
- }).strict().refine(({ location, rootSessionId, detail }) => (location.sessionId === void 0 ? rootSessionId === null && location.sourcePartId === void 0 : rootSessionId !== null) && (detail === null || location.sourcePartId !== void 0));
2556
+ detail: artifactFailureDetailSchema.nullable(),
2557
+ ...siteShape
2558
+ }).strict().refine(onlySiteFields).refine(({ location, rootSessionId, detail }) => (location.sessionId === void 0 ? rootSessionId === null && location.sourcePartId === void 0 : rootSessionId !== null) && (detail === null || location.sourcePartId !== void 0));
2559
+ /** One count per refusal group over every reported source, unlike the bounded list. */
2560
+ const preparationFailureCountSchema = z.object({
2561
+ code: z.string().regex(/^[A-Z_]{3,80}$/u).optional(),
2562
+ ...siteShape,
2563
+ count: z.number().int().positive().max(Number.MAX_SAFE_INTEGER)
2564
+ }).strict().refine(onlySiteFields);
2488
2565
  const preparationFailureContextSchema = preparationFailureBindingsSchema.extend({
2489
2566
  sourceFailures: z.array(preparationSourceFailureSchema).max(16),
2567
+ sourceFailureCounts: z.array(preparationFailureCountSchema).max(32),
2568
+ /** The list or the counts left something out; the counts cover every source otherwise. */
2490
2569
  truncated: z.boolean()
2491
2570
  }).refine((context) => Buffer.byteLength(JSON.stringify(context)) <= maximumPreparationContextBytes);
2492
2571
  /** Invalid diagnostic input must never prevent the independent original-error write. */
@@ -2506,16 +2585,19 @@ function collectPreparationFailures(inventory, rootSessionIds, bindings) {
2506
2585
  let context;
2507
2586
  let families;
2508
2587
  const roots = new Set(rootSessionIds);
2588
+ const counted = [];
2509
2589
  try {
2510
2590
  const parsed = preparationFailureBindingsSchema.safeParse(bindings);
2511
2591
  if (parsed.success) context = {
2512
2592
  ...parsed.data,
2513
2593
  sourceFailures: [],
2594
+ sourceFailureCounts: [],
2514
2595
  truncated: false
2515
2596
  };
2516
2597
  } catch {}
2517
2598
  return {
2518
- report(location, detail) {
2599
+ /** `cause` contributes only its code and the fixed site fields (siteFields), never text. */
2600
+ report(location, detail, cause) {
2519
2601
  if (!context) return;
2520
2602
  try {
2521
2603
  const checked = validateFailureLocation(inventory, location);
@@ -2527,11 +2609,20 @@ function collectPreparationFailures(inventory, rootSessionIds, bindings) {
2527
2609
  sessionId: root.id
2528
2610
  }))) return;
2529
2611
  if (checked.sessionId === void 0 && !inventory.sessions.some((session) => session.workspaceId === checked.workspaceId && roots.has(families?.owners.get(session.id)?.id ?? ""))) return;
2612
+ const named = processingSite(cause);
2613
+ if (!counted.some((seen) => contains(seen, checked) || contains(checked, seen))) {
2614
+ counted.push(checked);
2615
+ context = countFailure(context, {
2616
+ ...cause === void 0 ? {} : { code: safeCode(cause) },
2617
+ ...named
2618
+ });
2619
+ }
2530
2620
  const facts = artifactFailureDetailSchema.safeParse(detail);
2531
2621
  const parsed = preparationSourceFailureSchema.safeParse({
2532
2622
  location: checked,
2533
2623
  rootSessionId: root?.id ?? null,
2534
- detail: facts.success ? facts.data : null
2624
+ detail: facts.success ? facts.data : null,
2625
+ ...named
2535
2626
  });
2536
2627
  if (!parsed.success) return;
2537
2628
  const entry = parsed.data;
@@ -2556,13 +2647,50 @@ function collectPreparationFailures(inventory, rootSessionIds, bindings) {
2556
2647
  }
2557
2648
  };
2558
2649
  }
2650
+ /** Adds one source to its (code, site, harness, version) group; a group past the bound truncates. */
2651
+ function countFailure(context, group) {
2652
+ const same = JSON.stringify({
2653
+ ...group,
2654
+ count: 0
2655
+ });
2656
+ const index = context.sourceFailureCounts.findIndex((entry) => JSON.stringify({
2657
+ ...entry,
2658
+ count: 0
2659
+ }) === same);
2660
+ if (index < 0 && context.sourceFailureCounts.length >= 32) return {
2661
+ ...context,
2662
+ truncated: true
2663
+ };
2664
+ const next = {
2665
+ ...context,
2666
+ sourceFailureCounts: index < 0 ? [...context.sourceFailureCounts, {
2667
+ ...group,
2668
+ count: 1
2669
+ }] : context.sourceFailureCounts.map((entry, at) => at === index ? {
2670
+ ...entry,
2671
+ count: entry.count + 1
2672
+ } : entry)
2673
+ };
2674
+ return readPreparationFailureContext(next) ? next : {
2675
+ ...context,
2676
+ truncated: true
2677
+ };
2678
+ }
2559
2679
  //#endregion
2560
2680
  //#region apps/cli/src/journey/prepare.ts
2681
+ const pendingSelected = (work) => work.status === "pending" && isSelectedWork(work);
2682
+ /** A retried Work is pending even if a previous generation produced a static receipt. */
2683
+ function initialScanWorks(state, scanRequested) {
2684
+ if (!scanRequested || state.scope.status !== "confirmed") return [];
2685
+ const selected = state.works.filter(isSelectedWork);
2686
+ if (!selected.some(pendingSelected)) return selected;
2687
+ return selected.filter((work) => work.status === "pending" || ["reading", "review"].includes(work.status) && !hasCurrentStaticReview(state, work));
2688
+ }
2561
2689
  function createJourneyPreparation(options) {
2562
2690
  return async (journeyId, signal) => {
2563
2691
  const { storage } = options;
2564
2692
  if (signal?.aborted) throw new ApplicationError("OPERATION_CANCELED");
2565
- const current = await storage.current(journeyId);
2693
+ const current = await storage.preparationState(journeyId);
2566
2694
  if (!current?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
2567
2695
  if (obsoleteJourney(current)) throw new ApplicationError("JOURNEY_OBSOLETE");
2568
2696
  if (current.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
@@ -2585,10 +2713,11 @@ function createJourneyPreparation(options) {
2585
2713
  terms: []
2586
2714
  };
2587
2715
  const plan = await prepareSourcePlan(storage, options.harnesses, journeyId, terms, abort.signal, current.period, options.stateDir);
2588
- const initial = await storage.current(journeyId);
2716
+ const initial = await storage.preparationState(journeyId);
2589
2717
  if (!initial?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
2590
2718
  const formed = initial.snapshotId ? [] : namedJourneyWorks(formJourneyWorks(plan.approval, options.processing.environment), options.processing, abort.signal);
2591
- const state = await changeJourney(storage, journeyId, "snapshot", (before) => ({
2719
+ let state;
2720
+ if (!initial.snapshotId) state = await changeJourney(storage, journeyId, "snapshot", (before) => ({
2592
2721
  ...before,
2593
2722
  snapshotId: before.snapshotId ?? textDigest(JSON.stringify(plan)),
2594
2723
  ...before.snapshotId ? {} : {
@@ -2596,17 +2725,22 @@ function createJourneyPreparation(options) {
2596
2725
  reviewMethod: "dictionary"
2597
2726
  },
2598
2727
  works: before.snapshotId ? before.works : formed,
2599
- phase: before.phase === "paused" ? "paused" : !before.snapshotId || before.works.some((work) => work.status === "pending" && isSelectedWork(work)) ? "processing" : before.phase
2728
+ phase: before.phase !== "paused" && (!before.snapshotId || before.works.some(pendingSelected)) ? "processing" : before.phase
2600
2729
  }));
2730
+ else if (initial.phase !== "processing" && initial.phase !== "paused" && initial.works.some(pendingSelected)) state = await changePreparation(storage, journeyId, "snapshot", (before) => before.phase !== "paused" && before.works.some(pendingSelected) ? {
2731
+ ...before,
2732
+ phase: "processing"
2733
+ } : before);
2734
+ else state = initial;
2601
2735
  if (!state.works.length) throw new ApplicationError("INVALID_SELECTION");
2602
2736
  if (!scanRequested) await captureSortingScope(storage, journeyId, workApproval(plan.approval, state.works.flatMap((work) => work.rootSessionIds)), directory, abort.signal);
2603
- const scanWorks = scanRequested && state.scope.status === "confirmed" ? state.works : [];
2737
+ const scanWorks = initialScanWorks(state, scanRequested);
2604
2738
  if (scanWorks.length) run = await runWorkers(options.workers);
2605
2739
  const frozen = state;
2606
2740
  const blockedForRetry = new Set(state.works.filter((work) => work.status === "blocked").map((work) => `${work.id}:${work.inputRevision}`));
2607
2741
  const deferredForHide = /* @__PURE__ */ new Set();
2608
2742
  const blockWork = async (work, error) => {
2609
- const blocked = await changeJourney(storage, journeyId, "snapshot", (before) => {
2743
+ const blocked = await changePreparation(storage, journeyId, "snapshot", (before) => {
2610
2744
  if (before.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
2611
2745
  if (before.phase === "sending" || before.phase === "completed") return before;
2612
2746
  return withReadingProgress({
@@ -2645,7 +2779,7 @@ function createJourneyPreparation(options) {
2645
2779
  abort.signal.throwIfAborted();
2646
2780
  let resumed = false;
2647
2781
  let deferred = false;
2648
- const committed = await changeJourney(storage, journeyId, "snapshot", (before) => {
2782
+ const committed = await changePreparation(storage, journeyId, "snapshot", (before) => {
2649
2783
  resumed = false;
2650
2784
  deferred = false;
2651
2785
  const target = before.works.find((item) => item.id === work.id);
@@ -2654,17 +2788,13 @@ function createJourneyPreparation(options) {
2654
2788
  deferred = !resumed;
2655
2789
  return withReadingProgress({
2656
2790
  ...before,
2657
- privacy: {
2658
- ...before.privacy,
2659
- packageApproval: null
2660
- },
2661
2791
  works: before.works.map((item) => item.id === work.id ? {
2662
2792
  ...item,
2663
2793
  status: resumed ? work.status : "pending",
2664
2794
  blockedCodes: []
2665
2795
  } : item)
2666
2796
  });
2667
- }, 100);
2797
+ }, 100, true);
2668
2798
  if (deferred && committed.pendingHideId && committed.works.some((item) => generation(item) === generation(work) && item.status === "pending")) deferredForHide.add(generation(work));
2669
2799
  return resumed;
2670
2800
  };
@@ -2684,7 +2814,7 @@ function createJourneyPreparation(options) {
2684
2814
  };
2685
2815
  /** D2: the person dropped this Work while it was prepared; the other Works go on. */
2686
2816
  const droppedMidScan = async (workId) => {
2687
- const now = await storage.current(journeyId);
2817
+ const now = await storage.preparationState(journeyId);
2688
2818
  const work = now?.works.find((item) => item.id === workId);
2689
2819
  return Boolean(now && now.phase !== "paused" && now.scope.status === "confirmed" && work && !isSelectedWork(work));
2690
2820
  };
@@ -2693,7 +2823,7 @@ function createJourneyPreparation(options) {
2693
2823
  /** True when failing roots left the Work, which is then prepared again (ADR 0188). */
2694
2824
  const prepareWorkAttempt = async (planned, onSubmitted, releaseRead) => {
2695
2825
  abort.signal.throwIfAborted();
2696
- const latest = await storage.current(journeyId) ?? frozen;
2826
+ const latest = await storage.preparationState(journeyId) ?? frozen;
2697
2827
  abort.signal.throwIfAborted();
2698
2828
  if (latest.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
2699
2829
  let work = latest.works.find((item) => item.id === planned.id);
@@ -2717,7 +2847,7 @@ function createJourneyPreparation(options) {
2717
2847
  signal: abort.signal,
2718
2848
  onTemporary: async () => {},
2719
2849
  onPhase: async () => {},
2720
- onFailureLocation: (location) => failures.report(location),
2850
+ onFailureLocation: (location, _detail, cause) => failures.report(location, void 0, cause),
2721
2851
  onTiming: (timing) => options.onTiming?.(planned.id, timing)
2722
2852
  }, run.workers, (revised) => {
2723
2853
  work = revised;
@@ -2745,7 +2875,7 @@ function createJourneyPreparation(options) {
2745
2875
  readRetries.set(generation(work), attempt + 1);
2746
2876
  return prepareWorkOnce(planned, onSubmitted);
2747
2877
  }
2748
- const now = await storage.current(journeyId);
2878
+ const now = await storage.preparationState(journeyId);
2749
2879
  if (now?.phase === "sending" || now?.phase === "completed") return false;
2750
2880
  if (blockedCodes(error, code).includes("PRIVACY_CHANGE_PENDING")) {
2751
2881
  if (work) await blockWork(work, error);
@@ -2775,7 +2905,7 @@ function createJourneyPreparation(options) {
2775
2905
  const staticWork = async (planned, passSubmitted) => {
2776
2906
  await passSubmitted;
2777
2907
  abort.signal.throwIfAborted();
2778
- const latest = await storage.current(journeyId);
2908
+ const latest = await storage.preparationState(journeyId);
2779
2909
  abort.signal.throwIfAborted();
2780
2910
  if (!latest?.consent || latest.phase === "paused" || latest.scope.status !== "confirmed") throw new ApplicationError("OPERATION_CANCELED");
2781
2911
  const work = latest.works.find((item) => item.id === planned.id);
@@ -2803,7 +2933,7 @@ function createJourneyPreparation(options) {
2803
2933
  staticRetries.set(generation(work), attempt + 1);
2804
2934
  return staticWork(planned, passSubmitted);
2805
2935
  }
2806
- const now = await storage.current(journeyId);
2936
+ const now = await storage.preparationState(journeyId);
2807
2937
  if (now?.phase !== "sending" && now?.phase !== "completed" && (attempt >= retryLimit || blockedCodes(error, safeCode(error)).includes("PRIVACY_CHANGE_PENDING"))) await blockWork(work, error);
2808
2938
  }
2809
2939
  };
@@ -2881,7 +3011,7 @@ function createJourneyPreparation(options) {
2881
3011
  for (;;) {
2882
3012
  abort.signal.throwIfAborted();
2883
3013
  const settled = workSettled.promise;
2884
- const latest = await storage.current(journeyId);
3014
+ const latest = await storage.preparationState(journeyId);
2885
3015
  if (!latest || latest.phase === "paused" || latest.scope.status !== "confirmed") return [];
2886
3016
  for (const work of latest.works) if (work.status === "blocked") blockedForRetry.add(generation(work));
2887
3017
  const ready = latest.works.filter((work) => isSelectedWork(work) && [
@@ -2902,7 +3032,8 @@ function createJourneyPreparation(options) {
2902
3032
  }
2903
3033
  };
2904
3034
  try {
2905
- for (let batch = scanWorks; batch.length && !fatal; batch = await later()) {
3035
+ const firstBatch = scanRequested && !scanWorks.length && isClassifying(state) ? await later() : scanWorks;
3036
+ for (let batch = firstBatch; batch.length && !fatal; batch = await later()) {
2906
3037
  if (!run.workers && !run.pool) run = await runWorkers(options.workers);
2907
3038
  await pass(batch);
2908
3039
  }
@@ -2921,7 +3052,7 @@ function createJourneyPreparation(options) {
2921
3052
  });
2922
3053
  if (fatal) throw fatal.error;
2923
3054
  abort.signal.throwIfAborted();
2924
- const result = await storage.current(journeyId);
3055
+ const result = await storage.preparationState(journeyId);
2925
3056
  await stopObserving();
2926
3057
  abort.signal.throwIfAborted();
2927
3058
  if (!result?.consent || result.phase === "paused" || scanRequested && result.scope.status !== "confirmed") throw new ApplicationError("OPERATION_CANCELED");
@@ -3582,7 +3713,7 @@ function assertScope(journey, send) {
3582
3713
  if (journey.pendingHideId) throw new ApplicationError("SCOPE_NOT_READY");
3583
3714
  if (journey.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
3584
3715
  if (scopeDigest(journey) !== send.scopeDigest) throw new ApplicationError("SEND_SCOPE_INVALIDATED");
3585
- assertJourneyPrivacyReady(journey, true);
3716
+ assertJourneyPrivacyReady(journey, true, { publicChoices: send.status !== "completed" });
3586
3717
  for (const item of send.items) {
3587
3718
  const work = journey.works.find((value) => value.id === item.workId);
3588
3719
  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");
@@ -3867,31 +3998,32 @@ function createJourneyRenderer(options) {
3867
3998
  async function renderScope(journeyId, works, signal) {
3868
3999
  const workers = options.workers;
3869
4000
  const size = workers ? workers.parallelism ?? renderWorkerCount() : 0;
3870
- if (!workers || works < 1 || size < 1) {
3871
- const retry = renderRetry(signal, Math.max(1, works));
3872
- return {
3873
- render: (workId) => render(journeyId, workId, signal, { retry }),
3874
- close: async () => void 0
3875
- };
3876
- }
3877
- const retry = renderRetry(signal, Math.min(works, size + 1));
3878
- const store = retry.store(storage);
4001
+ if (!workers || works < 1 || size < 1) return {
4002
+ render: (workId) => render(journeyId, workId, signal, { retry: renderRetry(signal, Math.max(1, Math.min(16, works))) }),
4003
+ close: async () => void 0
4004
+ };
3879
4005
  const pool = createRenderWorkers(workers, size);
3880
- const session = batchedRenderSession(store, journeyId);
4006
+ const session = batchedRenderSession(storage, journeyId);
3881
4007
  let plan;
3882
- const readPlan = () => plan ??= readJourneyPlan(store, journeyId).catch((error) => {
4008
+ const readPlan = () => plan ??= readJourneyPlan(storage, journeyId).catch((error) => {
3883
4009
  plan = void 0;
3884
4010
  throw error;
3885
4011
  });
3886
4012
  return {
3887
4013
  concurrency: size + 1,
3888
- render: (workId) => render(journeyId, workId, signal, {
3889
- write: pool.write,
3890
- session,
3891
- plan: readPlan,
3892
- writeBase: pool.writeBase,
3893
- retry
3894
- }),
4014
+ render: (workId) => {
4015
+ const retry = renderRetry(signal, Math.min(16, works, size + 1));
4016
+ return render(journeyId, workId, signal, {
4017
+ write: pool.write,
4018
+ session: {
4019
+ current: () => retry.run(() => session.current()),
4020
+ commit: (change) => retry.run(() => session.commit(change))
4021
+ },
4022
+ plan: () => retry.run(readPlan),
4023
+ writeBase: pool.writeBase,
4024
+ retry
4025
+ });
4026
+ },
3895
4027
  close: pool.close
3896
4028
  };
3897
4029
  }
@@ -4691,7 +4823,7 @@ async function resolveChunkSessions(storage, state, works, signal) {
4691
4823
  * read and applied by the store against the current state, so scan writes in between do not
4692
4824
  * refuse it (BL-216).
4693
4825
  */
4694
- function createClassificationProposal(storage) {
4826
+ function createClassificationProposal(storage, accountAccess) {
4695
4827
  return async (input, signal) => {
4696
4828
  signal.throwIfAborted();
4697
4829
  const state = await storage.current(input.journeyId);
@@ -4785,9 +4917,18 @@ function createClassificationProposal(storage) {
4785
4917
  };
4786
4918
  const { works } = classificationWorks(state, prepared);
4787
4919
  const chunkSessions = await resolveChunkSessions(storage, state, works, signal);
4920
+ const read = late ? await accountAccess?.({
4921
+ ...state,
4922
+ works
4923
+ }, signal) : void 0;
4924
+ const sameAccount = read?.status === "read" && state.accountRead?.status === "read" && state.accountRead.subject === read.subject;
4788
4925
  return storage.commitClassification({
4789
4926
  ...prepared,
4790
- chunkSessions
4927
+ chunkSessions,
4928
+ ...sameAccount ? {
4929
+ accountAccess: Object.fromEntries(read.access),
4930
+ accountSubject: read.subject
4931
+ } : read ? { accountRead: read.status === "read" ? "unavailable" : read.status } : {}
4791
4932
  });
4792
4933
  };
4793
4934
  }
@@ -5133,11 +5274,11 @@ function createJourneyClassification(storage, _processing) {
5133
5274
  //#endregion
5134
5275
  //#region apps/cli/src/journey/works/index.ts
5135
5276
  /** The journey local port's project catalog and project-type suggestions. */
5136
- function createJourneyWorks(storage, processing, stateDir) {
5277
+ function createJourneyWorks(storage, processing, stateDir, accountAccess) {
5137
5278
  return {
5138
5279
  classificationSessions: createClassificationSessions(storage, processing, stateDir),
5139
5280
  classificationRead: createClassificationReader(storage, stateDir),
5140
- classificationSubmit: createClassificationProposal(storage),
5281
+ classificationSubmit: createClassificationProposal(storage, accountAccess),
5141
5282
  catalog: createJourneyCatalog(storage, processing, stateDir),
5142
5283
  classify: createJourneyClassification(storage, processing)
5143
5284
  };
@@ -5231,6 +5372,46 @@ function protectedSourceText(field, hidden) {
5231
5372
  }
5232
5373
  //#endregion
5233
5374
  //#region apps/cli/src/journey/send-package.ts
5375
+ /** Explicit admission refusals must never become automatic packaging retries. */
5376
+ const packagingRefusalCodes = [
5377
+ "ARTIFACT_INVALID_APPROVAL",
5378
+ "ARTIFACT_IDENTITY_MISMATCH",
5379
+ "CONFIRMATION_REQUIRED",
5380
+ "INCOMPLETE_REVIEW",
5381
+ "PRIVACY_CHANGE_PENDING",
5382
+ "PRIVACY_UNCERTAIN",
5383
+ "REVISION_CONFLICT",
5384
+ "SCOPE_NOT_READY",
5385
+ "JOURNEY_OBSOLETE",
5386
+ "UNKNOWN_ITEM",
5387
+ "UNKNOWN_WORKSPACE",
5388
+ "OPERATION_CANCELED"
5389
+ ];
5390
+ /**
5391
+ * Deterministic failures a later attempt cannot clear: the refusals above, the source refusals
5392
+ * preparation isolates per root, size limits, a missing source and a full disk. Every other code,
5393
+ * including Windows sharing or antivirus locks, retries until the saved request expires.
5394
+ */
5395
+ const finalPackagingCodes = [
5396
+ ...packagingRefusalCodes,
5397
+ ...rootScanRefusals,
5398
+ "ARTIFACT_ARCHIVE_LIMIT",
5399
+ "ARTIFACT_ENTRY_LIMIT",
5400
+ "ARTIFACT_FILESYSTEM_LIMIT",
5401
+ "ARTIFACT_INDEX_LIMIT",
5402
+ "ARTIFACT_METADATA_LIMIT",
5403
+ "ARTIFACT_PART_LIMIT",
5404
+ "ARTIFACT_SOURCE_MISSING",
5405
+ "ARTIFACT_DISK_FULL",
5406
+ "ARTIFACT_INVALID_APPROVAL",
5407
+ "ARTIFACT_INVALID_ARCHIVE",
5408
+ "ARTIFACT_INVALID_MANIFEST",
5409
+ "ARTIFACT_OUTPUT_UNSAFE",
5410
+ "ARTIFACT_PROCESSING_MISMATCH",
5411
+ "ARTIFACT_QUOTA_EXCEEDED",
5412
+ "ARTIFACT_SOURCE_UNSAFE",
5413
+ "ARTIFACT_UNSAFE_REMOTE"
5414
+ ];
5234
5415
  const packaged = (work) => work.status === "ready" && work.artifact?.revision === work.revision;
5235
5416
  /** One host-lifetime packaging batch per advance; persisted artifacts are its progress. */
5236
5417
  function createSendPackager(ports) {
@@ -5249,13 +5430,16 @@ function createSendPackager(ports) {
5249
5430
  if (!current) throw new ApplicationError("UNKNOWN_ITEM");
5250
5431
  }
5251
5432
  const ready = (works, id) => works.some((work) => work.id === id && packaged(work));
5252
- const batch = workIds.filter((id) => !ready(current.works, id)).slice(0, 16);
5433
+ const batch = workIds.filter((id) => !ready(current.works, id));
5253
5434
  if (!batch.length) return true;
5254
5435
  signal.throwIfAborted();
5255
5436
  const result = await ports.render({
5256
5437
  journeyId,
5257
5438
  workIds: batch
5258
5439
  }, signal);
5440
+ const failures = result.renders?.filter((item) => item.status === "failed") ?? [];
5441
+ const failure = failures.find((item) => packagingRefusalCodes.includes(item.code)) ?? failures.find((item) => finalPackagingCodes.includes(item.code)) ?? failures[0];
5442
+ if (failure) throw new ApplicationError(failure.code);
5259
5443
  if (batch.some((id) => !ready(result.works, id))) throw new Error("PACKAGING_FAILED");
5260
5444
  return false;
5261
5445
  };
@@ -5628,6 +5812,19 @@ async function classifyExit(child, startup, exited, busy) {
5628
5812
  }
5629
5813
  //#endregion
5630
5814
  //#region apps/cli/src/send-host.ts
5815
+ /**
5816
+ * The detached focused host (ADR 0227) drives a Send only for the store's current journey, the
5817
+ * check its timer makes, and only while `liveHost` verifies its process identity and URL.
5818
+ * Anything the CLI cannot confirm returns false, so the CLI keeps stepping as before.
5819
+ */
5820
+ async function focusedHostAlive(stateDir, journeyId, currentJourneyId) {
5821
+ try {
5822
+ if (await currentJourneyId() !== journeyId) return false;
5823
+ return (await liveHost(join(stateDir, "serve"), journeyId)).status === "alive";
5824
+ } catch {
5825
+ return false;
5826
+ }
5827
+ }
5631
5828
  const recordSchema = z.object({
5632
5829
  hostId: z.uuid(),
5633
5830
  pid: z.number().int().positive()
@@ -6091,49 +6288,72 @@ var SessionContentConflict = class extends ApplicationError {
6091
6288
  this.contentConflict = contentConflict;
6092
6289
  }
6093
6290
  };
6094
- /** Check the entire pending Send before admitting any session mutation. */
6291
+ /** Collect safe conflicts across the entire pending Send before any session mutation. */
6095
6292
  async function sessionAccessBlocked(dependencies, journeyId, items, inventory, signal) {
6096
6293
  const sessions = dependencies.artifacts.sessionFacts ?? dependencies.artifacts.sessions;
6097
- if (!sessions) return false;
6294
+ if (!sessions) return [];
6295
+ const conflicts = /* @__PURE__ */ new Map();
6296
+ const accountAccesses = /* @__PURE__ */ new Map();
6297
+ function observe(item, access) {
6298
+ const values = accountAccesses.get(item.workId) ?? /* @__PURE__ */ new Set();
6299
+ values.add(access);
6300
+ accountAccesses.set(item.workId, values);
6301
+ }
6302
+ function record(item, sessionKey, rule, account) {
6303
+ if (account) observe(item, account.access);
6304
+ const conflict = {
6305
+ workId: item.workId,
6306
+ sessionKey,
6307
+ requestedAccess: item.access,
6308
+ accountWorkId: account?.logicalWorkId ?? null,
6309
+ accountAccess: account?.access ?? null,
6310
+ rule
6311
+ };
6312
+ conflicts.set(JSON.stringify(conflict), conflict);
6313
+ }
6098
6314
  const requestedAccess = /* @__PURE__ */ new Map();
6099
6315
  const requestedDigests = /* @__PURE__ */ new Map();
6100
6316
  const contentAccess = /* @__PURE__ */ new Map();
6101
- const claimContent = (key, sha256, access) => {
6102
- const identity = JSON.stringify([key, sha256]);
6103
- const previous = contentAccess.get(identity);
6104
- if (previous !== void 0 && previous !== access) return true;
6105
- contentAccess.set(identity, access);
6106
- return false;
6107
- };
6317
+ function claim(claims, identity, key, item, rule) {
6318
+ const previous = claims.get(identity) ?? [];
6319
+ for (const other of previous) {
6320
+ if (other.access === item.access) continue;
6321
+ record(item, key, rule);
6322
+ record(other, key, rule);
6323
+ }
6324
+ if (!previous.some((other) => other.workId === item.workId)) previous.push(item);
6325
+ claims.set(identity, previous);
6326
+ }
6327
+ const claimContent = (key, sha256, item) => claim(contentAccess, JSON.stringify([key, sha256]), key, item, "content-access");
6108
6328
  for (const item of items) {
6109
6329
  if (item.status === "blocked" || item.sessionAssembly) continue;
6110
6330
  const keys = item.sessionMembership?.keys ?? (item.sessionUpload ? [item.sessionUpload.key] : []);
6111
6331
  if (!keys.length) continue;
6112
6332
  const destination = inventory.works.find((work) => work.logicalWorkId === item.workId);
6113
6333
  if (destination?.access === "removed") throw new ApplicationError("WORK_REMOVED");
6334
+ if (destination) observe(item, destination.access);
6114
6335
  for (const key of keys) {
6115
- const access = requestedAccess.get(key);
6116
- if (access !== void 0 && access !== item.access) return true;
6117
- requestedAccess.set(key, item.access);
6118
- const source = inventory.works.find((work) => work.sessions.some((entry) => entry.key === key));
6119
- if (source && source.logicalWorkId !== item.workId && source.access !== item.access) return true;
6120
- const sha256 = item.sessionMembership ? source?.sessions.find((session) => session.key === key)?.sha256 : item.sessionUpload?.sha256;
6121
- if (sha256 && claimContent(key, sha256, item.access)) return true;
6122
- if (destination && destination.access !== item.access && !destination.sessions.some((entry) => entry.key === key && (item.sessionMembership || entry.sha256 === sha256))) return true;
6336
+ claim(requestedAccess, key, key, item, "requested-access");
6337
+ const sources = inventory.works.filter((work) => work.sessions.some((entry) => entry.key === key));
6338
+ for (const source of sources) observe(item, source.access);
6339
+ for (const source of sources) if (source.logicalWorkId !== item.workId && source.access !== item.access) record(item, key, "held-elsewhere", source);
6340
+ const sha256 = item.sessionMembership ? sources[0]?.sessions.find((session) => session.key === key)?.sha256 : item.sessionUpload?.sha256;
6341
+ if (sha256) claimContent(key, sha256, item);
6342
+ if (destination && destination.access !== item.access) record(item, key, "destination-access", destination);
6123
6343
  }
6124
6344
  }
6125
6345
  for (const item of items) {
6126
6346
  if (item.sessionAssembly || item.sessionMembership || ["completed", "blocked"].includes(item.status)) continue;
6127
6347
  const destination = inventory.works.find((work) => work.logicalWorkId === item.workId);
6128
6348
  if (destination?.access === "removed") throw new ApplicationError("WORK_REMOVED");
6349
+ if (destination) observe(item, destination.access);
6129
6350
  for await (const session of sessions(journeyId, item.artifact, signal)) {
6130
6351
  const key = await sessionKeyFor(session);
6131
6352
  const { workerIdentity: _, ...original } = session;
6132
6353
  const legacyKey = session.workerIdentity ? await sessionKeyFor(original) : key;
6133
- if (claimContent(key, session.sha256, item.access) || legacyKey !== key && claimContent(legacyKey, session.sha256, item.access)) return true;
6134
- const otherAccess = requestedAccess.get(key);
6135
- if (otherAccess !== void 0 && otherAccess !== item.access) return true;
6136
- requestedAccess.set(key, item.access);
6354
+ claimContent(key, session.sha256, item);
6355
+ if (legacyKey !== key) claimContent(legacyKey, session.sha256, item);
6356
+ claim(requestedAccess, key, key, item, "requested-access");
6137
6357
  const otherDigest = requestedDigests.get(key);
6138
6358
  if (otherDigest !== void 0 && otherDigest.sha256 !== session.sha256) throw new SessionContentConflict({
6139
6359
  workIds: [.../* @__PURE__ */ new Set([...otherDigest.workIds, item.workId])],
@@ -6143,11 +6363,18 @@ async function sessionAccessBlocked(dependencies, journeyId, items, inventory, s
6143
6363
  sha256: session.sha256,
6144
6364
  workIds: [.../* @__PURE__ */ new Set([...otherDigest?.workIds ?? [], item.workId])]
6145
6365
  });
6146
- 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;
6147
- if (destination && destination.access !== item.access && !destination.sessions.some((entry) => entry.key === key && entry.sha256 === session.sha256)) return true;
6366
+ for (const work of inventory.works) for (const entry of work.sessions) {
6367
+ if (entry.key !== key && !(entry.key === legacyKey && entry.sha256 === session.sha256)) continue;
6368
+ observe(item, work.access);
6369
+ if (work.logicalWorkId !== item.workId && work.access !== item.access) record(item, entry.key, "held-elsewhere", work);
6370
+ }
6371
+ if (destination && destination.access !== item.access) record(item, key, "destination-access", destination);
6148
6372
  }
6149
6373
  }
6150
- return false;
6374
+ return [...conflicts.values()].map((conflict) => (accountAccesses.get(conflict.workId)?.size ?? 0) > 1 ? {
6375
+ ...conflict,
6376
+ accountAccess: "mixed"
6377
+ } : conflict);
6151
6378
  }
6152
6379
  /** Keep overlapping session ownership serial through membership, including retained members. */
6153
6380
  async function sessionIndependentItems(dependencies, journeyId, items, inventory, signal) {
@@ -6825,8 +7052,8 @@ async function put(context, item, receiptId, upload, connection, signal, guard)
6825
7052
  await guard();
6826
7053
  await context.upload(item.artifact, upload, signal);
6827
7054
  const sent = withoutBusy(item);
6828
- await guard();
6829
7055
  try {
7056
+ await guard();
6830
7057
  return observe(sent, await connection.complete(receiptId, signal), connection, signal, context.snapshot === true);
6831
7058
  } catch (error) {
6832
7059
  if (upload.headers !== void 0 && error instanceof JourneyRemoteError && error.code === "bundle_mismatch") return {
@@ -6956,13 +7183,14 @@ function isCode(error, code) {
6956
7183
  }
6957
7184
  /**
6958
7185
  * A step that ended without a definite answer and is safe to repeat: a request timeout or abort
6959
- * that is not the Send's own cancellation, or a connection failure from `fetch`. The Work keeps
7186
+ * that is not the Send's own cancellation, or a connection failure from `fetch`: `fetch failed`
7187
+ * before the response, `terminated` when the socket resets while the body is read. The Work keeps
6960
7188
  * its state, so the next advance repeats the same idempotent step (ADR 0197).
6961
7189
  */
6962
7190
  function isInterrupted(error) {
6963
7191
  if (!(error instanceof Error)) return false;
6964
7192
  if (error.name === "TimeoutError" || error.name === "AbortError") return true;
6965
- return error instanceof TypeError && error.message === "fetch failed";
7193
+ return error instanceof TypeError && (error.message === "fetch failed" || error.message === "terminated");
6966
7194
  }
6967
7195
  const sessionBackoffMs = (attempt) => 1e3 * 2 ** (attempt - 1);
6968
7196
  const zeroByteBackoffMs = (count) => [
@@ -7183,8 +7411,10 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7183
7411
  break;
7184
7412
  }
7185
7413
  }
7414
+ const ownKeys = /* @__PURE__ */ new Set();
7186
7415
  for await (const session of sessions(journeyId, item.artifact, signal)) {
7187
7416
  const identity = await keys.add(session);
7417
+ ownKeys.add(identity.key);
7188
7418
  if (recoverLegacy && (identity.legacyKey !== pending?.key || session.sha256 !== pending?.sha256)) continue;
7189
7419
  const key = recoverLegacy && pending ? pending.key : identity.key;
7190
7420
  if (stored.get(key)?.sha256 === session.sha256) {
@@ -7210,13 +7440,23 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7210
7440
  const assembly = await remote.assembly(item.workId, signal);
7211
7441
  const current = (await remote.inventory(signal)).works.find((work) => work.logicalWorkId === item.workId);
7212
7442
  if (current && current.access !== item.access) {
7213
- const resolution = await connection.resolve(resolveInput(item, context.bindingId), signal);
7214
- const [candidate] = resolution.candidates;
7215
- if (resolution.status === "blocked" || candidate?.access === "removed") throw new ApplicationError("WORK_REMOVED");
7216
- if (resolution.status !== "matched" || resolution.logicalWorkId !== item.workId || resolution.candidates.length !== 1 || !candidate || context.bindingId !== void 0 && candidate.bindingId !== context.bindingId) throw new ApplicationError("INVALID_STATE");
7217
- const settings = await connection.preview(candidate.workId, item.access, signal);
7443
+ if (current.access === "removed") throw new ApplicationError("WORK_REMOVED");
7444
+ const [ownKey] = ownKeys;
7445
+ if (!ownKey) throw new ApplicationError("INVALID_STATE");
7218
7446
  await guard();
7219
- await connection.confirm(settings, signal);
7447
+ return {
7448
+ ...item,
7449
+ status: "blocked",
7450
+ error: "SESSION_MOVE_ACCESS_REQUIRED",
7451
+ accessConflict: {
7452
+ sessionKey: ownKey,
7453
+ requestedAccess: item.access,
7454
+ accountWorkId: current.logicalWorkId,
7455
+ accountAccess: current.access,
7456
+ rule: "destination-access"
7457
+ },
7458
+ accessConflictSessionCount: ownKeys.size
7459
+ };
7220
7460
  }
7221
7461
  await guard();
7222
7462
  return {
@@ -7280,6 +7520,10 @@ function aggregate(send) {
7280
7520
  function awaitingPassport(send) {
7281
7521
  return send.status === "completed" && send.items.some((item) => !["completed", "blocked"].includes(item.status));
7282
7522
  }
7523
+ /** The host continues confirmed uploads and unfinished Passport assessments without a panel. */
7524
+ function hostDrives(send) {
7525
+ return awaitingPassport(send) || send.confirmedAt != null && ["sending", "waiting"].includes(send.status);
7526
+ }
7283
7527
  /** Explicit resume retries recoverable items and recaptures a pending sign-in or profile. */
7284
7528
  function resumeState(send) {
7285
7529
  if (send.status !== "completed" && send.items.length && send.items.every((item) => item.status === "completed") && send.items.some((item) => !item.sessionAssembly)) return {
@@ -7304,18 +7548,23 @@ function resumeState(send) {
7304
7548
  ...send,
7305
7549
  status: "sending",
7306
7550
  error: null,
7307
- items: send.items.map((item) => item.status === "blocked" && [
7308
- "OPERATION_FAILED",
7309
- "UPLOAD_FAILED",
7310
- "SEND_REMOTE_REFUSED",
7311
- "SESSION_MOVE_ACCESS_REQUIRED",
7312
- "IDENTITY_UNAVAILABLE",
7313
- "ACCOUNT_SIGNED_OUT"
7314
- ].includes(item.error ?? "") ? {
7315
- ...item,
7316
- status: item.receipt ? item.receipt.state === "awaiting_upload" ? "uploading" : "assessing" : "ready",
7317
- error: null
7318
- } : item)
7551
+ items: send.items.map((item) => {
7552
+ const { accessConflict: _previous, accessConflictSessionCount: _count, ...current } = item;
7553
+ return item.status === "blocked" && [
7554
+ "OPERATION_FAILED",
7555
+ "COORDINATION_BUSY",
7556
+ "UPLOAD_FAILED",
7557
+ "SEND_REMOTE_REFUSED",
7558
+ "SESSION_MOVE_ACCESS_REQUIRED",
7559
+ "SESSION_MOVE_ACCESS_HELD",
7560
+ "IDENTITY_UNAVAILABLE",
7561
+ "ACCOUNT_SIGNED_OUT"
7562
+ ].includes(item.error ?? "") ? {
7563
+ ...current,
7564
+ status: item.receipt ? item.receipt.state === "awaiting_upload" ? "uploading" : "assessing" : "ready",
7565
+ error: null
7566
+ } : item;
7567
+ })
7319
7568
  });
7320
7569
  return profileRequired && !["blocked", "completed"].includes(next.status) ? {
7321
7570
  ...next,
@@ -7356,7 +7605,7 @@ async function writeProfile(connection, chosen, guard, signal, failure, captured
7356
7605
  await guard();
7357
7606
  return captured;
7358
7607
  } catch (error) {
7359
- if (signal.aborted) throw error;
7608
+ if (signal.aborted || safeCode(error) === "COORDINATION_BUSY") throw error;
7360
7609
  if (error instanceof JourneyRemoteError && error.status === 409 && addressRefusals[error.code]) return {
7361
7610
  status: "blocked",
7362
7611
  error: addressRefusals[error.code] ?? null
@@ -7382,7 +7631,7 @@ async function captureProfile(connection, guard, signal, failure) {
7382
7631
  parallelCompletes: parallelCompletes ? true : void 0
7383
7632
  };
7384
7633
  } catch (error) {
7385
- if (signal.aborted) throw error;
7634
+ if (signal.aborted || safeCode(error) === "COORDINATION_BUSY") throw error;
7386
7635
  if (error instanceof JourneyRemoteError && error.status === 401 || error instanceof ApplicationError && error.code === "ACCOUNT_SIGNED_OUT") return {
7387
7636
  status: "sign-in",
7388
7637
  error: null
@@ -7503,7 +7752,14 @@ async function advanceItems(host, signal) {
7503
7752
  });
7504
7753
  host.failure("SEND_SCOPE_BUSY", cause);
7505
7754
  error = "SEND_SCOPE_BUSY";
7506
- } else if (isInterrupted(cause) || isTransientRefusal(cause)) {
7755
+ } else if (isInterrupted(cause) || isTransientRefusal(cause) || safeCode(cause) === "COORDINATION_BUSY") {
7756
+ if (safeCode(cause) === "COORDINATION_BUSY") {
7757
+ const current = progressed ?? item;
7758
+ next.set(item.workId, {
7759
+ ...current,
7760
+ busyRefusals: (current.busyRefusals ?? 0) + 1
7761
+ });
7762
+ }
7507
7763
  host.failure("SEND_INTERRUPTED", cause);
7508
7764
  error = "SEND_INTERRUPTED";
7509
7765
  } else {
@@ -7537,7 +7793,7 @@ async function advanceItems(host, signal) {
7537
7793
  items
7538
7794
  }));
7539
7795
  } catch (error) {
7540
- if (!signal.aborted && (isInterrupted(error) || error instanceof Error && isInterrupted(error.cause) || isCode(error, "IDENTITY_UNAVAILABLE"))) {
7796
+ if (!signal.aborted && (isInterrupted(error) || error instanceof Error && isInterrupted(error.cause) || isCode(error, "IDENTITY_UNAVAILABLE") || safeCode(error) === "COORDINATION_BUSY")) {
7541
7797
  host.failure("SEND_INTERRUPTED", error);
7542
7798
  return send.error === "SEND_INTERRUPTED" ? host.applied() : host.save({
7543
7799
  ...send,
@@ -7643,8 +7899,22 @@ async function advanceSend(dependencies, address, signal, chain) {
7643
7899
  error: "SEND_SCOPE_CHANGED"
7644
7900
  })
7645
7901
  });
7902
+ const invalidated = safeCode(error) === "SEND_SCOPE_INVALIDATED";
7903
+ const answered = (item) => {
7904
+ const work = snapshot.journey.works.find((work) => work.id === item.workId);
7905
+ return !work || work.access !== item.access;
7906
+ };
7907
+ const answeredSessions = new Set(snapshot.send.items.flatMap((item) => item.error === "SESSION_MOVE_ACCESS_REQUIRED" && item.accessConflict && answered(item) ? [item.accessConflict.sessionKey] : []));
7646
7908
  return blockSend({
7647
- send: snapshot.send,
7909
+ send: invalidated ? {
7910
+ ...snapshot.send,
7911
+ items: snapshot.send.items.filter((item) => {
7912
+ if (item.error === "SESSION_MOVE_ACCESS_HELD") return false;
7913
+ if (item.error !== "SESSION_MOVE_ACCESS_REQUIRED") return true;
7914
+ if (item.accessConflict?.accountAccess === null && answeredSessions.has(item.accessConflict.sessionKey)) return false;
7915
+ return !answered(item);
7916
+ })
7917
+ } : snapshot.send,
7648
7918
  failure: dependencies.failure,
7649
7919
  save
7650
7920
  }, error, signal);
@@ -7670,18 +7940,41 @@ async function advanceSend(dependencies, address, signal, chain) {
7670
7940
  if (!connection.sessions) throw new ApplicationError("INVALID_STATE");
7671
7941
  if (!send.items.some((item) => item.status === "uploading" && !item.sessionAssembly && !item.receipt && !item.reservation)) return null;
7672
7942
  const inventory = await connection.sessions.inventory(preflightSignal);
7673
- if (!await sessionAccessBlocked(preparation.dependencies, snapshot.journey.journeyId, send.items, inventory, preflightSignal)) {
7943
+ const conflicts = await sessionAccessBlocked(preparation.dependencies, snapshot.journey.journeyId, send.items, inventory, preflightSignal);
7944
+ if (conflicts.length === 0) {
7674
7945
  independentItems = await sessionIndependentItems(preparation.dependencies, snapshot.journey.journeyId, send.items, inventory, preflightSignal);
7675
7946
  return null;
7676
7947
  }
7677
7948
  await connection.assertCurrent(preflightSignal);
7949
+ dependencies.failure("SESSION_MOVE_ACCESS_REQUIRED", {
7950
+ code: "SESSION_MOVE_ACCESS_REQUIRED",
7951
+ message: JSON.stringify({
7952
+ conflicts: conflicts.length,
7953
+ works: new Set(conflicts.map((conflict) => conflict.workId)).size,
7954
+ rules: [...new Set(conflicts.map((conflict) => conflict.rule))].sort()
7955
+ })
7956
+ });
7678
7957
  return save({
7679
7958
  ...send,
7680
7959
  status: "blocked",
7681
- items: send.items.map((item) => item.sessionAssembly || item.status === "completed" ? item : {
7682
- ...item,
7683
- status: "blocked",
7684
- error: "SESSION_MOVE_ACCESS_REQUIRED"
7960
+ items: send.items.map((item) => {
7961
+ if (item.sessionAssembly || item.status === "completed") return item;
7962
+ const { accessConflict: _previous, accessConflictSessionCount: _count, ...current } = item;
7963
+ const itemConflicts = conflicts.filter((entry) => entry.workId === item.workId);
7964
+ const conflict = itemConflicts.find((entry) => entry.accountAccess !== null) ?? itemConflicts[0];
7965
+ if (!conflict) return {
7966
+ ...current,
7967
+ status: "blocked",
7968
+ error: "SESSION_MOVE_ACCESS_HELD"
7969
+ };
7970
+ const { workId: _, ...accessConflict } = conflict;
7971
+ return {
7972
+ ...current,
7973
+ status: "blocked",
7974
+ error: "SESSION_MOVE_ACCESS_REQUIRED",
7975
+ accessConflict,
7976
+ accessConflictSessionCount: new Set(itemConflicts.map((entry) => entry.sessionKey)).size
7977
+ };
7685
7978
  })
7686
7979
  });
7687
7980
  },
@@ -7702,7 +7995,7 @@ async function advanceSend(dependencies, address, signal, chain) {
7702
7995
  });
7703
7996
  saving = pending.catch(() => void 0);
7704
7997
  return pending;
7705
- }).then((result) => {
7998
+ }).then(({ busyRefusals: _busy, ...result }) => {
7706
7999
  fulfilled.add(item.workId);
7707
8000
  return result;
7708
8001
  })
@@ -8023,20 +8316,15 @@ function createManualUploader(dependencies) {
8023
8316
  upload,
8024
8317
  status: "applied"
8025
8318
  };
8026
- const host = {
8319
+ let bindings = [];
8320
+ return advanceItems({
8027
8321
  send: upload,
8028
8322
  failure: dependencies.failure,
8029
- save: (next) => save(upload, next)
8030
- };
8031
- let bindings;
8032
- try {
8033
- bindings = await storage.bindings();
8034
- } catch (error) {
8035
- return blockSend(host, error, signal);
8036
- }
8037
- return advanceItems({
8038
- ...host,
8039
- connect: dependencies.auth.connect,
8323
+ save: (next) => save(upload, next),
8324
+ connect: async (connectSignal) => {
8325
+ bindings = await storage.bindings();
8326
+ return dependencies.auth.connect(connectSignal);
8327
+ },
8040
8328
  profile: (connection, profileSignal) => captureProfile(connection, guard(upload), profileSignal, dependencies.failure),
8041
8329
  context: (item) => ({
8042
8330
  active: guard(upload, item),
@@ -9330,7 +9618,7 @@ async function requestSend(dependencies, raw, hostId, source) {
9330
9618
  ...current,
9331
9619
  status: "stale"
9332
9620
  };
9333
- assertJourneyPrivacyReady(current.journey);
9621
+ assertJourneyPrivacyReady(current.journey, false, { publicChoices: true });
9334
9622
  if (current.journey.pendingHideId || current.journey.phase === "paused") throw new ApplicationError("SCOPE_NOT_READY");
9335
9623
  if (current.send && ([
9336
9624
  "awaiting-packaging",
@@ -9452,6 +9740,7 @@ function createJourneySender(dependencies, phases = 3, host = {
9452
9740
  /** The serving host owns a pending request; another process leaves it unchanged. */
9453
9741
  const pendingElsewhere = (send) => !host.serving && send?.request !== void 0 && !send.confirmedAt;
9454
9742
  const progress = /* @__PURE__ */ new Map();
9743
+ let packagingRetry;
9455
9744
  const sessionFacts = createSessionFactsCache();
9456
9745
  const running = /* @__PURE__ */ new Map();
9457
9746
  const runKey = (address) => `${address.journeyId}:${address.sendId}`;
@@ -9551,27 +9840,51 @@ function createJourneySender(dependencies, phases = 3, host = {
9551
9840
  const pendingError = pending ? await requestError(dependencies, pending, hostId, signal) : null;
9552
9841
  if (pending && (pendingError || !requestMatches(pending, snapshot.journey))) return block(snapshot, pending, pendingError ?? "REVISION_CONFLICT");
9553
9842
  const draft = await draftSend(snapshot.journey, dependencies, signal);
9843
+ let observed = false;
9554
9844
  const recover = snapshot.send && [
9555
9845
  "blocked",
9556
9846
  "paused",
9557
9847
  "completed"
9558
9848
  ].includes(snapshot.send.status) && snapshot.send.items.some((item) => sendRecoveryCodes.includes(item.error ?? ""));
9559
- if (recover && snapshot.send) {
9849
+ if (recover || dependencies.artifacts.sessions) {
9560
9850
  const connection = await dependencies.auth.connect(signal);
9561
- if (!connection) throw new ApplicationError("ACCOUNT_SIGNED_OUT");
9562
- if (snapshot.send.subject !== null && snapshot.send.subject !== connection.subject) throw new ApplicationError("IDENTITY_REJECTED");
9851
+ if (connection && draft.subject !== connection.subject) return {
9852
+ ...snapshot,
9853
+ status: "stale"
9854
+ };
9855
+ if (recover && !connection) throw new ApplicationError("ACCOUNT_SIGNED_OUT");
9856
+ if (recover && snapshot.send?.subject && snapshot.send.subject !== connection?.subject) throw new ApplicationError("IDENTITY_REJECTED");
9563
9857
  const sessions = dependencies.artifacts.sessions;
9564
- const remote = connection.sessions;
9565
- if (sessions && remote) {
9566
- const inventory = await remote.inventory(signal);
9567
- for (const item of draft.items) {
9568
- const current = inventory.works.find((work) => work.logicalWorkId === item.workId);
9858
+ const remote = connection?.sessions;
9859
+ if (sessions && remote && connection) {
9860
+ const inventory = await remote.inventory(signal).catch((error) => {
9861
+ if (signal.aborted || recover) throw error;
9862
+ dependencies.failure("SEND_INTERRUPTED", error);
9863
+ return null;
9864
+ });
9865
+ observed = inventory !== null;
9866
+ try {
9867
+ const conflicts = inventory ? await sessionAccessBlocked(dependencies, input.journeyId, draft.items, inventory, signal) : [];
9868
+ for (const item of draft.items) {
9869
+ const itemConflicts = conflicts.filter((entry) => entry.workId === item.workId);
9870
+ const conflict = itemConflicts.find((entry) => entry.accountAccess !== null) ?? itemConflicts[0];
9871
+ if (conflict) {
9872
+ const { workId: _, ...accessConflict } = conflict;
9873
+ item.accessConflict = accessConflict;
9874
+ item.accessConflictSessionCount = new Set(itemConflicts.map((entry) => entry.sessionKey)).size;
9875
+ }
9876
+ }
9877
+ } catch (error) {
9878
+ if (!(error instanceof SessionContentConflict) && !(error instanceof ApplicationError && error.code === "WORK_REMOVED")) throw error;
9879
+ }
9880
+ for (const item of inventory && recover && snapshot.send ? draft.items : []) {
9881
+ const current = inventory?.works.find((work) => work.logicalWorkId === item.workId);
9569
9882
  const assembly = await remote.assembly(item.workId, signal);
9570
9883
  const selected = createSessionKeys();
9571
9884
  for await (const session of sessions(input.journeyId, item.artifact, signal)) await selected.add(session);
9572
9885
  const currentKeys = current?.sessions.map((session) => session.key) ?? [];
9573
9886
  item.recoveryReview = {
9574
- previousSendId: snapshot.send.id,
9887
+ previousSendId: snapshot.send?.id ?? draft.id,
9575
9888
  revision: assembly?.revision ?? 0,
9576
9889
  currentAccess: current?.access ?? null,
9577
9890
  currentKeys,
@@ -9581,6 +9894,13 @@ function createJourneySender(dependencies, phases = 3, host = {
9581
9894
  await connection.assertCurrent(signal);
9582
9895
  }
9583
9896
  }
9897
+ for (const item of observed ? [] : draft.items) {
9898
+ const previous = snapshot.send?.items.find((entry) => entry.workId === item.workId && entry.access === item.access);
9899
+ if (previous?.accessConflict && !item.accessConflict) {
9900
+ item.accessConflict = previous.accessConflict;
9901
+ item.accessConflictSessionCount = previous.accessConflictSessionCount;
9902
+ }
9903
+ }
9584
9904
  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;
9585
9905
  const next = pending ? {
9586
9906
  ...draft,
@@ -9589,6 +9909,7 @@ function createJourneySender(dependencies, phases = 3, host = {
9589
9909
  request: pending.request
9590
9910
  } : canReuseExistingSend && snapshot.send ? {
9591
9911
  ...snapshot.send,
9912
+ ...snapshot.send.confirmedAt === null ? { items: draft.items } : {},
9592
9913
  revision: snapshot.send.revision + 1
9593
9914
  } : draft;
9594
9915
  const result = await dependencies.storage.commitSend({
@@ -9619,7 +9940,7 @@ function createJourneySender(dependencies, phases = 3, host = {
9619
9940
  const authorizationError = await requestError(dependencies, snapshot.send, hostId, signal);
9620
9941
  if (snapshot.send.request && !snapshot.send.confirmedAt && (authorizationError || !requestMatches(snapshot.send, snapshot.journey))) return block(snapshot, snapshot.send, authorizationError ?? "REVISION_CONFLICT");
9621
9942
  assertScope(snapshot.journey, snapshot.send);
9622
- 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");
9943
+ 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");
9623
9944
  if (snapshot.send.confirmedAt !== null) return saveStart(snapshot.send);
9624
9945
  const connection = await dependencies.auth.connect(signal);
9625
9946
  if (snapshot.send.request && (connection?.subject ?? null) !== snapshot.send.request.subject) return saveSend(dependencies, snapshot, {
@@ -9728,6 +10049,10 @@ function createJourneySender(dependencies, phases = 3, host = {
9728
10049
  ...snapshot,
9729
10050
  status: "applied"
9730
10051
  };
10052
+ if (!host.serving && hostDrives(snapshot.send) && await host.focused?.(address.journeyId)) return {
10053
+ ...snapshot,
10054
+ status: "applied"
10055
+ };
9731
10056
  const authorizationError = await requestError(dependencies, snapshot.send, hostId, lifetime);
9732
10057
  if (authorizationError) return block(snapshot, snapshot.send, authorizationError);
9733
10058
  if (snapshot.send.request && !snapshot.send.confirmedAt && ["awaiting-packaging", "prepared"].includes(snapshot.send.status)) {
@@ -9742,10 +10067,52 @@ function createJourneySender(dependencies, phases = 3, host = {
9742
10067
  error: "REVISION_CONFLICT"
9743
10068
  });
9744
10069
  if (snapshot.send.status === "awaiting-packaging") {
9745
- if (await dependencies.package?.(address.journeyId, snapshot.send.request.included.map((item) => item.workId), lifetime, snapshot.send.request.source) === false) return {
9746
- ...await load(address),
10070
+ if (packagingRetry?.key !== key) packagingRetry = void 0;
10071
+ if (packagingRetry && Date.parse(dependencies.now()) < packagingRetry.after) return {
10072
+ ...snapshot,
9747
10073
  status: "applied"
9748
10074
  };
10075
+ let packaged;
10076
+ try {
10077
+ packaged = await dependencies.package?.(address.journeyId, snapshot.send.request.included.map((item) => item.workId), lifetime, snapshot.send.request.source);
10078
+ } catch (cause) {
10079
+ lifetime.throwIfAborted();
10080
+ const code = cause instanceof ApplicationError ? cause.code : safeCode(cause);
10081
+ if (packagingRefusalCodes.includes(code)) throw cause;
10082
+ dependencies.failure(code, cause);
10083
+ snapshot = await load(address);
10084
+ if (snapshot.send.status !== "awaiting-packaging") return {
10085
+ ...snapshot,
10086
+ status: "stale"
10087
+ };
10088
+ const error = await requestError(dependencies, snapshot.send, hostId, lifetime);
10089
+ if (error || !requestMatches(snapshot.send, snapshot.journey)) return block(snapshot, snapshot.send, error ?? "REVISION_CONFLICT");
10090
+ if (finalPackagingCodes.includes(code)) {
10091
+ packagingRetry = void 0;
10092
+ return block(snapshot, snapshot.send, code);
10093
+ }
10094
+ const failures = Math.min((packagingRetry?.failures ?? 0) + 1, 5);
10095
+ packagingRetry = {
10096
+ key,
10097
+ failures,
10098
+ after: Date.parse(dependencies.now()) + Math.min(2e3 * 2 ** (failures - 1), 3e4)
10099
+ };
10100
+ return saveSend(dependencies, snapshot, {
10101
+ ...snapshot.send,
10102
+ error: code
10103
+ });
10104
+ }
10105
+ packagingRetry = void 0;
10106
+ if (packaged === false) {
10107
+ snapshot = await load(address);
10108
+ return snapshot.send.error ? saveSend(dependencies, snapshot, {
10109
+ ...snapshot.send,
10110
+ error: null
10111
+ }) : {
10112
+ ...snapshot,
10113
+ status: "applied"
10114
+ };
10115
+ }
9749
10116
  snapshot = await load(address);
9750
10117
  if (snapshot.send.status !== "awaiting-packaging") return {
9751
10118
  ...snapshot,
@@ -9759,6 +10126,10 @@ function createJourneySender(dependencies, phases = 3, host = {
9759
10126
  if (prepared.status === "stale" || prepared.send?.status !== "prepared") return prepared;
9760
10127
  snapshot = requireSend(prepared);
9761
10128
  }
10129
+ if (snapshot.send.items.some((item) => item.accessConflict)) return saveSend(dependencies, snapshot, {
10130
+ ...snapshot.send,
10131
+ request: void 0
10132
+ });
9762
10133
  if (!requestMatches(snapshot.send, snapshot.journey) || !requestedItemsMatch(snapshot.send, snapshot.journey)) return saveSend(dependencies, snapshot, {
9763
10134
  ...snapshot.send,
9764
10135
  status: "blocked",
@@ -9796,10 +10167,6 @@ function createJourneySender(dependencies, phases = 3, host = {
9796
10167
  }
9797
10168
  //#endregion
9798
10169
  //#region apps/cli/src/account/journey/auth.ts
9799
- /** An access token this close to expiry is renewed first, so a step never starts on it. */
9800
- const renewBeforeMs = 12e4;
9801
- const renewTimeoutMs = 3e4;
9802
- const renewals = /* @__PURE__ */ new WeakMap();
9803
10170
  /** Reuses the existing device sign-in credential; a token is never returned to the panel or Agent. */
9804
10171
  function createJourneySendAuth(account, requestFetch) {
9805
10172
  /**
@@ -9826,39 +10193,11 @@ function createJourneySendAuth(account, requestFetch) {
9826
10193
  return value;
9827
10194
  })).credential?.refreshToken) await expire(accessToken);
9828
10195
  }
9829
- function renew(credential) {
9830
- const running = renewals.get(account);
9831
- if (running) return running;
9832
- const renewing = (async () => {
9833
- try {
9834
- const token = await account.identity.refresh(credential.refreshToken, AbortSignal.timeout(renewTimeoutMs));
9835
- if (!token) {
9836
- await expire(credential.accessToken);
9837
- return null;
9838
- }
9839
- return (await account.credentials.update((value) => {
9840
- if (value.credential?.refreshToken === credential.refreshToken) value.credential = {
9841
- ...value.credential,
9842
- accessToken: token.accessToken,
9843
- refreshToken: token.refreshToken ?? credential.refreshToken,
9844
- scope: token.scope,
9845
- issuedAt: account.now(),
9846
- expiresAt: token.expiresAt
9847
- };
9848
- return value;
9849
- })).credential;
9850
- } finally {
9851
- renewals.delete(account);
9852
- }
9853
- })();
9854
- renewals.set(account, renewing);
9855
- return renewing;
9856
- }
9857
10196
  async function current() {
9858
10197
  const credential = (await account.credentials.update((value) => value)).credential;
9859
10198
  if (!credential || credential.identityUrl !== account.configuration.identityUrl || credential.clientId !== account.configuration.clientId) return null;
9860
10199
  const left = Date.parse(credential.expiresAt) - Date.parse(account.now());
9861
- if (Number.isFinite(left) && left > renewBeforeMs) return credential;
10200
+ if (Number.isFinite(left) && left > 12e4) return credential;
9862
10201
  const { refreshToken } = credential;
9863
10202
  if (!refreshToken) {
9864
10203
  if (Number.isFinite(left) && left > 0) return credential;
@@ -9866,7 +10205,7 @@ function createJourneySendAuth(account, requestFetch) {
9866
10205
  return null;
9867
10206
  }
9868
10207
  try {
9869
- return await renew({
10208
+ return await renew(account, {
9870
10209
  ...credential,
9871
10210
  refreshToken
9872
10211
  });
@@ -9916,6 +10255,136 @@ function createJourneySendAuth(account, requestFetch) {
9916
10255
  };
9917
10256
  }
9918
10257
  //#endregion
10258
+ //#region apps/cli/src/account/journey/account-access.ts
10259
+ /**
10260
+ * Whole scope-confirm account read: identity, every inventory page and a token refresh. A 203-Work,
10261
+ * 1,400-session account took 2.3 to 3.4 s for its pages plus a refresh, past the earlier 5 s
10262
+ * (issue #53). On expiry the read is `unavailable` (ADR 0277 section 5).
10263
+ */
10264
+ const accountReadBoundMs = 3e4;
10265
+ const ownerBodyErrors = /* @__PURE__ */ new Set(["OWNER_EMPTY_RESPONSE", "OWNER_RESPONSE_LIMIT"]);
10266
+ /** Why the read failed, from error types and codes only; messages are never logged. */
10267
+ function failureClass(error, depth = 0) {
10268
+ if (!(error instanceof Error)) return "other";
10269
+ if (error.name === "TimeoutError") return "timeout";
10270
+ if (isCode(error, "ACCOUNT_SIGNED_OUT") || isCode(error, "IDENTITY_REJECTED")) return "signed-out";
10271
+ if (error instanceof JourneyRemoteError) {
10272
+ if (error.status === 401 || error.status === 403) return "signed-out";
10273
+ return error.status >= 500 || error.status === 429 ? "network" : "protocol";
10274
+ }
10275
+ if (isCode(error, "IDENTITY_UNAVAILABLE") && isInterrupted(error.cause)) return failureClass(error.cause, 1);
10276
+ if (isCode(error, "IDENTITY_UNAVAILABLE") || error instanceof SyntaxError || ownerBodyErrors.has(error.message)) return "protocol";
10277
+ if (isInterrupted(error)) return "network";
10278
+ return depth === 0 && error.cause !== void 0 ? failureClass(error.cause, 1) : "other";
10279
+ }
10280
+ /** Current and legacy upload keys of every part of one local session. */
10281
+ async function keysOf(session) {
10282
+ const keys = [];
10283
+ for (const part of session.parts.length ? session.parts : [void 0]) {
10284
+ const native = part?.nativeEvidence?.identity;
10285
+ const identity = {
10286
+ harness: session.harness,
10287
+ nativeSessionId: native?.sessionId ?? session.sourceSessionId,
10288
+ ...part?.harnessRelativePath ? { relativePath: part.harnessRelativePath } : {},
10289
+ ...session.harness === "claude" && native?.role === "worker" ? { workerIdentity: { agentId: native.agentId } } : {}
10290
+ };
10291
+ const { workerIdentity: _, ...original } = identity;
10292
+ keys.push([await sessionKeyFor(identity), await sessionKeyFor(original)]);
10293
+ }
10294
+ return keys;
10295
+ }
10296
+ /**
10297
+ * Compare frozen identities with the same hashed keys used by session uploads. A Work sends each
10298
+ * root's whole family (`members`, from `reviewFamilies`), so uniform Public is `public-all` only
10299
+ * when every part of every member is held; any key without history keeps it `public` (ADR 0275).
10300
+ */
10301
+ async function accountAccessForRoots(state, roots, inventory, members) {
10302
+ const sessions = new Map(roots.map((session) => [session.id, session]));
10303
+ const held = /* @__PURE__ */ new Map();
10304
+ for (const work of inventory.works) for (const session of work.sessions) {
10305
+ const access = held.get(session.key) ?? /* @__PURE__ */ new Set();
10306
+ access.add(work.access);
10307
+ held.set(session.key, access);
10308
+ }
10309
+ const result = /* @__PURE__ */ new Map();
10310
+ for (const work of state.works) {
10311
+ const access = /* @__PURE__ */ new Set();
10312
+ let known = 0;
10313
+ let unknown = 0;
10314
+ for (const id of work.rootSessionIds) {
10315
+ const root = sessions.get(id);
10316
+ if (!root) throw new ApplicationError("CORRUPTED_STATE");
10317
+ for (const session of members.get(id) ?? [root]) for (const [key, legacyKey] of await keysOf(session)) {
10318
+ const values = /* @__PURE__ */ new Set([...held.get(key) ?? [], ...held.get(legacyKey) ?? []]);
10319
+ if (!values.size) {
10320
+ unknown++;
10321
+ continue;
10322
+ }
10323
+ known++;
10324
+ for (const value of values) access.add(value);
10325
+ }
10326
+ }
10327
+ if (!known) continue;
10328
+ const uniform = access.size === 1;
10329
+ result.set(work.id, uniform && access.has("private") ? "private" : uniform && access.has("public") ? unknown === 0 ? "public-all" : "public" : "mixed");
10330
+ }
10331
+ return result;
10332
+ }
10333
+ /**
10334
+ * Existing authenticated owner ports supply inventory. The result is three-way (ADR 0277 section
10335
+ * 5): a caller stops on `signed-out` or `unavailable` instead of using model access.
10336
+ */
10337
+ function createAccountAccess(storage, auth) {
10338
+ return async (state, signal) => {
10339
+ const started = Date.now();
10340
+ const controller = new AbortController();
10341
+ const boundedSignal = AbortSignal.any([signal, controller.signal]);
10342
+ let timer;
10343
+ let onAbort = () => {};
10344
+ const stopped = new Promise((_resolve, reject) => {
10345
+ onAbort = () => reject(/* @__PURE__ */ new Error("ACCOUNT_ACCESS_CANCELLED"));
10346
+ boundedSignal.addEventListener("abort", onAbort, { once: true });
10347
+ if (boundedSignal.aborted) onAbort();
10348
+ timer = setTimeout(() => controller.abort(), accountReadBoundMs);
10349
+ });
10350
+ const read = async () => {
10351
+ boundedSignal.throwIfAborted();
10352
+ const connection = await auth.connect(boundedSignal);
10353
+ if (!connection?.sessions) return { status: "signed-out" };
10354
+ const plan = (await storage.resources({
10355
+ journeyId: state.journeyId,
10356
+ kind: "plan"
10357
+ }))[0];
10358
+ if (plan?.kind !== "plan") throw new ApplicationError("SCOPE_NOT_READY");
10359
+ const inventory = await connection.sessions.inventory(boundedSignal);
10360
+ const access = await accountAccessForRoots(state, plan.approval.inventory.sessions, inventory, reviewFamilies(plan.approval.inventory).members);
10361
+ await connection.assertCurrent(boundedSignal);
10362
+ return {
10363
+ status: "read",
10364
+ subject: connection.subject,
10365
+ access
10366
+ };
10367
+ };
10368
+ try {
10369
+ return await Promise.race([read(), stopped]);
10370
+ } catch (error) {
10371
+ if (error instanceof ApplicationError && ![
10372
+ "ACCOUNT_SIGNED_OUT",
10373
+ "IDENTITY_REJECTED",
10374
+ "IDENTITY_UNAVAILABLE",
10375
+ "CREDENTIAL_STORE_FAILED"
10376
+ ].includes(error.code)) throw error;
10377
+ const reason = signal.aborted ? "cancelled" : controller.signal.aborted ? "timeout" : failureClass(error);
10378
+ console.error(`ACCOUNT_ACCESS_UNAVAILABLE: account read failed (class=${reason} elapsedMs=${Date.now() - started})`);
10379
+ return { status: reason === "signed-out" ? "signed-out" : "unavailable" };
10380
+ } finally {
10381
+ clearTimeout(timer);
10382
+ boundedSignal.removeEventListener("abort", onAbort);
10383
+ controller.abort();
10384
+ }
10385
+ };
10386
+ }
10387
+ //#endregion
9919
10388
  //#region apps/cli/src/operation/protocol.ts
9920
10389
  const sourceExclusionReasonSchema = z.enum([
9921
10390
  "guardian-excluded",
@@ -10511,6 +10980,9 @@ async function readJson(response, where) {
10511
10980
  }
10512
10981
  chunks.push(next.value);
10513
10982
  }
10983
+ } catch (error) {
10984
+ if (error instanceof ApplicationError) throw error;
10985
+ throw unavailable(`${where} body read failed`, error);
10514
10986
  } finally {
10515
10987
  reader.releaseLock();
10516
10988
  }
@@ -11040,7 +11512,8 @@ function createDiagnostics(stateDir, writeSummary, onFatal) {
11040
11512
  code: safeCode(error),
11041
11513
  at: (/* @__PURE__ */ new Date()).toISOString(),
11042
11514
  stage,
11043
- ...operationId ? { operationId } : {}
11515
+ ...operationId ? { operationId } : {},
11516
+ ...processingSite(error)
11044
11517
  };
11045
11518
  summary(diagnostic);
11046
11519
  if (closed) {
@@ -11076,14 +11549,18 @@ function createDiagnostics(stateDir, writeSummary, onFatal) {
11076
11549
  }
11077
11550
  },
11078
11551
  onDiagnostic(diagnostic, error) {
11079
- summary(diagnostic);
11552
+ const named = {
11553
+ ...diagnostic,
11554
+ ...processingSite(error)
11555
+ };
11556
+ summary(named);
11080
11557
  if (closed) {
11081
- unavailable(diagnostic);
11558
+ unavailable(named);
11082
11559
  return;
11083
11560
  }
11084
11561
  try {
11085
11562
  local.logger.error({
11086
- ...diagnostic,
11563
+ ...named,
11087
11564
  error: errorDetail(error)
11088
11565
  });
11089
11566
  } catch {
@@ -11545,6 +12022,8 @@ async function compose(options, callbacks, bundles) {
11545
12022
  onFatal: fatalStore.fail
11546
12023
  });
11547
12024
  const account = createAccount(accountPorts);
12025
+ const sendAuth = createJourneySendAuth(accountPorts);
12026
+ const accountAccess = createAccountAccess(journeyStorage, sendAuth);
11548
12027
  const application = createApplication({
11549
12028
  sessionPeriod: options.sessionPeriod,
11550
12029
  journeySender: createJourneySender({
@@ -11561,7 +12040,7 @@ async function compose(options, callbacks, bundles) {
11561
12040
  }
11562
12041
  }),
11563
12042
  artifacts: createJourneySendArtifacts(journeyStorage),
11564
- auth: createJourneySendAuth(accountPorts),
12043
+ auth: sendAuth,
11565
12044
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
11566
12045
  failure: (code, cause) => diagnostics.onFailure(code, cause),
11567
12046
  sessionTransfer: (workId, transfer) => diagnostics.onActivity({
@@ -11584,18 +12063,29 @@ async function compose(options, callbacks, bundles) {
11584
12063
  phases: { upload: elapsedMs },
11585
12064
  volumes: { bytes }
11586
12065
  })
11587
- }, void 0, sendHost),
12066
+ }, void 0, sendHost.serving ? sendHost : {
12067
+ ...sendHost,
12068
+ focused: (journeyId) => focusedHostAlive(stateDir, journeyId, async () => (await journeyStorage.current())?.journeyId)
12069
+ }),
11588
12070
  manualUploader: createManualUploader({
11589
12071
  storage: store.manualUpload,
11590
12072
  artifacts: createManualUploadArtifacts({ getDeliverySources: () => application.getDeliverySources() }),
11591
- auth: createJourneySendAuth(accountPorts),
12073
+ auth: sendAuth,
11592
12074
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
11593
12075
  failure: (code, cause) => diagnostics.onFailure(code, cause)
11594
12076
  }),
11595
12077
  journeyLocal: {
12078
+ accountAccess,
12079
+ async accountState() {
12080
+ const view = await account.status();
12081
+ return {
12082
+ configured: view.configuration.clientId !== null,
12083
+ subject: view.state === "signed-in" ? view.identityId : null
12084
+ };
12085
+ },
11596
12086
  titles: createJourneyTitles(journeyStorage, processing),
11597
12087
  describe: createJourneyDescription(journeyStorage, processing),
11598
- works: createJourneyWorks(journeyStorage, processing, stateDir),
12088
+ works: createJourneyWorks(journeyStorage, processing, stateDir, accountAccess),
11599
12089
  hide: createJourneyHide({
11600
12090
  storage: journeyStorage,
11601
12091
  execution,
@@ -11864,7 +12354,14 @@ async function invoke(runtime, command, value, signal) {
11864
12354
  case "journey.findingImpact": return app.journey?.findingImpact(value);
11865
12355
  case "journey.preview": return app.journey?.preview(value);
11866
12356
  case "journey.files": return app.journey?.files(value);
11867
- case "journey.prepare": return app.journey?.prepare(value, signal);
12357
+ case "journey.prepare": {
12358
+ const input = z.object({
12359
+ journeyId: z.uuid(),
12360
+ result: z.literal("receipt").optional()
12361
+ }).strict().parse(value);
12362
+ return input.result === "receipt" ? app.journey?.prepareReceipt({ journeyId: input.journeyId }, signal) : app.journey?.prepare({ journeyId: input.journeyId }, signal);
12363
+ }
12364
+ case "review.reopenEmpty": return app.journey?.reopenEmpty(value);
11868
12365
  case "review.terms": return app.journey?.terms(value);
11869
12366
  case "review.status": return app.journey?.reviewStatus(value);
11870
12367
  case "review.guidance": return app.journey?.guidance(value);
@@ -12064,4 +12561,4 @@ async function cancelOperation(runtime, id) {
12064
12561
  return readOperation(runtime, id);
12065
12562
  }
12066
12563
  //#endregion
12067
- 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 };
12564
+ 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 };