@gitroll/tacitt-cli 0.1.0 → 0.3.0

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 (59) hide show
  1. package/dist/{agent-setup-B85GaVIz.mjs → agent-setup-CLXdKr2I.mjs} +2 -2
  2. package/dist/agent-setup-worker.mjs +5 -5
  3. package/dist/cli.mjs +165 -9
  4. package/dist/codex-title-worker.mjs +1 -1
  5. package/dist/configured-preview-worker.mjs +5 -5
  6. package/dist/configured-session-worker.mjs +25 -4
  7. package/dist/{frame-D1j9R8xX.mjs → frame-waS5FnN5.mjs} +1 -1
  8. package/dist/{heap-tfU61Ehy.mjs → heap-eO_uWpD1.mjs} +63 -9
  9. package/dist/inspection-worker.mjs +57 -0
  10. package/dist/job-rK-Ho9OD.mjs +613 -0
  11. package/dist/{locate-Bn32k2se.mjs → locate-U3-nkird.mjs} +29 -6
  12. package/dist/messages-D3m3bhva.mjs +40 -0
  13. package/dist/{model-7Nu7szNP.mjs → model-BBAnsFAx.mjs} +1 -1
  14. package/dist/operation-executor.mjs +3 -3
  15. package/dist/preview-worker.mjs +1 -1
  16. package/dist/privacy-validator.mjs +1 -1
  17. package/dist/{query-Da-KDbSV.mjs → query-DN99jiRc.mjs} +710 -630
  18. package/dist/{run-CCATsHS2.mjs → run-4G_CvdDj.mjs} +4 -4
  19. package/dist/session-process.mjs +6 -1
  20. package/dist/{src-CPPjOyHZ.mjs → src-B4R5XMEP.mjs} +621 -224
  21. package/dist/{src-BngGpFKd.mjs → src-CPbdnGrk.mjs} +130 -53
  22. package/dist/{src-DynBRDBc.mjs → src-qoLRvX-A.mjs} +34 -11
  23. package/dist/store-database-worker.mjs +1 -1
  24. package/dist/web/assets/index-BkuUI3Eu.js +67 -0
  25. package/dist/web/assets/index-Clg6Pfv_.css +1 -0
  26. package/dist/web/index.html +2 -2
  27. package/package.json +1 -1
  28. package/skills/tacitt/SKILL.md +15 -11
  29. package/skills/tacitt/references/b-method.md +1 -1
  30. package/skills/tacitt/references/classification.md +3 -3
  31. package/skills/tacitt/references/commands.md +19 -14
  32. package/skills/tacitt/references/journey.md +17 -11
  33. package/skills/tacitt/references/parallel-review.md +3 -3
  34. package/skills/tacitt/references/send.md +5 -5
  35. package/skills/tacitt/scripts/INDEX.md +6 -2
  36. package/skills/tacitt/scripts/answer.mjs +29 -8
  37. package/skills/tacitt/scripts/journey-card-consolidation.d.mts +13 -4
  38. package/skills/tacitt/scripts/journey-card-consolidation.mjs +170 -21
  39. package/skills/tacitt/scripts/journey-card-values.d.mts +17 -0
  40. package/skills/tacitt/scripts/journey-card-values.mjs +116 -0
  41. package/skills/tacitt/scripts/journey-classification-progress.mjs +2 -1
  42. package/skills/tacitt/scripts/journey-classification-validation.mjs +49 -28
  43. package/skills/tacitt/scripts/journey-classification.mjs +6 -2
  44. package/skills/tacitt/scripts/journey-consolidation-submit.mjs +42 -5
  45. package/skills/tacitt/scripts/journey-consolidation.d.mts +5 -0
  46. package/skills/tacitt/scripts/journey-consolidation.mjs +4 -1
  47. package/skills/tacitt/scripts/journey-panel-host.d.mts +9 -0
  48. package/skills/tacitt/scripts/journey-panel-host.mjs +56 -0
  49. package/skills/tacitt/scripts/journey-state.d.mts +1 -0
  50. package/skills/tacitt/scripts/journey-state.mjs +38 -6
  51. package/skills/tacitt/scripts/journey-tasks.mjs +5 -3
  52. package/skills/tacitt/scripts/journey.d.mts +4 -0
  53. package/skills/tacitt/scripts/journey.mjs +249 -41
  54. package/skills/tacitt/scripts/privacy-validator.mjs +25 -6
  55. package/skills/tacitt/scripts/review-stream-client.mjs +1 -1
  56. package/skills/tacitt/scripts/review.mjs +24 -3
  57. package/dist/job--TceKk0l.mjs +0 -194
  58. package/dist/web/assets/index-CWQ05PFl.js +0 -67
  59. package/dist/web/assets/index-J-AWt_cW.css +0 -1
@@ -1,10 +1,11 @@
1
- import { A as runConfiguredSessionJob, C as readConfiguredSearchFields, D as captureLogicalJourneySources, E as prepareCollectionProduction, F as sessionWorkerFailure, G as staticMaskCategory, H as readLogicalJourneyFinalMasks, I as privateCollectionDirectory, K as readWorkspacePair, L as defaultParallelism, M as parseCollectionWorkspaceSummary, N as createSharedSessionPool, O as capturedJourneyScope, P as SessionWorkerError, R as gitSeedsProvider, S as readOriginalSearchFields, T as checkReusableBundle, U as readConfiguredOutputInfo, V as privateJourneyDirectory, W as processJourneyMetadataFields, X as createTemporary, Y as createInspectionProcessor, Z as createCleanup, _ as readStaticSessionMasks, a as scanReviewFields, at as windowsAclCodes, c as readSessionScanRecords, et as makePrivateDirectory, g as processJourneyMetadata, h as readCapturedOriginalRows, i as buildWorkScanBatches, it as isWindowsAclFailure, j as readCollectionIndex, k as renderJobDigest, l as scanPreparedSession, n as verifyPreparedSessionSources, nt as restrictToOwner, o as createWorkPieces, p as openConfiguredUpload, r as prepareSessionScans, rt as windowsOwnerOnly, s as readSessionScanFields, t as preparedScanConfiguration, tt as openPrivateFile, v as staticSessionJobs, w as createCollectionArtifacts, y as writeLogicalJourneyBase, z as parallelismLimits } from "./src-BngGpFKd.mjs";
2
- import { Bt as sessionPeriodSchema, Dn as entityDictionaryVersion, F as bClassificationKey, Fn as frameRecords, Ft as termTypeInfo, Gt as safeField, H as codePointLength, Ht as ApplicationError, Jt as reviewFamilies, K as utf16Offset, N as bUnitFragments, Ot as privacyTermKey, Qn as admitGitName, T as bLiteralTerms, Ut as UnfinishedJourneyError, V as bDigest, Wt as safeCode, Xn as createEntityMatcher, Zn as admitCustomTerm, Zt as observeSource, b as validateBReview, c as textDigest, h as reviewFindingsDigest, ht as journeyDescribeSchema, jt as reviewTermsInputSchema, l as validateReview, mn as currentDetectorVersion, n as compileBFindings, r as reviewBatchChunks, s as reviewSubmitSchema, t as locateTerms, yn as createStaticProcessor, z as bAnchorKey, zt as filterInventoryByPeriod } from "./locate-Bn32k2se.mjs";
3
- 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-DynBRDBc.mjs";
4
- import { $ as settingsBatchSchema, $n as bReviewDigest, $r as defaultScopedSettings, $t as journeyResourceQuerySchema, A as accountProfileSchema, Ar as staticSessionResourceSchema, At as sendSettingsSchema, B as inspectionCancelSchema, Br as journeyRenderInputSchema, C as rebindReviewAlignment, Cn as readConfirmedHideChange, Cr as privacyDecisionDigest, Ct as sendPrepareSchema, D as guardedFindingText, Dr as hasCurrentStaticReview, Dt as sendReceiptSchema, E as reviewBatchReuse, En as termHideAdditions, Er as nativeRespondSchema, Et as sendArtifactSchema, F as storedCredentialSchema, Fr as withReadingProgress, Gn as workDecisionDigest, Gr as selectionSchema, Hn as maskedHitsStale, Hr as commandSchema, Ht as reviewCompileSchema, In as confirmedHideGroupIds, Jn as reviewCandidateInputSchema, Jr as stateSchema, Jt as journeySearchResultSchema, Lr as journeyFindingImpactSchema, M as deviceProfileScopeSchema, Mr as isReadingComplete, Mt as currentJourneyAdapters, N as profileUrlMatchesIdentity, Nr as isSelectedWork, Nt as obsoleteJourney, O as staticallyHiddenFindings, Or as staticReviewDigest, Ot as sendReplaceResultSchema, P as readAccountAvatarUrl, Pn as bTermMatcher, Rn as confirmedHideKinds, Rr as journeyFindingsSchema, S as bReviewScope, Sr as assertJourneyPrivacyReady, St as sendChoiceSchema, T as rebindReviewSubmit, Tr as nativeGetSchema, Tt as journeySendSchema, Ur as packageCommandSchema, Ut as journeyContextQuerySchema, V as inspectionStartSchema, Vn as journeyConfirmedHides, Vr as journeyRenderSchema, Vt as journeyReviewStorageQuerySchema, W as inspectionQuerySchema, Wn as retireWorkDescription, Wr as selectionBatchSchema, Xn as reviewRepairInputSchema, Xr as recommendParallelism, Xt as searchScopeDigest, Yn as reviewCandidateResourceSchema, Zn as reviewRepairResourceSchema, Zt as journeyPolicyRespondSchema, _t as emptyTermsRevision, a as validateBCrossSegmentReview, ar as scanSessionResourceSchema, bt as replaceSensitiveTermsSchema, c as reconcileBSession, cr as verifiedScanGitSeeds, ct as resolveFailureLocation, ei as freezeConfiguration, et as settingsQuerySchema, fn as sessionReviewInputSchema, fr as journeyInventoryQuerySchema, ft as manualUploadChoiceSchema, g as hideScopeDigest, gn as sessionUsefulnessInputSchema, gr as emptyPreparationFacts, h as activeJourneyTerms, ht as manualUploadStartSchema, i as locateTermEvidence, in as reviewAttemptResourceSchema, ir as scanPreparationDigest, j as deviceAuthorizationSchema, jr as hasCompleteWorkDescription, k as createAccount, kt as sendResolutionSchema, l as reconstructBFields, lr as scanExclusionResourceSchema, lt as validateFailureLocation, m as splitSelectorValues, mt as manualUploadSchema, n as scanBatchContextDigest, nn as journeyPlaceSchema, o as bCrossSegmentReviewSchema, or as scanGitSeedsId, p as selectedValueMembers, pn as sessionReviewQuerySchema, pt as manualUploadPrepareSchema, qn as workCorrectionBinding, qr as producerVersion, r as scanBatchEquivalenceDigest, rt as appendSensitiveTerms, s as bRereadDigest, sn as sessionReconcileInputSchema, sr as scanGitSeedsResource, st as artifactFailureDetailSchema, t as retireScanGeneration, ti as selectFrozenConfiguration, tr as planBRepair, tt as readWorkspaceOutcomes, u as newJourneyWork, un as sessionRereadInputSchema, ut as manualUploadActionSchema, v as changeJourney, vt as maximumTermsFileBytes, w as rebindReviewOverlay, wr as nativeEventsSchema, wt as sendStartSchema, x as isGenericPublicTerm, xn as confirmedHideAdditions, xr as journeyClassificationSessionsSchema, xt as sendActionSchema, yt as normalizeSensitiveTerms, z as createApplication, zr as journeyPreviewSchema, zt as applyClassification } from "./src-CPPjOyHZ.mjs";
5
- import { $ as schemaVersion, A as inspectionQuerySchema$1, B as privateReviewHash, C as loadAuthenticatedReview, D as sessionUsefulnessRecommendation, E as aggregateUsefulness, F as reuseQuerySchema, G as initializeAndClaim, H as createJourneyPayloadFiles, I as batchQuery, J as withDatabase, K as ownership, L as isPrivateResourceKind, M as sameCandidateRequest, N as validateCandidateAttempt, O as sessionQuery, P as validateUnitCandidate, Q as checkVersion, R as privateResourceHeader, S as loadAcceptedReview, T as loadPrivateResource, U as validateBatchBody, V as batchHeader, W as requireActiveBatch, X as readStoredState, Y as readBundleDirectory, Z as validateState, _ as loadRereadBodies, _t as isSqliteCorruption, a as decodeError, at as inspectStateFiles, b as validateSessionReconciliation, c as dropsAnchor, ct as readResetMarker, d as parseDelivery, dt as syncDirectory, et as beginReset, f as validateCandidate, ft as withStateSnapshot, g as loadReconciliation, gt as isCoordinationBusy, h as loadPreparedBodies, ht as errorCode$1, i as createExecutor, it as hasResetMarker, j as evaluateUnitEvidence, k as resourceSelectionQuerySchema, l as replySchema, lt as recheckPrivatePath, m as loadPreparation, mt as StoreError, n as releaseInheritedHeapFlags, nt as databaseName, o as ProtocolError, ot as present, p as loadStatus, pt as withFileCheckTimings, q as validateDatabase, r as storeHeapLimits, rt as finishReset, s as allowedReplies, st as privatePath, tt as createDatabaseFile, u as operations, ut as stateDirectory, v as loadSessionBodies, w as loadBatch, x as validateSessionBindings, y as validatePreparationSelection, z as termReuseQuerySchema } from "./heap-tfU61Ehy.mjs";
6
- import { a as writeJourneyOutput, c as mergeHideIndexTokens, l as packRenderHideIndex, o as createHideIndexTokens, s as hideIndexTexts, t as journeyOutputJob } from "./job--TceKk0l.mjs";
7
- import { t as createAgentSetupPort } from "./agent-setup-B85GaVIz.mjs";
1
+ import { A as runConfiguredSessionJob, C as readConfiguredSearchFields, D as captureLogicalJourneySources, F as sessionWorkerFailure, G as staticMaskCategory, H as readLogicalJourneyFinalMasks, I as privateCollectionDirectory, K as readWorkspacePair, L as defaultParallelism, M as parseCollectionWorkspaceSummary, N as createSharedSessionPool, O as capturedJourneyScope, P as SessionWorkerError, Q as createCleanup, R as gitSeedsProvider, S as readOriginalSearchFields, T as checkReusableBundle, U as readConfiguredOutputInfo, V as privateJourneyDirectory, W as processJourneyMetadataFields, X as createTemporary, Z as removeNamedTemporarySpools, _ as readStaticSessionMasks, a as scanReviewFields, at as isWindowsAclFailure, c as readSessionScanRecords, g as processJourneyMetadata, h as readCapturedOriginalRows, i as buildWorkScanBatches, it as windowsOwnerOnly, j as readCollectionIndex, l as scanPreparedSession, n as verifyPreparedSessionSources, nt as openPrivateFile, o as createWorkPieces, ot as windowsAclCodes, p as openConfiguredUpload, r as prepareSessionScans, rt as restrictToOwner, s as readSessionScanFields, t as preparedScanConfiguration, tt as makePrivateDirectory, v as staticSessionJobs, w as createCollectionArtifacts, z as parallelismLimits } from "./src-CPbdnGrk.mjs";
2
+ import { Bt as sessionPeriodSchema, Dn as entityDictionaryVersion, F as bClassificationKey, Fn as frameRecords, Ft as termTypeInfo, Gt as safeField, H as codePointLength, Ht as ApplicationError, Jt as reviewFamilies, K as utf16Offset, N as bUnitFragments, Ot as privacyTermKey, Qn as admitGitName, T as bLiteralTerms, Ut as UnfinishedJourneyError, V as bDigest, Wt as safeCode, Xn as createEntityMatcher, Zn as admitCustomTerm, Zt as observeSource, b as validateBReview, c as textDigest, h as reviewFindingsDigest, ht as journeyDescribeSchema, jt as reviewTermsInputSchema, l as validateReview, mn as currentDetectorVersion, n as compileBFindings, r as reviewBatchChunks, s as reviewSubmitSchema, t as locateTerms, yn as createStaticProcessor, z as bAnchorKey } from "./locate-U3-nkird.mjs";
3
+ 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-qoLRvX-A.mjs";
4
+ import { $ as settingsQuerySchema, $n as reviewCandidateInputSchema, $t as journeySearchResultSchema, A as deviceProfileScopeSchema, Ar as nativeEventsSchema, B as validateInspectionBatch, Br as isSelectedWork, Bt as journeyReviewStorageQuerySchema, C as rebindReviewSubmit, Ct as sendStartSchema, D as createAccount, Dt as sendReplaceResultSchema, E as staticallyHiddenFindings, En as confirmedHideAdditions, Er as journeyClassificationSessionsSchema, Et as sendReceiptSchema, Gr as journeyFindingsSchema, Gt as retireScanGeneration, Hr as withReadingProgress, Jr as journeyRenderSchema, Jt as journeyContextQuerySchema, Kn as journeyConfirmedHides, Kr as journeyPreviewSchema, Kt as newJourneyWork, L as createApplication, Lr as hasCompleteWorkDescription, M as readAccountAvatarUrl, Mr as nativeRespondSchema, Mt as obsoleteJourney, N as storedCredentialSchema, Nr as hasCurrentStaticReview, O as accountProfileSchema, On as readConfirmedHideChange, Or as assertJourneyPrivacyReady, Ot as sendResolutionSchema, Pr as staticReviewDigest, Q as settingsBatchSchema, Qn as workCorrectionBinding, Qr as selectionSchema, R as inspectionCancelSchema, Rr as isClassifying, Rt as applyClassification, S as rebindReviewOverlay, St as sendPrepareSchema, T as guardedFindingText, Tt as sendArtifactSchema, U as inspectionQuerySchema, Un as confirmedHideKinds, Ut as classificationPartition, Vn as confirmedHideGroupIds, Wr as journeyFindingImpactSchema, Wt as classificationWorks, Xn as workDecisionDigest, Xr as packageCommandSchema, Yn as retireWorkDescription, Yr as commandSchema, Zr as selectionBatchSchema, _n as sessionReviewInputSchema, _r as journeyInventoryQuerySchema, _t as maximumTermsFileBytes, a as bCrossSegmentReviewSchema, b as bReviewScope, bt as sendActionSchema, c as reconstructBFields, ct as validateFailureLocation, d as selectedValueMembers, dr as scanGitSeedsId, dt as manualUploadChoiceSchema, ei as producerVersion, er as reviewCandidateResourceSchema, et as readWorkspaceOutcomes, f as splitSelectorValues, fn as sessionReconcileInputSchema, fr as scanGitSeedsResource, ft as manualUploadPrepareSchema, g as changeJourney, gt as emptyTermsRevision, hn as sessionRereadInputSchema, i as validateBCrossSegmentReview, in as journeyResourceQuerySchema, ir as bReviewDigest, j as profileUrlMatchesIdentity, jn as termHideAdditions, jr as nativeGetSchema, jt as currentJourneyAdapters, k as deviceAuthorizationSchema, kr as privacyDecisionDigest, kt as sendSettingsSchema, ln as reviewAttemptResourceSchema, lr as scanPreparationDigest, lt as manualUploadActionSchema, m as hideScopeDigest, mr as scanExclusionResourceSchema, mt as manualUploadStartSchema, n as scanBatchEquivalenceDigest, nn as journeyPolicyRespondSchema, nr as reviewRepairResourceSchema, nt as appendSensitiveTerms, o as bRereadDigest, or as planBRepair, ot as artifactFailureDetailSchema, p as activeJourneyTerms, pr as verifiedScanGitSeeds, pt as manualUploadSchema, qn as maskedHitsStale, qr as journeyRenderInputSchema, qt as reviewCompileSchema, r as locateTermEvidence, ri as recommendParallelism, s as reconcileBSession, sn as journeyPlaceSchema, st as resolveFailureLocation, t as scanBatchContextDigest, ti as stateSchema, tn as searchScopeDigest, tr as reviewRepairInputSchema, ur as scanSessionResourceSchema, vn as sessionReviewQuerySchema, vt as normalizeSensitiveTerms, w as reviewBatchReuse, wt as journeySendSchema, x as rebindReviewAlignment, xn as sessionUsefulnessInputSchema, xr as emptyPreparationFacts, xt as sendChoiceSchema, y as isGenericPublicTerm, yt as replaceSensitiveTermsSchema, z as inspectionStartSchema, zn as bTermMatcher, zr as isReadingComplete } from "./src-B4R5XMEP.mjs";
5
+ import { $ as checkVersion, A as resourceSelectionQuerySchema, B as termReuseQuerySchema, C as loadAcceptedReview, D as aggregateUsefulness, E as loadPrivateResource, F as validateUnitCandidate, G as requireActiveBatch, H as batchHeader, I as reuseQuerySchema, J as validateDatabase, K as initializeAndClaim, L as batchQuery, M as evaluateUnitEvidence, N as sameCandidateRequest, O as sessionUsefulnessRecommendation, P as validateCandidateAttempt, Q as validateState, R as isPrivateResourceKind, S as validateSessionBindings, T as loadBatch, U as createJourneyPayloadFiles, V as privateReviewHash, W as validateBatchBody, X as readBundleDirectory, Y as withDatabase, Z as readStoredState, _ as loadReconciliation, _t as isCoordinationBusy, a as decodeError, at as hasResetMarker, b as validatePreparationSelection, c as dropsAnchor, ct as privatePath, d as parseDelivery, dt as stateDirectory, et as schemaVersion, f as validateCandidate, ft as syncDirectory, g as loadPreparedBodies, gt as errorCode$1, h as loadPreparation, ht as StoreError, i as createExecutor, it as finishReset, j as inspectionQuerySchema$1, k as sessionQuery, l as replySchema, lt as readResetMarker, m as loadStatus, mt as withFileCheckTimings, n as releaseInheritedHeapFlags, nt as createDatabaseFile, o as ProtocolError, ot as inspectStateFiles, p as classificationJourneyCommit, pt as withStateSnapshot, q as ownership, r as storeHeapLimits, rt as databaseName, s as allowedReplies, st as present, tt as beginReset, u as operations, ut as recheckPrivatePath, v as loadRereadBodies, vt as isSqliteCorruption, w as loadAuthenticatedReview, x as validateSessionReconciliation, y as loadSessionBodies, z as privateResourceHeader } from "./heap-eO_uWpD1.mjs";
6
+ import { _ as staticBaseProduction, a as writeJourneyOutput, 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 prepareSourcePlan, y as workApproval } from "./job-rK-Ho9OD.mjs";
7
+ import { t as createAgentSetupPort } from "./agent-setup-CLXdKr2I.mjs";
8
+ import { t as workerMessageSchema } from "./messages-D3m3bhva.mjs";
8
9
  import { constants, write } from "node:fs";
