@gitroll/tacitt-cli 0.6.9 → 0.6.11
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 +43 -4
- package/dist/{agent-setup-CODlN6zl.mjs → agent-setup-DdfF3zch.mjs} +2 -2
- package/dist/agent-setup-worker.mjs +5 -5
- package/dist/cli.mjs +35 -12
- package/dist/codex-title-worker.mjs +1 -1
- package/dist/configured-preview-worker.mjs +5 -5
- package/dist/configured-session-worker.mjs +9 -7
- package/dist/{frame-Dj5Zs6UW.mjs → frame-Bq2zRh-i.mjs} +1 -1
- package/dist/{heap-Dt3-d27v.mjs → heap-CV8Eca8V.mjs} +217 -13
- package/dist/inspection-worker.mjs +4 -4
- package/dist/{job-BQXssW46.mjs → job-BKsifhyv.mjs} +38 -18
- package/dist/{locate-BWkblijS.mjs → locate-Cx52Zqxv.mjs} +204 -20
- package/dist/{model-CSx2dP_P.mjs → model-C1UXkHh9.mjs} +1 -1
- package/dist/operation-executor.mjs +3 -3
- package/dist/preview-worker.mjs +1 -1
- package/dist/privacy-validator.mjs +29 -2
- package/dist/{query-CfhcS4gd.mjs → query-AMZerUh8.mjs} +712 -215
- package/dist/{run-xlOaKCk8.mjs → run-CaPktPhH.mjs} +4 -4
- package/dist/{src-DzKuzGVH.mjs → src-D1iONla3.mjs} +2215 -1613
- package/dist/{src-V9GHLZQs.mjs → src-DlVbGB32.mjs} +74 -37
- package/dist/{src-BgsEnMsy.mjs → src-wZx_v0Ih.mjs} +297 -207
- package/dist/store-database-worker.mjs +1 -1
- package/dist/web/assets/index-CYl7eDUu.js +172 -0
- package/dist/web/assets/index-DzOozqms.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/skills/tacitt/INDEX.md +6 -6
- package/skills/tacitt/SKILL.md +11 -11
- package/skills/tacitt/references/INDEX.md +3 -3
- package/skills/tacitt/references/b-method.md +2 -2
- package/skills/tacitt/references/bootstrap.md +2 -2
- package/skills/tacitt/references/classification.md +15 -5
- package/skills/tacitt/references/commands.md +37 -30
- package/skills/tacitt/references/journey.md +14 -6
- package/skills/tacitt/references/parallel-review.md +27 -33
- package/skills/tacitt/references/send.md +35 -9
- package/skills/tacitt/scripts/INDEX.md +37 -37
- package/skills/tacitt/scripts/answer.mjs +73 -15
- package/skills/tacitt/scripts/journey-classification-metadata.mjs +75 -26
- package/skills/tacitt/scripts/journey-classification-progress.mjs +1 -2
- package/skills/tacitt/scripts/journey-classification-validation.d.mts +13 -0
- package/skills/tacitt/scripts/journey-classification-validation.mjs +38 -0
- package/skills/tacitt/scripts/journey-classification.mjs +6 -1
- package/skills/tacitt/scripts/journey-consolidation.mjs +1 -0
- package/skills/tacitt/scripts/journey-scan-timing.d.mts +1 -0
- package/skills/tacitt/scripts/journey-scan-timing.mjs +19 -4
- package/skills/tacitt/scripts/journey-state.d.mts +15 -1
- package/skills/tacitt/scripts/journey-state.mjs +104 -5
- package/skills/tacitt/scripts/journey-tasks.d.mts +1 -0
- package/skills/tacitt/scripts/journey-tasks.mjs +13 -5
- package/skills/tacitt/scripts/journey.d.mts +46 -28
- package/skills/tacitt/scripts/journey.mjs +57 -8
- package/skills/tacitt/scripts/privacy-validator.mjs +178 -12
- package/skills/tacitt/scripts/review.d.mts +7 -6
- package/skills/tacitt/scripts/review.mjs +576 -147
- package/dist/web/assets/index-9UfCT_2C.js +0 -174
- package/dist/web/assets/index-CYJBq1px.css +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { A as createExecutionPort, C as saveRemoteState, D as createPackagingProgressPort, E as exportDelivery, F as loadDeliveryResults, L as inBackgroundStoreLane, M as withStateMutex, N as createDeliveryService, O as createStore, P as preflight, R as isBackgroundStatusPoll, S as readRemoteState, T as collectRemoteWorkers, _ as staticBaseProduction, a as writeJourneyOutput, b as workApproval, c as mergeHideIndexTokens, f as writeJourneyBase, g as staticBaseInputs, h as staticSessions, l as packRenderHideIndex, m as saveStaticSession, o as createHideIndexTokens, p as readStaticResult, s as hideIndexTexts, t as journeyOutputJob, u as journeyBaseJob, v as readSelectedCaptures, w as journeyDirectory, x as isRemoteImport, y as prepareSourcePlan, z as retrySharingViolation } from "./job-
|
|
3
|
-
import { $t as
|
|
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 { $ as
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
1
|
+
import { $ as removeNamedTemporarySpools, B as parallelismLimits, C as readOriginalSearchFields, F as SessionWorkerError, G as processJourneyMetadataFields, H as privateJourneyDirectory, I as sessionWorkerFailure, K as staticMaskCategory, O as captureLogicalJourneySources, P as createSharedSessionPool, Q as createTemporary, R as defaultParallelism, T as createCollectionArtifacts, U as readLogicalJourneyFinalMasks, W as readConfiguredOutputInfo, Z as decodeZstd, _ as processJourneyMetadata, a as scanReviewFields, at as restrictToOwner, c as readSessionScanRecords, ct as windowsAclCodes, et as createCleanup, g as readCapturedOriginalRows, i as buildWorkScanBatches, it as openPrivateFile, j as runConfiguredSessionJob, k as capturedJourneyScope, l as scanPreparedSession, m as openConfiguredUpload, n as verifyPreparedSessionSources, o as createWorkPieces, ot as windowsOwnerOnly, p as configuredSessionUploads, q as readWorkspacePair, r as prepareSessionScans, rt as makePrivateDirectory, s as readSessionScanFields, st as isWindowsAclFailure, t as preparedScanConfiguration, v as readStaticSessionMasks, w as readConfiguredSearchFields, y as staticSessionJobs, z as gitSeedsProvider } from "./src-DlVbGB32.mjs";
|
|
2
|
+
import { A as createExecutionPort, C as saveRemoteState, D as createPackagingProgressPort, E as exportDelivery, F as loadDeliveryResults, L as inBackgroundStoreLane, M as withStateMutex, N as createDeliveryService, O as createStore, P as preflight, R as isBackgroundStatusPoll, S as readRemoteState, T as collectRemoteWorkers, _ as staticBaseProduction, a as writeJourneyOutput, b as workApproval, c as mergeHideIndexTokens, f as writeJourneyBase, g as staticBaseInputs, h as staticSessions, l as packRenderHideIndex, m as saveStaticSession, o as createHideIndexTokens, p as readStaticResult, s as hideIndexTexts, t as journeyOutputJob, u as journeyBaseJob, v as readSelectedCaptures, w as journeyDirectory, x as isRemoteImport, y as prepareSourcePlan, z as retrySharingViolation } from "./job-BKsifhyv.mjs";
|
|
3
|
+
import { $t as profileHasChoices, D as bLiteralTerms, Gt as filterInventoryByPeriod, Kt as sessionPeriodSchema, Mt as privacyTermKey, Nn as createStaticProcessor, On as currentDetectorVersion, Qt as passportAddressSchema, Vn as entityDictionaryVersion, Vt as termTypeInfo, Xt as mergeOwnerProfile, Yn as frameRecords, Zt as ownerProfileSchema, an as safeField, br as processingSite, cn as reviewFamilies, dn as observeSource, dr as admitCustomTerm, fr as admitGitName, gr as ProcessingError, in as safeCode, nn as ApplicationError, rn as UnfinishedJourneyError, sn as familySelected, u as textDigest, ur as createEntityMatcher, xr as siteFields } from "./locate-Cx52Zqxv.mjs";
|
|
4
|
+
import { a as mainTextChunks, c as previewInWorker, i as nearestSortingRepository, l as createNativeAdapters, n as createProjectEvidenceCollector, o as mainTextFields, r as sortingRepositoryId, s as textChunks, t as createHarnesses, u as createGitNameSeedProvider } from "./src-wZx_v0Ih.mjs";
|
|
5
|
+
import { $r as sendReplaceResultSchema, An as scanGitSeedsResource, At as obsoleteJourney, B as createApplication, Bn as journeySearchResultSchema, Bt as classificationPartition, Ct as sendActionSchema, Dn as scanPreparationDigest, Dt as sendStartSchema, Et as sendRequestSchema, F as readAccountAvatarUrl, H as inspectionStartSchema, Hn as searchScopeDigest, Ht as retireScanGeneration, I as storedCredentialSchema, Ir as assertJourneyPrivacyReady, Jr as agentSourceSchema, K as inspectionQuerySchema, Lr as privacyDecisionDigest, M as deviceAuthorizationSchema, Mn as scanExclusionResourceSchema, N as deviceProfileScopeSchema, Nr as displayedScopeDigest, O as createAccount, On as scanSessionResourceSchema, P as profileUrlMatchesIdentity, Pr as requestedScope, Qr as sendReceiptSchema, St as replaceSensitiveTermsSchema, Tt as sendPrepareSchema, U as validateInspectionBatch, Ur as hasCurrentStaticReview, Ut as newJourneyWork, V as inspectionCancelSchema, Vt as classificationWorks, Wr as staticReviewDigest, Xr as journeySendSchema, Yr as agentSourcesSchema, Zr as sendArtifactSchema, _ as changeJourney, _t as manualUploadStartSchema, ai as isReadingComplete, ar as bTermMatcher, at as appendSensitiveTerms, di as packageCommandSchema, dt as validateFailureLocation, ei as sendResolutionSchema, fi as selectionBatchSchema, fr as journeyConfirmedHides, ft as manualUploadActionSchema, gr as workDecisionDigest, gt as manualUploadSchema, h as hideScopeDigest, hi as producerVersion, hr as retireWorkDescription, ht as manualUploadPrepareSchema, ii as isIncludedWork, j as accountProfileSchema, jn as verifiedScanGitSeeds, k as renew, kn as scanGitSeedsId, kt as currentJourneyAdapters, li as withReadingProgress, lr as confirmedHideKinds, lt as artifactFailureDetailSchema, m as activeJourneyTerms, mt as manualUploadChoiceSchema, ni as hasCompleteWorkDescription, nt as settingsQuerySchema, oi as isSelectedWork, pi as selectionSchema, ri as isClassifying, ti as sendSettingsSchema, tt as settingsBatchSchema, ui as commandSchema, vi as recommendParallelism, vr as workCorrectionBinding, wt as sendChoiceSchema, xt as normalizeSensitiveTerms, yr as emptyPreparationFacts, yt as emptyTermsRevision, zn as applyClassification } from "./src-D1iONla3.mjs";
|
|
6
|
+
import { vt as isCoordinationBusy } from "./heap-CV8Eca8V.mjs";
|
|
7
|
+
import { d as checkOpenSource, h as openSource } from "./run-CaPktPhH.mjs";
|
|
8
|
+
import { t as createAgentSetupPort } from "./agent-setup-DdfF3zch.mjs";
|
|
8
9
|
import { t as workerMessageSchema } from "./messages-D3m3bhva.mjs";
|
|
9
10
|
import { constants, write } from "node:fs";
|
|
10
11
|
import { z } from "zod";
|
|
@@ -92,7 +93,7 @@ function permissionDiagnostic(error) {
|
|
|
92
93
|
function startupFailure(error, platform = process.platform) {
|
|
93
94
|
const code = safeCode(error);
|
|
94
95
|
const permission = permissionDiagnostic(error);
|
|
95
|
-
const advice = code === "STATE_CLOSE_FAILED" ? " Keep the state directory intact and stop this CLI before retrying. Closing the state database could not be confirmed; state was not reset." : permission ? " Windows permissions could not be verified. Check that system Windows PowerShell can run and access the entry; retry after resolving the reported helper failure. Permissions were not established as unsafe." : code === "OBSOLETE_SCHEMA" ? " This control-plane database uses an older format. --overwrite-corrupted explicitly resets workflow progress, approvals and scan counts; source logs, completed ZIP files and private sensitive terms are retained." : code === "UNSAFE_STATE_PATH" ? platform === "win32" ? " State requires current-user ownership and a private Windows access control list. Repair permissions explicitly or use a private --state-dir." : " State requires current-user ownership, directory mode 0700 and file mode 0600. Repair permissions explicitly or use a private --state-dir." : ["CORRUPTED_STATE", "RESET_INCOMPLETE"].includes(code) ? " Inspect state first; --overwrite-corrupted explicitly resets only control-plane state." : code === "UNSUPPORTED_SCHEMA" ? " Stop this CLI, then run `npx --yes @gitroll/tacitt-cli@latest`
|
|
96
|
+
const advice = code === "STATE_CLOSE_FAILED" ? " Keep the state directory intact and stop this CLI before retrying. Closing the state database could not be confirmed; state was not reset." : permission ? " Windows permissions could not be verified. Check that system Windows PowerShell can run and access the entry; retry after resolving the reported helper failure. Permissions were not established as unsafe." : code === "OBSOLETE_SCHEMA" ? " This control-plane database uses an older format. --overwrite-corrupted explicitly resets workflow progress, approvals and scan counts; source logs, completed ZIP files and private sensitive terms are retained." : code === "UNSAFE_STATE_PATH" ? platform === "win32" ? " State requires current-user ownership and a private Windows access control list. Repair permissions explicitly or use a private --state-dir." : " State requires current-user ownership, directory mode 0700 and file mode 0600. Repair permissions explicitly or use a private --state-dir." : ["CORRUPTED_STATE", "RESET_INCOMPLETE"].includes(code) ? " Inspect state first; --overwrite-corrupted explicitly resets only control-plane state." : code === "UNSUPPORTED_SCHEMA" ? " Stop this CLI, then run the same command through `npx --yes @gitroll/tacitt-cli@latest` to use the latest release. If you use the Skill, run `npx --yes @gitroll/tacitt-cli@latest skill.install`, then start a fresh conversation and invoke $tacitt in Codex or /tacitt in Claude Code. State was not reset." : code === "STATE_LAYOUT_INVALID" ? " Keep the state directory intact for inspection. Its storage layout cannot be recovered automatically; --overwrite-corrupted does not bypass this check." : code === "DEV_REQUIRES_SOURCE_CHECKOUT" ? " --dev serves the repository UI through Vite and only works from a source checkout; run without --dev." : code === "ARTIFACT_OUTPUT_UNSAFE" ? " Use a writable private --bundle-dir or repair the configured bundle library. The saved directory was not changed." : code === "STATE_IN_USE" ? " Another tacitt process still has this state open. Stop the other tacitt process and retry; state was not reset." : error instanceof UnfinishedJourneyError ? unfinishedJourneyAdvice(error) : " Run --help for usage.";
|
|
96
97
|
return `${code}.${code === "OPERATION_FAILED" ? failureCause(error) : ""}${permission}${advice}\n`;
|
|
97
98
|
}
|
|
98
99
|
const safeJourneyId = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/u;
|
|
@@ -141,6 +142,7 @@ const businessCommands = [
|
|
|
141
142
|
"journey.findingImpact",
|
|
142
143
|
"journey.files",
|
|
143
144
|
"journey.prepare",
|
|
145
|
+
"review.reopenEmpty",
|
|
144
146
|
"review.terms",
|
|
145
147
|
"review.status",
|
|
146
148
|
"review.guidance",
|
|
@@ -342,13 +344,14 @@ function parseOptions(args, source = sourceCheckout) {
|
|
|
342
344
|
}
|
|
343
345
|
const fullHelp = `tacitt — review and package local session logs
|
|
344
346
|
|
|
345
|
-
Usage: tacitt [--
|
|
346
|
-
[--since DATE] [--until DATE]
|
|
347
|
-
tacitt state dump [--state-dir PATH] [--output PATH]
|
|
348
|
-
tacitt status [--state-dir PATH]
|
|
347
|
+
Usage: tacitt skill.install [--agent codex|claude|all] [--scope user|project] [--cli-path PATH]
|
|
349
348
|
tacitt PROCEDURE [--input FILE|-] [--async] [--timeout-ms N]
|
|
350
349
|
tacitt operation.get|operation.wait|operation.cancel ID [--timeout-ms N]
|
|
351
|
-
tacitt
|
|
350
|
+
tacitt state dump [--state-dir PATH] [--output PATH]
|
|
351
|
+
tacitt status [--state-dir PATH]
|
|
352
|
+
|
|
353
|
+
Tacitt has three surfaces: the Skill (/tacitt in Claude Code, $tacitt in Codex), the Skill
|
|
354
|
+
panel that the Skill opens, and the CLI commands below. Bare tacitt prints how to start the Skill.
|
|
352
355
|
|
|
353
356
|
Business procedures (JSON output):
|
|
354
357
|
${Array.from(businessCommands).join("\n")}
|
|
@@ -372,12 +375,11 @@ Workspace ZIPs and collection indexes use the saved bundle library directory.
|
|
|
372
375
|
Native Skill Send uploads only the confirmed primary artifacts to the configured Tacitt profile API.
|
|
373
376
|
status prints safe workspace outcomes from the latest saved packaging operation.
|
|
374
377
|
Private packaging error details stay in <state-dir>/diagnostics/ (up to 4 MiB).
|
|
375
|
-
Sign in from the
|
|
378
|
+
Sign in from the Skill panel to link this tool to your GitRoll Identity account; the CLI contacts
|
|
376
379
|
only the Identity and profile API origins below, only when you ask, and keeps the issued
|
|
377
380
|
token in <state-dir>/account/ (owner-only).
|
|
378
381
|
|
|
379
382
|
Options:
|
|
380
|
-
--no-open Print the URL without opening a browser
|
|
381
383
|
--agent CLIENT skill.install only: codex, claude or all (default: all)
|
|
382
384
|
--scope SCOPE skill.install only: user or project (default: user)
|
|
383
385
|
--cli-path PATH skill.install only: bind an absolute installed package executable
|
|
@@ -388,10 +390,7 @@ Options:
|
|
|
388
390
|
--claude-home PATH Override the Claude configuration root
|
|
389
391
|
--codex-home PATH Override the Codex configuration root
|
|
390
392
|
--overwrite-corrupted Explicitly reset unusable control-plane state
|
|
391
|
-
--parallelism N
|
|
392
|
-
--since DATE Manual: preselect only sessions last active on or after DATE
|
|
393
|
-
--until DATE Manual: preselect only sessions last active on or before DATE
|
|
394
|
-
(DATE: YYYY-MM-DD local day, inclusive, or an ISO timestamp)
|
|
393
|
+
--parallelism N Preparation and packaging worker count (default: recommended for this computer)
|
|
395
394
|
--identity-url URL Identity origin for sign-in (default: ${accountDefaults.identityUrl})
|
|
396
395
|
--identity-client-id ID Public device client ID registered for this tool
|
|
397
396
|
--profile-api-url URL Profile API origin the token is checked against
|
|
@@ -617,12 +616,13 @@ async function scanRemoteDispatches(sessions, signal) {
|
|
|
617
616
|
const pending = /* @__PURE__ */ new Map();
|
|
618
617
|
for (const part of session.parts) {
|
|
619
618
|
signal.throwIfAborted();
|
|
620
|
-
|
|
621
|
-
const file = await open(part.path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
619
|
+
const file = await openSource(part);
|
|
622
620
|
try {
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
621
|
+
if (!part.snapshot.size) {
|
|
622
|
+
if (part.representation === "jsonl.zst") throw new Error("ARTIFACT_PROCESSING_DECODE_FAILED");
|
|
623
|
+
await checkOpenSource(part, file);
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
626
|
const stream = file.createReadStream({
|
|
627
627
|
autoClose: false,
|
|
628
628
|
start: 0,
|
|
@@ -637,7 +637,7 @@ async function scanRemoteDispatches(sessions, signal) {
|
|
|
637
637
|
unreadableLines++;
|
|
638
638
|
cause ??= /* @__PURE__ */ new Error(`line ${location.line}, invalid UTF-8`);
|
|
639
639
|
};
|
|
640
|
-
const lines = frameRecords(numberedChunks(stream, location), "jsonl", 16777216, signal);
|
|
640
|
+
const lines = frameRecords(numberedChunks(part.representation === "jsonl.zst" ? decodeZstd(stream, signal) : stream, location), "jsonl", 16777216, signal);
|
|
641
641
|
try {
|
|
642
642
|
for await (const framed of skipLiveTail(lines, location, skipped)) {
|
|
643
643
|
signal.throwIfAborted();
|
|
@@ -671,11 +671,10 @@ async function scanRemoteDispatches(sessions, signal) {
|
|
|
671
671
|
end: end && end >= at ? end : at
|
|
672
672
|
}));
|
|
673
673
|
windows.push(...found);
|
|
674
|
-
pending.set(command.id, found);
|
|
674
|
+
if (found.length) pending.set(command.id, found);
|
|
675
675
|
}
|
|
676
676
|
}
|
|
677
|
-
|
|
678
|
-
if (after.mtimeNs !== before.mtimeNs || after.size !== before.size) throw new Error("REMOTE_SOURCE_CHANGED");
|
|
677
|
+
await checkOpenSource(part, file);
|
|
679
678
|
} finally {
|
|
680
679
|
stream.destroy();
|
|
681
680
|
}
|
|
@@ -743,7 +742,10 @@ function createRemotePreflight(options) {
|
|
|
743
742
|
} catch (error) {
|
|
744
743
|
signal.throwIfAborted();
|
|
745
744
|
options.reportFailure(error);
|
|
746
|
-
state.failures.push("source-scan");
|
|
745
|
+
if (!state.failures.includes("source-scan")) state.failures.push("source-scan");
|
|
746
|
+
const reason = sourceFailureReason(error);
|
|
747
|
+
const counts = state.sourceFailureCounts ??= {};
|
|
748
|
+
counts[reason] = (counts[reason] ?? 0) + 1;
|
|
747
749
|
}
|
|
748
750
|
await saveRemoteState(options.stateDir, input.journeyId, state);
|
|
749
751
|
}
|
|
@@ -780,9 +782,23 @@ function result(state) {
|
|
|
780
782
|
})),
|
|
781
783
|
copied: state.copied.length,
|
|
782
784
|
failures: state.failures,
|
|
785
|
+
...state.sourceFailureCounts ? { sourceFailureCounts: state.sourceFailureCounts } : {},
|
|
783
786
|
...state.failures.some((failure) => ["source-lines-unreadable", "source-scan"].includes(failure)) ? { warning: state.failures.includes("source-scan") ? "Some remote sources could not be scanned; remote worker discovery may be incomplete." : "Some remote dispatch lines were unreadable; remote worker discovery may be incomplete." } : {}
|
|
784
787
|
};
|
|
785
788
|
}
|
|
789
|
+
function sourceFailureReason(error) {
|
|
790
|
+
if (!(error instanceof Error)) return "other";
|
|
791
|
+
if (error instanceof SyntaxError) return "invalid-json";
|
|
792
|
+
if (error.message === "SOURCE_CHANGED") return "source-changed";
|
|
793
|
+
if (error.message === "UNSAFE_SOURCE_PATH") return "source-inaccessible";
|
|
794
|
+
if (error.message === "ARTIFACT_PROCESSING_DECODE_FAILED") return "compressed-decode";
|
|
795
|
+
const code = "code" in error ? error.code : null;
|
|
796
|
+
if (code === "PROCESSING_LIMIT") return "record-limit";
|
|
797
|
+
if (code === "PROCESSING_INVALID_RECORD") return "invalid-utf8";
|
|
798
|
+
if (code === "ENOENT" || code === "ENOTDIR") return "source-missing";
|
|
799
|
+
if (code === "EACCES" || code === "EPERM" || code === "ELOOP") return "source-inaccessible";
|
|
800
|
+
return "other";
|
|
801
|
+
}
|
|
786
802
|
//#endregion
|
|
787
803
|
//#region apps/cli/src/journey/confirmed-hides.ts
|
|
788
804
|
/** The Work's explicit exception values; an occurrence inside one stays visible (ADR 0069). */
|
|
@@ -1144,7 +1160,10 @@ function bindJourneyDestination(storage, destination, options = {}) {
|
|
|
1144
1160
|
return state;
|
|
1145
1161
|
};
|
|
1146
1162
|
const guard = async (id) => {
|
|
1147
|
-
if (
|
|
1163
|
+
if (verified.has(id)) return;
|
|
1164
|
+
const permission = await storage.preparationPermission(id);
|
|
1165
|
+
if (permission && permission.destination !== destination) throw new ApplicationError("JOURNEY_DESTINATION_CHANGED");
|
|
1166
|
+
if (permission) verified.add(id);
|
|
1148
1167
|
};
|
|
1149
1168
|
const readStatus = async (input) => {
|
|
1150
1169
|
await guard(input.journeyId);
|
|
@@ -1152,6 +1171,18 @@ function bindJourneyDestination(storage, destination, options = {}) {
|
|
|
1152
1171
|
};
|
|
1153
1172
|
return {
|
|
1154
1173
|
current,
|
|
1174
|
+
async preparationState(id) {
|
|
1175
|
+
await guard(id);
|
|
1176
|
+
return storage.preparationState(id);
|
|
1177
|
+
},
|
|
1178
|
+
async preparationPermission(id) {
|
|
1179
|
+
await guard(id);
|
|
1180
|
+
return storage.preparationPermission(id);
|
|
1181
|
+
},
|
|
1182
|
+
async updatePreparation(input) {
|
|
1183
|
+
await guard(input.journeyId);
|
|
1184
|
+
return storage.updatePreparation(input);
|
|
1185
|
+
},
|
|
1155
1186
|
readInventory: (input) => storage.readInventory({
|
|
1156
1187
|
...input,
|
|
1157
1188
|
expectedDestination: destination
|
|
@@ -1270,6 +1301,10 @@ function bindJourneyDestination(storage, destination, options = {}) {
|
|
|
1270
1301
|
await guard(input.journeyId);
|
|
1271
1302
|
return storage.submitReview(input);
|
|
1272
1303
|
},
|
|
1304
|
+
async reopenEmpty(input) {
|
|
1305
|
+
await guard(input.journeyId);
|
|
1306
|
+
return storage.reopenEmpty(input);
|
|
1307
|
+
},
|
|
1273
1308
|
async recordTerms(input) {
|
|
1274
1309
|
await guard(input.journeyId);
|
|
1275
1310
|
return storage.recordTerms(input);
|
|
@@ -1321,6 +1356,31 @@ function bindSendDestination(storage, destination) {
|
|
|
1321
1356
|
};
|
|
1322
1357
|
}
|
|
1323
1358
|
//#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
|
|
1324
1384
|
//#region apps/cli/src/journey/works/metadata.ts
|
|
1325
1385
|
/** Original classification fields remain durable; only the derived display cache is replaced. */
|
|
1326
1386
|
const classificationMetadataInput = (work) => ({
|
|
@@ -1368,7 +1428,7 @@ async function prepareClassificationMetadata(storage, state, work, processing, s
|
|
|
1368
1428
|
fields: Object.values(raw).map((text) => ({ text }))
|
|
1369
1429
|
}, signal);
|
|
1370
1430
|
if (title === void 0 || projectName === void 0 || description === void 0 || reason === void 0) throw new ApplicationError("CORRUPTED_STATE");
|
|
1371
|
-
await
|
|
1431
|
+
await changePreparation(storage, state.journeyId, "snapshot", (before) => {
|
|
1372
1432
|
const active = before.works.find((item) => item.id === work.id);
|
|
1373
1433
|
if (before.scope.status !== "confirmed" || !active || textDigest(JSON.stringify([
|
|
1374
1434
|
active.inputRevision,
|
|
@@ -1699,7 +1759,7 @@ async function isolateFailingRoots(storage, journeyId, work, error) {
|
|
|
1699
1759
|
});
|
|
1700
1760
|
if (roots.length === 0 || roots.length === work.rootSessionIds.length) return false;
|
|
1701
1761
|
for (;;) {
|
|
1702
|
-
const state = await storage.
|
|
1762
|
+
const state = await storage.preparationState(journeyId);
|
|
1703
1763
|
const current = state?.works.find((item) => item.id === work.id);
|
|
1704
1764
|
if (!state || state.phase === "paused" || state.pendingHideId || current?.inputRevision !== work.inputRevision || current.status !== "pending") return false;
|
|
1705
1765
|
if ((await storage.isolateRoots({
|
|
@@ -1801,7 +1861,7 @@ function scanFailureCode(reason) {
|
|
|
1801
1861
|
}
|
|
1802
1862
|
/** Resolve only the frozen inspection settings and captures; classification stays unchanged. */
|
|
1803
1863
|
async function prepareScanContext(storage, journeyId, work, selected, processing, signal, workers) {
|
|
1804
|
-
const current = await storage.
|
|
1864
|
+
const current = await storage.preparationState(journeyId);
|
|
1805
1865
|
if (!current?.consent || !current.snapshotId) throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
1806
1866
|
const active = current.works.find((item) => item.id === work.id);
|
|
1807
1867
|
if (current.scope.status !== "confirmed" || current.phase === "paused" || active?.access === "drop") throw new ApplicationError("OPERATION_CANCELED");
|
|
@@ -2127,7 +2187,7 @@ async function prepareScannedWork(storage, plan, work, directory, hooks, observa
|
|
|
2127
2187
|
workspaceId: session.workspaceId,
|
|
2128
2188
|
sessionId: session.id,
|
|
2129
2189
|
...failure.sourcePartId ? { sourcePartId: failure.sourcePartId } : {}
|
|
2130
|
-
});
|
|
2190
|
+
}, void 0, failure.cause ?? new ApplicationError(scanFailureCode(failure.reason)));
|
|
2131
2191
|
const root = families.owners.get(failure.sessionId);
|
|
2132
2192
|
if (root) failures.set(root.id, failure.cause === void 0 ? scanFailureCode(failure.reason) : safeCode(failure.cause));
|
|
2133
2193
|
}
|
|
@@ -2153,7 +2213,7 @@ async function prepareScannedWork(storage, plan, work, directory, hooks, observa
|
|
|
2153
2213
|
...proof,
|
|
2154
2214
|
rootSessionIds: excludedRoots
|
|
2155
2215
|
} : void 0;
|
|
2156
|
-
await
|
|
2216
|
+
await changePreparation(storage, plan.journeyId, "batch", (state) => {
|
|
2157
2217
|
const target = state.works.find((item) => item.id === work.id);
|
|
2158
2218
|
if (state.scope.status !== "confirmed" || state.phase === "paused" || target?.access === "drop") throw new ApplicationError("OPERATION_CANCELED");
|
|
2159
2219
|
if (target?.inputRevision !== work.inputRevision) throw new ApplicationError("REVISION_CONFLICT");
|
|
@@ -2212,7 +2272,7 @@ async function observePreparation(storage, input, observation, operation) {
|
|
|
2212
2272
|
//#endregion
|
|
2213
2273
|
//#region apps/cli/src/journey/prepare/work.ts
|
|
2214
2274
|
async function prepareJourneyWork(storage, plan, work, directory, processing, hooks, workers, onGeneration, onSubmitted) {
|
|
2215
|
-
let state = await storage.
|
|
2275
|
+
let state = await storage.preparationState(plan.journeyId);
|
|
2216
2276
|
if (!state?.consent || !state.snapshotId) throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
2217
2277
|
if (state.scope.status !== "confirmed" || state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
|
|
2218
2278
|
const selected = workApproval(plan.approval, work.rootSessionIds);
|
|
@@ -2270,9 +2330,9 @@ async function prepareJourneyWork(storage, plan, work, directory, processing, ho
|
|
|
2270
2330
|
context ??= await resolveContext();
|
|
2271
2331
|
return prepareScannedWork(storage, plan, work, directory, {
|
|
2272
2332
|
...hooks,
|
|
2273
|
-
onFailureLocation(location, detail) {
|
|
2333
|
+
onFailureLocation(location, detail, cause) {
|
|
2274
2334
|
observation.location(location);
|
|
2275
|
-
hooks.onFailureLocation?.(location, detail);
|
|
2335
|
+
hooks.onFailureLocation?.(location, detail, cause);
|
|
2276
2336
|
}
|
|
2277
2337
|
}, observation, context, onSubmitted);
|
|
2278
2338
|
});
|
|
@@ -2294,7 +2354,7 @@ async function prepareJourneyWork(storage, plan, work, directory, processing, ho
|
|
|
2294
2354
|
*/
|
|
2295
2355
|
async function prepareStaticWork(options) {
|
|
2296
2356
|
const { storage, journeyId, signal } = options;
|
|
2297
|
-
const state = await storage.
|
|
2357
|
+
const state = await storage.preparationState(journeyId);
|
|
2298
2358
|
const work = state?.works.find((item) => item.id === options.workId);
|
|
2299
2359
|
if (!state?.snapshotId || !work || !isSelectedWork(work) || !work.rootSessionIds.length) return;
|
|
2300
2360
|
if (![
|
|
@@ -2366,7 +2426,7 @@ async function prepareStaticWork(options) {
|
|
|
2366
2426
|
if (failure) throw failure.error;
|
|
2367
2427
|
signal.throwIfAborted();
|
|
2368
2428
|
if (resources.length !== jobs.length) throw new ApplicationError("SCOPE_NOT_READY");
|
|
2369
|
-
await
|
|
2429
|
+
await changePreparation(storage, journeyId, "batch", (current) => {
|
|
2370
2430
|
const target = current.works.find((item) => item.id === work.id);
|
|
2371
2431
|
if (current.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
|
|
2372
2432
|
if (current.snapshotId !== snapshotId || target?.inputRevision !== work.inputRevision) throw new ApplicationError("REVISION_CONFLICT");
|
|
@@ -2403,7 +2463,7 @@ function observeJourneyPause(storage, journeyId, processing, onFailure, requireC
|
|
|
2403
2463
|
const closing = new AbortController();
|
|
2404
2464
|
async function readPermission() {
|
|
2405
2465
|
for (let attempt = 1;; attempt++) try {
|
|
2406
|
-
return await storage.
|
|
2466
|
+
return await storage.preparationPermission(journeyId);
|
|
2407
2467
|
} catch (error) {
|
|
2408
2468
|
if (safeCode(error) !== "COORDINATION_BUSY" || attempt === 4) throw error;
|
|
2409
2469
|
onFailure(error);
|
|
@@ -2476,6 +2536,16 @@ const preparationFailureBindingsSchema = z.object({
|
|
|
2476
2536
|
workId: z.uuid(),
|
|
2477
2537
|
inputRevision: z.number().int().positive().max(Number.MAX_SAFE_INTEGER)
|
|
2478
2538
|
}).strict();
|
|
2539
|
+
/** Issue #59: the named fields are exactly what siteFields keeps, so no other text passes. */
|
|
2540
|
+
const siteShape = {
|
|
2541
|
+
site: z.string().optional(),
|
|
2542
|
+
harness: z.string().optional(),
|
|
2543
|
+
harnessVersion: z.string().optional()
|
|
2544
|
+
};
|
|
2545
|
+
const onlySiteFields = (value) => {
|
|
2546
|
+
const kept = siteFields(value);
|
|
2547
|
+
return kept?.site === value.site && kept?.harness === value.harness && kept?.harnessVersion === value.harnessVersion;
|
|
2548
|
+
};
|
|
2479
2549
|
const preparationSourceFailureSchema = z.object({
|
|
2480
2550
|
location: z.object({
|
|
2481
2551
|
workspaceId: z.uuid(),
|
|
@@ -2483,10 +2553,19 @@ const preparationSourceFailureSchema = z.object({
|
|
|
2483
2553
|
sourcePartId: z.uuid().optional()
|
|
2484
2554
|
}).strict(),
|
|
2485
2555
|
rootSessionId: z.uuid().nullable(),
|
|
2486
|
-
detail: artifactFailureDetailSchema.nullable()
|
|
2487
|
-
|
|
2556
|
+
detail: artifactFailureDetailSchema.nullable(),
|
|
2557
|
+
...siteShape
|
|
2558
|
+
}).strict().refine(onlySiteFields).refine(({ location, rootSessionId, detail }) => (location.sessionId === void 0 ? rootSessionId === null && location.sourcePartId === void 0 : rootSessionId !== null) && (detail === null || location.sourcePartId !== void 0));
|
|
2559
|
+
/** One count per refusal group over every reported source, unlike the bounded list. */
|
|
2560
|
+
const preparationFailureCountSchema = z.object({
|
|
2561
|
+
code: z.string().regex(/^[A-Z_]{3,80}$/u).optional(),
|
|
2562
|
+
...siteShape,
|
|
2563
|
+
count: z.number().int().positive().max(Number.MAX_SAFE_INTEGER)
|
|
2564
|
+
}).strict().refine(onlySiteFields);
|
|
2488
2565
|
const preparationFailureContextSchema = preparationFailureBindingsSchema.extend({
|
|
2489
2566
|
sourceFailures: z.array(preparationSourceFailureSchema).max(16),
|
|
2567
|
+
sourceFailureCounts: z.array(preparationFailureCountSchema).max(32),
|
|
2568
|
+
/** The list or the counts left something out; the counts cover every source otherwise. */
|
|
2490
2569
|
truncated: z.boolean()
|
|
2491
2570
|
}).refine((context) => Buffer.byteLength(JSON.stringify(context)) <= maximumPreparationContextBytes);
|
|
2492
2571
|
/** Invalid diagnostic input must never prevent the independent original-error write. */
|
|
@@ -2506,16 +2585,19 @@ function collectPreparationFailures(inventory, rootSessionIds, bindings) {
|
|
|
2506
2585
|
let context;
|
|
2507
2586
|
let families;
|
|
2508
2587
|
const roots = new Set(rootSessionIds);
|
|
2588
|
+
const counted = [];
|
|
2509
2589
|
try {
|
|
2510
2590
|
const parsed = preparationFailureBindingsSchema.safeParse(bindings);
|
|
2511
2591
|
if (parsed.success) context = {
|
|
2512
2592
|
...parsed.data,
|
|
2513
2593
|
sourceFailures: [],
|
|
2594
|
+
sourceFailureCounts: [],
|
|
2514
2595
|
truncated: false
|
|
2515
2596
|
};
|
|
2516
2597
|
} catch {}
|
|
2517
2598
|
return {
|
|
2518
|
-
|
|
2599
|
+
/** `cause` contributes only its code and the fixed site fields (siteFields), never text. */
|
|
2600
|
+
report(location, detail, cause) {
|
|
2519
2601
|
if (!context) return;
|
|
2520
2602
|
try {
|
|
2521
2603
|
const checked = validateFailureLocation(inventory, location);
|
|
@@ -2527,11 +2609,20 @@ function collectPreparationFailures(inventory, rootSessionIds, bindings) {
|
|
|
2527
2609
|
sessionId: root.id
|
|
2528
2610
|
}))) return;
|
|
2529
2611
|
if (checked.sessionId === void 0 && !inventory.sessions.some((session) => session.workspaceId === checked.workspaceId && roots.has(families?.owners.get(session.id)?.id ?? ""))) return;
|
|
2612
|
+
const named = processingSite(cause);
|
|
2613
|
+
if (!counted.some((seen) => contains(seen, checked) || contains(checked, seen))) {
|
|
2614
|
+
counted.push(checked);
|
|
2615
|
+
context = countFailure(context, {
|
|
2616
|
+
...cause === void 0 ? {} : { code: safeCode(cause) },
|
|
2617
|
+
...named
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2530
2620
|
const facts = artifactFailureDetailSchema.safeParse(detail);
|
|
2531
2621
|
const parsed = preparationSourceFailureSchema.safeParse({
|
|
2532
2622
|
location: checked,
|
|
2533
2623
|
rootSessionId: root?.id ?? null,
|
|
2534
|
-
detail: facts.success ? facts.data : null
|
|
2624
|
+
detail: facts.success ? facts.data : null,
|
|
2625
|
+
...named
|
|
2535
2626
|
});
|
|
2536
2627
|
if (!parsed.success) return;
|
|
2537
2628
|
const entry = parsed.data;
|
|
@@ -2556,13 +2647,50 @@ function collectPreparationFailures(inventory, rootSessionIds, bindings) {
|
|
|
2556
2647
|
}
|
|
2557
2648
|
};
|
|
2558
2649
|
}
|
|
2650
|
+
/** Adds one source to its (code, site, harness, version) group; a group past the bound truncates. */
|
|
2651
|
+
function countFailure(context, group) {
|
|
2652
|
+
const same = JSON.stringify({
|
|
2653
|
+
...group,
|
|
2654
|
+
count: 0
|
|
2655
|
+
});
|
|
2656
|
+
const index = context.sourceFailureCounts.findIndex((entry) => JSON.stringify({
|
|
2657
|
+
...entry,
|
|
2658
|
+
count: 0
|
|
2659
|
+
}) === same);
|
|
2660
|
+
if (index < 0 && context.sourceFailureCounts.length >= 32) return {
|
|
2661
|
+
...context,
|
|
2662
|
+
truncated: true
|
|
2663
|
+
};
|
|
2664
|
+
const next = {
|
|
2665
|
+
...context,
|
|
2666
|
+
sourceFailureCounts: index < 0 ? [...context.sourceFailureCounts, {
|
|
2667
|
+
...group,
|
|
2668
|
+
count: 1
|
|
2669
|
+
}] : context.sourceFailureCounts.map((entry, at) => at === index ? {
|
|
2670
|
+
...entry,
|
|
2671
|
+
count: entry.count + 1
|
|
2672
|
+
} : entry)
|
|
2673
|
+
};
|
|
2674
|
+
return readPreparationFailureContext(next) ? next : {
|
|
2675
|
+
...context,
|
|
2676
|
+
truncated: true
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2559
2679
|
//#endregion
|
|
2560
2680
|
//#region apps/cli/src/journey/prepare.ts
|
|
2681
|
+
const pendingSelected = (work) => work.status === "pending" && isSelectedWork(work);
|
|
2682
|
+
/** A retried Work is pending even if a previous generation produced a static receipt. */
|
|
2683
|
+
function initialScanWorks(state, scanRequested) {
|
|
2684
|
+
if (!scanRequested || state.scope.status !== "confirmed") return [];
|
|
2685
|
+
const selected = state.works.filter(isSelectedWork);
|
|
2686
|
+
if (!selected.some(pendingSelected)) return selected;
|
|
2687
|
+
return selected.filter((work) => work.status === "pending" || ["reading", "review"].includes(work.status) && !hasCurrentStaticReview(state, work));
|
|
2688
|
+
}
|
|
2561
2689
|
function createJourneyPreparation(options) {
|
|
2562
2690
|
return async (journeyId, signal) => {
|
|
2563
2691
|
const { storage } = options;
|
|
2564
2692
|
if (signal?.aborted) throw new ApplicationError("OPERATION_CANCELED");
|
|
2565
|
-
const current = await storage.
|
|
2693
|
+
const current = await storage.preparationState(journeyId);
|
|
2566
2694
|
if (!current?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
2567
2695
|
if (obsoleteJourney(current)) throw new ApplicationError("JOURNEY_OBSOLETE");
|
|
2568
2696
|
if (current.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
|
|
@@ -2585,10 +2713,11 @@ function createJourneyPreparation(options) {
|
|
|
2585
2713
|
terms: []
|
|
2586
2714
|
};
|
|
2587
2715
|
const plan = await prepareSourcePlan(storage, options.harnesses, journeyId, terms, abort.signal, current.period, options.stateDir);
|
|
2588
|
-
const initial = await storage.
|
|
2716
|
+
const initial = await storage.preparationState(journeyId);
|
|
2589
2717
|
if (!initial?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
2590
2718
|
const formed = initial.snapshotId ? [] : namedJourneyWorks(formJourneyWorks(plan.approval, options.processing.environment), options.processing, abort.signal);
|
|
2591
|
-
|
|
2719
|
+
let state;
|
|
2720
|
+
if (!initial.snapshotId) state = await changeJourney(storage, journeyId, "snapshot", (before) => ({
|
|
2592
2721
|
...before,
|
|
2593
2722
|
snapshotId: before.snapshotId ?? textDigest(JSON.stringify(plan)),
|
|
2594
2723
|
...before.snapshotId ? {} : {
|
|
@@ -2596,17 +2725,22 @@ function createJourneyPreparation(options) {
|
|
|
2596
2725
|
reviewMethod: "dictionary"
|
|
2597
2726
|
},
|
|
2598
2727
|
works: before.snapshotId ? before.works : formed,
|
|
2599
|
-
phase: before.phase
|
|
2728
|
+
phase: before.phase !== "paused" && (!before.snapshotId || before.works.some(pendingSelected)) ? "processing" : before.phase
|
|
2600
2729
|
}));
|
|
2730
|
+
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) ? {
|
|
2731
|
+
...before,
|
|
2732
|
+
phase: "processing"
|
|
2733
|
+
} : before);
|
|
2734
|
+
else state = initial;
|
|
2601
2735
|
if (!state.works.length) throw new ApplicationError("INVALID_SELECTION");
|
|
2602
2736
|
if (!scanRequested) await captureSortingScope(storage, journeyId, workApproval(plan.approval, state.works.flatMap((work) => work.rootSessionIds)), directory, abort.signal);
|
|
2603
|
-
const scanWorks =
|
|
2737
|
+
const scanWorks = initialScanWorks(state, scanRequested);
|
|
2604
2738
|
if (scanWorks.length) run = await runWorkers(options.workers);
|
|
2605
2739
|
const frozen = state;
|
|
2606
2740
|
const blockedForRetry = new Set(state.works.filter((work) => work.status === "blocked").map((work) => `${work.id}:${work.inputRevision}`));
|
|
2607
2741
|
const deferredForHide = /* @__PURE__ */ new Set();
|
|
2608
2742
|
const blockWork = async (work, error) => {
|
|
2609
|
-
const blocked = await
|
|
2743
|
+
const blocked = await changePreparation(storage, journeyId, "snapshot", (before) => {
|
|
2610
2744
|
if (before.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
|
|
2611
2745
|
if (before.phase === "sending" || before.phase === "completed") return before;
|
|
2612
2746
|
return withReadingProgress({
|
|
@@ -2645,7 +2779,7 @@ function createJourneyPreparation(options) {
|
|
|
2645
2779
|
abort.signal.throwIfAborted();
|
|
2646
2780
|
let resumed = false;
|
|
2647
2781
|
let deferred = false;
|
|
2648
|
-
const committed = await
|
|
2782
|
+
const committed = await changePreparation(storage, journeyId, "snapshot", (before) => {
|
|
2649
2783
|
resumed = false;
|
|
2650
2784
|
deferred = false;
|
|
2651
2785
|
const target = before.works.find((item) => item.id === work.id);
|
|
@@ -2654,17 +2788,13 @@ function createJourneyPreparation(options) {
|
|
|
2654
2788
|
deferred = !resumed;
|
|
2655
2789
|
return withReadingProgress({
|
|
2656
2790
|
...before,
|
|
2657
|
-
privacy: {
|
|
2658
|
-
...before.privacy,
|
|
2659
|
-
packageApproval: null
|
|
2660
|
-
},
|
|
2661
2791
|
works: before.works.map((item) => item.id === work.id ? {
|
|
2662
2792
|
...item,
|
|
2663
2793
|
status: resumed ? work.status : "pending",
|
|
2664
2794
|
blockedCodes: []
|
|
2665
2795
|
} : item)
|
|
2666
2796
|
});
|
|
2667
|
-
}, 100);
|
|
2797
|
+
}, 100, true);
|
|
2668
2798
|
if (deferred && committed.pendingHideId && committed.works.some((item) => generation(item) === generation(work) && item.status === "pending")) deferredForHide.add(generation(work));
|
|
2669
2799
|
return resumed;
|
|
2670
2800
|
};
|
|
@@ -2684,7 +2814,7 @@ function createJourneyPreparation(options) {
|
|
|
2684
2814
|
};
|
|
2685
2815
|
/** D2: the person dropped this Work while it was prepared; the other Works go on. */
|
|
2686
2816
|
const droppedMidScan = async (workId) => {
|
|
2687
|
-
const now = await storage.
|
|
2817
|
+
const now = await storage.preparationState(journeyId);
|
|
2688
2818
|
const work = now?.works.find((item) => item.id === workId);
|
|
2689
2819
|
return Boolean(now && now.phase !== "paused" && now.scope.status === "confirmed" && work && !isSelectedWork(work));
|
|
2690
2820
|
};
|
|
@@ -2693,7 +2823,7 @@ function createJourneyPreparation(options) {
|
|
|
2693
2823
|
/** True when failing roots left the Work, which is then prepared again (ADR 0188). */
|
|
2694
2824
|
const prepareWorkAttempt = async (planned, onSubmitted, releaseRead) => {
|
|
2695
2825
|
abort.signal.throwIfAborted();
|
|
2696
|
-
const latest = await storage.
|
|
2826
|
+
const latest = await storage.preparationState(journeyId) ?? frozen;
|
|
2697
2827
|
abort.signal.throwIfAborted();
|
|
2698
2828
|
if (latest.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
|
|
2699
2829
|
let work = latest.works.find((item) => item.id === planned.id);
|
|
@@ -2717,7 +2847,7 @@ function createJourneyPreparation(options) {
|
|
|
2717
2847
|
signal: abort.signal,
|
|
2718
2848
|
onTemporary: async () => {},
|
|
2719
2849
|
onPhase: async () => {},
|
|
2720
|
-
onFailureLocation: (location) => failures.report(location),
|
|
2850
|
+
onFailureLocation: (location, _detail, cause) => failures.report(location, void 0, cause),
|
|
2721
2851
|
onTiming: (timing) => options.onTiming?.(planned.id, timing)
|
|
2722
2852
|
}, run.workers, (revised) => {
|
|
2723
2853
|
work = revised;
|
|
@@ -2745,7 +2875,7 @@ function createJourneyPreparation(options) {
|
|
|
2745
2875
|
readRetries.set(generation(work), attempt + 1);
|
|
2746
2876
|
return prepareWorkOnce(planned, onSubmitted);
|
|
2747
2877
|
}
|
|
2748
|
-
const now = await storage.
|
|
2878
|
+
const now = await storage.preparationState(journeyId);
|
|
2749
2879
|
if (now?.phase === "sending" || now?.phase === "completed") return false;
|
|
2750
2880
|
if (blockedCodes(error, code).includes("PRIVACY_CHANGE_PENDING")) {
|
|
2751
2881
|
if (work) await blockWork(work, error);
|
|
@@ -2775,7 +2905,7 @@ function createJourneyPreparation(options) {
|
|
|
2775
2905
|
const staticWork = async (planned, passSubmitted) => {
|
|
2776
2906
|
await passSubmitted;
|
|
2777
2907
|
abort.signal.throwIfAborted();
|
|
2778
|
-
const latest = await storage.
|
|
2908
|
+
const latest = await storage.preparationState(journeyId);
|
|
2779
2909
|
abort.signal.throwIfAborted();
|
|
2780
2910
|
if (!latest?.consent || latest.phase === "paused" || latest.scope.status !== "confirmed") throw new ApplicationError("OPERATION_CANCELED");
|
|
2781
2911
|
const work = latest.works.find((item) => item.id === planned.id);
|
|
@@ -2803,7 +2933,7 @@ function createJourneyPreparation(options) {
|
|
|
2803
2933
|
staticRetries.set(generation(work), attempt + 1);
|
|
2804
2934
|
return staticWork(planned, passSubmitted);
|
|
2805
2935
|
}
|
|
2806
|
-
const now = await storage.
|
|
2936
|
+
const now = await storage.preparationState(journeyId);
|
|
2807
2937
|
if (now?.phase !== "sending" && now?.phase !== "completed" && (attempt >= retryLimit || blockedCodes(error, safeCode(error)).includes("PRIVACY_CHANGE_PENDING"))) await blockWork(work, error);
|
|
2808
2938
|
}
|
|
2809
2939
|
};
|
|
@@ -2881,7 +3011,7 @@ function createJourneyPreparation(options) {
|
|
|
2881
3011
|
for (;;) {
|
|
2882
3012
|
abort.signal.throwIfAborted();
|
|
2883
3013
|
const settled = workSettled.promise;
|
|
2884
|
-
const latest = await storage.
|
|
3014
|
+
const latest = await storage.preparationState(journeyId);
|
|
2885
3015
|
if (!latest || latest.phase === "paused" || latest.scope.status !== "confirmed") return [];
|
|
2886
3016
|
for (const work of latest.works) if (work.status === "blocked") blockedForRetry.add(generation(work));
|
|
2887
3017
|
const ready = latest.works.filter((work) => isSelectedWork(work) && [
|
|
@@ -2902,7 +3032,8 @@ function createJourneyPreparation(options) {
|
|
|
2902
3032
|
}
|
|
2903
3033
|
};
|
|
2904
3034
|
try {
|
|
2905
|
-
|
|
3035
|
+
const firstBatch = scanRequested && !scanWorks.length && isClassifying(state) ? await later() : scanWorks;
|
|
3036
|
+
for (let batch = firstBatch; batch.length && !fatal; batch = await later()) {
|
|
2906
3037
|
if (!run.workers && !run.pool) run = await runWorkers(options.workers);
|
|
2907
3038
|
await pass(batch);
|
|
2908
3039
|
}
|
|
@@ -2921,7 +3052,7 @@ function createJourneyPreparation(options) {
|
|
|
2921
3052
|
});
|
|
2922
3053
|
if (fatal) throw fatal.error;
|
|
2923
3054
|
abort.signal.throwIfAborted();
|
|
2924
|
-
const result = await storage.
|
|
3055
|
+
const result = await storage.preparationState(journeyId);
|
|
2925
3056
|
await stopObserving();
|
|
2926
3057
|
abort.signal.throwIfAborted();
|
|
2927
3058
|
if (!result?.consent || result.phase === "paused" || scanRequested && result.scope.status !== "confirmed") throw new ApplicationError("OPERATION_CANCELED");
|
|
@@ -3582,7 +3713,7 @@ function assertScope(journey, send) {
|
|
|
3582
3713
|
if (journey.pendingHideId) throw new ApplicationError("SCOPE_NOT_READY");
|
|
3583
3714
|
if (journey.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
|
|
3584
3715
|
if (scopeDigest(journey) !== send.scopeDigest) throw new ApplicationError("SEND_SCOPE_INVALIDATED");
|
|
3585
|
-
assertJourneyPrivacyReady(journey, true);
|
|
3716
|
+
assertJourneyPrivacyReady(journey, true, { publicChoices: send.status !== "completed" });
|
|
3586
3717
|
for (const item of send.items) {
|
|
3587
3718
|
const work = journey.works.find((value) => value.id === item.workId);
|
|
3588
3719
|
if (!work || work.status !== "ready" || !hasCompleteWorkDescription(work) || !work.accessConfirmed || work.access !== item.access || work.artifact?.revision !== work.revision || work.artifact.sha256 !== item.artifact.sha256 || work.artifact.archiveBytes !== item.artifact.byteLength || work.artifact.bundleId !== item.artifact.bundleId) throw new ApplicationError("SCOPE_NOT_READY");
|
|
@@ -3867,31 +3998,32 @@ function createJourneyRenderer(options) {
|
|
|
3867
3998
|
async function renderScope(journeyId, works, signal) {
|
|
3868
3999
|
const workers = options.workers;
|
|
3869
4000
|
const size = workers ? workers.parallelism ?? renderWorkerCount() : 0;
|
|
3870
|
-
if (!workers || works < 1 || size < 1) {
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
close: async () => void 0
|
|
3875
|
-
};
|
|
3876
|
-
}
|
|
3877
|
-
const retry = renderRetry(signal, Math.min(works, size + 1));
|
|
3878
|
-
const store = retry.store(storage);
|
|
4001
|
+
if (!workers || works < 1 || size < 1) return {
|
|
4002
|
+
render: (workId) => render(journeyId, workId, signal, { retry: renderRetry(signal, Math.max(1, Math.min(16, works))) }),
|
|
4003
|
+
close: async () => void 0
|
|
4004
|
+
};
|
|
3879
4005
|
const pool = createRenderWorkers(workers, size);
|
|
3880
|
-
const session = batchedRenderSession(
|
|
4006
|
+
const session = batchedRenderSession(storage, journeyId);
|
|
3881
4007
|
let plan;
|
|
3882
|
-
const readPlan = () => plan ??= readJourneyPlan(
|
|
4008
|
+
const readPlan = () => plan ??= readJourneyPlan(storage, journeyId).catch((error) => {
|
|
3883
4009
|
plan = void 0;
|
|
3884
4010
|
throw error;
|
|
3885
4011
|
});
|
|
3886
4012
|
return {
|
|
3887
4013
|
concurrency: size + 1,
|
|
3888
|
-
render: (workId) =>
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
4014
|
+
render: (workId) => {
|
|
4015
|
+
const retry = renderRetry(signal, Math.min(16, works, size + 1));
|
|
4016
|
+
return render(journeyId, workId, signal, {
|
|
4017
|
+
write: pool.write,
|
|
4018
|
+
session: {
|
|
4019
|
+
current: () => retry.run(() => session.current()),
|
|
4020
|
+
commit: (change) => retry.run(() => session.commit(change))
|
|
4021
|
+
},
|
|
4022
|
+
plan: () => retry.run(readPlan),
|
|
4023
|
+
writeBase: pool.writeBase,
|
|
4024
|
+
retry
|
|
4025
|
+
});
|
|
4026
|
+
},
|
|
3895
4027
|
close: pool.close
|
|
3896
4028
|
};
|
|
3897
4029
|
}
|
|
@@ -4691,7 +4823,7 @@ async function resolveChunkSessions(storage, state, works, signal) {
|
|
|
4691
4823
|
* read and applied by the store against the current state, so scan writes in between do not
|
|
4692
4824
|
* refuse it (BL-216).
|
|
4693
4825
|
*/
|
|
4694
|
-
function createClassificationProposal(storage) {
|
|
4826
|
+
function createClassificationProposal(storage, accountAccess) {
|
|
4695
4827
|
return async (input, signal) => {
|
|
4696
4828
|
signal.throwIfAborted();
|
|
4697
4829
|
const state = await storage.current(input.journeyId);
|
|
@@ -4785,9 +4917,18 @@ function createClassificationProposal(storage) {
|
|
|
4785
4917
|
};
|
|
4786
4918
|
const { works } = classificationWorks(state, prepared);
|
|
4787
4919
|
const chunkSessions = await resolveChunkSessions(storage, state, works, signal);
|
|
4920
|
+
const read = late ? await accountAccess?.({
|
|
4921
|
+
...state,
|
|
4922
|
+
works
|
|
4923
|
+
}, signal) : void 0;
|
|
4924
|
+
const sameAccount = read?.status === "read" && state.accountRead?.status === "read" && state.accountRead.subject === read.subject;
|
|
4788
4925
|
return storage.commitClassification({
|
|
4789
4926
|
...prepared,
|
|
4790
|
-
chunkSessions
|
|
4927
|
+
chunkSessions,
|
|
4928
|
+
...sameAccount ? {
|
|
4929
|
+
accountAccess: Object.fromEntries(read.access),
|
|
4930
|
+
accountSubject: read.subject
|
|
4931
|
+
} : read ? { accountRead: read.status === "read" ? "unavailable" : read.status } : {}
|
|
4791
4932
|
});
|
|
4792
4933
|
};
|
|
4793
4934
|
}
|
|
@@ -5133,11 +5274,11 @@ function createJourneyClassification(storage, _processing) {
|
|
|
5133
5274
|
//#endregion
|
|
5134
5275
|
//#region apps/cli/src/journey/works/index.ts
|
|
5135
5276
|
/** The journey local port's project catalog and project-type suggestions. */
|
|
5136
|
-
function createJourneyWorks(storage, processing, stateDir) {
|
|
5277
|
+
function createJourneyWorks(storage, processing, stateDir, accountAccess) {
|
|
5137
5278
|
return {
|
|
5138
5279
|
classificationSessions: createClassificationSessions(storage, processing, stateDir),
|
|
5139
5280
|
classificationRead: createClassificationReader(storage, stateDir),
|
|
5140
|
-
classificationSubmit: createClassificationProposal(storage),
|
|
5281
|
+
classificationSubmit: createClassificationProposal(storage, accountAccess),
|
|
5141
5282
|
catalog: createJourneyCatalog(storage, processing, stateDir),
|
|
5142
5283
|
classify: createJourneyClassification(storage, processing)
|
|
5143
5284
|
};
|
|
@@ -5231,6 +5372,46 @@ function protectedSourceText(field, hidden) {
|
|
|
5231
5372
|
}
|
|
5232
5373
|
//#endregion
|
|
5233
5374
|
//#region apps/cli/src/journey/send-package.ts
|
|
5375
|
+
/** Explicit admission refusals must never become automatic packaging retries. */
|
|
5376
|
+
const packagingRefusalCodes = [
|
|
5377
|
+
"ARTIFACT_INVALID_APPROVAL",
|
|
5378
|
+
"ARTIFACT_IDENTITY_MISMATCH",
|
|
5379
|
+
"CONFIRMATION_REQUIRED",
|
|
5380
|
+
"INCOMPLETE_REVIEW",
|
|
5381
|
+
"PRIVACY_CHANGE_PENDING",
|
|
5382
|
+
"PRIVACY_UNCERTAIN",
|
|
5383
|
+
"REVISION_CONFLICT",
|
|
5384
|
+
"SCOPE_NOT_READY",
|
|
5385
|
+
"JOURNEY_OBSOLETE",
|
|
5386
|
+
"UNKNOWN_ITEM",
|
|
5387
|
+
"UNKNOWN_WORKSPACE",
|
|
5388
|
+
"OPERATION_CANCELED"
|
|
5389
|
+
];
|
|
5390
|
+
/**
|
|
5391
|
+
* Deterministic failures a later attempt cannot clear: the refusals above, the source refusals
|
|
5392
|
+
* preparation isolates per root, size limits, a missing source and a full disk. Every other code,
|
|
5393
|
+
* including Windows sharing or antivirus locks, retries until the saved request expires.
|
|
5394
|
+
*/
|
|
5395
|
+
const finalPackagingCodes = [
|
|
5396
|
+
...packagingRefusalCodes,
|
|
5397
|
+
...rootScanRefusals,
|
|
5398
|
+
"ARTIFACT_ARCHIVE_LIMIT",
|
|
5399
|
+
"ARTIFACT_ENTRY_LIMIT",
|
|
5400
|
+
"ARTIFACT_FILESYSTEM_LIMIT",
|
|
5401
|
+
"ARTIFACT_INDEX_LIMIT",
|
|
5402
|
+
"ARTIFACT_METADATA_LIMIT",
|
|
5403
|
+
"ARTIFACT_PART_LIMIT",
|
|
5404
|
+
"ARTIFACT_SOURCE_MISSING",
|
|
5405
|
+
"ARTIFACT_DISK_FULL",
|
|
5406
|
+
"ARTIFACT_INVALID_APPROVAL",
|
|
5407
|
+
"ARTIFACT_INVALID_ARCHIVE",
|
|
5408
|
+
"ARTIFACT_INVALID_MANIFEST",
|
|
5409
|
+
"ARTIFACT_OUTPUT_UNSAFE",
|
|
5410
|
+
"ARTIFACT_PROCESSING_MISMATCH",
|
|
5411
|
+
"ARTIFACT_QUOTA_EXCEEDED",
|
|
5412
|
+
"ARTIFACT_SOURCE_UNSAFE",
|
|
5413
|
+
"ARTIFACT_UNSAFE_REMOTE"
|
|
5414
|
+
];
|
|
5234
5415
|
const packaged = (work) => work.status === "ready" && work.artifact?.revision === work.revision;
|
|
5235
5416
|
/** One host-lifetime packaging batch per advance; persisted artifacts are its progress. */
|
|
5236
5417
|
function createSendPackager(ports) {
|
|
@@ -5249,13 +5430,16 @@ function createSendPackager(ports) {
|
|
|
5249
5430
|
if (!current) throw new ApplicationError("UNKNOWN_ITEM");
|
|
5250
5431
|
}
|
|
5251
5432
|
const ready = (works, id) => works.some((work) => work.id === id && packaged(work));
|
|
5252
|
-
const batch = workIds.filter((id) => !ready(current.works, id))
|
|
5433
|
+
const batch = workIds.filter((id) => !ready(current.works, id));
|
|
5253
5434
|
if (!batch.length) return true;
|
|
5254
5435
|
signal.throwIfAborted();
|
|
5255
5436
|
const result = await ports.render({
|
|
5256
5437
|
journeyId,
|
|
5257
5438
|
workIds: batch
|
|
5258
5439
|
}, signal);
|
|
5440
|
+
const failures = result.renders?.filter((item) => item.status === "failed") ?? [];
|
|
5441
|
+
const failure = failures.find((item) => packagingRefusalCodes.includes(item.code)) ?? failures.find((item) => finalPackagingCodes.includes(item.code)) ?? failures[0];
|
|
5442
|
+
if (failure) throw new ApplicationError(failure.code);
|
|
5259
5443
|
if (batch.some((id) => !ready(result.works, id))) throw new Error("PACKAGING_FAILED");
|
|
5260
5444
|
return false;
|
|
5261
5445
|
};
|
|
@@ -5628,6 +5812,19 @@ async function classifyExit(child, startup, exited, busy) {
|
|
|
5628
5812
|
}
|
|
5629
5813
|
//#endregion
|
|
5630
5814
|
//#region apps/cli/src/send-host.ts
|
|
5815
|
+
/**
|
|
5816
|
+
* The detached focused host (ADR 0227) drives a Send only for the store's current journey, the
|
|
5817
|
+
* check its timer makes, and only while `liveHost` verifies its process identity and URL.
|
|
5818
|
+
* Anything the CLI cannot confirm returns false, so the CLI keeps stepping as before.
|
|
5819
|
+
*/
|
|
5820
|
+
async function focusedHostAlive(stateDir, journeyId, currentJourneyId) {
|
|
5821
|
+
try {
|
|
5822
|
+
if (await currentJourneyId() !== journeyId) return false;
|
|
5823
|
+
return (await liveHost(join(stateDir, "serve"), journeyId)).status === "alive";
|
|
5824
|
+
} catch {
|
|
5825
|
+
return false;
|
|
5826
|
+
}
|
|
5827
|
+
}
|
|
5631
5828
|
const recordSchema = z.object({
|
|
5632
5829
|
hostId: z.uuid(),
|
|
5633
5830
|
pid: z.number().int().positive()
|
|
@@ -6091,49 +6288,72 @@ var SessionContentConflict = class extends ApplicationError {
|
|
|
6091
6288
|
this.contentConflict = contentConflict;
|
|
6092
6289
|
}
|
|
6093
6290
|
};
|
|
6094
|
-
/**
|
|
6291
|
+
/** Collect safe conflicts across the entire pending Send before any session mutation. */
|
|
6095
6292
|
async function sessionAccessBlocked(dependencies, journeyId, items, inventory, signal) {
|
|
6096
6293
|
const sessions = dependencies.artifacts.sessionFacts ?? dependencies.artifacts.sessions;
|
|
6097
|
-
if (!sessions) return
|
|
6294
|
+
if (!sessions) return [];
|
|
6295
|
+
const conflicts = /* @__PURE__ */ new Map();
|
|
6296
|
+
const accountAccesses = /* @__PURE__ */ new Map();
|
|
6297
|
+
function observe(item, access) {
|
|
6298
|
+
const values = accountAccesses.get(item.workId) ?? /* @__PURE__ */ new Set();
|
|
6299
|
+
values.add(access);
|
|
6300
|
+
accountAccesses.set(item.workId, values);
|
|
6301
|
+
}
|
|
6302
|
+
function record(item, sessionKey, rule, account) {
|
|
6303
|
+
if (account) observe(item, account.access);
|
|
6304
|
+
const conflict = {
|
|
6305
|
+
workId: item.workId,
|
|
6306
|
+
sessionKey,
|
|
6307
|
+
requestedAccess: item.access,
|
|
6308
|
+
accountWorkId: account?.logicalWorkId ?? null,
|
|
6309
|
+
accountAccess: account?.access ?? null,
|
|
6310
|
+
rule
|
|
6311
|
+
};
|
|
6312
|
+
conflicts.set(JSON.stringify(conflict), conflict);
|
|
6313
|
+
}
|
|
6098
6314
|
const requestedAccess = /* @__PURE__ */ new Map();
|
|
6099
6315
|
const requestedDigests = /* @__PURE__ */ new Map();
|
|
6100
6316
|
const contentAccess = /* @__PURE__ */ new Map();
|
|
6101
|
-
|
|
6102
|
-
const
|
|
6103
|
-
const
|
|
6104
|
-
|
|
6105
|
-
|
|
6106
|
-
|
|
6107
|
-
|
|
6317
|
+
function claim(claims, identity, key, item, rule) {
|
|
6318
|
+
const previous = claims.get(identity) ?? [];
|
|
6319
|
+
for (const other of previous) {
|
|
6320
|
+
if (other.access === item.access) continue;
|
|
6321
|
+
record(item, key, rule);
|
|
6322
|
+
record(other, key, rule);
|
|
6323
|
+
}
|
|
6324
|
+
if (!previous.some((other) => other.workId === item.workId)) previous.push(item);
|
|
6325
|
+
claims.set(identity, previous);
|
|
6326
|
+
}
|
|
6327
|
+
const claimContent = (key, sha256, item) => claim(contentAccess, JSON.stringify([key, sha256]), key, item, "content-access");
|
|
6108
6328
|
for (const item of items) {
|
|
6109
6329
|
if (item.status === "blocked" || item.sessionAssembly) continue;
|
|
6110
6330
|
const keys = item.sessionMembership?.keys ?? (item.sessionUpload ? [item.sessionUpload.key] : []);
|
|
6111
6331
|
if (!keys.length) continue;
|
|
6112
6332
|
const destination = inventory.works.find((work) => work.logicalWorkId === item.workId);
|
|
6113
6333
|
if (destination?.access === "removed") throw new ApplicationError("WORK_REMOVED");
|
|
6334
|
+
if (destination) observe(item, destination.access);
|
|
6114
6335
|
for (const key of keys) {
|
|
6115
|
-
|
|
6116
|
-
|
|
6117
|
-
|
|
6118
|
-
const source
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
if (
|
|
6122
|
-
if (destination && destination.access !== item.access && !destination.sessions.some((entry) => entry.key === key && (item.sessionMembership || entry.sha256 === sha256))) return true;
|
|
6336
|
+
claim(requestedAccess, key, key, item, "requested-access");
|
|
6337
|
+
const sources = inventory.works.filter((work) => work.sessions.some((entry) => entry.key === key));
|
|
6338
|
+
for (const source of sources) observe(item, source.access);
|
|
6339
|
+
for (const source of sources) if (source.logicalWorkId !== item.workId && source.access !== item.access) record(item, key, "held-elsewhere", source);
|
|
6340
|
+
const sha256 = item.sessionMembership ? sources[0]?.sessions.find((session) => session.key === key)?.sha256 : item.sessionUpload?.sha256;
|
|
6341
|
+
if (sha256) claimContent(key, sha256, item);
|
|
6342
|
+
if (destination && destination.access !== item.access) record(item, key, "destination-access", destination);
|
|
6123
6343
|
}
|
|
6124
6344
|
}
|
|
6125
6345
|
for (const item of items) {
|
|
6126
6346
|
if (item.sessionAssembly || item.sessionMembership || ["completed", "blocked"].includes(item.status)) continue;
|
|
6127
6347
|
const destination = inventory.works.find((work) => work.logicalWorkId === item.workId);
|
|
6128
6348
|
if (destination?.access === "removed") throw new ApplicationError("WORK_REMOVED");
|
|
6349
|
+
if (destination) observe(item, destination.access);
|
|
6129
6350
|
for await (const session of sessions(journeyId, item.artifact, signal)) {
|
|
6130
6351
|
const key = await sessionKeyFor(session);
|
|
6131
6352
|
const { workerIdentity: _, ...original } = session;
|
|
6132
6353
|
const legacyKey = session.workerIdentity ? await sessionKeyFor(original) : key;
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
requestedAccess.set(key, item.access);
|
|
6354
|
+
claimContent(key, session.sha256, item);
|
|
6355
|
+
if (legacyKey !== key) claimContent(legacyKey, session.sha256, item);
|
|
6356
|
+
claim(requestedAccess, key, key, item, "requested-access");
|
|
6137
6357
|
const otherDigest = requestedDigests.get(key);
|
|
6138
6358
|
if (otherDigest !== void 0 && otherDigest.sha256 !== session.sha256) throw new SessionContentConflict({
|
|
6139
6359
|
workIds: [.../* @__PURE__ */ new Set([...otherDigest.workIds, item.workId])],
|
|
@@ -6143,11 +6363,18 @@ async function sessionAccessBlocked(dependencies, journeyId, items, inventory, s
|
|
|
6143
6363
|
sha256: session.sha256,
|
|
6144
6364
|
workIds: [.../* @__PURE__ */ new Set([...otherDigest?.workIds ?? [], item.workId])]
|
|
6145
6365
|
});
|
|
6146
|
-
|
|
6147
|
-
|
|
6366
|
+
for (const work of inventory.works) for (const entry of work.sessions) {
|
|
6367
|
+
if (entry.key !== key && !(entry.key === legacyKey && entry.sha256 === session.sha256)) continue;
|
|
6368
|
+
observe(item, work.access);
|
|
6369
|
+
if (work.logicalWorkId !== item.workId && work.access !== item.access) record(item, entry.key, "held-elsewhere", work);
|
|
6370
|
+
}
|
|
6371
|
+
if (destination && destination.access !== item.access) record(item, key, "destination-access", destination);
|
|
6148
6372
|
}
|
|
6149
6373
|
}
|
|
6150
|
-
return
|
|
6374
|
+
return [...conflicts.values()].map((conflict) => (accountAccesses.get(conflict.workId)?.size ?? 0) > 1 ? {
|
|
6375
|
+
...conflict,
|
|
6376
|
+
accountAccess: "mixed"
|
|
6377
|
+
} : conflict);
|
|
6151
6378
|
}
|
|
6152
6379
|
/** Keep overlapping session ownership serial through membership, including retained members. */
|
|
6153
6380
|
async function sessionIndependentItems(dependencies, journeyId, items, inventory, signal) {
|
|
@@ -6825,8 +7052,8 @@ async function put(context, item, receiptId, upload, connection, signal, guard)
|
|
|
6825
7052
|
await guard();
|
|
6826
7053
|
await context.upload(item.artifact, upload, signal);
|
|
6827
7054
|
const sent = withoutBusy(item);
|
|
6828
|
-
await guard();
|
|
6829
7055
|
try {
|
|
7056
|
+
await guard();
|
|
6830
7057
|
return observe(sent, await connection.complete(receiptId, signal), connection, signal, context.snapshot === true);
|
|
6831
7058
|
} catch (error) {
|
|
6832
7059
|
if (upload.headers !== void 0 && error instanceof JourneyRemoteError && error.code === "bundle_mismatch") return {
|
|
@@ -6956,13 +7183,14 @@ function isCode(error, code) {
|
|
|
6956
7183
|
}
|
|
6957
7184
|
/**
|
|
6958
7185
|
* A step that ended without a definite answer and is safe to repeat: a request timeout or abort
|
|
6959
|
-
* that is not the Send's own cancellation, or a connection failure from `fetch
|
|
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
|
|
6960
7188
|
* its state, so the next advance repeats the same idempotent step (ADR 0197).
|
|
6961
7189
|
*/
|
|
6962
7190
|
function isInterrupted(error) {
|
|
6963
7191
|
if (!(error instanceof Error)) return false;
|
|
6964
7192
|
if (error.name === "TimeoutError" || error.name === "AbortError") return true;
|
|
6965
|
-
return error instanceof TypeError && error.message === "fetch failed";
|
|
7193
|
+
return error instanceof TypeError && (error.message === "fetch failed" || error.message === "terminated");
|
|
6966
7194
|
}
|
|
6967
7195
|
const sessionBackoffMs = (attempt) => 1e3 * 2 ** (attempt - 1);
|
|
6968
7196
|
const zeroByteBackoffMs = (count) => [
|
|
@@ -7183,8 +7411,10 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
|
|
|
7183
7411
|
break;
|
|
7184
7412
|
}
|
|
7185
7413
|
}
|
|
7414
|
+
const ownKeys = /* @__PURE__ */ new Set();
|
|
7186
7415
|
for await (const session of sessions(journeyId, item.artifact, signal)) {
|
|
7187
7416
|
const identity = await keys.add(session);
|
|
7417
|
+
ownKeys.add(identity.key);
|
|
7188
7418
|
if (recoverLegacy && (identity.legacyKey !== pending?.key || session.sha256 !== pending?.sha256)) continue;
|
|
7189
7419
|
const key = recoverLegacy && pending ? pending.key : identity.key;
|
|
7190
7420
|
if (stored.get(key)?.sha256 === session.sha256) {
|
|
@@ -7210,13 +7440,23 @@ async function stepSessionWork(dependencies, journeyId, context, item, connectio
|
|
|
7210
7440
|
const assembly = await remote.assembly(item.workId, signal);
|
|
7211
7441
|
const current = (await remote.inventory(signal)).works.find((work) => work.logicalWorkId === item.workId);
|
|
7212
7442
|
if (current && current.access !== item.access) {
|
|
7213
|
-
|
|
7214
|
-
const [
|
|
7215
|
-
if (
|
|
7216
|
-
if (resolution.status !== "matched" || resolution.logicalWorkId !== item.workId || resolution.candidates.length !== 1 || !candidate || context.bindingId !== void 0 && candidate.bindingId !== context.bindingId) throw new ApplicationError("INVALID_STATE");
|
|
7217
|
-
const settings = await connection.preview(candidate.workId, item.access, signal);
|
|
7443
|
+
if (current.access === "removed") throw new ApplicationError("WORK_REMOVED");
|
|
7444
|
+
const [ownKey] = ownKeys;
|
|
7445
|
+
if (!ownKey) throw new ApplicationError("INVALID_STATE");
|
|
7218
7446
|
await guard();
|
|
7219
|
-
|
|
7447
|
+
return {
|
|
7448
|
+
...item,
|
|
7449
|
+
status: "blocked",
|
|
7450
|
+
error: "SESSION_MOVE_ACCESS_REQUIRED",
|
|
7451
|
+
accessConflict: {
|
|
7452
|
+
sessionKey: ownKey,
|
|
7453
|
+
requestedAccess: item.access,
|
|
7454
|
+
accountWorkId: current.logicalWorkId,
|
|
7455
|
+
accountAccess: current.access,
|
|
7456
|
+
rule: "destination-access"
|
|
7457
|
+
},
|
|
7458
|
+
accessConflictSessionCount: ownKeys.size
|
|
7459
|
+
};
|
|
7220
7460
|
}
|
|
7221
7461
|
await guard();
|
|
7222
7462
|
return {
|
|
@@ -7280,6 +7520,10 @@ function aggregate(send) {
|
|
|
7280
7520
|
function awaitingPassport(send) {
|
|
7281
7521
|
return send.status === "completed" && send.items.some((item) => !["completed", "blocked"].includes(item.status));
|
|
7282
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
|
+
}
|
|
7283
7527
|
/** Explicit resume retries recoverable items and recaptures a pending sign-in or profile. */
|
|
7284
7528
|
function resumeState(send) {
|
|
7285
7529
|
if (send.status !== "completed" && send.items.length && send.items.every((item) => item.status === "completed") && send.items.some((item) => !item.sessionAssembly)) return {
|
|
@@ -7304,18 +7548,23 @@ function resumeState(send) {
|
|
|
7304
7548
|
...send,
|
|
7305
7549
|
status: "sending",
|
|
7306
7550
|
error: null,
|
|
7307
|
-
items: send.items.map((item) =>
|
|
7308
|
-
|
|
7309
|
-
"
|
|
7310
|
-
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
|
|
7315
|
-
|
|
7316
|
-
|
|
7317
|
-
|
|
7318
|
-
|
|
7551
|
+
items: send.items.map((item) => {
|
|
7552
|
+
const { accessConflict: _previous, accessConflictSessionCount: _count, ...current } = item;
|
|
7553
|
+
return item.status === "blocked" && [
|
|
7554
|
+
"OPERATION_FAILED",
|
|
7555
|
+
"COORDINATION_BUSY",
|
|
7556
|
+
"UPLOAD_FAILED",
|
|
7557
|
+
"SEND_REMOTE_REFUSED",
|
|
7558
|
+
"SESSION_MOVE_ACCESS_REQUIRED",
|
|
7559
|
+
"SESSION_MOVE_ACCESS_HELD",
|
|
7560
|
+
"IDENTITY_UNAVAILABLE",
|
|
7561
|
+
"ACCOUNT_SIGNED_OUT"
|
|
7562
|
+
].includes(item.error ?? "") ? {
|
|
7563
|
+
...current,
|
|
7564
|
+
status: item.receipt ? item.receipt.state === "awaiting_upload" ? "uploading" : "assessing" : "ready",
|
|
7565
|
+
error: null
|
|
7566
|
+
} : item;
|
|
7567
|
+
})
|
|
7319
7568
|
});
|
|
7320
7569
|
return profileRequired && !["blocked", "completed"].includes(next.status) ? {
|
|
7321
7570
|
...next,
|
|
@@ -7356,7 +7605,7 @@ async function writeProfile(connection, chosen, guard, signal, failure, captured
|
|
|
7356
7605
|
await guard();
|
|
7357
7606
|
return captured;
|
|
7358
7607
|
} catch (error) {
|
|
7359
|
-
if (signal.aborted) throw error;
|
|
7608
|
+
if (signal.aborted || safeCode(error) === "COORDINATION_BUSY") throw error;
|
|
7360
7609
|
if (error instanceof JourneyRemoteError && error.status === 409 && addressRefusals[error.code]) return {
|
|
7361
7610
|
status: "blocked",
|
|
7362
7611
|
error: addressRefusals[error.code] ?? null
|
|
@@ -7382,7 +7631,7 @@ async function captureProfile(connection, guard, signal, failure) {
|
|
|
7382
7631
|
parallelCompletes: parallelCompletes ? true : void 0
|
|
7383
7632
|
};
|
|
7384
7633
|
} catch (error) {
|
|
7385
|
-
if (signal.aborted) throw error;
|
|
7634
|
+
if (signal.aborted || safeCode(error) === "COORDINATION_BUSY") throw error;
|
|
7386
7635
|
if (error instanceof JourneyRemoteError && error.status === 401 || error instanceof ApplicationError && error.code === "ACCOUNT_SIGNED_OUT") return {
|
|
7387
7636
|
status: "sign-in",
|
|
7388
7637
|
error: null
|
|
@@ -7503,7 +7752,14 @@ async function advanceItems(host, signal) {
|
|
|
7503
7752
|
});
|
|
7504
7753
|
host.failure("SEND_SCOPE_BUSY", cause);
|
|
7505
7754
|
error = "SEND_SCOPE_BUSY";
|
|
7506
|
-
} else if (isInterrupted(cause) || isTransientRefusal(cause)) {
|
|
7755
|
+
} else if (isInterrupted(cause) || isTransientRefusal(cause) || safeCode(cause) === "COORDINATION_BUSY") {
|
|
7756
|
+
if (safeCode(cause) === "COORDINATION_BUSY") {
|
|
7757
|
+
const current = progressed ?? item;
|
|
7758
|
+
next.set(item.workId, {
|
|
7759
|
+
...current,
|
|
7760
|
+
busyRefusals: (current.busyRefusals ?? 0) + 1
|
|
7761
|
+
});
|
|
7762
|
+
}
|
|
7507
7763
|
host.failure("SEND_INTERRUPTED", cause);
|
|
7508
7764
|
error = "SEND_INTERRUPTED";
|
|
7509
7765
|
} else {
|
|
@@ -7537,7 +7793,7 @@ async function advanceItems(host, signal) {
|
|
|
7537
7793
|
items
|
|
7538
7794
|
}));
|
|
7539
7795
|
} catch (error) {
|
|
7540
|
-
if (!signal.aborted && (isInterrupted(error) || error instanceof Error && isInterrupted(error.cause) || isCode(error, "IDENTITY_UNAVAILABLE"))) {
|
|
7796
|
+
if (!signal.aborted && (isInterrupted(error) || error instanceof Error && isInterrupted(error.cause) || isCode(error, "IDENTITY_UNAVAILABLE") || safeCode(error) === "COORDINATION_BUSY")) {
|
|
7541
7797
|
host.failure("SEND_INTERRUPTED", error);
|
|
7542
7798
|
return send.error === "SEND_INTERRUPTED" ? host.applied() : host.save({
|
|
7543
7799
|
...send,
|
|
@@ -7643,8 +7899,22 @@ async function advanceSend(dependencies, address, signal, chain) {
|
|
|
7643
7899
|
error: "SEND_SCOPE_CHANGED"
|
|
7644
7900
|
})
|
|
7645
7901
|
});
|
|
7902
|
+
const invalidated = safeCode(error) === "SEND_SCOPE_INVALIDATED";
|
|
7903
|
+
const answered = (item) => {
|
|
7904
|
+
const work = snapshot.journey.works.find((work) => work.id === item.workId);
|
|
7905
|
+
return !work || work.access !== item.access;
|
|
7906
|
+
};
|
|
7907
|
+
const answeredSessions = new Set(snapshot.send.items.flatMap((item) => item.error === "SESSION_MOVE_ACCESS_REQUIRED" && item.accessConflict && answered(item) ? [item.accessConflict.sessionKey] : []));
|
|
7646
7908
|
return blockSend({
|
|
7647
|
-
send:
|
|
7909
|
+
send: invalidated ? {
|
|
7910
|
+
...snapshot.send,
|
|
7911
|
+
items: snapshot.send.items.filter((item) => {
|
|
7912
|
+
if (item.error === "SESSION_MOVE_ACCESS_HELD") return false;
|
|
7913
|
+
if (item.error !== "SESSION_MOVE_ACCESS_REQUIRED") return true;
|
|
7914
|
+
if (item.accessConflict?.accountAccess === null && answeredSessions.has(item.accessConflict.sessionKey)) return false;
|
|
7915
|
+
return !answered(item);
|
|
7916
|
+
})
|
|
7917
|
+
} : snapshot.send,
|
|
7648
7918
|
failure: dependencies.failure,
|
|
7649
7919
|
save
|
|
7650
7920
|
}, error, signal);
|
|
@@ -7670,18 +7940,41 @@ async function advanceSend(dependencies, address, signal, chain) {
|
|
|
7670
7940
|
if (!connection.sessions) throw new ApplicationError("INVALID_STATE");
|
|
7671
7941
|
if (!send.items.some((item) => item.status === "uploading" && !item.sessionAssembly && !item.receipt && !item.reservation)) return null;
|
|
7672
7942
|
const inventory = await connection.sessions.inventory(preflightSignal);
|
|
7673
|
-
|
|
7943
|
+
const conflicts = await sessionAccessBlocked(preparation.dependencies, snapshot.journey.journeyId, send.items, inventory, preflightSignal);
|
|
7944
|
+
if (conflicts.length === 0) {
|
|
7674
7945
|
independentItems = await sessionIndependentItems(preparation.dependencies, snapshot.journey.journeyId, send.items, inventory, preflightSignal);
|
|
7675
7946
|
return null;
|
|
7676
7947
|
}
|
|
7677
7948
|
await connection.assertCurrent(preflightSignal);
|
|
7949
|
+
dependencies.failure("SESSION_MOVE_ACCESS_REQUIRED", {
|
|
7950
|
+
code: "SESSION_MOVE_ACCESS_REQUIRED",
|
|
7951
|
+
message: JSON.stringify({
|
|
7952
|
+
conflicts: conflicts.length,
|
|
7953
|
+
works: new Set(conflicts.map((conflict) => conflict.workId)).size,
|
|
7954
|
+
rules: [...new Set(conflicts.map((conflict) => conflict.rule))].sort()
|
|
7955
|
+
})
|
|
7956
|
+
});
|
|
7678
7957
|
return save({
|
|
7679
7958
|
...send,
|
|
7680
7959
|
status: "blocked",
|
|
7681
|
-
items: send.items.map((item) =>
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
7960
|
+
items: send.items.map((item) => {
|
|
7961
|
+
if (item.sessionAssembly || item.status === "completed") return item;
|
|
7962
|
+
const { accessConflict: _previous, accessConflictSessionCount: _count, ...current } = item;
|
|
7963
|
+
const itemConflicts = conflicts.filter((entry) => entry.workId === item.workId);
|
|
7964
|
+
const conflict = itemConflicts.find((entry) => entry.accountAccess !== null) ?? itemConflicts[0];
|
|
7965
|
+
if (!conflict) return {
|
|
7966
|
+
...current,
|
|
7967
|
+
status: "blocked",
|
|
7968
|
+
error: "SESSION_MOVE_ACCESS_HELD"
|
|
7969
|
+
};
|
|
7970
|
+
const { workId: _, ...accessConflict } = conflict;
|
|
7971
|
+
return {
|
|
7972
|
+
...current,
|
|
7973
|
+
status: "blocked",
|
|
7974
|
+
error: "SESSION_MOVE_ACCESS_REQUIRED",
|
|
7975
|
+
accessConflict,
|
|
7976
|
+
accessConflictSessionCount: new Set(itemConflicts.map((entry) => entry.sessionKey)).size
|
|
7977
|
+
};
|
|
7685
7978
|
})
|
|
7686
7979
|
});
|
|
7687
7980
|
},
|
|
@@ -7702,7 +7995,7 @@ async function advanceSend(dependencies, address, signal, chain) {
|
|
|
7702
7995
|
});
|
|
7703
7996
|
saving = pending.catch(() => void 0);
|
|
7704
7997
|
return pending;
|
|
7705
|
-
}).then((result) => {
|
|
7998
|
+
}).then(({ busyRefusals: _busy, ...result }) => {
|
|
7706
7999
|
fulfilled.add(item.workId);
|
|
7707
8000
|
return result;
|
|
7708
8001
|
})
|
|
@@ -8023,20 +8316,15 @@ function createManualUploader(dependencies) {
|
|
|
8023
8316
|
upload,
|
|
8024
8317
|
status: "applied"
|
|
8025
8318
|
};
|
|
8026
|
-
|
|
8319
|
+
let bindings = [];
|
|
8320
|
+
return advanceItems({
|
|
8027
8321
|
send: upload,
|
|
8028
8322
|
failure: dependencies.failure,
|
|
8029
|
-
save: (next) => save(upload, next)
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
} catch (error) {
|
|
8035
|
-
return blockSend(host, error, signal);
|
|
8036
|
-
}
|
|
8037
|
-
return advanceItems({
|
|
8038
|
-
...host,
|
|
8039
|
-
connect: dependencies.auth.connect,
|
|
8323
|
+
save: (next) => save(upload, next),
|
|
8324
|
+
connect: async (connectSignal) => {
|
|
8325
|
+
bindings = await storage.bindings();
|
|
8326
|
+
return dependencies.auth.connect(connectSignal);
|
|
8327
|
+
},
|
|
8040
8328
|
profile: (connection, profileSignal) => captureProfile(connection, guard(upload), profileSignal, dependencies.failure),
|
|
8041
8329
|
context: (item) => ({
|
|
8042
8330
|
active: guard(upload, item),
|
|
@@ -9330,7 +9618,7 @@ async function requestSend(dependencies, raw, hostId, source) {
|
|
|
9330
9618
|
...current,
|
|
9331
9619
|
status: "stale"
|
|
9332
9620
|
};
|
|
9333
|
-
assertJourneyPrivacyReady(current.journey);
|
|
9621
|
+
assertJourneyPrivacyReady(current.journey, false, { publicChoices: true });
|
|
9334
9622
|
if (current.journey.pendingHideId || current.journey.phase === "paused") throw new ApplicationError("SCOPE_NOT_READY");
|
|
9335
9623
|
if (current.send && ([
|
|
9336
9624
|
"awaiting-packaging",
|
|
@@ -9452,6 +9740,7 @@ function createJourneySender(dependencies, phases = 3, host = {
|
|
|
9452
9740
|
/** The serving host owns a pending request; another process leaves it unchanged. */
|
|
9453
9741
|
const pendingElsewhere = (send) => !host.serving && send?.request !== void 0 && !send.confirmedAt;
|
|
9454
9742
|
const progress = /* @__PURE__ */ new Map();
|
|
9743
|
+
let packagingRetry;
|
|
9455
9744
|
const sessionFacts = createSessionFactsCache();
|
|
9456
9745
|
const running = /* @__PURE__ */ new Map();
|
|
9457
9746
|
const runKey = (address) => `${address.journeyId}:${address.sendId}`;
|
|
@@ -9551,27 +9840,51 @@ function createJourneySender(dependencies, phases = 3, host = {
|
|
|
9551
9840
|
const pendingError = pending ? await requestError(dependencies, pending, hostId, signal) : null;
|
|
9552
9841
|
if (pending && (pendingError || !requestMatches(pending, snapshot.journey))) return block(snapshot, pending, pendingError ?? "REVISION_CONFLICT");
|
|
9553
9842
|
const draft = await draftSend(snapshot.journey, dependencies, signal);
|
|
9843
|
+
let observed = false;
|
|
9554
9844
|
const recover = snapshot.send && [
|
|
9555
9845
|
"blocked",
|
|
9556
9846
|
"paused",
|
|
9557
9847
|
"completed"
|
|
9558
9848
|
].includes(snapshot.send.status) && snapshot.send.items.some((item) => sendRecoveryCodes.includes(item.error ?? ""));
|
|
9559
|
-
if (recover
|
|
9849
|
+
if (recover || dependencies.artifacts.sessions) {
|
|
9560
9850
|
const connection = await dependencies.auth.connect(signal);
|
|
9561
|
-
if (
|
|
9562
|
-
|
|
9851
|
+
if (connection && draft.subject !== connection.subject) return {
|
|
9852
|
+
...snapshot,
|
|
9853
|
+
status: "stale"
|
|
9854
|
+
};
|
|
9855
|
+
if (recover && !connection) throw new ApplicationError("ACCOUNT_SIGNED_OUT");
|
|
9856
|
+
if (recover && snapshot.send?.subject && snapshot.send.subject !== connection?.subject) throw new ApplicationError("IDENTITY_REJECTED");
|
|
9563
9857
|
const sessions = dependencies.artifacts.sessions;
|
|
9564
|
-
const remote = connection
|
|
9565
|
-
if (sessions && remote) {
|
|
9566
|
-
const inventory = await remote.inventory(signal)
|
|
9567
|
-
|
|
9568
|
-
|
|
9858
|
+
const remote = connection?.sessions;
|
|
9859
|
+
if (sessions && remote && connection) {
|
|
9860
|
+
const inventory = await remote.inventory(signal).catch((error) => {
|
|
9861
|
+
if (signal.aborted || recover) throw error;
|
|
9862
|
+
dependencies.failure("SEND_INTERRUPTED", error);
|
|
9863
|
+
return null;
|
|
9864
|
+
});
|
|
9865
|
+
observed = inventory !== null;
|
|
9866
|
+
try {
|
|
9867
|
+
const conflicts = inventory ? await sessionAccessBlocked(dependencies, input.journeyId, draft.items, inventory, signal) : [];
|
|
9868
|
+
for (const item of draft.items) {
|
|
9869
|
+
const itemConflicts = conflicts.filter((entry) => entry.workId === item.workId);
|
|
9870
|
+
const conflict = itemConflicts.find((entry) => entry.accountAccess !== null) ?? itemConflicts[0];
|
|
9871
|
+
if (conflict) {
|
|
9872
|
+
const { workId: _, ...accessConflict } = conflict;
|
|
9873
|
+
item.accessConflict = accessConflict;
|
|
9874
|
+
item.accessConflictSessionCount = new Set(itemConflicts.map((entry) => entry.sessionKey)).size;
|
|
9875
|
+
}
|
|
9876
|
+
}
|
|
9877
|
+
} catch (error) {
|
|
9878
|
+
if (!(error instanceof SessionContentConflict) && !(error instanceof ApplicationError && error.code === "WORK_REMOVED")) throw error;
|
|
9879
|
+
}
|
|
9880
|
+
for (const item of inventory && recover && snapshot.send ? draft.items : []) {
|
|
9881
|
+
const current = inventory?.works.find((work) => work.logicalWorkId === item.workId);
|
|
9569
9882
|
const assembly = await remote.assembly(item.workId, signal);
|
|
9570
9883
|
const selected = createSessionKeys();
|
|
9571
9884
|
for await (const session of sessions(input.journeyId, item.artifact, signal)) await selected.add(session);
|
|
9572
9885
|
const currentKeys = current?.sessions.map((session) => session.key) ?? [];
|
|
9573
9886
|
item.recoveryReview = {
|
|
9574
|
-
previousSendId: snapshot.send.id,
|
|
9887
|
+
previousSendId: snapshot.send?.id ?? draft.id,
|
|
9575
9888
|
revision: assembly?.revision ?? 0,
|
|
9576
9889
|
currentAccess: current?.access ?? null,
|
|
9577
9890
|
currentKeys,
|
|
@@ -9581,6 +9894,13 @@ function createJourneySender(dependencies, phases = 3, host = {
|
|
|
9581
9894
|
await connection.assertCurrent(signal);
|
|
9582
9895
|
}
|
|
9583
9896
|
}
|
|
9897
|
+
for (const item of observed ? [] : draft.items) {
|
|
9898
|
+
const previous = snapshot.send?.items.find((entry) => entry.workId === item.workId && entry.access === item.access);
|
|
9899
|
+
if (previous?.accessConflict && !item.accessConflict) {
|
|
9900
|
+
item.accessConflict = previous.accessConflict;
|
|
9901
|
+
item.accessConflictSessionCount = previous.accessConflictSessionCount;
|
|
9902
|
+
}
|
|
9903
|
+
}
|
|
9584
9904
|
const canReuseExistingSend = !(snapshot.send?.confirmedAt && ["blocked", "paused"].includes(snapshot.send.status)) && !recover && snapshot.send && scopeDigest(snapshot.journey) === snapshot.send.scopeDigest && snapshot.send.subject === draft.subject;
|
|
9585
9905
|
const next = pending ? {
|
|
9586
9906
|
...draft,
|
|
@@ -9589,6 +9909,7 @@ function createJourneySender(dependencies, phases = 3, host = {
|
|
|
9589
9909
|
request: pending.request
|
|
9590
9910
|
} : canReuseExistingSend && snapshot.send ? {
|
|
9591
9911
|
...snapshot.send,
|
|
9912
|
+
...snapshot.send.confirmedAt === null ? { items: draft.items } : {},
|
|
9592
9913
|
revision: snapshot.send.revision + 1
|
|
9593
9914
|
} : draft;
|
|
9594
9915
|
const result = await dependencies.storage.commitSend({
|
|
@@ -9619,7 +9940,7 @@ function createJourneySender(dependencies, phases = 3, host = {
|
|
|
9619
9940
|
const authorizationError = await requestError(dependencies, snapshot.send, hostId, signal);
|
|
9620
9941
|
if (snapshot.send.request && !snapshot.send.confirmedAt && (authorizationError || !requestMatches(snapshot.send, snapshot.journey))) return block(snapshot, snapshot.send, authorizationError ?? "REVISION_CONFLICT");
|
|
9621
9942
|
assertScope(snapshot.journey, snapshot.send);
|
|
9622
|
-
if (!sameIds$1(input.confirmedWorkIds, snapshot.send.items.map((item) => item.workId)) || !sameIds$1(input.confirmedOmittedWorkIds, snapshot.send.omitted.map((item) => item.workId)) || input.scopeDigest !== snapshot.send.scopeDigest || snapshot.send.items.length === 0) throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
9943
|
+
if (!sameIds$1(input.confirmedWorkIds, snapshot.send.items.map((item) => item.workId)) || !sameIds$1(input.confirmedOmittedWorkIds, snapshot.send.omitted.map((item) => item.workId)) || input.scopeDigest !== snapshot.send.scopeDigest || snapshot.send.items.length === 0 || snapshot.send.items.some((item) => item.accessConflict !== void 0)) throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
9623
9944
|
if (snapshot.send.confirmedAt !== null) return saveStart(snapshot.send);
|
|
9624
9945
|
const connection = await dependencies.auth.connect(signal);
|
|
9625
9946
|
if (snapshot.send.request && (connection?.subject ?? null) !== snapshot.send.request.subject) return saveSend(dependencies, snapshot, {
|
|
@@ -9728,6 +10049,10 @@ function createJourneySender(dependencies, phases = 3, host = {
|
|
|
9728
10049
|
...snapshot,
|
|
9729
10050
|
status: "applied"
|
|
9730
10051
|
};
|
|
10052
|
+
if (!host.serving && hostDrives(snapshot.send) && await host.focused?.(address.journeyId)) return {
|
|
10053
|
+
...snapshot,
|
|
10054
|
+
status: "applied"
|
|
10055
|
+
};
|
|
9731
10056
|
const authorizationError = await requestError(dependencies, snapshot.send, hostId, lifetime);
|
|
9732
10057
|
if (authorizationError) return block(snapshot, snapshot.send, authorizationError);
|
|
9733
10058
|
if (snapshot.send.request && !snapshot.send.confirmedAt && ["awaiting-packaging", "prepared"].includes(snapshot.send.status)) {
|
|
@@ -9742,10 +10067,52 @@ function createJourneySender(dependencies, phases = 3, host = {
|
|
|
9742
10067
|
error: "REVISION_CONFLICT"
|
|
9743
10068
|
});
|
|
9744
10069
|
if (snapshot.send.status === "awaiting-packaging") {
|
|
9745
|
-
if (
|
|
9746
|
-
|
|
10070
|
+
if (packagingRetry?.key !== key) packagingRetry = void 0;
|
|
10071
|
+
if (packagingRetry && Date.parse(dependencies.now()) < packagingRetry.after) return {
|
|
10072
|
+
...snapshot,
|
|
9747
10073
|
status: "applied"
|
|
9748
10074
|
};
|
|
10075
|
+
let packaged;
|
|
10076
|
+
try {
|
|
10077
|
+
packaged = await dependencies.package?.(address.journeyId, snapshot.send.request.included.map((item) => item.workId), lifetime, snapshot.send.request.source);
|
|
10078
|
+
} catch (cause) {
|
|
10079
|
+
lifetime.throwIfAborted();
|
|
10080
|
+
const code = cause instanceof ApplicationError ? cause.code : safeCode(cause);
|
|
10081
|
+
if (packagingRefusalCodes.includes(code)) throw cause;
|
|
10082
|
+
dependencies.failure(code, cause);
|
|
10083
|
+
snapshot = await load(address);
|
|
10084
|
+
if (snapshot.send.status !== "awaiting-packaging") return {
|
|
10085
|
+
...snapshot,
|
|
10086
|
+
status: "stale"
|
|
10087
|
+
};
|
|
10088
|
+
const error = await requestError(dependencies, snapshot.send, hostId, lifetime);
|
|
10089
|
+
if (error || !requestMatches(snapshot.send, snapshot.journey)) return block(snapshot, snapshot.send, error ?? "REVISION_CONFLICT");
|
|
10090
|
+
if (finalPackagingCodes.includes(code)) {
|
|
10091
|
+
packagingRetry = void 0;
|
|
10092
|
+
return block(snapshot, snapshot.send, code);
|
|
10093
|
+
}
|
|
10094
|
+
const failures = Math.min((packagingRetry?.failures ?? 0) + 1, 5);
|
|
10095
|
+
packagingRetry = {
|
|
10096
|
+
key,
|
|
10097
|
+
failures,
|
|
10098
|
+
after: Date.parse(dependencies.now()) + Math.min(2e3 * 2 ** (failures - 1), 3e4)
|
|
10099
|
+
};
|
|
10100
|
+
return saveSend(dependencies, snapshot, {
|
|
10101
|
+
...snapshot.send,
|
|
10102
|
+
error: code
|
|
10103
|
+
});
|
|
10104
|
+
}
|
|
10105
|
+
packagingRetry = void 0;
|
|
10106
|
+
if (packaged === false) {
|
|
10107
|
+
snapshot = await load(address);
|
|
10108
|
+
return snapshot.send.error ? saveSend(dependencies, snapshot, {
|
|
10109
|
+
...snapshot.send,
|
|
10110
|
+
error: null
|
|
10111
|
+
}) : {
|
|
10112
|
+
...snapshot,
|
|
10113
|
+
status: "applied"
|
|
10114
|
+
};
|
|
10115
|
+
}
|
|
9749
10116
|
snapshot = await load(address);
|
|
9750
10117
|
if (snapshot.send.status !== "awaiting-packaging") return {
|
|
9751
10118
|
...snapshot,
|
|
@@ -9759,6 +10126,10 @@ function createJourneySender(dependencies, phases = 3, host = {
|
|
|
9759
10126
|
if (prepared.status === "stale" || prepared.send?.status !== "prepared") return prepared;
|
|
9760
10127
|
snapshot = requireSend(prepared);
|
|
9761
10128
|
}
|
|
10129
|
+
if (snapshot.send.items.some((item) => item.accessConflict)) return saveSend(dependencies, snapshot, {
|
|
10130
|
+
...snapshot.send,
|
|
10131
|
+
request: void 0
|
|
10132
|
+
});
|
|
9762
10133
|
if (!requestMatches(snapshot.send, snapshot.journey) || !requestedItemsMatch(snapshot.send, snapshot.journey)) return saveSend(dependencies, snapshot, {
|
|
9763
10134
|
...snapshot.send,
|
|
9764
10135
|
status: "blocked",
|
|
@@ -9796,10 +10167,6 @@ function createJourneySender(dependencies, phases = 3, host = {
|
|
|
9796
10167
|
}
|
|
9797
10168
|
//#endregion
|
|
9798
10169
|
//#region apps/cli/src/account/journey/auth.ts
|
|
9799
|
-
/** An access token this close to expiry is renewed first, so a step never starts on it. */
|
|
9800
|
-
const renewBeforeMs = 12e4;
|
|
9801
|
-
const renewTimeoutMs = 3e4;
|
|
9802
|
-
const renewals = /* @__PURE__ */ new WeakMap();
|
|
9803
10170
|
/** Reuses the existing device sign-in credential; a token is never returned to the panel or Agent. */
|
|
9804
10171
|
function createJourneySendAuth(account, requestFetch) {
|
|
9805
10172
|
/**
|
|
@@ -9826,39 +10193,11 @@ function createJourneySendAuth(account, requestFetch) {
|
|
|
9826
10193
|
return value;
|
|
9827
10194
|
})).credential?.refreshToken) await expire(accessToken);
|
|
9828
10195
|
}
|
|
9829
|
-
function renew(credential) {
|
|
9830
|
-
const running = renewals.get(account);
|
|
9831
|
-
if (running) return running;
|
|
9832
|
-
const renewing = (async () => {
|
|
9833
|
-
try {
|
|
9834
|
-
const token = await account.identity.refresh(credential.refreshToken, AbortSignal.timeout(renewTimeoutMs));
|
|
9835
|
-
if (!token) {
|
|
9836
|
-
await expire(credential.accessToken);
|
|
9837
|
-
return null;
|
|
9838
|
-
}
|
|
9839
|
-
return (await account.credentials.update((value) => {
|
|
9840
|
-
if (value.credential?.refreshToken === credential.refreshToken) value.credential = {
|
|
9841
|
-
...value.credential,
|
|
9842
|
-
accessToken: token.accessToken,
|
|
9843
|
-
refreshToken: token.refreshToken ?? credential.refreshToken,
|
|
9844
|
-
scope: token.scope,
|
|
9845
|
-
issuedAt: account.now(),
|
|
9846
|
-
expiresAt: token.expiresAt
|
|
9847
|
-
};
|
|
9848
|
-
return value;
|
|
9849
|
-
})).credential;
|
|
9850
|
-
} finally {
|
|
9851
|
-
renewals.delete(account);
|
|
9852
|
-
}
|
|
9853
|
-
})();
|
|
9854
|
-
renewals.set(account, renewing);
|
|
9855
|
-
return renewing;
|
|
9856
|
-
}
|
|
9857
10196
|
async function current() {
|
|
9858
10197
|
const credential = (await account.credentials.update((value) => value)).credential;
|
|
9859
10198
|
if (!credential || credential.identityUrl !== account.configuration.identityUrl || credential.clientId !== account.configuration.clientId) return null;
|
|
9860
10199
|
const left = Date.parse(credential.expiresAt) - Date.parse(account.now());
|
|
9861
|
-
if (Number.isFinite(left) && left >
|
|
10200
|
+
if (Number.isFinite(left) && left > 12e4) return credential;
|
|
9862
10201
|
const { refreshToken } = credential;
|
|
9863
10202
|
if (!refreshToken) {
|
|
9864
10203
|
if (Number.isFinite(left) && left > 0) return credential;
|
|
@@ -9866,7 +10205,7 @@ function createJourneySendAuth(account, requestFetch) {
|
|
|
9866
10205
|
return null;
|
|
9867
10206
|
}
|
|
9868
10207
|
try {
|
|
9869
|
-
return await renew({
|
|
10208
|
+
return await renew(account, {
|
|
9870
10209
|
...credential,
|
|
9871
10210
|
refreshToken
|
|
9872
10211
|
});
|
|
@@ -9916,6 +10255,136 @@ function createJourneySendAuth(account, requestFetch) {
|
|
|
9916
10255
|
};
|
|
9917
10256
|
}
|
|
9918
10257
|
//#endregion
|
|
10258
|
+
//#region apps/cli/src/account/journey/account-access.ts
|
|
10259
|
+
/**
|
|
10260
|
+
* Whole scope-confirm account read: identity, every inventory page and a token refresh. A 203-Work,
|
|
10261
|
+
* 1,400-session account took 2.3 to 3.4 s for its pages plus a refresh, past the earlier 5 s
|
|
10262
|
+
* (issue #53). On expiry the read is `unavailable` (ADR 0277 section 5).
|
|
10263
|
+
*/
|
|
10264
|
+
const accountReadBoundMs = 3e4;
|
|
10265
|
+
const ownerBodyErrors = /* @__PURE__ */ new Set(["OWNER_EMPTY_RESPONSE", "OWNER_RESPONSE_LIMIT"]);
|
|
10266
|
+
/** Why the read failed, from error types and codes only; messages are never logged. */
|
|
10267
|
+
function failureClass(error, depth = 0) {
|
|
10268
|
+
if (!(error instanceof Error)) return "other";
|
|
10269
|
+
if (error.name === "TimeoutError") return "timeout";
|
|
10270
|
+
if (isCode(error, "ACCOUNT_SIGNED_OUT") || isCode(error, "IDENTITY_REJECTED")) return "signed-out";
|
|
10271
|
+
if (error instanceof JourneyRemoteError) {
|
|
10272
|
+
if (error.status === 401 || error.status === 403) return "signed-out";
|
|
10273
|
+
return error.status >= 500 || error.status === 429 ? "network" : "protocol";
|
|
10274
|
+
}
|
|
10275
|
+
if (isCode(error, "IDENTITY_UNAVAILABLE") && isInterrupted(error.cause)) return failureClass(error.cause, 1);
|
|
10276
|
+
if (isCode(error, "IDENTITY_UNAVAILABLE") || error instanceof SyntaxError || ownerBodyErrors.has(error.message)) return "protocol";
|
|
10277
|
+
if (isInterrupted(error)) return "network";
|
|
10278
|
+
return depth === 0 && error.cause !== void 0 ? failureClass(error.cause, 1) : "other";
|
|
10279
|
+
}
|
|
10280
|
+
/** Current and legacy upload keys of every part of one local session. */
|
|
10281
|
+
async function keysOf(session) {
|
|
10282
|
+
const keys = [];
|
|
10283
|
+
for (const part of session.parts.length ? session.parts : [void 0]) {
|
|
10284
|
+
const native = part?.nativeEvidence?.identity;
|
|
10285
|
+
const identity = {
|
|
10286
|
+
harness: session.harness,
|
|
10287
|
+
nativeSessionId: native?.sessionId ?? session.sourceSessionId,
|
|
10288
|
+
...part?.harnessRelativePath ? { relativePath: part.harnessRelativePath } : {},
|
|
10289
|
+
...session.harness === "claude" && native?.role === "worker" ? { workerIdentity: { agentId: native.agentId } } : {}
|
|
10290
|
+
};
|
|
10291
|
+
const { workerIdentity: _, ...original } = identity;
|
|
10292
|
+
keys.push([await sessionKeyFor(identity), await sessionKeyFor(original)]);
|
|
10293
|
+
}
|
|
10294
|
+
return keys;
|
|
10295
|
+
}
|
|
10296
|
+
/**
|
|
10297
|
+
* Compare frozen identities with the same hashed keys used by session uploads. A Work sends each
|
|
10298
|
+
* root's whole family (`members`, from `reviewFamilies`), so uniform Public is `public-all` only
|
|
10299
|
+
* when every part of every member is held; any key without history keeps it `public` (ADR 0275).
|
|
10300
|
+
*/
|
|
10301
|
+
async function accountAccessForRoots(state, roots, inventory, members) {
|
|
10302
|
+
const sessions = new Map(roots.map((session) => [session.id, session]));
|
|
10303
|
+
const held = /* @__PURE__ */ new Map();
|
|
10304
|
+
for (const work of inventory.works) for (const session of work.sessions) {
|
|
10305
|
+
const access = held.get(session.key) ?? /* @__PURE__ */ new Set();
|
|
10306
|
+
access.add(work.access);
|
|
10307
|
+
held.set(session.key, access);
|
|
10308
|
+
}
|
|
10309
|
+
const result = /* @__PURE__ */ new Map();
|
|
10310
|
+
for (const work of state.works) {
|
|
10311
|
+
const access = /* @__PURE__ */ new Set();
|
|
10312
|
+
let known = 0;
|
|
10313
|
+
let unknown = 0;
|
|
10314
|
+
for (const id of work.rootSessionIds) {
|
|
10315
|
+
const root = sessions.get(id);
|
|
10316
|
+
if (!root) throw new ApplicationError("CORRUPTED_STATE");
|
|
10317
|
+
for (const session of members.get(id) ?? [root]) for (const [key, legacyKey] of await keysOf(session)) {
|
|
10318
|
+
const values = /* @__PURE__ */ new Set([...held.get(key) ?? [], ...held.get(legacyKey) ?? []]);
|
|
10319
|
+
if (!values.size) {
|
|
10320
|
+
unknown++;
|
|
10321
|
+
continue;
|
|
10322
|
+
}
|
|
10323
|
+
known++;
|
|
10324
|
+
for (const value of values) access.add(value);
|
|
10325
|
+
}
|
|
10326
|
+
}
|
|
10327
|
+
if (!known) continue;
|
|
10328
|
+
const uniform = access.size === 1;
|
|
10329
|
+
result.set(work.id, uniform && access.has("private") ? "private" : uniform && access.has("public") ? unknown === 0 ? "public-all" : "public" : "mixed");
|
|
10330
|
+
}
|
|
10331
|
+
return result;
|
|
10332
|
+
}
|
|
10333
|
+
/**
|
|
10334
|
+
* Existing authenticated owner ports supply inventory. The result is three-way (ADR 0277 section
|
|
10335
|
+
* 5): a caller stops on `signed-out` or `unavailable` instead of using model access.
|
|
10336
|
+
*/
|
|
10337
|
+
function createAccountAccess(storage, auth) {
|
|
10338
|
+
return async (state, signal) => {
|
|
10339
|
+
const started = Date.now();
|
|
10340
|
+
const controller = new AbortController();
|
|
10341
|
+
const boundedSignal = AbortSignal.any([signal, controller.signal]);
|
|
10342
|
+
let timer;
|
|
10343
|
+
let onAbort = () => {};
|
|
10344
|
+
const stopped = new Promise((_resolve, reject) => {
|
|
10345
|
+
onAbort = () => reject(/* @__PURE__ */ new Error("ACCOUNT_ACCESS_CANCELLED"));
|
|
10346
|
+
boundedSignal.addEventListener("abort", onAbort, { once: true });
|
|
10347
|
+
if (boundedSignal.aborted) onAbort();
|
|
10348
|
+
timer = setTimeout(() => controller.abort(), accountReadBoundMs);
|
|
10349
|
+
});
|
|
10350
|
+
const read = async () => {
|
|
10351
|
+
boundedSignal.throwIfAborted();
|
|
10352
|
+
const connection = await auth.connect(boundedSignal);
|
|
10353
|
+
if (!connection?.sessions) return { status: "signed-out" };
|
|
10354
|
+
const plan = (await storage.resources({
|
|
10355
|
+
journeyId: state.journeyId,
|
|
10356
|
+
kind: "plan"
|
|
10357
|
+
}))[0];
|
|
10358
|
+
if (plan?.kind !== "plan") throw new ApplicationError("SCOPE_NOT_READY");
|
|
10359
|
+
const inventory = await connection.sessions.inventory(boundedSignal);
|
|
10360
|
+
const access = await accountAccessForRoots(state, plan.approval.inventory.sessions, inventory, reviewFamilies(plan.approval.inventory).members);
|
|
10361
|
+
await connection.assertCurrent(boundedSignal);
|
|
10362
|
+
return {
|
|
10363
|
+
status: "read",
|
|
10364
|
+
subject: connection.subject,
|
|
10365
|
+
access
|
|
10366
|
+
};
|
|
10367
|
+
};
|
|
10368
|
+
try {
|
|
10369
|
+
return await Promise.race([read(), stopped]);
|
|
10370
|
+
} catch (error) {
|
|
10371
|
+
if (error instanceof ApplicationError && ![
|
|
10372
|
+
"ACCOUNT_SIGNED_OUT",
|
|
10373
|
+
"IDENTITY_REJECTED",
|
|
10374
|
+
"IDENTITY_UNAVAILABLE",
|
|
10375
|
+
"CREDENTIAL_STORE_FAILED"
|
|
10376
|
+
].includes(error.code)) throw error;
|
|
10377
|
+
const reason = signal.aborted ? "cancelled" : controller.signal.aborted ? "timeout" : failureClass(error);
|
|
10378
|
+
console.error(`ACCOUNT_ACCESS_UNAVAILABLE: account read failed (class=${reason} elapsedMs=${Date.now() - started})`);
|
|
10379
|
+
return { status: reason === "signed-out" ? "signed-out" : "unavailable" };
|
|
10380
|
+
} finally {
|
|
10381
|
+
clearTimeout(timer);
|
|
10382
|
+
boundedSignal.removeEventListener("abort", onAbort);
|
|
10383
|
+
controller.abort();
|
|
10384
|
+
}
|
|
10385
|
+
};
|
|
10386
|
+
}
|
|
10387
|
+
//#endregion
|
|
9919
10388
|
//#region apps/cli/src/operation/protocol.ts
|
|
9920
10389
|
const sourceExclusionReasonSchema = z.enum([
|
|
9921
10390
|
"guardian-excluded",
|
|
@@ -10511,6 +10980,9 @@ async function readJson(response, where) {
|
|
|
10511
10980
|
}
|
|
10512
10981
|
chunks.push(next.value);
|
|
10513
10982
|
}
|
|
10983
|
+
} catch (error) {
|
|
10984
|
+
if (error instanceof ApplicationError) throw error;
|
|
10985
|
+
throw unavailable(`${where} body read failed`, error);
|
|
10514
10986
|
} finally {
|
|
10515
10987
|
reader.releaseLock();
|
|
10516
10988
|
}
|
|
@@ -11040,7 +11512,8 @@ function createDiagnostics(stateDir, writeSummary, onFatal) {
|
|
|
11040
11512
|
code: safeCode(error),
|
|
11041
11513
|
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
11042
11514
|
stage,
|
|
11043
|
-
...operationId ? { operationId } : {}
|
|
11515
|
+
...operationId ? { operationId } : {},
|
|
11516
|
+
...processingSite(error)
|
|
11044
11517
|
};
|
|
11045
11518
|
summary(diagnostic);
|
|
11046
11519
|
if (closed) {
|
|
@@ -11076,14 +11549,18 @@ function createDiagnostics(stateDir, writeSummary, onFatal) {
|
|
|
11076
11549
|
}
|
|
11077
11550
|
},
|
|
11078
11551
|
onDiagnostic(diagnostic, error) {
|
|
11079
|
-
|
|
11552
|
+
const named = {
|
|
11553
|
+
...diagnostic,
|
|
11554
|
+
...processingSite(error)
|
|
11555
|
+
};
|
|
11556
|
+
summary(named);
|
|
11080
11557
|
if (closed) {
|
|
11081
|
-
unavailable(
|
|
11558
|
+
unavailable(named);
|
|
11082
11559
|
return;
|
|
11083
11560
|
}
|
|
11084
11561
|
try {
|
|
11085
11562
|
local.logger.error({
|
|
11086
|
-
...
|
|
11563
|
+
...named,
|
|
11087
11564
|
error: errorDetail(error)
|
|
11088
11565
|
});
|
|
11089
11566
|
} catch {
|
|
@@ -11545,6 +12022,8 @@ async function compose(options, callbacks, bundles) {
|
|
|
11545
12022
|
onFatal: fatalStore.fail
|
|
11546
12023
|
});
|
|
11547
12024
|
const account = createAccount(accountPorts);
|
|
12025
|
+
const sendAuth = createJourneySendAuth(accountPorts);
|
|
12026
|
+
const accountAccess = createAccountAccess(journeyStorage, sendAuth);
|
|
11548
12027
|
const application = createApplication({
|
|
11549
12028
|
sessionPeriod: options.sessionPeriod,
|
|
11550
12029
|
journeySender: createJourneySender({
|
|
@@ -11561,7 +12040,7 @@ async function compose(options, callbacks, bundles) {
|
|
|
11561
12040
|
}
|
|
11562
12041
|
}),
|
|
11563
12042
|
artifacts: createJourneySendArtifacts(journeyStorage),
|
|
11564
|
-
auth:
|
|
12043
|
+
auth: sendAuth,
|
|
11565
12044
|
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
11566
12045
|
failure: (code, cause) => diagnostics.onFailure(code, cause),
|
|
11567
12046
|
sessionTransfer: (workId, transfer) => diagnostics.onActivity({
|
|
@@ -11584,18 +12063,29 @@ async function compose(options, callbacks, bundles) {
|
|
|
11584
12063
|
phases: { upload: elapsedMs },
|
|
11585
12064
|
volumes: { bytes }
|
|
11586
12065
|
})
|
|
11587
|
-
}, void 0, sendHost
|
|
12066
|
+
}, void 0, sendHost.serving ? sendHost : {
|
|
12067
|
+
...sendHost,
|
|
12068
|
+
focused: (journeyId) => focusedHostAlive(stateDir, journeyId, async () => (await journeyStorage.current())?.journeyId)
|
|
12069
|
+
}),
|
|
11588
12070
|
manualUploader: createManualUploader({
|
|
11589
12071
|
storage: store.manualUpload,
|
|
11590
12072
|
artifacts: createManualUploadArtifacts({ getDeliverySources: () => application.getDeliverySources() }),
|
|
11591
|
-
auth:
|
|
12073
|
+
auth: sendAuth,
|
|
11592
12074
|
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
11593
12075
|
failure: (code, cause) => diagnostics.onFailure(code, cause)
|
|
11594
12076
|
}),
|
|
11595
12077
|
journeyLocal: {
|
|
12078
|
+
accountAccess,
|
|
12079
|
+
async accountState() {
|
|
12080
|
+
const view = await account.status();
|
|
12081
|
+
return {
|
|
12082
|
+
configured: view.configuration.clientId !== null,
|
|
12083
|
+
subject: view.state === "signed-in" ? view.identityId : null
|
|
12084
|
+
};
|
|
12085
|
+
},
|
|
11596
12086
|
titles: createJourneyTitles(journeyStorage, processing),
|
|
11597
12087
|
describe: createJourneyDescription(journeyStorage, processing),
|
|
11598
|
-
works: createJourneyWorks(journeyStorage, processing, stateDir),
|
|
12088
|
+
works: createJourneyWorks(journeyStorage, processing, stateDir, accountAccess),
|
|
11599
12089
|
hide: createJourneyHide({
|
|
11600
12090
|
storage: journeyStorage,
|
|
11601
12091
|
execution,
|
|
@@ -11864,7 +12354,14 @@ async function invoke(runtime, command, value, signal) {
|
|
|
11864
12354
|
case "journey.findingImpact": return app.journey?.findingImpact(value);
|
|
11865
12355
|
case "journey.preview": return app.journey?.preview(value);
|
|
11866
12356
|
case "journey.files": return app.journey?.files(value);
|
|
11867
|
-
case "journey.prepare":
|
|
12357
|
+
case "journey.prepare": {
|
|
12358
|
+
const input = z.object({
|
|
12359
|
+
journeyId: z.uuid(),
|
|
12360
|
+
result: z.literal("receipt").optional()
|
|
12361
|
+
}).strict().parse(value);
|
|
12362
|
+
return input.result === "receipt" ? app.journey?.prepareReceipt({ journeyId: input.journeyId }, signal) : app.journey?.prepare({ journeyId: input.journeyId }, signal);
|
|
12363
|
+
}
|
|
12364
|
+
case "review.reopenEmpty": return app.journey?.reopenEmpty(value);
|
|
11868
12365
|
case "review.terms": return app.journey?.terms(value);
|
|
11869
12366
|
case "review.status": return app.journey?.reviewStatus(value);
|
|
11870
12367
|
case "review.guidance": return app.journey?.guidance(value);
|
|
@@ -12064,4 +12561,4 @@ async function cancelOperation(runtime, id) {
|
|
|
12064
12561
|
return readOperation(runtime, id);
|
|
12065
12562
|
}
|
|
12066
12563
|
//#endregion
|
|
12067
|
-
export {
|
|
12564
|
+
export { longCommands as _, readAgentPresence as a, traceStartup as b, executorRequestSchema as c, serveDetached as d, guardDetachedStartup as f, businessCommands as g, parseOptions as h, invoke as i, awaitingPassport as l, helpText as m, readOperation as n, createRuntime as o, createRenderActivity as p, waitOperation as r, executorEventSchema as s, cancelOperation as t, hostDrives as u, permissionDiagnostic as v, startupFailure as y };
|