@gitroll/tacitt-cli 0.6.11 → 0.6.12

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 (53) 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-B12VbvN_.mjs} +2 -2
  5. package/dist/agent-setup-worker.mjs +5 -5
  6. package/dist/cli.mjs +28 -28
  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-B3He21G8.mjs} +1 -1
  11. package/dist/{heap-CV8Eca8V.mjs → heap-Blrat--4.mjs} +260 -23
  12. package/dist/inspection-worker.mjs +4 -4
  13. package/dist/{job-BKsifhyv.mjs → job-CBuIMTQ0.mjs} +24 -9
  14. package/dist/{locate-Cx52Zqxv.mjs → locate-D-ZaHpI0.mjs} +5 -0
  15. package/dist/{model-C1UXkHh9.mjs → model-DR9odhWP.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-BYDcNKy2.mjs} +1614 -679
  20. package/dist/{run-CaPktPhH.mjs → run-DQ9xyKSt.mjs} +4 -4
  21. package/dist/session-process.mjs +20 -0
  22. package/dist/{src-wZx_v0Ih.mjs → src-CKQlTA2Z.mjs} +4 -4
  23. package/dist/{src-DlVbGB32.mjs → src-DeNJ8vOD.mjs} +465 -107
  24. package/dist/{src-D1iONla3.mjs → src-Do-qzuMn.mjs} +377 -130
  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 +5 -5
  32. package/skills/tacitt/references/INDEX.md +14 -11
  33. package/skills/tacitt/references/b-method.md +4 -4
  34. package/skills/tacitt/references/bootstrap.md +17 -3
  35. package/skills/tacitt/references/classification.md +1 -1
  36. package/skills/tacitt/references/commands.md +24 -6
  37. package/skills/tacitt/references/journey.md +4 -4
  38. package/skills/tacitt/references/parallel-review.md +13 -7
  39. package/skills/tacitt/references/send.md +7 -3
  40. package/skills/tacitt/scripts/INDEX.md +40 -37
  41. package/skills/tacitt/scripts/answer.mjs +19 -1
  42. package/skills/tacitt/scripts/journey-card-consolidation.mjs +1 -1
  43. package/skills/tacitt/scripts/journey-classification-tasks.mjs +28 -8
  44. package/skills/tacitt/scripts/journey-panel-host.d.mts +2 -0
  45. package/skills/tacitt/scripts/journey-panel-host.mjs +25 -2
  46. package/skills/tacitt/scripts/journey-scan-timing.d.mts +4 -0
  47. package/skills/tacitt/scripts/journey-scan-timing.mjs +12 -0
  48. package/skills/tacitt/scripts/journey-state.mjs +11 -0
  49. package/skills/tacitt/scripts/journey-tasks.mjs +12 -5
  50. package/skills/tacitt/scripts/journey.mjs +10 -2
  51. package/skills/tacitt/scripts/privacy-validator.mjs +28 -7
  52. package/skills/tacitt/scripts/review.mjs +399 -99
  53. 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 { $ as decodeZstd, A as captureLogicalJourneySources, B as defaultParallelism, D as createCollectionArtifacts, E as readConfiguredSearchFields, G as readLogicalJourneyFinalMasks, H as parallelismLimits, I as createSharedSessionPool, J as staticMaskCategory, K as readConfiguredOutputInfo, L as SessionWorkerError, N as runConfiguredSessionJob, R as sessionWorkerFailure, T as readOriginalSearchFields, V as gitSeedsProvider, W as privateJourneyDirectory, Y as readWorkspacePair, a as prepareSessionScans, at as makePrivateDirectory, b as readStaticSessionMasks, c as createWorkPieces, ct as windowsOwnerOnly, d as scanPreparedSession, et as createTemporary, g as openConfiguredUpload, h as configuredSessionUploads, i as verifyPreparedSessionSources, j as capturedJourneyScope, l as readSessionScanFields, lt as isWindowsAclFailure, n as sessionExtensions, nt as createCleanup, o as buildWorkScanBatches, ot as openPrivateFile, q as processJourneyMetadataFields, r as preparedScanConfiguration, s as scanReviewFields, st as restrictToOwner, t as hasRetiredSessionMarker, tt as removeNamedTemporarySpools, u as readSessionScanRecords, ut as windowsAclCodes, v as readCapturedOriginalRows, x as staticSessionJobs, y as processJourneyMetadata } from "./src-DeNJ8vOD.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-CBuIMTQ0.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-D-ZaHpI0.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-CKQlTA2Z.mjs";
5
+ import { An as scanExclusionResourceSchema, B as createApplication, Bt as newJourneyWork, Ci as withReadingProgress, Ct as sendActionSchema, Di as selectionSchema, Dn as scanGitSeedsId, Dt as sendStartSchema, Ei as selectionBatchSchema, En as scanSessionResourceSchema, Et as sendRequestSchema, F as readAccountAvatarUrl, Fr as assertJourneyPrivacyReady, Gr as currentJourneyAdapters, H as inspectionStartSchema, I as storedCredentialSchema, Ir as privacyDecisionDigest, It as classificationPartition, Jr as obsoleteJourney, K as inspectionQuerySchema, Kr as hostDrives, Lt as classificationWorks, M as deviceAuthorizationSchema, Mi as recommendParallelism, Mr as displayedScopeDigest, N as deviceProfileScopeSchema, Nr as requestedScope, O as createAccount, On as scanGitSeedsResource, P as profileUrlMatchesIdentity, Rn as applyClassification, Rt as retireScanGeneration, Si as unfinishedPackagingCode, St as replaceSensitiveTermsSchema, Ti as packageCommandSchema, Tn as scanPreparationDigest, Tt as sendPrepareSchema, U as validateInspectionBatch, Ur as advanceStepsSend, V as inspectionCancelSchema, Vn as searchScopeDigest, Vr as nativeStartSchema, Zr as sendsOnNextAdvance, _ as changeJourney, _i as isIncludedWork, _r as workCorrectionBinding, _t as manualUploadStartSchema, at as appendSensitiveTerms, ci as journeySendSchema, cr as confirmedHideKinds, di as sendReplaceResultSchema, dr as journeyConfirmedHides, dt as validateFailureLocation, fi as sendResolutionSchema, ft as manualUploadActionSchema, gi as isClassifying, gt as manualUploadSchema, h as hideScopeDigest, hi as hasCompleteWorkDescription, hr as workDecisionDigest, ht as manualUploadPrepareSchema, ir as bTermMatcher, j as accountProfileSchema, k as renew, ki as producerVersion, kn as verifiedScanGitSeeds, li as sendArtifactSchema, lt as artifactFailureDetailSchema, m as activeJourneyTerms, mi as sendSettingsSchema, mr as retireWorkDescription, mt as manualUploadChoiceSchema, ni as staticReviewDigest, nt as settingsQuerySchema, oi as agentSourceSchema, qr as itemAtPassport, ri as staticSessionId, si as agentSourcesSchema, ti as hasCurrentStaticReview, tt as settingsBatchSchema, ui as sendReceiptSchema, vi as isReadingComplete, vr as emptyPreparationFacts, wi as commandSchema, wt as sendChoiceSchema, xt as normalizeSensitiveTerms, yi as isSelectedWork, yt as emptyTermsRevision, zn as journeySearchResultSchema, zt as isolatedWorkId } from "./src-Do-qzuMn.mjs";
6
+ import { vt as isCoordinationBusy } from "./heap-Blrat--4.mjs";
7
+ import { d as checkOpenSource, h as openSource } from "./run-DQ9xyKSt.mjs";
8
+ import { t as createAgentSetupPort } from "./agent-setup-B12VbvN_.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",
@@ -216,6 +217,23 @@ const accountDefaults = {
216
217
  clientId: "BdSyvRSunWdXfJUdfvaKuAgoRjaZQpOZ",
217
218
  passportApiUrl: "https://talent-api.catalyte.ai"
218
219
  };
220
+ /** The web origin paired with each deployed Passport API; custom targets supply theirs. */
221
+ function passportWebOrigin(apiUrl, configured) {
222
+ if (configured) {
223
+ const url = new URL(configured);
224
+ const local = [
225
+ "127.0.0.1",
226
+ "[::1]",
227
+ "localhost"
228
+ ].includes(url.hostname);
229
+ if (url.protocol !== "https:" && !(local && url.protocol === "http:") || url.username || url.password || url.pathname !== "/" || url.search || url.hash) throw new Error("INVALID_ARGUMENTS");
230
+ return url.origin;
231
+ }
232
+ return {
233
+ "https://talent-api.catalyte.ai": "https://talent.catalyte.ai",
234
+ "https://talent-api-dev.catalyte.ai": "https://talent-dev.catalyte.ai"
235
+ }[new URL(apiUrl).origin];
236
+ }
219
237
  //#endregion
220
238
  //#region apps/cli/src/options.ts
221
239
  /** An HTTP(S) origin without path, query, credentials or fragment. */