9
10
  import { z } from "zod";
10
11
  import { copyFile, link, lstat, mkdir, mkdtemp, open, opendir, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
@@ -650,6 +651,7 @@ const operation = z.enum([
650
651
  "journey.terms.select-aliases",
651
652
  "journey.commit",
652
653
  "journey.commit.checked",
654
+ "journey.classification.commit",
653
655
  "journey.events",
654
656
  "journey.inventory",
655
657
  "journey.preparation.progress",
@@ -826,12 +828,16 @@ function runObserved(frame, leg, work) {
826
828
  }
827
829
  //#endregion
828
830
  //#region packages/local-host/src/router.ts
829
- /** The browser may explicitly change one Work's visibility; all other decisions stay in chat. */
831
+ /** Decisions the person makes in the panel while the agent waits for them. */
830
832
  const nativePanelRespondSchema = z.object({
831
833
  journeyId: nativeRespondSchema.shape.journeyId,
832
834
  expectedRevision: nativeRespondSchema.shape.expectedRevision,
833
835
  idempotencyKey: nativeRespondSchema.shape.idempotencyKey,
834
836
  action: z.union([
837
+ z.object({
838
+ kind: z.literal("consent"),
839
+ allow: z.boolean()
840
+ }).strict(),
835
841
  z.object({
836
842
  kind: z.literal("work-access"),
837
843
  workId: z.uuid(),
@@ -845,12 +851,17 @@ const nativePanelRespondSchema = z.object({
845
851
  kind: z.literal("card-answer"),
846
852
  cardId: z.uuid(),
847
853
  optionId: z.string(),
848
- remember: z.boolean().optional()
854
+ remember: z.boolean().optional(),
855
+ excludedTermKeys: z.array(z.string().min(1)).max(2e4).optional()
849
856
  }).strict(),
850
- z.object({ kind: z.literal("package-confirm") }).strict()
857
+ z.object({ kind: z.literal("package-confirm") }).strict(),
858
+ z.object({
859
+ kind: z.literal("retry-work"),
860
+ workId: z.uuid()
861
+ }).strict()
851
862
  ])
852
863
  }).strict();
853
- function createRouter(application, account, stopping, delivery, nativeInspection) {
864
+ function createRouter(application, account, stopping, delivery, nativeInspection, agentPresence) {
854
865
  const t = initTRPC.context().create({
855
866
  sse: {
856
867
  ping: {
@@ -920,6 +931,7 @@ function createRouter(application, account, stopping, delivery, nativeInspection
920
931
  respond: t.procedure.input(sendChoiceSchema).mutation(({ input }) => sending().respond(input)),
921
932
  pause: t.procedure.input(sendActionSchema).mutation(({ input }) => sending().pause(input)),
922
933
  resume: t.procedure.input(sendActionSchema).mutation(({ input }) => sending().resume(input)),
934
+ withdraw: t.procedure.input(sendActionSchema).mutation(({ input }) => sending().withdraw(input)),
923
935
  advance: t.procedure.input(sendAddress).mutation(({ input }) => sending().advance(input, stopping))
924
936
  }),
925
937
  manualUpload: t.router({
@@ -937,11 +949,13 @@ function createRouter(application, account, stopping, delivery, nativeInspection
937
949
  cardsRead: t.procedure.input(nativeGetSchema.required()).query(({ input }) => native().cardsRead(input)),
938
950
  files: t.procedure.input(journeyRenderSchema).query(({ input, signal }) => native().files(input, signal)),
939
951
  searchResult: t.procedure.input(nativeGetSchema.required()).query(({ input }) => native().searchResult(input)),
952
+ prepare: t.procedure.input(nativeGetSchema.required()).mutation(({ input }) => native().prepare(input, stopping)),
940
953
  render: t.procedure.input(journeyRenderInputSchema).mutation(({ input, signal }) => native().render(input, signal)),
941
954
  findings: t.procedure.input(journeyFindingsSchema).query(({ input, signal }) => native().findings(input, signal)),
942
955
  findingImpact: t.procedure.input(journeyFindingImpactSchema).query(({ input, signal }) => native().findingImpact(input, signal)),
943
956
  preview: t.procedure.input(journeyPreviewSchema).query(({ input, signal }) => native().preview(input, signal)),
944
957
  get: t.procedure.input(nativeGetSchema).query(({ input, signal }) => readThroughContention(() => native().get(input), signal)),
958
+ presence: t.procedure.input(z.object({ journeyId: z.uuid() }).strict()).query(async ({ input }) => ({ agent: await agentPresence?.(input.journeyId) ?? null })),
945
959
  inventory: t.procedure.input(journeyInventoryQuerySchema).query(({ input }) => native().inventory(input)),
946
960
  events: t.procedure.input(nativeEventsSchema).query(({ input, signal }) => readThroughContention(() => native().events(input, signal), signal))
947
961
  }),
@@ -2827,7 +2841,7 @@ async function startHost(options) {
2827
2841
  reply.raw.once("close", () => streams.delete(reply.raw));
2828
2842
  });
2829
2843
  const trpcOptions = {
2830
- router: createRouter(options.application, options.account, stopping.signal, delivery, options.nativeInspection),
2844
+ router: createRouter(options.application, options.account, stopping.signal, delivery, options.nativeInspection, options.agentPresence),
2831
2845
  createContext: ({ req }) => {
2832
2846
  const currentView = requestObservations.get(req);
2833
2847
  return {
@@ -4714,6 +4728,19 @@ function createStagedJourney(access, root, databaseId) {
4714
4728
  });
4715
4729
  });
4716
4730
  },
4731
+ /** BL-216: as `commit`, for a proposal the store applies to its current state. */
4732
+ commitClassification(input) {
4733
+ return access.staged(async (leg, signal) => {
4734
+ const first = await leg.run("journey.classification.commit", input);
4735
+ if (first.status !== "hide-delta") return first;
4736
+ const key = await files.hideKey(first.state.journeyId, signal);
4737
+ const delta = await readConfirmedHideChange({ journeyTerms: (query) => leg.run("journey.terms.read", query) }, key, first.state, first.next);
4738
+ return leg.run("journey.commit", {
4739
+ ...classificationJourneyCommit(input, first.next, first.state.revision),
4740
+ confirmedHideDelta: delta
4741
+ });
4742
+ });
4743
+ },
4717
4744
  resources: createStagedResources(access, files, (input) => inspection.protectedBase(input)),
4718
4745
  saveResource: createStagedResourceSave(access, files),
4719
4746
  async putBatch(batch) {
@@ -4988,6 +5015,7 @@ async function openStore(options, backupObsolete) {
4988
5015
  acknowledgeSessionUsefulness: (input) => stagedJourney.acknowledgeSessionUsefulness(input),
4989
5016
  current: (journeyId) => access.run("journey.current", journeyId ? { journeyId } : {}),
4990
5017
  commit: (input) => stagedJourney.commit(input),
5018
+ commitClassification: (input) => stagedJourney.commitClassification(input),
4991
5019
  hideIndexKey: (journeyId) => stagedJourney.hideIndexKey(journeyId),
4992
5020
  recordTerms: (input) => stagedJourney.recordTerms(input),
4993
5021
  journeyTerms: (input) => stagedJourney.journeyTerms(input),
@@ -5458,6 +5486,10 @@ function parseOptions(args, source = sourceCheckout) {
5458
5486
  type: "boolean",
5459
5487
  default: false
5460
5488
  },
5489
+ detach: {
5490
+ type: "boolean",
5491
+ default: false
5492
+ },
5461
5493
  "state-dir": { type: "string" },
5462
5494
  "bundle-dir": { type: "string" },
5463
5495
  "claude-home": { type: "string" },
@@ -5507,6 +5539,7 @@ function parseOptions(args, source = sourceCheckout) {
5507
5539
  "all"
5508
5540
  ].includes(parsed.values.agent) || parsed.values.scope !== void 0 && !["user", "project"].includes(parsed.values.scope) || parsed.values["cli-path"] !== void 0 && !isAbsolute(parsed.values["cli-path"]) || parsed.values["configure-agents"] !== void 0 && parsed.values["skip-agent-config"] !== void 0) throw new Error("INVALID_ARGUMENTS");
5509
5541
  if (parsed.values.async && (!command || !longCommands.has(command))) throw new Error("INVALID_ARGUMENTS");
5542
+ if (parsed.values.detach && command !== "journey.serve") throw new Error("INVALID_ARGUMENTS");
5510
5543
  const timeoutMs = parsed.values["timeout-ms"] === void 0 ? 3e4 : Number(parsed.values["timeout-ms"]);
5511
5544
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647) throw new Error("INVALID_ARGUMENTS");
5512
5545
  if (parsed.values.output && !stateDump) throw new Error("INVALID_ARGUMENTS");
@@ -5552,6 +5585,9 @@ ${Array.from(businessCommands).join("\n")}
5552
5585
 
5553
5586
  Business commands invoke the shared Application directly without an HTTP server.
5554
5587
  journey.serve starts a focused confirmation host, emits JSON readiness, and waits until stopped.
5588
+ journey.serve --detach starts that host in its own process, prints its readiness with the host's
5589
+ pid, and exits; stop the host with that pid. A detached host already serving the journey is
5590
+ reused and reported with reused: true.
5555
5591
  It does not discover sessions or open a browser. The agent opens its private URL in the right pane.
5556
5592
  Workflow mutations require protocolVersion, runId and expectedRevision in JSON input.
5557
5593
  Long commands emit the operation ID immediately as NDJSON, then wait up to 30000 ms.
@@ -5760,105 +5796,6 @@ function createJourneyFindings(storage) {
5760
5796
  };
5761
5797
  }
5762
5798
  //#endregion
5763
- //#region apps/cli/src/journey/plan.ts
5764
- async function prepareSourcePlan(storage, harnesses, journeyId, sensitiveTerms, signal, period = {}) {
5765
- const saved = (await storage.resources({
5766
- journeyId,
5767
- kind: "plan"
5768
- }))[0];
5769
- if (saved?.kind === "plan") return saved;
5770
- const discovered = await harnesses.discover(signal);
5771
- if (!discovered.complete) throw new ApplicationError("SCOPE_NOT_READY");
5772
- const inventory = filterInventoryByPeriod(discovered, period);
5773
- if (inventory.sessions.length === 0) throw new ApplicationError("INVALID_SELECTION");
5774
- const families = reviewFamilies(inventory);
5775
- const defaults = defaultScopedSettings();
5776
- const settings = {
5777
- ...defaults,
5778
- overall: {
5779
- ...defaults.overall,
5780
- cleanup: {
5781
- ref: {
5782
- id: "native-evidence",
5783
- version: "2"
5784
- },
5785
- options: { assessedSessionsOnly: true }
5786
- }
5787
- },
5788
- workspaces: Object.fromEntries(inventory.workspaces.map((work) => [work.id, {
5789
- parameters: {
5790
- "screened-identifiers": { enabled: false },
5791
- "ip-addresses": { enabled: false },
5792
- hostnames: { enabled: false },
5793
- "cloud-resources": { enabled: false },
5794
- "mac-addresses": { enabled: false },
5795
- "public-key-containers": { enabled: false },
5796
- "geographic-coordinates": { enabled: false },
5797
- urls: { enabled: false },
5798
- "filesystem-paths": { mode: "identity-roots" }
5799
- },
5800
- optionalSteps: { "phone-numbers": true }
5801
- }]))
5802
- };
5803
- const configuration = freezeConfiguration(settings, inventory.sessions.flatMap((session) => {
5804
- const owner = families.owners.get(session.id);
5805
- return owner ? [{
5806
- sessionId: session.id,
5807
- workspaceId: session.workspaceId,
5808
- settingsOwner: {
5809
- sessionId: owner.id,
5810
- workspaceId: owner.workspaceId
5811
- }
5812
- }] : [];
5813
- }));
5814
- const resource = {
5815
- kind: "plan",
5816
- journeyId,
5817
- approval: {
5818
- runId: journeyId,
5819
- revision: 0,
5820
- approvedAt: (/* @__PURE__ */ new Date()).toISOString(),
5821
- inventory,
5822
- sourceChangePolicy: "fail",
5823
- treatment: {
5824
- kind: "configured",
5825
- configuration,
5826
- dictionaryRevision: sensitiveTerms.revision
5827
- }
5828
- },
5829
- replacementKey: randomBytes(32).toString("hex"),
5830
- sensitiveTerms: {
5831
- revision: sensitiveTerms.revision,
5832
- terms: [...sensitiveTerms.terms]
5833
- }
5834
- };
5835
- await storage.saveResource(resource);
5836
- return resource;
5837
- }
5838
- function workApproval(approval, rootIds) {
5839
- if (approval.treatment?.kind !== "configured") throw new ApplicationError("PROCESSING_PLAN_UNAVAILABLE");
5840
- const families = reviewFamilies(approval.inventory);
5841
- const ids = new Set(rootIds.flatMap((id) => {
5842
- const members = families.members.get(id);
5843
- if (!members) throw new ApplicationError("UNKNOWN_ITEM");
5844
- return members.map((session) => session.id);
5845
- }));
5846
- const sessions = approval.inventory.sessions.filter((session) => ids.has(session.id));
5847
- const workspaces = new Set(sessions.map((session) => session.workspaceId));
5848
- return {
5849
- ...approval,
5850
- inventory: {
5851
- ...approval.inventory,
5852
- sessions,
5853
- workspaces: approval.inventory.workspaces.filter((workspace) => workspaces.has(workspace.id))
5854
- },
5855
- treatment: {
5856
- ...approval.treatment,
5857
- configuration: selectFrozenConfiguration(approval.treatment.configuration, ids)
5858
- }
5859
- };
5860
- }
5861
- //#endregion
5862
5799
  //#region apps/cli/src/journey/metadata/processing.ts
5863
5800
  /** Load the frozen plan and active terms once; each Work then resolves its own policy. */
5864
5801
  async function metadataPolicy(storage, state, processing) {
@@ -6060,6 +5997,10 @@ function bindJourneyDestination(storage, destination) {
6060
5997
  await guard(input.journeyId);
6061
5998
  return storage.saveResource(input);
6062
5999
  },
6000
+ async commitClassification(input) {
6001
+ await guard(input.journeyId);
6002
+ return storage.commitClassification(input);
6003
+ },
6063
6004
  async respondPolicy(input) {
6064
6005
  await guard(input.request.journeyId);
6065
6006
  return storage.respondPolicy(input);
@@ -6784,6 +6725,14 @@ async function publishWorkBatches(storage, plan, work, resources, signal) {
6784
6725
  }
6785
6726
  //#endregion
6786
6727
  //#region apps/cli/src/journey/prepare/scan-sessions.ts
6728
+ /** Scan refusals caused by one session's own sources, the codes admission refusals also isolate. */
6729
+ const rootScanRefusals = /* @__PURE__ */ new Set([
6730
+ "ARTIFACT_PROCESSING_UNSUPPORTED",
6731
+ "PROCESSING_UNSUPPORTED",
6732
+ "ARTIFACT_SOURCE_CHANGED",
6733
+ "ARTIFACT_LIMIT",
6734
+ "PROCESSING_LIMIT"
6735
+ ]);
6787
6736
  /**
6788
6737
  * Track A (ADR 0172, ADR 0177): every session of the Work is submitted to the shared pool at once,
6789
6738
  * ahead of Track B, and each completed session's receipt is saved as it arrives. Once every session
@@ -6847,7 +6796,11 @@ async function publishScanSessions(storage, plan, work, prepared, binding, direc
6847
6796
  volumes.scanInputFields += resource.input.fieldCount;
6848
6797
  volumes.scanFileBytes += resource.input.file.byteLength;
6849
6798
  }, (error) => {
6850
- failure ??= { error };
6799
+ const rootSessionId = families.owners.get(selected.session.id)?.id;
6800
+ failure ??= rootSessionId ? {
6801
+ error,
6802
+ rootSessionId
6803
+ } : { error };
6851
6804
  withdraw.abort();
6852
6805
  });
6853
6806
  if (pooled) {
@@ -6885,7 +6838,11 @@ async function publishScanSessions(storage, plan, work, prepared, binding, direc
6885
6838
  ...paragraphs ? { scanHostedCopies: paragraphs.hosted } : {}
6886
6839
  }
6887
6840
  });
6888
- if (failure) throw failure.error;
6841
+ if (failure) {
6842
+ const code = safeCode(failure.error);
6843
+ if (!hooks.signal.aborted && failure.rootSessionId && rootScanRefusals.has(code)) throw new RootPreparationFailure(code, /* @__PURE__ */ new Map([[failure.rootSessionId, code]]), { cause: failure.error });
6844
+ throw failure.error;
6845
+ }
6889
6846
  hooks.signal.throwIfAborted();
6890
6847
  if (resources.length !== prepared.sessions.length) throw new ApplicationError("SCOPE_NOT_READY");
6891
6848
  return {
@@ -7111,223 +7068,6 @@ async function prepareJourneyWork(storage, plan, work, directory, processing, ho
7111
7068
  }
7112
7069
  }
7113
7070
  //#endregion
7114
- //#region apps/cli/src/journey/static/production.ts
7115
- /**
7116
- * The single production both Track B and packaging use for one Work generation (ADR 0172):
7117
- * frozen captures, built-in static detection only (no custom terms, no Git seeds), the Work's
7118
- * exceptions and a replacement key bound to this generation. Identical inputs give identical
7119
- * session render jobs, so packaging can require the stored Track B result by job digest.
7120
- */
7121
- async function staticBaseProduction(storage, state, work, processing, hooks) {
7122
- const excluded = work.excludedReview?.snapshotId === state.snapshotId && work.excludedReview.inputRevision === work.inputRevision ? new Set(work.excludedReview.rootSessionIds) : /* @__PURE__ */ new Set();
7123
- const roots = work.rootSessionIds.filter((id) => !excluded.has(id));
7124
- if (!roots.length) return void 0;
7125
- const plan = (await storage.resources({
7126
- journeyId: state.journeyId,
7127
- kind: "plan"
7128
- }))[0];
7129
- if (plan?.kind !== "plan") throw new ApplicationError("CORRUPTED_STATE");
7130
- const approval = workApproval(plan.approval, roots);
7131
- if (approval.treatment?.kind !== "configured") throw new ApplicationError("CORRUPTED_STATE");
7132
- const parts = new Set(approval.inventory.sessions.flatMap((session) => session.parts.map((part) => part.id)));
7133
- const captures = (await storage.resources({
7134
- journeyId: state.journeyId,
7135
- kind: "capture"
7136
- })).flatMap((item) => item.kind === "capture" && parts.has(item.capture.partId) ? [item.capture] : []);
7137
- if (captures.length !== parts.size) throw new ApplicationError("SCOPE_NOT_READY");
7138
- const { sensitiveTerms: _live, customTerms: _terms, staticExceptions: _unscoped, ...local } = processing;
7139
- const production = await prepareCollectionProduction(approval, state.journeyId, {
7140
- configured: {
7141
- ...local,
7142
- gitNames: async () => ({
7143
- names: [],
7144
- snapshots: []
7145
- }),
7146
- replacementKey: createHash("sha256").update(JSON.stringify([
7147
- "static-base-v1",
7148
- plan.replacementKey,
7149
- work.id,
7150
- work.inputRevision
7151
- ])).digest("hex"),
7152
- staticExceptions: {
7153
- workId: work.id,
7154
- values: state.exceptions.filter((item) => item.workId === work.id).map(({ kind, value }) => ({
7155
- kind,
7156
- value
7157
- }))
7158
- }
7159
- },
7160
- captures,
7161
- everyAdmissionRefusal: true
7162
- }, hooks);
7163
- if (production.kind !== "configured" || production.workspaceFailures.length) throw new ApplicationError("SCOPE_NOT_READY");
7164
- const families = reviewFamilies(approval.inventory);
7165
- const admitted = new Set(production.selection.sessions.map((item) => item.session.id));
7166
- return {
7167
- production,
7168
- families,
7169
- work: {
7170
- ...work,
7171
- rootSessionIds: roots.filter((id) => families.members.get(id)?.some((member) => admitted.has(member.id))),
7172
- access: work.access === "private" ? "private" : "public"
7173
- }
7174
- };
7175
- }
7176
- //#endregion
7177
- //#region apps/cli/src/journey/static/store.ts
7178
- /** Identity, length and complete digest of one owner-only file; a symlink is never followed. */
7179
- async function describe(path, signal) {
7180
- const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
7181
- try {
7182
- const before = await handle.stat({ bigint: true });
7183
- if (!before.isFile()) throw new ApplicationError("CORRUPTED_STATE");
7184
- const digest = createHash("sha256");
7185
- let byteLength = 0;
7186
- const buffer = Buffer.allocUnsafe(1048576);
7187
- for (;;) {
7188
- signal.throwIfAborted();
7189
- const { bytesRead } = await handle.read(buffer, 0, buffer.length, byteLength);
7190
- if (!bytesRead) break;
7191
- digest.update(buffer.subarray(0, bytesRead));
7192
- byteLength += bytesRead;
7193
- }
7194
- const after = await handle.stat({ bigint: true });
7195
- if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.mtimeNs !== before.mtimeNs || BigInt(byteLength) !== after.size) throw new ApplicationError("CORRUPTED_STATE");
7196
- return {
7197
- path,
7198
- identity: {
7199
- dev: String(after.dev),
7200
- ino: String(after.ino)
7201
- },
7202
- sha256: digest.digest("hex"),
7203
- byteLength
7204
- };
7205
- } finally {
7206
- await handle.close();
7207
- }
7208
- }
7209
- /** A stored file must still be the exact recorded file and bytes. */
7210
- async function authenticate(file, signal) {
7211
- const actual = await describe(file.path, signal);
7212
- if (JSON.stringify(actual) !== JSON.stringify(file)) throw new ApplicationError("CORRUPTED_STATE");
7213
- }
7214
- /**
7215
- * Persist one Track B session as owner-only private state beside the journey's other scan
7216
- * files: its three spools and a digest-bound result description. Nothing here is uploaded.
7217
- */
7218
- async function saveStaticSession(storage, binding, item, result, signal) {
7219
- const owned = [
7220
- result.spoolPath,
7221
- result.companionSpoolPath,
7222
- result.maskSpoolPath
7223
- ];
7224
- const resultPath = join(binding.directory, `${randomUUID()}.static.json`);
7225
- let saved = false;
7226
- try {
7227
- if (result.kind !== "render" || result.maskSpoolPath === void 0 || result.maskEntries === void 0 || result.metadataMasks === void 0 || result.reviewSpoolPath !== void 0) throw new ApplicationError("CORRUPTED_STATE");
7228
- const stored = {
7229
- kind: "render",
7230
- processingMs: result.processingMs,
7231
- maskEntries: result.maskEntries,
7232
- metadataMasks: result.metadataMasks,
7233
- metadata: result.metadata,
7234
- parts: result.parts,
7235
- entries: result.entries,
7236
- agentSetup: result.agentSetup
7237
- };
7238
- const file = await open(resultPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 384);
7239
- owned.push(resultPath);
7240
- try {
7241
- await file.writeFile(JSON.stringify(stored));
7242
- await file.sync();
7243
- } finally {
7244
- await file.close();
7245
- }
7246
- const resource = staticSessionResourceSchema.parse({
7247
- kind: "static-session",
7248
- journeyId: binding.journeyId,
7249
- workId: binding.work.id,
7250
- inputRevision: binding.work.inputRevision,
7251
- snapshotId: binding.snapshotId,
7252
- sessionId: item.sessionId,
7253
- jobDigest: item.digest,
7254
- result: await describe(resultPath, signal),
7255
- primary: await describe(result.spoolPath, signal),
7256
- companion: await describe(result.companionSpoolPath, signal),
7257
- masks: await describe(result.maskSpoolPath, signal)
7258
- });
7259
- await storage.saveResource(resource);
7260
- saved = true;
7261
- return resource;
7262
- } finally {
7263
- if (!saved) {
7264
- for (const path of owned) if (path !== void 0) await unlink(path).catch(() => {});
7265
- }
7266
- }
7267
- }
7268
- /** Current-generation Track B receipts of one Work, keyed by application session ID. */
7269
- async function staticSessions(storage, journeyId, work) {
7270
- const resources = (await storage.resources({
7271
- journeyId,
7272
- kind: "static-session",
7273
- idPrefix: `${work.id}:${work.inputRevision}:`
7274
- })).filter((item) => item.kind === "static-session" && item.workId === work.id && item.inputRevision === work.inputRevision);
7275
- return new Map(resources.map((item) => [item.sessionId, item]));
7276
- }
7277
- /**
7278
- * Shape guard like the worker transport's: the bytes are bound by the recorded digest, and the
7279
- * archive writer checks parts, entries and masks again when it consumes them.
7280
- */
7281
- function isStoredResult(value) {
7282
- return typeof value === "object" && value !== null && Reflect.get(value, "kind") === "render" && Array.isArray(Reflect.get(value, "maskEntries")) && Array.isArray(Reflect.get(value, "metadataMasks")) && Array.isArray(Reflect.get(value, "parts")) && Array.isArray(Reflect.get(value, "entries")) && Array.isArray(Reflect.get(value, "agentSetup")) && typeof Reflect.get(value, "metadata") === "string" && Number.isSafeInteger(Reflect.get(value, "processingMs"));
7283
- }
7284
- /** Authenticated result description of one stored Track B session. */
7285
- async function readStaticResult(resource, signal) {
7286
- await authenticate(resource.result, signal);
7287
- const handle = await open(resource.result.path, constants.O_RDONLY | constants.O_NOFOLLOW);
7288
- try {
7289
- const text = await handle.readFile("utf8");
7290
- if (createHash("sha256").update(text).digest("hex") !== resource.result.sha256) throw new ApplicationError("CORRUPTED_STATE");
7291
- const value = JSON.parse(text);
7292
- if (!isStoredResult(value)) throw new ApplicationError("CORRUPTED_STATE");
7293
- return value;
7294
- } finally {
7295
- await handle.close();
7296
- }
7297
- }
7298
- /**
7299
- * Packaging from the Track B base: the stored result of exactly this job, with fresh hard links
7300
- * to its authenticated spools that the archive writer may consume. Static detection never runs.
7301
- */
7302
- async function linkedStaticResult(resource, job, signal) {
7303
- if (!resource || resource.jobDigest !== job.digest) throw new ApplicationError("SCOPE_NOT_READY");
7304
- const result = await readStaticResult(resource, signal);
7305
- const links = [];
7306
- try {
7307
- const linked = async (file) => {
7308
- const path = join(job.spoolDirectory, `${randomUUID()}.session`);
7309
- await link(file.path, path);
7310
- links.push(path);
7311
- const stat = await lstat(path, { bigint: true });
7312
- if (String(stat.dev) !== file.identity.dev || String(stat.ino) !== file.identity.ino) throw new ApplicationError("CORRUPTED_STATE");
7313
- await authenticate({
7314
- ...file,
7315
- path
7316
- }, signal);
7317
- return path;
7318
- };
7319
- return {
7320
- ...result,
7321
- spoolPath: await linked(resource.primary),
7322
- companionSpoolPath: await linked(resource.companion),
7323
- maskSpoolPath: await linked(resource.masks)
7324
- };
7325
- } catch (error) {
7326
- for (const path of links) await unlink(path).catch(() => {});
7327
- throw error;
7328
- }
7329
- }
7330
- //#endregion
7331
7071
  //#region apps/cli/src/journey/static/run.ts
7332
7072
  /**
7333
7073
  * Track B for one Work generation (ADR 0172): render every admitted session with all built-in
@@ -7413,6 +7153,7 @@ async function prepareStaticWork(options) {
7413
7153
  const target = current.works.find((item) => item.id === work.id);
7414
7154
  if (current.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
7415
7155
  if (current.snapshotId !== snapshotId || target?.inputRevision !== work.inputRevision) throw new ApplicationError("REVISION_CONFLICT");
7156
+ const images = resources.map((resource) => resource.imagesOmitted);
7416
7157
  return {
7417
7158
  ...current,
7418
7159
  works: current.works.map((item) => item.id === work.id ? {
@@ -7420,7 +7161,8 @@ async function prepareStaticWork(options) {
7420
7161
  staticReview: {
7421
7162
  snapshotId,
7422
7163
  inputRevision: work.inputRevision,
7423
- digest: staticReviewDigest(resources)
7164
+ digest: staticReviewDigest(resources),
7165
+ ...images.every((count) => count !== void 0) ? { imagesOmitted: images.reduce((sum, count) => sum + count, 0) } : {}
7424
7166
  }
7425
7167
  } : item)
7426
7168
  };
@@ -7657,8 +7399,8 @@ function createJourneyPreparation(options) {
7657
7399
  }));
7658
7400
  if (!state.works.length) throw new ApplicationError("INVALID_SELECTION");
7659
7401
  if (!scanRequested) await captureSortingScope(storage, journeyId, workApproval(plan.approval, state.works.flatMap((work) => work.rootSessionIds)), directory, abort.signal);
7660
- const works = scanRequested && state.scope.status === "confirmed" ? state.works : [];
7661
- if (works.length) run = await runWorkers(options.workers);
7402
+ const scanWorks = scanRequested && state.scope.status === "confirmed" ? state.works : [];
7403
+ if (scanWorks.length) run = await runWorkers(options.workers);
7662
7404
  const frozen = state;
7663
7405
  const blockWork = (work, error) => changeJourney(storage, journeyId, "snapshot", (before) => {
7664
7406
  if (before.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
@@ -7672,8 +7414,8 @@ function createJourneyPreparation(options) {
7672
7414
  });
7673
7415
  }, 100);
7674
7416
  let submittedWorks = 0;
7675
- const submitted = Promise.withResolvers();
7676
- if (!works.length) submitted.resolve();
7417
+ let works = [];
7418
+ let submitted = Promise.withResolvers();
7677
7419
  const submission = () => {
7678
7420
  let done = false;
7679
7421
  return () => {
@@ -7694,6 +7436,14 @@ function createJourneyPreparation(options) {
7694
7436
  return false;
7695
7437
  }
7696
7438
  };
7439
+ /** D2: the person dropped this Work while it was prepared; the other Works go on. */
7440
+ const droppedMidScan = async (workId) => {
7441
+ const now = await storage.current(journeyId);
7442
+ const work = now?.works.find((item) => item.id === workId);
7443
+ return Boolean(now && now.phase !== "paused" && now.scope.status === "confirmed" && work && !isSelectedWork(work));
7444
+ };
7445
+ /** Scan generations this run prepared, including those its own isolation started. */
7446
+ const prepared = /* @__PURE__ */ new Set();
7697
7447
  /** True when failing roots left the Work, which is then prepared again (ADR 0188). */
7698
7448
  const prepareWorkOnce = async (planned, onSubmitted) => {
7699
7449
  abort.signal.throwIfAborted();
@@ -7707,6 +7457,7 @@ function createJourneyPreparation(options) {
7707
7457
  "reading",
7708
7458
  "review"
7709
7459
  ].includes(work.status)) return false;
7460
+ prepared.add(`${work.id}:${work.inputRevision}`);
7710
7461
  await prepareClassificationMetadata(storage, latest, work, options.processing, abort.signal);
7711
7462
  let failures = collectPreparationFailures(plan.approval.inventory, work.rootSessionIds, {
7712
7463
  journeyId,
@@ -7723,6 +7474,7 @@ function createJourneyPreparation(options) {
7723
7474
  onTiming: (timing) => options.onTiming?.(planned.id, timing)
7724
7475
  }, run.workers, (revised) => {
7725
7476
  work = revised;
7477
+ prepared.add(`${revised.id}:${revised.inputRevision}`);
7726
7478
  failures = collectPreparationFailures(plan.approval.inventory, revised.rootSessionIds, {
7727
7479
  journeyId,
7728
7480
  snapshotId: latest.snapshotId,
@@ -7735,7 +7487,10 @@ function createJourneyPreparation(options) {
7735
7487
  const code = safeCode(error);
7736
7488
  if (code !== "OPERATION_CANCELED") options.onFailure(error, failures.read());
7737
7489
  abort.signal.throwIfAborted();
7738
- if (code === "OPERATION_CANCELED") throw error;
7490
+ if (code === "OPERATION_CANCELED") {
7491
+ if (await droppedMidScan(planned.id)) return false;
7492
+ throw error;
7493
+ }
7739
7494
  if (work && await isolateRoots(work, error)) return true;
7740
7495
  if (work) await blockWork(work, error);
7741
7496
  }
@@ -7745,14 +7500,14 @@ function createJourneyPreparation(options) {
7745
7500
  for (let attempt = 0; attempt < planned.rootSessionIds.length; attempt++) if (!await prepareWorkOnce(planned, onSubmitted)) return;
7746
7501
  };
7747
7502
  /** Track B for one Work; its failure blocks that Work like a Track A failure would. */
7748
- const staticWork = async (planned) => {
7749
- await submitted.promise;
7503
+ const staticWork = async (planned, passSubmitted) => {
7504
+ await passSubmitted;
7750
7505
  abort.signal.throwIfAborted();
7751
7506
  const latest = await storage.current(journeyId);
7752
7507
  abort.signal.throwIfAborted();
7753
7508
  if (!latest?.consent || latest.phase === "paused" || latest.scope.status !== "confirmed") throw new ApplicationError("OPERATION_CANCELED");
7754
7509
  const work = latest.works.find((item) => item.id === planned.id);
7755
- if (!work) return;
7510
+ if (!work || !isSelectedWork(work)) return;
7756
7511
  try {
7757
7512
  await inBackgroundStoreLane(() => prepareStaticWork({
7758
7513
  storage,
@@ -7767,6 +7522,7 @@ function createJourneyPreparation(options) {
7767
7522
  } catch (error) {
7768
7523
  abort.signal.throwIfAborted();
7769
7524
  const code = safeCode(error);
7525
+ if (code === "OPERATION_CANCELED" && await droppedMidScan(work.id)) return;
7770
7526
  if (code === "COORDINATION_BUSY" || code === "OPERATION_CANCELED") throw error;
7771
7527
  options.onFailure(error);
7772
7528
  await blockWork(work, error);
@@ -7779,23 +7535,50 @@ function createJourneyPreparation(options) {
7779
7535
  abort.abort(error);
7780
7536
  };
7781
7537
  const statics = [];
7782
- await Promise.all(Array.from({ length: run.pool ? works.length : Math.min(1, works.length) }, async () => {
7783
- for (let work = works[next++]; work && !fatal; work = works[next++]) {
7784
- const planned = work;
7785
- const onSubmitted = submission();
7786
- try {
7787
- await retryContention(() => prepareWork(planned, onSubmitted), (error) => options.onFailure(error), abort.signal);
7788
- } catch (error) {
7789
- stop(error);
7790
- } finally {
7791
- onSubmitted();
7538
+ const pass = async (batch) => {
7539
+ works = batch;
7540
+ next = 0;
7541
+ submittedWorks = 0;
7542
+ submitted = Promise.withResolvers();
7543
+ if (!works.length) submitted.resolve();
7544
+ const passSubmitted = submitted.promise;
7545
+ await Promise.all(Array.from({ length: run.pool ? works.length : Math.min(1, works.length) }, async () => {
7546
+ for (let work = works[next++]; work && !fatal; work = works[next++]) {
7547
+ const planned = work;
7548
+ const onSubmitted = submission();
7549
+ try {
7550
+ await retryContention(() => prepareWork(planned, onSubmitted), (error) => options.onFailure(error), abort.signal);
7551
+ } catch (error) {
7552
+ stop(error);
7553
+ } finally {
7554
+ onSubmitted();
7555
+ }
7556
+ statics.push(retryContention(() => staticWork(planned, passSubmitted), (error) => options.onFailure(error), abort.signal).catch(stop));
7792
7557
  }
7793
- statics.push(retryContention(() => staticWork(planned), (error) => options.onFailure(error), abort.signal).catch(stop));
7558
+ }));
7559
+ while (submittedWorks < works.length) submission()();
7560
+ };
7561
+ /** Selected Works this run has not prepared yet, waiting while grouping continues (E2). */
7562
+ const later = async () => {
7563
+ for (;;) {
7564
+ abort.signal.throwIfAborted();
7565
+ const latest = await storage.current(journeyId);
7566
+ if (!latest || latest.phase === "paused" || latest.scope.status !== "confirmed") return [];
7567
+ const ready = latest.works.filter((work) => isSelectedWork(work) && [
7568
+ "pending",
7569
+ "reading",
7570
+ "review"
7571
+ ].includes(work.status) && !prepared.has(`${work.id}:${work.inputRevision}`));
7572
+ if (ready.length || !isClassifying(latest)) return ready;
7573
+ await setTimeout$1(laterWorksPollMs, void 0, { signal: abort.signal }).catch(() => abort.signal.throwIfAborted());
7794
7574
  }
7795
- }));
7796
- while (submittedWorks < works.length) submission()();
7575
+ };
7576
+ for (let batch = scanWorks; batch.length && !fatal; batch = await later()) {
7577
+ if (!run.workers && !run.pool) run = await runWorkers(options.workers);
7578
+ await pass(batch);
7579
+ }
7797
7580
  await Promise.all(statics);
7798
- if (works.length) options.onTiming?.(journeyId, {
7581
+ if (scanWorks.length) options.onTiming?.(journeyId, {
7799
7582
  scope: "preparation",
7800
7583
  phases: { "journey-prepare": Math.round(performance$1.now() - startedAt) },
7801
7584
  volumes: {
@@ -7820,6 +7603,8 @@ function createJourneyPreparation(options) {
7820
7603
  }
7821
7604
  };
7822
7605
  }
7606
+ /** How often a run waiting for grouping to publish more Works checks the journey (E2). */
7607
+ const laterWorksPollMs = 2e3;
7823
7608
  /**
7824
7609
  * Another process can hold the store past one admission deadline (ADR 0082); the Work runs
7825
7610
  * again after a bounded backoff instead of stopping the pass. A pending Work re-prepares
@@ -7863,7 +7648,7 @@ async function runWorkers(workers) {
7863
7648
  //#endregion
7864
7649
  //#region apps/cli/src/journey/package-base.ts
7865
7650
  /** Package-only staging from the complete Track B base; scanning never calls this. */
7866
- async function packageBase(storage, state, work, processing, directory, signal, readState = () => storage.current(state.journeyId)) {
7651
+ async function packageBase(storage, state, work, processing, directory, signal, readState = () => storage.current(state.journeyId), write = writeJourneyBase) {
7867
7652
  assertJourneyPrivacyReady(state, true);
7868
7653
  const existing = (await storage.resources({
7869
7654
  journeyId: state.journeyId,
@@ -7871,53 +7656,18 @@ async function packageBase(storage, state, work, processing, directory, signal,
7871
7656
  id: `${work.id}:${work.inputRevision}`
7872
7657
  })).find((item) => item.kind === "base" && item.workId === work.id && item.inputRevision === work.inputRevision);
7873
7658
  if (existing?.kind === "base") return existing;
7874
- const hooks = {
7875
- signal,
7876
- onTemporary: async () => {},
7877
- onPhase: async () => {}
7878
- };
7879
- const prepared = await staticBaseProduction(storage, state, work, processing, hooks);
7880
- if (!prepared) throw new ApplicationError("ARTIFACT_EMPTY_SCOPE");
7881
- const { families } = prepared;
7882
- const stored = await staticSessions(storage, state.journeyId, work);
7883
- const written = await writeLogicalJourneyBase({
7884
- production: prepared.production,
7659
+ const inputs = await staticBaseInputs(storage, state, work);
7660
+ if (!inputs) throw new ApplicationError("ARTIFACT_EMPTY_SCOPE");
7661
+ const base = await write({
7662
+ inputs,
7663
+ stored: [...(await staticSessions(storage, state.journeyId, work)).values()],
7885
7664
  directory,
7886
- captureReview: false,
7887
- work: prepared.work,
7888
- prerendered: async (job) => {
7889
- if (job.spoolDirectory === void 0) throw new ApplicationError("CORRUPTED_STATE");
7890
- return linkedStaticResult(stored.get(job.session.id), {
7891
- digest: renderJobDigest(job),
7892
- spoolDirectory: job.spoolDirectory
7893
- }, signal);
7894
- }
7895
- }, hooks);
7665
+ processing
7666
+ }, signal);
7896
7667
  const after = await readState();
7897
7668
  const current = after?.works.find((item) => item.id === work.id);
7898
7669
  if (!after || after.snapshotId !== state.snapshotId || after.configurationId !== state.configurationId || current?.revision !== work.revision || current.inputRevision !== work.inputRevision || after.pendingHideId) throw new ApplicationError("REVISION_CONFLICT");
7899
7670
  assertJourneyPrivacyReady(after, true);
7900
- const base = {
7901
- kind: "base",
7902
- journeyId: state.journeyId,
7903
- workId: work.id,
7904
- inputRevision: work.inputRevision,
7905
- bundleId: written.bundleId,
7906
- primary: written.primary,
7907
- companion: written.companion,
7908
- maskLedger: written.maskLedger,
7909
- sourceBindings: [...written.sourceBindings],
7910
- sessions: written.sessionBindings.map((binding) => {
7911
- const root = families.owners.get(binding.applicationSessionId);
7912
- if (!root) throw new ApplicationError("CORRUPTED_STATE");
7913
- return {
7914
- applicationSessionId: binding.applicationSessionId,
7915
- archiveSessionId: binding.sessionId,
7916
- rootSessionId: root.id,
7917
- role: root.id === binding.applicationSessionId ? "main" : "worker"
7918
- };
7919
- })
7920
- };
7921
7671
  await storage.saveResource(base);
7922
7672
  return base;
7923
7673
  }
@@ -7931,12 +7681,21 @@ const priorities = [
7931
7681
  ];
7932
7682
  //#endregion
7933
7683
  //#region apps/cli/src/journey/keywords.ts
7684
+ /** The journey's frozen source plan; it is saved once and never rewritten. */
7685
+ async function readJourneyPlan(storage, journeyId) {
7686
+ const plan = (await storage.resources({
7687
+ journeyId,
7688
+ kind: "plan"
7689
+ }))[0];
7690
+ if (plan?.kind !== "plan") throw new ApplicationError("CORRUPTED_STATE");
7691
+ return plan;
7692
+ }
7934
7693
  /**
7935
7694
  * ADR 0172: the confirmed dictionary applied once, as the last step before the ZIP — custom
7936
7695
  * terms, the snapshot-frozen Git seeds of the given sessions' workspaces (ADR 0169, never live
7937
7696
  * Git) and AI-confirmed Hidden values. Neither scan track applies any of these.
7938
7697
  */
7939
- async function finalKeywordDictionary(storage, state, sessionIds) {
7698
+ async function finalKeywordDictionary(storage, state, sessionIds, plan) {
7940
7699
  if (!state.snapshotId) throw new ApplicationError("CONFIRMATION_REQUIRED");
7941
7700
  const dictionary = /* @__PURE__ */ new Map();
7942
7701
  const add = (term, kind, bounded) => {
@@ -7951,16 +7710,12 @@ async function finalKeywordDictionary(storage, state, sessionIds) {
7951
7710
  const admitted = admitCustomTerm(value);
7952
7711
  if (admitted) add(admitted.value, "private", false);
7953
7712
  }
7954
- const plan = (await storage.resources({
7955
- journeyId: state.journeyId,
7956
- kind: "plan"
7957
- }))[0];
7958
- if (plan?.kind !== "plan") throw new ApplicationError("CORRUPTED_STATE");
7713
+ const { approval } = plan ?? await readJourneyPlan(storage, state.journeyId);
7959
7714
  const sessions = new Set(sessionIds);
7960
7715
  const roots = /* @__PURE__ */ new Map();
7961
- for (const session of plan.approval.inventory.sessions) {
7716
+ for (const session of approval.inventory.sessions) {
7962
7717
  if (!sessions.has(session.id)) continue;
7963
- const workspace = plan.approval.inventory.workspaces.find((item) => item.id === session.workspaceId);
7718
+ const workspace = approval.inventory.workspaces.find((item) => item.id === session.workspaceId);
7964
7719
  if (!workspace) throw new ApplicationError("CORRUPTED_STATE");
7965
7720
  const paths = workspace.path === null ? [] : [workspace.path];
7966
7721
  roots.set(JSON.stringify(paths), paths);
@@ -8307,8 +8062,26 @@ function createRenderWorkers(workers, size) {
8307
8062
  return pool.serial(() => writeJourneyOutput(work, signal));
8308
8063
  }
8309
8064
  };
8065
+ const writeBase = async (work, signal) => {
8066
+ const named = /* @__PURE__ */ new Set();
8067
+ try {
8068
+ const result = await pool.acquire().request({
8069
+ kind: "journey-base",
8070
+ job: journeyBaseJob(work)
8071
+ }, signal, { onSpool: (path) => named.add(path) });
8072
+ if (result.kind !== "journey-base") throw new Error("ARTIFACT_PROCESSING_FAILED");
8073
+ return result.base;
8074
+ } catch (error) {
8075
+ await removeNamedTemporarySpools(named).catch((cleanup) => workers.onFallback?.(cleanup));
8076
+ if (error instanceof SessionWorkerError) throw new ApplicationError(error.message, void 0, { cause: error });
8077
+ if (signal.aborted) throw error;
8078
+ workers.onFallback?.(error);
8079
+ return pool.serial(() => writeJourneyBase(work, signal));
8080
+ }
8081
+ };
8310
8082
  return {
8311
8083
  write,
8084
+ writeBase,
8312
8085
  close: () => pool.discard()
8313
8086
  };
8314
8087
  }
@@ -8486,7 +8259,7 @@ function createJourneyRenderer(options) {
8486
8259
  work
8487
8260
  };
8488
8261
  }
8489
- async function produce(state, work, signal, write, session) {
8262
+ async function produce(state, work, signal, write, session, plan, writeBase) {
8490
8263
  const { journeyId } = state;
8491
8264
  const workId = work.id;
8492
8265
  assertJourneyPrivacyReady(state, true);
@@ -8518,12 +8291,12 @@ function createJourneyRenderer(options) {
8518
8291
  });
8519
8292
  const next = moved.works.find((item) => item.id === workId);
8520
8293
  if (!next) throw new ApplicationError("REVISION_CONFLICT");
8521
- return produce(moved, next, signal, write, session);
8294
+ return produce(moved, next, signal, write, session, plan, writeBase);
8522
8295
  }
8523
8296
  if (!output) {
8524
8297
  const directory = await journeyDirectory(options.stateDir, journeyId);
8525
- await packageBase(storage, state, work, options.processing, directory, signal, () => session.current());
8526
- const { base, tokens, confirmed, request } = await journeyOutputRequest(storage, state, work, options.processing, signal);
8298
+ await packageBase(storage, state, work, options.processing, directory, signal, () => session.current(), writeBase);
8299
+ const { base, tokens, confirmed, request } = await journeyOutputRequest(storage, state, work, options.processing, signal, await plan());
8527
8300
  const { output: written, tokens: collected } = await write({
8528
8301
  request: {
8529
8302
  ...request,
@@ -8614,10 +8387,10 @@ function createJourneyRenderer(options) {
8614
8387
  };
8615
8388
  });
8616
8389
  }
8617
- async function render(journeyId, workId, signal, write = writeJourneyOutput, session = directRenderSession(storage, journeyId)) {
8390
+ async function render(journeyId, workId, signal, write = writeJourneyOutput, session = directRenderSession(storage, journeyId), plan = () => readJourneyPlan(storage, journeyId), writeBase = writeJourneyBase) {
8618
8391
  const { state, work } = await admit(workId, session);
8619
8392
  try {
8620
- return await produce(state, work, signal, write, session);
8393
+ return await produce(state, work, signal, write, session, plan, writeBase);
8621
8394
  } catch (error) {
8622
8395
  if (signal.aborted) throw error;
8623
8396
  const renderFailure = {
@@ -8640,19 +8413,26 @@ function createJourneyRenderer(options) {
8640
8413
  }
8641
8414
  }
8642
8415
  /**
8643
- * A several-Work render writes each Work's output in packaging workers; state, database and
8644
- * resource writes stay on this thread under the cross-process state lock (ADR 0181). Any Work
8645
- * may be split into shards, so the pool is not limited to one worker per Work (ADR 0199).
8416
+ * A several-Work render writes each Work's base and output in packaging workers; state,
8417
+ * database and resource writes stay on this thread under the cross-process state lock
8418
+ * (ADR 0181, ADR 0228). Any Work may be split into shards, so the pool is not limited to one
8419
+ * worker per Work (ADR 0199). A `workIds` call of one Work uses workers too, so a panel batch
8420
+ * never packages on the serving thread.
8646
8421
  */
8647
8422
  async function renderScope(journeyId, works, signal) {
8648
8423
  const workers = options.workers;
8649
8424
  const size = workers ? workers.parallelism ?? renderWorkerCount() : 0;
8650
- if (!workers || works < 2 || size < 1) return void 0;
8425
+ if (!workers || works < 1 || size < 1) return void 0;
8651
8426
  const pool = createRenderWorkers(workers, size);
8652
8427
  const session = batchedRenderSession(storage, journeyId);
8428
+ let plan;
8429
+ const readPlan = () => plan ??= readJourneyPlan(storage, journeyId).catch((error) => {
8430
+ plan = void 0;
8431
+ throw error;
8432
+ });
8653
8433
  return {
8654
8434
  concurrency: size + 1,
8655
- render: (workId) => render(journeyId, workId, signal, pool.write, session),
8435
+ render: (workId) => render(journeyId, workId, signal, pool.write, session, readPlan, pool.writeBase),
8656
8436
  close: pool.close
8657
8437
  };
8658
8438
  }
@@ -8662,7 +8442,7 @@ function createJourneyRenderer(options) {
8662
8442
  };
8663
8443
  }
8664
8444
  /** Package-only native output request: the final keyword dictionary covers all retained fields. */
8665
- async function journeyOutputRequest(storage, state, work, processingConfig, _signal) {
8445
+ async function journeyOutputRequest(storage, state, work, processingConfig, _signal, plan) {
8666
8446
  const { journeyId } = state;
8667
8447
  const workId = work.id;
8668
8448
  const base = (await storage.resources({
@@ -8670,14 +8450,11 @@ async function journeyOutputRequest(storage, state, work, processingConfig, _sig
8670
8450
  kind: "base",
8671
8451
  id: `${workId}:${work.inputRevision}`
8672
8452
  })).find((item) => item.kind === "base" && item.workId === workId && item.inputRevision === work.inputRevision);
8673
- const plan = (await storage.resources({
8674
- journeyId,
8675
- kind: "plan"
8676
- }))[0];
8677
- if (base?.kind !== "base" || plan?.kind !== "plan") throw new ApplicationError("CORRUPTED_STATE");
8453
+ if (base?.kind !== "base") throw new ApplicationError("CORRUPTED_STATE");
8454
+ const { replacementKey } = plan ?? await readJourneyPlan(storage, journeyId);
8678
8455
  const hides = createFamilyHides();
8679
8456
  const confirmed = await journeyConfirmedHides(storage, state);
8680
- const dictionary = await finalKeywordDictionary(storage, state, base.sessions.map((session) => session.applicationSessionId));
8457
+ const dictionary = await finalKeywordDictionary(storage, state, base.sessions.map((session) => session.applicationSessionId), plan);
8681
8458
  const exceptions = workExceptions(state, workId);
8682
8459
  const tokens = createHideIndexTokens();
8683
8460
  const title = await renderTitle(storage, state, work);
@@ -8692,7 +8469,7 @@ async function journeyOutputRequest(storage, state, work, processingConfig, _sig
8692
8469
  companion: base.companion,
8693
8470
  workId,
8694
8471
  baseBundleId: base.bundleId,
8695
- replacementKey: plan.replacementKey,
8472
+ replacementKey,
8696
8473
  edits: [],
8697
8474
  families: hides.request(base.sessions, dictionary, exceptions),
8698
8475
  metadata: {
@@ -9411,73 +9188,73 @@ function createClassificationReader(storage, stateDir) {
9411
9188
  }
9412
9189
  //#endregion
9413
9190
  //#region apps/cli/src/journey/works/regroup.ts
9414
- /** Rebind accepted frozen evidence; original batch/chunk IDs remain its audit provenance. */
9415
- async function regroupPrivacySources(storage, state, works, changedMetadata, signal) {
9416
- const owners = new Map(works.flatMap((work) => work.rootSessionIds.map((id) => [id, work.id])));
9191
+ /**
9192
+ * The session of every accepted finding chunk outside the proposal's retained Works, read from
9193
+ * frozen batches before the commit so the store transaction can rebind them without I/O.
9194
+ */
9195
+ async function resolveChunkSessions(storage, state, works, signal) {
9417
9196
  const retained = new Set(works.map((work) => work.id));
9418
9197
  const batches = /* @__PURE__ */ new Map();
9419
- const terms = [];
9420
- for (const term of state.privacy.terms) {
9421
- const sources = [];
9422
- for (const source of term.sources) {
9423
- signal.throwIfAborted();
9424
- if (source.kind === "metadata") {
9425
- if (retained.has(source.workId) && !changedMetadata.has(source.workId)) sources.push(source);
9426
- continue;
9427
- }
9428
- if (retained.has(source.workId)) {
9429
- sources.push(source);
9430
- continue;
9431
- }
9432
- let batch = batches.get(source.batchId);
9433
- if (!batch) {
9434
- const record = await storage.inspectReview({
9435
- kind: "attempt",
9436
- journeyId: state.journeyId,
9437
- batchId: source.batchId
9438
- });
9439
- if (!record || record.batch.snapshotId !== state.snapshotId) throw new ApplicationError("CORRUPTED_STATE");
9440
- batch = record.batch;
9441
- if (batches.size >= 16) batches.clear();
9442
- batches.set(source.batchId, batch);
9443
- }
9444
- const chunk = batch.chunks.find((item) => item.id === source.chunkId) ?? batch.duplicates?.find((item) => item.id === source.chunkId);
9445
- const workId = chunk && owners.get(chunk.sessionId);
9446
- if (!workId) throw new ApplicationError("CORRUPTED_STATE");
9447
- sources.push({
9448
- ...source,
9449
- workId
9198
+ const resolved = /* @__PURE__ */ new Map();
9199
+ for (const term of state.privacy.terms) for (const source of term.sources) {
9200
+ signal.throwIfAborted();
9201
+ if (source.kind === "metadata" || retained.has(source.workId)) continue;
9202
+ const key = `${source.batchId}:${source.chunkId}`;
9203
+ if (resolved.has(key)) continue;
9204
+ let batch = batches.get(source.batchId);
9205
+ if (!batch) {
9206
+ const record = await storage.inspectReview({
9207
+ kind: "attempt",
9208
+ journeyId: state.journeyId,
9209
+ batchId: source.batchId
9450
9210
  });
9451
- }
9452
- terms.push({
9453
- ...term,
9454
- sources
9211
+ if (!record || record.batch.snapshotId !== state.snapshotId) throw new ApplicationError("CORRUPTED_STATE");
9212
+ batch = record.batch;
9213
+ if (batches.size >= 16) batches.clear();
9214
+ batches.set(source.batchId, batch);
9215
+ }
9216
+ const chunk = batch.chunks.find((item) => item.id === source.chunkId) ?? batch.duplicates?.find((item) => item.id === source.chunkId);
9217
+ if (!chunk) throw new ApplicationError("CORRUPTED_STATE");
9218
+ resolved.set(key, {
9219
+ batchId: source.batchId,
9220
+ chunkId: source.chunkId,
9221
+ sessionId: chunk.sessionId
9455
9222
  });
9456
9223
  }
9457
- return terms;
9224
+ return [...resolved.values()];
9458
9225
  }
9459
9226
  //#endregion
9460
9227
  //#region apps/cli/src/journey/works/proposal.ts
9461
- /** One atomic complete root partition; recommendations have no access authority. */
9228
+ /**
9229
+ * One atomic root partition; recommendations have no access authority. Roots still being grouped
9230
+ * wait in one unread Work while the other Works are confirmed and read; the submission that
9231
+ * groups them may arrive after confirmation and keeps it (E2). The proposal is prepared from one
9232
+ * read and applied by the store against the current state, so scan writes in between do not
9233
+ * refuse it (BL-216).
9234
+ */
9462
9235
  function createClassificationProposal(storage) {
9463
9236
  return async (input, signal) => {
9464
9237
  signal.throwIfAborted();
9465
9238
  const state = await storage.current(input.journeyId);
9466
9239
  if (!state?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
9467
- const commit = (next = state) => storage.commit({
9240
+ const classifying = isClassifying(state);
9241
+ const late = state.scope.status === "confirmed";
9242
+ const bound = input.expectedClassificationRevision !== void 0 && classifying && input.expectedClassificationRevision === state.classificationRevision;
9243
+ const requestHash = textDigest(JSON.stringify(input));
9244
+ const event = {
9245
+ journeyId: state.journeyId,
9246
+ kind: "response",
9247
+ source: "conversation",
9248
+ at: (/* @__PURE__ */ new Date()).toISOString()
9249
+ };
9250
+ if (input.expectedClassificationRevision !== void 0 ? !bound : state.revision !== input.expectedRevision) return storage.commit({
9468
9251
  expectedRevision: input.expectedRevision,
9469
9252
  requestKey: input.idempotencyKey,
9470
- requestHash: textDigest(JSON.stringify(input)),
9471
- state: next,
9472
- event: {
9473
- journeyId: state.journeyId,
9474
- kind: "response",
9475
- source: "conversation",
9476
- at: (/* @__PURE__ */ new Date()).toISOString()
9477
- }
9253
+ requestHash,
9254
+ state,
9255
+ event
9478
9256
  });
9479
- if (state.revision !== input.expectedRevision) return commit();
9480
- if (state.scope.status === "confirmed") throw new ApplicationError("INVALID_TRANSITION");
9257
+ if (late && !classifying) throw new ApplicationError("INVALID_TRANSITION");
9481
9258
  if (state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
9482
9259
  if (!state.snapshotId || state.pendingHideId) throw new ApplicationError("SCOPE_NOT_READY");
9483
9260
  const plan = (await storage.resources({
@@ -9503,6 +9280,11 @@ function createClassificationProposal(storage) {
9503
9280
  });
9504
9281
  } else merged.set(key, item);
9505
9282
  }
9283
+ const pending = input.pendingRootSessionIds ?? [];
9284
+ for (const root of pending) {
9285
+ if (!roots.has(root) || seen.has(root)) throw new ApplicationError("INVALID_SELECTION");
9286
+ seen.add(root);
9287
+ }
9506
9288
  if (seen.size !== roots.size) throw new ApplicationError("INVALID_SELECTION");
9507
9289
  const families = reviewFamilies(plan.approval.inventory);
9508
9290
  const captures = await storage.resources({
@@ -9510,75 +9292,43 @@ function createClassificationProposal(storage) {
9510
9292
  kind: "capture"
9511
9293
  });
9512
9294
  const captured = new Set(captures.flatMap((resource) => resource.kind === "capture" ? [resource.capture.partId] : []));
9513
- const works = [];
9295
+ const workspacesOf = (rootSessionIds) => {
9296
+ const sessions = rootSessionIds.flatMap((root) => families.members.get(root) ?? []);
9297
+ if (sessions.some((session) => session.parts.some((part) => !captured.has(part.id)))) throw new ApplicationError("SCOPE_NOT_READY");
9298
+ return [...new Set(sessions.map((session) => session.workspaceId))];
9299
+ };
9300
+ const items = [];
9514
9301
  for (const item of merged.values()) {
9515
9302
  signal.throwIfAborted();
9516
- const sessions = item.rootSessionIds.flatMap((root) => families.members.get(root) ?? []);
9517
- if (sessions.some((session) => session.parts.some((part) => !captured.has(part.id)))) throw new ApplicationError("SCOPE_NOT_READY");
9518
- const base = state.works.find((work) => work.rootSessionIds.length === item.rootSessionIds.length && work.rootSessionIds.every((root) => item.rootSessionIds.includes(root))) ?? newJourneyWork(randomUUID(), item.projectName, item.rootSessionIds, [...new Set(sessions.map((session) => session.workspaceId))]);
9519
- const { classificationMetadata: _display, ...original } = base;
9520
- works.push({
9521
- ...original,
9522
- projectName: item.projectName,
9523
- title: base.titleOrigin === "explicit" ? base.title : item.projectName,
9524
- description: item.description,
9525
- projectType: {
9526
- type: item.projectType,
9527
- reason: item.recommendation.reason,
9528
- origin: "model"
9529
- },
9530
- placement: "placed",
9531
- classificationRecommendation: item.recommendation,
9532
- suggestion: null,
9533
- artifact: null,
9534
- revision: base.revision + 1
9303
+ items.push({
9304
+ ...item,
9305
+ workId: randomUUID(),
9306
+ workspaceIds: workspacesOf(item.rootSessionIds)
9535
9307
  });
9536
9308
  }
9537
- const retained = new Set(works.map((work) => work.id));
9538
- const changedMetadata = new Set(works.filter((work) => {
9539
- const old = state.works.find((item) => item.id === work.id);
9540
- return !old || JSON.stringify([
9541
- work.title,
9542
- work.projectName,
9543
- work.description,
9544
- work.classificationRecommendation?.reason
9545
- ]) !== JSON.stringify([
9546
- old.title,
9547
- old.projectName,
9548
- old.description,
9549
- old.classificationRecommendation?.reason
9550
- ]);
9551
- }).map((work) => work.id));
9552
- const regrouped = state.works.some((work) => !retained.has(work.id)) || changedMetadata.size > 0;
9553
- const cardTerms = new Set(state.privacy.cards.flatMap((card) => card.termKeys));
9554
- const preserved = await regroupPrivacySources(storage, state, works, changedMetadata, signal);
9555
- const forced = new Set(preserved.filter((term) => !termTypeInfo[term.type].asked).map((term) => term.value));
9556
- const overrides = new Map(state.valueOverrides?.map((item) => [item.value, item.choice]) ?? []);
9557
- const terms = preserved.map((term) => ({
9558
- ...term,
9559
- disposition: forced.has(term.value) ? "hidden" : overrides.has(term.value) ? overrides.get(term.value) === "hide" ? "hidden" : "keep" : regrouped && cardTerms.has(privacyTermKey(term)) ? "pending" : term.disposition
9560
- }));
9561
- return commit({
9562
- ...state,
9563
- works,
9564
- classificationRevision: state.revision + 1,
9565
- scope: {
9566
- status: "pending",
9567
- actor: null,
9568
- at: null
9569
- },
9570
- privacy: {
9571
- ...state.privacy,
9572
- terms,
9573
- packageApproval: null,
9574
- ...regrouped ? {
9575
- cards: [],
9576
- answersRevision: state.privacy.answersRevision + 1,
9577
- confirmedAnswersRevision: null
9578
- } : {}
9579
- },
9580
- readingComplete: works.filter((work) => work.access !== "drop").every((work) => work.totalBatches > 0 && work.completedBatches === work.totalBatches),
9581
- phase: "processing"
9309
+ const prepared = {
9310
+ journeyId: state.journeyId,
9311
+ requestKey: input.idempotencyKey,
9312
+ requestHash,
9313
+ at: event.at,
9314
+ expectedRevision: input.expectedRevision,
9315
+ expectedClassificationRevision: bound ? input.expectedClassificationRevision ?? null : null,
9316
+ snapshotId: state.snapshotId,
9317
+ partition: classificationPartition(state),
9318
+ late,
9319
+ items,
9320
+ pending: pending.length ? {
9321
+ workId: randomUUID(),
9322
+ rootSessionIds: pending,
9323
+ workspaceIds: [...new Set(pending.flatMap((root) => (families.members.get(root) ?? []).map((session) => session.workspaceId)))]
9324
+ } : null,
9325
+ chunkSessions: []
9326
+ };
9327
+ const { works } = classificationWorks(state, prepared);
9328
+ const chunkSessions = await resolveChunkSessions(storage, state, works, signal);
9329
+ return storage.commitClassification({
9330
+ ...prepared,
9331
+ chunkSessions
9582
9332
  });
9583
9333
  };
9584
9334
  }
@@ -10055,16 +9805,19 @@ function createJourneySendArtifacts(storage) {
10055
9805
  sha256: output.primary.sha256
10056
9806
  });
10057
9807
  },
10058
- async upload(journeyId, artifact, uploadUrl, signal) {
9808
+ async upload(journeyId, artifact, upload, signal) {
10059
9809
  const output = await find(journeyId, artifact.workId, artifact.revision);
10060
9810
  if (output.bundleId !== artifact.bundleId || output.primary.sha256 !== artifact.sha256 || output.primary.archiveBytes !== artifact.byteLength || artifact.byteLength > 5368709120) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
10061
- await putPrimary(output.primary, artifact.byteLength, uploadUrl, signal);
9811
+ await putPrimary(output.primary, artifact.byteLength, upload, signal);
10062
9812
  }
10063
9813
  };
10064
9814
  }
10065
- /** Stream a verified primary to a signed HTTPS URL; shared by native Send and Manual upload. */
10066
- async function putPrimary(primary, byteLength, uploadUrl, signal) {
10067
- const url = new URL(uploadUrl);
9815
+ /**
9816
+ * Stream a verified primary to a signed HTTPS URL; shared by native Send and Manual upload. A
9817
+ * checksum upload also sends the SHA-256 header the URL signs, so storage refuses other bytes.
9818
+ */
9819
+ async function putPrimary(primary, byteLength, upload, signal) {
9820
+ const url = new URL(upload.uploadUrl);
10068
9821
  if (url.protocol !== "https:" || url.username || url.password || url.hash) throw new ApplicationError("INVALID_ARGUMENTS");
10069
9822
  const source = await openConfiguredUpload(primary, signal);
10070
9823
  const cleanup = createCleanup();
@@ -10076,7 +9829,8 @@ async function putPrimary(primary, byteLength, uploadUrl, signal) {
10076
9829
  redirect: "error",
10077
9830
  headers: {
10078
9831
  "Content-Type": "application/zip",
10079
- "Content-Length": String(byteLength)
9832
+ "Content-Length": String(byteLength),
9833
+ ...upload.headers
10080
9834
  },
10081
9835
  duplex: "half"
10082
9836
  };
@@ -10246,7 +10000,7 @@ const completeTimeoutMs = 6e5;
10246
10000
  function createJourneyOwnerClient(options) {
10247
10001
  const base = apiBase(options.apiUrl);
10248
10002
  const requestFetch = options.fetch ?? fetch;
10249
- async function request(path, schema, signal, body, method = "POST", key, timeoutMs = requestTimeoutMs$2) {
10003
+ async function request(path, schema, signal, body, method = "POST", key, timeoutMs = requestTimeoutMs$2, checksumUpload = false) {
10250
10004
  const token = await options.authorize();
10251
10005
  signal.throwIfAborted();
10252
10006
  const response = await requestFetch(new URL(path, base), {
@@ -10255,7 +10009,8 @@ function createJourneyOwnerClient(options) {
10255
10009
  accept: "application/json",
10256
10010
  authorization: `Bearer ${token}`,
10257
10011
  ...body === void 0 ? {} : { "content-type": "application/json" },
10258
- ...key === void 0 ? {} : { "idempotency-key": key }
10012
+ ...key === void 0 ? {} : { "idempotency-key": key },
10013
+ ...checksumUpload ? { "upload-checksum": "sha256" } : {}
10259
10014
  },
10260
10015
  ...body === void 0 ? {} : { body: JSON.stringify(body) },
10261
10016
  credentials: "omit",
@@ -10310,14 +10065,14 @@ function createJourneyOwnerClient(options) {
10310
10065
  membersDigest: preview.membersDigest
10311
10066
  }, "PUT");
10312
10067
  },
10313
- reserve(key, input, signal) {
10314
- return request("/me/work-bundles/uploads", sendReceiptSchema.refine((value) => receiptMatches(input, value)), signal, input, "POST", key);
10068
+ reserve(key, input, signal, checksumUpload = true) {
10069
+ return request("/me/work-bundles/uploads", sendReceiptSchema.refine((value) => receiptMatches(input, value)), signal, input, "POST", key, requestTimeoutMs$2, checksumUpload);
10315
10070
  },
10316
10071
  complete(id, signal) {
10317
10072
  return request(`/me/work-bundles/uploads/${encodeURIComponent(id)}/complete`, receipt(id), signal, {}, "POST", void 0, completeTimeoutMs);
10318
10073
  },
10319
- status(id, signal) {
10320
- return request(`/me/work-bundles/uploads/${encodeURIComponent(id)}`, receipt(id), signal, void 0, "GET");
10074
+ status(id, signal, checksumUpload = true) {
10075
+ return request(`/me/work-bundles/uploads/${encodeURIComponent(id)}`, receipt(id), signal, void 0, "GET", void 0, requestTimeoutMs$2, checksumUpload);
10321
10076
  },
10322
10077
  async replace(keepWorkIds, signal) {
10323
10078
  if (keepWorkIds.length === 0) throw new ApplicationError("INVALID_STATE");
@@ -10507,16 +10262,72 @@ async function perform(context, item, connection, signal) {
10507
10262
  const auto = context.snapshot === true;
10508
10263
  await guard();
10509
10264
  if (item.settings && item.settingsConfirmed) return applySettings(item, connection, signal, guard, auto);
10510
- if (!item.reservation) return resolveItem(item, context.bindingId, connection, signal, auto, context.name);
10511
- if (!item.receipt) return observe(item, await connection.reserve(item.operationKey, item.reservation, signal), connection, signal, auto);
10512
- const receipt = await connection.status(item.receipt.receiptId, signal);
10265
+ if (!item.reservation) {
10266
+ const next = await resolveItem(item, context.bindingId, connection, signal, auto, context.name);
10267
+ if (next.reservation) context.fresh?.set(item.workId, null);
10268
+ return next;
10269
+ }
10270
+ if (!item.receipt) {
10271
+ const reserved = await connection.reserve(item.operationKey, item.reservation, signal, context.checksumUploads !== false);
10272
+ const next = await observe(item, reserved, connection, signal, auto);
10273
+ if (context.fresh?.has(item.workId) && reserved.upload && next.status === "uploading") context.fresh.set(item.workId, {
10274
+ receiptId: reserved.receiptId,
10275
+ upload: reserved.upload
10276
+ });
10277
+ return next;
10278
+ }
10279
+ const fresh = context.fresh?.get(item.workId);
10280
+ context.fresh?.delete(item.workId);
10281
+ if (fresh?.receiptId === item.receipt.receiptId) return put(context, item, fresh.receiptId, fresh.upload, connection, signal, guard);
10282
+ const receipt = await connection.status(item.receipt.receiptId, signal, context.checksumUploads !== false);
10513
10283
  const observed = await observe(item, receipt, connection, signal, auto);
10514
- if (receipt.state !== "awaiting_upload") return observed;
10284
+ if (receipt.state !== "awaiting_upload") return withoutUpload(observed);
10515
10285
  if (!receipt.upload) throw new ApplicationError("INVALID_STATE");
10286
+ if (item.uploadedReceiptId === receipt.receiptId) {
10287
+ await guard();
10288
+ try {
10289
+ return withoutUpload(await observe(item, await connection.complete(receipt.receiptId, signal), connection, signal, auto));
10290
+ } catch (error) {
10291
+ if (!(error instanceof JourneyRemoteError) || error.status === 401 || [
10292
+ "settings_changed",
10293
+ "target_conflict",
10294
+ "work_removed"
10295
+ ].includes(error.code)) throw error;
10296
+ return withoutUpload(item);
10297
+ }
10298
+ }
10299
+ const current = await connection.resolve(item.reservation.work, signal);
10300
+ if (current.scopeVersion !== item.reservation.expectedScopeVersion || current.accessVersion !== item.reservation.expectedAccessVersion) return renewed(item, null);
10301
+ return put(context, item, receipt.receiptId, receipt.upload, connection, signal, guard);
10302
+ }
10303
+ /** Uploads the bytes to the receipt's signed URL and completes it. */
10304
+ async function put(context, item, receiptId, upload, connection, signal, guard) {
10516
10305
  await guard();
10517
- await context.upload(item.artifact, receipt.upload.uploadUrl, signal);
10306
+ await context.upload(item.artifact, upload, signal);
10518
10307
  await guard();
10519
- return observe(item, await connection.complete(receipt.receiptId, signal), connection, signal, auto);
10308
+ try {
10309
+ return observe(item, await connection.complete(receiptId, signal), connection, signal, context.snapshot === true);
10310
+ } catch (error) {
10311
+ if (upload.headers !== void 0 && error instanceof JourneyRemoteError && error.code === "bundle_mismatch") return {
10312
+ ...renewed(withoutUpload(item), null),
10313
+ checksumUploadRefused: true
10314
+ };
10315
+ if (error instanceof Object) uploaded.set(error, {
10316
+ ...item,
10317
+ uploadedReceiptId: receiptId
10318
+ });
10319
+ throw error;
10320
+ }
10321
+ }
10322
+ const uploaded = /* @__PURE__ */ new WeakMap();
10323
+ /** The item to save for a failed step whose bytes were already uploaded, if any. */
10324
+ function uploadedItem(error) {
10325
+ return error instanceof Object ? uploaded.get(error) : void 0;
10326
+ }
10327
+ function withoutUpload(item) {
10328
+ if (item.uploadedReceiptId === void 0) return item;
10329
+ const { uploadedReceiptId: _, ...rest } = item;
10330
+ return rest;
10520
10331
  }
10521
10332
  /** A definite stale refusal can renew the intent; unknown outcomes always retry the same key. */
10522
10333
  async function stepItem(context, item, connection, signal) {
@@ -10546,31 +10357,41 @@ async function stepItem(context, item, connection, signal) {
10546
10357
  status: "blocked",
10547
10358
  error: "WORK_REMOVED"
10548
10359
  };
10549
- return {
10550
- ...item,
10551
- operationKey: randomUUID(),
10552
- reservation: null,
10553
- resolution: null,
10554
- previousReceipts: item.receipt ? [...item.previousReceipts, item.receipt] : item.previousReceipts,
10555
- receipt: null,
10556
- status: "ready",
10557
- error: "SETTINGS_CHANGED"
10558
- };
10360
+ return renewed(item, "SETTINGS_CHANGED");
10559
10361
  }
10560
10362
  }
10363
+ /** A fresh intent for the same Work: the old receipt is kept as history and resolve runs again. */
10364
+ function renewed(item, error) {
10365
+ return {
10366
+ ...withoutUpload(item),
10367
+ operationKey: randomUUID(),
10368
+ reservation: null,
10369
+ resolution: null,
10370
+ previousReceipts: item.receipt ? [...item.previousReceipts, item.receipt] : item.previousReceipts,
10371
+ receipt: null,
10372
+ status: "ready",
10373
+ error
10374
+ };
10375
+ }
10561
10376
  const confirmedSettings = (item) => item.settings !== null && item.settingsConfirmed;
10562
10377
  /**
10563
- * The Works one advance steps together. Each Work appears at most once, so its own steps stay
10564
- * strictly ordered. Upload work comes before assessment checks, as before. A confirmed linked
10565
- * Settings change can alter other Works' access versions, so it always steps alone.
10378
+ * The Works one advance steps together. Upload work comes before assessment checks, smallest
10379
+ * ZIP first. Completing
10380
+ * an upload advances the owner's scope version, which every resolution and reservation carries,
10381
+ * so one Work's resolve-to-complete steps never overlap another's: uploads step one Work at a
10382
+ * time (ADR 0214). Assessment checks change nothing and step up to three together; a confirmed
10383
+ * linked Settings change alters access versions, so it always steps alone.
10566
10384
  */
10567
- function pickBatch(items) {
10568
- const uploading = items.filter((item) => ["ready", "uploading"].includes(item.status));
10569
- const pool = uploading.length ? uploading : items.filter((item) => item.status === "assessing");
10570
- const first = pool[0];
10385
+ function pickBatch(items, now = Date.now()) {
10386
+ const uploading = items.find((item) => item.status === "uploading") ?? items.filter((item) => item.status === "ready").reduce((smallest, item) => !smallest || item.artifact.byteLength < smallest.artifact.byteLength ? item : smallest, void 0);
10387
+ if (uploading) return [uploading];
10388
+ const assessing = items.filter((item) => item.status === "assessing");
10389
+ const first = assessing[0];
10571
10390
  if (!first) return [];
10572
10391
  if (confirmedSettings(first)) return [first];
10573
- return pool.filter((item) => !confirmedSettings(item)).slice(0, 3);
10392
+ const checks = assessing.filter((item) => !confirmedSettings(item));
10393
+ const start = Math.floor(now / 1e3) * 3 % checks.length;
10394
+ return [...checks.slice(start), ...checks.slice(0, start)].slice(0, 3);
10574
10395
  }
10575
10396
  function isCode(error, code) {
10576
10397
  return error instanceof ApplicationError && error.code === code;
@@ -10664,7 +10485,11 @@ async function advanceItems(host, signal) {
10664
10485
  if (finished !== null) return finished;
10665
10486
  }
10666
10487
  if (!batch.length) return host.applied();
10667
- const results = await Promise.allSettled(batch.map((item) => stepItem(host.context(item), item, connection, signal)));
10488
+ const checksumUploads = !send.items.some((item) => item.checksumUploadRefused);
10489
+ const results = await Promise.allSettled(batch.map((item) => stepItem({
10490
+ ...host.context(item),
10491
+ checksumUploads
10492
+ }, item, connection, signal)));
10668
10493
  const failures = results.flatMap((result) => result.status === "rejected" ? [result] : []);
10669
10494
  if (failures.some(({ reason }) => isCode(reason, "REVISION_CONFLICT"))) return host.stale();
10670
10495
  const first = failures[0];
@@ -10681,9 +10506,11 @@ async function advanceItems(host, signal) {
10681
10506
  continue;
10682
10507
  }
10683
10508
  const cause = result.reason;
10509
+ const progressed = uploadedItem(cause);
10510
+ if (progressed) next.set(item.workId, progressed);
10684
10511
  if (cause instanceof JourneyRemoteError && cause.status === 401 || isCode(cause, "ACCOUNT_SIGNED_OUT")) signIn = true;
10685
10512
  else if (isCode(cause, "IDENTITY_REJECTED")) changed = true;
10686
- else if (isInterrupted(cause)) {
10513
+ else if (isInterrupted(cause) || isTransientRefusal(cause)) {
10687
10514
  host.failure("SEND_INTERRUPTED", cause);
10688
10515
  error = "SEND_INTERRUPTED";
10689
10516
  } else {
@@ -10717,7 +10544,7 @@ async function advanceItems(host, signal) {
10717
10544
  items
10718
10545
  }));
10719
10546
  } catch (error) {
10720
- if (!signal.aborted && (isInterrupted(error) || error instanceof Error && isInterrupted(error.cause))) {
10547
+ if (!signal.aborted && (isInterrupted(error) || error instanceof Error && isInterrupted(error.cause) || isCode(error, "IDENTITY_UNAVAILABLE"))) {
10721
10548
  host.failure("SEND_INTERRUPTED", error);
10722
10549
  return send.error === "SEND_INTERRUPTED" ? host.applied() : host.save({
10723
10550
  ...send,
@@ -10791,8 +10618,9 @@ function withoutQuestions(send) {
10791
10618
  };
10792
10619
  return send.status === "review" ? aggregate(next) : next;
10793
10620
  }
10794
- async function advanceSend(dependencies, address, signal) {
10621
+ async function advanceSend(dependencies, address, signal, chain) {
10795
10622
  const snapshot = requireSend(await dependencies.storage.loadSend(address.journeyId, address.sendId));
10623
+ if (chain) chain.revision = snapshot.send.revision;
10796
10624
  if ((await dependencies.storage.loadSend(address.journeyId))?.send?.id !== snapshot.send.id) return {
10797
10625
  ...snapshot,
10798
10626
  status: "stale"
@@ -10824,20 +10652,25 @@ async function advanceSend(dependencies, address, signal) {
10824
10652
  return advanceItems({
10825
10653
  send,
10826
10654
  failure: dependencies.failure,
10827
- connect: dependencies.auth.connect,
10655
+ connect: chain?.connect ?? dependencies.auth.connect,
10828
10656
  profile: (connection, profileSignal) => prepareProfile(dependencies, snapshot, connection, profileSignal),
10829
10657
  context: (item) => {
10830
10658
  const binding = snapshot.journey.works.find((value) => value.id === item.workId)?.binding;
10831
10659
  const retired = !item.receipt && binding?.sentBundleId === item.artifact.bundleId && binding.sentAccess === item.access;
10832
10660
  return {
10833
10661
  snapshot: true,
10662
+ ...chain ? { fresh: chain.fresh } : {},
10834
10663
  active: async () => {
10835
10664
  const current = requireSend(await dependencies.storage.loadSend(snapshot.journey.journeyId));
10836
10665
  if (current.send.id !== snapshot.send.id || !sameItem(current.send.items, item) || current.send.status === "paused" || current.journey.phase === "paused") throw new ApplicationError("REVISION_CONFLICT");
10837
10666
  assertScope(current.journey, current.send);
10838
10667
  },
10839
10668
  bindingId: binding?.subject === send.subject && !retired ? binding.bundleRecordId : void 0,
10840
- upload: (artifact, uploadUrl, uploadSignal) => dependencies.artifacts.upload(snapshot.journey.journeyId, artifact, uploadUrl, uploadSignal)
10669
+ upload: async (artifact, upload, uploadSignal) => {
10670
+ const started = performance.now();
10671
+ await dependencies.artifacts.upload(snapshot.journey.journeyId, artifact, upload, uploadSignal);
10672
+ dependencies.uploaded?.(artifact.workId, artifact.byteLength, Math.round(performance.now() - started));
10673
+ }
10841
10674
  };
10842
10675
  },
10843
10676
  finish: async (connection, finishSignal) => {
@@ -10881,6 +10714,14 @@ async function advanceSend(dependencies, address, signal) {
10881
10714
  })
10882
10715
  }, signal);
10883
10716
  }
10717
+ /**
10718
+ * A refusal Passport answers while it or Identity is briefly unavailable. It says nothing about
10719
+ * the Work, so the step is retried instead of blocking it (the 2026-11-01 recorded run blocked a
10720
+ * Work on one `identity_unavailable`).
10721
+ */
10722
+ function isTransientRefusal(error) {
10723
+ return error instanceof JourneyRemoteError && (error.status >= 500 || error.status === 429 || error.code === "identity_unavailable");
10724
+ }
10884
10725
  //#endregion
10885
10726
  //#region apps/cli/src/account/journey/decisions.ts
10886
10727
  function decide(item, input) {
@@ -11151,10 +10992,10 @@ function createManualUploader(dependencies) {
11151
10992
  active: guard(upload, item),
11152
10993
  name: item.title,
11153
10994
  bindingId: bindings.find((value) => value.subject === upload.subject && value.pathHash === pathHash(item))?.bundleRecordId,
11154
- upload: (artifact, uploadUrl, uploadSignal) => {
10995
+ upload: (artifact, signedUpload, uploadSignal) => {
11155
10996
  const slot = upload.slots[item.workId];
11156
10997
  if (slot === void 0) throw new ApplicationError("ARTIFACT_MISSING");
11157
- return dependencies.artifacts.upload(upload.resultId, slot, artifact, uploadUrl, uploadSignal);
10998
+ return dependencies.artifacts.upload(upload.resultId, slot, artifact, signedUpload, uploadSignal);
11158
10999
  }
11159
11000
  }),
11160
11001
  applied: () => ({
@@ -11224,10 +11065,10 @@ function createManualUploadArtifacts(application) {
11224
11065
  })
11225
11066
  };
11226
11067
  },
11227
- async upload(resultId, slot, artifact, uploadUrl, signal) {
11068
+ async upload(resultId, slot, artifact, upload, signal) {
11228
11069
  const { row, primary } = await find(resultId, slot);
11229
11070
  if (row.bundleId !== artifact.bundleId || primary.sha256 !== artifact.sha256 || primary.archiveBytes !== artifact.byteLength || artifact.byteLength > 5368709120) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
11230
- await putPrimary(primary, artifact.byteLength, uploadUrl, signal);
11071
+ await putPrimary(primary, artifact.byteLength, upload, signal);
11231
11072
  }
11232
11073
  };
11233
11074
  }
@@ -12390,13 +12231,42 @@ function createJourneyFiles(storage) {
12390
12231
  };
12391
12232
  };
12392
12233
  }
12234
+ /**
12235
+ * One advance carries the Work being uploaded through its phases with one verified connection,
12236
+ * instead of one phase per panel round trip, and uploads a receipt it just resolved and reserved
12237
+ * without reading it again (ADR 0229). Each phase is still fenced and saved on its own, so Pause, a
12238
+ * revision change or a failure stops the chain at the next phase exactly as between advances.
12239
+ * The advance returns once that Work leaves its upload phases, so the next Work starts from the
12240
+ * caller's next advance as before.
12241
+ */
12242
+ async function advanceWork(dependencies, address, signal, phases = 3) {
12243
+ let connection;
12244
+ const chain = {
12245
+ fresh: /* @__PURE__ */ new Map(),
12246
+ async connect(connectSignal) {
12247
+ if (connection === void 0) connection = await dependencies.auth.connect(connectSignal);
12248
+ return connection;
12249
+ }
12250
+ };
12251
+ let result = await advanceSend(dependencies, address, signal, chain);
12252
+ for (let phase = 1; phase < phases && uploading(result, chain.revision); phase += 1) result = await advanceSend(dependencies, address, signal, chain);
12253
+ return result;
12254
+ }
12255
+ /** The phase saved progress and left its Work mid-upload, with nothing else to report. */
12256
+ function uploading(result, before) {
12257
+ const send = result.send;
12258
+ return result.status === "applied" && send !== null && before !== void 0 && send.revision > before && send.status === "sending" && send.error === null && pickBatch(send.items)[0]?.status === "uploading";
12259
+ }
12393
12260
  //#endregion
12394
12261
  //#region apps/cli/src/account/journey/send.ts
12395
12262
  function sameIds(left, right) {
12396
12263
  return left.length === right.length && new Set(left).size === left.length && left.every((id) => right.includes(id));
12397
12264
  }
12398
- /** Durable, bounded orchestration; native chat and panel use the same commands. */
12399
- function createJourneySender(dependencies) {
12265
+ /**
12266
+ * Durable, bounded orchestration; native chat and panel use the same commands. One advance runs
12267
+ * at most `phases` upload phases of one Work (ADR 0229); tests pass 1 to observe each phase.
12268
+ */
12269
+ function createJourneySender(dependencies, phases = 3) {
12400
12270
  const running = /* @__PURE__ */ new Map();
12401
12271
  const runKey = (address) => `${address.journeyId}:${address.sendId}`;
12402
12272
  const load = async (address) => {
@@ -12481,6 +12351,20 @@ function createJourneySender(dependencies) {
12481
12351
  if (result.status !== "stale") running.get(runKey(input))?.stop.abort();
12482
12352
  return result;
12483
12353
  },
12354
+ /** BL-201: Cancel sign-in returns a confirmed Send that uploaded nothing to Ready to send. */
12355
+ async withdraw(raw) {
12356
+ const input = sendActionSchema.parse(raw);
12357
+ const snapshot = await load(input);
12358
+ if (!matchesAction(snapshot, input)) return saveSend(dependencies, snapshot, snapshot.send, input);
12359
+ if (snapshot.send.status !== "sign-in" || snapshot.send.items.some((item) => item.receipt)) throw new ApplicationError("INVALID_TRANSITION");
12360
+ const result = await saveSend(dependencies, snapshot, {
12361
+ ...snapshot.send,
12362
+ status: "prepared",
12363
+ confirmedAt: null
12364
+ }, input);
12365
+ if (result.status !== "stale") running.get(runKey(input))?.stop.abort();
12366
+ return result;
12367
+ },
12484
12368
  async resume(raw) {
12485
12369
  const input = sendActionSchema.parse(raw);
12486
12370
  const snapshot = await load(input);
@@ -12495,7 +12379,7 @@ function createJourneySender(dependencies) {
12495
12379
  const current = running.get(key);
12496
12380
  if (current) return current.result;
12497
12381
  const stop = new AbortController();
12498
- const result = advanceSend(dependencies, address, AbortSignal.any([signal, stop.signal])).catch(async (error) => {
12382
+ const result = advanceWork(dependencies, address, AbortSignal.any([signal, stop.signal]), phases).catch(async (error) => {
12499
12383
  if (!stop.signal.aborted || signal.aborted) throw error;
12500
12384
  return {
12501
12385
  ...requireSend(await dependencies.storage.loadSend(address.journeyId, address.sendId)),
@@ -12512,6 +12396,10 @@ function createJourneySender(dependencies) {
12512
12396
  }
12513
12397
  //#endregion
12514
12398
  //#region apps/cli/src/account/journey/auth.ts
12399
+ /** An access token this close to expiry is renewed first, so a step never starts on it. */
12400
+ const renewBeforeMs = 12e4;
12401
+ const renewTimeoutMs = 3e4;
12402
+ const renewals = /* @__PURE__ */ new WeakMap();
12515
12403
  /** Reuses the existing device sign-in credential; a token is never returned to the panel or Agent. */
12516
12404
  function createJourneySendAuth(account, requestFetch) {
12517
12405
  /**
@@ -12528,31 +12416,87 @@ function createJourneySendAuth(account, requestFetch) {
12528
12416
  return state;
12529
12417
  });
12530
12418
  }
12419
+ /** A refused access token with a refresh token is renewed on the next use instead (BL-210). */
12420
+ async function refuse(accessToken) {
12421
+ if (!(await account.credentials.update((value) => {
12422
+ if (value.credential?.accessToken === accessToken && value.credential.refreshToken) value.credential = {
12423
+ ...value.credential,
12424
+ expiresAt: account.now()
12425
+ };
12426
+ return value;
12427
+ })).credential?.refreshToken) await expire(accessToken);
12428
+ }
12429
+ function renew(credential) {
12430
+ const running = renewals.get(account);
12431
+ if (running) return running;
12432
+ const renewing = (async () => {
12433
+ try {
12434
+ const token = await account.identity.refresh(credential.refreshToken, AbortSignal.timeout(renewTimeoutMs));
12435
+ if (!token) {
12436
+ await expire(credential.accessToken);
12437
+ return null;
12438
+ }
12439
+ return (await account.credentials.update((value) => {
12440
+ if (value.credential?.refreshToken === credential.refreshToken) value.credential = {
12441
+ ...value.credential,
12442
+ accessToken: token.accessToken,
12443
+ refreshToken: token.refreshToken ?? credential.refreshToken,
12444
+ scope: token.scope,
12445
+ issuedAt: account.now(),
12446
+ expiresAt: token.expiresAt
12447
+ };
12448
+ return value;
12449
+ })).credential;
12450
+ } finally {
12451
+ renewals.delete(account);
12452
+ }
12453
+ })();
12454
+ renewals.set(account, renewing);
12455
+ return renewing;
12456
+ }
12531
12457
  async function current() {
12532
12458
  const credential = (await account.credentials.update((value) => value)).credential;
12533
12459
  if (!credential || credential.identityUrl !== account.configuration.identityUrl || credential.clientId !== account.configuration.clientId) return null;
12534
- if (!Number.isFinite(Date.parse(credential.expiresAt)) || Date.parse(credential.expiresAt) <= Date.parse(account.now())) {
12460
+ const left = Date.parse(credential.expiresAt) - Date.parse(account.now());
12461
+ if (Number.isFinite(left) && left > renewBeforeMs) return credential;
12462
+ const { refreshToken } = credential;
12463
+ if (!refreshToken) {
12464
+ if (Number.isFinite(left) && left > 0) return credential;
12535
12465
  await expire(credential.accessToken);
12536
12466
  return null;
12537
12467
  }
12538
- return credential;
12468
+ try {
12469
+ return await renew({
12470
+ ...credential,
12471
+ refreshToken
12472
+ });
12473
+ } catch (error) {
12474
+ if (Number.isFinite(left) && left > 0) return credential;
12475
+ throw error;
12476
+ }
12539
12477
  }
12540
12478
  const fetchOwner = requestFetch ?? fetch;
12541
12479
  const ownerFetch = async (input, init) => {
12542
12480
  const response = await fetchOwner(input, init);
12543
12481
  if (response.status === 401) {
12544
12482
  const header = new Headers(init?.headers).get("authorization");
12545
- if (header?.startsWith("Bearer ")) await expire(header.slice(7));
12483
+ if (header?.startsWith("Bearer ")) await refuse(header.slice(7));
12546
12484
  }
12547
12485
  return response;
12548
12486
  };
12549
12487
  return { async connect(signal) {
12550
- const credential = await current();
12488
+ let credential = await current();
12551
12489
  if (!credential) return null;
12552
- const verified = await account.identity.readIdentity(credential.accessToken, signal);
12490
+ let verified = await account.identity.readIdentity(credential.accessToken, signal);
12553
12491
  if (!verified) {
12554
- await expire(credential.accessToken);
12555
- return null;
12492
+ await refuse(credential.accessToken);
12493
+ credential = await current();
12494
+ if (!credential) return null;
12495
+ verified = await account.identity.readIdentity(credential.accessToken, signal);
12496
+ if (!verified) {
12497
+ await expire(credential.accessToken);
12498
+ return null;
12499
+ }
12556
12500
  }
12557
12501
  if (verified.identityId !== credential.identityId) throw new ApplicationError("IDENTITY_REJECTED");
12558
12502
  return createJourneyOwnerClient({
@@ -13097,6 +13041,7 @@ const deviceCodeResponse = z.object({
13097
13041
  });
13098
13042
  const tokenResponse = z.object({
13099
13043
  access_token: z.string().min(1).max(4096),
13044
+ refresh_token: z.string().min(1).max(4096).optional(),
13100
13045
  token_type: z.string(),
13101
13046
  expires_in: z.number().int().positive(),
13102
13047
  scope: deviceProfileScopeSchema
@@ -13108,13 +13053,24 @@ const userInfoResponse = z.object({
13108
13053
  picture: z.unknown().optional(),
13109
13054
  profile: z.string()
13110
13055
  });
13111
- function unavailable(cause) {
13112
- const error = new ApplicationError("IDENTITY_UNAVAILABLE");
13056
+ /**
13057
+ * `detail` names the endpoint and what failed (HTTP status, network error name, schema paths),
13058
+ * never a response value, so the authorization log can say why without exposing a token.
13059
+ */
13060
+ function unavailable(detail, cause) {
13061
+ const error = Object.assign(new ApplicationError("IDENTITY_UNAVAILABLE"), { detail });
13113
13062
  if (cause !== void 0) error.cause = cause;
13114
13063
  return error;
13115
13064
  }
13116
- async function readJson(response) {
13117
- if (!response.body) throw unavailable();
13065
+ const issuePaths = (error) => error.issues.map((issue) => issue.path.join(".") || "(root)").join(",");
13066
+ function networkDetail(error) {
13067
+ if (!(error instanceof Error)) return "Error";
13068
+ const cause = error.cause;
13069
+ const code = typeof cause === "object" && cause !== null && "code" in cause ? cause.code : null;
13070
+ return typeof code === "string" ? `${error.name} ${code}` : error.name;
13071
+ }
13072
+ async function readJson(response, where) {
13073
+ if (!response.body) throw unavailable(`${where} empty body`);
13118
13074
  const reader = response.body.getReader();
13119
13075
  const chunks = [];
13120
13076
  let bytes = 0;
@@ -13126,7 +13082,7 @@ async function readJson(response) {
13126
13082
  bytes += next.value.byteLength;
13127
13083
  if (bytes > maximumResponseBytes) {
13128
13084
  await reader.cancel().catch(() => void 0);
13129
- throw unavailable();
13085
+ throw unavailable(`${where} body too large`);
13130
13086
  }
13131
13087
  chunks.push(next.value);
13132
13088
  }
@@ -13136,7 +13092,7 @@ async function readJson(response) {
13136
13092
  try {
13137
13093
  return JSON.parse(Buffer.concat(chunks, bytes).toString("utf8"));
13138
13094
  } catch (error) {
13139
- throw unavailable(error);
13095
+ throw unavailable(`${where} invalid JSON`, error);
13140
13096
  }
13141
13097
  }
13142
13098
  function verificationUrl(value, identity) {
@@ -13144,9 +13100,9 @@ function verificationUrl(value, identity) {
13144
13100
  try {
13145
13101
  url = new URL(value);
13146
13102
  } catch (error) {
13147
- throw unavailable(error);
13103
+ throw unavailable("device-code verification URL invalid", error);
13148
13104
  }
13149
- if (!["http:", "https:"].includes(url.protocol) || url.origin !== identity.origin || url.username !== "" || url.password !== "") throw unavailable();
13105
+ if (!["http:", "https:"].includes(url.protocol) || url.origin !== identity.origin || url.username !== "" || url.password !== "") throw unavailable("device-code verification URL outside Identity");
13150
13106
  return url;
13151
13107
  }
13152
13108
  /**
@@ -13157,7 +13113,7 @@ function createIdentityDeviceClient(options) {
13157
13113
  const requestFetch = options.fetch ?? fetch;
13158
13114
  const now = options.now ?? Date.now;
13159
13115
  const base = new URL(options.identityUrl);
13160
- if (!["http:", "https:"].includes(base.protocol)) throw unavailable();
13116
+ if (!["http:", "https:"].includes(base.protocol)) throw unavailable("identity URL protocol");
13161
13117
  async function send(path, init, signal) {
13162
13118
  try {
13163
13119
  return await requestFetch(new URL(path, base), {
@@ -13167,9 +13123,18 @@ function createIdentityDeviceClient(options) {
13167
13123
  });
13168
13124
  } catch (error) {
13169
13125
  if (signal.aborted) throw error;
13170
- throw unavailable(error);
13126
+ throw unavailable(`${path} ${networkDetail(error)}`, error);
13171
13127
  }
13172
13128
  }
13129
+ function issued(token) {
13130
+ return {
13131
+ kind: "issued",
13132
+ accessToken: token.access_token,
13133
+ ...token.refresh_token ? { refreshToken: token.refresh_token } : {},
13134
+ expiresAt: new Date(now() + token.expires_in * 1e3).toISOString(),
13135
+ scope: token.scope
13136
+ };
13137
+ }
13173
13138
  function form(fields) {
13174
13139
  return {
13175
13140
  method: "POST",
@@ -13182,14 +13147,18 @@ function createIdentityDeviceClient(options) {
13182
13147
  }
13183
13148
  return {
13184
13149
  async requestDeviceCode(signal) {
13185
- const response = await send("/api/auth/device/code", form({
13150
+ let response = await send("/api/auth/device/code", form({
13151
+ client_id: options.clientId,
13152
+ scope: "openid profile offline_access"
13153
+ }), signal);
13154
+ if (response.status === 400 && errorResponse.safeParse(await readJson(response, "device-code")).data?.error === "invalid_scope") response = await send("/api/auth/device/code", form({
13186
13155
  client_id: options.clientId,
13187
13156
  scope: "openid profile"
13188
13157
  }), signal);
13189
13158
  if (response.status === 400 || response.status === 401) throw new ApplicationError("IDENTITY_REJECTED");
13190
- if (response.status !== 200) throw unavailable();
13191
- const parsed = deviceCodeResponse.safeParse(await readJson(response));
13192
- if (!parsed.success) throw unavailable(parsed.error);
13159
+ if (response.status !== 200) throw unavailable(`device-code HTTP ${response.status}`);
13160
+ const parsed = deviceCodeResponse.safeParse(await readJson(response, "device-code"));
13161
+ if (!parsed.success) throw unavailable(`device-code schema ${issuePaths(parsed.error)}`, parsed.error);
13193
13162
  const verification = verificationUrl(parsed.data.verification_uri, base);
13194
13163
  const complete = verificationUrl(parsed.data.verification_uri_complete, base);
13195
13164
  return {
@@ -13208,20 +13177,16 @@ function createIdentityDeviceClient(options) {
13208
13177
  grant_type: deviceCodeGrantType
13209
13178
  }), signal);
13210
13179
  if (response.status === 200) {
13211
- const parsed = tokenResponse.safeParse(await readJson(response));
13212
- if (!parsed.success || parsed.data.token_type.toLowerCase() !== "bearer") throw unavailable(parsed.success ? void 0 : parsed.error);
13213
- return {
13214
- kind: "issued",
13215
- accessToken: parsed.data.access_token,
13216
- expiresAt: new Date(now() + parsed.data.expires_in * 1e3).toISOString(),
13217
- scope: parsed.data.scope
13218
- };
13180
+ const parsed = tokenResponse.safeParse(await readJson(response, "token"));
13181
+ if (!parsed.success) throw unavailable(`token schema ${issuePaths(parsed.error)}`, parsed.error);
13182
+ if (parsed.data.token_type.toLowerCase() !== "bearer") throw unavailable("token type not bearer");
13183
+ return issued(parsed.data);
13219
13184
  }
13220
13185
  if (response.status !== 400) {
13221
13186
  if (response.status === 401) throw new ApplicationError("IDENTITY_REJECTED");
13222
- throw unavailable();
13187
+ throw unavailable(`token HTTP ${response.status}`);
13223
13188
  }
13224
- const parsed = errorResponse.safeParse(await readJson(response));
13189
+ const parsed = errorResponse.safeParse(await readJson(response, "token"));
13225
13190
  const code = parsed.success ? parsed.data.error : "";
13226
13191
  if (code === "authorization_pending") return { kind: "pending" };
13227
13192
  if (code === "slow_down") return { kind: "slow_down" };
@@ -13229,6 +13194,19 @@ function createIdentityDeviceClient(options) {
13229
13194
  if (code === "access_denied") return { kind: "denied" };
13230
13195
  throw new ApplicationError("IDENTITY_REJECTED");
13231
13196
  },
13197
+ async refresh(refreshToken, signal) {
13198
+ const response = await send("/api/auth/oauth2/token", form({
13199
+ client_id: options.clientId,
13200
+ grant_type: "refresh_token",
13201
+ refresh_token: refreshToken
13202
+ }), signal);
13203
+ if (response.status === 400 || response.status === 401) return null;
13204
+ if (response.status !== 200) throw unavailable(`refresh HTTP ${response.status}`);
13205
+ const parsed = tokenResponse.safeParse(await readJson(response, "refresh"));
13206
+ if (!parsed.success) throw unavailable(`refresh schema ${issuePaths(parsed.error)}`, parsed.error);
13207
+ if (parsed.data.token_type.toLowerCase() !== "bearer") throw unavailable("token type not bearer");
13208
+ return issued(parsed.data);
13209
+ },
13232
13210
  async readIdentity(accessToken, signal) {
13233
13211
  const response = await send("/api/auth/oauth2/userinfo", {
13234
13212
  method: "GET",
@@ -13238,17 +13216,17 @@ function createIdentityDeviceClient(options) {
13238
13216
  }
13239
13217
  }, signal);
13240
13218
  if (response.status === 401 || response.status === 403) return null;
13241
- if (response.status !== 200) throw unavailable();
13242
- const parsed = userInfoResponse.safeParse(await readJson(response));
13243
- if (!parsed.success) throw unavailable(parsed.error);
13219
+ if (response.status !== 200) throw unavailable(`userinfo HTTP ${response.status}`);
13220
+ const parsed = userInfoResponse.safeParse(await readJson(response, "userinfo"));
13221
+ if (!parsed.success) throw unavailable(`userinfo schema ${issuePaths(parsed.error)}`, parsed.error);
13244
13222
  const profile = accountProfileSchema.safeParse({
13245
13223
  identityId: parsed.data.sub,
13246
13224
  displayName: parsed.data.name,
13247
13225
  avatarUrl: readAccountAvatarUrl(parsed.data.picture),
13248
13226
  profileUrl: parsed.data.profile
13249
13227
  });
13250
- if (!profile.success) throw unavailable(profile.error);
13251
- if (!profileUrlMatchesIdentity(profile.data.profileUrl, options.identityUrl)) throw unavailable();
13228
+ if (!profile.success) throw unavailable(`userinfo profile ${issuePaths(profile.error)}`, profile.error);
13229
+ if (!profileUrlMatchesIdentity(profile.data.profileUrl, options.identityUrl)) throw unavailable("userinfo profile URL outside Identity");
13252
13230
  return profile.data;
13253
13231
  }
13254
13232
  };
@@ -13367,6 +13345,8 @@ function errorDetail(error) {
13367
13345
  if (typeof code === "number" && Number.isFinite(code)) detail.code = code;
13368
13346
  const syscall = data(value, "syscall");
13369
13347
  if (typeof syscall === "string") detail.syscall = text(syscall);
13348
+ const reason = data(value, "detail");
13349
+ if (typeof reason === "string") detail.detail = text(reason);
13370
13350
  const cause = data(value, "cause");
13371
13351
  if (cause !== void 0) detail.cause = visit(cause, depth + 1);
13372
13352
  const errors = data(value, "errors");
@@ -13849,6 +13829,61 @@ function createConfiguredPreviewPort(workerUrl, environment) {
13849
13829
  }) };
13850
13830
  }
13851
13831
  //#endregion
13832
+ //#region apps/cli/src/inspection/index.ts
13833
+ /**
13834
+ * Runs each inspection scan in its own Worker so static detection never holds the host's event
13835
+ * loop, which also serves requests and store accesses (BL-199).
13836
+ */
13837
+ function createInspectionPort(workerUrl, environment, onAnomaly) {
13838
+ return { async *scan(request, signal, onFailure) {
13839
+ const worker = new Worker(workerUrl, { workerData: {
13840
+ request,
13841
+ environment
13842
+ } });
13843
+ const events = [];
13844
+ let wake;
13845
+ const push = (event) => {
13846
+ events.push(event);
13847
+ wake?.();
13848
+ };
13849
+ const fail = (cause) => push({ error: new Error("INSPECTION_WORKER_FAILED", { cause }) });
13850
+ worker.on("message", (message) => {
13851
+ const parsed = workerMessageSchema.safeParse(message);
13852
+ if (!parsed.success) return fail(parsed.error);
13853
+ const value = parsed.data;
13854
+ try {
13855
+ if (value.kind === "batch") push({ batch: validateInspectionBatch(value.batch) });
13856
+ else if (value.kind === "done") push({ done: true });
13857
+ else if (value.kind === "anomaly") onAnomaly?.(value.event);
13858
+ else onFailure?.(value.stage, Object.assign(new Error(value.message), value.code ? { code: value.code } : {}));
13859
+ } catch (error) {
13860
+ fail(error);
13861
+ }
13862
+ });
13863
+ worker.on("error", fail);
13864
+ worker.on("exit", (code) => fail(/* @__PURE__ */ new Error(`Inspection Worker exited: code=${code}`)));
13865
+ const abort = () => worker.postMessage({ kind: "abort" });
13866
+ if (signal.aborted) abort();
13867
+ else signal.addEventListener("abort", abort, { once: true });
13868
+ try {
13869
+ for (;;) {
13870
+ const event = events.shift();
13871
+ if (!event) {
13872
+ await new Promise((resolve) => wake = resolve);
13873
+ wake = void 0;
13874
+ continue;
13875
+ }
13876
+ if ("done" in event) return;
13877
+ if ("error" in event) throw event.error;
13878
+ yield event.batch;
13879
+ }
13880
+ } finally {
13881
+ signal.removeEventListener("abort", abort);
13882
+ await worker.terminate();
13883
+ }
13884
+ } };
13885
+ }
13886
+ //#endregion
13852
13887
  //#region apps/cli/src/bundle-directory.ts
13853
13888
  /** The platform cache folder; Windows has none, so its default library is the launch directory. */
13854
13889
  function cacheRoot(options) {
@@ -14080,7 +14115,14 @@ async function compose(options, callbacks, bundles) {
14080
14115
  artifacts: createJourneySendArtifacts(journeyStorage),
14081
14116
  auth: createJourneySendAuth(accountPorts),
14082
14117
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
14083
- failure: (code, cause) => diagnostics.onFailure(code, cause)
14118
+ failure: (code, cause) => diagnostics.onFailure(code, cause),
14119
+ uploaded: (workId, bytes, elapsedMs) => diagnostics.onActivity({
14120
+ event: "journey.send_upload_timing",
14121
+ at: (/* @__PURE__ */ new Date()).toISOString(),
14122
+ operationId: workId,
14123
+ phases: { upload: elapsedMs },
14124
+ volumes: { bytes }
14125
+ })
14084
14126
  }),
14085
14127
  manualUploader: createManualUploader({
14086
14128
  storage: store.manualUpload,
@@ -14152,10 +14194,7 @@ async function compose(options, callbacks, bundles) {
14152
14194
  onCreated: (path) => process.stderr.write(`Temporary file may contain unredacted originals: ${path}\n`),
14153
14195
  onRetained: (path) => process.stderr.write(`Retained temporary file may contain unredacted originals: ${path}\n`)
14154
14196
  }),
14155
- inspection: createInspectionProcessor({
14156
- ...processing,
14157
- onAnomaly: producers.onInspectionAnomaly
14158
- }),
14197
+ inspection: createInspectionPort(bundles?.url("inspection-worker") ?? new URL("./inspection-worker.mjs", import.meta.url), processing.environment, producers.onInspectionAnomaly),
14159
14198
  systemResources,
14160
14199
  onDiagnostic: diagnostics.onDiagnostic,
14161
14200
  onPackagingActivity: diagnostics.onActivity,
@@ -14249,6 +14288,46 @@ async function compose(options, callbacks, bundles) {
14249
14288
  }
14250
14289
  }
14251
14290
  //#endregion
14291
+ //#region apps/cli/src/agent-presence.ts
14292
+ const journey = z.object({ journeyId: z.uuid() }).loose();
14293
+ const presence = z.object({ at: z.iso.datetime() }).strict();
14294
+ const written = /* @__PURE__ */ new Map();
14295
+ /** At most one presence write per journey this often, from one CLI process. */
14296
+ const presenceIntervalMs = 5e3;
14297
+ const presencePath = (stateDir, journeyId) => join(stateDir, "presence", `${journeyId}.json`);
14298
+ /**
14299
+ * Records that the agent just called the CLI for this journey (E16). It lives beside the journey,
14300
+ * not in it, so a heartbeat never moves the journey revision; a failed write is ignored.
14301
+ */
14302
+ async function markAgentPresence(stateDir, value) {
14303
+ const parsed = journey.safeParse(value);
14304
+ if (!parsed.success) return;
14305
+ const { journeyId } = parsed.data;
14306
+ const now = Date.now();
14307
+ if (now - (written.get(journeyId) ?? 0) < presenceIntervalMs) return;
14308
+ written.set(journeyId, now);
14309
+ try {
14310
+ const path = presencePath(stateDir, journeyId);
14311
+ await mkdir(join(stateDir, "presence"), {
14312
+ recursive: true,
14313
+ mode: 448
14314
+ });
14315
+ const temporary = `${path}.${now}.tmp`;
14316
+ await writeFile(temporary, JSON.stringify({ at: new Date(now).toISOString() }), { mode: 384 });
14317
+ await rename(temporary, path);
14318
+ } catch {
14319
+ written.delete(journeyId);
14320
+ }
14321
+ }
14322
+ /** When the agent last called the CLI for this journey, or null when it never did. */
14323
+ async function readAgentPresence(stateDir, journeyId) {
14324
+ try {
14325
+ return presence.parse(JSON.parse(await readFile(presencePath(stateDir, journeyId), "utf8")));
14326
+ } catch {
14327
+ return null;
14328
+ }
14329
+ }
14330
+ //#endregion
14252
14331
  //#region apps/cli/src/commands.ts
14253
14332
  const sendAddress = z.object({
14254
14333
  journeyId: z.uuid(),
@@ -14274,6 +14353,7 @@ const delivery = z.object({
14274
14353
  /** `signal` carries the process's SIGINT/SIGTERM to long procedures such as journey.prepare. */
14275
14354
  async function invoke(runtime, command, value, signal) {
14276
14355
  const app = runtime.application;
14356
+ markAgentPresence(runtime.stateDir, value);
14277
14357
  switch (command) {
14278
14358
  case "send.get": return app.journeySend?.get(sendAddress.partial({ sendId: true }).parse(value));
14279
14359
  case "send.prepare": return app.journeySend?.prepare(sendPrepareSchema.parse(value));
@@ -14508,4 +14588,4 @@ async function cancelOperation(runtime, id) {
14508
14588
  return readOperation(runtime, id);
14509
14589
  }
14510
14590
  //#endregion
14511
- export { startHost as _, createRuntime as a, createRenderActivity as c, businessCommands as d, longCommands as f, createExecutionPort as g, readStateDump as h, invoke as i, helpText as l, startupFailure as m, readOperation as n, executorEventSchema as o, permissionDiagnostic as p, waitOperation as r, executorRequestSchema as s, cancelOperation as t, parseOptions as u, traceStartup as v };
14591
+ export { createExecutionPort as _, readAgentPresence as a, executorRequestSchema as c, parseOptions as d, businessCommands as f, readStateDump as g, startupFailure as h, invoke as i, createRenderActivity as l, permissionDiagnostic as m, readOperation as n, createRuntime as o, longCommands as p, waitOperation as r, executorEventSchema as s, cancelOperation as t, helpText as u, startHost as v, traceStartup as y };