@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.
- package/README.md +4 -0
- package/bin/INDEX.md +6 -4
- package/bin/tacitt.cjs +1 -0
- package/dist/{agent-setup-DdfF3zch.mjs → agent-setup-B12VbvN_.mjs} +2 -2
- package/dist/agent-setup-worker.mjs +5 -5
- package/dist/cli.mjs +28 -28
- package/dist/codex-title-worker.mjs +2 -2
- package/dist/configured-preview-worker.mjs +5 -5
- package/dist/configured-session-worker.mjs +23 -25
- package/dist/{frame-Bq2zRh-i.mjs → frame-B3He21G8.mjs} +1 -1
- package/dist/{heap-CV8Eca8V.mjs → heap-Blrat--4.mjs} +260 -23
- package/dist/inspection-worker.mjs +4 -4
- package/dist/{job-BKsifhyv.mjs → job-CBuIMTQ0.mjs} +24 -9
- package/dist/{locate-Cx52Zqxv.mjs → locate-D-ZaHpI0.mjs} +5 -0
- package/dist/{model-C1UXkHh9.mjs → model-DR9odhWP.mjs} +1 -1
- package/dist/operation-executor.mjs +3 -3
- package/dist/preview-worker.mjs +1 -1
- package/dist/privacy-validator.mjs +28 -8
- package/dist/{query-AMZerUh8.mjs → query-BYDcNKy2.mjs} +1614 -679
- package/dist/{run-CaPktPhH.mjs → run-DQ9xyKSt.mjs} +4 -4
- package/dist/session-process.mjs +20 -0
- package/dist/{src-wZx_v0Ih.mjs → src-CKQlTA2Z.mjs} +4 -4
- package/dist/{src-DlVbGB32.mjs → src-DeNJ8vOD.mjs} +465 -107
- package/dist/{src-D1iONla3.mjs → src-Do-qzuMn.mjs} +377 -130
- package/dist/store-database-worker.mjs +1 -1
- package/dist/web/assets/index-BgDdPL1c.js +172 -0
- package/dist/web/index.html +2 -1
- package/package.json +2 -1
- package/skills/INDEX.md +6 -4
- package/skills/tacitt/INDEX.md +7 -6
- package/skills/tacitt/SKILL.md +5 -5
- package/skills/tacitt/references/INDEX.md +14 -11
- package/skills/tacitt/references/b-method.md +4 -4
- package/skills/tacitt/references/bootstrap.md +17 -3
- package/skills/tacitt/references/classification.md +1 -1
- package/skills/tacitt/references/commands.md +24 -6
- package/skills/tacitt/references/journey.md +4 -4
- package/skills/tacitt/references/parallel-review.md +13 -7
- package/skills/tacitt/references/send.md +7 -3
- package/skills/tacitt/scripts/INDEX.md +40 -37
- package/skills/tacitt/scripts/answer.mjs +19 -1
- package/skills/tacitt/scripts/journey-card-consolidation.mjs +1 -1
- package/skills/tacitt/scripts/journey-classification-tasks.mjs +28 -8
- package/skills/tacitt/scripts/journey-panel-host.d.mts +2 -0
- package/skills/tacitt/scripts/journey-panel-host.mjs +25 -2
- package/skills/tacitt/scripts/journey-scan-timing.d.mts +4 -0
- package/skills/tacitt/scripts/journey-scan-timing.mjs +12 -0
- package/skills/tacitt/scripts/journey-state.mjs +11 -0
- package/skills/tacitt/scripts/journey-tasks.mjs +12 -5
- package/skills/tacitt/scripts/journey.mjs +10 -2
- package/skills/tacitt/scripts/privacy-validator.mjs +28 -7
- package/skills/tacitt/scripts/review.mjs +399 -99
- package/dist/web/assets/index-CYl7eDUu.js +0 -172
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { $ as
|
|
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-
|
|
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-
|
|
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-
|
|
5
|
-
import {
|
|
6
|
-
import { vt as isCoordinationBusy } from "./heap-
|
|
7
|
-
import { d as checkOpenSource, h as openSource } from "./run-
|
|
8
|
-
import { t as createAgentSetupPort } from "./agent-setup-
|
|
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
|
|
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
|
|
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
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
|
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
|
-
|
|
3568
|
-
|
|
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
|
|
4859
|
-
|
|
4860
|
-
|
|
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
|
|
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
|
|
5433
|
-
const batch = workIds.filter((id) => !ready(
|
|
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
|
|
5441
|
-
const
|
|
5442
|
-
|
|
5443
|
-
|
|
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
|
-
|
|
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(
|
|
5553
|
-
|
|
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
|
|
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
|
-
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
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 (
|
|
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
|
-
|
|
5775
|
-
|
|
5776
|
-
if (
|
|
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
|
-
...
|
|
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/
|
|
5864
|
-
|
|
5865
|
-
|
|
5866
|
-
|
|
5867
|
-
|
|
5868
|
-
|
|
5869
|
-
|
|
5870
|
-
|
|
5871
|
-
|
|
5872
|
-
|
|
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
|
-
|
|
5915
|
-
|
|
5916
|
-
|
|
5917
|
-
async function
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
5922
|
-
|
|
5923
|
-
|
|
5924
|
-
|
|
5925
|
-
|
|
5926
|
-
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
const
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
return
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
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
|
-
|
|
5981
|
-
|
|
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/
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
const
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
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
|
|
6352
|
-
const
|
|
6353
|
-
const legacyKey =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
6956
|
-
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
6963
|
-
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
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
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
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.
|
|
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
|
|
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 =
|
|
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,
|
|
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
|
|
7724
|
-
|
|
7725
|
-
|
|
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
|
|
8503
|
+
const sessionFacts = chain.sessionFacts;
|
|
7850
8504
|
sessionFacts.retain(address.journeyId, snapshot.send);
|
|
7851
|
-
|
|
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
|
|
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
|
|
7866
|
-
|
|
7867
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
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
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
|
|
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
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
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
|
-
|
|
8001
|
-
|
|
8760
|
+
const { busyRefusals: _busy, ...finished } = result;
|
|
8761
|
+
return finished;
|
|
8762
|
+
}
|
|
8002
8763
|
} : {},
|
|
8003
|
-
connect: chain
|
|
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
|
-
|
|
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,
|
|
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)
|
|
9634
|
-
|
|
9635
|
-
|
|
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,
|
|
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
|
|
9818
|
-
|
|
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
|
|
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,
|
|
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":
|
|
12330
|
-
|
|
12331
|
-
|
|
12332
|
-
|
|
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 {
|
|
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 };
|