@gitroll/tacitt-cli 0.1.0 → 0.2.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 (50) hide show
  1. package/dist/{agent-setup-B85GaVIz.mjs → agent-setup-B8DAiQ6Z.mjs} +2 -2
  2. package/dist/agent-setup-worker.mjs +5 -5
  3. package/dist/cli.mjs +108 -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 +4 -4
  7. package/dist/{frame-D1j9R8xX.mjs → frame-waS5FnN5.mjs} +1 -1
  8. package/dist/{heap-tfU61Ehy.mjs → heap-BHcvxWCw.mjs} +20 -8
  9. package/dist/inspection-worker.mjs +57 -0
  10. package/dist/{job--TceKk0l.mjs → job-J3NFw3MH.mjs} +2 -2
  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-BJdTLNjT.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-BA0XCwnR.mjs} +486 -128
  18. package/dist/{run-CCATsHS2.mjs → run-BeLHsZWb.mjs} +4 -4
  19. package/dist/{src-DynBRDBc.mjs → src-B5SArpVY.mjs} +20 -9
  20. package/dist/{src-CPPjOyHZ.mjs → src-CC95LYYp.mjs} +271 -68
  21. package/dist/{src-BngGpFKd.mjs → src-k0Ya1Bri.mjs} +13 -7
  22. package/dist/store-database-worker.mjs +1 -1
  23. package/dist/web/assets/index-Bjos73wK.js +67 -0
  24. package/dist/web/assets/index-Bl4C2b2_.css +1 -0
  25. package/dist/web/index.html +2 -2
  26. package/package.json +1 -1
  27. package/skills/tacitt/SKILL.md +13 -11
  28. package/skills/tacitt/references/b-method.md +1 -1
  29. package/skills/tacitt/references/classification.md +3 -3
  30. package/skills/tacitt/references/commands.md +6 -6
  31. package/skills/tacitt/references/journey.md +13 -11
  32. package/skills/tacitt/references/parallel-review.md +3 -3
  33. package/skills/tacitt/references/send.md +5 -5
  34. package/skills/tacitt/scripts/answer.mjs +29 -8
  35. package/skills/tacitt/scripts/journey-card-consolidation.d.mts +13 -4
  36. package/skills/tacitt/scripts/journey-card-consolidation.mjs +81 -18
  37. package/skills/tacitt/scripts/journey-classification-progress.mjs +2 -1
  38. package/skills/tacitt/scripts/journey-classification-validation.mjs +49 -28
  39. package/skills/tacitt/scripts/journey-consolidation-submit.mjs +42 -5
  40. package/skills/tacitt/scripts/journey-consolidation.d.mts +5 -0
  41. package/skills/tacitt/scripts/journey-consolidation.mjs +4 -1
  42. package/skills/tacitt/scripts/journey-state.d.mts +1 -0
  43. package/skills/tacitt/scripts/journey-state.mjs +29 -3
  44. package/skills/tacitt/scripts/journey-tasks.mjs +4 -2
  45. package/skills/tacitt/scripts/journey.d.mts +2 -0
  46. package/skills/tacitt/scripts/journey.mjs +154 -13
  47. package/skills/tacitt/scripts/privacy-validator.mjs +25 -6
  48. package/skills/tacitt/scripts/review.mjs +24 -3
  49. package/dist/web/assets/index-CWQ05PFl.js +0 -67
  50. 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, 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, 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-k0Ya1Bri.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-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-B5SArpVY.mjs";
4
+ import { $n as reviewRepairResourceSchema, $t as journeyPolicyRespondSchema, A as createAccount, Ar as hasCurrentStaticReview, At as sendReplaceResultSchema, B as createApplication, Bn as confirmedHideKinds, C as bReviewScope, Cn as confirmedHideAdditions, Cr as journeyClassificationSessionsSchema, Ct as sendActionSchema, D as reviewBatchReuse, Dr as nativeEventsSchema, Dt as journeySendSchema, E as rebindReviewSubmit, Er as privacyDecisionDigest, Et as sendStartSchema, F as readAccountAvatarUrl, Fr as isClassifying, Ft as obsoleteJourney, Gr as journeyRenderSchema, Gt as journeyContextQuerySchema, H as inspectionStartSchema, Hr as journeyFindingsSchema, I as storedCredentialSchema, In as bTermMatcher, Ir as isReadingComplete, Jr as selectionBatchSchema, K as inspectionQuerySchema, Kn as retireWorkDescription, Kr as commandSchema, Lr as isSelectedWork, M as deviceAuthorizationSchema, Mt as sendSettingsSchema, N as deviceProfileScopeSchema, Nr as staticSessionResourceSchema, O as guardedFindingText, On as termHideAdditions, Or as nativeGetSchema, Ot as sendArtifactSchema, P as profileUrlMatchesIdentity, Pr as hasCompleteWorkDescription, Pt as currentJourneyAdapters, Qn as reviewRepairInputSchema, Qr as stateSchema, Qt as searchScopeDigest, Rn as confirmedHideGroupIds, S as isGenericPublicTerm, St as replaceSensitiveTermsSchema, T as rebindReviewOverlay, Tn as readConfirmedHideChange, Tr as assertJourneyPrivacyReady, Tt as sendPrepareSchema, U as validateInspectionBatch, Un as journeyConfirmedHides, Ur as journeyPreviewSchema, Ut as journeyReviewStorageQuerySchema, V as inspectionCancelSchema, Vr as journeyFindingImpactSchema, Vt as applyClassification, Wn as maskedHitsStale, Wr as journeyRenderInputSchema, Wt as reviewCompileSchema, Xn as reviewCandidateInputSchema, Xt as journeySearchResultSchema, Yn as workCorrectionBinding, Yr as selectionSchema, Zn as reviewCandidateResourceSchema, Zr as producerVersion, _ as hideScopeDigest, _t as manualUploadStartSchema, a as locateTermEvidence, ai as selectFrozenConfiguration, at as appendSensitiveTerms, bt as maximumTermsFileBytes, c as bRereadDigest, cr as scanGitSeedsId, d as newJourneyWork, dr as scanExclusionResourceSchema, dt as validateFailureLocation, ei as recommendParallelism, fn as sessionRereadInputSchema, ft as manualUploadActionSchema, g as activeJourneyTerms, gt as manualUploadSchema, h as splitSelectorValues, hn as sessionReviewQuerySchema, ht as manualUploadPrepareSchema, i as scanBatchEquivalenceDigest, ii as freezeConfiguration, in as journeyPlaceSchema, j as accountProfileSchema, jr as staticReviewDigest, jt as sendResolutionSchema, k as staticallyHiddenFindings, kr as nativeRespondSchema, kt as sendReceiptSchema, l as reconcileBSession, ln as sessionReconcileInputSchema, lr as scanGitSeedsResource, lt as artifactFailureDetailSchema, m as selectedValueMembers, mn as sessionReviewInputSchema, mr as journeyInventoryQuerySchema, mt as manualUploadChoiceSchema, n as retireScanGeneration, nt as settingsQuerySchema, o as validateBCrossSegmentReview, on as reviewAttemptResourceSchema, or as scanPreparationDigest, qn as workDecisionDigest, qr as packageCommandSchema, r as scanBatchContextDigest, ri as defaultScopedSettings, rr as planBRepair, rt as readWorkspaceOutcomes, s as bCrossSegmentReviewSchema, sr as scanSessionResourceSchema, t as resetWorkScan, tn as journeyResourceQuerySchema, tr as bReviewDigest, tt as settingsBatchSchema, u as reconstructBFields, ur as verifiedScanGitSeeds, ut as resolveFailureLocation, vn as sessionUsefulnessInputSchema, vr as emptyPreparationFacts, w as rebindReviewAlignment, wt as sendChoiceSchema, xt as normalizeSensitiveTerms, y as changeJourney, yt as emptyTermsRevision, zr as withReadingProgress } from "./src-CC95LYYp.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-BHcvxWCw.mjs";
6
+ import { a as writeJourneyOutput, c as mergeHideIndexTokens, l as packRenderHideIndex, o as createHideIndexTokens, s as hideIndexTexts, t as journeyOutputJob } from "./job-J3NFw3MH.mjs";
7
+ import { t as createAgentSetupPort } from "./agent-setup-B8DAiQ6Z.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";
@@ -826,12 +827,16 @@ function runObserved(frame, leg, work) {
826
827
  }
827
828
  //#endregion
828
829
  //#region packages/local-host/src/router.ts
