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