@gitroll/tacitt-cli 0.6.11 → 0.6.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +4 -0
  2. package/bin/INDEX.md +6 -4
  3. package/bin/tacitt.cjs +1 -0
  4. package/dist/{agent-setup-DdfF3zch.mjs → agent-setup-Doy_AnPY.mjs} +2 -2
  5. package/dist/agent-setup-worker.mjs +5 -5
  6. package/dist/cli.mjs +32 -32
  7. package/dist/codex-title-worker.mjs +2 -2
  8. package/dist/configured-preview-worker.mjs +5 -5
  9. package/dist/configured-session-worker.mjs +23 -25
  10. package/dist/{frame-Bq2zRh-i.mjs → frame-BPbypzmt.mjs} +1 -1
  11. package/dist/{heap-CV8Eca8V.mjs → heap-DCS_WXIA.mjs} +264 -23
  12. package/dist/inspection-worker.mjs +4 -4
  13. package/dist/{job-BKsifhyv.mjs → job--zxwgaPy.mjs} +155 -41
  14. package/dist/{locate-Cx52Zqxv.mjs → locate-PLEyLYW-.mjs} +292 -236
  15. package/dist/{model-C1UXkHh9.mjs → model-CyXvsWLz.mjs} +1 -1
  16. package/dist/operation-executor.mjs +3 -3
  17. package/dist/preview-worker.mjs +1 -1
  18. package/dist/privacy-validator.mjs +28 -8
  19. package/dist/{query-AMZerUh8.mjs → query-BC7MIaWc.mjs} +1914 -759
  20. package/dist/{run-CaPktPhH.mjs → run-Dod7Vykp.mjs} +6 -6
  21. package/dist/session-process.mjs +20 -0
  22. package/dist/{src-DlVbGB32.mjs → src-CP-K2PBv.mjs} +2100 -1649
  23. package/dist/{src-D1iONla3.mjs → src-CttNuxOn.mjs} +925 -630
  24. package/dist/{src-wZx_v0Ih.mjs → src-dL6y2mcu.mjs} +180 -30
  25. package/dist/store-database-worker.mjs +1 -1
  26. package/dist/web/assets/index-BgDdPL1c.js +172 -0
  27. package/dist/web/index.html +2 -1
  28. package/package.json +2 -1
  29. package/skills/INDEX.md +6 -4
  30. package/skills/tacitt/INDEX.md +7 -6
  31. package/skills/tacitt/SKILL.md +7 -7
  32. package/skills/tacitt/references/INDEX.md +14 -11
  33. package/skills/tacitt/references/b-method.md +6 -4
  34. package/skills/tacitt/references/bootstrap.md +18 -4
  35. package/skills/tacitt/references/classification.md +1 -1
  36. package/skills/tacitt/references/commands.md +34 -10
  37. package/skills/tacitt/references/journey.md +7 -7
  38. package/skills/tacitt/references/parallel-review.md +31 -12
  39. package/skills/tacitt/references/remote-workers.md +6 -2
  40. package/skills/tacitt/references/send.md +7 -3
  41. package/skills/tacitt/scripts/INDEX.md +40 -37
  42. package/skills/tacitt/scripts/answer.mjs +31 -2
  43. package/skills/tacitt/scripts/describe.mjs +3 -2
  44. package/skills/tacitt/scripts/journey-card-consolidation.mjs +1 -1
  45. package/skills/tacitt/scripts/journey-classification-tasks.mjs +28 -8
  46. package/skills/tacitt/scripts/journey-classification.mjs +1 -0
  47. package/skills/tacitt/scripts/journey-panel-host.d.mts +2 -0
  48. package/skills/tacitt/scripts/journey-panel-host.mjs +25 -2
  49. package/skills/tacitt/scripts/journey-scan-timing.d.mts +4 -0
  50. package/skills/tacitt/scripts/journey-scan-timing.mjs +15 -2
  51. package/skills/tacitt/scripts/journey-state.mjs +11 -0
  52. package/skills/tacitt/scripts/journey-tasks.mjs +12 -5
  53. package/skills/tacitt/scripts/journey.mjs +11 -3
  54. package/skills/tacitt/scripts/privacy-validator.mjs +112 -40
  55. package/skills/tacitt/scripts/review.d.mts +1 -0
  56. package/skills/tacitt/scripts/review.mjs +680 -119
  57. package/dist/web/assets/index-CYl7eDUu.js +0 -172
@@ -1,11 +1,11 @@
1
- import { $ as removeNamedTemporarySpools, B as parallelismLimits, C as readOriginalSearchFields, F as SessionWorkerError, G as processJourneyMetadataFields, H as privateJourneyDirectory, I as sessionWorkerFailure, K as staticMaskCategory, O as captureLogicalJourneySources, P as createSharedSessionPool, Q as createTemporary, R as defaultParallelism, T as createCollectionArtifacts, U as readLogicalJourneyFinalMasks, W as readConfiguredOutputInfo, Z as decodeZstd, _ as processJourneyMetadata, a as scanReviewFields, at as restrictToOwner, c as readSessionScanRecords, ct as windowsAclCodes, et as createCleanup, g as readCapturedOriginalRows, i as buildWorkScanBatches, it as openPrivateFile, j as runConfiguredSessionJob, k as capturedJourneyScope, l as scanPreparedSession, m as openConfiguredUpload, n as verifyPreparedSessionSources, o as createWorkPieces, ot as windowsOwnerOnly, p as configuredSessionUploads, q as readWorkspacePair, r as prepareSessionScans, rt as makePrivateDirectory, s as readSessionScanFields, st as isWindowsAclFailure, t as preparedScanConfiguration, v as readStaticSessionMasks, w as readConfiguredSearchFields, y as staticSessionJobs, z as gitSeedsProvider } from "./src-DlVbGB32.mjs";
2
- import { A as createExecutionPort, C as saveRemoteState, D as createPackagingProgressPort, E as exportDelivery, F as loadDeliveryResults, L as inBackgroundStoreLane, M as withStateMutex, N as createDeliveryService, O as createStore, P as preflight, R as isBackgroundStatusPoll, S as readRemoteState, T as collectRemoteWorkers, _ as staticBaseProduction, a as writeJourneyOutput, b as workApproval, c as mergeHideIndexTokens, f as writeJourneyBase, g as staticBaseInputs, h as staticSessions, l as packRenderHideIndex, m as saveStaticSession, o as createHideIndexTokens, p as readStaticResult, s as hideIndexTexts, t as journeyOutputJob, u as journeyBaseJob, v as readSelectedCaptures, w as journeyDirectory, x as isRemoteImport, y as prepareSourcePlan, z as retrySharingViolation } from "./job-BKsifhyv.mjs";
3
- import { $t as profileHasChoices, D as bLiteralTerms, Gt as filterInventoryByPeriod, Kt as sessionPeriodSchema, Mt as privacyTermKey, Nn as createStaticProcessor, On as currentDetectorVersion, Qt as passportAddressSchema, Vn as entityDictionaryVersion, Vt as termTypeInfo, Xt as mergeOwnerProfile, Yn as frameRecords, Zt as ownerProfileSchema, an as safeField, br as processingSite, cn as reviewFamilies, dn as observeSource, dr as admitCustomTerm, fr as admitGitName, gr as ProcessingError, in as safeCode, nn as ApplicationError, rn as UnfinishedJourneyError, sn as familySelected, u as textDigest, ur as createEntityMatcher, xr as siteFields } from "./locate-Cx52Zqxv.mjs";
4
- import { a as mainTextChunks, c as previewInWorker, i as nearestSortingRepository, l as createNativeAdapters, n as createProjectEvidenceCollector, o as mainTextFields, r as sortingRepositoryId, s as textChunks, t as createHarnesses, u as createGitNameSeedProvider } from "./src-wZx_v0Ih.mjs";
5
- import { $r as sendReplaceResultSchema, An as scanGitSeedsResource, At as obsoleteJourney, B as createApplication, Bn as journeySearchResultSchema, Bt as classificationPartition, Ct as sendActionSchema, Dn as scanPreparationDigest, Dt as sendStartSchema, Et as sendRequestSchema, F as readAccountAvatarUrl, H as inspectionStartSchema, Hn as searchScopeDigest, Ht as retireScanGeneration, I as storedCredentialSchema, Ir as assertJourneyPrivacyReady, Jr as agentSourceSchema, K as inspectionQuerySchema, Lr as privacyDecisionDigest, M as deviceAuthorizationSchema, Mn as scanExclusionResourceSchema, N as deviceProfileScopeSchema, Nr as displayedScopeDigest, O as createAccount, On as scanSessionResourceSchema, P as profileUrlMatchesIdentity, Pr as requestedScope, Qr as sendReceiptSchema, St as replaceSensitiveTermsSchema, Tt as sendPrepareSchema, U as validateInspectionBatch, Ur as hasCurrentStaticReview, Ut as newJourneyWork, V as inspectionCancelSchema, Vt as classificationWorks, Wr as staticReviewDigest, Xr as journeySendSchema, Yr as agentSourcesSchema, Zr as sendArtifactSchema, _ as changeJourney, _t as manualUploadStartSchema, ai as isReadingComplete, ar as bTermMatcher, at as appendSensitiveTerms, di as packageCommandSchema, dt as validateFailureLocation, ei as sendResolutionSchema, fi as selectionBatchSchema, fr as journeyConfirmedHides, ft as manualUploadActionSchema, gr as workDecisionDigest, gt as manualUploadSchema, h as hideScopeDigest, hi as producerVersion, hr as retireWorkDescription, ht as manualUploadPrepareSchema, ii as isIncludedWork, j as accountProfileSchema, jn as verifiedScanGitSeeds, k as renew, kn as scanGitSeedsId, kt as currentJourneyAdapters, li as withReadingProgress, lr as confirmedHideKinds, lt as artifactFailureDetailSchema, m as activeJourneyTerms, mt as manualUploadChoiceSchema, ni as hasCompleteWorkDescription, nt as settingsQuerySchema, oi as isSelectedWork, pi as selectionSchema, ri as isClassifying, ti as sendSettingsSchema, tt as settingsBatchSchema, ui as commandSchema, vi as recommendParallelism, vr as workCorrectionBinding, wt as sendChoiceSchema, xt as normalizeSensitiveTerms, yr as emptyPreparationFacts, yt as emptyTermsRevision, zn as applyClassification } from "./src-D1iONla3.mjs";
6
- import { vt as isCoordinationBusy } from "./heap-CV8Eca8V.mjs";
7
- import { d as checkOpenSource, h as openSource } from "./run-CaPktPhH.mjs";
8
- import { t as createAgentSetupPort } from "./agent-setup-DdfF3zch.mjs";
1
+ import { A as captureLogicalJourneySources, B as defaultParallelism, D as createCollectionArtifacts, E as readConfiguredSearchFields, G as readLogicalJourneyFinalMasks, H as parallelismLimits, I as createSharedSessionPool, J as confirmedFamily, K as readConfiguredOutputInfo, L as SessionWorkerError, N as runConfiguredSessionJob, R as sessionWorkerFailure, T as readOriginalSearchFields, V as gitSeedsProvider, W as privateJourneyDirectory, X as staticMaskCategory, Y as confirmedTitleSpans, Z as readWorkspacePair, a as prepareSessionScans, b as readStaticSessionMasks, c as createWorkPieces, ct as openPrivateFile, d as scanPreparedSession, dt as isWindowsAclFailure, ft as windowsAclCodes, g as openConfiguredUpload, h as configuredSessionUploads, i as verifyPreparedSessionSources, it as createCleanup, j as capturedJourneyScope, l as readSessionScanFields, lt as restrictToOwner, n as sessionExtensions, nt as createTemporary, o as buildWorkScanBatches, q as processJourneyMetadataFields, r as preparedScanConfiguration, rt as removeNamedTemporarySpools, s as scanReviewFields, st as makePrivateDirectory, t as hasRetiredSessionMarker, tt as decodeZstd, u as readSessionScanRecords, ut as windowsOwnerOnly, v as readCapturedOriginalRows, x as staticSessionJobs, y as processJourneyMetadata } from "./src-CP-K2PBv.mjs";
2
+ import { A as exportDelivery, C as prepareSourcePlan, D as saveRemoteState, E as readRemoteState, H as isBackgroundStatusPoll, I as withStateMutex, L as createDeliveryService, M as createStore, O as journeyDirectory, P as createExecutionPort, R as preflight, S as changePreparation, T as isRemoteImport, U as retrySharingViolation, V as inBackgroundStoreLane, _ as staticBaseProduction, a as writeJourneyOutput, b as blockedCodes, c as mergeHideIndexTokens, f as writeJourneyBase, g as staticBaseInputs, h as staticSessions, j as createPackagingProgressPort, k as collectRemoteWorkers, l as packRenderHideIndex, m as saveStaticSession, o as createHideIndexTokens, p as readStaticResult, s as hideIndexTexts, t as journeyOutputJob, u as journeyBaseJob, v as readSelectedCaptures, w as workApproval, x as isolateFailingRoots, y as RootPreparationFailure, z as loadDeliveryResults } from "./job--zxwgaPy.mjs";
3
+ import { $t as ownerProfileSchema, An as currentDetectorVersion, Cr as siteFields, Fn as createStaticProcessor, Jt as sessionPeriodSchema, P as bLiteralTerms, Pt as privacyTermKey, Qt as mergeOwnerProfile, Sr as processingSite, Un as entityDictionaryVersion, Ut as termTypeInfo, Zn as frameRecords, an as UnfinishedJourneyError, en as passportAddressSchema, f as textDigest, fr as createEntityMatcher, in as ApplicationError, ln as familySelected, mr as admitGitName, on as safeCode, pn as observeSource, pr as admitCustomTerm, qt as filterInventoryByPeriod, sn as safeField, tn as profileHasChoices, un as reviewFamilies, vr as ProcessingError } from "./locate-PLEyLYW-.mjs";
4
+ import { a as mainTextChunks, c as previewInWorker, i as nearestSortingRepository, l as createNativeAdapters, n as createProjectEvidenceCollector, o as mainTextFields, r as sortingRepositoryId, s as textChunks, t as createHarnesses, u as createGitNameSeedProvider } from "./src-dL6y2mcu.mjs";
5
+ import { $r as sendReplaceResultSchema, An as scanExclusionResourceSchema, B as createApplication, Bt as newJourneyWork, Ci as bTermMatcher, Cr as privacyDecisionDigest, Ct as sendActionSchema, Di as packageCommandSchema, Dn as scanGitSeedsId, Dt as sendStartSchema, Ei as commandSchema, En as scanSessionResourceSchema, Et as sendRequestSchema, F as readAccountAvatarUrl, Fr as itemAtPassport, Gr as staticSessionId, H as inspectionStartSchema, I as storedCredentialSchema, Ir as obsoleteJourney, It as classificationPartition, Jr as agentSourceSchema, K as inspectionQuerySchema, Lt as classificationWorks, M as deviceAuthorizationSchema, N as deviceProfileScopeSchema, Nr as currentJourneyAdapters, O as createAccount, Oi as selectionBatchSchema, On as scanGitSeedsResource, P as profileUrlMatchesIdentity, Pi as recommendParallelism, Pr as hostDrives, Qr as sendReceiptSchema, Rn as applyClassification, Rt as retireScanGeneration, Sr as assertJourneyPrivacyReady, St as replaceSensitiveTermsSchema, Ti as replacementMarkerPattern, Tn as scanPreparationDigest, Tr as rememberedHideValues, Tt as sendPrepareSchema, U as validateInspectionBatch, Ur as hasCurrentStaticReview, V as inspectionCancelSchema, Vn as searchScopeDigest, Wr as staticReviewDigest, Xr as journeySendSchema, Yr as agentSourcesSchema, Zr as sendArtifactSchema, _ as changeJourney, _i as journeyConfirmedHides, _t as manualUploadStartSchema, ai as isIncludedWork, ar as emptyPreparationFacts, at as appendSensitiveTerms, br as requestedScope, di as withReadingProgress, dt as validateFailureLocation, ei as sendResolutionSchema, ft as manualUploadActionSchema, gt as manualUploadSchema, h as hideScopeDigest, ht as manualUploadPrepareSchema, ii as isClassifying, ir as workCorrectionBinding, j as accountProfileSchema, ji as producerVersion, jr as advanceStepsSend, k as renew, ki as selectionSchema, kn as verifiedScanGitSeeds, kr as nativeStartSchema, lt as artifactFailureDetailSchema, m as activeJourneyTerms, mi as confirmedHideKinds, mt as manualUploadChoiceSchema, ni as sendSettingsSchema, nr as workDecisionDigest, nt as settingsQuerySchema, oi as isReadingComplete, ri as hasCompleteWorkDescription, si as isSelectedWork, tr as retireWorkDescription, tt as settingsBatchSchema, ui as unfinishedPackagingCode, wt as sendChoiceSchema, xt as normalizeSensitiveTerms, yr as displayedScopeDigest, yt as emptyTermsRevision, zn as journeySearchResultSchema, zr as sendsOnNextAdvance, zt as isolatedWorkId } from "./src-CttNuxOn.mjs";
6
+ import { vt as isCoordinationBusy } from "./heap-DCS_WXIA.mjs";
7
+ import { d as checkOpenSource, h as openSource } from "./run-Dod7Vykp.mjs";
8
+ import { t as createAgentSetupPort } from "./agent-setup-Doy_AnPY.mjs";
9
9
  import { t as workerMessageSchema } from "./messages-D3m3bhva.mjs";
10
10
  import { constants, write } from "node:fs";
11
11
  import { z } from "zod";