829
- /** The browser may explicitly change one Work's visibility; all other decisions stay in chat. */
830
+ /** Decisions the person makes in the panel while the agent waits for them. */
830
831
  const nativePanelRespondSchema = z.object({
831
832
  journeyId: nativeRespondSchema.shape.journeyId,
832
833
  expectedRevision: nativeRespondSchema.shape.expectedRevision,
833
834
  idempotencyKey: nativeRespondSchema.shape.idempotencyKey,
834
835
  action: z.union([
836
+ z.object({
837
+ kind: z.literal("consent"),
838
+ allow: z.boolean()
839
+ }).strict(),
835
840
  z.object({
836
841
  kind: z.literal("work-access"),
837
842
  workId: z.uuid(),
@@ -845,12 +850,13 @@ const nativePanelRespondSchema = z.object({
845
850
  kind: z.literal("card-answer"),
846
851
  cardId: z.uuid(),
847
852
  optionId: z.string(),
848
- remember: z.boolean().optional()
853
+ remember: z.boolean().optional(),
854
+ excludedTermKeys: z.array(z.string().min(1)).max(2e4).optional()
849
855
  }).strict(),
850
856
  z.object({ kind: z.literal("package-confirm") }).strict()
851
857
  ])
852
858
  }).strict();
853
- function createRouter(application, account, stopping, delivery, nativeInspection) {
859
+ function createRouter(application, account, stopping, delivery, nativeInspection, agentPresence) {
854
860
  const t = initTRPC.context().create({
855
861
  sse: {
856
862
  ping: {
@@ -920,6 +926,7 @@ function createRouter(application, account, stopping, delivery, nativeInspection
920
926
  respond: t.procedure.input(sendChoiceSchema).mutation(({ input }) => sending().respond(input)),
921
927
  pause: t.procedure.input(sendActionSchema).mutation(({ input }) => sending().pause(input)),
922
928
  resume: t.procedure.input(sendActionSchema).mutation(({ input }) => sending().resume(input)),
929
+ withdraw: t.procedure.input(sendActionSchema).mutation(({ input }) => sending().withdraw(input)),
923
930
  advance: t.procedure.input(sendAddress).mutation(({ input }) => sending().advance(input, stopping))
924
931
  }),
925
932
  manualUpload: t.router({
@@ -942,6 +949,7 @@ function createRouter(application, account, stopping, delivery, nativeInspection
942
949
  findingImpact: t.procedure.input(journeyFindingImpactSchema).query(({ input, signal }) => native().findingImpact(input, signal)),
943
950
  preview: t.procedure.input(journeyPreviewSchema).query(({ input, signal }) => native().preview(input, signal)),
944
951
  get: t.procedure.input(nativeGetSchema).query(({ input, signal }) => readThroughContention(() => native().get(input), signal)),
952
+ presence: t.procedure.input(z.object({ journeyId: z.uuid() }).strict()).query(async ({ input }) => ({ agent: await agentPresence?.(input.journeyId) ?? null })),
945
953
  inventory: t.procedure.input(journeyInventoryQuerySchema).query(({ input }) => native().inventory(input)),
946
954
  events: t.procedure.input(nativeEventsSchema).query(({ input, signal }) => readThroughContention(() => native().events(input, signal), signal))
947
955
  }),
@@ -2827,7 +2835,7 @@ async function startHost(options) {
2827
2835
  reply.raw.once("close", () => streams.delete(reply.raw));
2828
2836
  });
2829
2837
  const trpcOptions = {
2830
- router: createRouter(options.application, options.account, stopping.signal, delivery, options.nativeInspection),
2838
+ router: createRouter(options.application, options.account, stopping.signal, delivery, options.nativeInspection, options.agentPresence),
2831
2839
  createContext: ({ req }) => {
2832
2840
  const currentView = requestObservations.get(req);
2833
2841
  return {
@@ -5458,6 +5466,10 @@ function parseOptions(args, source = sourceCheckout) {
5458
5466
  type: "boolean",
5459
5467
  default: false
5460
5468
  },
5469
+ detach: {
5470
+ type: "boolean",
5471
+ default: false
5472
+ },
5461
5473
  "state-dir": { type: "string" },
5462
5474
  "bundle-dir": { type: "string" },
5463
5475
  "claude-home": { type: "string" },
@@ -5507,6 +5519,7 @@ function parseOptions(args, source = sourceCheckout) {
5507
5519
  "all"
5508
5520
  ].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
5521
  if (parsed.values.async && (!command || !longCommands.has(command))) throw new Error("INVALID_ARGUMENTS");
5522
+ if (parsed.values.detach && command !== "journey.serve") throw new Error("INVALID_ARGUMENTS");
5510
5523
  const timeoutMs = parsed.values["timeout-ms"] === void 0 ? 3e4 : Number(parsed.values["timeout-ms"]);
5511
5524
  if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647) throw new Error("INVALID_ARGUMENTS");
5512
5525
  if (parsed.values.output && !stateDump) throw new Error("INVALID_ARGUMENTS");
@@ -5552,6 +5565,8 @@ ${Array.from(businessCommands).join("\n")}
5552
5565
 
5553
5566
  Business commands invoke the shared Application directly without an HTTP server.
5554
5567
  journey.serve starts a focused confirmation host, emits JSON readiness, and waits until stopped.
5568
+ journey.serve --detach starts that host in its own process, prints its readiness with the host's
5569
+ pid, and exits; stop the host with that pid.
5555
5570
  It does not discover sessions or open a browser. The agent opens its private URL in the right pane.
5556
5571
  Workflow mutations require protocolVersion, runId and expectedRevision in JSON input.
5557
5572
  Long commands emit the operation ID immediately as NDJSON, then wait up to 30000 ms.
@@ -7254,7 +7269,8 @@ async function saveStaticSession(storage, binding, item, result, signal) {
7254
7269
  result: await describe(resultPath, signal),
7255
7270
  primary: await describe(result.spoolPath, signal),
7256
7271
  companion: await describe(result.companionSpoolPath, signal),
7257
- masks: await describe(result.maskSpoolPath, signal)
7272
+ masks: await describe(result.maskSpoolPath, signal),
7273
+ ...result.images === void 0 ? {} : { imagesOmitted: result.images }
7258
7274
  });
7259
7275
  await storage.saveResource(resource);
7260
7276
  saved = true;
@@ -7413,6 +7429,7 @@ async function prepareStaticWork(options) {
7413
7429
  const target = current.works.find((item) => item.id === work.id);
7414
7430
  if (current.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
7415
7431
  if (current.snapshotId !== snapshotId || target?.inputRevision !== work.inputRevision) throw new ApplicationError("REVISION_CONFLICT");
7432
+ const images = resources.map((resource) => resource.imagesOmitted);
7416
7433
  return {
7417
7434
  ...current,
7418
7435
  works: current.works.map((item) => item.id === work.id ? {
@@ -7420,7 +7437,8 @@ async function prepareStaticWork(options) {
7420
7437
  staticReview: {
7421
7438
  snapshotId,
7422
7439
  inputRevision: work.inputRevision,
7423
- digest: staticReviewDigest(resources)
7440
+ digest: staticReviewDigest(resources),
7441
+ ...images.every((count) => count !== void 0) ? { imagesOmitted: images.reduce((sum, count) => sum + count, 0) } : {}
7424
7442
  }
7425
7443
  } : item)
7426
7444
  };
@@ -7657,8 +7675,8 @@ function createJourneyPreparation(options) {
7657
7675
  }));
7658
7676
  if (!state.works.length) throw new ApplicationError("INVALID_SELECTION");
7659
7677
  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);
7678
+ const scanWorks = scanRequested && state.scope.status === "confirmed" ? state.works : [];
7679
+ if (scanWorks.length) run = await runWorkers(options.workers);
7662
7680
  const frozen = state;
7663
7681
  const blockWork = (work, error) => changeJourney(storage, journeyId, "snapshot", (before) => {
7664
7682
  if (before.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
@@ -7672,8 +7690,8 @@ function createJourneyPreparation(options) {
7672
7690
  });
7673
7691
  }, 100);
7674
7692
  let submittedWorks = 0;
7675
- const submitted = Promise.withResolvers();
7676
- if (!works.length) submitted.resolve();
7693
+ let works = [];
7694
+ let submitted = Promise.withResolvers();
7677
7695
  const submission = () => {
7678
7696
  let done = false;
7679
7697
  return () => {
@@ -7694,6 +7712,14 @@ function createJourneyPreparation(options) {
7694
7712
  return false;
7695
7713
  }
7696
7714
  };
7715
+ /** D2: the person dropped this Work while it was prepared; the other Works go on. */
7716
+ const droppedMidScan = async (workId) => {
7717
+ const now = await storage.current(journeyId);
7718
+ const work = now?.works.find((item) => item.id === workId);
7719
+ return Boolean(now && now.phase !== "paused" && now.scope.status === "confirmed" && work && !isSelectedWork(work));
7720
+ };
7721
+ /** Scan generations this run prepared, including those its own isolation started. */
7722
+ const prepared = /* @__PURE__ */ new Set();
7697
7723
  /** True when failing roots left the Work, which is then prepared again (ADR 0188). */
7698
7724
  const prepareWorkOnce = async (planned, onSubmitted) => {
7699
7725
  abort.signal.throwIfAborted();
@@ -7707,6 +7733,7 @@ function createJourneyPreparation(options) {
7707
7733
  "reading",
7708
7734
  "review"
7709
7735
  ].includes(work.status)) return false;
7736
+ prepared.add(`${work.id}:${work.inputRevision}`);
7710
7737
  await prepareClassificationMetadata(storage, latest, work, options.processing, abort.signal);
7711
7738
  let failures = collectPreparationFailures(plan.approval.inventory, work.rootSessionIds, {
7712
7739
  journeyId,
@@ -7723,6 +7750,7 @@ function createJourneyPreparation(options) {
7723
7750
  onTiming: (timing) => options.onTiming?.(planned.id, timing)
7724
7751
  }, run.workers, (revised) => {
7725
7752
  work = revised;
7753
+ prepared.add(`${revised.id}:${revised.inputRevision}`);
7726
7754
  failures = collectPreparationFailures(plan.approval.inventory, revised.rootSessionIds, {
7727
7755
  journeyId,
7728
7756
  snapshotId: latest.snapshotId,
@@ -7735,7 +7763,10 @@ function createJourneyPreparation(options) {
7735
7763
  const code = safeCode(error);
7736
7764
  if (code !== "OPERATION_CANCELED") options.onFailure(error, failures.read());
7737
7765
  abort.signal.throwIfAborted();
7738
- if (code === "OPERATION_CANCELED") throw error;
7766
+ if (code === "OPERATION_CANCELED") {
7767
+ if (await droppedMidScan(planned.id)) return false;
7768
+ throw error;
7769
+ }
7739
7770
  if (work && await isolateRoots(work, error)) return true;
7740
7771
  if (work) await blockWork(work, error);
7741
7772
  }
@@ -7745,14 +7776,14 @@ function createJourneyPreparation(options) {
7745
7776
  for (let attempt = 0; attempt < planned.rootSessionIds.length; attempt++) if (!await prepareWorkOnce(planned, onSubmitted)) return;
7746
7777
  };
7747
7778
  /** 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;
7779
+ const staticWork = async (planned, passSubmitted) => {
7780
+ await passSubmitted;
7750
7781
  abort.signal.throwIfAborted();
7751
7782
  const latest = await storage.current(journeyId);
7752
7783
  abort.signal.throwIfAborted();
7753
7784
  if (!latest?.consent || latest.phase === "paused" || latest.scope.status !== "confirmed") throw new ApplicationError("OPERATION_CANCELED");
7754
7785
  const work = latest.works.find((item) => item.id === planned.id);
7755
- if (!work) return;
7786
+ if (!work || !isSelectedWork(work)) return;
7756
7787
  try {
7757
7788
  await inBackgroundStoreLane(() => prepareStaticWork({
7758
7789
  storage,
@@ -7767,6 +7798,7 @@ function createJourneyPreparation(options) {
7767
7798
  } catch (error) {
7768
7799
  abort.signal.throwIfAborted();
7769
7800
  const code = safeCode(error);
7801
+ if (code === "OPERATION_CANCELED" && await droppedMidScan(work.id)) return;
7770
7802
  if (code === "COORDINATION_BUSY" || code === "OPERATION_CANCELED") throw error;
7771
7803
  options.onFailure(error);
7772
7804
  await blockWork(work, error);
@@ -7779,23 +7811,50 @@ function createJourneyPreparation(options) {
7779
7811
  abort.abort(error);
7780
7812
  };
7781
7813
  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();
7814
+ const pass = async (batch) => {
7815
+ works = batch;
7816
+ next = 0;
7817
+ submittedWorks = 0;
7818
+ submitted = Promise.withResolvers();
7819
+ if (!works.length) submitted.resolve();
7820
+ const passSubmitted = submitted.promise;
7821
+ await Promise.all(Array.from({ length: run.pool ? works.length : Math.min(1, works.length) }, async () => {
7822
+ for (let work = works[next++]; work && !fatal; work = works[next++]) {
7823
+ const planned = work;
7824
+ const onSubmitted = submission();
7825
+ try {
7826
+ await retryContention(() => prepareWork(planned, onSubmitted), (error) => options.onFailure(error), abort.signal);
7827
+ } catch (error) {
7828
+ stop(error);
7829
+ } finally {
7830
+ onSubmitted();
7831
+ }
7832
+ statics.push(retryContention(() => staticWork(planned, passSubmitted), (error) => options.onFailure(error), abort.signal).catch(stop));
7792
7833
  }
7793
- statics.push(retryContention(() => staticWork(planned), (error) => options.onFailure(error), abort.signal).catch(stop));
7834
+ }));
7835
+ while (submittedWorks < works.length) submission()();
7836
+ };
7837
+ /** Selected Works this run has not prepared yet, waiting while grouping continues (E2). */
7838
+ const later = async () => {
7839
+ for (;;) {
7840
+ abort.signal.throwIfAborted();
7841
+ const latest = await storage.current(journeyId);
7842
+ if (!latest || latest.phase === "paused" || latest.scope.status !== "confirmed") return [];
7843
+ const ready = latest.works.filter((work) => isSelectedWork(work) && [
7844
+ "pending",
7845
+ "reading",
7846
+ "review"
7847
+ ].includes(work.status) && !prepared.has(`${work.id}:${work.inputRevision}`));
7848
+ if (ready.length || !isClassifying(latest)) return ready;
7849
+ await setTimeout$1(laterWorksPollMs, void 0, { signal: abort.signal }).catch(() => abort.signal.throwIfAborted());
7794
7850
  }
7795
- }));
7796
- while (submittedWorks < works.length) submission()();
7851
+ };
7852
+ for (let batch = scanWorks; batch.length && !fatal; batch = await later()) {
7853
+ if (!run.workers && !run.pool) run = await runWorkers(options.workers);
7854
+ await pass(batch);
7855
+ }
7797
7856
  await Promise.all(statics);
7798
- if (works.length) options.onTiming?.(journeyId, {
7857
+ if (scanWorks.length) options.onTiming?.(journeyId, {
7799
7858
  scope: "preparation",
7800
7859
  phases: { "journey-prepare": Math.round(performance$1.now() - startedAt) },
7801
7860
  volumes: {
@@ -7820,6 +7879,8 @@ function createJourneyPreparation(options) {
7820
7879
  }
7821
7880
  };
7822
7881
  }
7882
+ /** How often a run waiting for grouping to publish more Works checks the journey (E2). */
7883
+ const laterWorksPollMs = 2e3;
7823
7884
  /**
7824
7885
  * Another process can hold the store past one admission deadline (ADR 0082); the Work runs
7825
7886
  * again after a bounded backoff instead of stopping the pass. A pending Work re-prepares
@@ -7931,12 +7992,21 @@ const priorities = [
7931
7992
  ];
7932
7993
  //#endregion
7933
7994
  //#region apps/cli/src/journey/keywords.ts
7995
+ /** The journey's frozen source plan; it is saved once and never rewritten. */
7996
+ async function readJourneyPlan(storage, journeyId) {
7997
+ const plan = (await storage.resources({
7998
+ journeyId,
7999
+ kind: "plan"
8000
+ }))[0];
8001
+ if (plan?.kind !== "plan") throw new ApplicationError("CORRUPTED_STATE");
8002
+ return plan;
8003
+ }
7934
8004
  /**
7935
8005
  * ADR 0172: the confirmed dictionary applied once, as the last step before the ZIP — custom
7936
8006
  * terms, the snapshot-frozen Git seeds of the given sessions' workspaces (ADR 0169, never live
7937
8007
  * Git) and AI-confirmed Hidden values. Neither scan track applies any of these.
7938
8008
  */
7939
- async function finalKeywordDictionary(storage, state, sessionIds) {
8009
+ async function finalKeywordDictionary(storage, state, sessionIds, plan) {
7940
8010
  if (!state.snapshotId) throw new ApplicationError("CONFIRMATION_REQUIRED");
7941
8011
  const dictionary = /* @__PURE__ */ new Map();
7942
8012
  const add = (term, kind, bounded) => {
@@ -7951,16 +8021,12 @@ async function finalKeywordDictionary(storage, state, sessionIds) {
7951
8021
  const admitted = admitCustomTerm(value);
7952
8022
  if (admitted) add(admitted.value, "private", false);
7953
8023
  }
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");
8024
+ const { approval } = plan ?? await readJourneyPlan(storage, state.journeyId);
7959
8025
  const sessions = new Set(sessionIds);
7960
8026
  const roots = /* @__PURE__ */ new Map();
7961
- for (const session of plan.approval.inventory.sessions) {
8027
+ for (const session of approval.inventory.sessions) {
7962
8028
  if (!sessions.has(session.id)) continue;
7963
- const workspace = plan.approval.inventory.workspaces.find((item) => item.id === session.workspaceId);
8029
+ const workspace = approval.inventory.workspaces.find((item) => item.id === session.workspaceId);
7964
8030
  if (!workspace) throw new ApplicationError("CORRUPTED_STATE");
7965
8031
  const paths = workspace.path === null ? [] : [workspace.path];
7966
8032
  roots.set(JSON.stringify(paths), paths);
@@ -8486,7 +8552,7 @@ function createJourneyRenderer(options) {
8486
8552
  work
8487
8553
  };
8488
8554
  }
8489
- async function produce(state, work, signal, write, session) {
8555
+ async function produce(state, work, signal, write, session, plan) {
8490
8556
  const { journeyId } = state;
8491
8557
  const workId = work.id;
8492
8558
  assertJourneyPrivacyReady(state, true);
@@ -8518,12 +8584,12 @@ function createJourneyRenderer(options) {
8518
8584
  });
8519
8585
  const next = moved.works.find((item) => item.id === workId);
8520
8586
  if (!next) throw new ApplicationError("REVISION_CONFLICT");
8521
- return produce(moved, next, signal, write, session);
8587
+ return produce(moved, next, signal, write, session, plan);
8522
8588
  }
8523
8589
  if (!output) {
8524
8590
  const directory = await journeyDirectory(options.stateDir, journeyId);
8525
8591
  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);
8592
+ const { base, tokens, confirmed, request } = await journeyOutputRequest(storage, state, work, options.processing, signal, await plan());
8527
8593
  const { output: written, tokens: collected } = await write({
8528
8594
  request: {
8529
8595
  ...request,
@@ -8614,10 +8680,10 @@ function createJourneyRenderer(options) {
8614
8680
  };
8615
8681
  });
8616
8682
  }
8617
- async function render(journeyId, workId, signal, write = writeJourneyOutput, session = directRenderSession(storage, journeyId)) {
8683
+ async function render(journeyId, workId, signal, write = writeJourneyOutput, session = directRenderSession(storage, journeyId), plan = () => readJourneyPlan(storage, journeyId)) {
8618
8684
  const { state, work } = await admit(workId, session);
8619
8685
  try {
8620
- return await produce(state, work, signal, write, session);
8686
+ return await produce(state, work, signal, write, session, plan);
8621
8687
  } catch (error) {
8622
8688
  if (signal.aborted) throw error;
8623
8689
  const renderFailure = {
@@ -8650,9 +8716,14 @@ function createJourneyRenderer(options) {
8650
8716
  if (!workers || works < 2 || size < 1) return void 0;
8651
8717
  const pool = createRenderWorkers(workers, size);
8652
8718
  const session = batchedRenderSession(storage, journeyId);
8719
+ let plan;
8720
+ const readPlan = () => plan ??= readJourneyPlan(storage, journeyId).catch((error) => {
8721
+ plan = void 0;
8722
+ throw error;
8723
+ });
8653
8724
  return {
8654
8725
  concurrency: size + 1,
8655
- render: (workId) => render(journeyId, workId, signal, pool.write, session),
8726
+ render: (workId) => render(journeyId, workId, signal, pool.write, session, readPlan),
8656
8727
  close: pool.close
8657
8728
  };
8658
8729
  }
@@ -8662,7 +8733,7 @@ function createJourneyRenderer(options) {
8662
8733
  };
8663
8734
  }
8664
8735
  /** Package-only native output request: the final keyword dictionary covers all retained fields. */
8665
- async function journeyOutputRequest(storage, state, work, processingConfig, _signal) {
8736
+ async function journeyOutputRequest(storage, state, work, processingConfig, _signal, plan) {
8666
8737
  const { journeyId } = state;
8667
8738
  const workId = work.id;
8668
8739
  const base = (await storage.resources({
@@ -8670,14 +8741,11 @@ async function journeyOutputRequest(storage, state, work, processingConfig, _sig
8670
8741
  kind: "base",
8671
8742
  id: `${workId}:${work.inputRevision}`
8672
8743
  })).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");
8744
+ if (base?.kind !== "base") throw new ApplicationError("CORRUPTED_STATE");
8745
+ const { replacementKey } = plan ?? await readJourneyPlan(storage, journeyId);
8678
8746
  const hides = createFamilyHides();
8679
8747
  const confirmed = await journeyConfirmedHides(storage, state);
8680
- const dictionary = await finalKeywordDictionary(storage, state, base.sessions.map((session) => session.applicationSessionId));
8748
+ const dictionary = await finalKeywordDictionary(storage, state, base.sessions.map((session) => session.applicationSessionId), plan);
8681
8749
  const exceptions = workExceptions(state, workId);
8682
8750
  const tokens = createHideIndexTokens();
8683
8751
  const title = await renderTitle(storage, state, work);
@@ -8692,7 +8760,7 @@ async function journeyOutputRequest(storage, state, work, processingConfig, _sig
8692
8760
  companion: base.companion,
8693
8761
  workId,
8694
8762
  baseBundleId: base.bundleId,
8695
- replacementKey: plan.replacementKey,
8763
+ replacementKey,
8696
8764
  edits: [],
8697
8765
  families: hides.request(base.sessions, dictionary, exceptions),
8698
8766
  metadata: {
@@ -9458,14 +9526,21 @@ async function regroupPrivacySources(storage, state, works, changedMetadata, sig
9458
9526
  }
9459
9527
  //#endregion
9460
9528
  //#region apps/cli/src/journey/works/proposal.ts
9461
- /** One atomic complete root partition; recommendations have no access authority. */
9529
+ /**
9530
+ * One atomic root partition; recommendations have no access authority. Roots still being grouped
9531
+ * wait in one unread Work while the other Works are confirmed and read; the submission that
9532
+ * groups them may arrive after confirmation and keeps it (E2).
9533
+ */
9462
9534
  function createClassificationProposal(storage) {
9463
9535
  return async (input, signal) => {
9464
9536
  signal.throwIfAborted();
9465
9537
  const state = await storage.current(input.journeyId);
9466
9538
  if (!state?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
9539
+ const classifying = isClassifying(state);
9540
+ const late = state.scope.status === "confirmed";
9541
+ const bound = input.expectedClassificationRevision !== void 0 && classifying && input.expectedClassificationRevision === state.classificationRevision;
9467
9542
  const commit = (next = state) => storage.commit({
9468
- expectedRevision: input.expectedRevision,
9543
+ expectedRevision: bound ? state.revision : input.expectedRevision,
9469
9544
  requestKey: input.idempotencyKey,
9470
9545
  requestHash: textDigest(JSON.stringify(input)),
9471
9546
  state: next,
@@ -9476,8 +9551,8 @@ function createClassificationProposal(storage) {
9476
9551
  at: (/* @__PURE__ */ new Date()).toISOString()
9477
9552
  }
9478
9553
  });
9479
- if (state.revision !== input.expectedRevision) return commit();
9480
- if (state.scope.status === "confirmed") throw new ApplicationError("INVALID_TRANSITION");
9554
+ if (input.expectedClassificationRevision !== void 0 ? !bound : state.revision !== input.expectedRevision) return commit();
9555
+ if (late && !classifying) throw new ApplicationError("INVALID_TRANSITION");
9481
9556
  if (state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
9482
9557
  if (!state.snapshotId || state.pendingHideId) throw new ApplicationError("SCOPE_NOT_READY");
9483
9558
  const plan = (await storage.resources({
@@ -9503,6 +9578,11 @@ function createClassificationProposal(storage) {
9503
9578
  });
9504
9579
  } else merged.set(key, item);
9505
9580
  }
9581
+ const pending = input.pendingRootSessionIds ?? [];
9582
+ for (const root of pending) {
9583
+ if (!roots.has(root) || seen.has(root)) throw new ApplicationError("INVALID_SELECTION");
9584
+ seen.add(root);
9585
+ }
9506
9586
  if (seen.size !== roots.size) throw new ApplicationError("INVALID_SELECTION");
9507
9587
  const families = reviewFamilies(plan.approval.inventory);
9508
9588
  const captures = await storage.resources({
@@ -9515,7 +9595,18 @@ function createClassificationProposal(storage) {
9515
9595
  signal.throwIfAborted();
9516
9596
  const sessions = item.rootSessionIds.flatMap((root) => families.members.get(root) ?? []);
9517
9597
  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))]);
9598
+ const existing = state.works.find((work) => work.placement !== "classifying" && work.rootSessionIds.length === item.rootSessionIds.length && work.rootSessionIds.every((root) => item.rootSessionIds.includes(root)));
9599
+ const extended = late ? state.works.find((work) => work.placement === "placed" && work.rootSessionIds.every((root) => item.rootSessionIds.includes(root))) : void 0;
9600
+ if (late && existing) {
9601
+ works.push(existing);
9602
+ continue;
9603
+ }
9604
+ const workspaces = [...new Set(sessions.map((session) => session.workspaceId))];
9605
+ const base = existing ?? (extended ? {
9606
+ ...resetWorkScan(extended),
9607
+ rootSessionIds: item.rootSessionIds,
9608
+ sourceWorkspaceIds: [.../* @__PURE__ */ new Set([...extended.sourceWorkspaceIds, ...workspaces])]
9609
+ } : newJourneyWork(randomUUID(), item.projectName, item.rootSessionIds, workspaces, "placed"));
9519
9610
  const { classificationMetadata: _display, ...original } = base;
9520
9611
  works.push({
9521
9612
  ...original,
@@ -9528,12 +9619,22 @@ function createClassificationProposal(storage) {
9528
9619
  origin: "model"
9529
9620
  },
9530
9621
  placement: "placed",
9622
+ ...late && !extended ? {
9623
+ access: item.recommendation.access,
9624
+ accessOrigin: "model",
9625
+ accessConfirmed: true
9626
+ } : {},
9531
9627
  classificationRecommendation: item.recommendation,
9532
9628
  suggestion: null,
9533
9629
  artifact: null,
9534
9630
  revision: base.revision + 1
9535
9631
  });
9536
9632
  }
9633
+ if (pending.length) {
9634
+ const prior = state.works.find((work) => work.placement === "classifying" && work.rootSessionIds.length === pending.length && work.rootSessionIds.every((root) => pending.includes(root)));
9635
+ const sessions = pending.flatMap((root) => families.members.get(root) ?? []);
9636
+ works.push(prior ?? newJourneyWork(randomUUID(), "Still being grouped", pending, [...new Set(sessions.map((session) => session.workspaceId))], "classifying"));
9637
+ }
9537
9638
  const retained = new Set(works.map((work) => work.id));
9538
9639
  const changedMetadata = new Set(works.filter((work) => {
9539
9640
  const old = state.works.find((item) => item.id === work.id);
@@ -9549,7 +9650,7 @@ function createClassificationProposal(storage) {
9549
9650
  old.classificationRecommendation?.reason
9550
9651
  ]);
9551
9652
  }).map((work) => work.id));
9552
- const regrouped = state.works.some((work) => !retained.has(work.id)) || changedMetadata.size > 0;
9653
+ const regrouped = !late && (state.works.some((work) => !retained.has(work.id)) || changedMetadata.size > 0);
9553
9654
  const cardTerms = new Set(state.privacy.cards.flatMap((card) => card.termKeys));
9554
9655
  const preserved = await regroupPrivacySources(storage, state, works, changedMetadata, signal);
9555
9656
  const forced = new Set(preserved.filter((term) => !termTypeInfo[term.type].asked).map((term) => term.value));
@@ -9562,7 +9663,7 @@ function createClassificationProposal(storage) {
9562
9663
  ...state,
9563
9664
  works,
9564
9665
  classificationRevision: state.revision + 1,
9565
- scope: {
9666
+ scope: late ? state.scope : {
9566
9667
  status: "pending",
9567
9668
  actor: null,
9568
9669
  at: null
@@ -9577,7 +9678,7 @@ function createClassificationProposal(storage) {
9577
9678
  confirmedAnswersRevision: null
9578
9679
  } : {}
9579
9680
  },
9580
- readingComplete: works.filter((work) => work.access !== "drop").every((work) => work.totalBatches > 0 && work.completedBatches === work.totalBatches),
9681
+ readingComplete: !pending.length && works.filter((work) => work.access !== "drop").every((work) => work.totalBatches > 0 && work.completedBatches === work.totalBatches),
9581
9682
  phase: "processing"
9582
9683
  });
9583
9684
  };
@@ -10511,12 +10612,45 @@ async function perform(context, item, connection, signal) {
10511
10612
  if (!item.receipt) return observe(item, await connection.reserve(item.operationKey, item.reservation, signal), connection, signal, auto);
10512
10613
  const receipt = await connection.status(item.receipt.receiptId, signal);
10513
10614
  const observed = await observe(item, receipt, connection, signal, auto);
10514
- if (receipt.state !== "awaiting_upload") return observed;
10615
+ if (receipt.state !== "awaiting_upload") return withoutUpload(observed);
10515
10616
  if (!receipt.upload) throw new ApplicationError("INVALID_STATE");
10617
+ if (item.uploadedReceiptId === receipt.receiptId) {
10618
+ await guard();
10619
+ try {
10620
+ return withoutUpload(await observe(item, await connection.complete(receipt.receiptId, signal), connection, signal, auto));
10621
+ } catch (error) {
10622
+ if (!(error instanceof JourneyRemoteError) || error.status === 401 || [
10623
+ "settings_changed",
10624
+ "target_conflict",
10625
+ "work_removed"
10626
+ ].includes(error.code)) throw error;
10627
+ return withoutUpload(item);
10628
+ }
10629
+ }
10630
+ const current = await connection.resolve(item.reservation.work, signal);
10631
+ if (current.scopeVersion !== item.reservation.expectedScopeVersion || current.accessVersion !== item.reservation.expectedAccessVersion) return renewed(item, null);
10516
10632
  await guard();
10517
10633
  await context.upload(item.artifact, receipt.upload.uploadUrl, signal);
10518
10634
  await guard();
10519
- return observe(item, await connection.complete(receipt.receiptId, signal), connection, signal, auto);
10635
+ try {
10636
+ return observe(item, await connection.complete(receipt.receiptId, signal), connection, signal, auto);
10637
+ } catch (error) {
10638
+ if (error instanceof Object) uploaded.set(error, {
10639
+ ...item,
10640
+ uploadedReceiptId: receipt.receiptId
10641
+ });
10642
+ throw error;
10643
+ }
10644
+ }
10645
+ const uploaded = /* @__PURE__ */ new WeakMap();
10646
+ /** The item to save for a failed step whose bytes were already uploaded, if any. */
10647
+ function uploadedItem(error) {
10648
+ return error instanceof Object ? uploaded.get(error) : void 0;
10649
+ }
10650
+ function withoutUpload(item) {
10651
+ if (item.uploadedReceiptId === void 0) return item;
10652
+ const { uploadedReceiptId: _, ...rest } = item;
10653
+ return rest;
10520
10654
  }
10521
10655
  /** A definite stale refusal can renew the intent; unknown outcomes always retry the same key. */
10522
10656
  async function stepItem(context, item, connection, signal) {
@@ -10546,31 +10680,39 @@ async function stepItem(context, item, connection, signal) {
10546
10680
  status: "blocked",
10547
10681
  error: "WORK_REMOVED"
10548
10682
  };
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
- };
10683
+ return renewed(item, "SETTINGS_CHANGED");
10559
10684
  }
10560
10685
  }
10686
+ /** A fresh intent for the same Work: the old receipt is kept as history and resolve runs again. */
10687
+ function renewed(item, error) {
10688
+ return {
10689
+ ...withoutUpload(item),
10690
+ operationKey: randomUUID(),
10691
+ reservation: null,
10692
+ resolution: null,
10693
+ previousReceipts: item.receipt ? [...item.previousReceipts, item.receipt] : item.previousReceipts,
10694
+ receipt: null,
10695
+ status: "ready",
10696
+ error
10697
+ };
10698
+ }
10561
10699
  const confirmedSettings = (item) => item.settings !== null && item.settingsConfirmed;
10562
10700
  /**
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.
10701
+ * The Works one advance steps together. Upload work comes before assessment checks, smallest
10702
+ * ZIP first. Completing
10703
+ * an upload advances the owner's scope version, which every resolution and reservation carries,
10704
+ * so one Work's resolve-to-complete steps never overlap another's: uploads step one Work at a
10705
+ * time (ADR 0214). Assessment checks change nothing and step up to three together; a confirmed
10706
+ * linked Settings change alters access versions, so it always steps alone.
10566
10707
  */
10567
10708
  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];
10709
+ 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);
10710
+ if (uploading) return [uploading];
10711
+ const assessing = items.filter((item) => item.status === "assessing");
10712
+ const first = assessing[0];
10571
10713
  if (!first) return [];
10572
10714
  if (confirmedSettings(first)) return [first];
10573
- return pool.filter((item) => !confirmedSettings(item)).slice(0, 3);
10715
+ return assessing.filter((item) => !confirmedSettings(item)).slice(0, 3);
10574
10716
  }
10575
10717
  function isCode(error, code) {
10576
10718
  return error instanceof ApplicationError && error.code === code;
@@ -10681,6 +10823,8 @@ async function advanceItems(host, signal) {
10681
10823
  continue;
10682
10824
  }
10683
10825
  const cause = result.reason;
10826
+ const progressed = uploadedItem(cause);
10827
+ if (progressed) next.set(item.workId, progressed);
10684
10828
  if (cause instanceof JourneyRemoteError && cause.status === 401 || isCode(cause, "ACCOUNT_SIGNED_OUT")) signIn = true;
10685
10829
  else if (isCode(cause, "IDENTITY_REJECTED")) changed = true;
10686
10830
  else if (isInterrupted(cause)) {
@@ -10717,7 +10861,7 @@ async function advanceItems(host, signal) {
10717
10861
  items
10718
10862
  }));
10719
10863
  } catch (error) {
10720
- if (!signal.aborted && (isInterrupted(error) || error instanceof Error && isInterrupted(error.cause))) {
10864
+ if (!signal.aborted && (isInterrupted(error) || error instanceof Error && isInterrupted(error.cause) || isCode(error, "IDENTITY_UNAVAILABLE"))) {
10721
10865
  host.failure("SEND_INTERRUPTED", error);
10722
10866
  return send.error === "SEND_INTERRUPTED" ? host.applied() : host.save({
10723
10867
  ...send,
@@ -10837,7 +10981,11 @@ async function advanceSend(dependencies, address, signal) {
10837
10981
  assertScope(current.journey, current.send);
10838
10982
  },
10839
10983
  bindingId: binding?.subject === send.subject && !retired ? binding.bundleRecordId : void 0,
10840
- upload: (artifact, uploadUrl, uploadSignal) => dependencies.artifacts.upload(snapshot.journey.journeyId, artifact, uploadUrl, uploadSignal)
10984
+ upload: async (artifact, uploadUrl, uploadSignal) => {
10985
+ const started = performance.now();
10986
+ await dependencies.artifacts.upload(snapshot.journey.journeyId, artifact, uploadUrl, uploadSignal);
10987
+ dependencies.uploaded?.(artifact.workId, artifact.byteLength, Math.round(performance.now() - started));
10988
+ }
10841
10989
  };
10842
10990
  },
10843
10991
  finish: async (connection, finishSignal) => {
@@ -12481,6 +12629,20 @@ function createJourneySender(dependencies) {
12481
12629
  if (result.status !== "stale") running.get(runKey(input))?.stop.abort();
12482
12630
  return result;
12483
12631
  },
12632
+ /** BL-201: Cancel sign-in returns a confirmed Send that uploaded nothing to Ready to send. */
12633
+ async withdraw(raw) {
12634
+ const input = sendActionSchema.parse(raw);
12635
+ const snapshot = await load(input);
12636
+ if (!matchesAction(snapshot, input)) return saveSend(dependencies, snapshot, snapshot.send, input);
12637
+ if (snapshot.send.status !== "sign-in" || snapshot.send.items.some((item) => item.receipt)) throw new ApplicationError("INVALID_TRANSITION");
12638
+ const result = await saveSend(dependencies, snapshot, {
12639
+ ...snapshot.send,
12640
+ status: "prepared",
12641
+ confirmedAt: null
12642
+ }, input);
12643
+ if (result.status !== "stale") running.get(runKey(input))?.stop.abort();
12644
+ return result;
12645
+ },
12484
12646
  async resume(raw) {
12485
12647
  const input = sendActionSchema.parse(raw);
12486
12648
  const snapshot = await load(input);
@@ -12512,6 +12674,10 @@ function createJourneySender(dependencies) {
12512
12674
  }
12513
12675
  //#endregion
12514
12676
  //#region apps/cli/src/account/journey/auth.ts
12677
+ /** An access token this close to expiry is renewed first, so a step never starts on it. */
12678
+ const renewBeforeMs = 12e4;
12679
+ const renewTimeoutMs = 3e4;
12680
+ const renewals = /* @__PURE__ */ new WeakMap();
12515
12681
  /** Reuses the existing device sign-in credential; a token is never returned to the panel or Agent. */
12516
12682
  function createJourneySendAuth(account, requestFetch) {
12517
12683
  /**
@@ -12528,31 +12694,87 @@ function createJourneySendAuth(account, requestFetch) {
12528
12694
  return state;
12529
12695
  });
12530
12696
  }
12697
+ /** A refused access token with a refresh token is renewed on the next use instead (BL-210). */
12698
+ async function refuse(accessToken) {
12699
+ if (!(await account.credentials.update((value) => {
12700
+ if (value.credential?.accessToken === accessToken && value.credential.refreshToken) value.credential = {
12701
+ ...value.credential,
12702
+ expiresAt: account.now()
12703
+ };
12704
+ return value;
12705
+ })).credential?.refreshToken) await expire(accessToken);
12706
+ }
12707
+ function renew(credential) {
12708
+ const running = renewals.get(account);
12709
+ if (running) return running;
12710
+ const renewing = (async () => {
12711
+ try {
12712
+ const token = await account.identity.refresh(credential.refreshToken, AbortSignal.timeout(renewTimeoutMs));
12713
+ if (!token) {
12714
+ await expire(credential.accessToken);
12715
+ return null;
12716
+ }
12717
+ return (await account.credentials.update((value) => {
12718
+ if (value.credential?.refreshToken === credential.refreshToken) value.credential = {
12719
+ ...value.credential,
12720
+ accessToken: token.accessToken,
12721
+ refreshToken: token.refreshToken ?? credential.refreshToken,
12722
+ scope: token.scope,
12723
+ issuedAt: account.now(),
12724
+ expiresAt: token.expiresAt
12725
+ };
12726
+ return value;
12727
+ })).credential;
12728
+ } finally {
12729
+ renewals.delete(account);
12730
+ }
12731
+ })();
12732
+ renewals.set(account, renewing);
12733
+ return renewing;
12734
+ }
12531
12735
  async function current() {
12532
12736
  const credential = (await account.credentials.update((value) => value)).credential;
12533
12737
  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())) {
12738
+ const left = Date.parse(credential.expiresAt) - Date.parse(account.now());
12739
+ if (Number.isFinite(left) && left > renewBeforeMs) return credential;
12740
+ const { refreshToken } = credential;
12741
+ if (!refreshToken) {
12742
+ if (Number.isFinite(left) && left > 0) return credential;
12535
12743
  await expire(credential.accessToken);
12536
12744
  return null;
12537
12745
  }
12538
- return credential;
12746
+ try {
12747
+ return await renew({
12748
+ ...credential,
12749
+ refreshToken
12750
+ });
12751
+ } catch (error) {
12752
+ if (Number.isFinite(left) && left > 0) return credential;
12753
+ throw error;
12754
+ }
12539
12755
  }
12540
12756
  const fetchOwner = requestFetch ?? fetch;
12541
12757
  const ownerFetch = async (input, init) => {
12542
12758
  const response = await fetchOwner(input, init);
12543
12759
  if (response.status === 401) {
12544
12760
  const header = new Headers(init?.headers).get("authorization");
12545
- if (header?.startsWith("Bearer ")) await expire(header.slice(7));
12761
+ if (header?.startsWith("Bearer ")) await refuse(header.slice(7));
12546
12762
  }
12547
12763
  return response;
12548
12764
  };
12549
12765
  return { async connect(signal) {
12550
- const credential = await current();
12766
+ let credential = await current();
12551
12767
  if (!credential) return null;
12552
- const verified = await account.identity.readIdentity(credential.accessToken, signal);
12768
+ let verified = await account.identity.readIdentity(credential.accessToken, signal);
12553
12769
  if (!verified) {
12554
- await expire(credential.accessToken);
12555
- return null;
12770
+ await refuse(credential.accessToken);
12771
+ credential = await current();
12772
+ if (!credential) return null;
12773
+ verified = await account.identity.readIdentity(credential.accessToken, signal);
12774
+ if (!verified) {
12775
+ await expire(credential.accessToken);
12776
+ return null;
12777
+ }
12556
12778
  }
12557
12779
  if (verified.identityId !== credential.identityId) throw new ApplicationError("IDENTITY_REJECTED");
12558
12780
  return createJourneyOwnerClient({
@@ -13097,6 +13319,7 @@ const deviceCodeResponse = z.object({
13097
13319
  });
13098
13320
  const tokenResponse = z.object({
13099
13321
  access_token: z.string().min(1).max(4096),
13322
+ refresh_token: z.string().min(1).max(4096).optional(),
13100
13323
  token_type: z.string(),
13101
13324
  expires_in: z.number().int().positive(),
13102
13325
  scope: deviceProfileScopeSchema
@@ -13108,13 +13331,24 @@ const userInfoResponse = z.object({
13108
13331
  picture: z.unknown().optional(),
13109
13332
  profile: z.string()
13110
13333
  });
13111
- function unavailable(cause) {
13112
- const error = new ApplicationError("IDENTITY_UNAVAILABLE");
13334
+ /**
13335
+ * `detail` names the endpoint and what failed (HTTP status, network error name, schema paths),
13336
+ * never a response value, so the authorization log can say why without exposing a token.
13337
+ */
13338
+ function unavailable(detail, cause) {
13339
+ const error = Object.assign(new ApplicationError("IDENTITY_UNAVAILABLE"), { detail });
13113
13340
  if (cause !== void 0) error.cause = cause;
13114
13341
  return error;
13115
13342
  }
13116
- async function readJson(response) {
13117
- if (!response.body) throw unavailable();
13343
+ const issuePaths = (error) => error.issues.map((issue) => issue.path.join(".") || "(root)").join(",");
13344
+ function networkDetail(error) {
13345
+ if (!(error instanceof Error)) return "Error";
13346
+ const cause = error.cause;
13347
+ const code = typeof cause === "object" && cause !== null && "code" in cause ? cause.code : null;
13348
+ return typeof code === "string" ? `${error.name} ${code}` : error.name;
13349
+ }
13350
+ async function readJson(response, where) {
13351
+ if (!response.body) throw unavailable(`${where} empty body`);
13118
13352
  const reader = response.body.getReader();
13119
13353
  const chunks = [];
13120
13354
  let bytes = 0;
@@ -13126,7 +13360,7 @@ async function readJson(response) {
13126
13360
  bytes += next.value.byteLength;
13127
13361
  if (bytes > maximumResponseBytes) {
13128
13362
  await reader.cancel().catch(() => void 0);
13129
- throw unavailable();
13363
+ throw unavailable(`${where} body too large`);
13130
13364
  }
13131
13365
  chunks.push(next.value);
13132
13366
  }
@@ -13136,7 +13370,7 @@ async function readJson(response) {
13136
13370
  try {
13137
13371
  return JSON.parse(Buffer.concat(chunks, bytes).toString("utf8"));
13138
13372
  } catch (error) {
13139
- throw unavailable(error);
13373
+ throw unavailable(`${where} invalid JSON`, error);
13140
13374
  }
13141
13375
  }
13142
13376
  function verificationUrl(value, identity) {
@@ -13144,9 +13378,9 @@ function verificationUrl(value, identity) {
13144
13378
  try {
13145
13379
  url = new URL(value);
13146
13380
  } catch (error) {
13147
- throw unavailable(error);
13381
+ throw unavailable("device-code verification URL invalid", error);
13148
13382
  }
13149
- if (!["http:", "https:"].includes(url.protocol) || url.origin !== identity.origin || url.username !== "" || url.password !== "") throw unavailable();
13383
+ if (!["http:", "https:"].includes(url.protocol) || url.origin !== identity.origin || url.username !== "" || url.password !== "") throw unavailable("device-code verification URL outside Identity");
13150
13384
  return url;
13151
13385
  }
13152
13386
  /**
@@ -13157,7 +13391,7 @@ function createIdentityDeviceClient(options) {
13157
13391
  const requestFetch = options.fetch ?? fetch;
13158
13392
  const now = options.now ?? Date.now;
13159
13393
  const base = new URL(options.identityUrl);
13160
- if (!["http:", "https:"].includes(base.protocol)) throw unavailable();
13394
+ if (!["http:", "https:"].includes(base.protocol)) throw unavailable("identity URL protocol");
13161
13395
  async function send(path, init, signal) {
13162
13396
  try {
13163
13397
  return await requestFetch(new URL(path, base), {
@@ -13167,9 +13401,18 @@ function createIdentityDeviceClient(options) {
13167
13401
  });
13168
13402
  } catch (error) {
13169
13403
  if (signal.aborted) throw error;
13170
- throw unavailable(error);
13404
+ throw unavailable(`${path} ${networkDetail(error)}`, error);
13171
13405
  }
13172
13406
  }
13407
+ function issued(token) {
13408
+ return {
13409
+ kind: "issued",
13410
+ accessToken: token.access_token,
13411
+ ...token.refresh_token ? { refreshToken: token.refresh_token } : {},
13412
+ expiresAt: new Date(now() + token.expires_in * 1e3).toISOString(),
13413
+ scope: token.scope
13414
+ };
13415
+ }
13173
13416
  function form(fields) {
13174
13417
  return {
13175
13418
  method: "POST",
@@ -13182,14 +13425,18 @@ function createIdentityDeviceClient(options) {
13182
13425
  }
13183
13426
  return {
13184
13427
  async requestDeviceCode(signal) {
13185
- const response = await send("/api/auth/device/code", form({
13428
+ let response = await send("/api/auth/device/code", form({
13429
+ client_id: options.clientId,
13430
+ scope: "openid profile offline_access"
13431
+ }), signal);
13432
+ if (response.status === 400 && errorResponse.safeParse(await readJson(response, "device-code")).data?.error === "invalid_scope") response = await send("/api/auth/device/code", form({
13186
13433
  client_id: options.clientId,
13187
13434
  scope: "openid profile"
13188
13435
  }), signal);
13189
13436
  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);
13437
+ if (response.status !== 200) throw unavailable(`device-code HTTP ${response.status}`);
13438
+ const parsed = deviceCodeResponse.safeParse(await readJson(response, "device-code"));
13439
+ if (!parsed.success) throw unavailable(`device-code schema ${issuePaths(parsed.error)}`, parsed.error);
13193
13440
  const verification = verificationUrl(parsed.data.verification_uri, base);
13194
13441
  const complete = verificationUrl(parsed.data.verification_uri_complete, base);
13195
13442
  return {
@@ -13208,20 +13455,16 @@ function createIdentityDeviceClient(options) {
13208
13455
  grant_type: deviceCodeGrantType
13209
13456
  }), signal);
13210
13457
  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
- };
13458
+ const parsed = tokenResponse.safeParse(await readJson(response, "token"));
13459
+ if (!parsed.success) throw unavailable(`token schema ${issuePaths(parsed.error)}`, parsed.error);
13460
+ if (parsed.data.token_type.toLowerCase() !== "bearer") throw unavailable("token type not bearer");
13461
+ return issued(parsed.data);
13219
13462
  }
13220
13463
  if (response.status !== 400) {
13221
13464
  if (response.status === 401) throw new ApplicationError("IDENTITY_REJECTED");
13222
- throw unavailable();
13465
+ throw unavailable(`token HTTP ${response.status}`);
13223
13466
  }
13224
- const parsed = errorResponse.safeParse(await readJson(response));
13467
+ const parsed = errorResponse.safeParse(await readJson(response, "token"));
13225
13468
  const code = parsed.success ? parsed.data.error : "";
13226
13469
  if (code === "authorization_pending") return { kind: "pending" };
13227
13470
  if (code === "slow_down") return { kind: "slow_down" };
@@ -13229,6 +13472,19 @@ function createIdentityDeviceClient(options) {
13229
13472
  if (code === "access_denied") return { kind: "denied" };
13230
13473
  throw new ApplicationError("IDENTITY_REJECTED");
13231
13474
  },
13475
+ async refresh(refreshToken, signal) {
13476
+ const response = await send("/api/auth/oauth2/token", form({
13477
+ client_id: options.clientId,
13478
+ grant_type: "refresh_token",
13479
+ refresh_token: refreshToken
13480
+ }), signal);
13481
+ if (response.status === 400 || response.status === 401) return null;
13482
+ if (response.status !== 200) throw unavailable(`refresh HTTP ${response.status}`);
13483
+ const parsed = tokenResponse.safeParse(await readJson(response, "refresh"));
13484
+ if (!parsed.success) throw unavailable(`refresh schema ${issuePaths(parsed.error)}`, parsed.error);
13485
+ if (parsed.data.token_type.toLowerCase() !== "bearer") throw unavailable("token type not bearer");
13486
+ return issued(parsed.data);
13487
+ },
13232
13488
  async readIdentity(accessToken, signal) {
13233
13489
  const response = await send("/api/auth/oauth2/userinfo", {
13234
13490
  method: "GET",
@@ -13238,17 +13494,17 @@ function createIdentityDeviceClient(options) {
13238
13494
  }
13239
13495
  }, signal);
13240
13496
  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);
13497
+ if (response.status !== 200) throw unavailable(`userinfo HTTP ${response.status}`);
13498
+ const parsed = userInfoResponse.safeParse(await readJson(response, "userinfo"));
13499
+ if (!parsed.success) throw unavailable(`userinfo schema ${issuePaths(parsed.error)}`, parsed.error);
13244
13500
  const profile = accountProfileSchema.safeParse({
13245
13501
  identityId: parsed.data.sub,
13246
13502
  displayName: parsed.data.name,
13247
13503
  avatarUrl: readAccountAvatarUrl(parsed.data.picture),
13248
13504
  profileUrl: parsed.data.profile
13249
13505
  });
13250
- if (!profile.success) throw unavailable(profile.error);
13251
- if (!profileUrlMatchesIdentity(profile.data.profileUrl, options.identityUrl)) throw unavailable();
13506
+ if (!profile.success) throw unavailable(`userinfo profile ${issuePaths(profile.error)}`, profile.error);
13507
+ if (!profileUrlMatchesIdentity(profile.data.profileUrl, options.identityUrl)) throw unavailable("userinfo profile URL outside Identity");
13252
13508
  return profile.data;
13253
13509
  }
13254
13510
  };
@@ -13367,6 +13623,8 @@ function errorDetail(error) {
13367
13623
  if (typeof code === "number" && Number.isFinite(code)) detail.code = code;
13368
13624
  const syscall = data(value, "syscall");
13369
13625
  if (typeof syscall === "string") detail.syscall = text(syscall);
13626
+ const reason = data(value, "detail");
13627
+ if (typeof reason === "string") detail.detail = text(reason);
13370
13628
  const cause = data(value, "cause");
13371
13629
  if (cause !== void 0) detail.cause = visit(cause, depth + 1);
13372
13630
  const errors = data(value, "errors");
@@ -13849,6 +14107,61 @@ function createConfiguredPreviewPort(workerUrl, environment) {
13849
14107
  }) };
13850
14108
  }
13851
14109
  //#endregion
14110
+ //#region apps/cli/src/inspection/index.ts
14111
+ /**
14112
+ * Runs each inspection scan in its own Worker so static detection never holds the host's event
14113
+ * loop, which also serves requests and store accesses (BL-199).
14114
+ */
14115
+ function createInspectionPort(workerUrl, environment, onAnomaly) {
14116
+ return { async *scan(request, signal, onFailure) {
14117
+ const worker = new Worker(workerUrl, { workerData: {
14118
+ request,
14119
+ environment
14120
+ } });
14121
+ const events = [];
14122
+ let wake;
14123
+ const push = (event) => {
14124
+ events.push(event);
14125
+ wake?.();
14126
+ };
14127
+ const fail = (cause) => push({ error: new Error("INSPECTION_WORKER_FAILED", { cause }) });
14128
+ worker.on("message", (message) => {
14129
+ const parsed = workerMessageSchema.safeParse(message);
14130
+ if (!parsed.success) return fail(parsed.error);
14131
+ const value = parsed.data;
14132
+ try {
14133
+ if (value.kind === "batch") push({ batch: validateInspectionBatch(value.batch) });
14134
+ else if (value.kind === "done") push({ done: true });
14135
+ else if (value.kind === "anomaly") onAnomaly?.(value.event);
14136
+ else onFailure?.(value.stage, Object.assign(new Error(value.message), value.code ? { code: value.code } : {}));
14137
+ } catch (error) {
14138
+ fail(error);
14139
+ }
14140
+ });
14141
+ worker.on("error", fail);
14142
+ worker.on("exit", (code) => fail(/* @__PURE__ */ new Error(`Inspection Worker exited: code=${code}`)));
14143
+ const abort = () => worker.postMessage({ kind: "abort" });
14144
+ if (signal.aborted) abort();
14145
+ else signal.addEventListener("abort", abort, { once: true });
14146
+ try {
14147
+ for (;;) {
14148
+ const event = events.shift();
14149
+ if (!event) {
14150
+ await new Promise((resolve) => wake = resolve);
14151
+ wake = void 0;
14152
+ continue;
14153
+ }
14154
+ if ("done" in event) return;
14155
+ if ("error" in event) throw event.error;
14156
+ yield event.batch;
14157
+ }
14158
+ } finally {
14159
+ signal.removeEventListener("abort", abort);
14160
+ await worker.terminate();
14161
+ }
14162
+ } };
14163
+ }
14164
+ //#endregion
13852
14165
  //#region apps/cli/src/bundle-directory.ts
13853
14166
  /** The platform cache folder; Windows has none, so its default library is the launch directory. */
13854
14167
  function cacheRoot(options) {
@@ -14080,7 +14393,14 @@ async function compose(options, callbacks, bundles) {
14080
14393
  artifacts: createJourneySendArtifacts(journeyStorage),
14081
14394
  auth: createJourneySendAuth(accountPorts),
14082
14395
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
14083
- failure: (code, cause) => diagnostics.onFailure(code, cause)
14396
+ failure: (code, cause) => diagnostics.onFailure(code, cause),
14397
+ uploaded: (workId, bytes, elapsedMs) => diagnostics.onActivity({
14398
+ event: "journey.send_upload_timing",
14399
+ at: (/* @__PURE__ */ new Date()).toISOString(),
14400
+ operationId: workId,
14401
+ phases: { upload: elapsedMs },
14402
+ volumes: { bytes }
14403
+ })
14084
14404
  }),
14085
14405
  manualUploader: createManualUploader({
14086
14406
  storage: store.manualUpload,
@@ -14152,10 +14472,7 @@ async function compose(options, callbacks, bundles) {
14152
14472
  onCreated: (path) => process.stderr.write(`Temporary file may contain unredacted originals: ${path}\n`),
14153
14473
  onRetained: (path) => process.stderr.write(`Retained temporary file may contain unredacted originals: ${path}\n`)
14154
14474
  }),
14155
- inspection: createInspectionProcessor({
14156
- ...processing,
14157
- onAnomaly: producers.onInspectionAnomaly
14158
- }),
14475
+ inspection: createInspectionPort(bundles?.url("inspection-worker") ?? new URL("./inspection-worker.mjs", import.meta.url), processing.environment, producers.onInspectionAnomaly),
14159
14476
  systemResources,
14160
14477
  onDiagnostic: diagnostics.onDiagnostic,
14161
14478
  onPackagingActivity: diagnostics.onActivity,
@@ -14249,6 +14566,46 @@ async function compose(options, callbacks, bundles) {
14249
14566
  }
14250
14567
  }
14251
14568
  //#endregion
14569
+ //#region apps/cli/src/agent-presence.ts
14570
+ const journey = z.object({ journeyId: z.uuid() }).loose();
14571
+ const presence = z.object({ at: z.iso.datetime() }).strict();
14572
+ const written = /* @__PURE__ */ new Map();
14573
+ /** At most one presence write per journey this often, from one CLI process. */
14574
+ const presenceIntervalMs = 5e3;
14575
+ const presencePath = (stateDir, journeyId) => join(stateDir, "presence", `${journeyId}.json`);
14576
+ /**
14577
+ * Records that the agent just called the CLI for this journey (E16). It lives beside the journey,
14578
+ * not in it, so a heartbeat never moves the journey revision; a failed write is ignored.
14579
+ */
14580
+ async function markAgentPresence(stateDir, value) {
14581
+ const parsed = journey.safeParse(value);
14582
+ if (!parsed.success) return;
14583
+ const { journeyId } = parsed.data;
14584
+ const now = Date.now();
14585
+ if (now - (written.get(journeyId) ?? 0) < presenceIntervalMs) return;
14586
+ written.set(journeyId, now);
14587
+ try {
14588
+ const path = presencePath(stateDir, journeyId);
14589
+ await mkdir(join(stateDir, "presence"), {
14590
+ recursive: true,
14591
+ mode: 448
14592
+ });
14593
+ const temporary = `${path}.${now}.tmp`;
14594
+ await writeFile(temporary, JSON.stringify({ at: new Date(now).toISOString() }), { mode: 384 });
14595
+ await rename(temporary, path);
14596
+ } catch {
14597
+ written.delete(journeyId);
14598
+ }
14599
+ }
14600
+ /** When the agent last called the CLI for this journey, or null when it never did. */
14601
+ async function readAgentPresence(stateDir, journeyId) {
14602
+ try {
14603
+ return presence.parse(JSON.parse(await readFile(presencePath(stateDir, journeyId), "utf8")));
14604
+ } catch {
14605
+ return null;
14606
+ }
14607
+ }
14608
+ //#endregion
14252
14609
  //#region apps/cli/src/commands.ts
14253
14610
  const sendAddress = z.object({
14254
14611
  journeyId: z.uuid(),
@@ -14274,6 +14631,7 @@ const delivery = z.object({
14274
14631
  /** `signal` carries the process's SIGINT/SIGTERM to long procedures such as journey.prepare. */
14275
14632
  async function invoke(runtime, command, value, signal) {
14276
14633
  const app = runtime.application;
14634
+ markAgentPresence(runtime.stateDir, value);
14277
14635
  switch (command) {
14278
14636
  case "send.get": return app.journeySend?.get(sendAddress.partial({ sendId: true }).parse(value));
14279
14637
  case "send.prepare": return app.journeySend?.prepare(sendPrepareSchema.parse(value));
@@ -14508,4 +14866,4 @@ async function cancelOperation(runtime, id) {
14508
14866
  return readOperation(runtime, id);
14509
14867
  }
14510
14868
  //#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 };
14869
+ 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 };