@@ -335,6 +353,7 @@ function parseOptions(args, source = sourceCheckout) {
335
353
  sessionPeriod: period.data,
336
354
  identityUrl: readOrigin(parsed.values["identity-url"] ?? process.env["SESS_PILOT_IDENTITY_URL"] ?? accountDefaults.identityUrl),
337
355
  identityClientId: identityClientId === "" ? null : identityClientId,
356
+ ...process.env["SESS_PILOT_PASSPORT_WEB_ORIGIN"] ? { passportWebOrigin: readOrigin(process.env["SESS_PILOT_PASSPORT_WEB_ORIGIN"]) } : {},
338
357
  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
358
  stateDir: resolve(parsed.values["state-dir"] ?? defaultState),
340
359
  bundleDir: parsed.values["bundle-dir"] === void 0 ? void 0 : resolve(parsed.values["bundle-dir"]),
@@ -359,7 +378,8 @@ ${Array.from(businessCommands).join("\n")}
359
378
  Business commands invoke the shared Application directly without an HTTP server.
360
379
  journey.serve starts a focused confirmation host, emits JSON readiness, and waits until stopped.
361
380
  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
381
+ pid, and exits. The launcher verifies the release and process identity before reusing a panel.
382
+ A detached host already serving the journey with the paired release is
363
383
  reused and reported with reused: true.
364
384
  It does not discover sessions or open a browser. The agent opens its private URL in the right pane.
365
385
  Workflow mutations require protocolVersion, runId and expectedRevision in JSON input.
@@ -616,11 +636,12 @@ async function scanRemoteDispatches(sessions, signal) {
616
636
  const pending = /* @__PURE__ */ new Map();
617
637
  for (const part of session.parts) {
618
638
  signal.throwIfAborted();
619
- const file = await openSource(part);
639
+ const mode = part.representation === "jsonl" ? "append" : "snapshot";
640
+ const file = await openSource(part, mode);
620
641
  try {
621
642
  if (!part.snapshot.size) {
622
643
  if (part.representation === "jsonl.zst") throw new Error("ARTIFACT_PROCESSING_DECODE_FAILED");
623
- await checkOpenSource(part, file);
644
+ await checkOpenSource(part, file, mode);
624
645
  continue;
625
646
  }
626
647
  const stream = file.createReadStream({
@@ -674,7 +695,7 @@ async function scanRemoteDispatches(sessions, signal) {
674
695
  if (found.length) pending.set(command.id, found);
675
696
  }
676
697
  }
677
- await checkOpenSource(part, file);
698
+ await checkOpenSource(part, file, mode);
678
699
  } finally {
679
700
  stream.destroy();
680
701
  }
@@ -1171,6 +1192,10 @@ function bindJourneyDestination(storage, destination, options = {}) {
1171
1192
  };
1172
1193
  return {
1173
1194
  current,
1195
+ async readSendOutput(input) {
1196
+ await guard(input.journeyId);
1197
+ return storage.readSendOutput(input);
1198
+ },
1174
1199
  async preparationState(id) {
1175
1200
  await guard(id);
1176
1201
  return storage.preparationState(id);
@@ -1236,6 +1261,10 @@ function bindJourneyDestination(storage, destination, options = {}) {
1236
1261
  await guard(input.journeyId);
1237
1262
  return storage.saveResource(input);
1238
1263
  },
1264
+ async receiptRevision(input) {
1265
+ await guard(input.journeyId);
1266
+ return storage.receiptRevision(input);
1267
+ },
1239
1268
  async commitClassification(input) {
1240
1269
  await guard(input.journeyId);
1241
1270
  return storage.commitClassification(input);
@@ -1340,10 +1369,29 @@ function bindJourneyDestination(storage, destination, options = {}) {
1340
1369
  };
1341
1370
  }
1342
1371
  function bindSendDestination(storage, destination) {
1372
+ const verified = /* @__PURE__ */ new Set();
1373
+ async function verify(id) {
1374
+ if (verified.has(id)) return;
1375
+ const snapshot = await storage.loadSend(id);
1376
+ if (!snapshot) throw new ApplicationError("UNKNOWN_ITEM");
1377
+ check(snapshot.journey, destination);
1378
+ verified.add(id);
1379
+ }
1343
1380
  return {
1344
- async loadSend(id, sendId) {
1345
- const result = await storage.loadSend(id, sendId);
1346
- if (result) check(result.journey, destination);
1381
+ async isCurrentSend(id, sendId, revision) {
1382
+ await verify(id);
1383
+ return storage.isCurrentSend(id, sendId, revision);
1384
+ },
1385
+ async checkpointTransfer(input) {
1386
+ await verify(input.journeyId);
1387
+ return storage.checkpointTransfer(input);
1388
+ },
1389
+ async loadSend(id, sendId, observedRevision) {
1390
+ const result = await storage.loadSend(id, sendId, verified.has(id) ? observedRevision : void 0);
1391
+ if (result) {
1392
+ check(result.journey, destination);
1393
+ verified.add(result.journey.journeyId);
1394
+ }
1347
1395
  return result;
1348
1396
  },
1349
1397
  async commitSend(input) {
@@ -1363,6 +1411,7 @@ async function changePreparation(storage, journeyId, kind, change, attempts = Nu
1363
1411
  const before = await storage.preparationState(journeyId);
1364
1412
  if (!before?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
1365
1413
  const after = change(before);
1414
+ if (after === before) return before;
1366
1415
  const previous = new Map(before.works.map((work) => [work.id, work]));
1367
1416
  const result = await storage.updatePreparation({
1368
1417
  journeyId,
@@ -1389,7 +1438,7 @@ const classificationMetadataInput = (work) => ({
1389
1438
  description: work.description,
1390
1439
  reason: work.classificationRecommendation?.reason ?? work.reason
1391
1440
  });
1392
- async function prepareClassificationMetadata(storage, state, work, processing, signal, plan) {
1441
+ async function processedClassificationMetadata(storage, state, work, processing, signal, plan) {
1393
1442
  if (state.scope.status !== "confirmed") throw new ApplicationError("CONFIRMATION_REQUIRED");
1394
1443
  if (work.classificationMetadata && (work.explicitTitleId || work.suggestion)) return;
1395
1444
  const identity = classificationMetadataInput(work);
@@ -1428,6 +1477,19 @@ async function prepareClassificationMetadata(storage, state, work, processing, s
1428
1477
  fields: Object.values(raw).map((text) => ({ text }))
1429
1478
  }, signal);
1430
1479
  if (title === void 0 || projectName === void 0 || description === void 0 || reason === void 0) throw new ApplicationError("CORRUPTED_STATE");
1480
+ return {
1481
+ digest,
1482
+ title: title.text,
1483
+ projectName: projectName.text,
1484
+ description: description.text,
1485
+ reason: reason.text,
1486
+ titleOriginalSha256: textDigest(raw.title),
1487
+ titleStaticMasks: [...title.staticMasks]
1488
+ };
1489
+ }
1490
+ async function prepareClassificationMetadata(storage, state, work, processing, signal, plan) {
1491
+ const metadata = await processedClassificationMetadata(storage, state, work, processing, signal, plan);
1492
+ if (!metadata) return;
1431
1493
  await changePreparation(storage, state.journeyId, "snapshot", (before) => {
1432
1494
  const active = before.works.find((item) => item.id === work.id);
1433
1495
  if (before.scope.status !== "confirmed" || !active || textDigest(JSON.stringify([
@@ -1436,20 +1498,12 @@ async function prepareClassificationMetadata(storage, state, work, processing, s
1436
1498
  active.suggestion?.id,
1437
1499
  active.explicitTitleId,
1438
1500
  before.exceptions.filter((item) => item.workId === active.id)
1439
- ])) !== digest) throw new ApplicationError("REVISION_CONFLICT");
1501
+ ])) !== metadata.digest) throw new ApplicationError("REVISION_CONFLICT");
1440
1502
  return {
1441
1503
  ...before,
1442
1504
  works: before.works.map((item) => item.id === work.id ? {
1443
1505
  ...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
- }
1506
+ classificationMetadata: metadata
1453
1507
  } : item)
1454
1508
  };
1455
1509
  });
@@ -3190,14 +3244,15 @@ async function readJourneyPlan(storage, journeyId) {
3190
3244
  async function finalKeywordDictionary(storage, state, sessionIds, plan) {
3191
3245
  if (!state.snapshotId) throw new ApplicationError("CONFIRMATION_REQUIRED");
3192
3246
  const dictionary = /* @__PURE__ */ new Map();
3193
- const add = (term, kind, bounded) => {
3247
+ const add = (term, kind, bounded, toolResultEscapes = false) => {
3194
3248
  const prior = dictionary.get(term);
3195
3249
  dictionary.set(term, {
3196
3250
  kind: prior && confirmedHideKinds.indexOf(prior.kind) > confirmedHideKinds.indexOf(kind) ? prior.kind : kind,
3197
- bounded: bounded && (prior?.bounded ?? true)
3251
+ bounded: bounded && (prior?.bounded ?? true),
3252
+ ...toolResultEscapes || prior?.toolResultEscapes ? { toolResultEscapes: true } : {}
3198
3253
  });
3199
3254
  };
3200
- for (const [term, { kind }] of await journeyConfirmedHides(storage, state)) add(term, kind, true);
3255
+ for (const [term, { kind }] of await journeyConfirmedHides(storage, state)) add(term, kind, true, true);
3201
3256
  for (const value of (await activeJourneyTerms(storage, state)).terms) {
3202
3257
  const admitted = admitCustomTerm(value);
3203
3258
  if (admitted) add(admitted.value, "private", false);
@@ -3303,10 +3358,11 @@ function createFamilyHides() {
3303
3358
  families.set(sessionId, terms);
3304
3359
  }
3305
3360
  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 }]) => ({
3361
+ const all = [...confirmed].map(([term, { kind, bounded, toolResultEscapes }]) => ({
3307
3362
  term,
3308
3363
  category: kind,
3309
- ...bounded ? { bounded: true } : {}
3364
+ ...bounded ? { bounded: true } : {},
3365
+ ...bounded && toolResultEscapes ? { toolResultEscapes: true } : {}
3310
3366
  }));
3311
3367
  return [...families].flatMap(([root, terms]) => {
3312
3368
  const auto = terms.size ? all.filter(({ term }) => !terms.has(term)) : all;
@@ -3564,8 +3620,10 @@ function createRenderWorkers(workers, size) {
3564
3620
  if (result.kind !== "journey-base") throw new Error("ARTIFACT_PROCESSING_FAILED");
3565
3621
  return result.base;
3566
3622
  } 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 });
3623
+ if (error instanceof SessionWorkerError) {
3624
+ await removeNamedTemporarySpools(named).catch((cleanup) => workers.onFallback?.(cleanup));
3625
+ throw new ApplicationError(error.message, void 0, { cause: error });
3626
+ }
3569
3627
  if (signal.aborted) throw error;
3570
3628
  workers.onFallback?.(error);
3571
3629
  return pool.serial(() => writeJourneyBase(work, signal));
@@ -3808,6 +3866,7 @@ function createJourneyRenderer(options) {
3808
3866
  async function admit(workId, session) {
3809
3867
  const state = await session.current();
3810
3868
  if (!state?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
3869
+ if (state.phase === "completed" && state.outputVersion !== 1) throw new ApplicationError("SCOPE_NOT_READY");
3811
3870
  assertJourneyPrivacyReady(state, true);
3812
3871
  if (obsoleteJourney(state)) throw new ApplicationError("JOURNEY_OBSOLETE");
3813
3872
  if (state.pendingHideId) throw new ApplicationError("SCOPE_NOT_READY");
@@ -4816,6 +4875,22 @@ async function resolveChunkSessions(storage, state, works, signal) {
4816
4875
  }
4817
4876
  //#endregion
4818
4877
  //#region apps/cli/src/journey/works/proposal.ts
4878
+ /** Materialize exactly the Work families accepted by classification, preserving first-name spelling. */
4879
+ function groupedClassificationItems(items) {
4880
+ const merged = /* @__PURE__ */ new Map();
4881
+ for (const item of items) {
4882
+ const key = projectNameKey(item.projectName);
4883
+ const prior = merged.get(key);
4884
+ if (prior) {
4885
+ if (prior.description !== item.description || prior.projectType !== item.projectType || JSON.stringify(prior.recommendation) !== JSON.stringify(item.recommendation)) throw new ApplicationError("INVALID_SELECTION");
4886
+ merged.set(key, {
4887
+ ...prior,
4888
+ rootSessionIds: [...prior.rootSessionIds, ...item.rootSessionIds]
4889
+ });
4890
+ } else merged.set(key, item);
4891
+ }
4892
+ return [...merged.values()];
4893
+ }
4819
4894
  /**
4820
4895
  * One atomic root partition; recommendations have no access authority. Roots still being grouped
4821
4896
  * wait in one unread Work while the other Works are confirmed and read; the submission that
@@ -4855,21 +4930,9 @@ function createClassificationProposal(storage, accountAccess) {
4855
4930
  if (plan?.kind !== "plan") throw new ApplicationError("SCOPE_NOT_READY");
4856
4931
  const roots = new Set(state.works.flatMap((work) => work.rootSessionIds));
4857
4932
  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);
4933
+ for (const item of input.items) for (const root of item.rootSessionIds) {
4934
+ if (!roots.has(root) || seen.has(root)) throw new ApplicationError("INVALID_SELECTION");
4935
+ seen.add(root);
4873
4936
  }
4874
4937
  const pending = input.pendingRootSessionIds ?? [];
4875
4938
  for (const root of pending) {
@@ -4889,7 +4952,7 @@ function createClassificationProposal(storage, accountAccess) {
4889
4952
  return [...new Set(sessions.map((session) => session.workspaceId))];
4890
4953
  };
4891
4954
  const items = [];
4892
- for (const item of merged.values()) {
4955
+ for (const item of groupedClassificationItems(input.items)) {
4893
4956
  signal.throwIfAborted();
4894
4957
  items.push({
4895
4958
  ...item,
@@ -4933,6 +4996,102 @@ function createClassificationProposal(storage, accountAccess) {
4933
4996
  };
4934
4997
  }
4935
4998
  //#endregion
4999
+ //#region apps/cli/src/journey/works/recover-isolated.ts
5000
+ /** Match the entire original family partition, including other isolated siblings of this parent. */
5001
+ function approvedItem(state, child, items) {
5002
+ const matches = items.filter((item) => child.rootSessionIds.every((id) => item.rootSessionIds.includes(id)));
5003
+ const item = matches[0];
5004
+ if (matches.length !== 1 || !item) throw new ApplicationError("INVALID_SELECTION");
5005
+ 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);
5006
+ const parent = parents[0];
5007
+ if (parents.length !== 1 || !parent) throw new ApplicationError("INVALID_SELECTION");
5008
+ const siblingIds = new Set(Array.from({ length: state.works.length + 1 }, (_, i) => isolatedWorkId(parent.id, i)));
5009
+ if (!siblingIds.has(child.id)) throw new ApplicationError("INVALID_SELECTION");
5010
+ const roots = state.works.filter((w) => w.id === parent.id || siblingIds.has(w.id)).flatMap((w) => w.rootSessionIds);
5011
+ if (roots.length !== item.rootSessionIds.length || new Set(roots).size !== roots.length || !roots.every((root) => item.rootSessionIds.includes(root))) throw new ApplicationError("INVALID_SELECTION");
5012
+ return item;
5013
+ }
5014
+ /** Repair the missing classification only, using its immutable receipt and the ordinary static metadata policy. */
5015
+ function createIsolatedMetadataRecovery(storage, processing) {
5016
+ return async (input, signal) => {
5017
+ const { proposal, workIds, idempotencyKey } = input;
5018
+ const journeyId = proposal.journeyId;
5019
+ const hash = textDigest(JSON.stringify(input));
5020
+ const state = await storage.current();
5021
+ if (!state || state.journeyId !== journeyId) throw new ApplicationError("UNKNOWN_ITEM");
5022
+ if (await storage.receiptRevision({
5023
+ journeyId,
5024
+ key: idempotencyKey,
5025
+ hash
5026
+ }) !== null) return {
5027
+ status: "replayed",
5028
+ state
5029
+ };
5030
+ const accepted = await storage.receiptRevision({
5031
+ journeyId,
5032
+ key: proposal.idempotencyKey,
5033
+ hash: textDigest(JSON.stringify(proposal))
5034
+ });
5035
+ if (accepted === null || accepted !== state.classificationRevision) throw new ApplicationError("REVISION_CONFLICT");
5036
+ if (!state.consent || state.scope.status !== "confirmed" || state.abandonedAt || !state.readingComplete || state.pendingHideId || !["ready", "review"].includes(state.phase)) throw new ApplicationError("SCOPE_NOT_READY");
5037
+ if (new Set(workIds).size !== workIds.length || proposal.pendingRootSessionIds?.length) throw new ApplicationError("INVALID_SELECTION");
5038
+ const plan = (await storage.resources({
5039
+ journeyId,
5040
+ kind: "plan",
5041
+ id: "plan"
5042
+ }))[0];
5043
+ if (plan?.kind !== "plan") throw new ApplicationError("SCOPE_NOT_READY");
5044
+ const recovered = /* @__PURE__ */ new Map();
5045
+ const families = groupedClassificationItems(proposal.items);
5046
+ for (const id of workIds) {
5047
+ signal.throwIfAborted();
5048
+ const work = state.works.find((w) => w.id === id);
5049
+ 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");
5050
+ const item = approvedItem(state, work, families);
5051
+ const restored = {
5052
+ ...work,
5053
+ description: item.description,
5054
+ classificationRecommendation: item.recommendation,
5055
+ reason: item.recommendation.reason,
5056
+ projectType: work.projectType ?? {
5057
+ type: item.projectType,
5058
+ reason: item.recommendation.reason,
5059
+ origin: "model"
5060
+ },
5061
+ artifact: null,
5062
+ revision: work.revision + 1
5063
+ };
5064
+ const metadata = await processedClassificationMetadata(storage, state, restored, processing, signal, plan);
5065
+ if (!metadata || !hasCompleteWorkDescription(restored)) throw new ApplicationError("INVALID_SELECTION");
5066
+ recovered.set(id, {
5067
+ ...restored,
5068
+ classificationMetadata: metadata
5069
+ });
5070
+ }
5071
+ signal.throwIfAborted();
5072
+ return storage.commit({
5073
+ expectedRevision: state.revision,
5074
+ requestKey: idempotencyKey,
5075
+ requestHash: hash,
5076
+ state: {
5077
+ ...state,
5078
+ phase: "review",
5079
+ works: state.works.map((w) => recovered.get(w.id) ?? w),
5080
+ privacy: {
5081
+ ...state.privacy,
5082
+ packageApproval: null
5083
+ }
5084
+ },
5085
+ event: {
5086
+ journeyId,
5087
+ kind: "snapshot",
5088
+ source: "system",
5089
+ at: (/* @__PURE__ */ new Date()).toISOString()
5090
+ }
5091
+ });
5092
+ };
5093
+ }
5094
+ //#endregion
4936
5095
  //#region apps/cli/src/journey/works/catalog-project-evidence.ts
4937
5096
  /** Collects every unresolved root's frozen structural evidence with bounded I/O fan-out. */
4938
5097
  async function collectCatalogProjectEvidence(options, signal) {
@@ -5276,6 +5435,7 @@ function createJourneyClassification(storage, _processing) {
5276
5435
  /** The journey local port's project catalog and project-type suggestions. */
5277
5436
  function createJourneyWorks(storage, processing, stateDir, accountAccess) {
5278
5437
  return {
5438
+ recoverIsolatedMetadata: createIsolatedMetadataRecovery(storage, processing),
5279
5439
  classificationSessions: createClassificationSessions(storage, processing, stateDir),
5280
5440
  classificationRead: createClassificationReader(storage, stateDir),
5281
5441
  classificationSubmit: createClassificationProposal(storage, accountAccess),
@@ -5394,6 +5554,8 @@ const packagingRefusalCodes = [
5394
5554
  */
5395
5555
  const finalPackagingCodes = [
5396
5556
  ...packagingRefusalCodes,
5557
+ "WORK_METADATA_INCOMPLETE",
5558
+ "PACKAGING_FAILED",
5397
5559
  ...rootScanRefusals,
5398
5560
  "ARTIFACT_ARCHIVE_LIMIT",
5399
5561
  "ARTIFACT_ENTRY_LIMIT",
@@ -5429,22 +5591,45 @@ function createSendPackager(ports) {
5429
5591
  current = await ports.current(journeyId);
5430
5592
  if (!current) throw new ApplicationError("UNKNOWN_ITEM");
5431
5593
  }
5432
- const ready = (works, id) => works.some((work) => work.id === id && packaged(work));
5433
- const batch = workIds.filter((id) => !ready(current.works, id));
5594
+ const ready = new Set(current.works.filter(packaged).map((work) => work.id));
5595
+ const batch = workIds.filter((id) => !ready.has(id));
5434
5596
  if (!batch.length) return true;
5597
+ const selected = new Set(batch);
5598
+ if (current.works.some((work) => selected.has(work.id) && !hasCompleteWorkDescription(work))) throw new ApplicationError("WORK_METADATA_INCOMPLETE");
5435
5599
  signal.throwIfAborted();
5436
5600
  const result = await ports.render({
5437
5601
  journeyId,
5438
5602
  workIds: batch
5439
5603
  }, 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");
5604
+ const works = new Map(result.works.map((work) => [work.id, work]));
5605
+ const failures = new Map(result.renders?.filter((item) => item.status === "failed").map((item) => [item.workId, item.code]));
5606
+ for (const id of batch) {
5607
+ const work = works.get(id);
5608
+ if ((!work || !packaged(work)) && !failures.has(id)) failures.set(id, unfinishedPackagingCode(work));
5609
+ }
5610
+ const codes = [...failures.values()];
5611
+ const failure = codes.find((code) => packagingRefusalCodes.includes(code)) ?? codes.find((code) => finalPackagingCodes.includes(code)) ?? codes[0];
5612
+ if (failure) throw new ApplicationError(failure);
5444
5613
  return false;
5445
5614
  };
5446
5615
  }
5447
5616
  //#endregion
5617
+ //#region apps/cli/src/package-identity.ts
5618
+ /** Release identity stays independent of producer/processing format versions. */
5619
+ const packageIdentity = Object.freeze(z.object({
5620
+ name: z.literal("@gitroll/tacitt-cli"),
5621
+ version: z.string().regex(/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/)
5622
+ }).parse(JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"))));
5623
+ //#endregion
5624
+ //#region apps/cli/skills/tacitt/scripts/journey-panel-host.mjs
5625
+ /** Only the host's canonical browser bootstrap may be handed to a fresh browser. */
5626
+ function focusedPanelUrl(value, journeyId) {
5627
+ if (typeof value !== "string") return null;
5628
+ const url = URL.parse(value);
5629
+ 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;
5630
+ return url;
5631
+ }
5632
+ //#endregion
5448
5633
  //#region apps/cli/src/serve-process.ts
5449
5634
  const execute = promisify(execFile);
5450
5635
  /** OS process creation identity, independent of PID recycling. */
@@ -5509,8 +5694,14 @@ const recordSchema$1 = z.object({
5509
5694
  journeyId: z.string(),
5510
5695
  pid: z.number().int().positive(),
5511
5696
  log: z.string(),
5697
+ version: z.string().optional(),
5698
+ currentKeepAuthority: z.boolean().optional(),
5699
+ outputVersion: z.number().int().optional(),
5512
5700
  processStart: z.string().optional()
5513
5701
  });
5702
+ function enforcesPrivacy(record) {
5703
+ return record?.currentKeepAuthority && record.outputVersion === 1;
5704
+ }
5514
5705
  const recordPath$1 = (directory, journeyId) => join(directory, `${journeyId}.host.json`);
5515
5706
  function running(pid) {
5516
5707
  try {
@@ -5521,19 +5712,18 @@ function running(pid) {
5521
5712
  }
5522
5713
  }
5523
5714
  /** Only verified absence admits a replacement; transport failure is inconclusive. */
5524
- async function liveHost(directory, journeyId) {
5715
+ async function liveHost(directory, journeyId, strictRecord = false) {
5525
5716
  let record;
5526
5717
  try {
5527
5718
  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" };
5719
+ if (!parsed.success || parsed.data.journeyId !== journeyId) return { status: strictRecord ? "unknown" : "dead" };
5529
5720
  record = parsed.data;
5530
5721
  } catch (error) {
5531
- if (error instanceof SyntaxError || error instanceof Error && "code" in error && error.code === "ENOENT") return { status: "dead" };
5722
+ if (!strictRecord && error instanceof SyntaxError || error instanceof Error && "code" in error && error.code === "ENOENT") return { status: "dead" };
5532
5723
  process.stderr.write("HOST_RECORD_UNAVAILABLE\n");
5533
5724
  return { status: "unknown" };
5534
5725
  }
5535
- const state = running(record.pid);
5536
- if (state === "dead") return { status: "dead" };
5726
+ if (running(record.pid) === "dead") return { status: "dead" };
5537
5727
  if (record.processStart !== void 0) try {
5538
5728
  if (await processStart(record.pid) !== record.processStart) return { status: "dead" };
5539
5729
  } catch {
@@ -5543,19 +5733,37 @@ async function liveHost(directory, journeyId) {
5543
5733
  record
5544
5734
  };
5545
5735
  }
5546
- else if (state === "unknown") return { status: "dead" };
5547
5736
  if (!record.url) return {
5548
5737
  status: "unknown",
5549
5738
  record
5550
5739
  };
5740
+ if (!record.processStart) return {
5741
+ status: "unknown",
5742
+ record
5743
+ };
5744
+ if (record.version !== packageIdentity.version || !enforcesPrivacy(record)) return {
5745
+ status: "obsolete",
5746
+ record
5747
+ };
5748
+ const url = focusedPanelUrl(record.url, journeyId);
5749
+ if (!url) return {
5750
+ status: "obsolete",
5751
+ record
5752
+ };
5753
+ url.hash = "";
5551
5754
  try {
5552
- await fetch(record.url, { signal: AbortSignal.timeout(2e3) });
5553
- return {
5755
+ return (await fetch(url, {
5756
+ method: "HEAD",
5757
+ redirect: "manual",
5758
+ signal: AbortSignal.timeout(2e3)
5759
+ })).ok ? {
5554
5760
  status: "alive",
5555
5761
  record
5762
+ } : {
5763
+ status: "unknown",
5764
+ record
5556
5765
  };
5557
- } catch (error) {
5558
- if (record.processStart === void 0 && loopbackRefused(record.url, error)) return { status: "dead" };
5766
+ } catch {
5559
5767
  process.stderr.write("HOST_PROBE_UNAVAILABLE\n");
5560
5768
  return {
5561
5769
  status: "unknown",
@@ -5563,16 +5771,57 @@ async function liveHost(directory, journeyId) {
5563
5771
  };
5564
5772
  }
5565
5773
  }
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);
5774
+ /** Called only while the launch mutex is held. Never start alongside an old owner. */
5775
+ async function relinquish(record) {
5776
+ const sameOwner = async () => running(record.pid) !== "dead" && await processStart(record.pid) === record.processStart;
5777
+ try {
5778
+ if (!await sameOwner()) return true;
5779
+ process.kill(record.pid, "SIGTERM");
5780
+ const deadline = Date.now() + 1e4;
5781
+ while (Date.now() < deadline) {
5782
+ if (!await sameOwner()) return true;
5783
+ await setTimeout$1(100);
5784
+ }
5785
+ } catch {
5786
+ if (running(record.pid) === "dead") return true;
5787
+ }
5788
+ process.stderr.write("HOST_REPLACEMENT_PENDING: the previous panel host has not confirmed shutdown. Retry the handoff.\n");
5789
+ return false;
5790
+ }
5791
+ /** A previous sender cannot observe a new policy by reusing its cached approval.
5792
+ * Only a live, PID-and-birth-verified owner may be signalled. */
5793
+ async function fenceUnapprovedSendHost(stateDir, journey) {
5794
+ if (journey?.phase !== "sending" || journey.privacy.packageApproval !== null) return;
5795
+ const directory = join(stateDir, "serve");
5796
+ const status = await liveHost(directory, journey.journeyId, true);
5797
+ if (status.status === "dead" || enforcesPrivacy(status.record)) return;
5798
+ await withStateMutex(directory, async () => {
5799
+ const current = await liveHost(directory, journey.journeyId, true);
5800
+ if (current.status === "dead" || enforcesPrivacy(current.record)) return;
5801
+ if (current.status !== "obsolete" || !await relinquish(current.record)) throw new ApplicationError("HOST_REPLACEMENT_PENDING");
5802
+ });
5803
+ }
5804
+ /** Best-effort cleanup before an obsolete journey can lose its current ID. */
5805
+ async function relinquishObsoleteHost(stateDir, journeyId) {
5806
+ const directory = join(stateDir, "serve");
5807
+ try {
5808
+ await readFile(recordPath$1(directory, journeyId), "utf8");
5809
+ await withStateMutex(directory, async () => {
5810
+ const current = await liveHost(directory, journeyId);
5811
+ if (current.status === "obsolete" && current.record.version !== packageIdentity.version) await relinquish(current.record);
5812
+ });
5813
+ } catch (error) {
5814
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return;
5815
+ process.stderr.write("HOST_REPLACEMENT_PENDING: obsolete journey host cleanup failed.\n");
5816
+ }
5573
5817
  }
5574
5818
  function reuse(existing) {
5575
5819
  if (existing.status === "dead") return false;
5820
+ if (existing.status === "obsolete") {
5821
+ process.stderr.write("HOST_REPLACEMENT_PENDING: retry the handoff.\n");
5822
+ process.exitCode = 1;
5823
+ return true;
5824
+ }
5576
5825
  if (existing.status === "alive") process.stdout.write(`${JSON.stringify({
5577
5826
  ok: true,
5578
5827
  event: "ready",
@@ -5634,7 +5883,12 @@ async function serveDetached(args, stateDir, journeyId) {
5634
5883
  recursive: true,
5635
5884
  force: true
5636
5885
  });
5637
- if (reuse(current)) return;
5886
+ if (current.status === "obsolete") {
5887
+ if (!await relinquish(current.record)) {
5888
+ process.exitCode = 1;
5889
+ return;
5890
+ }
5891
+ } else if (reuse(current)) return;
5638
5892
  let retries = 0;
5639
5893
  let delay = 500;
5640
5894
  for (;;) {
@@ -5698,6 +5952,11 @@ async function startHost(args, directory, journeyId, deadline) {
5698
5952
  await output.close();
5699
5953
  throw error;
5700
5954
  }
5955
+ let ready;
5956
+ child.on("message", (message) => {
5957
+ const parsed = readySchema.safeParse(message);
5958
+ if (parsed.success) ready ??= parsed.data.result;
5959
+ });
5701
5960
  let exited = null;
5702
5961
  child.once("exit", (code) => exited = code ?? 1);
5703
5962
  let spawnFailure;
@@ -5713,6 +5972,9 @@ async function startHost(args, directory, journeyId, deadline) {
5713
5972
  identity = await processStart(child.pid);
5714
5973
  await publishRecord(directory, id, {
5715
5974
  url: "",
5975
+ version: packageIdentity.version,
5976
+ currentKeepAuthority: true,
5977
+ outputVersion: 1,
5716
5978
  journeyId,
5717
5979
  pid: child.pid,
5718
5980
  processStart: identity,
@@ -5740,7 +6002,8 @@ async function startHost(args, directory, journeyId, deadline) {
5740
6002
  deadline
5741
6003
  }, () => ({
5742
6004
  exited,
5743
- spawnFailure
6005
+ spawnFailure,
6006
+ ready
5744
6007
  }));
5745
6008
  } finally {
5746
6009
  if (child.connected) child.disconnect();
@@ -5753,29 +6016,27 @@ async function publishRecord(directory, id, record) {
5753
6016
  }
5754
6017
  async function readStartup(log) {
5755
6018
  let busy = false;
5756
- let ready;
5757
6019
  for (const line of (await readFile(log, "utf8")).split("\n")) try {
5758
6020
  const parsed = JSON.parse(line);
5759
6021
  const failure = failureSchema.safeParse(parsed);
5760
6022
  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
6023
  } catch {}
5764
- return {
5765
- busy,
5766
- ready
5767
- };
6024
+ return { busy };
5768
6025
  }
5769
6026
  async function waitForReady(child, startup, state) {
5770
6027
  let busy = false;
5771
6028
  while (Date.now() < startup.deadline) {
5772
6029
  const failure = state().spawnFailure;
5773
6030
  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) {
6031
+ busy = (await readStartup(startup.log)).busy;
6032
+ const ready = state().ready;
6033
+ if (ready?.journeyId === startup.journeyId && child.pid !== void 0) {
6034
+ if (!focusedPanelUrl(ready.url, startup.journeyId)) throw new Error("INVALID_PANEL_URL");
5777
6035
  const record = {
5778
- ...observed.ready,
6036
+ ...ready,
6037
+ version: packageIdentity.version,
6038
+ currentKeepAuthority: true,
6039
+ outputVersion: 1,
5779
6040
  pid: child.pid,
5780
6041
  processStart: startup.identity,
5781
6042
  log: startup.log
@@ -5860,378 +6121,82 @@ async function readSendHost(stateDir) {
5860
6121
  }
5861
6122
  }
5862
6123
  //#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
- } };
6124
+ //#region apps/cli/src/journey/send-extension.ts
6125
+ function selectionKey({ output, extension, identities }) {
6126
+ return hash$1([
6127
+ output.journeyId,
6128
+ output.workId,
6129
+ output.primary.sha256,
6130
+ output.finalMaskLedger.sha256,
6131
+ identities,
6132
+ extension
6133
+ ]);
5913
6134
  }
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;
6135
+ /** Sender-local cache. Its only values are archive IDs already covered by a scope-bound proof. */
6136
+ function createSendExtensionSelection() {
6137
+ const cache = /* @__PURE__ */ new Map();
6138
+ return async function select(storage, descriptor, signal) {
6139
+ const { output, identities, extension } = descriptor;
6140
+ if (!extension) return /* @__PURE__ */ new Set();
6141
+ const key = selectionKey(descriptor);
6142
+ const prior = cache.get(key);
6143
+ if (prior) return prior;
6144
+ const bindings = /* @__PURE__ */ new Map();
6145
+ for (const binding of extension.sources) {
6146
+ const location = JSON.stringify([binding.session, binding.source]);
6147
+ if (bindings.has(location)) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
6148
+ bindings.set(location, binding.part);
6149
+ }
6150
+ const dates = new Map(identities.map((item) => [item.archiveSessionId, item.lastAt]));
6151
+ const proof = await sessionExtensions({
6152
+ primary: output.primary,
6153
+ ledger: output.finalMaskLedger,
6154
+ groups: extension.groups,
6155
+ sourceFor: (session, source) => bindings.get(JSON.stringify([session, source])),
6156
+ lastAt: (session) => dates.get(session) ?? ""
6157
+ }, signal);
6158
+ if (!proof) return /* @__PURE__ */ new Set();
6159
+ if (proof.retired.size) for (const work of extension.outputs) {
6160
+ const { output: candidate } = await storage.readSendOutput({
6161
+ journeyId: output.journeyId,
6162
+ workId: work.workId,
6163
+ revision: work.revision,
6164
+ sessions: false
5979
6165
  });
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);
6166
+ if (candidate.primary.sha256 !== work.sha256 || candidate.bundleId !== work.bundleId) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
6167
+ const omitted = candidate.workId === output.workId ? proof.omitted : /* @__PURE__ */ new Set();
6168
+ if (await hasRetiredSessionMarker(candidate.primary, omitted, proof.retired, signal)) return /* @__PURE__ */ new Set();
6003
6169
  }
6170
+ if (selectionKey(await storage.readSendOutput({
6171
+ journeyId: output.journeyId,
6172
+ workId: output.workId,
6173
+ revision: output.revision,
6174
+ sessions: true
6175
+ })) !== key) throw new ApplicationError("REVISION_CONFLICT");
6176
+ if (cache.size >= 32) cache.clear();
6177
+ cache.set(key, proof.omitted);
6178
+ return proof.omitted;
6004
6179
  };
6005
6180
  }
6006
6181
  //#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,
6058
- 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
6067
- });
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()));
6213
- }
6214
- };
6215
- }
6216
- //#endregion
6217
- //#region apps/cli/src/account/journey/session-key.ts
6218
- /** Engine native identity, or harness-root-relative path identity when native identity is absent. */
6219
- async function sessionKeyFor(input) {
6220
- const native = input.nativeSessionId;
6221
- const path = input.relativePath?.replaceAll("\\", "/");
6222
- if (!input.harness || (!native || !native.trim()) && !path) throw new TypeError("SESSION_RELATIVE_PATH_REQUIRED");
6223
- if (input.harness === "claude" && native?.trim() && input.workerIdentity) {
6224
- const agentId = input.workerIdentity.agentId;
6225
- const discriminator = agentId?.trim() ? ["agent-id", agentId] : ["path", path];
6226
- if (!agentId?.trim() && !path) throw new TypeError("SESSION_RELATIVE_PATH_REQUIRED");
6227
- const source = JSON.stringify([
6228
- "native-v2-worker",
6229
- input.harness,
6230
- "session",
6231
- native,
6232
- ...discriminator
6233
- ]);
6234
- return createHash("sha256").update(source, "utf8").digest("hex");
6182
+ //#region apps/cli/src/account/journey/session-key.ts
6183
+ /** Engine native identity, or harness-root-relative path identity when native identity is absent. */
6184
+ async function sessionKeyFor(input) {
6185
+ const native = input.nativeSessionId;
6186
+ const path = input.relativePath?.replaceAll("\\", "/");
6187
+ if (!input.harness || (!native || !native.trim()) && !path) throw new TypeError("SESSION_RELATIVE_PATH_REQUIRED");
6188
+ if (input.harness === "claude" && native?.trim() && input.workerIdentity) {
6189
+ const agentId = input.workerIdentity.agentId;
6190
+ const discriminator = agentId?.trim() ? ["agent-id", agentId] : ["path", path];
6191
+ if (!agentId?.trim() && !path) throw new TypeError("SESSION_RELATIVE_PATH_REQUIRED");
6192
+ const source = JSON.stringify([
6193
+ "native-v2-worker",
6194
+ input.harness,
6195
+ "session",
6196
+ native,
6197
+ ...discriminator
6198
+ ]);
6199
+ return createHash("sha256").update(source, "utf8").digest("hex");
6235
6200
  }
6236
6201
  const source = native && native.trim() ? JSON.stringify([
6237
6202
  "native-v1",
@@ -6279,15 +6244,86 @@ function createSessionKeys() {
6279
6244
  };
6280
6245
  }
6281
6246
  //#endregion
6282
- //#region apps/cli/src/account/journey/session-access.ts
6247
+ //#region apps/cli/src/account/journey/session-content-claims.ts
6283
6248
  /** Safe facts for a local preflight conflict; no paths or session content escape. */
6284
6249
  var SessionContentConflict = class extends ApplicationError {
6285
6250
  contentConflict;
6286
- constructor(contentConflict) {
6251
+ affectedWorkIds;
6252
+ constructor(contentConflict, affectedWorkIds) {
6287
6253
  super("SESSION_CONTENT_CONFLICT");
6288
6254
  this.contentConflict = contentConflict;
6255
+ this.affectedWorkIds = affectedWorkIds;
6289
6256
  }
6290
6257
  };
6258
+ function sessionContentClaims(items, inventory) {
6259
+ const requestedDigests = /* @__PURE__ */ new Map();
6260
+ const divergent = /* @__PURE__ */ new Set();
6261
+ const contentOwners = /* @__PURE__ */ new Map();
6262
+ const savedUploads = /* @__PURE__ */ new Map();
6263
+ const owners = /* @__PURE__ */ new Map();
6264
+ const holdings = /* @__PURE__ */ new Map();
6265
+ function hold(key, workId) {
6266
+ const byKey = owners.get(key) ?? /* @__PURE__ */ new Set();
6267
+ const byWork = holdings.get(workId) ?? /* @__PURE__ */ new Set();
6268
+ byKey.add(workId);
6269
+ byWork.add(key);
6270
+ owners.set(key, byKey);
6271
+ holdings.set(workId, byWork);
6272
+ }
6273
+ const byId = new Map(items.map((item) => [item.workId, item]));
6274
+ for (const work of inventory.works) if (byId.has(work.logicalWorkId)) for (const stored of work.sessions) hold(stored.key, work.logicalWorkId);
6275
+ for (const item of items) {
6276
+ if (item.sessionUpload) {
6277
+ hold(item.sessionUpload.key, item.workId);
6278
+ const claims = savedUploads.get(item.sessionUpload.key) ?? /* @__PURE__ */ new Map();
6279
+ claims.set(item.workId, item.sessionUpload.sha256);
6280
+ savedUploads.set(item.sessionUpload.key, claims);
6281
+ }
6282
+ for (const key of item.sessionMembership?.keys ?? []) hold(key, item.workId);
6283
+ }
6284
+ return {
6285
+ observe(key, sha256, item) {
6286
+ hold(key, item.workId);
6287
+ const requestedBy = contentOwners.get(key) ?? /* @__PURE__ */ new Set();
6288
+ requestedBy.add(item.workId);
6289
+ contentOwners.set(key, requestedBy);
6290
+ for (const [owner, sha] of savedUploads.get(key) ?? []) if (owner !== item.workId && byId.get(owner)?.access === item.access && sha !== sha256) divergent.add(key);
6291
+ const otherDigest = requestedDigests.get(key);
6292
+ if (otherDigest !== void 0 && otherDigest !== sha256) divergent.add(key);
6293
+ requestedDigests.set(key, sha256);
6294
+ },
6295
+ requireIndependent() {
6296
+ for (const [key, claims] of savedUploads) {
6297
+ 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);
6298
+ if (!divergent.has(key)) continue;
6299
+ const requestedBy = contentOwners.get(key) ?? /* @__PURE__ */ new Set();
6300
+ for (const owner of claims.keys()) requestedBy.add(owner);
6301
+ contentOwners.set(key, requestedBy);
6302
+ }
6303
+ const [first] = divergent;
6304
+ if (first) {
6305
+ const affected = /* @__PURE__ */ new Set();
6306
+ const pending = [...divergent];
6307
+ const visited = new Set(pending);
6308
+ for (const key of pending) for (const workId of owners.get(key) ?? []) {
6309
+ if (affected.has(workId)) continue;
6310
+ affected.add(workId);
6311
+ for (const owned of holdings.get(workId) ?? []) if (!visited.has(owned)) {
6312
+ visited.add(owned);
6313
+ pending.push(owned);
6314
+ }
6315
+ }
6316
+ if (items.some((item) => affected.has(item.workId) && !["blocked", "completed"].includes(item.status) && !item.sessionAssembly)) throw new SessionContentConflict({
6317
+ workIds: items.filter((item) => contentOwners.get(first)?.has(item.workId)).map((item) => item.workId),
6318
+ sessionKey: first
6319
+ }, affected);
6320
+ }
6321
+ }
6322
+ };
6323
+ }
6324
+ //#endregion
6325
+ //#region apps/cli/src/account/journey/session-access.ts
6326
+ const passportSessionPartByteLimit = 5 * 1024 ** 3 - 128 * 1024 ** 2;
6291
6327
  /** Collect safe conflicts across the entire pending Send before any session mutation. */
6292
6328
  async function sessionAccessBlocked(dependencies, journeyId, items, inventory, signal) {
6293
6329
  const sessions = dependencies.artifacts.sessionFacts ?? dependencies.artifacts.sessions;
@@ -6312,7 +6348,7 @@ async function sessionAccessBlocked(dependencies, journeyId, items, inventory, s
6312
6348
  conflicts.set(JSON.stringify(conflict), conflict);
6313
6349
  }
6314
6350
  const requestedAccess = /* @__PURE__ */ new Map();
6315
- const requestedDigests = /* @__PURE__ */ new Map();
6351
+ const content = sessionContentClaims(items, inventory);
6316
6352
  const contentAccess = /* @__PURE__ */ new Map();
6317
6353
  function claim(claims, identity, key, item, rule) {
6318
6354
  const previous = claims.get(identity) ?? [];
@@ -6343,26 +6379,28 @@ async function sessionAccessBlocked(dependencies, journeyId, items, inventory, s
6343
6379
  }
6344
6380
  }
6345
6381
  for (const item of items) {
6346
- if (item.sessionAssembly || item.sessionMembership || ["completed", "blocked"].includes(item.status)) continue;
6382
+ if (item.sessionAssembly || item.sessionMembership || item.status === "completed" || item.status === "blocked" && item.error !== "SESSION_CONTENT_CONFLICT") continue;
6347
6383
  const destination = inventory.works.find((work) => work.logicalWorkId === item.workId);
6348
6384
  if (destination?.access === "removed") throw new ApplicationError("WORK_REMOVED");
6349
6385
  if (destination) observe(item, destination.access);
6386
+ const selected = createSessionKeys();
6387
+ const measured = /* @__PURE__ */ new Set();
6388
+ let partBytes = 0;
6389
+ let partCount = 0;
6350
6390
  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;
6391
+ const identity = await selected.add(session);
6392
+ const key = identity.key;
6393
+ const legacyKey = identity.legacyKey ?? key;
6394
+ if (!measured.has(key)) {
6395
+ measured.add(key);
6396
+ partCount += session.metadata.parts.length;
6397
+ for (const part of session.metadata.parts) partBytes += part.byteLength;
6398
+ }
6399
+ content.observe(key, session.sha256, item);
6400
+ if (item.status === "blocked") continue;
6354
6401
  claimContent(key, session.sha256, item);
6355
6402
  if (legacyKey !== key) claimContent(legacyKey, session.sha256, item);
6356
6403
  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
6404
  for (const work of inventory.works) for (const entry of work.sessions) {
6367
6405
  if (entry.key !== key && !(entry.key === legacyKey && entry.sha256 === session.sha256)) continue;
6368
6406
  observe(item, work.access);
@@ -6370,48 +6408,288 @@ async function sessionAccessBlocked(dependencies, journeyId, items, inventory, s
6370
6408
  }
6371
6409
  if (destination && destination.access !== item.access) record(item, key, "destination-access", destination);
6372
6410
  }
6411
+ if (selected.membership(destination?.sessions ?? []).length > 1e3 || partCount > 1e4 || partBytes > passportSessionPartByteLimit) throw new ApplicationError("WORK_SESSION_CAPACITY_EXCEEDED");
6373
6412
  }
6413
+ content.requireIndependent();
6374
6414
  return [...conflicts.values()].map((conflict) => (accountAccesses.get(conflict.workId)?.size ?? 0) > 1 ? {
6375
6415
  ...conflict,
6376
6416
  accountAccess: "mixed"
6377
6417
  } : conflict);
6378
6418
  }
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);
6419
+ /** Keep overlapping session ownership serial through membership, including retained members. */
6420
+ async function sessionIndependentItems(dependencies, journeyId, items, inventory, signal) {
6421
+ const sessions = dependencies.artifacts.sessionFacts ?? dependencies.artifacts.sessions;
6422
+ if (!sessions) return [];
6423
+ 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);
6424
+ const claimed = /* @__PURE__ */ new Set();
6425
+ const independent = [];
6426
+ for (const item of pending) {
6427
+ const keys = /* @__PURE__ */ new Set([
6428
+ ...inventory.works.find((work) => work.logicalWorkId === item.workId)?.sessions.map((session) => session.key) ?? [],
6429
+ ...item.sessionMembership?.keys ?? [],
6430
+ ...item.sessionUpload ? [item.sessionUpload.key] : []
6431
+ ]);
6432
+ if (!item.sessionMembership && !item.sessionAssembly) for await (const session of sessions(journeyId, item.artifact, signal)) keys.add(await sessionKeyFor(session));
6433
+ if (![...keys].some((key) => claimed.has(key))) independent.push(item);
6434
+ for (const key of keys) claimed.add(key);
6435
+ }
6436
+ return independent;
6437
+ }
6438
+ //#endregion
6439
+ //#region apps/cli/src/account/journey/remote-error.ts
6440
+ const refusalSchema = z.object({
6441
+ error: z.string(),
6442
+ preview: sendSettingsSchema.nullable().optional(),
6443
+ resolution: sendResolutionSchema.optional(),
6444
+ /** `unknown_keep_work_ids`: the kept IDs Passport did not accept (ADR 0204). */
6445
+ workIds: z.array(z.string()).optional()
6446
+ });
6447
+ var JourneyRemoteError = class extends Error {
6448
+ retryAfter;
6449
+ status;
6450
+ code;
6451
+ details;
6452
+ constructor(status, code, details, options, retryAfter) {
6453
+ super(code, options);
6454
+ this.name = "JourneyRemoteError";
6455
+ this.retryAfter = retryAfter;
6456
+ this.status = status;
6457
+ this.code = code;
6458
+ this.details = details;
6459
+ }
6460
+ };
6461
+ //#endregion
6462
+ //#region apps/cli/src/account/journey/session-transport.ts
6463
+ /** Session requests allow ten minutes for response headers and response-body inactivity. */
6464
+ const sessionRequestTimeoutMs = 6e5;
6465
+ /**
6466
+ * Node 24 bundles undici but does not expose its Agent as a public built-in module.
6467
+ * Reuse its dispatcher through the versioned compatibility symbol, without changing
6468
+ * global transport policy. Constructing Request initializes undici without any I/O.
6469
+ * Fail closed if a future runtime removes this bridge; never fall back to five minutes.
6470
+ */
6471
+ function runtimeSessionDispatcher() {
6472
+ new Request("https://session-transport.invalid");
6473
+ const dispatcher = Reflect.get(globalThis, Symbol.for("undici.globalDispatcher.1"));
6474
+ if (typeof dispatcher !== "object" || dispatcher === null || !("dispatch" in dispatcher) || typeof dispatcher.dispatch !== "function") throw new ApplicationError("SESSION_TRANSPORT_UNAVAILABLE");
6475
+ const dispatch = dispatcher.dispatch.bind(dispatcher);
6476
+ return { dispatch(options, handler) {
6477
+ return dispatch(options, handler);
6478
+ } };
6479
+ }
6480
+ /** Per-request overrides reach undici's parser; AbortSignal alone does not change these. */
6481
+ function sessionDispatcher(delegate = runtimeSessionDispatcher(), timeoutMs = sessionRequestTimeoutMs) {
6482
+ return { dispatch(options, handler) {
6483
+ return delegate.dispatch({
6484
+ ...options,
6485
+ headersTimeout: timeoutMs,
6486
+ bodyTimeout: timeoutMs
6487
+ }, handler);
6488
+ } };
6489
+ }
6490
+ //#endregion
6491
+ //#region apps/cli/src/journey/send-sessions.ts
6492
+ /** Build upload identities from frozen local metadata and bytes from the final primary only. */
6493
+ async function sessionInputs(storage, journeyId, artifact, signal, select) {
6494
+ const query = {
6495
+ journeyId,
6496
+ workId: artifact.workId,
6497
+ revision: artifact.revision
6498
+ };
6499
+ const descriptor = await storage.readSendOutput({
6500
+ ...query,
6501
+ sessions: true
6502
+ });
6503
+ const { output, identities } = descriptor;
6504
+ if (output.bundleId !== artifact.bundleId || output.primary.sha256 !== artifact.sha256 || output.primary.archiveBytes !== artifact.byteLength) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
6505
+ return {
6506
+ output,
6507
+ identities,
6508
+ omitted: await select(storage, descriptor, signal)
6509
+ };
6510
+ }
6511
+ async function* sessionFacts(storage, journeyId, artifact, signal, select = createSendExtensionSelection()) {
6512
+ const { output, identities, omitted } = await sessionInputs(storage, journeyId, artifact, signal, select);
6513
+ yield* configuredSessionUploads(output.primary, identities, signal, "facts", omitted, (bytes) => {
6514
+ if (bytes > passportSessionPartByteLimit) throw new ApplicationError("WORK_SESSION_CAPACITY_EXCEEDED");
6515
+ });
6516
+ }
6517
+ async function* sessionArtifacts(storage, journeyId, artifact, signal, select = createSendExtensionSelection()) {
6518
+ const { output, identities, omitted } = await sessionInputs(storage, journeyId, artifact, signal, select);
6519
+ for await (const session of configuredSessionUploads(output.primary, identities, signal, "upload", omitted)) yield {
6520
+ ...session,
6521
+ async upload(upload, uploadSignal, onProgress) {
6522
+ await storage.readSendOutput({
6523
+ journeyId,
6524
+ workId: artifact.workId,
6525
+ revision: artifact.revision,
6526
+ sessions: false
6527
+ });
6528
+ const url = new URL(upload.uploadUrl);
6529
+ if (url.protocol !== "https:" || url.username || url.password || url.hash) throw new ApplicationError("INVALID_ARGUMENTS");
6530
+ const dispatcher = sessionDispatcher();
6531
+ let bytes = 0;
6532
+ const progressFailure = {};
6533
+ const request = {
6534
+ method: "PUT",
6535
+ body: session.stream().pipeThrough(new TransformStream({ async transform(chunk, controller) {
6536
+ bytes += chunk.byteLength;
6537
+ try {
6538
+ await onProgress?.(bytes);
6539
+ } catch (cause) {
6540
+ progressFailure.error = { cause };
6541
+ throw cause;
6542
+ }
6543
+ controller.enqueue(chunk);
6544
+ } })),
6545
+ signal: uploadSignal,
6546
+ dispatcher,
6547
+ redirect: "error",
6548
+ credentials: "omit",
6549
+ referrerPolicy: "no-referrer",
6550
+ headers: {
6551
+ "content-type": "application/zip",
6552
+ "content-length": String(session.byteLength),
6553
+ ...upload.headers
6554
+ },
6555
+ duplex: "half"
6556
+ };
6557
+ const response = await fetch(url, request).catch((error) => {
6558
+ if (progressFailure.error) throw progressFailure.error.cause;
6559
+ throw error;
6560
+ });
6561
+ await response.body?.cancel();
6562
+ if (!response.ok) throw new JourneyRemoteError(response.status, "UPLOAD_FAILED", null, void 0, response.headers.get("retry-after") ?? void 0);
6563
+ }
6564
+ };
6565
+ }
6566
+ /** Own one live upload iterator; advancing it releases the preceding session ZIP. */
6567
+ async function prepareSessionArtifacts(storage, journeyId, artifact, signal, select = createSendExtensionSelection()) {
6568
+ const source = sessionArtifacts(storage, journeyId, artifact, signal, select);
6569
+ let closed = false;
6570
+ async function* iterate() {
6571
+ try {
6572
+ yield* source;
6573
+ } finally {
6574
+ closed = true;
6575
+ }
6576
+ }
6577
+ const items = iterate();
6578
+ return {
6579
+ items,
6580
+ async close() {
6581
+ if (closed) return;
6582
+ closed = true;
6583
+ await items.return(void 0);
6584
+ }
6585
+ };
6586
+ }
6587
+ //#endregion
6588
+ //#region apps/cli/src/journey/send-artifacts.ts
6589
+ function createJourneySendArtifacts(storage) {
6590
+ const selection = createSendExtensionSelection();
6591
+ return {
6592
+ sessionFacts: (journeyId, artifact, signal) => sessionFacts(storage, journeyId, artifact, signal, selection),
6593
+ prepareSessions: (journeyId, artifact, signal) => prepareSessionArtifacts(storage, journeyId, artifact, signal, selection),
6594
+ sessions: (journeyId, artifact, signal) => sessionArtifacts(storage, journeyId, artifact, signal, selection),
6595
+ async describe(journey, work, signal) {
6596
+ const { output } = await storage.readSendOutput({
6597
+ journeyId: journey.journeyId,
6598
+ workId: work.id,
6599
+ revision: work.revision,
6600
+ sessions: false
6601
+ });
6602
+ const info = await readConfiguredOutputInfo(output.primary, signal);
6603
+ if (info.work.id !== work.id || info.work.access !== work.access) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
6604
+ let agentSources = [];
6605
+ try {
6606
+ const base = (await storage.resources({
6607
+ journeyId: journey.journeyId,
6608
+ kind: "base",
6609
+ id: `${work.id}:${output.finalMaskLedger.inputRevision}`
6610
+ })).find((item) => item.kind === "base" && item.workId === work.id && item.inputRevision === output.finalMaskLedger.inputRevision && item.primary.sha256 === output.finalMaskLedger.basePrimarySha256);
6611
+ if (base?.kind === "base") {
6612
+ const ids = base.sessions.map((item) => staticSessionId({
6613
+ workId: work.id,
6614
+ inputRevision: base.inputRevision,
6615
+ sessionId: item.applicationSessionId
6616
+ }));
6617
+ const included = new Set(base.sessions.map((item) => item.applicationSessionId));
6618
+ const resources = ids.length ? await storage.resources({
6619
+ journeyId: journey.journeyId,
6620
+ kind: "static-session",
6621
+ ids
6622
+ }) : [];
6623
+ const unique = /* @__PURE__ */ new Map();
6624
+ for (const resource of resources) {
6625
+ if (resource.kind !== "static-session" || resource.workId !== work.id || resource.inputRevision !== base.inputRevision || !included.has(resource.sessionId)) continue;
6626
+ for (const candidate of resource.agentSources ?? []) {
6627
+ const parsed = agentSourceSchema.safeParse(candidate);
6628
+ if (parsed.success && unique.size < 500) unique.set(JSON.stringify(parsed.data), parsed.data);
6629
+ }
6630
+ }
6631
+ agentSources = [...unique.values()];
6632
+ }
6633
+ } catch {
6634
+ console.error("AGENT_SOURCES_METADATA_UNAVAILABLE");
6635
+ }
6636
+ return sendArtifactSchema.parse({
6637
+ ...agentSources.length === 0 ? {} : { agentSources },
6638
+ workId: work.id,
6639
+ revision: work.revision,
6640
+ bundleId: info.manifest.bundleId,
6641
+ createdAt: info.createdAt,
6642
+ title: info.work.title,
6643
+ sessionCount: info.sessionCount,
6644
+ grouping: info.manifest.work.grouping,
6645
+ sources: info.sources,
6646
+ byteLength: output.primary.archiveBytes,
6647
+ sha256: output.primary.sha256
6648
+ });
6649
+ },
6650
+ async upload(journeyId, artifact, upload, signal) {
6651
+ const { output } = await storage.readSendOutput({
6652
+ journeyId,
6653
+ workId: artifact.workId,
6654
+ revision: artifact.revision,
6655
+ sessions: false
6656
+ });
6657
+ if (output.bundleId !== artifact.bundleId || output.primary.sha256 !== artifact.sha256 || output.primary.archiveBytes !== artifact.byteLength || artifact.byteLength > 5368709120) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
6658
+ await putPrimary(output.primary, artifact.byteLength, upload, signal);
6659
+ }
6660
+ };
6661
+ }
6662
+ /**
6663
+ * Stream a verified primary to a signed HTTPS URL; shared by native Send and Manual upload. A
6664
+ * checksum upload also sends the SHA-256 header the URL signs, so storage refuses other bytes.
6665
+ */
6666
+ async function putPrimary(primary, byteLength, upload, signal) {
6667
+ const url = new URL(upload.uploadUrl);
6668
+ if (url.protocol !== "https:" || url.username || url.password || url.hash) throw new ApplicationError("INVALID_ARGUMENTS");
6669
+ const source = await openConfiguredUpload(primary, signal);
6670
+ const cleanup = createCleanup();
6671
+ try {
6672
+ const request = {
6673
+ method: "PUT",
6674
+ body: source.stream,
6675
+ signal,
6676
+ redirect: "error",
6677
+ headers: {
6678
+ "Content-Type": "application/zip",
6679
+ "Content-Length": String(byteLength),
6680
+ ...upload.headers
6681
+ },
6682
+ duplex: "half"
6683
+ };
6684
+ const result = await fetch(url, request);
6685
+ await result.body?.cancel();
6686
+ if (!result.ok) throw new ApplicationError("UPLOAD_FAILED");
6687
+ } catch (error) {
6688
+ cleanup.failed(error);
6689
+ throw error;
6690
+ } finally {
6691
+ await cleanup.run(() => source.close());
6395
6692
  }
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
6693
  }
6416
6694
  //#endregion
6417
6695
  //#region apps/cli/src/account/journey/protocol.ts
@@ -6520,6 +6798,7 @@ async function checkPublicAgentSources(candidates, requestFetch = fetch, signal)
6520
6798
  //#region apps/cli/src/account/journey/session-schema.ts
6521
6799
  const digest = z.string().regex(/^[0-9a-f]{64}$/u);
6522
6800
  const stored = z.strictObject({
6801
+ metadataSha256: digest.nullable(),
6523
6802
  key: digest,
6524
6803
  sessionKey: z.string().regex(/^sess-pilot\/[^/]+\/[^/]+\/[^/]+$/u).nullable(),
6525
6804
  sha256: digest,
@@ -6618,20 +6897,24 @@ function createSessionClient(request) {
6618
6897
  let next = null;
6619
6898
  const seen = /* @__PURE__ */ new Set();
6620
6899
  const sessions = /* @__PURE__ */ new Map();
6621
- let value;
6900
+ let access;
6622
6901
  do {
6623
6902
  const query = new URLSearchParams({ limit: "500" });
6624
6903
  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");
6904
+ 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) => {
6905
+ if (next === null && error instanceof JourneyRemoteError && error.status === 404 && error.code === "not_found") return null;
6906
+ throw error;
6907
+ });
6908
+ if (!page) return null;
6626
6909
  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
- };
6910
+ access = page.access;
6632
6911
  next = continuation(seen, page.nextCursor);
6633
6912
  } while (next !== null);
6634
- return value;
6913
+ return {
6914
+ logicalWorkId: id,
6915
+ access,
6916
+ sessions: [...sessions.values()]
6917
+ };
6635
6918
  },
6636
6919
  reserve: (input, signal) => request("/me/sessions/reserve", reserve, signal, input),
6637
6920
  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 +7035,21 @@ function createJourneyOwnerClient(options) {
6752
7035
  const { headers } = await requestWithHeaders("/me/profile", publicProfileSchema, signal);
6753
7036
  return { parallelCompletes: (headers.get("passport-capabilities") ?? "").split(",").some((token) => token.trim() === "parallel-completes") };
6754
7037
  },
7038
+ async ownedPassport(signal) {
7039
+ const webOrigin = passportWebOrigin(options.apiUrl, options.webOrigin);
7040
+ if (!webOrigin) throw new ApplicationError("ACCOUNT_PROFILE_UNAVAILABLE");
7041
+ try {
7042
+ const result = await request("/me/passport", z.strictObject({ publicId: sendReceiptSchema.shape.publicId }), signal, void 0, "GET");
7043
+ return new URL("/" + result.publicId, webOrigin).href;
7044
+ } catch (error) {
7045
+ if (error instanceof JourneyRemoteError && error.status === 404 && error.code === "passport not found") {
7046
+ await options.authorize();
7047
+ signal.throwIfAborted();
7048
+ return null;
7049
+ }
7050
+ throw error;
7051
+ }
7052
+ },
6755
7053
  readProfile(signal) {
6756
7054
  return request("/me/profile", ownerProfileSchema, signal, void 0, "GET");
6757
7055
  },
@@ -6944,48 +7242,267 @@ async function resolveItem(item, bindingId, connection, signal, snapshot = false
6944
7242
  kind: "existing",
6945
7243
  bindingId: named[0].bindingId
6946
7244
  };
6947
- else if (!target || target.kind === "existing" && !resolution.candidates.map((candidate) => candidate.bindingId).includes(target.bindingId)) return {
6948
- ...item,
6949
- target: null,
6950
- targetConfirmed: false,
6951
- resolution,
6952
- status: "target",
6953
- error: null
7245
+ else if (!target || target.kind === "existing" && !resolution.candidates.map((candidate) => candidate.bindingId).includes(target.bindingId)) return {
7246
+ ...item,
7247
+ target: null,
7248
+ targetConfirmed: false,
7249
+ resolution,
7250
+ status: "target",
7251
+ error: null
7252
+ };
7253
+ if (!target) throw new ApplicationError("INVALID_STATE");
7254
+ const targetId = target.kind === "existing" ? target.bindingId : null;
7255
+ const candidate = resolution.candidates.find((value) => value.bindingId === targetId);
7256
+ if (candidate && candidate.access !== item.access) {
7257
+ const settings = await connection.preview(candidate.workId, item.access, signal);
7258
+ return askSettings({
7259
+ ...item,
7260
+ target,
7261
+ resolution
7262
+ }, settings, null, snapshot);
7263
+ }
7264
+ const agentSources = await connection.checkAgentSources?.(item.artifact.agentSources, signal);
7265
+ signal.throwIfAborted();
7266
+ return {
7267
+ ...item,
7268
+ target,
7269
+ resolution,
7270
+ status: "uploading",
7271
+ error: null,
7272
+ reservation: {
7273
+ ...agentSources?.length ? { agentSources } : {},
7274
+ work,
7275
+ target,
7276
+ bundleId: item.artifact.bundleId,
7277
+ createdAt: item.artifact.createdAt,
7278
+ title: item.artifact.title,
7279
+ sessionCount: item.artifact.sessionCount,
7280
+ access: item.access,
7281
+ outputRevision: item.artifact.revision,
7282
+ byteLength: item.artifact.byteLength,
7283
+ sha256: item.artifact.sha256,
7284
+ expectedAccessVersion: resolution.accessVersion,
7285
+ expectedScopeVersion: resolution.scopeVersion
7286
+ }
7287
+ };
7288
+ }
7289
+ /** Keep only the current Send's pending assessment IDs. New arrivals wait behind unpolled peers. */
7290
+ function createAssessmentPoll() {
7291
+ let owner = null;
7292
+ let sendId;
7293
+ let pending = [];
7294
+ return (send, works) => {
7295
+ const byId = new Map(works.map((work) => [work.workId, work]));
7296
+ if (send.subject !== owner || send.id !== sendId) pending = [];
7297
+ owner = send.subject;
7298
+ sendId = send.id;
7299
+ const order = new Set(pending.filter((id) => byId.has(id)));
7300
+ for (const id of byId.keys()) order.add(id);
7301
+ pending = [...order];
7302
+ const selected = pending.splice(0, 3);
7303
+ pending.push(...selected);
7304
+ return selected.flatMap((id) => byId.get(id) ?? []);
7305
+ };
7306
+ }
7307
+ const confirmedSettings = (item) => item.settings !== null && item.settingsConfirmed;
7308
+ const started = (item) => Number(item.status === "uploading");
7309
+ /**
7310
+ * Works that share a workspace path or a git remote: once one completes, Passport offers it as
7311
+ * the other's candidate, so the other's target no longer resolves the same (ADR 0235).
7312
+ */
7313
+ function related(left, right) {
7314
+ 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))));
7315
+ }
7316
+ /**
7317
+ * The Works one advance steps together. Upload work comes before assessment checks: up to three
7318
+ * Works, started uploads first, then the largest ZIP, so the slowest Work starts earliest.
7319
+ * Completing an upload moves the owner's scope version, which Passport accepts while a Work's
7320
+ * own target resolves the same (ADR 0235). A Work related to one already picked waits for the
7321
+ * next batch. A reserved Work whose target would not survive that steps alone, and so does a
7322
+ * confirmed linked Settings change, which moves the access version every reservation carries,
7323
+ * once no upload has started. Assessment checks change nothing and step up to three together.
7324
+ */
7325
+ function pickBatch(items, limit, poll) {
7326
+ const uploads = items.filter(sendsOnNextAdvance).toSorted((left, right) => started(right) - started(left) || right.artifact.byteLength - left.artifact.byteLength);
7327
+ 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);
7328
+ if (alone) return [alone];
7329
+ if (uploads.length) return uploads.filter((item) => !confirmedSettings(item)).reduce((batch, item) => batch.length < limit && !batch.some((picked) => related(picked, item)) ? [...batch, item] : batch, []);
7330
+ const assessing = items.filter((item) => item.status === "assessing");
7331
+ const first = assessing[0];
7332
+ if (!first) return [];
7333
+ if (confirmedSettings(first)) return [first];
7334
+ const checks = assessing.filter((item) => !confirmedSettings(item));
7335
+ return poll ? poll(checks) : checks.slice(0, 3);
7336
+ }
7337
+ function isCode(error, code) {
7338
+ return error instanceof ApplicationError && error.code === code;
7339
+ }
7340
+ /**
7341
+ * A step that ended without a definite answer and is safe to repeat: a request timeout or abort
7342
+ * that is not the Send's own cancellation, or a connection failure from `fetch`: `fetch failed`
7343
+ * before the response, `terminated` when the socket resets while the body is read. The Work keeps
7344
+ * its state, so the next advance repeats the same idempotent step (ADR 0197).
7345
+ */
7346
+ function isInterrupted(error) {
7347
+ if (!(error instanceof Error)) return false;
7348
+ if (error.name === "TimeoutError" || error.name === "AbortError") return true;
7349
+ return error instanceof TypeError && (error.message === "fetch failed" || error.message === "terminated");
7350
+ }
7351
+ //#endregion
7352
+ //#region apps/cli/src/account/journey/session-facts-cache.ts
7353
+ const keyFor = (journeyId, artifact) => JSON.stringify([
7354
+ journeyId,
7355
+ artifact.workId,
7356
+ artifact.revision,
7357
+ artifact.sha256
7358
+ ]);
7359
+ /** Sender-owned facts only. In-flight computations never enter the bounded cache. */
7360
+ function createSessionFactsCache(limit = 256) {
7361
+ const facts = /* @__PURE__ */ new Map();
7362
+ let owner = "";
7363
+ let generation = 0;
7364
+ let allowed = /* @__PURE__ */ new Set();
7365
+ return {
7366
+ retain(journeyId, send) {
7367
+ const nextOwner = JSON.stringify([journeyId, send?.id]);
7368
+ const ended = !send || [
7369
+ "completed",
7370
+ "blocked",
7371
+ "prepared"
7372
+ ].includes(send.status);
7373
+ if (owner !== nextOwner || ended) {
7374
+ facts.clear();
7375
+ generation += 1;
7376
+ }
7377
+ owner = nextOwner;
7378
+ allowed = new Set(ended || !send ? [] : send.items.filter((item) => !item.sessionAssembly && item.status !== "completed").map((item) => keyFor(journeyId, item.artifact)));
7379
+ for (const key of facts.keys()) if (!allowed.has(key)) facts.delete(key);
7380
+ },
7381
+ async get(journeyId, artifact, signal, compute) {
7382
+ signal.throwIfAborted();
7383
+ const key = keyFor(journeyId, artifact);
7384
+ const cached = facts.get(key);
7385
+ if (cached) return cached;
7386
+ const started = generation;
7387
+ const items = await compute();
7388
+ signal.throwIfAborted();
7389
+ if (started === generation && allowed.has(key)) {
7390
+ facts.set(key, items);
7391
+ while (facts.size > limit) {
7392
+ const oldest = facts.keys().next().value;
7393
+ if (oldest === void 0) break;
7394
+ facts.delete(oldest);
7395
+ }
7396
+ }
7397
+ return items;
7398
+ }
7399
+ };
7400
+ }
7401
+ //#endregion
7402
+ //#region apps/cli/src/account/journey/session-preparation.ts
7403
+ /** Shared facts retain no bytes; only upload steps acquire phase-local live iterators. */
7404
+ function createSessionPreparation(dependencies, facts) {
7405
+ const prepare = dependencies.artifacts.prepareSessions;
7406
+ const prepared = [];
7407
+ const sessions = async function* (journeyId, artifact, signal) {
7408
+ if (!prepare) {
7409
+ const original = dependencies.artifacts.sessions;
7410
+ if (original) yield* original(journeyId, artifact, signal);
7411
+ return;
7412
+ }
7413
+ const result = await prepare(journeyId, artifact, signal);
7414
+ prepared.push(result);
7415
+ for await (const item of result.items) {
7416
+ signal.throwIfAborted();
7417
+ yield item;
7418
+ }
7419
+ };
7420
+ const sessionFacts = async function* (journeyId, artifact, signal) {
7421
+ const items = await facts.get(journeyId, artifact, signal, async () => {
7422
+ const original = dependencies.artifacts.sessionFacts ?? dependencies.artifacts.sessions;
7423
+ const items = [];
7424
+ if (original) for await (const item of original(journeyId, artifact, signal)) {
7425
+ const { workerIdentity, nativeSessionId, relativePath, harness, lastAt, metadata, sha256, byteLength, crc32 } = item;
7426
+ items.push({
7427
+ ...workerIdentity ? { workerIdentity } : {},
7428
+ nativeSessionId,
7429
+ relativePath,
7430
+ harness,
7431
+ lastAt,
7432
+ metadata,
7433
+ sha256,
7434
+ byteLength,
7435
+ crc32
7436
+ });
7437
+ }
7438
+ return items;
7439
+ });
7440
+ for (const item of items) {
7441
+ signal.throwIfAborted();
7442
+ yield item;
7443
+ }
7444
+ };
7445
+ return {
7446
+ dependencies: {
7447
+ ...dependencies,
7448
+ artifacts: {
7449
+ ...dependencies.artifacts,
7450
+ sessions,
7451
+ sessionFacts
7452
+ }
7453
+ },
7454
+ async close() {
7455
+ await createCleanup().run(...prepared.map((result) => () => result.close()));
7456
+ }
6954
7457
  };
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();
7458
+ }
7459
+ //#endregion
7460
+ //#region apps/cli/src/account/journey/session-metadata.ts
7461
+ /** The owner API uses this exact tuple; object key insertion order is not a wire dependency. */
7462
+ function sessionMetadataSha256(metadata) {
7463
+ return createHash("sha256").update(JSON.stringify([
7464
+ metadata.workspace.title,
7465
+ metadata.workspace.pathHash,
7466
+ metadata.workspace.remoteHashes,
7467
+ metadata.workspace.primaryRemoteHash,
7468
+ metadata.sourceSessionId,
7469
+ metadata.harness,
7470
+ metadata.lastAt,
7471
+ metadata.parts.map((part) => [
7472
+ part.path,
7473
+ part.byteLength,
7474
+ part.sha256,
7475
+ part.crc32,
7476
+ [
7477
+ part.native.role,
7478
+ part.native.sessionId,
7479
+ part.native.agentId,
7480
+ part.native.dispatchParentSessionId,
7481
+ part.native.fork ? [
7482
+ part.native.fork.parentSessionId,
7483
+ part.native.fork.position.kind,
7484
+ part.native.fork.position.value
7485
+ ] : null
7486
+ ]
7487
+ ])
7488
+ ])).digest("hex");
7489
+ }
7490
+ //#endregion
7491
+ //#region apps/cli/src/account/journey/receipt.ts
7492
+ function acceptReceipt(item, receipt) {
7493
+ 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");
7494
+ const { upload: _upload, ...saved } = receipt;
7495
+ const success = receipt.state === "succeeded" && receipt.assessmentState === "succeeded" && receipt.passportIncluded && receipt.effectiveAccess === item.access;
7496
+ const failure = [
7497
+ "failed",
7498
+ "partial",
7499
+ "superseded"
7500
+ ].includes(receipt.state) || receipt.state === "succeeded" && !success;
6968
7501
  return {
6969
7502
  ...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
6988
- }
7503
+ receipt: saved,
7504
+ status: success ? "completed" : failure ? "blocked" : receipt.state === "awaiting_upload" ? "uploading" : "assessing",
7505
+ error: failure ? receipt.state === "succeeded" ? "PASSPORT_NOT_CURRENT" : "ASSESSMENT_INCOMPLETE" : null
6989
7506
  };
6990
7507
  }
6991
7508
  //#endregion
@@ -7147,51 +7664,6 @@ function renewed(item, error) {
7147
7664
  error
7148
7665
  };
7149
7666
  }
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
7667
  const sessionBackoffMs = (attempt) => 1e3 * 2 ** (attempt - 1);
7196
7668
  const zeroByteBackoffMs = (count) => [
7197
7669
  15e3,
@@ -7206,7 +7678,7 @@ function retryDelay(error, fallback, now) {
7206
7678
  return Number.isFinite(delay) ? Math.max(fallback, delay) : fallback;
7207
7679
  }
7208
7680
  /** Reconcile retained intents; only byte-started failures charge the durable budget. */
7209
- async function attemptSession({ dependencies, original, session, key, remote, signal, guard, checkpoint }) {
7681
+ async function attemptSession({ dependencies, original, countCompletion, session, key, remote, signal, guard, checkpoint }) {
7210
7682
  const previous = original.sessionUpload?.key === key && original.sessionUpload.sha256 === session.sha256 ? original.sessionUpload : null;
7211
7683
  let failures = previous?.attempt ?? 0;
7212
7684
  let retryCount = previous?.retryCount ?? 0;
@@ -7259,12 +7731,12 @@ async function attemptSession({ dependencies, original, session, key, remote, si
7259
7731
  lastReported = Date.now();
7260
7732
  }
7261
7733
  });
7262
- await guard();
7263
- await remote.complete({
7264
- ...input,
7265
- metadata: session.metadata
7266
- }, signal);
7267
7734
  }
7735
+ await guard();
7736
+ await remote.complete({
7737
+ ...input,
7738
+ metadata: session.metadata
7739
+ }, signal);
7268
7740
  };
7269
7741
  try {
7270
7742
  if (previous === null) await reserveUploadComplete();
@@ -7278,6 +7750,10 @@ async function attemptSession({ dependencies, original, session, key, remote, si
7278
7750
  if (failures >= 3) throw new ApplicationError("SESSION_UPLOAD_EXHAUSTED");
7279
7751
  await reserveUploadComplete();
7280
7752
  }
7753
+ if (countCompletion) item = {
7754
+ ...item,
7755
+ storedSessions: (original.storedSessions ?? 0) + 1
7756
+ };
7281
7757
  transfer.bytesAcknowledged = session.byteLength;
7282
7758
  await guard();
7283
7759
  await save();
@@ -7317,6 +7793,7 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7317
7793
  if (!item.sessionAssembly && (item.status !== "uploading" || item.receipt || item.reservation)) return {
7318
7794
  ...item,
7319
7795
  receipt: null,
7796
+ storedSessions: item.sessionMembership ? item.storedSessions : 0,
7320
7797
  reservation: null,
7321
7798
  target: null,
7322
7799
  previousReceipts: item.receipt ? [...item.previousReceipts, item.receipt] : item.previousReceipts,
@@ -7350,7 +7827,7 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7350
7827
  const intent = item.sessionMembership;
7351
7828
  const assembly = await remote.assembly(intent.workId, signal);
7352
7829
  const conflict = async () => {
7353
- const current = await remote.inventory(signal);
7830
+ const current = await remote.work(intent.workId, signal);
7354
7831
  const revision = await remote.assembly(intent.workId, signal);
7355
7832
  await guard();
7356
7833
  return {
@@ -7359,7 +7836,7 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7359
7836
  error: "SESSION_MEMBERSHIP_CONFLICT",
7360
7837
  membershipConflict: {
7361
7838
  revision: revision?.revision ?? 0,
7362
- keys: current.works.find((work) => work.logicalWorkId === intent.workId)?.sessions.map((session) => session.key) ?? []
7839
+ keys: current?.sessions.map((session) => session.key) ?? []
7363
7840
  }
7364
7841
  };
7365
7842
  };
@@ -7376,7 +7853,7 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7376
7853
  const { workId, ...input } = intent;
7377
7854
  const result = await remote.membership(workId, input, signal);
7378
7855
  await guard();
7379
- if (migrate) {
7856
+ if (migrate || item.sessionMembershipMetadataVersion !== 1) {
7380
7857
  const { sessionMembership: _, ...rest } = item;
7381
7858
  return {
7382
7859
  ...rest,
@@ -7394,6 +7871,7 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7394
7871
  error: null
7395
7872
  };
7396
7873
  } catch (error) {
7874
+ if (error instanceof JourneyRemoteError && error.status === 413 && error.code === "work_session_capacity_exceeded") throw new ApplicationError("WORK_SESSION_CAPACITY_EXCEEDED");
7397
7875
  if (error instanceof JourneyRemoteError && error.status === 409) return conflict();
7398
7876
  throw error;
7399
7877
  }
@@ -7412,21 +7890,30 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7412
7890
  }
7413
7891
  }
7414
7892
  const ownKeys = /* @__PURE__ */ new Set();
7893
+ if (!recoverLegacy) item = {
7894
+ ...item,
7895
+ storedSessions: 0
7896
+ };
7415
7897
  for await (const session of sessions(journeyId, item.artifact, signal)) {
7416
7898
  const identity = await keys.add(session);
7417
7899
  ownKeys.add(identity.key);
7418
7900
  if (recoverLegacy && (identity.legacyKey !== pending?.key || session.sha256 !== pending?.sha256)) continue;
7419
7901
  const key = recoverLegacy && pending ? pending.key : identity.key;
7420
- if (stored.get(key)?.sha256 === session.sha256) {
7902
+ if (stored.get(key)?.sha256 === session.sha256 && stored.get(key)?.metadataSha256 === sessionMetadataSha256(session.metadata)) {
7421
7903
  if (recoverLegacy) return {
7422
7904
  ...item,
7423
7905
  sessionUpload: null
7424
7906
  };
7907
+ item = {
7908
+ ...item,
7909
+ storedSessions: (item.storedSessions ?? 0) + 1
7910
+ };
7425
7911
  continue;
7426
7912
  }
7427
7913
  item = await attemptSession({
7428
7914
  dependencies,
7429
7915
  original: item,
7916
+ countCompletion: !recoverLegacy,
7430
7917
  session,
7431
7918
  key,
7432
7919
  remote,
@@ -7438,7 +7925,7 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7438
7925
  }
7439
7926
  await guard();
7440
7927
  const assembly = await remote.assembly(item.workId, signal);
7441
- const current = (await remote.inventory(signal)).works.find((work) => work.logicalWorkId === item.workId);
7928
+ const current = await remote.work(item.workId, signal);
7442
7929
  if (current && current.access !== item.access) {
7443
7930
  if (current.access === "removed") throw new ApplicationError("WORK_REMOVED");
7444
7931
  const [ownKey] = ownKeys;
@@ -7463,6 +7950,7 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
7463
7950
  ...item,
7464
7951
  sessionUpload: null,
7465
7952
  sessionMembershipKeyVersion: 2,
7953
+ sessionMembershipMetadataVersion: 1,
7466
7954
  sessionMembership: {
7467
7955
  workId: item.workId,
7468
7956
  expectedRevision: assembly?.revision ?? 0,
@@ -7505,6 +7993,35 @@ async function saveSend(dependencies, snapshot, next, action, expectedJourneyRev
7505
7993
  at: dependencies.now()
7506
7994
  });
7507
7995
  }
7996
+ /** This transaction changes only the supplied transfer fields and durable cursor. */
7997
+ async function saveTransfer(dependencies, current, workId, patch) {
7998
+ const progress = await dependencies.storage.checkpointTransfer({
7999
+ journeyId: current.journey.journeyId,
8000
+ sendId: current.send.id,
8001
+ workId,
8002
+ expectedJourneyRevision: current.journey.revision,
8003
+ expectedSendRevision: current.send.revision,
8004
+ requestKey: randomUUID(),
8005
+ at: dependencies.now(),
8006
+ ...patch
8007
+ });
8008
+ if (progress.status !== "applied") throw new ApplicationError("REVISION_CONFLICT");
8009
+ return {
8010
+ journey: {
8011
+ ...current.journey,
8012
+ revision: progress.revision,
8013
+ lastEvent: progress.lastEvent
8014
+ },
8015
+ send: {
8016
+ ...current.send,
8017
+ revision: progress.sendRevision,
8018
+ items: current.send.items.map((item) => item.workId === workId ? {
8019
+ ...item,
8020
+ ...patch
8021
+ } : item)
8022
+ }
8023
+ };
8024
+ }
7508
8025
  function aggregate(send) {
7509
8026
  if (send.status === "paused") return send;
7510
8027
  const statuses = send.items.map((item) => item.status);
@@ -7513,17 +8030,6 @@ function aggregate(send) {
7513
8030
  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
8031
  };
7515
8032
  }
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
8033
  /** Explicit resume retries recoverable items and recaptures a pending sign-in or profile. */
7528
8034
  function resumeState(send) {
7529
8035
  if (send.status !== "completed" && send.items.length && send.items.every((item) => item.status === "completed") && send.items.some((item) => !item.sessionAssembly)) return {
@@ -7652,15 +8158,126 @@ async function captureProfile(connection, guard, signal, failure) {
7652
8158
  }
7653
8159
  }
7654
8160
  //#endregion
8161
+ //#region apps/cli/src/account/journey/remote-guard.ts
8162
+ /**
8163
+ * The class of every method. Adding a method to either remote without classifying it here fails
8164
+ * the type check, and the guarded remote exposes only the methods named below.
8165
+ */
8166
+ const connectionClasses = {
8167
+ assertCurrent: "read",
8168
+ syncProfile: "profile-sync",
8169
+ readProfile: "read",
8170
+ addressTaken: "read",
8171
+ status: "read",
8172
+ checkAgentSources: "agent-sources-check",
8173
+ resolve: "work-resolve",
8174
+ preview: "settings-preview",
8175
+ confirm: "settings-confirm",
8176
+ reserve: "work-reserve",
8177
+ complete: "work-complete",
8178
+ replace: "works-replace",
8179
+ saveProfile: "profile-save",
8180
+ ownedPassport: "read"
8181
+ };
8182
+ const sessionClasses = {
8183
+ inventory: "read",
8184
+ assessment: "read",
8185
+ work: "session-work",
8186
+ assembly: "session-assembly",
8187
+ reserve: "session-reserve",
8188
+ complete: "session-complete",
8189
+ membership: "session-membership"
8190
+ };
8191
+ /**
8192
+ * The one place that decides whether a Send-path call may send something about the person's
8193
+ * work (ADR 0284). Rules of this kind belong here, never in a caller; `mutation` is for the
8194
+ * rules to come.
8195
+ */
8196
+ function assertMayLeave(scope, mutation) {
8197
+ if (!mayLeave(scope, mutation)) throw new ApplicationError("JOURNEY_OBSOLETE");
8198
+ }
8199
+ /** `assertMayLeave` as a question, for a caller that must skip what the guard would refuse. */
8200
+ function mayLeave(scope, _mutation) {
8201
+ const atPassport = scope.item ? itemAtPassport(scope.item) : scope.send?.items.some(itemAtPassport) ?? false;
8202
+ return !(scope.journey && obsoleteJourney(scope.journey) && !atPassport);
8203
+ }
8204
+ /**
8205
+ * The remote a Send uses, closed by default: it holds only the methods classified above. A read
8206
+ * passes through; every other method asks `assertMayLeave` first. A refusal rejects like a failed
8207
+ * remote call, so the advance blocks only that Work.
8208
+ */
8209
+ function guardSendRemote(connection, scope) {
8210
+ const through = (kind, call) => kind === "read" ? call : (...args) => guardedSendMutation(scope, kind, () => call(...args));
8211
+ const work = connectionClasses;
8212
+ const { sessions, checkAgentSources } = connection;
8213
+ return {
8214
+ subject: connection.subject,
8215
+ assertCurrent: through(work.assertCurrent, connection.assertCurrent),
8216
+ syncProfile: through(work.syncProfile, connection.syncProfile),
8217
+ readProfile: through(work.readProfile, connection.readProfile),
8218
+ addressTaken: through(work.addressTaken, connection.addressTaken),
8219
+ status: through(work.status, connection.status),
8220
+ resolve: through(work.resolve, connection.resolve),
8221
+ preview: through(work.preview, connection.preview),
8222
+ confirm: through(work.confirm, connection.confirm),
8223
+ reserve: through(work.reserve, connection.reserve),
8224
+ complete: through(work.complete, connection.complete),
8225
+ replace: through(work.replace, connection.replace),
8226
+ saveProfile: through(work.saveProfile, connection.saveProfile),
8227
+ ownedPassport: through(work.ownedPassport, connection.ownedPassport),
8228
+ ...checkAgentSources ? { checkAgentSources: through(work.checkAgentSources, checkAgentSources) } : {},
8229
+ ...sessions ? { sessions: {
8230
+ inventory: through(sessionClasses.inventory, sessions.inventory),
8231
+ assessment: through(sessionClasses.assessment, sessions.assessment),
8232
+ work: through(sessionClasses.work, sessions.work),
8233
+ assembly: through(sessionClasses.assembly, sessions.assembly),
8234
+ reserve: through(sessionClasses.reserve, sessions.reserve),
8235
+ complete: through(sessionClasses.complete, sessions.complete),
8236
+ membership: through(sessionClasses.membership, sessions.membership)
8237
+ } } : {}
8238
+ };
8239
+ }
8240
+ /** A guarded call that is not a remote method: the byte upload to a signed address. */
8241
+ function guardSendCall(scope, mutation, call) {
8242
+ return guardedSendMutation(scope, mutation, call);
8243
+ }
8244
+ /** Each session's byte upload to a signed address asks the guard too (ADR 0284). */
8245
+ function guardSessionUploads(sessions, scope) {
8246
+ return async function* (journeyId, artifact, signal) {
8247
+ for await (const session of sessions(journeyId, artifact, signal)) yield {
8248
+ ...session,
8249
+ upload: (upload, uploadSignal, progress) => guardedSendMutation(scope, "session-upload", () => session.upload(upload, uploadSignal, progress))
8250
+ };
8251
+ };
8252
+ }
8253
+ /** Named so a test can tell a guarded call from one that bypassed the guard. */
8254
+ function guardedSendMutation(scope, mutation, call) {
8255
+ try {
8256
+ assertMayLeave(scope, mutation);
8257
+ } catch (error) {
8258
+ return Promise.reject(error);
8259
+ }
8260
+ return call();
8261
+ }
8262
+ //#endregion
7655
8263
  //#region apps/cli/src/account/journey/advance.ts
7656
8264
  /** A Work Passport is done with: assessed, or stopped unless Resume retries it. */
7657
8265
  const settled = (item) => ["completed", "blocked"].includes(item.status);
8266
+ /** The byte upload to a signed address is a guarded call too (ADR 0284). */
8267
+ function guardedContext(context, scope, checksumUploads) {
8268
+ const upload = context.upload;
8269
+ return {
8270
+ ...context,
8271
+ checksumUploads,
8272
+ upload: (artifact, signed, signal) => guardSendCall(scope, "work-upload", () => upload(artifact, signed, signal))
8273
+ };
8274
+ }
7658
8275
  /** Stored state equality: key order and explicit undefined fields do not count. */
7659
8276
  const sameJson = (left, right) => isDeepStrictEqual(JSON.parse(JSON.stringify(left)), JSON.parse(JSON.stringify(right)));
7660
8277
  /** The stored Send still holds exactly this Work state, whatever else changed meanwhile. */
7661
8278
  function sameItem(items, item) {
7662
8279
  const current = items.find((value) => value.workId === item.workId);
7663
- return current !== void 0 && sameJson(current, item);
8280
+ return current === item || current !== void 0 && sameJson(current, item);
7664
8281
  }
7665
8282
  /** Shared sign-in, account and per-item rules for native Send and Manual upload. */
7666
8283
  async function advanceItems(host, signal) {
@@ -7702,7 +8319,7 @@ async function advanceItems(host, signal) {
7702
8319
  });
7703
8320
  }
7704
8321
  if (send.subject === null || send.status === "sign-in") {
7705
- const { parallelCompletes: captured, ...profile } = await host.profile(connection, signal);
8322
+ const { parallelCompletes: captured, ...profile } = await host.profile(guardSendRemote(connection, host.scope(null)), signal);
7706
8323
  const parallelCompletes = send.subject === null ? captured : send.parallelCompletes && captured;
7707
8324
  const prepared = {
7708
8325
  ...send,
@@ -7713,17 +8330,16 @@ async function advanceItems(host, signal) {
7713
8330
  else delete prepared.parallelCompletes;
7714
8331
  return host.save(prepared.status === "sending" ? aggregate(prepared) : prepared);
7715
8332
  }
7716
- const blocked = await host.preflight?.(connection, signal);
8333
+ const blocked = await host.preflight?.(guardSendRemote(connection, host.scope(null)), signal);
7717
8334
  if (blocked) return blocked;
7718
- const batch = host.batch?.() ?? pickBatch(send.items, Date.now(), send.parallelCompletes ? 3 : 1);
8335
+ const batch = host.batch?.() ?? pickBatch(send.items, send.parallelCompletes ? 3 : 1, host.poll?.bind(void 0, send));
7719
8336
  if (!batch.length) return host.applied();
7720
8337
  const busy = Math.max(0, ...batch.map((item) => item.busyRefusals ?? 0));
7721
8338
  if (busy) await wait(busyDelay(busy), signal);
7722
8339
  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)));
8340
+ const run = (item) => (host.step ?? stepItem)(guardedContext(host.context(item), host.scope(item), checksumUploads), item, guardSendRemote(connection, host.scope(item)), signal);
8341
+ const refill = host.refill;
8342
+ 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
8343
  const failures = results.flatMap((result) => result.status === "rejected" ? [result] : []);
7728
8344
  if (failures.some(({ reason }) => isCode(reason, "REVISION_CONFLICT") || isCode(reason, "SEND_SCOPE_INVALIDATED"))) return host.stale();
7729
8345
  const first = failures[0];
@@ -7734,7 +8350,7 @@ async function advanceItems(host, signal) {
7734
8350
  const next = /* @__PURE__ */ new Map();
7735
8351
  for (const [index, result] of results.entries()) {
7736
8352
  const item = batch[index];
7737
- if (!item) continue;
8353
+ if (!item || !result) continue;
7738
8354
  if (result.status === "fulfilled") {
7739
8355
  next.set(item.workId, result.value);
7740
8356
  continue;
@@ -7803,6 +8419,44 @@ async function advanceItems(host, signal) {
7803
8419
  return blockSend(host, error, signal);
7804
8420
  }
7805
8421
  }
8422
+ /** Every queued Work was admitted as independent at preflight. Recheck delayed slots before use. */
8423
+ async function runSessionQueue(batch, run, admit, signal) {
8424
+ const results = [];
8425
+ let next = 0;
8426
+ let stopped = false;
8427
+ let admission;
8428
+ async function worker() {
8429
+ while (!stopped && next < batch.length) {
8430
+ const index = next++;
8431
+ const item = batch[index];
8432
+ if (!item) return;
8433
+ try {
8434
+ if (index >= 3) {
8435
+ admission ??= admit().finally(() => {
8436
+ admission = void 0;
8437
+ });
8438
+ if (!await admission) stopped = true;
8439
+ if (stopped) return;
8440
+ }
8441
+ signal.throwIfAborted();
8442
+ const value = await run(item);
8443
+ results[index] = {
8444
+ status: "fulfilled",
8445
+ value
8446
+ };
8447
+ if (value.status === "blocked") stopped = true;
8448
+ } catch (reason) {
8449
+ stopped = true;
8450
+ results[index] = {
8451
+ status: "rejected",
8452
+ reason
8453
+ };
8454
+ }
8455
+ }
8456
+ }
8457
+ await Promise.all(Array.from({ length: 3 }, worker));
8458
+ return results;
8459
+ }
7806
8460
  /** An unexpected failure blocks the whole send with a safe code; abort keeps propagating. */
7807
8461
  function blockSend(host, error, signal) {
7808
8462
  if (signal.aborted) throw error;
@@ -7846,35 +8500,47 @@ function completeAtUpload(send, sessions) {
7846
8500
  }
7847
8501
  async function advanceSend(dependencies, address, signal, chain) {
7848
8502
  let snapshot = requireSend(await dependencies.storage.loadSend(address.journeyId, address.sendId));
7849
- const sessionFacts = chain?.sessionFacts ?? createSessionFactsCache();
8503
+ const sessionFacts = chain.sessionFacts;
7850
8504
  sessionFacts.retain(address.journeyId, snapshot.send);
7851
- if (chain) chain.revision = snapshot.send.revision;
8505
+ chain.revision = snapshot.send.revision;
7852
8506
  if ((await dependencies.storage.loadSend(address.journeyId))?.send?.id !== snapshot.send.id) return {
7853
8507
  ...snapshot,
7854
8508
  status: "stale"
7855
8509
  };
7856
8510
  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 {
8511
+ if (!advanceStepsSend(snapshot.journey, snapshot.send)) return {
7858
8512
  ...snapshot,
7859
8513
  status: "applied"
7860
8514
  };
7861
8515
  const checkpoints = /* @__PURE__ */ new Map();
7862
8516
  const fulfilled = /* @__PURE__ */ new Set();
7863
8517
  let saving = Promise.resolve();
8518
+ let validated;
7864
8519
  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);
8520
+ const observed = snapshot;
8521
+ const isCurrent = (candidate) => dependencies.storage.isCurrentSend(address.journeyId, candidate.send.id, candidate.journey.revision);
8522
+ let current = observed;
8523
+ if (!await isCurrent(observed)) {
8524
+ current = requireSend(await dependencies.storage.loadSend(address.journeyId));
8525
+ if (current.send.id !== observed.send.id || !await isCurrent(current)) throw new ApplicationError("REVISION_CONFLICT");
8526
+ }
8527
+ if (!sameItem(current.send.items, checkpoints.get(item.workId) ?? item) || current.send.status === "paused" || current.journey.phase === "paused") throw new ApplicationError("REVISION_CONFLICT");
8528
+ if (current !== validated) {
8529
+ assertScope(current.journey, current.send);
8530
+ validated = current;
8531
+ }
7868
8532
  return current;
7869
8533
  }
7870
8534
  const save = (proposed) => {
7871
8535
  const next = {
7872
8536
  ...proposed,
8537
+ error: proposed.status === "blocked" && !proposed.error && proposed.contentConflict && proposed.items.some((item) => item.error === "SESSION_CONTENT_CONFLICT") ? "SESSION_CONTENT_CONFLICT" : proposed.error,
7873
8538
  items: proposed.items.map((item) => {
7874
8539
  const checkpoint = checkpoints.get(item.workId);
7875
8540
  return checkpoint && !fulfilled.has(item.workId) ? {
7876
8541
  ...checkpoint,
7877
8542
  ...item,
8543
+ ...checkpoint.storedSessions !== void 0 ? { storedSessions: checkpoint.storedSessions } : {},
7878
8544
  sessionUpload: checkpoint.sessionUpload
7879
8545
  } : item;
7880
8546
  })
@@ -7890,6 +8556,7 @@ async function advanceSend(dependencies, address, signal, chain) {
7890
8556
  const send = withoutQuestions(snapshot.send);
7891
8557
  try {
7892
8558
  assertScope(snapshot.journey, snapshot.send);
8559
+ validated = snapshot;
7893
8560
  } catch (error) {
7894
8561
  if (completed) return save({
7895
8562
  ...snapshot.send,
@@ -7925,24 +8592,83 @@ async function advanceSend(dependencies, address, signal, chain) {
7925
8592
  };
7926
8593
  if (send !== snapshot.send) return save(send);
7927
8594
  let independentItems = [];
8595
+ let queuedIds = [];
7928
8596
  const preparation = createSessionPreparation(dependencies, sessionFacts);
8597
+ const sessionStepDependencies = (item) => {
8598
+ const { artifacts } = preparation.dependencies;
8599
+ if (!artifacts.sessions) return preparation.dependencies;
8600
+ const scope = {
8601
+ journey: snapshot.journey,
8602
+ send: snapshot.send,
8603
+ item
8604
+ };
8605
+ return {
8606
+ ...preparation.dependencies,
8607
+ artifacts: {
8608
+ ...artifacts,
8609
+ sessions: guardSessionUploads(artifacts.sessions, scope)
8610
+ }
8611
+ };
8612
+ };
8613
+ let queueActive = false;
8614
+ async function readAdmission(connection, currentItems, signal) {
8615
+ if (!connection.sessions) throw new ApplicationError("INVALID_STATE");
8616
+ const inventory = await connection.sessions.inventory(signal);
8617
+ const conflicts = await sessionAccessBlocked(preparation.dependencies, snapshot.journey.journeyId, currentItems, inventory, signal);
8618
+ return {
8619
+ conflicts,
8620
+ independent: conflicts.length ? [] : await sessionIndependentItems(preparation.dependencies, snapshot.journey.journeyId, currentItems, inventory, signal)
8621
+ };
8622
+ }
8623
+ let stopObservation;
7929
8624
  const cleanup = createCleanup();
7930
8625
  try {
7931
8626
  return await advanceItems({
7932
8627
  send,
8628
+ poll: chain.poll,
7933
8629
  failure: dependencies.failure,
7934
8630
  ...usesSessionSend(dependencies, send) ? {
7935
8631
  batch: () => {
7936
8632
  const limit = send.parallelCompletes ? 3 : 1;
7937
- return independentItems.length ? pickBatch(independentItems, Date.now(), limit) : pickBatch(send.items, Date.now(), limit);
8633
+ const candidates = independentItems.length ? independentItems : send.items;
8634
+ const planned = pickBatch(candidates, limit === 3 && independentItems.length ? candidates.length : limit, (works) => chain.poll(send, works));
8635
+ queueActive = limit === 3 && planned.length > limit && planned.some((item) => item.status === "uploading") && planned.every((item) => !item.busyRefusals && !item.sessionUpload?.nextAttemptAt);
8636
+ if (queueActive) queuedIds = planned.map((item) => item.workId);
8637
+ return queueActive ? planned : planned.slice(0, limit);
7938
8638
  },
7939
8639
  preflight: async (connection, preflightSignal) => {
7940
8640
  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);
8641
+ const checked = send.items.filter((item) => mayLeave({
8642
+ journey: snapshot.journey,
8643
+ send: snapshot.send,
8644
+ item
8645
+ }, "session-membership"));
8646
+ if (!checked.some((item) => item.status === "uploading" && !item.sessionAssembly && !item.receipt && !item.reservation)) return null;
8647
+ let admission;
8648
+ try {
8649
+ admission = await readAdmission(connection, checked, preflightSignal);
8650
+ } catch (error) {
8651
+ if (!(error instanceof SessionContentConflict)) throw error;
8652
+ await connection.assertCurrent(preflightSignal);
8653
+ dependencies.failure(error.code, error);
8654
+ const items = send.items.map((item) => error.affectedWorkIds.has(item.workId) && !["completed", "blocked"].includes(item.status) && !item.sessionAssembly ? {
8655
+ ...item,
8656
+ status: "blocked",
8657
+ error: error.code
8658
+ } : item);
8659
+ const next = aggregate({
8660
+ ...send,
8661
+ items,
8662
+ contentConflict: error.contentConflict
8663
+ });
8664
+ return save({
8665
+ ...next,
8666
+ error: next.status === "blocked" ? error.code : null
8667
+ });
8668
+ }
8669
+ const { conflicts, independent } = admission;
8670
+ if (!conflicts.length) {
8671
+ independentItems = independent;
7946
8672
  return null;
7947
8673
  }
7948
8674
  await connection.assertCurrent(preflightSignal);
@@ -7962,6 +8688,7 @@ async function advanceSend(dependencies, address, signal, chain) {
7962
8688
  const { accessConflict: _previous, accessConflictSessionCount: _count, ...current } = item;
7963
8689
  const itemConflicts = conflicts.filter((entry) => entry.workId === item.workId);
7964
8690
  const conflict = itemConflicts.find((entry) => entry.accountAccess !== null) ?? itemConflicts[0];
8691
+ if (item.status === "blocked" && item.error === "SESSION_CONTENT_CONFLICT") return item;
7965
8692
  if (!conflict) return {
7966
8693
  ...current,
7967
8694
  status: "blocked",
@@ -7978,35 +8705,74 @@ async function advanceSend(dependencies, address, signal, chain) {
7978
8705
  })
7979
8706
  });
7980
8707
  },
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);
8708
+ refill: async (connection, refillSignal) => {
8709
+ let checked;
8710
+ try {
8711
+ checked = await readAdmission(connection, snapshot.send.items, refillSignal);
8712
+ } catch {
8713
+ refillSignal.throwIfAborted();
8714
+ return false;
8715
+ }
8716
+ return !checked.conflicts.length && queuedIds.every((workId) => {
8717
+ return snapshot.send.items.find((entry) => entry.workId === workId)?.sessionAssembly || checked.independent.some((entry) => entry.workId === workId);
7995
8718
  });
7996
- saving = pending.catch(() => void 0);
7997
- return pending;
7998
- }).then(({ busyRefusals: _busy, ...result }) => {
8719
+ },
8720
+ step: async (context, item, connection, stepSignal) => {
8721
+ if (!stopObservation && (item.status === "ready" || item.status === "uploading")) stopObservation = dependencies.observeUpload?.(connection, () => snapshot.send, stepSignal);
8722
+ const checkpoint = (next, transfer = false) => {
8723
+ const pending = saving.then(async () => {
8724
+ const current = await active(item);
8725
+ if (current.send.revision !== snapshot.send.revision || current.journey.revision !== snapshot.journey.revision) throw new ApplicationError("REVISION_CONFLICT");
8726
+ stepSignal.throwIfAborted();
8727
+ if (transfer && next.sessionUpload) {
8728
+ snapshot = await saveTransfer(dependencies, current, next.workId, {
8729
+ sessionUpload: next.sessionUpload,
8730
+ ...next.storedSessions === void 0 ? {} : { storedSessions: next.storedSessions }
8731
+ });
8732
+ validated = snapshot;
8733
+ } else {
8734
+ const result = await saveSend(dependencies, current, {
8735
+ ...current.send,
8736
+ items: current.send.items.map((value) => value.workId === next.workId ? next : value)
8737
+ });
8738
+ if (result.status === "stale") throw new ApplicationError("REVISION_CONFLICT");
8739
+ snapshot = requireSend(result);
8740
+ }
8741
+ const saved = snapshot.send.items.find((value) => value.workId === next.workId);
8742
+ if (!saved) throw new ApplicationError("INVALID_STATE");
8743
+ checkpoints.set(next.workId, saved);
8744
+ });
8745
+ saving = pending.catch(() => void 0);
8746
+ return pending;
8747
+ };
8748
+ const step = (current) => stepSessionWork(sessionStepDependencies(current), snapshot.journey.journeyId, context, current, connection, stepSignal, (next) => checkpoint(next, true));
8749
+ let result = await step(item);
8750
+ if (queueActive && item.status === "ready" && result.status === "uploading" && mayLeave({
8751
+ journey: snapshot.journey,
8752
+ send: snapshot.send,
8753
+ item: result
8754
+ }, "session-reserve")) {
8755
+ await checkpoint(result);
8756
+ result = await step(result);
8757
+ }
8758
+ if (queueActive && !sameJson(result, checkpoints.get(item.workId) ?? item)) await checkpoint(result);
7999
8759
  fulfilled.add(item.workId);
8000
- return result;
8001
- })
8760
+ const { busyRefusals: _busy, ...finished } = result;
8761
+ return finished;
8762
+ }
8002
8763
  } : {},
8003
- connect: chain?.connect ?? dependencies.auth.connect,
8764
+ connect: chain.connect,
8765
+ scope: (item) => ({
8766
+ journey: snapshot.journey,
8767
+ send: snapshot.send,
8768
+ item
8769
+ }),
8004
8770
  profile: (connection, profileSignal) => prepareProfile(dependencies, snapshot, connection, profileSignal),
8005
8771
  context: (item) => {
8006
8772
  const binding = snapshot.journey.works.find((value) => value.id === item.workId)?.binding;
8007
8773
  return {
8008
8774
  snapshot: true,
8009
- ...chain ? { fresh: chain.fresh } : {},
8775
+ fresh: chain.fresh,
8010
8776
  active: async () => {
8011
8777
  await active(item);
8012
8778
  },
@@ -8032,6 +8798,7 @@ async function advanceSend(dependencies, address, signal, chain) {
8032
8798
  cleanup.failed(error);
8033
8799
  throw error;
8034
8800
  } finally {
8801
+ stopObservation?.();
8035
8802
  await cleanup.run(() => preparation.close());
8036
8803
  }
8037
8804
  }
@@ -8102,6 +8869,7 @@ function pathHash(item) {
8102
8869
  /** Manual upload: native Send's commands and recovery rules over one completed result's ZIPs. */
8103
8870
  function createManualUploader(dependencies) {
8104
8871
  const { storage } = dependencies;
8872
+ const poll = createAssessmentPoll();
8105
8873
  const running = /* @__PURE__ */ new Map();
8106
8874
  function save(prior, next, action) {
8107
8875
  return storage.commit({
@@ -8240,7 +9008,11 @@ function createManualUploader(dependencies) {
8240
9008
  error: null
8241
9009
  };
8242
9010
  if (connection) try {
8243
- profile = await captureProfile(connection, guard(upload), signal, dependencies.failure);
9011
+ profile = await captureProfile(guardSendRemote(connection, {
9012
+ journey: null,
9013
+ send: upload,
9014
+ item: null
9015
+ }), guard(upload), signal, dependencies.failure);
8244
9016
  } catch (error) {
8245
9017
  if (error instanceof ApplicationError && error.code === "REVISION_CONFLICT") return save(upload, upload, input);
8246
9018
  throw error;
@@ -8319,12 +9091,18 @@ function createManualUploader(dependencies) {
8319
9091
  let bindings = [];
8320
9092
  return advanceItems({
8321
9093
  send: upload,
9094
+ poll,
8322
9095
  failure: dependencies.failure,
8323
9096
  save: (next) => save(upload, next),
8324
9097
  connect: async (connectSignal) => {
8325
9098
  bindings = await storage.bindings();
8326
9099
  return dependencies.auth.connect(connectSignal);
8327
9100
  },
9101
+ scope: (item) => ({
9102
+ journey: null,
9103
+ send: upload,
9104
+ item
9105
+ }),
8328
9106
  profile: (connection, profileSignal) => captureProfile(connection, guard(upload), profileSignal, dependencies.failure),
8329
9107
  context: (item) => ({
8330
9108
  active: guard(upload, item),
@@ -9541,6 +10319,7 @@ function createJourneyFiles(storage) {
9541
10319
  return async (journeyId, workId, signal) => {
9542
10320
  const state = await storage.current(journeyId);
9543
10321
  if (!state?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
10322
+ if (state.phase === "completed" && state.outputVersion !== 1) throw new ApplicationError("SCOPE_NOT_READY");
9544
10323
  assertJourneyPrivacyReady(state, true);
9545
10324
  if (state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
9546
10325
  const work = state.works.find((item) => item.id === workId);
@@ -9575,6 +10354,7 @@ function createJourneyFiles(storage) {
9575
10354
  const sendRecoveryCodes = [
9576
10355
  "SESSION_MEMBERSHIP_CONFLICT",
9577
10356
  "SESSION_UPLOAD_EXHAUSTED",
10357
+ "WORK_SESSION_CAPACITY_EXCEEDED",
9578
10358
  "LEGACY_SEND_RECOVERY_REQUIRED",
9579
10359
  "ASSESSMENT_INCOMPLETE",
9580
10360
  "SESSION_PUBLICATION_FENCE_REQUIRED"
@@ -9630,10 +10410,14 @@ async function requestSend(dependencies, raw, hostId, source) {
9630
10410
  if (!hostId) throw new ApplicationError("INVALID_TRANSITION");
9631
10411
  const included = current.journey.works.filter(isIncludedWork);
9632
10412
  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
- };
10413
+ if (!binding) {
10414
+ const connection = await dependencies.auth.connect(new AbortController().signal);
10415
+ if (!connection) throw new ApplicationError(dependencies.auth.configured ? "ACCOUNT_SIGNED_OUT" : "ACCOUNT_NOT_CONFIGURED");
10416
+ binding = {
10417
+ subject: connection.subject,
10418
+ at: dependencies.now()
10419
+ };
10420
+ }
9637
10421
  const { subject, at } = binding;
9638
10422
  const result = await dependencies.storage.commitSend({
9639
10423
  journeyId: input.journeyId,
@@ -9695,10 +10479,11 @@ async function requestError(dependencies, send, hostId, signal) {
9695
10479
  * next phase exactly as between advances. The advance returns after three phases or once no
9696
10480
  * picked Work is mid-upload, so later Works continue from the caller's next advance.
9697
10481
  */
9698
- async function advanceWork(dependencies, address, signal, phases = 3, sessionFacts = createSessionFactsCache()) {
10482
+ async function advanceWork(dependencies, address, signal, phases = 3, sessionFacts = createSessionFactsCache(), poll = createAssessmentPoll()) {
9699
10483
  let connection;
9700
10484
  const chain = {
9701
10485
  sessionFacts,
10486
+ poll,
9702
10487
  fresh: /* @__PURE__ */ new Map(),
9703
10488
  async connect(connectSignal) {
9704
10489
  if (connection === void 0) connection = await dependencies.auth.connect(connectSignal);
@@ -9713,7 +10498,102 @@ async function advanceWork(dependencies, address, signal, phases = 3, sessionFac
9713
10498
  /** The phase saved progress and left its Work mid-upload, with nothing else to report. */
9714
10499
  function uploading(result, before) {
9715
10500
  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";
10501
+ 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";
10502
+ }
10503
+ //#endregion
10504
+ //#region apps/cli/src/account/journey/upload-observer.ts
10505
+ const intervalMs = 15e3;
10506
+ const budget = 3;
10507
+ /** A tab polls only the in-flight upload owner's read capability. No store or upload-slot acquisition. */
10508
+ function createUploadObserver() {
10509
+ let active;
10510
+ let retained = "";
10511
+ let cursor = 0;
10512
+ let next = 0;
10513
+ let reading;
10514
+ const observations = /* @__PURE__ */ new Map();
10515
+ function start(connection, current, signal) {
10516
+ const send = current();
10517
+ const scope = JSON.stringify([
10518
+ send.journeyId,
10519
+ send.id,
10520
+ send.subject
10521
+ ]);
10522
+ if (scope !== retained) {
10523
+ observations.clear();
10524
+ cursor = 0;
10525
+ next = 0;
10526
+ retained = scope;
10527
+ }
10528
+ const stop = new AbortController();
10529
+ const flight = {
10530
+ connection,
10531
+ current,
10532
+ signal: AbortSignal.any([signal, stop.signal])
10533
+ };
10534
+ active = flight;
10535
+ return () => {
10536
+ stop.abort();
10537
+ if (active === flight) active = void 0;
10538
+ };
10539
+ }
10540
+ function accepted(send) {
10541
+ return send.items.filter((item) => item.status === "assessing" && item.sessionAssembly);
10542
+ }
10543
+ async function refresh(flight) {
10544
+ const remote = flight.connection.sessions;
10545
+ if (!remote) return;
10546
+ const items = accepted(flight.current());
10547
+ if (!items.length) return;
10548
+ const startAt = cursor % items.length;
10549
+ const selected = [...items.slice(startAt), ...items.slice(0, startAt)].slice(0, budget);
10550
+ const signal = AbortSignal.any([flight.signal, AbortSignal.timeout(8e3)]);
10551
+ for (const item of selected) {
10552
+ const revision = item.sessionAssembly?.revision;
10553
+ if (revision === void 0 || signal.aborted) break;
10554
+ cursor = (cursor + 1) % items.length;
10555
+ let state = "unavailable";
10556
+ try {
10557
+ const assembly = await remote.assembly(item.workId, signal);
10558
+ if (assembly?.revision === revision) {
10559
+ if (assembly.error) state = "failed";
10560
+ else if (assembly.completedRevision !== revision || !assembly.scanId) state = "assembling";
10561
+ else state = await remote.assessment(assembly.scanId, signal);
10562
+ }
10563
+ } catch {
10564
+ state = "unavailable";
10565
+ }
10566
+ if (flight.signal.aborted || active !== flight) return;
10567
+ observations.set(item.workId, {
10568
+ workId: item.workId,
10569
+ revision,
10570
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
10571
+ state
10572
+ });
10573
+ }
10574
+ }
10575
+ async function read(address) {
10576
+ const flight = active;
10577
+ if (!flight || flight.signal.aborted) return [];
10578
+ const send = flight.current();
10579
+ if (send.journeyId !== address.journeyId || send.id !== address.sendId || send.subject !== flight.connection.subject || send.status !== "sending") return [];
10580
+ if (!reading && Date.now() >= next) {
10581
+ next = Date.now() + intervalMs;
10582
+ reading = refresh(flight).finally(() => {
10583
+ reading = void 0;
10584
+ });
10585
+ }
10586
+ await reading;
10587
+ if (active !== flight || flight.signal.aborted) return [];
10588
+ return accepted(flight.current()).flatMap((item) => {
10589
+ const value = observations.get(item.workId);
10590
+ return value && value.revision === item.sessionAssembly?.revision ? [value] : [];
10591
+ });
10592
+ }
10593
+ return {
10594
+ start,
10595
+ read
10596
+ };
9717
10597
  }
9718
10598
  //#endregion
9719
10599
  //#region apps/cli/src/account/journey/send.ts
@@ -9741,7 +10621,9 @@ function createJourneySender(dependencies, phases = 3, host = {
9741
10621
  const pendingElsewhere = (send) => !host.serving && send?.request !== void 0 && !send.confirmedAt;
9742
10622
  const progress = /* @__PURE__ */ new Map();
9743
10623
  let packagingRetry;
10624
+ const observer = createUploadObserver();
9744
10625
  const sessionFacts = createSessionFactsCache();
10626
+ const poll = createAssessmentPoll();
9745
10627
  const running = /* @__PURE__ */ new Map();
9746
10628
  const runKey = (address) => `${address.journeyId}:${address.sendId}`;
9747
10629
  const load = async (address) => {
@@ -9751,12 +10633,14 @@ function createJourneySender(dependencies, phases = 3, host = {
9751
10633
  return latest;
9752
10634
  };
9753
10635
  const sender = {
10636
+ uploadAssessments: observer.read,
9754
10637
  scopeValid(snapshot) {
9755
10638
  return !snapshot.send || (snapshot.send.request && !snapshot.send.confirmedAt ? requestMatches(snapshot.send, snapshot.journey) : scopeDigest(snapshot.journey) === snapshot.send.scopeDigest);
9756
10639
  },
9757
10640
  request: (input, source = "conversation") => requestSend(dependencies, input, hostId, source),
9758
10641
  async get(address) {
9759
- const snapshot = await dependencies.storage.loadSend(address.journeyId, address.sendId);
10642
+ const snapshot = await dependencies.storage.loadSend(address.journeyId, address.sendId, address.observedRevision);
10643
+ if (!snapshot && address.observedRevision !== void 0) return null;
9760
10644
  sessionFacts.retain(address.journeyId, snapshot?.send ?? null);
9761
10645
  if (host.serving && !address.sendId && snapshot?.send?.request && !snapshot.send.confirmedAt && snapshot.send.status !== "blocked") {
9762
10646
  const error = await requestError(dependencies, snapshot.send, hostId, new AbortController().signal);
@@ -9784,6 +10668,18 @@ function createJourneySender(dependencies, phases = 3, host = {
9784
10668
  }
9785
10669
  };
9786
10670
  },
10671
+ /** Link for the current native Send's authenticated owner, independent of Work assessment. */
10672
+ async ownedPassport(address, signal) {
10673
+ const { send } = await load(address);
10674
+ if (!send.confirmedAt || send.status !== "completed" || !send.subject || !send.items.length || !send.items.every((item) => item.sessionAssembly)) throw new ApplicationError("INVALID_TRANSITION");
10675
+ const connection = await dependencies.auth.connect(signal);
10676
+ if (!connection) throw new ApplicationError("ACCOUNT_SIGNED_OUT");
10677
+ if (connection.subject !== send.subject) throw new ApplicationError("IDENTITY_REJECTED");
10678
+ const url = await connection.ownedPassport(signal);
10679
+ await load(address);
10680
+ await connection.assertCurrent(signal);
10681
+ return { url };
10682
+ },
9787
10683
  /** The owner's current address, links and account email; null while signed out. */
9788
10684
  async ownerProfile(signal) {
9789
10685
  const connection = await dependencies.auth.connect(signal);
@@ -9814,8 +10710,12 @@ function createJourneySender(dependencies, phases = 3, host = {
9814
10710
  if (!connection) throw new ApplicationError("ACCOUNT_SIGNED_OUT");
9815
10711
  if (latest.send.subject !== connection.subject) throw new ApplicationError("IDENTITY_REJECTED");
9816
10712
  try {
9817
- const current = await connection.readProfile(signal);
9818
- await connection.saveProfile(mergeOwnerProfile(current.profile, chosen), signal);
10713
+ const remote = guardSendRemote(connection, {
10714
+ ...latest,
10715
+ item: null
10716
+ });
10717
+ const current = await remote.readProfile(signal);
10718
+ await remote.saveProfile(mergeOwnerProfile(current.profile, chosen), signal);
9819
10719
  } catch (error) {
9820
10720
  if (signal.aborted || error instanceof ApplicationError) throw error;
9821
10721
  const code = error instanceof JourneyRemoteError && error.status === 409 ? addressRefusals[error.code] : void 0;
@@ -9879,7 +10779,12 @@ function createJourneySender(dependencies, phases = 3, host = {
9879
10779
  }
9880
10780
  for (const item of inventory && recover && snapshot.send ? draft.items : []) {
9881
10781
  const current = inventory?.works.find((work) => work.logicalWorkId === item.workId);
9882
- const assembly = await remote.assembly(item.workId, signal);
10782
+ const earlier = snapshot.send?.items.find((value) => value.workId === item.workId);
10783
+ const assembly = await guardSendRemote(connection, {
10784
+ journey: snapshot.journey,
10785
+ send: snapshot.send,
10786
+ item: earlier ?? item
10787
+ }).sessions?.assembly(item.workId, signal);
9883
10788
  const selected = createSessionKeys();
9884
10789
  for await (const session of sessions(input.journeyId, item.artifact, signal)) await selected.add(session);
9885
10790
  const currentKeys = current?.sessions.map((session) => session.key) ?? [];
@@ -9955,7 +10860,10 @@ function createJourneySender(dependencies, phases = 3, host = {
9955
10860
  error: null
9956
10861
  };
9957
10862
  if (connection) try {
9958
- profile = await prepareProfile(dependencies, snapshot, connection, signal);
10863
+ profile = await prepareProfile(dependencies, snapshot, guardSendRemote(connection, {
10864
+ ...snapshot,
10865
+ item: null
10866
+ }), signal);
9959
10867
  } catch (error) {
9960
10868
  if (error instanceof ApplicationError && error.code === "REVISION_CONFLICT") return saveStart(snapshot.send);
9961
10869
  throw error;
@@ -10031,6 +10939,7 @@ function createJourneySender(dependencies, phases = 3, host = {
10031
10939
  const stop = new AbortController();
10032
10940
  const transferDependencies = {
10033
10941
  ...dependencies,
10942
+ ...host.serving ? { observeUpload: observer.start } : {},
10034
10943
  sessionTransfer: (workId, transfer) => {
10035
10944
  progress.set(`${key}:${workId}`, transfer);
10036
10945
  dependencies.sessionTransfer?.(workId, transfer);
@@ -10039,7 +10948,7 @@ function createJourneySender(dependencies, phases = 3, host = {
10039
10948
  const lifetime = AbortSignal.any([signal, stop.signal]);
10040
10949
  const result = (async () => {
10041
10950
  const currentSnapshot = await dependencies.storage.loadSend(address.journeyId);
10042
- if (!currentSnapshot?.send) return advanceWork(transferDependencies, address, lifetime, phases, sessionFacts);
10951
+ if (!currentSnapshot?.send) return advanceWork(transferDependencies, address, lifetime, phases, sessionFacts, poll);
10043
10952
  let snapshot = requireSend(currentSnapshot);
10044
10953
  if (snapshot.send.id !== address.sendId) return {
10045
10954
  ...snapshot,
@@ -10049,7 +10958,7 @@ function createJourneySender(dependencies, phases = 3, host = {
10049
10958
  ...snapshot,
10050
10959
  status: "applied"
10051
10960
  };
10052
- if (!host.serving && hostDrives(snapshot.send) && await host.focused?.(address.journeyId)) return {
10961
+ if (!host.serving && hostDrives(snapshot.journey, snapshot.send) && await host.focused?.(address.journeyId)) return {
10053
10962
  ...snapshot,
10054
10963
  status: "applied"
10055
10964
  };
@@ -10145,7 +11054,7 @@ function createJourneySender(dependencies, phases = 3, host = {
10145
11054
  confirmedOmittedWorkIds: snapshot.send.omitted.map((item) => item.workId)
10146
11055
  }, lifetime);
10147
11056
  }
10148
- return advanceWork(transferDependencies, address, lifetime, phases, sessionFacts);
11057
+ return advanceWork(transferDependencies, address, lifetime, phases, sessionFacts, poll);
10149
11058
  })().catch(async (error) => {
10150
11059
  if (!stop.signal.aborted || signal.aborted) throw error;
10151
11060
  return {
@@ -10242,6 +11151,7 @@ function createJourneySendAuth(account, requestFetch) {
10242
11151
  if (verified.identityId !== credential.identityId) throw new ApplicationError("IDENTITY_REJECTED");
10243
11152
  return createJourneyOwnerClient({
10244
11153
  apiUrl: account.configuration.passportApiUrl,
11154
+ ...account.configuration.passportWebOrigin ? { webOrigin: account.configuration.passportWebOrigin } : {},
10245
11155
  subject: verified.identityId,
10246
11156
  fetch: ownerFetch,
10247
11157
  async authorize() {
@@ -11164,8 +12074,10 @@ function createPassportConnection(options) {
11164
12074
  //#region apps/cli/src/account/index.ts
11165
12075
  /** Composes the concrete Identity client, credential file and backend check for the account. */
11166
12076
  function createAccountPorts(options) {
12077
+ const webOrigin = passportWebOrigin(options.passportApiUrl, options.passportWebOrigin);
11167
12078
  return {
11168
12079
  configuration: {
12080
+ ...webOrigin ? { passportWebOrigin: webOrigin } : {},
11169
12081
  identityUrl: options.identityUrl,
11170
12082
  clientId: options.clientId,
11171
12083
  passportApiUrl: options.passportApiUrl
@@ -12019,6 +12931,7 @@ async function compose(options, callbacks, bundles) {
12019
12931
  identityUrl: options.identityUrl,
12020
12932
  clientId: options.identityClientId,
12021
12933
  passportApiUrl: options.passportApiUrl,
12934
+ ...options.passportWebOrigin ? { passportWebOrigin: options.passportWebOrigin } : {},
12022
12935
  onFatal: fatalStore.fail
12023
12936
  });
12024
12937
  const account = createAccount(accountPorts);
@@ -12315,7 +13228,7 @@ const delivery = z.object({
12315
13228
  output: z.string().min(1).max(4096)
12316
13229
  });
12317
13230
  /** `signal` carries the process's SIGINT/SIGTERM to long procedures such as journey.prepare. */
12318
- async function invoke(runtime, command, value, signal) {
13231
+ async function invoke(runtime, command, value, signal, getAudience = "agent") {
12319
13232
  const app = runtime.application;
12320
13233
  markAgentPresence(runtime.stateDir, value);
12321
13234
  switch (command) {
@@ -12326,15 +13239,37 @@ async function invoke(runtime, command, value, signal) {
12326
13239
  case "send.start": return app.journeySend?.start(sendStartSchema.parse(value));
12327
13240
  case "send.respond": return app.journeySend?.respond(sendChoiceSchema.parse(value));
12328
13241
  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");
13242
+ case "send.resume": {
13243
+ const input = sendActionSchema.parse(value);
13244
+ const current = await runtime.store.journey?.current(input.journeyId);
13245
+ await fenceUnapprovedSendHost(runtime.stateDir, current);
13246
+ return app.journeySend?.resume(input);
13247
+ }
13248
+ case "send.advance": {
13249
+ const input = sendAddress.parse(value);
13250
+ const current = await runtime.store.journey?.current(input.journeyId);
13251
+ await fenceUnapprovedSendHost(runtime.stateDir, current);
13252
+ return app.journeySend?.advance(input);
13253
+ }
13254
+ case "journey.start": {
13255
+ const input = nativeStartSchema.parse(value);
13256
+ const current = await runtime.store.journey?.current();
13257
+ await fenceUnapprovedSendHost(runtime.stateDir, current);
13258
+ if (current && obsoleteJourney(current)) await relinquishObsoleteHost(runtime.stateDir, current.journeyId);
13259
+ return app.journey?.start(input);
13260
+ }
13261
+ case "journey.get": {
13262
+ const result = await app.journey?.get(value ?? {}, getAudience);
13263
+ await fenceUnapprovedSendHost(runtime.stateDir, result);
13264
+ if (result?.obsolete) await relinquishObsoleteHost(runtime.stateDir, result.journeyId);
13265
+ return result;
13266
+ }
12333
13267
  case "journey.groupItems": return app.journey?.groupItems(value);
12334
13268
  case "journey.inventory": return app.journey?.inventory(value);
12335
13269
  case "journey.classificationSessions": return app.journey?.classificationSessions(value);
12336
13270
  case "journey.classificationProgress": return app.journey?.classificationProgress(value);
12337
13271
  case "journey.classificationRead": return app.journey?.classificationRead(value);
13272
+ case "journey.recoverIsolatedMetadata": return app.journey?.recoverIsolatedMetadata(value, signal);
12338
13273
  case "journey.classificationSubmit": return app.journey?.classificationSubmit(value);
12339
13274
  case "journey.cardsRead": return app.journey?.cardsRead(value);
12340
13275
  case "journey.cardsSubmit": return app.journey?.cardsSubmit(value);
@@ -12561,4 +13496,4 @@ async function cancelOperation(runtime, id) {
12561
13496
  return readOperation(runtime, id);
12562
13497
  }
12563
13498
  //#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 };
13499
+ 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 };