@@ -124,6 +124,7 @@ const businessCommands = [
124
124
  "journey.classificationSessions",
125
125
  "journey.classificationProgress",
126
126
  "journey.classificationSubmit",
127
+ "journey.recoverIsolatedMetadata",
127
128
  "journey.cardsRead",
128
129
  "journey.cardsSubmit",
129
130
  "journey.resolve",
@@ -165,6 +166,7 @@ const businessCommands = [
165
166
  "workflow.events",
166
167
  "bundleLibrary.get",
167
168
  "bundleLibrary.save",
169
+ "rememberedHides.remove",
168
170
  "sensitiveTerms.get",
169
171
  "sensitiveTerms.replace",
170
172
  "processing.catalog",
@@ -216,6 +218,23 @@ const accountDefaults = {
216
218
  clientId: "BdSyvRSunWdXfJUdfvaKuAgoRjaZQpOZ",
217
219
  passportApiUrl: "https://talent-api.catalyte.ai"
218
220
  };
221
+ /** The web origin paired with each deployed Passport API; custom targets supply theirs. */
222
+ function passportWebOrigin(apiUrl, configured) {
223
+ if (configured) {
224
+ const url = new URL(configured);
225
+ const local = [
226
+ "127.0.0.1",
227
+ "[::1]",
228
+ "localhost"
229
+ ].includes(url.hostname);
230
+ if (url.protocol !== "https:" && !(local && url.protocol === "http:") || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new Error("INVALID_ARGUMENTS");
231
+ return url.origin;
232
+ }
233
+ return {
234
+ "https://talent-api.catalyte.ai": "https://talent.catalyte.ai",
235
+ "https://talent-api-dev.catalyte.ai": "https://talent-dev.catalyte.ai"
236
+ }[new URL(apiUrl).origin];
237
+ }
219
238
  //#endregion
220
239
  //#region apps/cli/src/options.ts
221
240
  /** An HTTP(S) origin without path, query, credentials or fragment. */
@@ -335,6 +354,7 @@ function parseOptions(args, source = sourceCheckout) {
335
354
  sessionPeriod: period.data,
336
355
  identityUrl: readOrigin(parsed.values["identity-url"] ?? process.env["SESS_PILOT_IDENTITY_URL"] ?? accountDefaults.identityUrl),
337
356
  identityClientId: identityClientId === "" ? null : identityClientId,
357
+ ...process.env["SESS_PILOT_PASSPORT_WEB_ORIGIN"] ? { passportWebOrigin: readOrigin(process.env["SESS_PILOT_PASSPORT_WEB_ORIGIN"]) } : {},
338
358
  passportApiUrl: readOrigin(parsed.values["profile-api-url"] ?? parsed.values["passport-api-url"] ?? process.env["SESS_PILOT_PROFILE_API_URL"] ?? process.env["SESS_PILOT_PASSPORT_API_URL"] ?? accountDefaults.passportApiUrl),
339
359
  stateDir: resolve(parsed.values["state-dir"] ?? defaultState),
340
360
  bundleDir: parsed.values["bundle-dir"] === void 0 ? void 0 : resolve(parsed.values["bundle-dir"]),
@@ -359,7 +379,8 @@ ${Array.from(businessCommands).join("\n")}
359
379
  Business commands invoke the shared Application directly without an HTTP server.
360
380
  journey.serve starts a focused confirmation host, emits JSON readiness, and waits until stopped.
361
381
  journey.serve --detach starts that host in its own process, prints its readiness with the host's
362
- pid, and exits; stop the host with that pid. A detached host already serving the journey is
382
+ pid, and exits. The launcher verifies the release and process identity before reusing a panel.
383
+ A detached host already serving the journey with the paired release is
363
384
  reused and reported with reused: true.
364
385
  It does not discover sessions or open a browser. The agent opens its private URL in the right pane.
365
386
  Workflow mutations require protocolVersion, runId and expectedRevision in JSON input.
@@ -451,6 +472,81 @@ function commands$1(command) {
451
472
  return [];
452
473
  }
453
474
  }
475
+ /** Transparent prefixes that run their operand command; `sudo` is deliberately absent. */
476
+ const wrappers = {
477
+ nohup: {
478
+ flags: [],
479
+ valued: []
480
+ },
481
+ command: {
482
+ flags: ["-p"],
483
+ valued: []
484
+ },
485
+ time: {
486
+ flags: ["-p"],
487
+ valued: []
488
+ },
489
+ nice: {
490
+ flags: [],
491
+ valued: ["-n", "--adjustment"]
492
+ },
493
+ timeout: {
494
+ flags: [
495
+ "--preserve-status",
496
+ "--foreground",
497
+ "-v",
498
+ "--verbose"
499
+ ],
500
+ valued: [
501
+ "-s",
502
+ "--signal",
503
+ "-k",
504
+ "--kill-after"
505
+ ],
506
+ operand: /^\d+(?:\.\d+)?[smhd]?$/u
507
+ },
508
+ env: {
509
+ flags: [
510
+ "-i",
511
+ "-",
512
+ "--ignore-environment"
513
+ ],
514
+ valued: ["-u", "--unset"],
515
+ assignments: true
516
+ }
517
+ };
518
+ /** Strip known wrappers; an unknown wrapper option fails closed rather than guessing. */
519
+ function unwrap(words) {
520
+ let rest = words;
521
+ for (let depth = 0; depth < 8; depth++) {
522
+ const name = posix.basename(rest[0] ?? "");
523
+ const wrapper = Object.hasOwn(wrappers, name) ? wrappers[name] : void 0;
524
+ if (!wrapper) return rest;
525
+ let index = 1;
526
+ for (; index < rest.length; index++) {
527
+ const word = rest[index] ?? "";
528
+ if (word === "--") {
529
+ index++;
530
+ break;
531
+ }
532
+ if (wrapper.flags.includes(word)) continue;
533
+ if (wrapper.valued.includes(word)) {
534
+ index++;
535
+ continue;
536
+ }
537
+ if (wrapper.valued.some((flag) => word.startsWith(flag.startsWith("--") ? `${flag}=` : flag))) continue;
538
+ if (word.startsWith("-")) return [];
539
+ break;
540
+ }
541
+ if (wrapper.operand) {
542
+ if (!wrapper.operand.test(rest[index] ?? "")) return [];
543
+ index++;
544
+ }
545
+ if (wrapper.assignments) while (/^[A-Za-z_][A-Za-z0-9_]*=/u.test(rest[index] ?? "")) index++;
546
+ rest = rest.slice(index);
547
+ }
548
+ return [];
549
+ }
454
550
  function agent(words) {
455
551
  const executable = posix.basename(words[0] ?? "");
456
552
  if (words.some((word) => [
@@ -462,6 +558,38 @@ function agent(words) {
462
558
  if (executable === "claude" && words.slice(1).some((word) => word === "-p" || word === "--print")) return "claude";
463
559
  return null;
464
560
  }
561
+ const shells = /* @__PURE__ */ new Set([
562
+ "sh",
563
+ "bash",
564
+ "zsh",
565
+ "dash",
566
+ "ksh"
567
+ ]);
568
+ const interpreters = /* @__PURE__ */ new Set([
569
+ ...shells,
570
+ "python",
571
+ "python3"
572
+ ]);
573
+ /**
574
+ * Literal agents at command positions, including inside `sh -c` strings. A script file
575
+ * run by a shell or Python interpreter, or a relative or script-named path executable,
576
+ * is a launcher that may start an agent the log cannot show.
577
+ */
578
+ function launches(command, depth = 0) {
579
+ return commands$1(command).flatMap((parsed) => {
580
+ const words = unwrap(parsed);
581
+ const found = agent(words);
582
+ if (found) return [found];
583
+ const executable = words[0] ?? "";
584
+ const name = posix.basename(executable);
585
+ if (!interpreters.has(name)) return /^(?:\.\.?|~)\//u.test(executable) || executable.includes("/") && /\.(?:sh|bash|zsh|py)$/u.test(executable) ? ["script"] : [];
586
+ const operand = words.findIndex((word, index) => index > 0 && !word.startsWith("-"));
587
+ const options = words.slice(1, operand === -1 ? void 0 : operand);
588
+ if (options.some((word) => /^-[a-zA-Z]*c[a-zA-Z]*$/u.test(word))) return shells.has(name) && operand !== -1 && depth < 2 ? launches(words[operand] ?? "", depth + 1) : [];
589
+ if (options.some((word) => /^-[a-zA-Z]*[ms]|^--help$/u.test(word))) return [];
590
+ return operand === -1 ? [] : ["script"];
591
+ });
592
+ }
465
593
  const validHost = (host) => /^(?:[a-zA-Z0-9_][a-zA-Z0-9_.-]*@)?(?:[a-zA-Z0-9_][a-zA-Z0-9_.-]*|\[[a-fA-F0-9:]+\])$/u.test(host);
466
594
  /** Consent and import identity include every retained connection selector. */
467
595
  function endpoint(host, transport, args) {
@@ -525,20 +653,27 @@ function gcloud(words) {
525
653
  }
526
654
  /** Matches executables at shell command positions, never mentions in prose or other commands. */
527
655
  function parseRemoteDispatch(command) {
528
- return commands$1(command).flatMap((words) => {
656
+ return commands$1(command).flatMap((parsed) => {
657
+ const words = unwrap(parsed);
529
658
  const executable = posix.basename(words[0] ?? "");
530
659
  const remote = executable === "ssh" ? ssh(words) : executable === "gcloud" ? gcloud(words) : null;
531
660
  if (!remote) return [];
532
- return [...new Set(commands$1(remote.command).map(agent))].flatMap((harness) => harness ? [{
661
+ const found = new Set(launches(remote.command));
662
+ const agents = [...found].filter((harness) => harness !== "script");
663
+ return (agents.length ? agents : found.has("script") ? ["unknown"] : []).map((harness) => ({
533
664
  host: endpoint(remote.host, remote.transport, remote.args),
534
665
  harness,
535
666
  transport: remote.transport,
536
667
  args: remote.args
537
- }] : []);
668
+ }));
538
669
  });
539
670
  }
540
671
  //#endregion
541
672
  //#region apps/cli/src/remote/scan.ts
673
+ /** Escapes and shell quotes may split executable spelling; retain those candidates. */
674
+ function mayContainRemoteDispatch(line) {
675
+ return line.includes("ssh") || line.includes("gcloud") || line.includes("\\") || line.includes("'");
676
+ }
542
677
  function object(value) {
543
678
  return typeof value === "object" && value !== null && !Array.isArray(value);
544
679
  }
@@ -616,11 +751,12 @@ async function scanRemoteDispatches(sessions, signal) {
616
751
  const pending = /* @__PURE__ */ new Map();
617
752
  for (const part of session.parts) {
618
753
  signal.throwIfAborted();
619
- const file = await openSource(part);
754
+ const mode = part.representation === "jsonl" ? "append" : "snapshot";
755
+ const file = await openSource(part, mode);
620
756
  try {
621
757
  if (!part.snapshot.size) {
622
758
  if (part.representation === "jsonl.zst") throw new Error("ARTIFACT_PROCESSING_DECODE_FAILED");
623
- await checkOpenSource(part, file);
759
+ await checkOpenSource(part, file, mode);
624
760
  continue;
625
761
  }
626
762
  const stream = file.createReadStream({
@@ -643,6 +779,7 @@ async function scanRemoteDispatches(sessions, signal) {
643
779
  signal.throwIfAborted();
644
780
  const line = framed.endsWith("\r") ? framed.slice(0, -1) : framed;
645
781
  if (!line.trim()) continue;
782
+ if (!pending.size && !mayContainRemoteDispatch(line)) continue;
646
783
  let row;
647
784
  try {
648
785
  row = JSON.parse(line);
@@ -674,7 +811,7 @@ async function scanRemoteDispatches(sessions, signal) {
674
811
  if (found.length) pending.set(command.id, found);
675
812
  }
676
813
  }
677
- await checkOpenSource(part, file);
814
+ await checkOpenSource(part, file, mode);
678
815
  } finally {
679
816
  stream.destroy();
680
817
  }
@@ -773,13 +910,17 @@ function createRemotePreflight(options) {
773
910
  }
774
911
  function result(state) {
775
912
  return {
776
- hosts: [...new Set(state.windows.map((window) => window.host))].filter((host) => !Object.hasOwn(state.decisions, host)).map((host) => ({
777
- host,
778
- windows: state.windows.filter((window) => window.host === host).map(({ start, end }) => ({
779
- start,
780
- end
781
- }))
782
- })),
913
+ hosts: [...new Set(state.windows.map((window) => window.host))].filter((host) => !Object.hasOwn(state.decisions, host)).map((host) => {
914
+ const windows = state.windows.filter((window) => window.host === host);
915
+ return {
916
+ host,
917
+ windows: windows.map(({ start, end }) => ({
918
+ start,
919
+ end
920
+ })),
921
+ ...windows.some((window) => window.harness === "unknown") ? { scriptLaunch: true } : {}
922
+ };
923
+ }),
783
924
  copied: state.copied.length,
784
925
  failures: state.failures,
785
926
  ...state.sourceFailureCounts ? { sourceFailureCounts: state.sourceFailureCounts } : {},
@@ -1054,14 +1195,16 @@ function createJourneyTitles(storage, processing) {
1054
1195
  if (state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
1055
1196
  const work = state.works.find((item) => item.id === action.workId);
1056
1197
  if (!work) throw new ApplicationError("UNKNOWN_WORKSPACE");
1057
- if (action.title === work.title) return commit();
1198
+ if (action.title === work.title && (work.titleOrigin === "explicit" || new RegExp(replacementMarkerPattern).test(work.title))) return commit();
1058
1199
  let title;
1200
+ let staticMasks = [];
1059
1201
  try {
1060
1202
  const [processed] = await processJourneyMetadataFields({
1061
1203
  ...await metadataProcessing(storage, state, work, processing),
1062
1204
  fields: [{ text: action.title }]
1063
1205
  }, signal);
1064
1206
  title = processed?.text;
1207
+ staticMasks = processed?.staticMasks ?? [];
1065
1208
  } catch (error) {
1066
1209
  if (typeof error === "object" && error !== null && "code" in error && error.code === "PROCESSING_INVALID_RECORD") throw new ApplicationError("GENERATED_METADATA_REPHRASE_REQUIRED", void 0, { cause: error });
1067
1210
  throw error;
@@ -1074,10 +1217,36 @@ function createJourneyTitles(storage, processing) {
1074
1217
  id: input.idempotencyKey,
1075
1218
  title: action.title
1076
1219
  });
1220
+ let suggestion = null;
1221
+ const described = work.suggestion ? (await storage.resources({
1222
+ journeyId: state.journeyId,
1223
+ kind: "work-description",
1224
+ id: work.suggestion.id
1225
+ }))[0] : void 0;
1226
+ if (work.suggestion && described?.kind === "work-description" && described.workId === work.id && described.inputRevision === work.inputRevision) {
1227
+ const id = randomUUID();
1228
+ await storage.saveResource({
1229
+ ...described,
1230
+ id,
1231
+ explicitTitleId: input.idempotencyKey,
1232
+ appliedFields: {
1233
+ ...described.appliedFields,
1234
+ title
1235
+ },
1236
+ appliedTitleStaticMasks: [...staticMasks],
1237
+ appliedTitleStaticText: title
1238
+ });
1239
+ suggestion = {
1240
+ ...work.suggestion,
1241
+ id,
1242
+ title
1243
+ };
1244
+ }
1077
1245
  return commit({
1078
1246
  ...state,
1079
1247
  works: state.works.map((item) => item.id !== work.id ? item : {
1080
1248
  ...retireWorkDescription(item),
1249
+ suggestion,
1081
1250
  title,
1082
1251
  titleOrigin: "explicit",
1083
1252
  explicitTitleId: input.idempotencyKey,
@@ -1171,6 +1340,10 @@ function bindJourneyDestination(storage, destination, options = {}) {
1171
1340
  };
1172
1341
  return {
1173
1342
  current,
1343
+ async readSendOutput(input) {
1344
+ await guard(input.journeyId);
1345
+ return storage.readSendOutput(input);
1346
+ },
1174
1347
  async preparationState(id) {
1175
1348
  await guard(id);
1176
1349
  return storage.preparationState(id);
@@ -1236,6 +1409,10 @@ function bindJourneyDestination(storage, destination, options = {}) {
1236
1409
  await guard(input.journeyId);
1237
1410
  return storage.saveResource(input);
1238
1411
  },
1412
+ async receiptRevision(input) {
1413
+ await guard(input.journeyId);
1414
+ return storage.receiptRevision(input);
1415
+ },
1239
1416
  async commitClassification(input) {
1240
1417
  await guard(input.journeyId);
1241
1418
  return storage.commitClassification(input);
@@ -1340,10 +1517,29 @@ function bindJourneyDestination(storage, destination, options = {}) {
1340
1517
  };
1341
1518
  }
1342
1519
  function bindSendDestination(storage, destination) {
1520
+ const verified = /* @__PURE__ */ new Set();
1521
+ async function verify(id) {
1522
+ if (verified.has(id)) return;
1523
+ const snapshot = await storage.loadSend(id);
1524
+ if (!snapshot) throw new ApplicationError("UNKNOWN_ITEM");
1525
+ check(snapshot.journey, destination);
1526
+ verified.add(id);
1527
+ }
1343
1528
  return {
1344
- async loadSend(id, sendId) {
1345
- const result = await storage.loadSend(id, sendId);
1346
- if (result) check(result.journey, destination);
1529
+ async isCurrentSend(id, sendId, revision) {
1530
+ await verify(id);
1531
+ return storage.isCurrentSend(id, sendId, revision);
1532
+ },
1533
+ async checkpointTransfer(input) {
1534
+ await verify(input.journeyId);
1535
+ return storage.checkpointTransfer(input);
1536
+ },
1537
+ async loadSend(id, sendId, observedRevision) {
1538
+ const result = await storage.loadSend(id, sendId, verified.has(id) ? observedRevision : void 0);
1539
+ if (result) {
1540
+ check(result.journey, destination);
1541
+ verified.add(result.journey.journeyId);
1542
+ }
1347
1543
  return result;
1348
1544
  },
1349
1545
  async commitSend(input) {
@@ -1356,31 +1552,6 @@ function bindSendDestination(storage, destination) {
1356
1552
  };
1357
1553
  }
1358
1554
  //#endregion
1359
- //#region apps/cli/src/journey/prepare/update.ts
1360
- /** Preparation owns only the changed Work rows, progress and package invalidation. */
1361
- async function changePreparation(storage, journeyId, kind, change, attempts = Number.POSITIVE_INFINITY, clearPackageApproval = false) {
1362
- for (let attempt = 0; attempt < attempts; attempt++) {
1363
- const before = await storage.preparationState(journeyId);
1364
- if (!before?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
1365
- const after = change(before);
1366
- const previous = new Map(before.works.map((work) => [work.id, work]));
1367
- const result = await storage.updatePreparation({
1368
- journeyId,
1369
- expectedRevision: before.revision,
1370
- requestKey: randomUUID(),
1371
- at: (/* @__PURE__ */ new Date()).toISOString(),
1372
- kind,
1373
- works: after.works.filter((work) => work !== previous.get(work.id)),
1374
- phase: after.phase,
1375
- readingComplete: after.readingComplete,
1376
- readingStartedAt: after.readingStartedAt,
1377
- clearPackageApproval: clearPackageApproval && before !== after
1378
- });
1379
- if (result.status !== "stale") return result.state;
1380
- }
1381
- throw new ApplicationError("REVISION_CONFLICT");
1382
- }
1383
- //#endregion
1384
1555
  //#region apps/cli/src/journey/works/metadata.ts
1385
1556
  /** Original classification fields remain durable; only the derived display cache is replaced. */
1386
1557
  const classificationMetadataInput = (work) => ({
@@ -1389,7 +1560,7 @@ const classificationMetadataInput = (work) => ({
1389
1560
  description: work.description,
1390
1561
  reason: work.classificationRecommendation?.reason ?? work.reason
1391
1562
  });
1392
- async function prepareClassificationMetadata(storage, state, work, processing, signal, plan) {
1563
+ async function processedClassificationMetadata(storage, state, work, processing, signal, plan) {
1393
1564
  if (state.scope.status !== "confirmed") throw new ApplicationError("CONFIRMATION_REQUIRED");
1394
1565
  if (work.classificationMetadata && (work.explicitTitleId || work.suggestion)) return;
1395
1566
  const identity = classificationMetadataInput(work);
@@ -1428,6 +1599,19 @@ async function prepareClassificationMetadata(storage, state, work, processing, s
1428
1599
  fields: Object.values(raw).map((text) => ({ text }))
1429
1600
  }, signal);
1430
1601
  if (title === void 0 || projectName === void 0 || description === void 0 || reason === void 0) throw new ApplicationError("CORRUPTED_STATE");
1602
+ return {
1603
+ digest,
1604
+ title: title.text,
1605
+ projectName: projectName.text,
1606
+ description: description.text,
1607
+ reason: reason.text,
1608
+ titleOriginalSha256: textDigest(raw.title),
1609
+ titleStaticMasks: [...title.staticMasks]
1610
+ };
1611
+ }
1612
+ async function prepareClassificationMetadata(storage, state, work, processing, signal, plan) {
1613
+ const metadata = await processedClassificationMetadata(storage, state, work, processing, signal, plan);
1614
+ if (!metadata) return;
1431
1615
  await changePreparation(storage, state.journeyId, "snapshot", (before) => {
1432
1616
  const active = before.works.find((item) => item.id === work.id);
1433
1617
  if (before.scope.status !== "confirmed" || !active || textDigest(JSON.stringify([
@@ -1436,20 +1620,12 @@ async function prepareClassificationMetadata(storage, state, work, processing, s
1436
1620
  active.suggestion?.id,
1437
1621
  active.explicitTitleId,
1438
1622
  before.exceptions.filter((item) => item.workId === active.id)
1439
- ])) !== digest) throw new ApplicationError("REVISION_CONFLICT");
1623
+ ])) !== metadata.digest) throw new ApplicationError("REVISION_CONFLICT");
1440
1624
  return {
1441
1625
  ...before,
1442
1626
  works: before.works.map((item) => item.id === work.id ? {
1443
1627
  ...item,
1444
- classificationMetadata: {
1445
- digest,
1446
- title: title.text,
1447
- projectName: projectName.text,
1448
- description: description.text,
1449
- reason: reason.text,
1450
- titleOriginalSha256: textDigest(raw.title),
1451
- titleStaticMasks: [...title.staticMasks]
1452
- }
1628
+ classificationMetadata: metadata
1453
1629
  } : item)
1454
1630
  };
1455
1631
  });
@@ -1730,49 +1906,6 @@ function preparationFacts(inventory, rootIds) {
1730
1906
  };
1731
1907
  }
1732
1908
  //#endregion
1733
- //#region apps/cli/src/journey/prepare/isolation.ts
1734
- /** A Work-level refusal caused by identified roots, each with its own public-safe code. */
1735
- var RootPreparationFailure = class extends ApplicationError {
1736
- roots;
1737
- constructor(code, roots, options) {
1738
- super(code, void 0, options);
1739
- this.roots = roots;
1740
- }
1741
- };
1742
- /** The distinct public-safe codes of the identified failing roots, else the Work-level code. */
1743
- function blockedCodes(error, code) {
1744
- return error instanceof RootPreparationFailure && error.roots.size ? [...new Set(error.roots.values())] : [code];
1745
- }
1746
- /**
1747
- * ADR 0188 (restoring ADR 0059 D06): moves the failing roots of a multi-root Work into its
1748
- * stable blocked "<title> – couldn't prepare" Work so the rest can be prepared again. False
1749
- * when the failure names no root or every root, or the Work changed or left pending.
1750
- */
1751
- async function isolateFailingRoots(storage, journeyId, work, error) {
1752
- if (!(error instanceof RootPreparationFailure)) return false;
1753
- const roots = work.rootSessionIds.flatMap((sessionId) => {
1754
- const code = error.roots.get(sessionId);
1755
- return code ? [{
1756
- sessionId,
1757
- code
1758
- }] : [];
1759
- });
1760
- if (roots.length === 0 || roots.length === work.rootSessionIds.length) return false;
1761
- for (;;) {
1762
- const state = await storage.preparationState(journeyId);
1763
- const current = state?.works.find((item) => item.id === work.id);
1764
- if (!state || state.phase === "paused" || state.pendingHideId || current?.inputRevision !== work.inputRevision || current.status !== "pending") return false;
1765
- if ((await storage.isolateRoots({
1766
- journeyId,
1767
- workId: work.id,
1768
- inputRevision: work.inputRevision,
1769
- roots,
1770
- expectedRevision: state.revision,
1771
- idempotencyKey: randomUUID()
1772
- })).status !== "stale") return true;
1773
- }
1774
- }
1775
- //#endregion
1776
1909
  //#region apps/cli/src/journey/prepare/empty-review.ts
1777
1910
  /** Empty AI coverage requires authenticated scanner proof and the original frozen source bindings. */
1778
1911
  async function verifyEmptyScanRoots(resources, roots, signal, storage, plan) {
@@ -1900,14 +2033,27 @@ async function prepareScanContext(storage, journeyId, work, selected, processing
1900
2033
  } } : {}
1901
2034
  }, signal);
1902
2035
  const seeds = frozenGitNames(storage, journeyId, current.snapshotId, processing.gitNames);
2036
+ const families = reviewFamilies(approval.inventory);
1903
2037
  const roots = /* @__PURE__ */ new Map();
1904
2038
  for (const session of approval.inventory.sessions) {
1905
2039
  const workspace = approval.inventory.workspaces.find((item) => item.id === session.workspaceId);
1906
2040
  if (!workspace) throw new ApplicationError("CORRUPTED_STATE");
1907
2041
  const paths = workspace.path === null ? [] : [workspace.path];
1908
- roots.set(JSON.stringify(paths), paths);
2042
+ const key = JSON.stringify(paths);
2043
+ const entry = roots.get(key) ?? {
2044
+ paths,
2045
+ rootIds: /* @__PURE__ */ new Set()
2046
+ };
2047
+ const root = families.owners.get(session.id);
2048
+ if (root) entry.rootIds.add(root.id);
2049
+ roots.set(key, entry);
2050
+ }
2051
+ for (const { paths, rootIds } of roots.values()) try {
2052
+ await seeds(paths, signal);
2053
+ } catch (error) {
2054
+ if (!(error instanceof ProcessingError) || error.code === "PROCESSING_CANCELED") throw error;
2055
+ throw new RootPreparationFailure(error.code, new Map([...rootIds].map((id) => [id, error.code])), { cause: error });
1909
2056
  }
1910
- for (const paths of roots.values()) await seeds(paths, signal);
1911
2057
  return {
1912
2058
  prepared,
1913
2059
  configurationDigest: textDigest(JSON.stringify({
@@ -2715,6 +2861,7 @@ function createJourneyPreparation(options) {
2715
2861
  const plan = await prepareSourcePlan(storage, options.harnesses, journeyId, terms, abort.signal, current.period, options.stateDir);
2716
2862
  const initial = await storage.preparationState(journeyId);
2717
2863
  if (!initial?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
2864
+ const remembered = initial.snapshotId ? [] : await options.rememberedHides?.() ?? [];
2718
2865
  const formed = initial.snapshotId ? [] : namedJourneyWorks(formJourneyWorks(plan.approval, options.processing.environment), options.processing, abort.signal);
2719
2866
  let state;
2720
2867
  if (!initial.snapshotId) state = await changeJourney(storage, journeyId, "snapshot", (before) => ({
@@ -2725,6 +2872,7 @@ function createJourneyPreparation(options) {
2725
2872
  reviewMethod: "dictionary"
2726
2873
  },
2727
2874
  works: before.snapshotId ? before.works : formed,
2875
+ ...before.snapshotId || !remembered.length ? {} : { rememberedHides: [...remembered] },
2728
2876
  phase: before.phase !== "paused" && (!before.snapshotId || before.works.some(pendingSelected)) ? "processing" : before.phase
2729
2877
  }));
2730
2878
  else if (initial.phase !== "processing" && initial.phase !== "paused" && initial.works.some(pendingSelected)) state = await changePreparation(storage, journeyId, "snapshot", (before) => before.phase !== "paused" && before.works.some(pendingSelected) ? {
@@ -2768,7 +2916,9 @@ function createJourneyPreparation(options) {
2768
2916
  };
2769
2917
  const releaseAdmission = /* @__PURE__ */ new Map();
2770
2918
  const retryWork = async (work, error, attempt) => {
2771
- if (blockedCodes(error, safeCode(error)).includes("PRIVACY_CHANGE_PENDING") || work.blockedCodes.includes("PRIVACY_CHANGE_PENDING")) return false;
2919
+ const code = safeCode(error);
2920
+ if (deterministicGitFailure(error)) return false;
2921
+ if (blockedCodes(error, code).includes("PRIVACY_CHANGE_PENDING") || work.blockedCodes.includes("PRIVACY_CHANGE_PENDING")) return false;
2772
2922
  const delay = workRetryDelaysMs[attempt];
2773
2923
  if (attempt >= retryLimit || delay === void 0) return false;
2774
2924
  const blocked = await blockWork(work, error);
@@ -2801,9 +2951,9 @@ function createJourneyPreparation(options) {
2801
2951
  const readRetries = /* @__PURE__ */ new Map();
2802
2952
  const staticRetries = /* @__PURE__ */ new Map();
2803
2953
  /** A failed isolation leaves the Work blocked as before; cancellation and contention escape. */
2804
- const isolateRoots = async (work, error) => {
2954
+ const isolateRoots = async (work, error, afterRead = false) => {
2805
2955
  try {
2806
- return await isolateFailingRoots(storage, journeyId, work, error);
2956
+ return await isolateFailingRoots(storage, journeyId, work, error, afterRead);
2807
2957
  } catch (isolation) {
2808
2958
  abort.signal.throwIfAborted();
2809
2959
  const code = safeCode(isolation);
@@ -2881,7 +3031,7 @@ function createJourneyPreparation(options) {
2881
3031
  if (work) await blockWork(work, error);
2882
3032
  return false;
2883
3033
  }
2884
- if (attempt < retryLimit) return false;
3034
+ if (attempt < retryLimit && !deterministicGitFailure(error)) return false;
2885
3035
  if (work && await isolateRoots(work, error)) {
2886
3036
  readRetries.delete(generation(work));
2887
3037
  return true;
@@ -2934,7 +3084,13 @@ function createJourneyPreparation(options) {
2934
3084
  return staticWork(planned, passSubmitted);
2935
3085
  }
2936
3086
  const now = await storage.preparationState(journeyId);
2937
- if (now?.phase !== "sending" && now?.phase !== "completed" && (attempt >= retryLimit || blockedCodes(error, safeCode(error)).includes("PRIVACY_CHANGE_PENDING"))) await blockWork(work, error);
3087
+ if (now?.phase !== "sending" && now?.phase !== "completed" && (attempt >= retryLimit || deterministicGitFailure(error) || blockedCodes(error, safeCode(error)).includes("PRIVACY_CHANGE_PENDING"))) {
3088
+ if (await isolateRoots(work, error, true)) {
3089
+ staticRetries.delete(generation(work));
3090
+ return;
3091
+ }
3092
+ await blockWork(work, error);
3093
+ }
2938
3094
  }
2939
3095
  };
2940
3096
  let fatal;
@@ -3066,6 +3222,10 @@ function createJourneyPreparation(options) {
3066
3222
  }
3067
3223
  };
3068
3224
  }
3225
+ /** A broken local Git command will not recover by rereading the same snapshot. */
3226
+ function deterministicGitFailure(error) {
3227
+ return /^git-(?:show-ref|log|config|for-each-ref|rev-parse|cat-file)-exit-\d+$/.test(processingSite(error)?.site ?? "");
3228
+ }
3069
3229
  /** Two automatic retries per Work, after two then five seconds. */
3070
3230
  const workRetryDelaysMs = [2e3, 5e3];
3071
3231
  async function waitForWorkRetry(milliseconds, signal) {
@@ -3190,14 +3350,15 @@ async function readJourneyPlan(storage, journeyId) {
3190
3350
  async function finalKeywordDictionary(storage, state, sessionIds, plan) {
3191
3351
  if (!state.snapshotId) throw new ApplicationError("CONFIRMATION_REQUIRED");
3192
3352
  const dictionary = /* @__PURE__ */ new Map();
3193
- const add = (term, kind, bounded) => {
3353
+ const add = (term, kind, bounded, toolResultEscapes = false) => {
3194
3354
  const prior = dictionary.get(term);
3195
3355
  dictionary.set(term, {
3196
3356
  kind: prior && confirmedHideKinds.indexOf(prior.kind) > confirmedHideKinds.indexOf(kind) ? prior.kind : kind,
3197
- bounded: bounded && (prior?.bounded ?? true)
3357
+ bounded: bounded && (prior?.bounded ?? true),
3358
+ ...toolResultEscapes || prior?.toolResultEscapes ? { toolResultEscapes: true } : {}
3198
3359
  });
3199
3360
  };
3200
- for (const [term, { kind }] of await journeyConfirmedHides(storage, state)) add(term, kind, true);
3361
+ for (const [term, { kind }] of await journeyConfirmedHides(storage, state)) add(term, kind, true, true);
3201
3362
  for (const value of (await activeJourneyTerms(storage, state)).terms) {
3202
3363
  const admitted = admitCustomTerm(value);
3203
3364
  if (admitted) add(admitted.value, "private", false);
@@ -3303,10 +3464,11 @@ function createFamilyHides() {
3303
3464
  families.set(sessionId, terms);
3304
3465
  }
3305
3466
  const members = (root, role) => sessions.filter((session) => session.role === role && session.rootSessionId === root).map((session) => session.archiveSessionId);
3306
- const all = [...confirmed].map(([term, { kind, bounded }]) => ({
3467
+ const all = [...confirmed].map(([term, { kind, bounded, toolResultEscapes }]) => ({
3307
3468
  term,
3308
3469
  category: kind,
3309
- ...bounded ? { bounded: true } : {}
3470
+ ...bounded ? { bounded: true } : {},
3471
+ ...bounded && toolResultEscapes ? { toolResultEscapes: true } : {}
3310
3472
  }));
3311
3473
  return [...families].flatMap(([root, terms]) => {
3312
3474
  const auto = terms.size ? all.filter(({ term }) => !terms.has(term)) : all;
@@ -3564,8 +3726,10 @@ function createRenderWorkers(workers, size) {
3564
3726
  if (result.kind !== "journey-base") throw new Error("ARTIFACT_PROCESSING_FAILED");
3565
3727
  return result.base;
3566
3728
  } catch (error) {
3567
- await removeNamedTemporarySpools(named).catch((cleanup) => workers.onFallback?.(cleanup));
3568
- if (error instanceof SessionWorkerError) throw new ApplicationError(error.message, void 0, { cause: error });
3729
+ if (error instanceof SessionWorkerError) {
3730
+ await removeNamedTemporarySpools(named).catch((cleanup) => workers.onFallback?.(cleanup));
3731
+ throw new ApplicationError(error.message, void 0, { cause: error });
3732
+ }
3569
3733
  if (signal.aborted) throw error;
3570
3734
  workers.onFallback?.(error);
3571
3735
  return pool.serial(() => writeJourneyBase(work, signal));
@@ -3648,6 +3812,14 @@ function renderRetry(signal, slots = 1) {
3648
3812
  //#region apps/cli/src/journey/metadata/render.ts
3649
3813
  /** Reuse private raw provenance, never a displayed anonymized title as new source text. */
3650
3814
  async function renderTitle(storage, state, work) {
3815
+ const terms = [...await journeyConfirmedHides(storage, state)].map(([term, { kind }]) => ({
3816
+ term,
3817
+ category: kind
3818
+ }));
3819
+ const semantic = (text) => ({
3820
+ textSha256: textDigest(text),
3821
+ spans: work.titleOrigin === "explicit" ? [] : confirmedTitleSpans(text, confirmedFamily(terms, workExceptions(state, work.id)))
3822
+ });
3651
3823
  if (!work.suggestion) {
3652
3824
  if (work.explicitTitleId) throw new ApplicationError("INCOMPLETE_REVIEW");
3653
3825
  const cached = work.classificationMetadata;
@@ -3655,10 +3827,7 @@ async function renderTitle(storage, state, work) {
3655
3827
  return { cachedTitle: {
3656
3828
  text: cached.title,
3657
3829
  staticMasks: cached.titleStaticMasks,
3658
- semantic: {
3659
- textSha256: textDigest(cached.title),
3660
- spans: []
3661
- }
3830
+ semantic: semantic(cached.title)
3662
3831
  } };
3663
3832
  }
3664
3833
  const description = (await storage.resources({
@@ -3672,10 +3841,7 @@ async function renderTitle(storage, state, work) {
3672
3841
  return { cachedTitle: {
3673
3842
  text: title,
3674
3843
  staticMasks: description.appliedTitleStaticMasks,
3675
- semantic: {
3676
- textSha256: textDigest(title),
3677
- spans: []
3678
- }
3844
+ semantic: semantic(title)
3679
3845
  } };
3680
3846
  }
3681
3847
  //#endregion
@@ -3726,7 +3892,7 @@ async function describeItem(journey, work, dependencies, signal) {
3726
3892
  if (work.access === "drop") throw new ApplicationError("INVALID_STATE");
3727
3893
  return {
3728
3894
  workId: work.id,
3729
- title: work.title,
3895
+ title: artifact.title,
3730
3896
  access: work.access,
3731
3897
  artifact,
3732
3898
  operationKey: randomUUID(),
@@ -3808,6 +3974,7 @@ function createJourneyRenderer(options) {
3808
3974
  async function admit(workId, session) {
3809
3975
  const state = await session.current();
3810
3976
  if (!state?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
3977
+ if (state.phase === "completed" && state.outputVersion !== 1) throw new ApplicationError("SCOPE_NOT_READY");
3811
3978
  assertJourneyPrivacyReady(state, true);
3812
3979
  if (obsoleteJourney(state)) throw new ApplicationError("JOURNEY_OBSOLETE");
3813
3980
  if (state.pendingHideId) throw new ApplicationError("SCOPE_NOT_READY");
@@ -4065,6 +4232,14 @@ async function journeyOutputRequest(storage, state, work, processingConfig, _sig
4065
4232
  families: hides.request(base.sessions, dictionary, exceptions),
4066
4233
  metadata: {
4067
4234
  ...title,
4235
+ workspaceTitleHides: {
4236
+ terms: [...dictionary].flatMap(([term, keyword]) => confirmed.has(term) || !keyword.bounded ? [{
4237
+ term,
4238
+ category: keyword.kind,
4239
+ bounded: keyword.bounded
4240
+ }] : []),
4241
+ exceptions
4242
+ },
4068
4243
  access: work.access === "private" ? "private" : "public"
4069
4244
  },
4070
4245
  processing: {
@@ -4582,6 +4757,38 @@ function createClassificationSessions(storage, processing, stateDir) {
4582
4757
  };
4583
4758
  }
4584
4759
  //#endregion
4760
+ //#region apps/cli/src/journey/works/projection-prefix.ts
4761
+ async function readClassificationPrefix(source, options, signal) {
4762
+ const stamp = await projectionStamp(source.capture.path);
4763
+ const rows = [];
4764
+ let ordinal = -1;
4765
+ outer: for await (const original of readCapturedOriginalRows(source, {
4766
+ directory: options.directory,
4767
+ verification: "classification-prefix"
4768
+ }, signal)) {
4769
+ const value = JSON.parse(original.text);
4770
+ for (const field of mainTextFields(value, original.harness, "main", true)) for (const chunk of mainTextChunks(field)) {
4771
+ signal.throwIfAborted();
4772
+ if (++ordinal <= options.after) continue;
4773
+ rows.push({
4774
+ sessionId: original.sessionId,
4775
+ sourcePartId: original.sourcePartId,
4776
+ recordIndex: original.recordIndex,
4777
+ pointer: chunk.pointer,
4778
+ offset: chunk.offset,
4779
+ role: chunk.role,
4780
+ text: chunk.text
4781
+ });
4782
+ if (rows.length === options.limit) break outer;
4783
+ }
4784
+ }
4785
+ if (await projectionStamp(source.capture.path) !== stamp) throw new ApplicationError("CORRUPTED_STATE");
4786
+ return {
4787
+ rows,
4788
+ count: ordinal + 1
4789
+ };
4790
+ }
4791
+ //#endregion
4585
4792
  //#region apps/cli/src/journey/works/projection.ts
4586
4793
  const digest$1 = (bytes) => createHash("sha256").update(bytes).digest();
4587
4794
  async function buildProjection(directory, key, source, role, signal) {
@@ -4664,9 +4871,14 @@ async function buildProjection(directory, key, source, role, signal) {
4664
4871
  }));
4665
4872
  }
4666
4873
  }
4667
- /** Persisted derived content, never a scan receipt. Only the first read replays the capture. */
4874
+ /** Explicit prefixes stream; default reads reuse a fully verified projection. Neither is a scan receipt. */
4668
4875
  async function readClassificationProjection(source, options, signal) {
4669
4876
  signal.throwIfAborted();
4877
+ if (options.role !== "main") return {
4878
+ rows: [],
4879
+ count: 0
4880
+ };
4881
+ if (options.prefix) return readClassificationPrefix(source, options, signal);
4670
4882
  const key = digest$1(JSON.stringify([
4671
4883
  1,
4672
4884
  options.snapshotId,
@@ -4736,9 +4948,11 @@ function createClassificationReader(storage, stateDir) {
4736
4948
  const directory = await journeyDirectory(stateDir, state.journeyId);
4737
4949
  const rows = [];
4738
4950
  let cursor = null;
4951
+ const limit = input.limit ?? 8;
4739
4952
  outer: for (let partIndex = input.cursor?.partIndex ?? 0; partIndex < sources.length; partIndex++) {
4740
4953
  const source = sources[partIndex];
4741
4954
  if (!source) throw new ApplicationError("CORRUPTED_STATE");
4955
+ if (!work.rootSessionIds.includes(source.session.id)) continue;
4742
4956
  const capture = captures.find((item) => item.kind === "capture" && item.capture.partId === source.part.id);
4743
4957
  if (capture?.kind !== "capture") throw new ApplicationError("SCOPE_NOT_READY");
4744
4958
  const after = partIndex === input.cursor?.partIndex ? input.cursor.afterOrdinal : -1;
@@ -4749,12 +4963,13 @@ function createClassificationReader(storage, stateDir) {
4749
4963
  }, {
4750
4964
  directory,
4751
4965
  snapshotId: state.snapshotId,
4752
- role: work.rootSessionIds.includes(source.session.id) ? "main" : "worker",
4966
+ role: "main",
4753
4967
  after,
4754
- limit: 9 - rows.length
4968
+ limit: limit + (input.limit === void 0 ? 1 : 0) - rows.length,
4969
+ prefix: input.limit !== void 0
4755
4970
  }, signal);
4756
4971
  for (const [index, row] of page.rows.entries()) {
4757
- if (rows.length === 8) {
4972
+ if (rows.length === limit) {
4758
4973
  cursor = {
4759
4974
  snapshotId: state.snapshotId,
4760
4975
  inputRevision: work.inputRevision,
@@ -4765,6 +4980,16 @@ function createClassificationReader(storage, stateDir) {
4765
4980
  break outer;
4766
4981
  }
4767
4982
  rows.push(row);
4983
+ if (input.limit !== void 0 && rows.length === limit) {
4984
+ cursor = {
4985
+ snapshotId: state.snapshotId,
4986
+ inputRevision: work.inputRevision,
4987
+ ...input.rootSessionId ? { rootSessionId: input.rootSessionId } : {},
4988
+ partIndex,
4989
+ afterOrdinal: after + index + 1
4990
+ };
4991
+ break outer;
4992
+ }
4768
4993
  }
4769
4994
  }
4770
4995
  const current = await storage.current(state.journeyId);
@@ -4816,6 +5041,22 @@ async function resolveChunkSessions(storage, state, works, signal) {
4816
5041
  }
4817
5042
  //#endregion
4818
5043
  //#region apps/cli/src/journey/works/proposal.ts
5044
+ /** Materialize exactly the Work families accepted by classification, preserving first-name spelling. */
5045
+ function groupedClassificationItems(items) {
5046
+ const merged = /* @__PURE__ */ new Map();
5047
+ for (const item of items) {
5048
+ const key = projectNameKey(item.projectName);
5049
+ const prior = merged.get(key);
5050
+ if (prior) {
5051
+ if (prior.description !== item.description || prior.projectType !== item.projectType || JSON.stringify(prior.recommendation) !== JSON.stringify(item.recommendation)) throw new ApplicationError("INVALID_SELECTION");
5052
+ merged.set(key, {
5053
+ ...prior,
5054
+ rootSessionIds: [...prior.rootSessionIds, ...item.rootSessionIds]
5055
+ });
5056
+ } else merged.set(key, item);
5057
+ }
5058
+ return [...merged.values()];
5059
+ }
4819
5060
  /**
4820
5061
  * One atomic root partition; recommendations have no access authority. Roots still being grouped
4821
5062
  * wait in one unread Work while the other Works are confirmed and read; the submission that
@@ -4855,21 +5096,9 @@ function createClassificationProposal(storage, accountAccess) {
4855
5096
  if (plan?.kind !== "plan") throw new ApplicationError("SCOPE_NOT_READY");
4856
5097
  const roots = new Set(state.works.flatMap((work) => work.rootSessionIds));
4857
5098
  const seen = /* @__PURE__ */ new Set();
4858
- const merged = /* @__PURE__ */ new Map();
4859
- for (const item of input.items) {
4860
- for (const root of item.rootSessionIds) {
4861
- if (!roots.has(root) || seen.has(root)) throw new ApplicationError("INVALID_SELECTION");
4862
- seen.add(root);
4863
- }
4864
- const key = projectNameKey(item.projectName);
4865
- const prior = merged.get(key);
4866
- if (prior) {
4867
- if (prior.description !== item.description || prior.projectType !== item.projectType || JSON.stringify(prior.recommendation) !== JSON.stringify(item.recommendation)) throw new ApplicationError("INVALID_SELECTION");
4868
- merged.set(key, {
4869
- ...prior,
4870
- rootSessionIds: [...prior.rootSessionIds, ...item.rootSessionIds]
4871
- });
4872
- } else merged.set(key, item);
5099
+ for (const item of input.items) for (const root of item.rootSessionIds) {
5100
+ if (!roots.has(root) || seen.has(root)) throw new ApplicationError("INVALID_SELECTION");
5101
+ seen.add(root);
4873
5102
  }
4874
5103
  const pending = input.pendingRootSessionIds ?? [];
4875
5104
  for (const root of pending) {
@@ -4889,7 +5118,7 @@ function createClassificationProposal(storage, accountAccess) {
4889
5118
  return [...new Set(sessions.map((session) => session.workspaceId))];
4890
5119
  };
4891
5120
  const items = [];
4892
- for (const item of merged.values()) {
5121
+ for (const item of groupedClassificationItems(input.items)) {
4893
5122
  signal.throwIfAborted();
4894
5123
  items.push({
4895
5124
  ...item,
@@ -4933,6 +5162,102 @@ function createClassificationProposal(storage, accountAccess) {
4933
5162
  };
4934
5163
  }
4935
5164
  //#endregion
5165
+ //#region apps/cli/src/journey/works/recover-isolated.ts
5166
+ /** Match the entire original family partition, including other isolated siblings of this parent. */
5167
+ function approvedItem(state, child, items) {
5168
+ const matches = items.filter((item) => child.rootSessionIds.every((id) => item.rootSessionIds.includes(id)));
5169
+ const item = matches[0];
5170
+ if (matches.length !== 1 || !item) throw new ApplicationError("INVALID_SELECTION");
5171
+ const parents = state.works.filter((work) => work.projectName === item.projectName && work.description === item.description && work.classificationRecommendation?.access === item.recommendation.access && work.classificationRecommendation.reason === item.recommendation.reason);
5172
+ const parent = parents[0];
5173
+ if (parents.length !== 1 || !parent) throw new ApplicationError("INVALID_SELECTION");
5174
+ const siblingIds = new Set(Array.from({ length: state.works.length + 1 }, (_, i) => isolatedWorkId(parent.id, i)));
5175
+ if (!siblingIds.has(child.id)) throw new ApplicationError("INVALID_SELECTION");
5176
+ const roots = state.works.filter((w) => w.id === parent.id || siblingIds.has(w.id)).flatMap((w) => w.rootSessionIds);
5177
+ if (roots.length !== item.rootSessionIds.length || new Set(roots).size !== roots.length || !roots.every((root) => item.rootSessionIds.includes(root))) throw new ApplicationError("INVALID_SELECTION");
5178
+ return item;
5179
+ }
5180
+ /** Repair the missing classification only, using its immutable receipt and the ordinary static metadata policy. */
5181
+ function createIsolatedMetadataRecovery(storage, processing) {
5182
+ return async (input, signal) => {
5183
+ const { proposal, workIds, idempotencyKey } = input;
5184
+ const journeyId = proposal.journeyId;
5185
+ const hash = textDigest(JSON.stringify(input));
5186
+ const state = await storage.current();
5187
+ if (!state || state.journeyId !== journeyId) throw new ApplicationError("UNKNOWN_ITEM");
5188
+ if (await storage.receiptRevision({
5189
+ journeyId,
5190
+ key: idempotencyKey,
5191
+ hash
5192
+ }) !== null) return {
5193
+ status: "replayed",
5194
+ state
5195
+ };
5196
+ const accepted = await storage.receiptRevision({
5197
+ journeyId,
5198
+ key: proposal.idempotencyKey,
5199
+ hash: textDigest(JSON.stringify(proposal))
5200
+ });
5201
+ if (accepted === null || accepted !== state.classificationRevision) throw new ApplicationError("REVISION_CONFLICT");
5202
+ if (!state.consent || state.scope.status !== "confirmed" || state.abandonedAt || !state.readingComplete || state.pendingHideId || !["ready", "review"].includes(state.phase)) throw new ApplicationError("SCOPE_NOT_READY");
5203
+ if (new Set(workIds).size !== workIds.length || proposal.pendingRootSessionIds?.length) throw new ApplicationError("INVALID_SELECTION");
5204
+ const plan = (await storage.resources({
5205
+ journeyId,
5206
+ kind: "plan",
5207
+ id: "plan"
5208
+ }))[0];
5209
+ if (plan?.kind !== "plan") throw new ApplicationError("SCOPE_NOT_READY");
5210
+ const recovered = /* @__PURE__ */ new Map();
5211
+ const families = groupedClassificationItems(proposal.items);
5212
+ for (const id of workIds) {
5213
+ signal.throwIfAborted();
5214
+ const work = state.works.find((w) => w.id === id);
5215
+ if (!work || !work.createdByPlacement || work.placement !== "placed" || work.description || work.classificationRecommendation || work.suggestion || work.explicitTitleId || work.titleOrigin !== "model" || !work.rootSessionIds.length || !work.accessConfirmed || work.access === "drop" || work.status !== "review" || !work.artifact || work.artifact.revision !== work.revision || work.completedBatches !== work.totalBatches) throw new ApplicationError("INVALID_SELECTION");
5216
+ const item = approvedItem(state, work, families);
5217
+ const restored = {
5218
+ ...work,
5219
+ description: item.description,
5220
+ classificationRecommendation: item.recommendation,
5221
+ reason: item.recommendation.reason,
5222
+ projectType: work.projectType ?? {
5223
+ type: item.projectType,
5224
+ reason: item.recommendation.reason,
5225
+ origin: "model"
5226
+ },
5227
+ artifact: null,
5228
+ revision: work.revision + 1
5229
+ };
5230
+ const metadata = await processedClassificationMetadata(storage, state, restored, processing, signal, plan);
5231
+ if (!metadata || !hasCompleteWorkDescription(restored)) throw new ApplicationError("INVALID_SELECTION");
5232
+ recovered.set(id, {
5233
+ ...restored,
5234
+ classificationMetadata: metadata
5235
+ });
5236
+ }
5237
+ signal.throwIfAborted();
5238
+ return storage.commit({
5239
+ expectedRevision: state.revision,
5240
+ requestKey: idempotencyKey,
5241
+ requestHash: hash,
5242
+ state: {
5243
+ ...state,
5244
+ phase: "review",
5245
+ works: state.works.map((w) => recovered.get(w.id) ?? w),
5246
+ privacy: {
5247
+ ...state.privacy,
5248
+ packageApproval: null
5249
+ }
5250
+ },
5251
+ event: {
5252
+ journeyId,
5253
+ kind: "snapshot",
5254
+ source: "system",
5255
+ at: (/* @__PURE__ */ new Date()).toISOString()
5256
+ }
5257
+ });
5258
+ };
5259
+ }
5260
+ //#endregion
4936
5261
  //#region apps/cli/src/journey/works/catalog-project-evidence.ts
4937
5262
  /** Collects every unresolved root's frozen structural evidence with bounded I/O fan-out. */
4938
5263
  async function collectCatalogProjectEvidence(options, signal) {
@@ -5276,6 +5601,7 @@ function createJourneyClassification(storage, _processing) {
5276
5601
  /** The journey local port's project catalog and project-type suggestions. */
5277
5602
  function createJourneyWorks(storage, processing, stateDir, accountAccess) {
5278
5603
  return {
5604
+ recoverIsolatedMetadata: createIsolatedMetadataRecovery(storage, processing),
5279
5605
  classificationSessions: createClassificationSessions(storage, processing, stateDir),
5280
5606
  classificationRead: createClassificationReader(storage, stateDir),
5281
5607
  classificationSubmit: createClassificationProposal(storage, accountAccess),
@@ -5326,7 +5652,7 @@ function createJourneyPreview(storage) {
5326
5652
  const title = protectedSourceText({
5327
5653
  text: cachedTitle.text,
5328
5654
  protectedRanges: []
5329
- }, cachedTitle.semantic.spans);
5655
+ }, cachedTitle.semantic.spans, "[hidden]", false);
5330
5656
  await scanViewFence(storage, state, work, sha256);
5331
5657
  return {
5332
5658
  workId: work.id,
@@ -5358,16 +5684,16 @@ async function* conversationFields(fields) {
5358
5684
  }
5359
5685
  }
5360
5686
  /** Static detector spans cannot be restored by a semantic Keep or exception. */
5361
- function protectedSourceText(field, hidden) {
5687
+ function protectedSourceText(field, hidden, replacement = "[Hidden]", joinAdjacent = true) {
5362
5688
  const merged = [];
5363
5689
  for (const range of [...field.protectedRanges, ...hidden].sort((a, b) => a.start - b.start || b.end - a.end)) {
5364
5690
  if (range.start < 0 || range.end > field.text.length || range.end <= range.start) throw new ApplicationError("CORRUPTED_STATE");
5365
5691
  const prior = merged.at(-1);
5366
- if (prior && range.start <= prior.end) prior.end = Math.max(prior.end, range.end);
5692
+ if (prior && (range.start < prior.end || joinAdjacent && range.start === prior.end)) prior.end = Math.max(prior.end, range.end);
5367
5693
  else merged.push({ ...range });
5368
5694
  }
5369
5695
  let text = field.text;
5370
- for (const range of merged.toReversed()) text = text.slice(0, range.start) + "[Hidden]" + text.slice(range.end);
5696
+ for (const range of merged.toReversed()) text = text.slice(0, range.start) + replacement + text.slice(range.end);
5371
5697
  return text;
5372
5698
  }
5373
5699
  //#endregion
@@ -5394,6 +5720,8 @@ const packagingRefusalCodes = [
5394
5720
  */
5395
5721
  const finalPackagingCodes = [
5396
5722
  ...packagingRefusalCodes,
5723
+ "WORK_METADATA_INCOMPLETE",
5724
+ "PACKAGING_FAILED",
5397
5725
  ...rootScanRefusals,
5398
5726
  "ARTIFACT_ARCHIVE_LIMIT",
5399
5727
  "ARTIFACT_ENTRY_LIMIT",
@@ -5429,22 +5757,45 @@ function createSendPackager(ports) {
5429
5757
  current = await ports.current(journeyId);
5430
5758
  if (!current) throw new ApplicationError("UNKNOWN_ITEM");
5431
5759
  }
5432
- const ready = (works, id) => works.some((work) => work.id === id && packaged(work));
5433
- const batch = workIds.filter((id) => !ready(current.works, id));
5760
+ const ready = new Set(current.works.filter(packaged).map((work) => work.id));
5761
+ const batch = workIds.filter((id) => !ready.has(id));
5434
5762
  if (!batch.length) return true;
5763
+ const selected = new Set(batch);
5764
+ if (current.works.some((work) => selected.has(work.id) && !hasCompleteWorkDescription(work))) throw new ApplicationError("WORK_METADATA_INCOMPLETE");
5435
5765
  signal.throwIfAborted();
5436
5766
  const result = await ports.render({
5437
5767
  journeyId,
5438
5768
  workIds: batch
5439
5769
  }, signal);
5440
- const failures = result.renders?.filter((item) => item.status === "failed") ?? [];
5441
- const failure = failures.find((item) => packagingRefusalCodes.includes(item.code)) ?? failures.find((item) => finalPackagingCodes.includes(item.code)) ?? failures[0];
5442
- if (failure) throw new ApplicationError(failure.code);
5443
- if (batch.some((id) => !ready(result.works, id))) throw new Error("PACKAGING_FAILED");
5770
+ const works = new Map(result.works.map((work) => [work.id, work]));
5771
+ const failures = new Map(result.renders?.filter((item) => item.status === "failed").map((item) => [item.workId, item.code]));
5772
+ for (const id of batch) {
5773
+ const work = works.get(id);
5774
+ if ((!work || !packaged(work)) && !failures.has(id)) failures.set(id, unfinishedPackagingCode(work));
5775
+ }
5776
+ const codes = [...failures.values()];
5777
+ const failure = codes.find((code) => packagingRefusalCodes.includes(code)) ?? codes.find((code) => finalPackagingCodes.includes(code)) ?? codes[0];
5778
+ if (failure) throw new ApplicationError(failure);
5444
5779
  return false;
5445
5780
  };
5446
5781
  }
5447
5782
  //#endregion
5783
+ //#region apps/cli/src/package-identity.ts
5784
+ /** Release identity stays independent of producer/processing format versions. */
5785
+ const packageIdentity = Object.freeze(z.object({
5786
+ name: z.literal("@gitroll/tacitt-cli"),
5787
+ version: z.string().regex(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/)
5788
+ }).parse(JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"))));
5789
+ //#endregion
5790
+ //#region apps/cli/skills/tacitt/scripts/journey-panel-host.mjs
5791
+ /** Only the host's canonical browser bootstrap may be handed to a fresh browser. */
5792
+ function focusedPanelUrl(value, journeyId) {
5793
+ if (typeof value !== "string") return null;
5794
+ const url = URL.parse(value);
5795
+ if (!url || url.href !== value || url.protocol !== "http:" || url.hostname !== "127.0.0.1" || !url.port || url.username || url.password || !/^\/session-[a-f0-9]{16}\/$/u.test(url.pathname) || url.searchParams.size !== 2 || url.searchParams.get("view") !== "skill" || url.searchParams.get("journey") !== journeyId || !/^#token=[A-Za-z0-9_-]{43}$/u.test(url.hash)) return null;
5796
+ return url;
5797
+ }
5798
+ //#endregion
5448
5799
  //#region apps/cli/src/serve-process.ts
5449
5800
  const execute = promisify(execFile);
5450
5801
  /** OS process creation identity, independent of PID recycling. */
@@ -5509,8 +5860,14 @@ const recordSchema$1 = z.object({
5509
5860
  journeyId: z.string(),
5510
5861
  pid: z.number().int().positive(),
5511
5862
  log: z.string(),
5863
+ version: z.string().optional(),
5864
+ currentKeepAuthority: z.boolean().optional(),
5865
+ outputVersion: z.number().int().optional(),
5512
5866
  processStart: z.string().optional()
5513
5867
  });
5868
+ function enforcesPrivacy(record) {
5869
+ return record?.currentKeepAuthority && record.outputVersion === 1;
5870
+ }
5514
5871
  const recordPath$1 = (directory, journeyId) => join(directory, `${journeyId}.host.json`);
5515
5872
  function running(pid) {
5516
5873
  try {
@@ -5521,19 +5878,18 @@ function running(pid) {
5521
5878
  }
5522
5879
  }
5523
5880
  /** Only verified absence admits a replacement; transport failure is inconclusive. */
5524
- async function liveHost(directory, journeyId) {
5881
+ async function liveHost(directory, journeyId, strictRecord = false) {
5525
5882
  let record;
5526
5883
  try {
5527
5884
  const parsed = recordSchema$1.safeParse(JSON.parse(await readFile(recordPath$1(directory, journeyId), "utf8")));
5528
- if (!parsed.success || parsed.data.journeyId !== journeyId) return { status: "dead" };
5885
+ if (!parsed.success || parsed.data.journeyId !== journeyId) return { status: strictRecord ? "unknown" : "dead" };
5529
5886
  record = parsed.data;
5530
5887
  } catch (error) {
5531
- if (error instanceof SyntaxError || error instanceof Error && "code" in error && error.code === "ENOENT") return { status: "dead" };
5888
+ if (!strictRecord && error instanceof SyntaxError || error instanceof Error && "code" in error && error.code === "ENOENT") return { status: "dead" };
5532
5889
  process.stderr.write("HOST_RECORD_UNAVAILABLE\n");
5533
5890
  return { status: "unknown" };
5534
5891
  }
5535
- const state = running(record.pid);
5536
- if (state === "dead") return { status: "dead" };
5892
+ if (running(record.pid) === "dead") return { status: "dead" };
5537
5893
  if (record.processStart !== void 0) try {
5538
5894
  if (await processStart(record.pid) !== record.processStart) return { status: "dead" };
5539
5895
  } catch {
@@ -5543,19 +5899,37 @@ async function liveHost(directory, journeyId) {
5543
5899
  record
5544
5900
  };
5545
5901
  }
5546
- else if (state === "unknown") return { status: "dead" };
5547
5902
  if (!record.url) return {
5548
5903
  status: "unknown",
5549
5904
  record
5550
5905
  };
5906
+ if (!record.processStart) return {
5907
+ status: "unknown",
5908
+ record
5909
+ };
5910
+ if (record.version !== packageIdentity.version || !enforcesPrivacy(record)) return {
5911
+ status: "obsolete",
5912
+ record
5913
+ };
5914
+ const url = focusedPanelUrl(record.url, journeyId);
5915
+ if (!url) return {
5916
+ status: "obsolete",
5917
+ record
5918
+ };
5919
+ url.hash = "";
5551
5920
  try {
5552
- await fetch(record.url, { signal: AbortSignal.timeout(2e3) });
5553
- return {
5921
+ return (await fetch(url, {
5922
+ method: "HEAD",
5923
+ redirect: "manual",
5924
+ signal: AbortSignal.timeout(2e3)
5925
+ })).ok ? {
5554
5926
  status: "alive",
5555
5927
  record
5928
+ } : {
5929
+ status: "unknown",
5930
+ record
5556
5931
  };
5557
- } catch (error) {
5558
- if (record.processStart === void 0 && loopbackRefused(record.url, error)) return { status: "dead" };
5932
+ } catch {
5559
5933
  process.stderr.write("HOST_PROBE_UNAVAILABLE\n");
5560
5934
  return {
5561
5935
  status: "unknown",
@@ -5563,16 +5937,57 @@ async function liveHost(directory, journeyId) {
5563
5937
  };
5564
5938
  }
5565
5939
  }
5566
- function loopbackRefused(url, error) {
5567
- const host = URL.parse(url)?.hostname;
5568
- if (host !== "127.0.0.1" && host !== "[::1]" && host !== "localhost") return false;
5569
- if (!(error instanceof Error)) return false;
5570
- if ("code" in error && error.code === "ECONNREFUSED") return true;
5571
- if (error instanceof AggregateError) return error.errors.length > 0 && error.errors.every((item) => loopbackRefused(url, item));
5572
- return loopbackRefused(url, error.cause);
5940
+ /** Called only while the launch mutex is held. Never start alongside an old owner. */
5941
+ async function relinquish(record) {
5942
+ const sameOwner = async () => running(record.pid) !== "dead" && await processStart(record.pid) === record.processStart;
5943
+ try {
5944
+ if (!await sameOwner()) return true;
5945
+ process.kill(record.pid, "SIGTERM");
5946
+ const deadline = Date.now() + 1e4;
5947
+ while (Date.now() < deadline) {
5948
+ if (!await sameOwner()) return true;
5949
+ await setTimeout$1(100);
5950
+ }
5951
+ } catch {
5952
+ if (running(record.pid) === "dead") return true;
5953
+ }
5954
+ process.stderr.write("HOST_REPLACEMENT_PENDING: the previous panel host has not confirmed shutdown. Retry the handoff.\n");
5955
+ return false;
5956
+ }
5957
+ /** A previous sender cannot observe a new policy by reusing its cached approval.
5958
+ * Only a live, PID-and-birth-verified owner may be signalled. */
5959
+ async function fenceUnapprovedSendHost(stateDir, journey) {
5960
+ if (journey?.phase !== "sending" || journey.privacy.packageApproval !== null) return;
5961
+ const directory = join(stateDir, "serve");
5962
+ const status = await liveHost(directory, journey.journeyId, true);
5963
+ if (status.status === "dead" || enforcesPrivacy(status.record)) return;
5964
+ await withStateMutex(directory, async () => {
5965
+ const current = await liveHost(directory, journey.journeyId, true);
5966
+ if (current.status === "dead" || enforcesPrivacy(current.record)) return;
5967
+ if (current.status !== "obsolete" || !await relinquish(current.record)) throw new ApplicationError("HOST_REPLACEMENT_PENDING");
5968
+ });
5969
+ }
5970
+ /** Best-effort cleanup before an obsolete journey can lose its current ID. */
5971
+ async function relinquishObsoleteHost(stateDir, journeyId) {
5972
+ const directory = join(stateDir, "serve");
5973
+ try {
5974
+ await readFile(recordPath$1(directory, journeyId), "utf8");
5975
+ await withStateMutex(directory, async () => {
5976
+ const current = await liveHost(directory, journeyId);
5977
+ if (current.status === "obsolete" && current.record.version !== packageIdentity.version) await relinquish(current.record);
5978
+ });
5979
+ } catch (error) {
5980
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
5981
+ process.stderr.write("HOST_REPLACEMENT_PENDING: obsolete journey host cleanup failed.\n");
5982
+ }
5573
5983
  }
5574
5984
  function reuse(existing) {
5575
5985
  if (existing.status === "dead") return false;
5986
+ if (existing.status === "obsolete") {
5987
+ process.stderr.write("HOST_REPLACEMENT_PENDING: retry the handoff.\n");
5988
+ process.exitCode = 1;
5989
+ return true;
5990
+ }
5576
5991
  if (existing.status === "alive") process.stdout.write(`${JSON.stringify({
5577
5992
  ok: true,
5578
5993
  event: "ready",
@@ -5634,7 +6049,12 @@ async function serveDetached(args, stateDir, journeyId) {
5634
6049
  recursive: true,
5635
6050
  force: true
5636
6051
  });
5637
- if (reuse(current)) return;
6052
+ if (current.status === "obsolete") {
6053
+ if (!await relinquish(current.record)) {
6054
+ process.exitCode = 1;
6055
+ return;
6056
+ }
6057
+ } else if (reuse(current)) return;
5638
6058
  let retries = 0;
5639
6059
  let delay = 500;
5640
6060
  for (;;) {
@@ -5698,6 +6118,11 @@ async function startHost(args, directory, journeyId, deadline) {
5698
6118
  await output.close();
5699
6119
  throw error;
5700
6120
  }
6121
+ let ready;
6122
+ child.on("message", (message) => {
6123
+ const parsed = readySchema.safeParse(message);
6124
+ if (parsed.success) ready ??= parsed.data.result;
6125
+ });
5701
6126
  let exited = null;
5702
6127
  child.once("exit", (code) => exited = code ?? 1);
5703
6128
  let spawnFailure;
@@ -5713,6 +6138,9 @@ async function startHost(args, directory, journeyId, deadline) {
5713
6138
  identity = await processStart(child.pid);
5714
6139
  await publishRecord(directory, id, {
5715
6140
  url: "",
6141
+ version: packageIdentity.version,
6142
+ currentKeepAuthority: true,
6143
+ outputVersion: 1,
5716
6144
  journeyId,
5717
6145
  pid: child.pid,
5718
6146
  processStart: identity,
@@ -5740,7 +6168,8 @@ async function startHost(args, directory, journeyId, deadline) {
5740
6168
  deadline
5741
6169
  }, () => ({
5742
6170
  exited,
5743
- spawnFailure
6171
+ spawnFailure,
6172
+ ready
5744
6173
  }));
5745
6174
  } finally {
5746
6175
  if (child.connected) child.disconnect();
@@ -5753,29 +6182,27 @@ async function publishRecord(directory, id, record) {
5753
6182
  }
5754
6183
  async function readStartup(log) {
5755
6184
  let busy = false;
5756
- let ready;
5757
6185
  for (const line of (await readFile(log, "utf8")).split("\n")) try {
5758
6186
  const parsed = JSON.parse(line);
5759
6187
  const failure = failureSchema.safeParse(parsed);
5760
6188
  if (failure.success) busy = failure.data.error.code === "COORDINATION_BUSY";
5761
- const value = readySchema.safeParse(parsed);
5762
- if (value.success) ready ??= value.data.result;
5763
6189
  } catch {}
5764
- return {
5765
- busy,
5766
- ready
5767
- };
6190
+ return { busy };
5768
6191
  }
5769
6192
  async function waitForReady(child, startup, state) {
5770
6193
  let busy = false;
5771
6194
  while (Date.now() < startup.deadline) {
5772
6195
  const failure = state().spawnFailure;
5773
6196
  if (failure) throw new Error("HOST_START_FAILED", { cause: failure });
5774
- const observed = await readStartup(startup.log);
5775
- busy = observed.busy;
5776
- if (observed.ready?.journeyId === startup.journeyId && child.pid !== void 0) {
6197
+ busy = (await readStartup(startup.log)).busy;
6198
+ const ready = state().ready;
6199
+ if (ready?.journeyId === startup.journeyId && child.pid !== void 0) {
6200
+ if (!focusedPanelUrl(ready.url, startup.journeyId)) throw new Error("INVALID_PANEL_URL");
5777
6201
  const record = {
5778
- ...observed.ready,
6202
+ ...ready,
6203
+ version: packageIdentity.version,
6204
+ currentKeepAuthority: true,
6205
+ outputVersion: 1,
5779
6206
  pid: child.pid,
5780
6207
  processStart: startup.identity,
5781
6208
  log: startup.log
@@ -5860,357 +6287,61 @@ async function readSendHost(stateDir) {
5860
6287
  }
5861
6288
  }
5862
6289
  //#endregion
5863
- //#region apps/cli/src/account/journey/remote-error.ts
5864
- const refusalSchema = z.object({
5865
- error: z.string(),
5866
- preview: sendSettingsSchema.nullable().optional(),
5867
- resolution: sendResolutionSchema.optional(),
5868
- /** `unknown_keep_work_ids`: the kept IDs Passport did not accept (ADR 0204). */
5869
- workIds: z.array(z.string()).optional()
5870
- });
5871
- var JourneyRemoteError = class extends Error {
5872
- retryAfter;
5873
- status;
5874
- code;
5875
- details;
5876
- constructor(status, code, details, options, retryAfter) {
5877
- super(code, options);
5878
- this.name = "JourneyRemoteError";
5879
- this.retryAfter = retryAfter;
5880
- this.status = status;
5881
- this.code = code;
5882
- this.details = details;
5883
- }
5884
- };
5885
- //#endregion
5886
- //#region apps/cli/src/account/journey/session-transport.ts
5887
- /** Session requests allow ten minutes for response headers and response-body inactivity. */
5888
- const sessionRequestTimeoutMs = 6e5;
5889
- /**
5890
- * Node 24 bundles undici but does not expose its Agent as a public built-in module.
5891
- * Reuse its dispatcher through the versioned compatibility symbol, without changing
5892
- * global transport policy. Constructing Request initializes undici without any I/O.
5893
- * Fail closed if a future runtime removes this bridge; never fall back to five minutes.
5894
- */
5895
- function runtimeSessionDispatcher() {
5896
- new Request("https://session-transport.invalid");
5897
- const dispatcher = Reflect.get(globalThis, Symbol.for("undici.globalDispatcher.1"));
5898
- if (typeof dispatcher !== "object" || dispatcher === null || !("dispatch" in dispatcher) || typeof dispatcher.dispatch !== "function") throw new ApplicationError("SESSION_TRANSPORT_UNAVAILABLE");
5899
- const dispatch = dispatcher.dispatch.bind(dispatcher);
5900
- return { dispatch(options, handler) {
5901
- return dispatch(options, handler);
5902
- } };
5903
- }
5904
- /** Per-request overrides reach undici's parser; AbortSignal alone does not change these. */
5905
- function sessionDispatcher(delegate = runtimeSessionDispatcher(), timeoutMs = sessionRequestTimeoutMs) {
5906
- return { dispatch(options, handler) {
5907
- return delegate.dispatch({
5908
- ...options,
5909
- headersTimeout: timeoutMs,
5910
- bodyTimeout: timeoutMs
5911
- }, handler);
5912
- } };
6290
+ //#region apps/cli/src/journey/send-extension.ts
6291
+ function selectionKey({ output, extension, identities }) {
6292
+ return hash$1([
6293
+ output.journeyId,
6294
+ output.workId,
6295
+ output.primary.sha256,
6296
+ output.finalMaskLedger.sha256,
6297
+ identities,
6298
+ extension
6299
+ ]);
5913
6300
  }
5914
- //#endregion
5915
- //#region apps/cli/src/journey/send-sessions.ts
5916
- /** Build upload identities from frozen local metadata and bytes from the final primary only. */
5917
- async function* sessionArtifacts(storage, find, journeyId, artifact, signal) {
5918
- const output = await find(journeyId, artifact.workId, artifact.revision);
5919
- if (output.bundleId !== artifact.bundleId || output.primary.sha256 !== artifact.sha256 || output.primary.archiveBytes !== artifact.byteLength) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
5920
- const base = (await storage.resources({
5921
- journeyId,
5922
- kind: "base"
5923
- })).find((item) => item.kind === "base" && item.workId === artifact.workId && item.inputRevision === output.finalMaskLedger.inputRevision && item.primary.sha256 === output.finalMaskLedger.basePrimarySha256);
5924
- const plan = (await storage.resources({
5925
- journeyId,
5926
- kind: "plan"
5927
- })).find((item) => item.kind === "plan");
5928
- if (base?.kind !== "base" || plan?.kind !== "plan") throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
5929
- const identities = base.sessions.map((binding) => {
5930
- const session = plan.approval.inventory.sessions.find((item) => item.id === binding.applicationSessionId);
5931
- const part = session?.parts[0];
5932
- const lastAt = session?.lastInteractionAt ?? session?.createdAt;
5933
- if (!session || !part || !lastAt || !Number.isFinite(Date.parse(lastAt))) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
5934
- const nativeId = part.nativeEvidence?.identity?.sessionId ?? session.sourceSessionId;
5935
- const path = part.harnessRelativePath ?? "";
5936
- if (!path && !nativeId?.trim() || isAbsolute(path) || path.includes("\\") || path.split("/").some((segment) => segment === ".." || segment === ".")) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
5937
- return {
5938
- archiveSessionId: binding.archiveSessionId,
5939
- relativePath: path,
5940
- lastAt: new Date(lastAt).toISOString()
5941
- };
5942
- });
5943
- for await (const session of configuredSessionUploads(output.primary, identities, signal)) yield {
5944
- ...session,
5945
- async upload(upload, uploadSignal, onProgress) {
5946
- await find(journeyId, artifact.workId, artifact.revision);
5947
- const url = new URL(upload.uploadUrl);
5948
- if (url.protocol !== "https:" || url.username || url.password || url.hash) throw new ApplicationError("INVALID_ARGUMENTS");
5949
- const dispatcher = sessionDispatcher();
5950
- let bytes = 0;
5951
- const progressFailure = {};
5952
- const request = {
5953
- method: "PUT",
5954
- body: session.stream().pipeThrough(new TransformStream({ async transform(chunk, controller) {
5955
- bytes += chunk.byteLength;
5956
- try {
5957
- await onProgress?.(bytes);
5958
- } catch (cause) {
5959
- progressFailure.error = { cause };
5960
- throw cause;
5961
- }
5962
- controller.enqueue(chunk);
5963
- } })),
5964
- signal: uploadSignal,
5965
- dispatcher,
5966
- redirect: "error",
5967
- credentials: "omit",
5968
- referrerPolicy: "no-referrer",
5969
- headers: {
5970
- "content-type": "application/zip",
5971
- "content-length": String(session.byteLength),
5972
- ...upload.headers
5973
- },
5974
- duplex: "half"
5975
- };
5976
- const response = await fetch(url, request).catch((error) => {
5977
- if (progressFailure.error) throw progressFailure.error.cause;
5978
- throw error;
5979
- });
5980
- await response.body?.cancel();
5981
- if (!response.ok) throw new JourneyRemoteError(response.status, "UPLOAD_FAILED", null, void 0, response.headers.get("retry-after") ?? void 0);
5982
- }
5983
- };
5984
- }
5985
- /** Own one live upload iterator; advancing it releases the preceding session ZIP. */
5986
- async function prepareSessionArtifacts(storage, find, journeyId, artifact, signal) {
5987
- const source = sessionArtifacts(storage, find, journeyId, artifact, signal);
5988
- let closed = false;
5989
- async function* iterate() {
5990
- try {
5991
- yield* source;
5992
- } finally {
5993
- closed = true;
5994
- }
5995
- }
5996
- const items = iterate();
5997
- return {
5998
- items,
5999
- async close() {
6000
- if (closed) return;
6001
- closed = true;
6002
- await items.return(void 0);
6003
- }
6004
- };
6005
- }
6006
- //#endregion
6007
- //#region apps/cli/src/journey/send-artifacts.ts
6008
- function createJourneySendArtifacts(storage) {
6009
- async function find(journeyId, workId, revision) {
6010
- const state = await storage.current(journeyId);
6011
- if (!state?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
6012
- assertJourneyPrivacyReady(state, true);
6013
- if (state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
6014
- const work = state.works.find((item) => item.id === workId);
6015
- if (!work?.artifact || work.revision !== revision || work.artifact.revision !== revision) throw new ApplicationError("REVISION_CONFLICT");
6016
- const output = (await storage.resources({
6017
- journeyId,
6018
- kind: "output"
6019
- })).find((item) => item.kind === "output" && item.workId === workId && item.revision === revision);
6020
- if (output?.kind !== "output" || output.primary.sha256 !== work.artifact.sha256) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
6021
- return output;
6022
- }
6023
- return {
6024
- prepareSessions: (journeyId, artifact, signal) => prepareSessionArtifacts(storage, find, journeyId, artifact, signal),
6025
- sessions: (journeyId, artifact, signal) => sessionArtifacts(storage, find, journeyId, artifact, signal),
6026
- async describe(journey, work, signal) {
6027
- const output = await find(journey.journeyId, work.id, work.revision);
6028
- const info = await readConfiguredOutputInfo(output.primary, signal);
6029
- if (info.work.id !== work.id || info.work.access !== work.access) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
6030
- let agentSources = [];
6031
- try {
6032
- const base = (await storage.resources({
6033
- journeyId: journey.journeyId,
6034
- kind: "base"
6035
- })).find((item) => item.kind === "base" && item.workId === work.id && item.inputRevision === output.finalMaskLedger.inputRevision && item.primary.sha256 === output.finalMaskLedger.basePrimarySha256);
6036
- if (base?.kind === "base") {
6037
- const included = new Set(base.sessions.map((item) => item.applicationSessionId));
6038
- const resources = await storage.resources({
6039
- journeyId: journey.journeyId,
6040
- kind: "static-session"
6041
- });
6042
- const unique = /* @__PURE__ */ new Map();
6043
- for (const resource of resources) {
6044
- if (resource.kind !== "static-session" || resource.workId !== work.id || resource.inputRevision !== base.inputRevision || !included.has(resource.sessionId)) continue;
6045
- for (const candidate of resource.agentSources ?? []) {
6046
- const parsed = agentSourceSchema.safeParse(candidate);
6047
- if (parsed.success && unique.size < 500) unique.set(JSON.stringify(parsed.data), parsed.data);
6048
- }
6049
- }
6050
- agentSources = [...unique.values()];
6051
- }
6052
- } catch {
6053
- console.error("AGENT_SOURCES_METADATA_UNAVAILABLE");
6054
- }
6055
- return sendArtifactSchema.parse({
6056
- ...agentSources.length === 0 ? {} : { agentSources },
6057
- workId: work.id,
6301
+ /** Sender-local cache. Its only values are archive IDs already covered by a scope-bound proof. */
6302
+ function createSendExtensionSelection() {
6303
+ const cache = /* @__PURE__ */ new Map();
6304
+ return async function select(storage, descriptor, signal) {
6305
+ const { output, identities, extension } = descriptor;
6306
+ if (!extension) return /* @__PURE__ */ new Set();
6307
+ const key = selectionKey(descriptor);
6308
+ const prior = cache.get(key);
6309
+ if (prior) return prior;
6310
+ const bindings = /* @__PURE__ */ new Map();
6311
+ for (const binding of extension.sources) {
6312
+ const location = JSON.stringify([binding.session, binding.source]);
6313
+ if (bindings.has(location)) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
6314
+ bindings.set(location, binding.part);
6315
+ }
6316
+ const dates = new Map(identities.map((item) => [item.archiveSessionId, item.lastAt]));
6317
+ const proof = await sessionExtensions({
6318
+ primary: output.primary,
6319
+ ledger: output.finalMaskLedger,
6320
+ groups: extension.groups,
6321
+ sourceFor: (session, source) => bindings.get(JSON.stringify([session, source])),
6322
+ lastAt: (session) => dates.get(session) ?? ""
6323
+ }, signal);
6324
+ if (!proof) return /* @__PURE__ */ new Set();
6325
+ if (proof.retired.size) for (const work of extension.outputs) {
6326
+ const { output: candidate } = await storage.readSendOutput({
6327
+ journeyId: output.journeyId,
6328
+ workId: work.workId,
6058
6329
  revision: work.revision,
6059
- bundleId: info.manifest.bundleId,
6060
- createdAt: info.createdAt,
6061
- title: info.work.title,
6062
- sessionCount: info.sessionCount,
6063
- grouping: info.manifest.work.grouping,
6064
- sources: info.sources,
6065
- byteLength: output.primary.archiveBytes,
6066
- sha256: output.primary.sha256
6330
+ sessions: false
6067
6331
  });
6068
- },
6069
- async upload(journeyId, artifact, upload, signal) {
6070
- const output = await find(journeyId, artifact.workId, artifact.revision);
6071
- if (output.bundleId !== artifact.bundleId || output.primary.sha256 !== artifact.sha256 || output.primary.archiveBytes !== artifact.byteLength || artifact.byteLength > 5368709120) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
6072
- await putPrimary(output.primary, artifact.byteLength, upload, signal);
6073
- }
6074
- };
6075
- }
6076
- /**
6077
- * Stream a verified primary to a signed HTTPS URL; shared by native Send and Manual upload. A
6078
- * checksum upload also sends the SHA-256 header the URL signs, so storage refuses other bytes.
6079
- */
6080
- async function putPrimary(primary, byteLength, upload, signal) {
6081
- const url = new URL(upload.uploadUrl);
6082
- if (url.protocol !== "https:" || url.username || url.password || url.hash) throw new ApplicationError("INVALID_ARGUMENTS");
6083
- const source = await openConfiguredUpload(primary, signal);
6084
- const cleanup = createCleanup();
6085
- try {
6086
- const request = {
6087
- method: "PUT",
6088
- body: source.stream,
6089
- signal,
6090
- redirect: "error",
6091
- headers: {
6092
- "Content-Type": "application/zip",
6093
- "Content-Length": String(byteLength),
6094
- ...upload.headers
6095
- },
6096
- duplex: "half"
6097
- };
6098
- const result = await fetch(url, request);
6099
- await result.body?.cancel();
6100
- if (!result.ok) throw new ApplicationError("UPLOAD_FAILED");
6101
- } catch (error) {
6102
- cleanup.failed(error);
6103
- throw error;
6104
- } finally {
6105
- await cleanup.run(() => source.close());
6106
- }
6107
- }
6108
- //#endregion
6109
- //#region apps/cli/src/account/journey/session-facts-cache.ts
6110
- const keyFor = (journeyId, artifact) => JSON.stringify([
6111
- journeyId,
6112
- artifact.workId,
6113
- artifact.revision,
6114
- artifact.sha256
6115
- ]);
6116
- /** Sender-owned facts only. In-flight computations never enter the bounded cache. */
6117
- function createSessionFactsCache(limit = 256) {
6118
- const facts = /* @__PURE__ */ new Map();
6119
- let owner = "";
6120
- let generation = 0;
6121
- let allowed = /* @__PURE__ */ new Set();
6122
- return {
6123
- retain(journeyId, send) {
6124
- const nextOwner = JSON.stringify([journeyId, send?.id]);
6125
- const ended = !send || [
6126
- "completed",
6127
- "blocked",
6128
- "prepared"
6129
- ].includes(send.status);
6130
- if (owner !== nextOwner || ended) {
6131
- facts.clear();
6132
- generation += 1;
6133
- }
6134
- owner = nextOwner;
6135
- allowed = new Set(ended || !send ? [] : send.items.filter((item) => !item.sessionAssembly && item.status !== "completed").map((item) => keyFor(journeyId, item.artifact)));
6136
- for (const key of facts.keys()) if (!allowed.has(key)) facts.delete(key);
6137
- },
6138
- async get(journeyId, artifact, signal, compute) {
6139
- signal.throwIfAborted();
6140
- const key = keyFor(journeyId, artifact);
6141
- const cached = facts.get(key);
6142
- if (cached) return cached;
6143
- const started = generation;
6144
- const items = await compute();
6145
- signal.throwIfAborted();
6146
- if (started === generation && allowed.has(key)) {
6147
- facts.set(key, items);
6148
- while (facts.size > limit) {
6149
- const oldest = facts.keys().next().value;
6150
- if (oldest === void 0) break;
6151
- facts.delete(oldest);
6152
- }
6153
- }
6154
- return items;
6155
- }
6156
- };
6157
- }
6158
- //#endregion
6159
- //#region apps/cli/src/account/journey/session-preparation.ts
6160
- /** Shared facts retain no bytes; only upload steps acquire phase-local live iterators. */
6161
- function createSessionPreparation(dependencies, facts) {
6162
- const prepare = dependencies.artifacts.prepareSessions;
6163
- const prepared = [];
6164
- const sessions = async function* (journeyId, artifact, signal) {
6165
- if (!prepare) {
6166
- const original = dependencies.artifacts.sessions;
6167
- if (original) yield* original(journeyId, artifact, signal);
6168
- return;
6169
- }
6170
- const result = await prepare(journeyId, artifact, signal);
6171
- prepared.push(result);
6172
- for await (const item of result.items) {
6173
- signal.throwIfAborted();
6174
- yield item;
6175
- }
6176
- };
6177
- const sessionFacts = async function* (journeyId, artifact, signal) {
6178
- const items = await facts.get(journeyId, artifact, signal, async () => {
6179
- const original = dependencies.artifacts.sessionFacts ?? dependencies.artifacts.sessions;
6180
- const items = [];
6181
- if (original) for await (const item of original(journeyId, artifact, signal)) {
6182
- const { workerIdentity, nativeSessionId, relativePath, harness, lastAt, metadata, sha256, byteLength, crc32 } = item;
6183
- items.push({
6184
- ...workerIdentity ? { workerIdentity } : {},
6185
- nativeSessionId,
6186
- relativePath,
6187
- harness,
6188
- lastAt,
6189
- metadata,
6190
- sha256,
6191
- byteLength,
6192
- crc32
6193
- });
6194
- }
6195
- return items;
6196
- });
6197
- for (const item of items) {
6198
- signal.throwIfAborted();
6199
- yield item;
6200
- }
6201
- };
6202
- return {
6203
- dependencies: {
6204
- ...dependencies,
6205
- artifacts: {
6206
- ...dependencies.artifacts,
6207
- sessions,
6208
- sessionFacts
6209
- }
6210
- },
6211
- async close() {
6212
- await createCleanup().run(...prepared.map((result) => () => result.close()));
6332
+ if (candidate.primary.sha256 !== work.sha256 || candidate.bundleId !== work.bundleId) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
6333
+ const omitted = candidate.workId === output.workId ? proof.omitted : /* @__PURE__ */ new Set();
6334
+ if (await hasRetiredSessionMarker(candidate.primary, omitted, proof.retired, signal)) return /* @__PURE__ */ new Set();
6213
6335
  }
6336
+ if (selectionKey(await storage.readSendOutput({
6337
+ journeyId: output.journeyId,
6338
+ workId: output.workId,
6339
+ revision: output.revision,
6340
+ sessions: true
6341
+ })) !== key) throw new ApplicationError("REVISION_CONFLICT");
6342
+ if (cache.size >= 32) cache.clear();
6343
+ cache.set(key, proof.omitted);
6344
+ return proof.omitted;
6214
6345
  };
6215
6346
  }
6216
6347
  //#endregion
@@ -6279,15 +6410,86 @@ function createSessionKeys() {
6279
6410
  };
6280
6411
  }
6281
6412
  //#endregion
6282
- //#region apps/cli/src/account/journey/session-access.ts
6413
+ //#region apps/cli/src/account/journey/session-content-claims.ts
6283
6414
  /** Safe facts for a local preflight conflict; no paths or session content escape. */
6284
6415
  var SessionContentConflict = class extends ApplicationError {
6285
6416
  contentConflict;
6286
- constructor(contentConflict) {
6417
+ affectedWorkIds;
6418
+ constructor(contentConflict, affectedWorkIds) {
6287
6419
  super("SESSION_CONTENT_CONFLICT");
6288
6420
  this.contentConflict = contentConflict;
6421
+ this.affectedWorkIds = affectedWorkIds;
6289
6422
  }
6290
6423
  };
6424
+ function sessionContentClaims(items, inventory) {
6425
+ const requestedDigests = /* @__PURE__ */ new Map();
6426
+ const divergent = /* @__PURE__ */ new Set();
6427
+ const contentOwners = /* @__PURE__ */ new Map();
6428
+ const savedUploads = /* @__PURE__ */ new Map();
6429
+ const owners = /* @__PURE__ */ new Map();
6430
+ const holdings = /* @__PURE__ */ new Map();
6431
+ function hold(key, workId) {
6432
+ const byKey = owners.get(key) ?? /* @__PURE__ */ new Set();
6433
+ const byWork = holdings.get(workId) ?? /* @__PURE__ */ new Set();
6434
+ byKey.add(workId);
6435
+ byWork.add(key);
6436
+ owners.set(key, byKey);
6437
+ holdings.set(workId, byWork);
6438
+ }
6439
+ const byId = new Map(items.map((item) => [item.workId, item]));
6440
+ for (const work of inventory.works) if (byId.has(work.logicalWorkId)) for (const stored of work.sessions) hold(stored.key, work.logicalWorkId);
6441
+ for (const item of items) {
6442
+ if (item.sessionUpload) {
6443
+ hold(item.sessionUpload.key, item.workId);
6444
+ const claims = savedUploads.get(item.sessionUpload.key) ?? /* @__PURE__ */ new Map();
6445
+ claims.set(item.workId, item.sessionUpload.sha256);
6446
+ savedUploads.set(item.sessionUpload.key, claims);
6447
+ }
6448
+ for (const key of item.sessionMembership?.keys ?? []) hold(key, item.workId);
6449
+ }
6450
+ return {
6451
+ observe(key, sha256, item) {
6452
+ hold(key, item.workId);
6453
+ const requestedBy = contentOwners.get(key) ?? /* @__PURE__ */ new Set();
6454
+ requestedBy.add(item.workId);
6455
+ contentOwners.set(key, requestedBy);
6456
+ for (const [owner, sha] of savedUploads.get(key) ?? []) if (owner !== item.workId && byId.get(owner)?.access === item.access && sha !== sha256) divergent.add(key);
6457
+ const otherDigest = requestedDigests.get(key);
6458
+ if (otherDigest !== void 0 && otherDigest !== sha256) divergent.add(key);
6459
+ requestedDigests.set(key, sha256);
6460
+ },
6461
+ requireIndependent() {
6462
+ for (const [key, claims] of savedUploads) {
6463
+ for (const access of ["private", "public"]) if (new Set([...claims].filter(([id]) => byId.get(id)?.access === access).map(([, sha]) => sha)).size > 1) divergent.add(key);
6464
+ if (!divergent.has(key)) continue;
6465
+ const requestedBy = contentOwners.get(key) ?? /* @__PURE__ */ new Set();
6466
+ for (const owner of claims.keys()) requestedBy.add(owner);
6467
+ contentOwners.set(key, requestedBy);
6468
+ }
6469
+ const [first] = divergent;
6470
+ if (first) {
6471
+ const affected = /* @__PURE__ */ new Set();
6472
+ const pending = [...divergent];
6473
+ const visited = new Set(pending);
6474
+ for (const key of pending) for (const workId of owners.get(key) ?? []) {
6475
+ if (affected.has(workId)) continue;
6476
+ affected.add(workId);
6477
+ for (const owned of holdings.get(workId) ?? []) if (!visited.has(owned)) {
6478
+ visited.add(owned);
6479
+ pending.push(owned);
6480
+ }
6481
+ }
6482
+ if (items.some((item) => affected.has(item.workId) && !["blocked", "completed"].includes(item.status) && !item.sessionAssembly)) throw new SessionContentConflict({
6483
+ workIds: items.filter((item) => contentOwners.get(first)?.has(item.workId)).map((item) => item.workId),
6484
+ sessionKey: first
6485
+ }, affected);
6486
+ }
6487
+ }
6488
+ };
6489
+ }
6490
+ //#endregion
6491
+ //#region apps/cli/src/account/journey/session-access.ts
6492
+ const passportSessionPartByteLimit = 5 * 1024 ** 3 - 128 * 1024 ** 2;
6291
6493
  /** Collect safe conflicts across the entire pending Send before any session mutation. */
6292
6494
  async function sessionAccessBlocked(dependencies, journeyId, items, inventory, signal) {
6293
6495
  const sessions = dependencies.artifacts.sessionFacts ?? dependencies.artifacts.sessions;
@@ -6312,7 +6514,7 @@ async function sessionAccessBlocked(dependencies, journeyId, items, inventory, s
6312
6514
  conflicts.set(JSON.stringify(conflict), conflict);
6313
6515
  }
6314
6516
  const requestedAccess = /* @__PURE__ */ new Map();
6315
- const requestedDigests = /* @__PURE__ */ new Map();
6517
+ const content = sessionContentClaims(items, inventory);
6316
6518
  const contentAccess = /* @__PURE__ */ new Map();
6317
6519
  function claim(claims, identity, key, item, rule) {
6318
6520
  const previous = claims.get(identity) ?? [];
@@ -6343,26 +6545,28 @@ async function sessionAccessBlocked(dependencies, journeyId, items, inventory, s
6343
6545
  }
6344
6546
  }
6345
6547
  for (const item of items) {
6346
- if (item.sessionAssembly || item.sessionMembership || ["completed", "blocked"].includes(item.status)) continue;
6548
+ if (item.sessionAssembly || item.sessionMembership || item.status === "completed" || item.status === "blocked" && item.error !== "SESSION_CONTENT_CONFLICT") continue;
6347
6549
  const destination = inventory.works.find((work) => work.logicalWorkId === item.workId);
6348
6550
  if (destination?.access === "removed") throw new ApplicationError("WORK_REMOVED");
6349
6551
  if (destination) observe(item, destination.access);
6552
+ const selected = createSessionKeys();
6553
+ const measured = /* @__PURE__ */ new Set();
6554
+ let partBytes = 0;
6555
+ let partCount = 0;
6350
6556
  for await (const session of sessions(journeyId, item.artifact, signal)) {
6351
- const key = await sessionKeyFor(session);
6352
- const { workerIdentity: _, ...original } = session;
6353
- const legacyKey = session.workerIdentity ? await sessionKeyFor(original) : key;
6557
+ const identity = await selected.add(session);
6558
+ const key = identity.key;
6559
+ const legacyKey = identity.legacyKey ?? key;
6560
+ if (!measured.has(key)) {
6561
+ measured.add(key);
6562
+ partCount += session.metadata.parts.length;
6563
+ for (const part of session.metadata.parts) partBytes += part.byteLength;
6564
+ }
6565
+ content.observe(key, session.sha256, item);
6566
+ if (item.status === "blocked") continue;
6354
6567
  claimContent(key, session.sha256, item);
6355
6568
  if (legacyKey !== key) claimContent(legacyKey, session.sha256, item);
6356
6569
  claim(requestedAccess, key, key, item, "requested-access");
6357
- const otherDigest = requestedDigests.get(key);
6358
- if (otherDigest !== void 0 && otherDigest.sha256 !== session.sha256) throw new SessionContentConflict({
6359
- workIds: [.../* @__PURE__ */ new Set([...otherDigest.workIds, item.workId])],
6360
- sessionKey: key
6361
- });
6362
- requestedDigests.set(key, {
6363
- sha256: session.sha256,
6364
- workIds: [.../* @__PURE__ */ new Set([...otherDigest?.workIds ?? [], item.workId])]
6365
- });
6366
6570
  for (const work of inventory.works) for (const entry of work.sessions) {
6367
6571
  if (entry.key !== key && !(entry.key === legacyKey && entry.sha256 === session.sha256)) continue;
6368
6572
  observe(item, work.access);
@@ -6370,48 +6574,288 @@ async function sessionAccessBlocked(dependencies, journeyId, items, inventory, s
6370
6574
  }
6371
6575
  if (destination && destination.access !== item.access) record(item, key, "destination-access", destination);
6372
6576
  }
6577
+ if (selected.membership(destination?.sessions ?? []).length > 1e3 || partCount > 1e4 || partBytes > passportSessionPartByteLimit) throw new ApplicationError("WORK_SESSION_CAPACITY_EXCEEDED");
6373
6578
  }
6579
+ content.requireIndependent();
6374
6580
  return [...conflicts.values()].map((conflict) => (accountAccesses.get(conflict.workId)?.size ?? 0) > 1 ? {
6375
6581
  ...conflict,
6376
6582
  accountAccess: "mixed"
6377
6583
  } : conflict);
6378
6584
  }
6379
- /** Keep overlapping session ownership serial through membership, including retained members. */
6380
- async function sessionIndependentItems(dependencies, journeyId, items, inventory, signal) {
6381
- const sessions = dependencies.artifacts.sessionFacts ?? dependencies.artifacts.sessions;
6382
- if (!sessions) return [];
6383
- const pending = items.filter((item) => item.status === "uploading" || item.status === "ready").toSorted((left, right) => Number(right.status === "uploading") - Number(left.status === "uploading") || right.artifact.byteLength - left.artifact.byteLength);
6384
- const claimed = /* @__PURE__ */ new Set();
6385
- const independent = [];
6386
- for (const item of pending) {
6387
- const keys = /* @__PURE__ */ new Set([
6388
- ...inventory.works.find((work) => work.logicalWorkId === item.workId)?.sessions.map((session) => session.key) ?? [],
6389
- ...item.sessionMembership?.keys ?? [],
6390
- ...item.sessionUpload ? [item.sessionUpload.key] : []
6391
- ]);
6392
- if (!item.sessionMembership && !item.sessionAssembly) for await (const session of sessions(journeyId, item.artifact, signal)) keys.add(await sessionKeyFor(session));
6393
- if (![...keys].some((key) => claimed.has(key))) independent.push(item);
6394
- for (const key of keys) claimed.add(key);
6585
+ /** Keep overlapping session ownership serial through membership, including retained members. */
6586
+ async function sessionIndependentItems(dependencies, journeyId, items, inventory, signal) {
6587
+ const sessions = dependencies.artifacts.sessionFacts ?? dependencies.artifacts.sessions;
6588
+ if (!sessions) return [];
6589
+ const pending = items.filter((item) => item.status === "uploading" || item.status === "ready").toSorted((left, right) => Number(right.status === "uploading") - Number(left.status === "uploading") || right.artifact.byteLength - left.artifact.byteLength);
6590
+ const claimed = /* @__PURE__ */ new Set();
6591
+ const independent = [];
6592
+ for (const item of pending) {
6593
+ const keys = /* @__PURE__ */ new Set([
6594
+ ...inventory.works.find((work) => work.logicalWorkId === item.workId)?.sessions.map((session) => session.key) ?? [],
6595
+ ...item.sessionMembership?.keys ?? [],
6596
+ ...item.sessionUpload ? [item.sessionUpload.key] : []
6597
+ ]);
6598
+ if (!item.sessionMembership && !item.sessionAssembly) for await (const session of sessions(journeyId, item.artifact, signal)) keys.add(await sessionKeyFor(session));
6599
+ if (![...keys].some((key) => claimed.has(key))) independent.push(item);
6600
+ for (const key of keys) claimed.add(key);
6601
+ }
6602
+ return independent;
6603
+ }
6604
+ //#endregion
6605
+ //#region apps/cli/src/account/journey/remote-error.ts
6606
+ const refusalSchema = z.object({
6607
+ error: z.string(),
6608
+ preview: sendSettingsSchema.nullable().optional(),
6609
+ resolution: sendResolutionSchema.optional(),
6610
+ /** `unknown_keep_work_ids`: the kept IDs Passport did not accept (ADR 0204). */
6611
+ workIds: z.array(z.string()).optional()
6612
+ });
6613
+ var JourneyRemoteError = class extends Error {
6614
+ retryAfter;
6615
+ status;
6616
+ code;
6617
+ details;
6618
+ constructor(status, code, details, options, retryAfter) {
6619
+ super(code, options);
6620
+ this.name = "JourneyRemoteError";
6621
+ this.retryAfter = retryAfter;
6622
+ this.status = status;
6623
+ this.code = code;
6624
+ this.details = details;
6625
+ }
6626
+ };
6627
+ //#endregion
6628
+ //#region apps/cli/src/account/journey/session-transport.ts
6629
+ /** Session requests allow ten minutes for response headers and response-body inactivity. */
6630
+ const sessionRequestTimeoutMs = 6e5;
6631
+ /**
6632
+ * Node 24 bundles undici but does not expose its Agent as a public built-in module.
6633
+ * Reuse its dispatcher through the versioned compatibility symbol, without changing
6634
+ * global transport policy. Constructing Request initializes undici without any I/O.
6635
+ * Fail closed if a future runtime removes this bridge; never fall back to five minutes.
6636
+ */
6637
+ function runtimeSessionDispatcher() {
6638
+ new Request("https://session-transport.invalid");
6639
+ const dispatcher = Reflect.get(globalThis, Symbol.for("undici.globalDispatcher.1"));
6640
+ if (typeof dispatcher !== "object" || dispatcher === null || !("dispatch" in dispatcher) || typeof dispatcher.dispatch !== "function") throw new ApplicationError("SESSION_TRANSPORT_UNAVAILABLE");
6641
+ const dispatch = dispatcher.dispatch.bind(dispatcher);
6642
+ return { dispatch(options, handler) {
6643
+ return dispatch(options, handler);
6644
+ } };
6645
+ }
6646
+ /** Per-request overrides reach undici's parser; AbortSignal alone does not change these. */
6647
+ function sessionDispatcher(delegate = runtimeSessionDispatcher(), timeoutMs = sessionRequestTimeoutMs) {
6648
+ return { dispatch(options, handler) {
6649
+ return delegate.dispatch({
6650
+ ...options,
6651
+ headersTimeout: timeoutMs,
6652
+ bodyTimeout: timeoutMs
6653
+ }, handler);
6654
+ } };
6655
+ }
6656
+ //#endregion
6657
+ //#region apps/cli/src/journey/send-sessions.ts
6658
+ /** Build upload identities from frozen local metadata and bytes from the final primary only. */
6659
+ async function sessionInputs(storage, journeyId, artifact, signal, select) {
6660
+ const query = {
6661
+ journeyId,
6662
+ workId: artifact.workId,
6663
+ revision: artifact.revision
6664
+ };
6665
+ const descriptor = await storage.readSendOutput({
6666
+ ...query,
6667
+ sessions: true
6668
+ });
6669
+ const { output, identities } = descriptor;
6670
+ if (output.bundleId !== artifact.bundleId || output.primary.sha256 !== artifact.sha256 || output.primary.archiveBytes !== artifact.byteLength) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
6671
+ return {
6672
+ output,
6673
+ identities,
6674
+ omitted: await select(storage, descriptor, signal)
6675
+ };
6676
+ }
6677
+ async function* sessionFacts(storage, journeyId, artifact, signal, select = createSendExtensionSelection()) {
6678
+ const { output, identities, omitted } = await sessionInputs(storage, journeyId, artifact, signal, select);
6679
+ yield* configuredSessionUploads(output.primary, identities, signal, "facts", omitted, (bytes) => {
6680
+ if (bytes > passportSessionPartByteLimit) throw new ApplicationError("WORK_SESSION_CAPACITY_EXCEEDED");
6681
+ });
6682
+ }
6683
+ async function* sessionArtifacts(storage, journeyId, artifact, signal, select = createSendExtensionSelection()) {
6684
+ const { output, identities, omitted } = await sessionInputs(storage, journeyId, artifact, signal, select);
6685
+ for await (const session of configuredSessionUploads(output.primary, identities, signal, "upload", omitted)) yield {
6686
+ ...session,
6687
+ async upload(upload, uploadSignal, onProgress) {
6688
+ await storage.readSendOutput({
6689
+ journeyId,
6690
+ workId: artifact.workId,
6691
+ revision: artifact.revision,
6692
+ sessions: false
6693
+ });
6694
+ const url = new URL(upload.uploadUrl);
6695
+ if (url.protocol !== "https:" || url.username || url.password || url.hash) throw new ApplicationError("INVALID_ARGUMENTS");
6696
+ const dispatcher = sessionDispatcher();
6697
+ let bytes = 0;
6698
+ const progressFailure = {};
6699
+ const request = {
6700
+ method: "PUT",
6701
+ body: session.stream().pipeThrough(new TransformStream({ async transform(chunk, controller) {
6702
+ bytes += chunk.byteLength;
6703
+ try {
6704
+ await onProgress?.(bytes);
6705
+ } catch (cause) {
6706
+ progressFailure.error = { cause };
6707
+ throw cause;
6708
+ }
6709
+ controller.enqueue(chunk);
6710
+ } })),
6711
+ signal: uploadSignal,
6712
+ dispatcher,
6713
+ redirect: "error",
6714
+ credentials: "omit",
6715
+ referrerPolicy: "no-referrer",
6716
+ headers: {
6717
+ "content-type": "application/zip",
6718
+ "content-length": String(session.byteLength),
6719
+ ...upload.headers
6720
+ },
6721
+ duplex: "half"
6722
+ };
6723
+ const response = await fetch(url, request).catch((error) => {
6724
+ if (progressFailure.error) throw progressFailure.error.cause;
6725
+ throw error;
6726
+ });
6727
+ await response.body?.cancel();
6728
+ if (!response.ok) throw new JourneyRemoteError(response.status, "UPLOAD_FAILED", null, void 0, response.headers.get("retry-after") ?? void 0);
6729
+ }
6730
+ };
6731
+ }
6732
+ /** Own one live upload iterator; advancing it releases the preceding session ZIP. */
6733
+ async function prepareSessionArtifacts(storage, journeyId, artifact, signal, select = createSendExtensionSelection()) {
6734
+ const source = sessionArtifacts(storage, journeyId, artifact, signal, select);
6735
+ let closed = false;
6736
+ async function* iterate() {
6737
+ try {
6738
+ yield* source;
6739
+ } finally {
6740
+ closed = true;
6741
+ }
6742
+ }
6743
+ const items = iterate();
6744
+ return {
6745
+ items,
6746
+ async close() {
6747
+ if (closed) return;
6748
+ closed = true;
6749
+ await items.return(void 0);
6750
+ }
6751
+ };
6752
+ }
6753
+ //#endregion
6754
+ //#region apps/cli/src/journey/send-artifacts.ts
6755
+ function createJourneySendArtifacts(storage) {
6756
+ const selection = createSendExtensionSelection();
6757
+ return {
6758
+ sessionFacts: (journeyId, artifact, signal) => sessionFacts(storage, journeyId, artifact, signal, selection),
6759
+ prepareSessions: (journeyId, artifact, signal) => prepareSessionArtifacts(storage, journeyId, artifact, signal, selection),
6760
+ sessions: (journeyId, artifact, signal) => sessionArtifacts(storage, journeyId, artifact, signal, selection),
6761
+ async describe(journey, work, signal) {
6762
+ const { output } = await storage.readSendOutput({
6763
+ journeyId: journey.journeyId,
6764
+ workId: work.id,
6765
+ revision: work.revision,
6766
+ sessions: false
6767
+ });
6768
+ const info = await readConfiguredOutputInfo(output.primary, signal);
6769
+ if (info.work.id !== work.id || info.work.access !== work.access) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
6770
+ let agentSources = [];
6771
+ try {
6772
+ const base = (await storage.resources({
6773
+ journeyId: journey.journeyId,
6774
+ kind: "base",
6775
+ id: `${work.id}:${output.finalMaskLedger.inputRevision}`
6776
+ })).find((item) => item.kind === "base" && item.workId === work.id && item.inputRevision === output.finalMaskLedger.inputRevision && item.primary.sha256 === output.finalMaskLedger.basePrimarySha256);
6777
+ if (base?.kind === "base") {
6778
+ const ids = base.sessions.map((item) => staticSessionId({
6779
+ workId: work.id,
6780
+ inputRevision: base.inputRevision,
6781
+ sessionId: item.applicationSessionId
6782
+ }));
6783
+ const included = new Set(base.sessions.map((item) => item.applicationSessionId));
6784
+ const resources = ids.length ? await storage.resources({
6785
+ journeyId: journey.journeyId,
6786
+ kind: "static-session",
6787
+ ids
6788
+ }) : [];
6789
+ const unique = /* @__PURE__ */ new Map();
6790
+ for (const resource of resources) {
6791
+ if (resource.kind !== "static-session" || resource.workId !== work.id || resource.inputRevision !== base.inputRevision || !included.has(resource.sessionId)) continue;
6792
+ for (const candidate of resource.agentSources ?? []) {
6793
+ const parsed = agentSourceSchema.safeParse(candidate);
6794
+ if (parsed.success && unique.size < 500) unique.set(JSON.stringify(parsed.data), parsed.data);
6795
+ }
6796
+ }
6797
+ agentSources = [...unique.values()];
6798
+ }
6799
+ } catch {
6800
+ console.error("AGENT_SOURCES_METADATA_UNAVAILABLE");
6801
+ }
6802
+ return sendArtifactSchema.parse({
6803
+ ...agentSources.length === 0 ? {} : { agentSources },
6804
+ workId: work.id,
6805
+ revision: work.revision,
6806
+ bundleId: info.manifest.bundleId,
6807
+ createdAt: info.createdAt,
6808
+ title: info.work.title,
6809
+ sessionCount: info.sessionCount,
6810
+ grouping: info.manifest.work.grouping,
6811
+ sources: info.sources,
6812
+ byteLength: output.primary.archiveBytes,
6813
+ sha256: output.primary.sha256
6814
+ });
6815
+ },
6816
+ async upload(journeyId, artifact, upload, signal) {
6817
+ const { output } = await storage.readSendOutput({
6818
+ journeyId,
6819
+ workId: artifact.workId,
6820
+ revision: artifact.revision,
6821
+ sessions: false
6822
+ });
6823
+ if (output.bundleId !== artifact.bundleId || output.primary.sha256 !== artifact.sha256 || output.primary.archiveBytes !== artifact.byteLength || artifact.byteLength > 5368709120) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
6824
+ await putPrimary(output.primary, artifact.byteLength, upload, signal);
6825
+ }
6826
+ };
6827
+ }
6828
+ /**
6829
+ * Stream a verified primary to a signed HTTPS URL; shared by native Send and Manual upload. A
6830
+ * checksum upload also sends the SHA-256 header the URL signs, so storage refuses other bytes.
6831
+ */
6832
+ async function putPrimary(primary, byteLength, upload, signal) {
6833
+ const url = new URL(upload.uploadUrl);
6834
+ if (url.protocol !== "https:" || url.username || url.password || url.hash) throw new ApplicationError("INVALID_ARGUMENTS");
6835
+ const source = await openConfiguredUpload(primary, signal);
6836
+ const cleanup = createCleanup();
6837
+ try {
6838
+ const request = {
6839
+ method: "PUT",
6840
+ body: source.stream,
6841
+ signal,
6842
+ redirect: "error",
6843
+ headers: {
6844
+ "Content-Type": "application/zip",
6845
+ "Content-Length": String(byteLength),
6846
+ ...upload.headers
6847
+ },
6848
+ duplex: "half"
6849
+ };
6850
+ const result = await fetch(url, request);
6851
+ await result.body?.cancel();
6852
+ if (!result.ok) throw new ApplicationError("UPLOAD_FAILED");
6853
+ } catch (error) {
6854
+ cleanup.failed(error);
6855
+ throw error;
6856
+ } finally {
6857
+ await cleanup.run(() => source.close());
6395
6858
  }
6396
- return independent;
6397
- }
6398
- //#endregion
6399
- //#region apps/cli/src/account/journey/receipt.ts
6400
- function acceptReceipt(item, receipt) {
6401
- if (receipt.logicalWorkId !== item.workId || receipt.bundleId !== item.artifact.bundleId || receipt.outputRevision !== item.artifact.revision || receipt.sha256 !== item.artifact.sha256 || receipt.byteLength !== item.artifact.byteLength || receipt.access !== item.access || item.receipt !== null && (receipt.receiptId !== item.receipt.receiptId || receipt.scanId !== item.receipt.scanId || receipt.bindingId !== item.receipt.bindingId) || item.target?.kind === "existing" && receipt.bindingId !== item.target.bindingId) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
6402
- const { upload: _upload, ...saved } = receipt;
6403
- const success = receipt.state === "succeeded" && receipt.assessmentState === "succeeded" && receipt.passportIncluded && receipt.effectiveAccess === item.access;
6404
- const failure = [
6405
- "failed",
6406
- "partial",
6407
- "superseded"
6408
- ].includes(receipt.state) || receipt.state === "succeeded" && !success;
6409
- return {
6410
- ...item,
6411
- receipt: saved,
6412
- status: success ? "completed" : failure ? "blocked" : receipt.state === "awaiting_upload" ? "uploading" : "assessing",
6413
- error: failure ? receipt.state === "succeeded" ? "PASSPORT_NOT_CURRENT" : "ASSESSMENT_INCOMPLETE" : null
6414
- };
6415
6859
  }
6416
6860
  //#endregion
6417
6861
  //#region apps/cli/src/account/journey/protocol.ts
@@ -6520,6 +6964,7 @@ async function checkPublicAgentSources(candidates, requestFetch = fetch, signal)
6520
6964
  //#region apps/cli/src/account/journey/session-schema.ts
6521
6965
  const digest = z.string().regex(/^[0-9a-f]{64}$/u);
6522
6966
  const stored = z.strictObject({
6967
+ metadataSha256: digest.nullable(),
6523
6968
  key: digest,
6524
6969
  sessionKey: z.string().regex(/^sess-pilot\/[^/]+\/[^/]+\/[^/]+$/u).nullable(),
6525
6970
  sha256: digest,
@@ -6618,20 +7063,24 @@ function createSessionClient(request) {
6618
7063
  let next = null;
6619
7064
  const seen = /* @__PURE__ */ new Set();
6620
7065
  const sessions = /* @__PURE__ */ new Map();
6621
- let value;
7066
+ let access;
6622
7067
  do {
6623
7068
  const query = new URLSearchParams({ limit: "500" });
6624
7069
  if (next !== null) query.set("cursor", next);
6625
- const page = await request(`/me/works/${encodeURIComponent(id)}/sessions?${query}`, work.extend({ nextCursor: cursor }).refine((item) => item.logicalWorkId === id), signal, void 0, "GET");
7070
+ const page = await request(`/me/works/${encodeURIComponent(id)}/sessions?${query}`, work.extend({ nextCursor: cursor }).refine((item) => item.logicalWorkId === id), signal, void 0, "GET").catch((error) => {
7071
+ if (next === null && error instanceof JourneyRemoteError && error.status === 404 && error.code === "not_found") return null;
7072
+ throw error;
7073
+ });
7074
+ if (!page) return null;
6626
7075
  for (const session of page.sessions) sessions.set(session.key, session);
6627
- value = {
6628
- logicalWorkId: page.logicalWorkId,
6629
- access: page.access,
6630
- sessions: [...sessions.values()]
6631
- };
7076
+ access = page.access;
6632
7077
  next = continuation(seen, page.nextCursor);
6633
7078
  } while (next !== null);
6634
- return value;
7079
+ return {
7080
+ logicalWorkId: id,
7081
+ access,
7082
+ sessions: [...sessions.values()]
7083
+ };
6635
7084
  },
6636
7085
  reserve: (input, signal) => request("/me/sessions/reserve", reserve, signal, input),
6637
7086
  complete: (input, signal) => mutate(signal, () => request("/me/sessions/complete", z.strictObject({ state: z.literal("stored") }), signal, input, "POST", void 0, sessionRequestTimeoutMs, false, 200, "session")),
@@ -6752,6 +7201,21 @@ function createJourneyOwnerClient(options) {
6752
7201
  const { headers } = await requestWithHeaders("/me/profile", publicProfileSchema, signal);
6753
7202
  return { parallelCompletes: (headers.get("passport-capabilities") ?? "").split(",").some((token) => token.trim() === "parallel-completes") };
6754
7203
  },
7204
+ async ownedPassport(signal) {
7205
+ const webOrigin = passportWebOrigin(options.apiUrl, options.webOrigin);
7206
+ if (!webOrigin) throw new ApplicationError("ACCOUNT_PROFILE_UNAVAILABLE");
7207
+ try {
7208
+ const result = await request("/me/passport", z.strictObject({ publicId: sendReceiptSchema.shape.publicId }), signal, void 0, "GET");
7209
+ return new URL("/" + result.publicId, webOrigin).href;
7210
+ } catch (error) {
7211
+ if (error instanceof JourneyRemoteError && error.status === 404 && error.code === "passport not found") {
7212
+ await options.authorize();
7213
+ signal.throwIfAborted();
7214
+ return null;
7215
+ }
7216
+ throw error;
7217
+ }
7218
+ },
6755
7219
  readProfile(signal) {
6756
7220
  return request("/me/profile", ownerProfileSchema, signal, void 0, "GET");
6757
7221
  },
@@ -6952,43 +7416,262 @@ async function resolveItem(item, bindingId, connection, signal, snapshot = false
6952
7416
  status: "target",
6953
7417
  error: null
6954
7418
  };
6955
- if (!target) throw new ApplicationError("INVALID_STATE");
6956
- const targetId = target.kind === "existing" ? target.bindingId : null;
6957
- const candidate = resolution.candidates.find((value) => value.bindingId === targetId);
6958
- if (candidate && candidate.access !== item.access) {
6959
- const settings = await connection.preview(candidate.workId, item.access, signal);
6960
- return askSettings({
6961
- ...item,
6962
- target,
6963
- resolution
6964
- }, settings, null, snapshot);
6965
- }
6966
- const agentSources = await connection.checkAgentSources?.(item.artifact.agentSources, signal);
6967
- signal.throwIfAborted();
7419
+ if (!target) throw new ApplicationError("INVALID_STATE");
7420
+ const targetId = target.kind === "existing" ? target.bindingId : null;
7421
+ const candidate = resolution.candidates.find((value) => value.bindingId === targetId);
7422
+ if (candidate && candidate.access !== item.access) {
7423
+ const settings = await connection.preview(candidate.workId, item.access, signal);
7424
+ return askSettings({
7425
+ ...item,
7426
+ target,
7427
+ resolution
7428
+ }, settings, null, snapshot);
7429
+ }
7430
+ const agentSources = await connection.checkAgentSources?.(item.artifact.agentSources, signal);
7431
+ signal.throwIfAborted();
7432
+ return {
7433
+ ...item,
7434
+ target,
7435
+ resolution,
7436
+ status: "uploading",
7437
+ error: null,
7438
+ reservation: {
7439
+ ...agentSources?.length ? { agentSources } : {},
7440
+ work,
7441
+ target,
7442
+ bundleId: item.artifact.bundleId,
7443
+ createdAt: item.artifact.createdAt,
7444
+ title: item.artifact.title,
7445
+ sessionCount: item.artifact.sessionCount,
7446
+ access: item.access,
7447
+ outputRevision: item.artifact.revision,
7448
+ byteLength: item.artifact.byteLength,
7449
+ sha256: item.artifact.sha256,
7450
+ expectedAccessVersion: resolution.accessVersion,
7451
+ expectedScopeVersion: resolution.scopeVersion
7452
+ }
7453
+ };
7454
+ }
7455
+ /** Keep only the current Send's pending assessment IDs. New arrivals wait behind unpolled peers. */
7456
+ function createAssessmentPoll() {
7457
+ let owner = null;
7458
+ let sendId;
7459
+ let pending = [];
7460
+ return (send, works) => {
7461
+ const byId = new Map(works.map((work) => [work.workId, work]));
7462
+ if (send.subject !== owner || send.id !== sendId) pending = [];
7463
+ owner = send.subject;
7464
+ sendId = send.id;
7465
+ const order = new Set(pending.filter((id) => byId.has(id)));
7466
+ for (const id of byId.keys()) order.add(id);
7467
+ pending = [...order];
7468
+ const selected = pending.splice(0, 3);
7469
+ pending.push(...selected);
7470
+ return selected.flatMap((id) => byId.get(id) ?? []);
7471
+ };
7472
+ }
7473
+ const confirmedSettings = (item) => item.settings !== null && item.settingsConfirmed;
7474
+ const started = (item) => Number(item.status === "uploading");
7475
+ /**
7476
+ * Works that share a workspace path or a git remote: once one completes, Passport offers it as
7477
+ * the other's candidate, so the other's target no longer resolves the same (ADR 0235).
7478
+ */
7479
+ function related(left, right) {
7480
+ return left.artifact.sources.some((one) => right.artifact.sources.some((other) => one.pathHash !== null && one.pathHash === other.pathHash || one.remoteHashes.some((hash) => other.remoteHashes.includes(hash))));
7481
+ }
7482
+ /**
7483
+ * The Works one advance steps together. Upload work comes before assessment checks: up to three
7484
+ * Works, started uploads first, then the largest ZIP, so the slowest Work starts earliest.
7485
+ * Completing an upload moves the owner's scope version, which Passport accepts while a Work's
7486
+ * own target resolves the same (ADR 0235). A Work related to one already picked waits for the
7487
+ * next batch. A reserved Work whose target would not survive that steps alone, and so does a
7488
+ * confirmed linked Settings change, which moves the access version every reservation carries,
7489
+ * once no upload has started. Assessment checks change nothing and step up to three together.
7490
+ */
7491
+ function pickBatch(items, limit, poll) {
7492
+ const uploads = items.filter(sendsOnNextAdvance).toSorted((left, right) => started(right) - started(left) || right.artifact.byteLength - left.artifact.byteLength);
7493
+ const alone = uploads.find((item) => item.status === "uploading" && item.reservation !== null && item.resolution !== null && !keepsTarget(item.resolution, item.reservation)) ?? (uploads[0] && confirmedSettings(uploads[0]) ? uploads[0] : void 0);
7494
+ if (alone) return [alone];
7495
+ if (uploads.length) return uploads.filter((item) => !confirmedSettings(item)).reduce((batch, item) => batch.length < limit && !batch.some((picked) => related(picked, item)) ? [...batch, item] : batch, []);
7496
+ const assessing = items.filter((item) => item.status === "assessing");
7497
+ const first = assessing[0];
7498
+ if (!first) return [];
7499
+ if (confirmedSettings(first)) return [first];
7500
+ const checks = assessing.filter((item) => !confirmedSettings(item));
7501
+ return poll ? poll(checks) : checks.slice(0, 3);
7502
+ }
7503
+ function isCode(error, code) {
7504
+ return error instanceof ApplicationError && error.code === code;
7505
+ }
7506
+ /**
7507
+ * A step that ended without a definite answer and is safe to repeat: a request timeout or abort
7508
+ * that is not the Send's own cancellation, or a connection failure from `fetch`: `fetch failed`
7509
+ * before the response, `terminated` when the socket resets while the body is read. The Work keeps
7510
+ * its state, so the next advance repeats the same idempotent step (ADR 0197).
7511
+ */
7512
+ function isInterrupted(error) {
7513
+ if (!(error instanceof Error)) return false;
7514
+ if (error.name === "TimeoutError" || error.name === "AbortError") return true;
7515
+ return error instanceof TypeError && (error.message === "fetch failed" || error.message === "terminated");
7516
+ }
7517
+ //#endregion
7518
+ //#region apps/cli/src/account/journey/session-facts-cache.ts
7519
+ const keyFor = (journeyId, artifact) => JSON.stringify([
7520
+ journeyId,
7521
+ artifact.workId,
7522
+ artifact.revision,
7523
+ artifact.sha256
7524
+ ]);
7525
+ /** Sender-owned facts only. In-flight computations never enter the bounded cache. */
7526
+ function createSessionFactsCache(limit = 256) {
7527
+ const facts = /* @__PURE__ */ new Map();
7528
+ let owner = "";
7529
+ let generation = 0;
7530
+ let allowed = /* @__PURE__ */ new Set();
7531
+ return {
7532
+ retain(journeyId, send) {
7533
+ const nextOwner = JSON.stringify([journeyId, send?.id]);
7534
+ const ended = !send || [
7535
+ "completed",
7536
+ "blocked",
7537
+ "prepared"
7538
+ ].includes(send.status);
7539
+ if (owner !== nextOwner || ended) {
7540
+ facts.clear();
7541
+ generation += 1;
7542
+ }
7543
+ owner = nextOwner;
7544
+ allowed = new Set(ended || !send ? [] : send.items.filter((item) => !item.sessionAssembly && item.status !== "completed").map((item) => keyFor(journeyId, item.artifact)));
7545
+ for (const key of facts.keys()) if (!allowed.has(key)) facts.delete(key);
7546
+ },
7547
+ async get(journeyId, artifact, signal, compute) {
7548
+ signal.throwIfAborted();
7549
+ const key = keyFor(journeyId, artifact);
7550
+ const cached = facts.get(key);
7551
+ if (cached) return cached;
7552
+ const started = generation;
7553
+ const items = await compute();
7554
+ signal.throwIfAborted();
7555
+ if (started === generation && allowed.has(key)) {
7556
+ facts.set(key, items);
7557
+ while (facts.size > limit) {
7558
+ const oldest = facts.keys().next().value;
7559
+ if (oldest === void 0) break;
7560
+ facts.delete(oldest);
7561
+ }
7562
+ }
7563
+ return items;
7564
+ }
7565
+ };
7566
+ }
7567
+ //#endregion
7568
+ //#region apps/cli/src/account/journey/session-preparation.ts
7569
+ /** Shared facts retain no bytes; only upload steps acquire phase-local live iterators. */
7570
+ function createSessionPreparation(dependencies, facts) {
7571
+ const prepare = dependencies.artifacts.prepareSessions;
7572
+ const prepared = [];
7573
+ const sessions = async function* (journeyId, artifact, signal) {
7574
+ if (!prepare) {
7575
+ const original = dependencies.artifacts.sessions;
7576
+ if (original) yield* original(journeyId, artifact, signal);
7577
+ return;
7578
+ }
7579
+ const result = await prepare(journeyId, artifact, signal);
7580
+ prepared.push(result);
7581
+ for await (const item of result.items) {
7582
+ signal.throwIfAborted();
7583
+ yield item;
7584
+ }
7585
+ };
7586
+ const sessionFacts = async function* (journeyId, artifact, signal) {
7587
+ const items = await facts.get(journeyId, artifact, signal, async () => {
7588
+ const original = dependencies.artifacts.sessionFacts ?? dependencies.artifacts.sessions;
7589
+ const items = [];
7590
+ if (original) for await (const item of original(journeyId, artifact, signal)) {
7591
+ const { workerIdentity, nativeSessionId, relativePath, harness, lastAt, metadata, sha256, byteLength, crc32 } = item;
7592
+ items.push({
7593
+ ...workerIdentity ? { workerIdentity } : {},
7594
+ nativeSessionId,
7595
+ relativePath,
7596
+ harness,
7597
+ lastAt,
7598
+ metadata,
7599
+ sha256,
7600
+ byteLength,
7601
+ crc32
7602
+ });
7603
+ }
7604
+ return items;
7605
+ });
7606
+ for (const item of items) {
7607
+ signal.throwIfAborted();
7608
+ yield item;
7609
+ }
7610
+ };
6968
7611
  return {
6969
- ...item,
6970
- target,
6971
- resolution,
6972
- status: "uploading",
6973
- error: null,
6974
- reservation: {
6975
- ...agentSources?.length ? { agentSources } : {},
6976
- work,
6977
- target,
6978
- bundleId: item.artifact.bundleId,
6979
- createdAt: item.artifact.createdAt,
6980
- title: item.artifact.title,
6981
- sessionCount: item.artifact.sessionCount,
6982
- access: item.access,
6983
- outputRevision: item.artifact.revision,
6984
- byteLength: item.artifact.byteLength,
6985
- sha256: item.artifact.sha256,
6986
- expectedAccessVersion: resolution.accessVersion,
6987
- expectedScopeVersion: resolution.scopeVersion
7612
+ dependencies: {
7613
+ ...dependencies,
7614
+ artifacts: {
7615
+ ...dependencies.artifacts,
7616
+ sessions,
7617
+ sessionFacts
7618
+ }
7619
+ },
7620
+ async close() {
7621
+ await createCleanup().run(...prepared.map((result) => () => result.close()));
6988
7622
  }
6989
7623
  };
6990
7624
  }
6991
7625
  //#endregion
7626
+ //#region apps/cli/src/account/journey/session-metadata.ts
7627
+ /** The owner API uses this exact tuple; object key insertion order is not a wire dependency. */
7628
+ function sessionMetadataSha256(metadata) {
7629
+ return createHash("sha256").update(JSON.stringify([
7630
+ metadata.workspace.title,
7631
+ metadata.workspace.pathHash,
7632
+ metadata.workspace.remoteHashes,
7633
+ metadata.workspace.primaryRemoteHash,
7634
+ metadata.sourceSessionId,
7635
+ metadata.harness,
7636
+ metadata.lastAt,
7637
+ metadata.parts.map((part) => [
7638
+ part.path,
7639
+ part.byteLength,
7640
+ part.sha256,
7641
+ part.crc32,
7642
+ [
7643
+ part.native.role,
7644
+ part.native.sessionId,
7645
+ part.native.agentId,
7646
+ part.native.dispatchParentSessionId,
7647
+ part.native.fork ? [
7648
+ part.native.fork.parentSessionId,
7649
+ part.native.fork.position.kind,
7650
+ part.native.fork.position.value
7651
+ ] : null
7652
+ ]
7653
+ ])
7654
+ ])).digest("hex");
7655
+ }
7656
+ //#endregion
7657
+ //#region apps/cli/src/account/journey/receipt.ts
7658
+ function acceptReceipt(item, receipt) {
7659
+ if (receipt.logicalWorkId !== item.workId || receipt.bundleId !== item.artifact.bundleId || receipt.outputRevision !== item.artifact.revision || receipt.sha256 !== item.artifact.sha256 || receipt.byteLength !== item.artifact.byteLength || receipt.access !== item.access || item.receipt !== null && (receipt.receiptId !== item.receipt.receiptId || receipt.scanId !== item.receipt.scanId || receipt.bindingId !== item.receipt.bindingId) || item.target?.kind === "existing" && receipt.bindingId !== item.target.bindingId) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
7660
+ const { upload: _upload, ...saved } = receipt;
7661
+ const success = receipt.state === "succeeded" && receipt.assessmentState === "succeeded" && receipt.passportIncluded && receipt.effectiveAccess === item.access;
7662
+ const failure = [
7663
+ "failed",
7664
+ "partial",
7665
+ "superseded"
7666
+ ].includes(receipt.state) || receipt.state === "succeeded" && !success;
7667
+ return {
7668
+ ...item,
7669
+ receipt: saved,
7670
+ status: success ? "completed" : failure ? "blocked" : receipt.state === "awaiting_upload" ? "uploading" : "assessing",
7671
+ error: failure ? receipt.state === "succeeded" ? "PASSPORT_NOT_CURRENT" : "ASSESSMENT_INCOMPLETE" : null
7672
+ };
7673
+ }
7674
+ //#endregion
6992
7675
  //#region apps/cli/src/account/journey/step.ts
6993
7676
  async function observe(item, receipt, connection, signal, auto) {
6994
7677
  const observed = acceptReceipt(item, receipt);
@@ -7147,51 +7830,6 @@ function renewed(item, error) {
7147
7830
  error
7148
7831
  };
7149
7832
  }
7150
- const confirmedSettings = (item) => item.settings !== null && item.settingsConfirmed;
7151
- const started = (item) => Number(item.status === "uploading");
7152
- /**
7153
- * Works that share a workspace path or a git remote: once one completes, Passport offers it as
7154
- * the other's candidate, so the other's target no longer resolves the same (ADR 0235).
7155
- */
7156
- function related(left, right) {
7157
- return left.artifact.sources.some((one) => right.artifact.sources.some((other) => one.pathHash !== null && one.pathHash === other.pathHash || one.remoteHashes.some((hash) => other.remoteHashes.includes(hash))));
7158
- }
7159
- /**
7160
- * The Works one advance steps together. Upload work comes before assessment checks: up to three
7161
- * Works, started uploads first, then the largest ZIP, so the slowest Work starts earliest.
7162
- * Completing an upload moves the owner's scope version, which Passport accepts while a Work's
7163
- * own target resolves the same (ADR 0235). A Work related to one already picked waits for the
7164
- * next batch. A reserved Work whose target would not survive that steps alone, and so does a
7165
- * confirmed linked Settings change, which moves the access version every reservation carries,
7166
- * once no upload has started. Assessment checks change nothing and step up to three together.
7167
- */
7168
- function pickBatch(items, now, limit) {
7169
- const uploads = items.filter((item) => item.status === "uploading" || item.status === "ready").toSorted((left, right) => started(right) - started(left) || right.artifact.byteLength - left.artifact.byteLength);
7170
- const alone = uploads.find((item) => item.status === "uploading" && item.reservation !== null && item.resolution !== null && !keepsTarget(item.resolution, item.reservation)) ?? (uploads[0] && confirmedSettings(uploads[0]) ? uploads[0] : void 0);
7171
- if (alone) return [alone];
7172
- if (uploads.length) return uploads.filter((item) => !confirmedSettings(item)).reduce((batch, item) => batch.length < limit && !batch.some((picked) => related(picked, item)) ? [...batch, item] : batch, []);
7173
- const assessing = items.filter((item) => item.status === "assessing");
7174
- const first = assessing[0];
7175
- if (!first) return [];
7176
- if (confirmedSettings(first)) return [first];
7177
- const checks = assessing.filter((item) => !confirmedSettings(item));
7178
- const start = Math.floor(now / 1e3) * 3 % checks.length;
7179
- return [...checks.slice(start), ...checks.slice(0, start)].slice(0, 3);
7180
- }
7181
- function isCode(error, code) {
7182
- return error instanceof ApplicationError && error.code === code;
7183
- }
7184
- /**
7185
- * A step that ended without a definite answer and is safe to repeat: a request timeout or abort
7186
- * that is not the Send's own cancellation, or a connection failure from `fetch`: `fetch failed`
7187
- * before the response, `terminated` when the socket resets while the body is read. The Work keeps
7188
- * its state, so the next advance repeats the same idempotent step (ADR 0197).
7189
- */
7190
- function isInterrupted(error) {
7191
- if (!(error instanceof Error)) return false;
7192
- if (error.name === "TimeoutError" || error.name === "AbortError") return true;
7193
- return error instanceof TypeError && (error.message === "fetch failed" || error.message === "terminated");
7194
- }
7195
7833
  const sessionBackoffMs = (attempt) => 1e3 * 2 ** (attempt - 1);
7196
7834
  const zeroByteBackoffMs = (count) => [
7197
7835
  15e3,
@@ -7206,7 +7844,7 @@ function retryDelay(error, fallback, now) {
7206
7844
  return Number.isFinite(delay) ? Math.max(fallback, delay) : fallback;
7207
7845
  }
7208
7846
  /** Reconcile retained intents; only byte-started failures charge the durable budget. */
7209
- async function attemptSession({ dependencies, original, session, key, remote, signal, guard, checkpoint }) {
7847
+ async function attemptSession({ dependencies, original, countCompletion, session, key, remote, signal, guard, checkpoint }) {
7210
7848
  const previous = original.sessionUpload?.key === key && original.sessionUpload.sha256 === session.sha256 ? original.sessionUpload : null;
7211
7849
  let failures = previous?.attempt ?? 0;
7212
7850
  let retryCount = previous?.retryCount ?? 0;
@@ -7259,12 +7897,12 @@ async function attemptSession({ dependencies, original, session, key, remote, si
7259
7897
  lastReported = Date.now();
7260
7898
  }
7261
7899
  });
7262
- await guard();
7263
- await remote.complete({
7264
- ...input,
7265
- metadata: session.metadata
7266
- }, signal);
7267
7900
  }
7901
+ await guard();
7902
+ await remote.complete({
7903
+ ...input,
7904
+ metadata: session.metadata
7905
+ }, signal);
7268
7906
  };
7269
7907
  try {
7270
7908
  if (previous === null) await reserveUploadComplete();
@@ -7278,6 +7916,10 @@ async function attemptSession({ dependencies, original, session, key, remote, si
7278
7916
  if (failures >= 3) throw new ApplicationError("SESSION_UPLOAD_EXHAUSTED");
7279
7917
  await reserveUploadComplete();
7280
7918
  }
7919
+ if (countCompletion) item = {
7920
+ ...item,
7921
+ storedSessions: (original.storedSessions ?? 0) + 1
7922
+ };
7281
7923
  transfer.bytesAcknowledged = session.byteLength;
7282
7924
  await guard();
7283
7925
  await save();
@@ -7317,6 +7959,7 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7317
7959
  if (!item.sessionAssembly && (item.status !== "uploading" || item.receipt || item.reservation)) return {
7318
7960
  ...item,
7319
7961
  receipt: null,
7962
+ storedSessions: item.sessionMembership ? item.storedSessions : 0,
7320
7963
  reservation: null,
7321
7964
  target: null,
7322
7965
  previousReceipts: item.receipt ? [...item.previousReceipts, item.receipt] : item.previousReceipts,
@@ -7350,7 +7993,7 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7350
7993
  const intent = item.sessionMembership;
7351
7994
  const assembly = await remote.assembly(intent.workId, signal);
7352
7995
  const conflict = async () => {
7353
- const current = await remote.inventory(signal);
7996
+ const current = await remote.work(intent.workId, signal);
7354
7997
  const revision = await remote.assembly(intent.workId, signal);
7355
7998
  await guard();
7356
7999
  return {
@@ -7359,7 +8002,7 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7359
8002
  error: "SESSION_MEMBERSHIP_CONFLICT",
7360
8003
  membershipConflict: {
7361
8004
  revision: revision?.revision ?? 0,
7362
- keys: current.works.find((work) => work.logicalWorkId === intent.workId)?.sessions.map((session) => session.key) ?? []
8005
+ keys: current?.sessions.map((session) => session.key) ?? []
7363
8006
  }
7364
8007
  };
7365
8008
  };
@@ -7376,7 +8019,7 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7376
8019
  const { workId, ...input } = intent;
7377
8020
  const result = await remote.membership(workId, input, signal);
7378
8021
  await guard();
7379
- if (migrate) {
8022
+ if (migrate || item.sessionMembershipMetadataVersion !== 1) {
7380
8023
  const { sessionMembership: _, ...rest } = item;
7381
8024
  return {
7382
8025
  ...rest,
@@ -7394,6 +8037,7 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7394
8037
  error: null
7395
8038
  };
7396
8039
  } catch (error) {
8040
+ if (error instanceof JourneyRemoteError && error.status === 413 && error.code === "work_session_capacity_exceeded") throw new ApplicationError("WORK_SESSION_CAPACITY_EXCEEDED");
7397
8041
  if (error instanceof JourneyRemoteError && error.status === 409) return conflict();
7398
8042
  throw error;
7399
8043
  }
@@ -7412,21 +8056,30 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7412
8056
  }
7413
8057
  }
7414
8058
  const ownKeys = /* @__PURE__ */ new Set();
8059
+ if (!recoverLegacy) item = {
8060
+ ...item,
8061
+ storedSessions: 0
8062
+ };
7415
8063
  for await (const session of sessions(journeyId, item.artifact, signal)) {
7416
8064
  const identity = await keys.add(session);
7417
8065
  ownKeys.add(identity.key);
7418
8066
  if (recoverLegacy && (identity.legacyKey !== pending?.key || session.sha256 !== pending?.sha256)) continue;
7419
8067
  const key = recoverLegacy && pending ? pending.key : identity.key;
7420
- if (stored.get(key)?.sha256 === session.sha256) {
8068
+ if (stored.get(key)?.sha256 === session.sha256 && stored.get(key)?.metadataSha256 === sessionMetadataSha256(session.metadata)) {
7421
8069
  if (recoverLegacy) return {
7422
8070
  ...item,
7423
8071
  sessionUpload: null
7424
8072
  };
8073
+ item = {
8074
+ ...item,
8075
+ storedSessions: (item.storedSessions ?? 0) + 1
8076
+ };
7425
8077
  continue;
7426
8078
  }
7427
8079
  item = await attemptSession({
7428
8080
  dependencies,
7429
8081
  original: item,
8082
+ countCompletion: !recoverLegacy,
7430
8083
  session,
7431
8084
  key,
7432
8085
  remote,
@@ -7438,7 +8091,7 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7438
8091
  }
7439
8092
  await guard();
7440
8093
  const assembly = await remote.assembly(item.workId, signal);
7441
- const current = (await remote.inventory(signal)).works.find((work) => work.logicalWorkId === item.workId);
8094
+ const current = await remote.work(item.workId, signal);
7442
8095
  if (current && current.access !== item.access) {
7443
8096
  if (current.access === "removed") throw new ApplicationError("WORK_REMOVED");
7444
8097
  const [ownKey] = ownKeys;
@@ -7463,6 +8116,7 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7463
8116
  ...item,
7464
8117
  sessionUpload: null,
7465
8118
  sessionMembershipKeyVersion: 2,
8119
+ sessionMembershipMetadataVersion: 1,
7466
8120
  sessionMembership: {
7467
8121
  workId: item.workId,
7468
8122
  expectedRevision: assembly?.revision ?? 0,
@@ -7505,6 +8159,35 @@ async function saveSend(dependencies, snapshot, next, action, expectedJourneyRev
7505
8159
  at: dependencies.now()
7506
8160
  });
7507
8161
  }
8162
+ /** This transaction changes only the supplied transfer fields and durable cursor. */
8163
+ async function saveTransfer(dependencies, current, workId, patch) {
8164
+ const progress = await dependencies.storage.checkpointTransfer({
8165
+ journeyId: current.journey.journeyId,
8166
+ sendId: current.send.id,
8167
+ workId,
8168
+ expectedJourneyRevision: current.journey.revision,
8169
+ expectedSendRevision: current.send.revision,
8170
+ requestKey: randomUUID(),
8171
+ at: dependencies.now(),
8172
+ ...patch
8173
+ });
8174
+ if (progress.status !== "applied") throw new ApplicationError("REVISION_CONFLICT");
8175
+ return {
8176
+ journey: {
8177
+ ...current.journey,
8178
+ revision: progress.revision,
8179
+ lastEvent: progress.lastEvent
8180
+ },
8181
+ send: {
8182
+ ...current.send,
8183
+ revision: progress.sendRevision,
8184
+ items: current.send.items.map((item) => item.workId === workId ? {
8185
+ ...item,
8186
+ ...patch
8187
+ } : item)
8188
+ }
8189
+ };
8190
+ }
7508
8191
  function aggregate(send) {
7509
8192
  if (send.status === "paused") return send;
7510
8193
  const statuses = send.items.map((item) => item.status);
@@ -7513,17 +8196,6 @@ function aggregate(send) {
7513
8196
  status: statuses.length === 0 ? "blocked" : statuses.every((status) => status === "completed") ? "completed" : statuses.some((status) => status === "target" || status === "settings") ? "review" : statuses.some((status) => status === "ready" || status === "uploading") ? "sending" : statuses.some((status) => status === "assessing") ? "waiting" : "blocked"
7514
8197
  };
7515
8198
  }
7516
- /**
7517
- * A completed Send that Passport has not finished with: a Work is still assessed or reapplied
7518
- * (ADR 0233). A blocked Work waits on the person's Retry, not on Passport.
7519
- */
7520
- function awaitingPassport(send) {
7521
- return send.status === "completed" && send.items.some((item) => !["completed", "blocked"].includes(item.status));
7522
- }
7523
- /** The host continues confirmed uploads and unfinished Passport assessments without a panel. */
7524
- function hostDrives(send) {
7525
- return awaitingPassport(send) || send.confirmedAt != null && ["sending", "waiting"].includes(send.status);
7526
- }
7527
8199
  /** Explicit resume retries recoverable items and recaptures a pending sign-in or profile. */
7528
8200
  function resumeState(send) {
7529
8201
  if (send.status !== "completed" && send.items.length && send.items.every((item) => item.status === "completed") && send.items.some((item) => !item.sessionAssembly)) return {
@@ -7652,15 +8324,126 @@ async function captureProfile(connection, guard, signal, failure) {
7652
8324
  }
7653
8325
  }
7654
8326
  //#endregion
8327
+ //#region apps/cli/src/account/journey/remote-guard.ts
8328
+ /**
8329
+ * The class of every method. Adding a method to either remote without classifying it here fails
8330
+ * the type check, and the guarded remote exposes only the methods named below.
8331
+ */
8332
+ const connectionClasses = {
8333
+ assertCurrent: "read",
8334
+ syncProfile: "profile-sync",
8335
+ readProfile: "read",
8336
+ addressTaken: "read",
8337
+ status: "read",
8338
+ checkAgentSources: "agent-sources-check",
8339
+ resolve: "work-resolve",
8340
+ preview: "settings-preview",
8341
+ confirm: "settings-confirm",
8342
+ reserve: "work-reserve",
8343
+ complete: "work-complete",
8344
+ replace: "works-replace",
8345
+ saveProfile: "profile-save",
8346
+ ownedPassport: "read"
8347
+ };
8348
+ const sessionClasses = {
8349
+ inventory: "read",
8350
+ assessment: "read",
8351
+ work: "session-work",
8352
+ assembly: "session-assembly",
8353
+ reserve: "session-reserve",
8354
+ complete: "session-complete",
8355
+ membership: "session-membership"
8356
+ };
8357
+ /**
8358
+ * The one place that decides whether a Send-path call may send something about the person's
8359
+ * work (ADR 0284). Rules of this kind belong here, never in a caller; `mutation` is for the
8360
+ * rules to come.
8361
+ */
8362
+ function assertMayLeave(scope, mutation) {
8363
+ if (!mayLeave(scope, mutation)) throw new ApplicationError("JOURNEY_OBSOLETE");
8364
+ }
8365
+ /** `assertMayLeave` as a question, for a caller that must skip what the guard would refuse. */
8366
+ function mayLeave(scope, _mutation) {
8367
+ const atPassport = scope.item ? itemAtPassport(scope.item) : scope.send?.items.some(itemAtPassport) ?? false;
8368
+ return !(scope.journey && obsoleteJourney(scope.journey) && !atPassport);
8369
+ }
8370
+ /**
8371
+ * The remote a Send uses, closed by default: it holds only the methods classified above. A read
8372
+ * passes through; every other method asks `assertMayLeave` first. A refusal rejects like a failed
8373
+ * remote call, so the advance blocks only that Work.
8374
+ */
8375
+ function guardSendRemote(connection, scope) {
8376
+ const through = (kind, call) => kind === "read" ? call : (...args) => guardedSendMutation(scope, kind, () => call(...args));
8377
+ const work = connectionClasses;
8378
+ const { sessions, checkAgentSources } = connection;
8379
+ return {
8380
+ subject: connection.subject,
8381
+ assertCurrent: through(work.assertCurrent, connection.assertCurrent),
8382
+ syncProfile: through(work.syncProfile, connection.syncProfile),
8383
+ readProfile: through(work.readProfile, connection.readProfile),
8384
+ addressTaken: through(work.addressTaken, connection.addressTaken),
8385
+ status: through(work.status, connection.status),
8386
+ resolve: through(work.resolve, connection.resolve),
8387
+ preview: through(work.preview, connection.preview),
8388
+ confirm: through(work.confirm, connection.confirm),
8389
+ reserve: through(work.reserve, connection.reserve),
8390
+ complete: through(work.complete, connection.complete),
8391
+ replace: through(work.replace, connection.replace),
8392
+ saveProfile: through(work.saveProfile, connection.saveProfile),
8393
+ ownedPassport: through(work.ownedPassport, connection.ownedPassport),
8394
+ ...checkAgentSources ? { checkAgentSources: through(work.checkAgentSources, checkAgentSources) } : {},
8395
+ ...sessions ? { sessions: {
8396
+ inventory: through(sessionClasses.inventory, sessions.inventory),
8397
+ assessment: through(sessionClasses.assessment, sessions.assessment),
8398
+ work: through(sessionClasses.work, sessions.work),
8399
+ assembly: through(sessionClasses.assembly, sessions.assembly),
8400
+ reserve: through(sessionClasses.reserve, sessions.reserve),
8401
+ complete: through(sessionClasses.complete, sessions.complete),
8402
+ membership: through(sessionClasses.membership, sessions.membership)
8403
+ } } : {}
8404
+ };
8405
+ }
8406
+ /** A guarded call that is not a remote method: the byte upload to a signed address. */
8407
+ function guardSendCall(scope, mutation, call) {
8408
+ return guardedSendMutation(scope, mutation, call);
8409
+ }
8410
+ /** Each session's byte upload to a signed address asks the guard too (ADR 0284). */
8411
+ function guardSessionUploads(sessions, scope) {
8412
+ return async function* (journeyId, artifact, signal) {
8413
+ for await (const session of sessions(journeyId, artifact, signal)) yield {
8414
+ ...session,
8415
+ upload: (upload, uploadSignal, progress) => guardedSendMutation(scope, "session-upload", () => session.upload(upload, uploadSignal, progress))
8416
+ };
8417
+ };
8418
+ }
8419
+ /** Named so a test can tell a guarded call from one that bypassed the guard. */
8420
+ function guardedSendMutation(scope, mutation, call) {
8421
+ try {
8422
+ assertMayLeave(scope, mutation);
8423
+ } catch (error) {
8424
+ return Promise.reject(error);
8425
+ }
8426
+ return call();
8427
+ }
8428
+ //#endregion
7655
8429
  //#region apps/cli/src/account/journey/advance.ts
7656
8430
  /** A Work Passport is done with: assessed, or stopped unless Resume retries it. */
7657
8431
  const settled = (item) => ["completed", "blocked"].includes(item.status);
8432
+ /** The byte upload to a signed address is a guarded call too (ADR 0284). */
8433
+ function guardedContext(context, scope, checksumUploads) {
8434
+ const upload = context.upload;
8435
+ return {
8436
+ ...context,
8437
+ checksumUploads,
8438
+ upload: (artifact, signed, signal) => guardSendCall(scope, "work-upload", () => upload(artifact, signed, signal))
8439
+ };
8440
+ }
7658
8441
  /** Stored state equality: key order and explicit undefined fields do not count. */
7659
8442
  const sameJson = (left, right) => isDeepStrictEqual(JSON.parse(JSON.stringify(left)), JSON.parse(JSON.stringify(right)));
7660
8443
  /** The stored Send still holds exactly this Work state, whatever else changed meanwhile. */
7661
8444
  function sameItem(items, item) {
7662
8445
  const current = items.find((value) => value.workId === item.workId);
7663
- return current !== void 0 && sameJson(current, item);
8446
+ return current === item || current !== void 0 && sameJson(current, item);
7664
8447
  }
7665
8448
  /** Shared sign-in, account and per-item rules for native Send and Manual upload. */
7666
8449
  async function advanceItems(host, signal) {
@@ -7702,7 +8485,7 @@ async function advanceItems(host, signal) {
7702
8485
  });
7703
8486
  }
7704
8487
  if (send.subject === null || send.status === "sign-in") {
7705
- const { parallelCompletes: captured, ...profile } = await host.profile(connection, signal);
8488
+ const { parallelCompletes: captured, ...profile } = await host.profile(guardSendRemote(connection, host.scope(null)), signal);
7706
8489
  const parallelCompletes = send.subject === null ? captured : send.parallelCompletes && captured;
7707
8490
  const prepared = {
7708
8491
  ...send,
@@ -7713,17 +8496,16 @@ async function advanceItems(host, signal) {
7713
8496
  else delete prepared.parallelCompletes;
7714
8497
  return host.save(prepared.status === "sending" ? aggregate(prepared) : prepared);
7715
8498
  }
7716
- const blocked = await host.preflight?.(connection, signal);
8499
+ const blocked = await host.preflight?.(guardSendRemote(connection, host.scope(null)), signal);
7717
8500
  if (blocked) return blocked;
7718
- const batch = host.batch?.() ?? pickBatch(send.items, Date.now(), send.parallelCompletes ? 3 : 1);
8501
+ const batch = host.batch?.() ?? pickBatch(send.items, send.parallelCompletes ? 3 : 1, host.poll?.bind(void 0, send));
7719
8502
  if (!batch.length) return host.applied();
7720
8503
  const busy = Math.max(0, ...batch.map((item) => item.busyRefusals ?? 0));
7721
8504
  if (busy) await wait(busyDelay(busy), signal);
7722
8505
  const checksumUploads = !send.items.some((item) => item.checksumUploadRefused);
7723
- const results = await Promise.allSettled(batch.map((item) => (host.step ?? stepItem)({
7724
- ...host.context(item),
7725
- checksumUploads
7726
- }, item, connection, signal)));
8506
+ const run = (item) => (host.step ?? stepItem)(guardedContext(host.context(item), host.scope(item), checksumUploads), item, guardSendRemote(connection, host.scope(item)), signal);
8507
+ const refill = host.refill;
8508
+ const results = refill && batch.length > 3 ? await runSessionQueue(batch, run, () => refill(guardSendRemote(connection, host.scope(null)), signal), signal) : await Promise.allSettled(batch.map(run));
7727
8509
  const failures = results.flatMap((result) => result.status === "rejected" ? [result] : []);
7728
8510
  if (failures.some(({ reason }) => isCode(reason, "REVISION_CONFLICT") || isCode(reason, "SEND_SCOPE_INVALIDATED"))) return host.stale();
7729
8511
  const first = failures[0];
@@ -7734,7 +8516,7 @@ async function advanceItems(host, signal) {
7734
8516
  const next = /* @__PURE__ */ new Map();
7735
8517
  for (const [index, result] of results.entries()) {
7736
8518
  const item = batch[index];
7737
- if (!item) continue;
8519
+ if (!item || !result) continue;
7738
8520
  if (result.status === "fulfilled") {
7739
8521
  next.set(item.workId, result.value);
7740
8522
  continue;
@@ -7803,6 +8585,44 @@ async function advanceItems(host, signal) {
7803
8585
  return blockSend(host, error, signal);
7804
8586
  }
7805
8587
  }
8588
+ /** Every queued Work was admitted as independent at preflight. Recheck delayed slots before use. */
8589
+ async function runSessionQueue(batch, run, admit, signal) {
8590
+ const results = [];
8591
+ let next = 0;
8592
+ let stopped = false;
8593
+ let admission;
8594
+ async function worker() {
8595
+ while (!stopped && next < batch.length) {
8596
+ const index = next++;
8597
+ const item = batch[index];
8598
+ if (!item) return;
8599
+ try {
8600
+ if (index >= 3) {
8601
+ admission ??= admit().finally(() => {
8602
+ admission = void 0;
8603
+ });
8604
+ if (!await admission) stopped = true;
8605
+ if (stopped) return;
8606
+ }
8607
+ signal.throwIfAborted();
8608
+ const value = await run(item);
8609
+ results[index] = {
8610
+ status: "fulfilled",
8611
+ value
8612
+ };
8613
+ if (value.status === "blocked") stopped = true;
8614
+ } catch (reason) {
8615
+ stopped = true;
8616
+ results[index] = {
8617
+ status: "rejected",
8618
+ reason
8619
+ };
8620
+ }
8621
+ }
8622
+ }
8623
+ await Promise.all(Array.from({ length: 3 }, worker));
8624
+ return results;
8625
+ }
7806
8626
  /** An unexpected failure blocks the whole send with a safe code; abort keeps propagating. */
7807
8627
  function blockSend(host, error, signal) {
7808
8628
  if (signal.aborted) throw error;
@@ -7846,35 +8666,47 @@ function completeAtUpload(send, sessions) {
7846
8666
  }
7847
8667
  async function advanceSend(dependencies, address, signal, chain) {
7848
8668
  let snapshot = requireSend(await dependencies.storage.loadSend(address.journeyId, address.sendId));
7849
- const sessionFacts = chain?.sessionFacts ?? createSessionFactsCache();
8669
+ const sessionFacts = chain.sessionFacts;
7850
8670
  sessionFacts.retain(address.journeyId, snapshot.send);
7851
- if (chain) chain.revision = snapshot.send.revision;
8671
+ chain.revision = snapshot.send.revision;
7852
8672
  if ((await dependencies.storage.loadSend(address.journeyId))?.send?.id !== snapshot.send.id) return {
7853
8673
  ...snapshot,
7854
8674
  status: "stale"
7855
8675
  };
7856
8676
  const completed = snapshot.send.status === "completed";
7857
- if (snapshot.journey.phase === "paused" || !snapshot.send.confirmedAt || ["paused", "prepared"].includes(snapshot.send.status) || completed && snapshot.send.items.every(settled)) return {
8677
+ if (!advanceStepsSend(snapshot.journey, snapshot.send)) return {
7858
8678
  ...snapshot,
7859
8679
  status: "applied"
7860
8680
  };
7861
8681
  const checkpoints = /* @__PURE__ */ new Map();
7862
8682
  const fulfilled = /* @__PURE__ */ new Set();
7863
8683
  let saving = Promise.resolve();
8684
+ let validated;
7864
8685
  async function active(item) {
7865
- const current = requireSend(await dependencies.storage.loadSend(address.journeyId));
7866
- if (current.send.id !== snapshot.send.id || !sameItem(current.send.items, checkpoints.get(item.workId) ?? item) || current.send.status === "paused" || current.journey.phase === "paused") throw new ApplicationError("REVISION_CONFLICT");
7867
- assertScope(current.journey, current.send);
8686
+ const observed = snapshot;
8687
+ const isCurrent = (candidate) => dependencies.storage.isCurrentSend(address.journeyId, candidate.send.id, candidate.journey.revision);
8688
+ let current = observed;
8689
+ if (!await isCurrent(observed)) {
8690
+ current = requireSend(await dependencies.storage.loadSend(address.journeyId));
8691
+ if (current.send.id !== observed.send.id || !await isCurrent(current)) throw new ApplicationError("REVISION_CONFLICT");
8692
+ }
8693
+ if (!sameItem(current.send.items, checkpoints.get(item.workId) ?? item) || current.send.status === "paused" || current.journey.phase === "paused") throw new ApplicationError("REVISION_CONFLICT");
8694
+ if (current !== validated) {
8695
+ assertScope(current.journey, current.send);
8696
+ validated = current;
8697
+ }
7868
8698
  return current;
7869
8699
  }
7870
8700
  const save = (proposed) => {
7871
8701
  const next = {
7872
8702
  ...proposed,
8703
+ error: proposed.status === "blocked" && !proposed.error && proposed.contentConflict && proposed.items.some((item) => item.error === "SESSION_CONTENT_CONFLICT") ? "SESSION_CONTENT_CONFLICT" : proposed.error,
7873
8704
  items: proposed.items.map((item) => {
7874
8705
  const checkpoint = checkpoints.get(item.workId);
7875
8706
  return checkpoint && !fulfilled.has(item.workId) ? {
7876
8707
  ...checkpoint,
7877
8708
  ...item,
8709
+ ...checkpoint.storedSessions !== void 0 ? { storedSessions: checkpoint.storedSessions } : {},
7878
8710
  sessionUpload: checkpoint.sessionUpload
7879
8711
  } : item;
7880
8712
  })
@@ -7890,6 +8722,7 @@ async function advanceSend(dependencies, address, signal, chain) {
7890
8722
  const send = withoutQuestions(snapshot.send);
7891
8723
  try {
7892
8724
  assertScope(snapshot.journey, snapshot.send);
8725
+ validated = snapshot;
7893
8726
  } catch (error) {
7894
8727
  if (completed) return save({
7895
8728
  ...snapshot.send,
@@ -7925,24 +8758,83 @@ async function advanceSend(dependencies, address, signal, chain) {
7925
8758
  };
7926
8759
  if (send !== snapshot.send) return save(send);
7927
8760
  let independentItems = [];
8761
+ let queuedIds = [];
7928
8762
  const preparation = createSessionPreparation(dependencies, sessionFacts);
8763
+ const sessionStepDependencies = (item) => {
8764
+ const { artifacts } = preparation.dependencies;
8765
+ if (!artifacts.sessions) return preparation.dependencies;
8766
+ const scope = {
8767
+ journey: snapshot.journey,
8768
+ send: snapshot.send,
8769
+ item
8770
+ };
8771
+ return {
8772
+ ...preparation.dependencies,
8773
+ artifacts: {
8774
+ ...artifacts,
8775
+ sessions: guardSessionUploads(artifacts.sessions, scope)
8776
+ }
8777
+ };
8778
+ };
8779
+ let queueActive = false;
8780
+ async function readAdmission(connection, currentItems, signal) {
8781
+ if (!connection.sessions) throw new ApplicationError("INVALID_STATE");
8782
+ const inventory = await connection.sessions.inventory(signal);
8783
+ const conflicts = await sessionAccessBlocked(preparation.dependencies, snapshot.journey.journeyId, currentItems, inventory, signal);
8784
+ return {
8785
+ conflicts,
8786
+ independent: conflicts.length ? [] : await sessionIndependentItems(preparation.dependencies, snapshot.journey.journeyId, currentItems, inventory, signal)
8787
+ };
8788
+ }
8789
+ let stopObservation;
7929
8790
  const cleanup = createCleanup();
7930
8791
  try {
7931
8792
  return await advanceItems({
7932
8793
  send,
8794
+ poll: chain.poll,
7933
8795
  failure: dependencies.failure,
7934
8796
  ...usesSessionSend(dependencies, send) ? {
7935
8797
  batch: () => {
7936
8798
  const limit = send.parallelCompletes ? 3 : 1;
7937
- return independentItems.length ? pickBatch(independentItems, Date.now(), limit) : pickBatch(send.items, Date.now(), limit);
8799
+ const candidates = independentItems.length ? independentItems : send.items;
8800
+ const planned = pickBatch(candidates, limit === 3 && independentItems.length ? candidates.length : limit, (works) => chain.poll(send, works));
8801
+ queueActive = limit === 3 && planned.length > limit && planned.some((item) => item.status === "uploading") && planned.every((item) => !item.busyRefusals && !item.sessionUpload?.nextAttemptAt);
8802
+ if (queueActive) queuedIds = planned.map((item) => item.workId);
8803
+ return queueActive ? planned : planned.slice(0, limit);
7938
8804
  },
7939
8805
  preflight: async (connection, preflightSignal) => {
7940
8806
  if (!connection.sessions) throw new ApplicationError("INVALID_STATE");
7941
- if (!send.items.some((item) => item.status === "uploading" && !item.sessionAssembly && !item.receipt && !item.reservation)) return null;
7942
- const inventory = await connection.sessions.inventory(preflightSignal);
7943
- const conflicts = await sessionAccessBlocked(preparation.dependencies, snapshot.journey.journeyId, send.items, inventory, preflightSignal);
7944
- if (conflicts.length === 0) {
7945
- independentItems = await sessionIndependentItems(preparation.dependencies, snapshot.journey.journeyId, send.items, inventory, preflightSignal);
8807
+ const checked = send.items.filter((item) => mayLeave({
8808
+ journey: snapshot.journey,
8809
+ send: snapshot.send,
8810
+ item
8811
+ }, "session-membership"));
8812
+ if (!checked.some((item) => item.status === "uploading" && !item.sessionAssembly && !item.receipt && !item.reservation)) return null;
8813
+ let admission;
8814
+ try {
8815
+ admission = await readAdmission(connection, checked, preflightSignal);
8816
+ } catch (error) {
8817
+ if (!(error instanceof SessionContentConflict)) throw error;
8818
+ await connection.assertCurrent(preflightSignal);
8819
+ dependencies.failure(error.code, error);
8820
+ const items = send.items.map((item) => error.affectedWorkIds.has(item.workId) && !["completed", "blocked"].includes(item.status) && !item.sessionAssembly ? {
8821
+ ...item,
8822
+ status: "blocked",
8823
+ error: error.code
8824
+ } : item);
8825
+ const next = aggregate({
8826
+ ...send,
8827
+ items,
8828
+ contentConflict: error.contentConflict
8829
+ });
8830
+ return save({
8831
+ ...next,
8832
+ error: next.status === "blocked" ? error.code : null
8833
+ });
8834
+ }
8835
+ const { conflicts, independent } = admission;
8836
+ if (!conflicts.length) {
8837
+ independentItems = independent;
7946
8838
  return null;
7947
8839
  }
7948
8840
  await connection.assertCurrent(preflightSignal);
@@ -7962,6 +8854,7 @@ async function advanceSend(dependencies, address, signal, chain) {
7962
8854
  const { accessConflict: _previous, accessConflictSessionCount: _count, ...current } = item;
7963
8855
  const itemConflicts = conflicts.filter((entry) => entry.workId === item.workId);
7964
8856
  const conflict = itemConflicts.find((entry) => entry.accountAccess !== null) ?? itemConflicts[0];
8857
+ if (item.status === "blocked" && item.error === "SESSION_CONTENT_CONFLICT") return item;
7965
8858
  if (!conflict) return {
7966
8859
  ...current,
7967
8860
  status: "blocked",
@@ -7978,35 +8871,74 @@ async function advanceSend(dependencies, address, signal, chain) {
7978
8871
  })
7979
8872
  });
7980
8873
  },
7981
- step: (context, item, connection, stepSignal) => stepSessionWork(preparation.dependencies, snapshot.journey.journeyId, context, item, connection, stepSignal, (next) => {
7982
- const pending = saving.then(async () => {
7983
- const current = await active(item);
7984
- if (current.send.revision !== snapshot.send.revision || current.journey.revision !== snapshot.journey.revision) throw new ApplicationError("REVISION_CONFLICT");
7985
- stepSignal.throwIfAborted();
7986
- const result = await saveSend(dependencies, current, {
7987
- ...current.send,
7988
- items: current.send.items.map((value) => value.workId === next.workId ? next : value)
7989
- });
7990
- if (result.status === "stale") throw new ApplicationError("REVISION_CONFLICT");
7991
- snapshot = requireSend(result);
7992
- const saved = snapshot.send.items.find((value) => value.workId === next.workId);
7993
- if (!saved) throw new ApplicationError("INVALID_STATE");
7994
- checkpoints.set(next.workId, saved);
8874
+ refill: async (connection, refillSignal) => {
8875
+ let checked;
8876
+ try {
8877
+ checked = await readAdmission(connection, snapshot.send.items, refillSignal);
8878
+ } catch {
8879
+ refillSignal.throwIfAborted();
8880
+ return false;
8881
+ }
8882
+ return !checked.conflicts.length && queuedIds.every((workId) => {
8883
+ return snapshot.send.items.find((entry) => entry.workId === workId)?.sessionAssembly || checked.independent.some((entry) => entry.workId === workId);
7995
8884
  });
7996
- saving = pending.catch(() => void 0);
7997
- return pending;
7998
- }).then(({ busyRefusals: _busy, ...result }) => {
8885
+ },
8886
+ step: async (context, item, connection, stepSignal) => {
8887
+ if (!stopObservation && (item.status === "ready" || item.status === "uploading")) stopObservation = dependencies.observeUpload?.(connection, () => snapshot.send, stepSignal);
8888
+ const checkpoint = (next, transfer = false) => {
8889
+ const pending = saving.then(async () => {
8890
+ const current = await active(item);
8891
+ if (current.send.revision !== snapshot.send.revision || current.journey.revision !== snapshot.journey.revision) throw new ApplicationError("REVISION_CONFLICT");
8892
+ stepSignal.throwIfAborted();
8893
+ if (transfer && next.sessionUpload) {
8894
+ snapshot = await saveTransfer(dependencies, current, next.workId, {
8895
+ sessionUpload: next.sessionUpload,
8896
+ ...next.storedSessions === void 0 ? {} : { storedSessions: next.storedSessions }
8897
+ });
8898
+ validated = snapshot;
8899
+ } else {
8900
+ const result = await saveSend(dependencies, current, {
8901
+ ...current.send,
8902
+ items: current.send.items.map((value) => value.workId === next.workId ? next : value)
8903
+ });
8904
+ if (result.status === "stale") throw new ApplicationError("REVISION_CONFLICT");
8905
+ snapshot = requireSend(result);
8906
+ }
8907
+ const saved = snapshot.send.items.find((value) => value.workId === next.workId);
8908
+ if (!saved) throw new ApplicationError("INVALID_STATE");
8909
+ checkpoints.set(next.workId, saved);
8910
+ });
8911
+ saving = pending.catch(() => void 0);
8912
+ return pending;
8913
+ };
8914
+ const step = (current) => stepSessionWork(sessionStepDependencies(current), snapshot.journey.journeyId, context, current, connection, stepSignal, (next) => checkpoint(next, true));
8915
+ let result = await step(item);
8916
+ if (queueActive && item.status === "ready" && result.status === "uploading" && mayLeave({
8917
+ journey: snapshot.journey,
8918
+ send: snapshot.send,
8919
+ item: result
8920
+ }, "session-reserve")) {
8921
+ await checkpoint(result);
8922
+ result = await step(result);
8923
+ }
8924
+ if (queueActive && !sameJson(result, checkpoints.get(item.workId) ?? item)) await checkpoint(result);
7999
8925
  fulfilled.add(item.workId);
8000
- return result;
8001
- })
8926
+ const { busyRefusals: _busy, ...finished } = result;
8927
+ return finished;
8928
+ }
8002
8929
  } : {},
8003
- connect: chain?.connect ?? dependencies.auth.connect,
8930
+ connect: chain.connect,
8931
+ scope: (item) => ({
8932
+ journey: snapshot.journey,
8933
+ send: snapshot.send,
8934
+ item
8935
+ }),
8004
8936
  profile: (connection, profileSignal) => prepareProfile(dependencies, snapshot, connection, profileSignal),
8005
8937
  context: (item) => {
8006
8938
  const binding = snapshot.journey.works.find((value) => value.id === item.workId)?.binding;
8007
8939
  return {
8008
8940
  snapshot: true,
8009
- ...chain ? { fresh: chain.fresh } : {},
8941
+ fresh: chain.fresh,
8010
8942
  active: async () => {
8011
8943
  await active(item);
8012
8944
  },
@@ -8032,6 +8964,7 @@ async function advanceSend(dependencies, address, signal, chain) {
8032
8964
  cleanup.failed(error);
8033
8965
  throw error;
8034
8966
  } finally {
8967
+ stopObservation?.();
8035
8968
  await cleanup.run(() => preparation.close());
8036
8969
  }
8037
8970
  }
@@ -8102,6 +9035,7 @@ function pathHash(item) {
8102
9035
  /** Manual upload: native Send's commands and recovery rules over one completed result's ZIPs. */
8103
9036
  function createManualUploader(dependencies) {
8104
9037
  const { storage } = dependencies;
9038
+ const poll = createAssessmentPoll();
8105
9039
  const running = /* @__PURE__ */ new Map();
8106
9040
  function save(prior, next, action) {
8107
9041
  return storage.commit({
@@ -8240,7 +9174,11 @@ function createManualUploader(dependencies) {
8240
9174
  error: null
8241
9175
  };
8242
9176
  if (connection) try {
8243
- profile = await captureProfile(connection, guard(upload), signal, dependencies.failure);
9177
+ profile = await captureProfile(guardSendRemote(connection, {
9178
+ journey: null,
9179
+ send: upload,
9180
+ item: null
9181
+ }), guard(upload), signal, dependencies.failure);
8244
9182
  } catch (error) {
8245
9183
  if (error instanceof ApplicationError && error.code === "REVISION_CONFLICT") return save(upload, upload, input);
8246
9184
  throw error;
@@ -8319,12 +9257,18 @@ function createManualUploader(dependencies) {
8319
9257
  let bindings = [];
8320
9258
  return advanceItems({
8321
9259
  send: upload,
9260
+ poll,
8322
9261
  failure: dependencies.failure,
8323
9262
  save: (next) => save(upload, next),
8324
9263
  connect: async (connectSignal) => {
8325
9264
  bindings = await storage.bindings();
8326
9265
  return dependencies.auth.connect(connectSignal);
8327
9266
  },
9267
+ scope: (item) => ({
9268
+ journey: null,
9269
+ send: upload,
9270
+ item
9271
+ }),
8328
9272
  profile: (connection, profileSignal) => captureProfile(connection, guard(upload), profileSignal, dependencies.failure),
8329
9273
  context: (item) => ({
8330
9274
  active: guard(upload, item),
@@ -9541,6 +10485,7 @@ function createJourneyFiles(storage) {
9541
10485
  return async (journeyId, workId, signal) => {
9542
10486
  const state = await storage.current(journeyId);
9543
10487
  if (!state?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
10488
+ if (state.phase === "completed" && state.outputVersion !== 1) throw new ApplicationError("SCOPE_NOT_READY");
9544
10489
  assertJourneyPrivacyReady(state, true);
9545
10490
  if (state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
9546
10491
  const work = state.works.find((item) => item.id === workId);
@@ -9575,6 +10520,7 @@ function createJourneyFiles(storage) {
9575
10520
  const sendRecoveryCodes = [
9576
10521
  "SESSION_MEMBERSHIP_CONFLICT",
9577
10522
  "SESSION_UPLOAD_EXHAUSTED",
10523
+ "WORK_SESSION_CAPACITY_EXCEEDED",
9578
10524
  "LEGACY_SEND_RECOVERY_REQUIRED",
9579
10525
  "ASSESSMENT_INCOMPLETE",
9580
10526
  "SESSION_PUBLICATION_FENCE_REQUIRED"
@@ -9630,10 +10576,14 @@ async function requestSend(dependencies, raw, hostId, source) {
9630
10576
  if (!hostId) throw new ApplicationError("INVALID_TRANSITION");
9631
10577
  const included = current.journey.works.filter(isIncludedWork);
9632
10578
  if (!sameIds$1(input.included.map((work) => work.workId), included.map((work) => work.id)) || !input.included.every((item) => included.some((work) => work.id === item.workId && work.access === item.access && work.accessConfirmed)) || !sameIds$1(input.omittedWorkIds, current.journey.works.filter((work) => !isIncludedWork(work)).map((work) => work.id))) throw new ApplicationError("CONFIRMATION_REQUIRED");
9633
- if (!binding) binding = {
9634
- subject: (await dependencies.auth.connect(new AbortController().signal))?.subject ?? null,
9635
- at: dependencies.now()
9636
- };
10579
+ if (!binding) {
10580
+ const connection = await dependencies.auth.connect(new AbortController().signal);
10581
+ if (!connection) throw new ApplicationError(dependencies.auth.configured ? "ACCOUNT_SIGNED_OUT" : "ACCOUNT_NOT_CONFIGURED");
10582
+ binding = {
10583
+ subject: connection.subject,
10584
+ at: dependencies.now()
10585
+ };
10586
+ }
9637
10587
  const { subject, at } = binding;
9638
10588
  const result = await dependencies.storage.commitSend({
9639
10589
  journeyId: input.journeyId,
@@ -9695,10 +10645,11 @@ async function requestError(dependencies, send, hostId, signal) {
9695
10645
  * next phase exactly as between advances. The advance returns after three phases or once no
9696
10646
  * picked Work is mid-upload, so later Works continue from the caller's next advance.
9697
10647
  */
9698
- async function advanceWork(dependencies, address, signal, phases = 3, sessionFacts = createSessionFactsCache()) {
10648
+ async function advanceWork(dependencies, address, signal, phases = 3, sessionFacts = createSessionFactsCache(), poll = createAssessmentPoll()) {
9699
10649
  let connection;
9700
10650
  const chain = {
9701
10651
  sessionFacts,
10652
+ poll,
9702
10653
  fresh: /* @__PURE__ */ new Map(),
9703
10654
  async connect(connectSignal) {
9704
10655
  if (connection === void 0) connection = await dependencies.auth.connect(connectSignal);
@@ -9713,7 +10664,102 @@ async function advanceWork(dependencies, address, signal, phases = 3, sessionFac
9713
10664
  /** The phase saved progress and left its Work mid-upload, with nothing else to report. */
9714
10665
  function uploading(result, before) {
9715
10666
  const send = result.send;
9716
- return result.status === "applied" && send !== null && before !== void 0 && send.revision > before && send.status === "sending" && send.error === null && pickBatch(send.items, Date.now(), send.parallelCompletes ? 3 : 1)[0]?.status === "uploading";
10667
+ return result.status === "applied" && send !== null && before !== void 0 && send.revision > before && send.status === "sending" && send.error === null && pickBatch(send.items, send.parallelCompletes ? 3 : 1)[0]?.status === "uploading";
10668
+ }
10669
+ //#endregion
10670
+ //#region apps/cli/src/account/journey/upload-observer.ts
10671
+ const intervalMs = 15e3;
10672
+ const budget = 3;
10673
+ /** A tab polls only the in-flight upload owner's read capability. No store or upload-slot acquisition. */
10674
+ function createUploadObserver() {
10675
+ let active;
10676
+ let retained = "";
10677
+ let cursor = 0;
10678
+ let next = 0;
10679
+ let reading;
10680
+ const observations = /* @__PURE__ */ new Map();
10681
+ function start(connection, current, signal) {
10682
+ const send = current();
10683
+ const scope = JSON.stringify([
10684
+ send.journeyId,
10685
+ send.id,
10686
+ send.subject
10687
+ ]);
10688
+ if (scope !== retained) {
10689
+ observations.clear();
10690
+ cursor = 0;
10691
+ next = 0;
10692
+ retained = scope;
10693
+ }
10694
+ const stop = new AbortController();
10695
+ const flight = {
10696
+ connection,
10697
+ current,
10698
+ signal: AbortSignal.any([signal, stop.signal])
10699
+ };
10700
+ active = flight;
10701
+ return () => {
10702
+ stop.abort();
10703
+ if (active === flight) active = void 0;
10704
+ };
10705
+ }
10706
+ function accepted(send) {
10707
+ return send.items.filter((item) => item.status === "assessing" && item.sessionAssembly);
10708
+ }
10709
+ async function refresh(flight) {
10710
+ const remote = flight.connection.sessions;
10711
+ if (!remote) return;
10712
+ const items = accepted(flight.current());
10713
+ if (!items.length) return;
10714
+ const startAt = cursor % items.length;
10715
+ const selected = [...items.slice(startAt), ...items.slice(0, startAt)].slice(0, budget);
10716
+ const signal = AbortSignal.any([flight.signal, AbortSignal.timeout(8e3)]);
10717
+ for (const item of selected) {
10718
+ const revision = item.sessionAssembly?.revision;
10719
+ if (revision === void 0 || signal.aborted) break;
10720
+ cursor = (cursor + 1) % items.length;
10721
+ let state = "unavailable";
10722
+ try {
10723
+ const assembly = await remote.assembly(item.workId, signal);
10724
+ if (assembly?.revision === revision) {
10725
+ if (assembly.error) state = "failed";
10726
+ else if (assembly.completedRevision !== revision || !assembly.scanId) state = "assembling";
10727
+ else state = await remote.assessment(assembly.scanId, signal);
10728
+ }
10729
+ } catch {
10730
+ state = "unavailable";
10731
+ }
10732
+ if (flight.signal.aborted || active !== flight) return;
10733
+ observations.set(item.workId, {
10734
+ workId: item.workId,
10735
+ revision,
10736
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
10737
+ state
10738
+ });
10739
+ }
10740
+ }
10741
+ async function read(address) {
10742
+ const flight = active;
10743
+ if (!flight || flight.signal.aborted) return [];
10744
+ const send = flight.current();
10745
+ if (send.journeyId !== address.journeyId || send.id !== address.sendId || send.subject !== flight.connection.subject || send.status !== "sending") return [];
10746
+ if (!reading && Date.now() >= next) {
10747
+ next = Date.now() + intervalMs;
10748
+ reading = refresh(flight).finally(() => {
10749
+ reading = void 0;
10750
+ });
10751
+ }
10752
+ await reading;
10753
+ if (active !== flight || flight.signal.aborted) return [];
10754
+ return accepted(flight.current()).flatMap((item) => {
10755
+ const value = observations.get(item.workId);
10756
+ return value && value.revision === item.sessionAssembly?.revision ? [value] : [];
10757
+ });
10758
+ }
10759
+ return {
10760
+ start,
10761
+ read
10762
+ };
9717
10763
  }
9718
10764
  //#endregion
9719
10765
  //#region apps/cli/src/account/journey/send.ts
@@ -9741,9 +10787,21 @@ function createJourneySender(dependencies, phases = 3, host = {
9741
10787
  const pendingElsewhere = (send) => !host.serving && send?.request !== void 0 && !send.confirmedAt;
9742
10788
  const progress = /* @__PURE__ */ new Map();
9743
10789
  let packagingRetry;
10790
+ const observer = createUploadObserver();
9744
10791
  const sessionFacts = createSessionFactsCache();
10792
+ const poll = createAssessmentPoll();
9745
10793
  const running = /* @__PURE__ */ new Map();
9746
10794
  const runKey = (address) => `${address.journeyId}:${address.sendId}`;
10795
+ const remembered = /* @__PURE__ */ new Set();
10796
+ const remember = async (journey) => {
10797
+ if (!dependencies.rememberHides || remembered.has(journey.journeyId)) return;
10798
+ try {
10799
+ await dependencies.rememberHides(journey);
10800
+ remembered.add(journey.journeyId);
10801
+ } catch (error) {
10802
+ dependencies.failure("REMEMBERED_HIDES_FAILED", error);
10803
+ }
10804
+ };
9747
10805
  const load = async (address) => {
9748
10806
  const latest = requireSend(await dependencies.storage.loadSend(address.journeyId));
9749
10807
  sessionFacts.retain(address.journeyId, latest.send);
@@ -9751,12 +10809,14 @@ function createJourneySender(dependencies, phases = 3, host = {
9751
10809
  return latest;
9752
10810
  };
9753
10811
  const sender = {
10812
+ uploadAssessments: observer.read,
9754
10813
  scopeValid(snapshot) {
9755
10814
  return !snapshot.send || (snapshot.send.request && !snapshot.send.confirmedAt ? requestMatches(snapshot.send, snapshot.journey) : scopeDigest(snapshot.journey) === snapshot.send.scopeDigest);
9756
10815
  },
9757
10816
  request: (input, source = "conversation") => requestSend(dependencies, input, hostId, source),
9758
10817
  async get(address) {
9759
- const snapshot = await dependencies.storage.loadSend(address.journeyId, address.sendId);
10818
+ const snapshot = await dependencies.storage.loadSend(address.journeyId, address.sendId, address.observedRevision);
10819
+ if (!snapshot && address.observedRevision !== void 0) return null;
9760
10820
  sessionFacts.retain(address.journeyId, snapshot?.send ?? null);
9761
10821
  if (host.serving && !address.sendId && snapshot?.send?.request && !snapshot.send.confirmedAt && snapshot.send.status !== "blocked") {
9762
10822
  const error = await requestError(dependencies, snapshot.send, hostId, new AbortController().signal);
@@ -9784,6 +10844,18 @@ function createJourneySender(dependencies, phases = 3, host = {
9784
10844
  }
9785
10845
  };
9786
10846
  },
10847
+ /** Link for the current native Send's authenticated owner, independent of Work assessment. */
10848
+ async ownedPassport(address, signal) {
10849
+ const { send } = await load(address);
10850
+ if (!send.confirmedAt || send.status !== "completed" || !send.subject || !send.items.length || !send.items.every((item) => item.sessionAssembly)) throw new ApplicationError("INVALID_TRANSITION");
10851
+ const connection = await dependencies.auth.connect(signal);
10852
+ if (!connection) throw new ApplicationError("ACCOUNT_SIGNED_OUT");
10853
+ if (connection.subject !== send.subject) throw new ApplicationError("IDENTITY_REJECTED");
10854
+ const url = await connection.ownedPassport(signal);
10855
+ await load(address);
10856
+ await connection.assertCurrent(signal);
10857
+ return { url };
10858
+ },
9787
10859
  /** The owner's current address, links and account email; null while signed out. */
9788
10860
  async ownerProfile(signal) {
9789
10861
  const connection = await dependencies.auth.connect(signal);
@@ -9814,8 +10886,12 @@ function createJourneySender(dependencies, phases = 3, host = {
9814
10886
  if (!connection) throw new ApplicationError("ACCOUNT_SIGNED_OUT");
9815
10887
  if (latest.send.subject !== connection.subject) throw new ApplicationError("IDENTITY_REJECTED");
9816
10888
  try {
9817
- const current = await connection.readProfile(signal);
9818
- await connection.saveProfile(mergeOwnerProfile(current.profile, chosen), signal);
10889
+ const remote = guardSendRemote(connection, {
10890
+ ...latest,
10891
+ item: null
10892
+ });
10893
+ const current = await remote.readProfile(signal);
10894
+ await remote.saveProfile(mergeOwnerProfile(current.profile, chosen), signal);
9819
10895
  } catch (error) {
9820
10896
  if (signal.aborted || error instanceof ApplicationError) throw error;
9821
10897
  const code = error instanceof JourneyRemoteError && error.status === 409 ? addressRefusals[error.code] : void 0;
@@ -9879,7 +10955,12 @@ function createJourneySender(dependencies, phases = 3, host = {
9879
10955
  }
9880
10956
  for (const item of inventory && recover && snapshot.send ? draft.items : []) {
9881
10957
  const current = inventory?.works.find((work) => work.logicalWorkId === item.workId);
9882
- const assembly = await remote.assembly(item.workId, signal);
10958
+ const earlier = snapshot.send?.items.find((value) => value.workId === item.workId);
10959
+ const assembly = await guardSendRemote(connection, {
10960
+ journey: snapshot.journey,
10961
+ send: snapshot.send,
10962
+ item: earlier ?? item
10963
+ }).sessions?.assembly(item.workId, signal);
9883
10964
  const selected = createSessionKeys();
9884
10965
  for await (const session of sessions(input.journeyId, item.artifact, signal)) await selected.add(session);
9885
10966
  const currentKeys = current?.sessions.map((session) => session.key) ?? [];
@@ -9955,7 +11036,10 @@ function createJourneySender(dependencies, phases = 3, host = {
9955
11036
  error: null
9956
11037
  };
9957
11038
  if (connection) try {
9958
- profile = await prepareProfile(dependencies, snapshot, connection, signal);
11039
+ profile = await prepareProfile(dependencies, snapshot, guardSendRemote(connection, {
11040
+ ...snapshot,
11041
+ item: null
11042
+ }), signal);
9959
11043
  } catch (error) {
9960
11044
  if (error instanceof ApplicationError && error.code === "REVISION_CONFLICT") return saveStart(snapshot.send);
9961
11045
  throw error;
@@ -9963,12 +11047,14 @@ function createJourneySender(dependencies, phases = 3, host = {
9963
11047
  signal.throwIfAborted();
9964
11048
  const finalAuthorizationError = await requestError(dependencies, snapshot.send, hostId, signal);
9965
11049
  if (finalAuthorizationError) return block(snapshot, snapshot.send, finalAuthorizationError);
9966
- return saveStart({
11050
+ const started = await saveStart({
9967
11051
  ...snapshot.send,
9968
11052
  subject: connection?.subject ?? snapshot.send.subject,
9969
11053
  confirmedAt: snapshot.send.request && !connection ? null : dependencies.now(),
9970
11054
  ...profile
9971
11055
  });
11056
+ if (started.send?.confirmedAt) await remember(snapshot.journey);
11057
+ return started;
9972
11058
  },
9973
11059
  async respond(raw) {
9974
11060
  const input = sendChoiceSchema.parse(raw);
@@ -10031,6 +11117,7 @@ function createJourneySender(dependencies, phases = 3, host = {
10031
11117
  const stop = new AbortController();
10032
11118
  const transferDependencies = {
10033
11119
  ...dependencies,
11120
+ ...host.serving ? { observeUpload: observer.start } : {},
10034
11121
  sessionTransfer: (workId, transfer) => {
10035
11122
  progress.set(`${key}:${workId}`, transfer);
10036
11123
  dependencies.sessionTransfer?.(workId, transfer);
@@ -10039,17 +11126,18 @@ function createJourneySender(dependencies, phases = 3, host = {
10039
11126
  const lifetime = AbortSignal.any([signal, stop.signal]);
10040
11127
  const result = (async () => {
10041
11128
  const currentSnapshot = await dependencies.storage.loadSend(address.journeyId);
10042
- if (!currentSnapshot?.send) return advanceWork(transferDependencies, address, lifetime, phases, sessionFacts);
11129
+ if (!currentSnapshot?.send) return advanceWork(transferDependencies, address, lifetime, phases, sessionFacts, poll);
10043
11130
  let snapshot = requireSend(currentSnapshot);
10044
11131
  if (snapshot.send.id !== address.sendId) return {
10045
11132
  ...snapshot,
10046
11133
  status: "stale"
10047
11134
  };
11135
+ if (snapshot.send.confirmedAt) await remember(snapshot.journey);
10048
11136
  if (!host.serving && snapshot.send.request && !snapshot.send.confirmedAt) return {
10049
11137
  ...snapshot,
10050
11138
  status: "applied"
10051
11139
  };
10052
- if (!host.serving && hostDrives(snapshot.send) && await host.focused?.(address.journeyId)) return {
11140
+ if (!host.serving && hostDrives(snapshot.journey, snapshot.send) && await host.focused?.(address.journeyId)) return {
10053
11141
  ...snapshot,
10054
11142
  status: "applied"
10055
11143
  };
@@ -10145,7 +11233,7 @@ function createJourneySender(dependencies, phases = 3, host = {
10145
11233
  confirmedOmittedWorkIds: snapshot.send.omitted.map((item) => item.workId)
10146
11234
  }, lifetime);
10147
11235
  }
10148
- return advanceWork(transferDependencies, address, lifetime, phases, sessionFacts);
11236
+ return advanceWork(transferDependencies, address, lifetime, phases, sessionFacts, poll);
10149
11237
  })().catch(async (error) => {
10150
11238
  if (!stop.signal.aborted || signal.aborted) throw error;
10151
11239
  return {
@@ -10242,6 +11330,7 @@ function createJourneySendAuth(account, requestFetch) {
10242
11330
  if (verified.identityId !== credential.identityId) throw new ApplicationError("IDENTITY_REJECTED");
10243
11331
  return createJourneyOwnerClient({
10244
11332
  apiUrl: account.configuration.passportApiUrl,
11333
+ ...account.configuration.passportWebOrigin ? { webOrigin: account.configuration.passportWebOrigin } : {},
10245
11334
  subject: verified.identityId,
10246
11335
  fetch: ownerFetch,
10247
11336
  async authorize() {
@@ -10631,9 +11720,9 @@ async function privateStat$2(path, stat, directory) {
10631
11720
  if (!(process.platform === "win32" ? await windowsOwnerOnly(path, directory ? "directory" : "file") : stat.uid === process.getuid?.() && (stat.mode & 63) === 0)) throw new ApplicationError("SENSITIVE_TERMS_UNSAFE_PATH");
10632
11721
  }
10633
11722
  /** Private user settings survive control-plane database reset. No learned entities enter here. */
10634
- function createSensitiveTermsFile(stateDirectory) {
11723
+ function createSensitiveTermsFile(stateDirectory, fileName = "sensitive-terms.json") {
10635
11724
  const directory = join(stateDirectory, "settings");
10636
- const path = join(directory, "sensitive-terms.json");
11725
+ const path = join(directory, fileName);
10637
11726
  let observed = false;
10638
11727
  async function checkedDirectory(create) {
10639
11728
  await privateStat$2(stateDirectory, await lstat(stateDirectory), true);
@@ -10740,6 +11829,41 @@ function createSensitiveTermsFile(stateDirectory) {
10740
11829
  };
10741
11830
  }
10742
11831
  //#endregion
11832
+ //#region apps/cli/src/remembered-hides.ts
11833
+ /**
11834
+ * Each value on its own: one value the file cannot hold never drops the others. `journeys` records
11835
+ * the journeys already kept, so a later restart never brings back a value the person removed.
11836
+ */
11837
+ async function appendRememberedHides(store, journeys, journeyId, candidates) {
11838
+ const done = await journeys.read();
11839
+ if (done.terms.includes(journeyId)) return;
11840
+ const values = candidates.filter((value) => {
11841
+ try {
11842
+ return normalizeSensitiveTerms([value]).length === 1;
11843
+ } catch {
11844
+ return false;
11845
+ }
11846
+ });
11847
+ if (values.length) {
11848
+ const saved = await store.read();
11849
+ const next = normalizeSensitiveTerms([...saved.terms, ...values]);
11850
+ if (next.length !== saved.terms.length) await store.replace(saved.revision, next);
11851
+ }
11852
+ await journeys.replace(done.revision, [...done.terms, journeyId]);
11853
+ }
11854
+ /** Counts only, so no other remembered value reaches the agent that asked. */
11855
+ async function removeRememberedHides(store, values) {
11856
+ const removed = new Set(values.map((item) => item.trim().normalize("NFC")));
11857
+ const saved = await store.read();
11858
+ const kept = saved.terms.filter((term) => !removed.has(term));
11859
+ if (kept.length !== saved.terms.length) await store.replace(saved.revision, kept);
11860
+ const found = saved.terms.length - kept.length;
11861
+ return {
11862
+ removed: found,
11863
+ notFound: removed.size - found
11864
+ };
11865
+ }
11866
+ //#endregion
10743
11867
  //#region apps/cli/src/account/snapshot.ts
10744
11868
  function accountSnapshot(value) {
10745
11869
  const empty = {
@@ -11164,8 +12288,10 @@ function createPassportConnection(options) {
11164
12288
  //#region apps/cli/src/account/index.ts
11165
12289
  /** Composes the concrete Identity client, credential file and backend check for the account. */
11166
12290
  function createAccountPorts(options) {
12291
+ const webOrigin = passportWebOrigin(options.passportApiUrl, options.passportWebOrigin);
11167
12292
  return {
11168
12293
  configuration: {
12294
+ ...webOrigin ? { passportWebOrigin: webOrigin } : {},
11169
12295
  identityUrl: options.identityUrl,
11170
12296
  clientId: options.clientId,
11171
12297
  passportApiUrl: options.passportApiUrl
@@ -11958,6 +13084,8 @@ async function compose(options, callbacks, bundles) {
11958
13084
  traceStartup("library-end");
11959
13085
  const outputDir = library.path;
11960
13086
  const sensitiveTerms = createSensitiveTermsFile(stateDir);
13087
+ const rememberedHides = createSensitiveTermsFile(stateDir, "remembered-hides.json");
13088
+ const rememberedJourneys = createSensitiveTermsFile(stateDir, "remembered-hides-journeys.json");
11961
13089
  const processing = {
11962
13090
  sensitiveTerms,
11963
13091
  adapters: createNativeAdapters(),
@@ -11996,6 +13124,7 @@ async function compose(options, callbacks, bundles) {
11996
13124
  };
11997
13125
  const prepareJourney = createJourneyPreparation({
11998
13126
  storage: journeyStorage,
13127
+ rememberedHides: async () => (await rememberedHides.read()).terms,
11999
13128
  harnesses,
12000
13129
  processing,
12001
13130
  execution,
@@ -12019,6 +13148,7 @@ async function compose(options, callbacks, bundles) {
12019
13148
  identityUrl: options.identityUrl,
12020
13149
  clientId: options.identityClientId,
12021
13150
  passportApiUrl: options.passportApiUrl,
13151
+ ...options.passportWebOrigin ? { passportWebOrigin: options.passportWebOrigin } : {},
12022
13152
  onFatal: fatalStore.fail
12023
13153
  });
12024
13154
  const account = createAccount(accountPorts);
@@ -12043,6 +13173,7 @@ async function compose(options, callbacks, bundles) {
12043
13173
  auth: sendAuth,
12044
13174
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
12045
13175
  failure: (code, cause) => diagnostics.onFailure(code, cause),
13176
+ rememberHides: (journey) => appendRememberedHides(rememberedHides, rememberedJourneys, journey.journeyId, rememberedHideValues(journey)),
12046
13177
  sessionTransfer: (workId, transfer) => diagnostics.onActivity({
12047
13178
  event: "journey.session_transfer",
12048
13179
  at: (/* @__PURE__ */ new Date()).toISOString(),
@@ -12203,6 +13334,7 @@ async function compose(options, callbacks, bundles) {
12203
13334
  })();
12204
13335
  return {
12205
13336
  application,
13337
+ rememberedHides,
12206
13338
  remoteWorkers: createRemotePreflight({
12207
13339
  storage: journeyStorage,
12208
13340
  harnesses,
@@ -12315,7 +13447,7 @@ const delivery = z.object({
12315
13447
  output: z.string().min(1).max(4096)
12316
13448
  });
12317
13449
  /** `signal` carries the process's SIGINT/SIGTERM to long procedures such as journey.prepare. */
12318
- async function invoke(runtime, command, value, signal) {
13450
+ async function invoke(runtime, command, value, signal, getAudience = "agent") {
12319
13451
  const app = runtime.application;
12320
13452
  markAgentPresence(runtime.stateDir, value);
12321
13453
  switch (command) {
@@ -12326,15 +13458,37 @@ async function invoke(runtime, command, value, signal) {
12326
13458
  case "send.start": return app.journeySend?.start(sendStartSchema.parse(value));
12327
13459
  case "send.respond": return app.journeySend?.respond(sendChoiceSchema.parse(value));
12328
13460
  case "send.pause": return app.journeySend?.pause(sendActionSchema.parse(value));
12329
- case "send.resume": return app.journeySend?.resume(sendActionSchema.parse(value));
12330
- case "send.advance": return app.journeySend?.advance(sendAddress.parse(value));
12331
- case "journey.start": return app.journey?.start(value);
12332
- case "journey.get": return app.journey?.get(value ?? {}, "agent");
13461
+ case "send.resume": {
13462
+ const input = sendActionSchema.parse(value);
13463
+ const current = await runtime.store.journey?.current(input.journeyId);
13464
+ await fenceUnapprovedSendHost(runtime.stateDir, current);
13465
+ return app.journeySend?.resume(input);
13466
+ }
13467
+ case "send.advance": {
13468
+ const input = sendAddress.parse(value);
13469
+ const current = await runtime.store.journey?.current(input.journeyId);
13470
+ await fenceUnapprovedSendHost(runtime.stateDir, current);
13471
+ return app.journeySend?.advance(input);
13472
+ }
13473
+ case "journey.start": {
13474
+ const input = nativeStartSchema.parse(value);
13475
+ const current = await runtime.store.journey?.current();
13476
+ await fenceUnapprovedSendHost(runtime.stateDir, current);
13477
+ if (current && obsoleteJourney(current)) await relinquishObsoleteHost(runtime.stateDir, current.journeyId);
13478
+ return app.journey?.start(input);
13479
+ }
13480
+ case "journey.get": {
13481
+ const result = await app.journey?.get(value ?? {}, getAudience);
13482
+ await fenceUnapprovedSendHost(runtime.stateDir, result);
13483
+ if (result?.obsolete) await relinquishObsoleteHost(runtime.stateDir, result.journeyId);
13484
+ return result;
13485
+ }
12333
13486
  case "journey.groupItems": return app.journey?.groupItems(value);
12334
13487
  case "journey.inventory": return app.journey?.inventory(value);
12335
13488
  case "journey.classificationSessions": return app.journey?.classificationSessions(value);
12336
13489
  case "journey.classificationProgress": return app.journey?.classificationProgress(value);
12337
13490
  case "journey.classificationRead": return app.journey?.classificationRead(value);
13491
+ case "journey.recoverIsolatedMetadata": return app.journey?.recoverIsolatedMetadata(value, signal);
12338
13492
  case "journey.classificationSubmit": return app.journey?.classificationSubmit(value);
12339
13493
  case "journey.cardsRead": return app.journey?.cardsRead(value);
12340
13494
  case "journey.cardsSubmit": return app.journey?.cardsSubmit(value);
@@ -12395,6 +13549,7 @@ async function invoke(runtime, command, value, signal) {
12395
13549
  case "workflow.resume": return app.resume(commandSchema.parse(value));
12396
13550
  case "bundleLibrary.get": return app.getBundleLibrary();
12397
13551
  case "bundleLibrary.save": return app.saveBundleLibrary(z.object({ directory: z.string().max(4096).nullable() }).parse(value).directory);
13552
+ case "rememberedHides.remove": return removeRememberedHides(runtime.rememberedHides, z.object({ values: z.array(z.string()).min(1).max(1e3) }).strict().parse(value).values);
12398
13553
  case "sensitiveTerms.get": return app.getSensitiveTerms();
12399
13554
  case "sensitiveTerms.replace": return app.replaceSensitiveTerms(replaceSensitiveTermsSchema.parse(value));
12400
13555
  case "processing.catalog": return app.getProcessingCatalog();
@@ -12561,4 +13716,4 @@ async function cancelOperation(runtime, id) {
12561
13716
  return readOperation(runtime, id);
12562
13717
  }
12563
13718
  //#endregion
12564
- export { longCommands as _, readAgentPresence as a, traceStartup as b, executorRequestSchema as c, serveDetached as d, guardDetachedStartup as f, businessCommands as g, parseOptions as h, invoke as i, awaitingPassport as l, helpText as m, readOperation as n, createRuntime as o, createRenderActivity as p, waitOperation as r, executorEventSchema as s, cancelOperation as t, hostDrives as u, permissionDiagnostic as v, startupFailure as y };
13719
+ export { permissionDiagnostic as _, readAgentPresence as a, executorRequestSchema as c, packageIdentity as d, createRenderActivity as f, longCommands as g, businessCommands as h, invoke as i, serveDetached as l, parseOptions as m, readOperation as n, createRuntime as o, helpText as p, waitOperation as r, executorEventSchema as s, cancelOperation as t, guardDetachedStartup as u, startupFailure as v, traceStartup as y };