@gitroll/tacitt-cli 0.1.0 → 0.3.0
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/dist/{agent-setup-B85GaVIz.mjs → agent-setup-CLXdKr2I.mjs} +2 -2
- package/dist/agent-setup-worker.mjs +5 -5
- package/dist/cli.mjs +165 -9
- package/dist/codex-title-worker.mjs +1 -1
- package/dist/configured-preview-worker.mjs +5 -5
- package/dist/configured-session-worker.mjs +25 -4
- package/dist/{frame-D1j9R8xX.mjs → frame-waS5FnN5.mjs} +1 -1
- package/dist/{heap-tfU61Ehy.mjs → heap-eO_uWpD1.mjs} +63 -9
- package/dist/inspection-worker.mjs +57 -0
- package/dist/job-rK-Ho9OD.mjs +613 -0
- package/dist/{locate-Bn32k2se.mjs → locate-U3-nkird.mjs} +29 -6
- package/dist/messages-D3m3bhva.mjs +40 -0
- package/dist/{model-7Nu7szNP.mjs → model-BBAnsFAx.mjs} +1 -1
- package/dist/operation-executor.mjs +3 -3
- package/dist/preview-worker.mjs +1 -1
- package/dist/privacy-validator.mjs +1 -1
- package/dist/{query-Da-KDbSV.mjs → query-DN99jiRc.mjs} +710 -630
- package/dist/{run-CCATsHS2.mjs → run-4G_CvdDj.mjs} +4 -4
- package/dist/session-process.mjs +6 -1
- package/dist/{src-CPPjOyHZ.mjs → src-B4R5XMEP.mjs} +621 -224
- package/dist/{src-BngGpFKd.mjs → src-CPbdnGrk.mjs} +130 -53
- package/dist/{src-DynBRDBc.mjs → src-qoLRvX-A.mjs} +34 -11
- package/dist/store-database-worker.mjs +1 -1
- package/dist/web/assets/index-BkuUI3Eu.js +67 -0
- package/dist/web/assets/index-Clg6Pfv_.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/skills/tacitt/SKILL.md +15 -11
- package/skills/tacitt/references/b-method.md +1 -1
- package/skills/tacitt/references/classification.md +3 -3
- package/skills/tacitt/references/commands.md +19 -14
- package/skills/tacitt/references/journey.md +17 -11
- package/skills/tacitt/references/parallel-review.md +3 -3
- package/skills/tacitt/references/send.md +5 -5
- package/skills/tacitt/scripts/INDEX.md +6 -2
- package/skills/tacitt/scripts/answer.mjs +29 -8
- package/skills/tacitt/scripts/journey-card-consolidation.d.mts +13 -4
- package/skills/tacitt/scripts/journey-card-consolidation.mjs +170 -21
- package/skills/tacitt/scripts/journey-card-values.d.mts +17 -0
- package/skills/tacitt/scripts/journey-card-values.mjs +116 -0
- package/skills/tacitt/scripts/journey-classification-progress.mjs +2 -1
- package/skills/tacitt/scripts/journey-classification-validation.mjs +49 -28
- package/skills/tacitt/scripts/journey-classification.mjs +6 -2
- package/skills/tacitt/scripts/journey-consolidation-submit.mjs +42 -5
- package/skills/tacitt/scripts/journey-consolidation.d.mts +5 -0
- package/skills/tacitt/scripts/journey-consolidation.mjs +4 -1
- package/skills/tacitt/scripts/journey-panel-host.d.mts +9 -0
- package/skills/tacitt/scripts/journey-panel-host.mjs +56 -0
- package/skills/tacitt/scripts/journey-state.d.mts +1 -0
- package/skills/tacitt/scripts/journey-state.mjs +38 -6
- package/skills/tacitt/scripts/journey-tasks.mjs +5 -3
- package/skills/tacitt/scripts/journey.d.mts +4 -0
- package/skills/tacitt/scripts/journey.mjs +249 -41
- package/skills/tacitt/scripts/privacy-validator.mjs +25 -6
- package/skills/tacitt/scripts/review-stream-client.mjs +1 -1
- package/skills/tacitt/scripts/review.mjs +24 -3
- package/dist/job--TceKk0l.mjs +0 -194
- package/dist/web/assets/index-CWQ05PFl.js +0 -67
- package/dist/web/assets/index-J-AWt_cW.css +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { A as runConfiguredSessionJob, C as readConfiguredSearchFields, D as captureLogicalJourneySources,
|
|
2
|
-
import { Bt as sessionPeriodSchema, Dn as entityDictionaryVersion, F as bClassificationKey, Fn as frameRecords, Ft as termTypeInfo, Gt as safeField, H as codePointLength, Ht as ApplicationError, Jt as reviewFamilies, K as utf16Offset, N as bUnitFragments, Ot as privacyTermKey, Qn as admitGitName, T as bLiteralTerms, Ut as UnfinishedJourneyError, V as bDigest, Wt as safeCode, Xn as createEntityMatcher, Zn as admitCustomTerm, Zt as observeSource, b as validateBReview, c as textDigest, h as reviewFindingsDigest, ht as journeyDescribeSchema, jt as reviewTermsInputSchema, l as validateReview, mn as currentDetectorVersion, n as compileBFindings, r as reviewBatchChunks, s as reviewSubmitSchema, t as locateTerms, yn as createStaticProcessor, z as bAnchorKey
|
|
3
|
-
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-
|
|
4
|
-
import { $ as
|
|
5
|
-
import { $ as
|
|
6
|
-
import { a as writeJourneyOutput, c as mergeHideIndexTokens, l as packRenderHideIndex, o as createHideIndexTokens, s as hideIndexTexts, t as journeyOutputJob } from "./job
|
|
7
|
-
import { t as createAgentSetupPort } from "./agent-setup-
|
|
1
|
+
import { A as runConfiguredSessionJob, C as readConfiguredSearchFields, D as captureLogicalJourneySources, F as sessionWorkerFailure, G as staticMaskCategory, H as readLogicalJourneyFinalMasks, I as privateCollectionDirectory, K as readWorkspacePair, L as defaultParallelism, M as parseCollectionWorkspaceSummary, N as createSharedSessionPool, O as capturedJourneyScope, P as SessionWorkerError, Q as createCleanup, R as gitSeedsProvider, S as readOriginalSearchFields, T as checkReusableBundle, U as readConfiguredOutputInfo, V as privateJourneyDirectory, W as processJourneyMetadataFields, X as createTemporary, Z as removeNamedTemporarySpools, _ as readStaticSessionMasks, a as scanReviewFields, at as isWindowsAclFailure, c as readSessionScanRecords, g as processJourneyMetadata, h as readCapturedOriginalRows, i as buildWorkScanBatches, it as windowsOwnerOnly, j as readCollectionIndex, l as scanPreparedSession, n as verifyPreparedSessionSources, nt as openPrivateFile, o as createWorkPieces, ot as windowsAclCodes, p as openConfiguredUpload, r as prepareSessionScans, rt as restrictToOwner, s as readSessionScanFields, t as preparedScanConfiguration, tt as makePrivateDirectory, v as staticSessionJobs, w as createCollectionArtifacts, z as parallelismLimits } from "./src-CPbdnGrk.mjs";
|
|
2
|
+
import { Bt as sessionPeriodSchema, Dn as entityDictionaryVersion, F as bClassificationKey, Fn as frameRecords, Ft as termTypeInfo, Gt as safeField, H as codePointLength, Ht as ApplicationError, Jt as reviewFamilies, K as utf16Offset, N as bUnitFragments, Ot as privacyTermKey, Qn as admitGitName, T as bLiteralTerms, Ut as UnfinishedJourneyError, V as bDigest, Wt as safeCode, Xn as createEntityMatcher, Zn as admitCustomTerm, Zt as observeSource, b as validateBReview, c as textDigest, h as reviewFindingsDigest, ht as journeyDescribeSchema, jt as reviewTermsInputSchema, l as validateReview, mn as currentDetectorVersion, n as compileBFindings, r as reviewBatchChunks, s as reviewSubmitSchema, t as locateTerms, yn as createStaticProcessor, z as bAnchorKey } from "./locate-U3-nkird.mjs";
|
|
3
|
+
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-qoLRvX-A.mjs";
|
|
4
|
+
import { $ as settingsQuerySchema, $n as reviewCandidateInputSchema, $t as journeySearchResultSchema, A as deviceProfileScopeSchema, Ar as nativeEventsSchema, B as validateInspectionBatch, Br as isSelectedWork, Bt as journeyReviewStorageQuerySchema, C as rebindReviewSubmit, Ct as sendStartSchema, D as createAccount, Dt as sendReplaceResultSchema, E as staticallyHiddenFindings, En as confirmedHideAdditions, Er as journeyClassificationSessionsSchema, Et as sendReceiptSchema, Gr as journeyFindingsSchema, Gt as retireScanGeneration, Hr as withReadingProgress, Jr as journeyRenderSchema, Jt as journeyContextQuerySchema, Kn as journeyConfirmedHides, Kr as journeyPreviewSchema, Kt as newJourneyWork, L as createApplication, Lr as hasCompleteWorkDescription, M as readAccountAvatarUrl, Mr as nativeRespondSchema, Mt as obsoleteJourney, N as storedCredentialSchema, Nr as hasCurrentStaticReview, O as accountProfileSchema, On as readConfirmedHideChange, Or as assertJourneyPrivacyReady, Ot as sendResolutionSchema, Pr as staticReviewDigest, Q as settingsBatchSchema, Qn as workCorrectionBinding, Qr as selectionSchema, R as inspectionCancelSchema, Rr as isClassifying, Rt as applyClassification, S as rebindReviewOverlay, St as sendPrepareSchema, T as guardedFindingText, Tt as sendArtifactSchema, U as inspectionQuerySchema, Un as confirmedHideKinds, Ut as classificationPartition, Vn as confirmedHideGroupIds, Wr as journeyFindingImpactSchema, Wt as classificationWorks, Xn as workDecisionDigest, Xr as packageCommandSchema, Yn as retireWorkDescription, Yr as commandSchema, Zr as selectionBatchSchema, _n as sessionReviewInputSchema, _r as journeyInventoryQuerySchema, _t as maximumTermsFileBytes, a as bCrossSegmentReviewSchema, b as bReviewScope, bt as sendActionSchema, c as reconstructBFields, ct as validateFailureLocation, d as selectedValueMembers, dr as scanGitSeedsId, dt as manualUploadChoiceSchema, ei as producerVersion, er as reviewCandidateResourceSchema, et as readWorkspaceOutcomes, f as splitSelectorValues, fn as sessionReconcileInputSchema, fr as scanGitSeedsResource, ft as manualUploadPrepareSchema, g as changeJourney, gt as emptyTermsRevision, hn as sessionRereadInputSchema, i as validateBCrossSegmentReview, in as journeyResourceQuerySchema, ir as bReviewDigest, j as profileUrlMatchesIdentity, jn as termHideAdditions, jr as nativeGetSchema, jt as currentJourneyAdapters, k as deviceAuthorizationSchema, kr as privacyDecisionDigest, kt as sendSettingsSchema, ln as reviewAttemptResourceSchema, lr as scanPreparationDigest, lt as manualUploadActionSchema, m as hideScopeDigest, mr as scanExclusionResourceSchema, mt as manualUploadStartSchema, n as scanBatchEquivalenceDigest, nn as journeyPolicyRespondSchema, nr as reviewRepairResourceSchema, nt as appendSensitiveTerms, o as bRereadDigest, or as planBRepair, ot as artifactFailureDetailSchema, p as activeJourneyTerms, pr as verifiedScanGitSeeds, pt as manualUploadSchema, qn as maskedHitsStale, qr as journeyRenderInputSchema, qt as reviewCompileSchema, r as locateTermEvidence, ri as recommendParallelism, s as reconcileBSession, sn as journeyPlaceSchema, st as resolveFailureLocation, t as scanBatchContextDigest, ti as stateSchema, tn as searchScopeDigest, tr as reviewRepairInputSchema, ur as scanSessionResourceSchema, vn as sessionReviewQuerySchema, vt as normalizeSensitiveTerms, w as reviewBatchReuse, wt as journeySendSchema, x as rebindReviewAlignment, xn as sessionUsefulnessInputSchema, xr as emptyPreparationFacts, xt as sendChoiceSchema, y as isGenericPublicTerm, yt as replaceSensitiveTermsSchema, z as inspectionStartSchema, zn as bTermMatcher, zr as isReadingComplete } from "./src-B4R5XMEP.mjs";
|
|
5
|
+
import { $ as checkVersion, A as resourceSelectionQuerySchema, B as termReuseQuerySchema, C as loadAcceptedReview, D as aggregateUsefulness, E as loadPrivateResource, F as validateUnitCandidate, G as requireActiveBatch, H as batchHeader, I as reuseQuerySchema, J as validateDatabase, K as initializeAndClaim, L as batchQuery, M as evaluateUnitEvidence, N as sameCandidateRequest, O as sessionUsefulnessRecommendation, P as validateCandidateAttempt, Q as validateState, R as isPrivateResourceKind, S as validateSessionBindings, T as loadBatch, U as createJourneyPayloadFiles, V as privateReviewHash, W as validateBatchBody, X as readBundleDirectory, Y as withDatabase, Z as readStoredState, _ as loadReconciliation, _t as isCoordinationBusy, a as decodeError, at as hasResetMarker, b as validatePreparationSelection, c as dropsAnchor, ct as privatePath, d as parseDelivery, dt as stateDirectory, et as schemaVersion, f as validateCandidate, ft as syncDirectory, g as loadPreparedBodies, gt as errorCode$1, h as loadPreparation, ht as StoreError, i as createExecutor, it as finishReset, j as inspectionQuerySchema$1, k as sessionQuery, l as replySchema, lt as readResetMarker, m as loadStatus, mt as withFileCheckTimings, n as releaseInheritedHeapFlags, nt as createDatabaseFile, o as ProtocolError, ot as inspectStateFiles, p as classificationJourneyCommit, pt as withStateSnapshot, q as ownership, r as storeHeapLimits, rt as databaseName, s as allowedReplies, st as present, tt as beginReset, u as operations, ut as recheckPrivatePath, v as loadRereadBodies, vt as isSqliteCorruption, w as loadAuthenticatedReview, x as validateSessionReconciliation, y as loadSessionBodies, z as privateResourceHeader } from "./heap-eO_uWpD1.mjs";
|
|
6
|
+
import { _ as staticBaseProduction, a as writeJourneyOutput, 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 prepareSourcePlan, y as workApproval } from "./job-rK-Ho9OD.mjs";
|
|
7
|
+
import { t as createAgentSetupPort } from "./agent-setup-CLXdKr2I.mjs";
|
|
8
|
+
import { t as workerMessageSchema } from "./messages-D3m3bhva.mjs";
|
|
8
9
|
import { constants, write } from "node:fs";
|
|
9
10
|
import { z } from "zod";
|
|
10
11
|
import { copyFile, link, lstat, mkdir, mkdtemp, open, opendir, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
|
|
@@ -650,6 +651,7 @@ const operation = z.enum([
|
|
|
650
651
|
"journey.terms.select-aliases",
|
|
651
652
|
"journey.commit",
|
|
652
653
|
"journey.commit.checked",
|
|
654
|
+
"journey.classification.commit",
|
|
653
655
|
"journey.events",
|
|
654
656
|
"journey.inventory",
|
|
655
657
|
"journey.preparation.progress",
|
|
@@ -826,12 +828,16 @@ function runObserved(frame, leg, work) {
|
|
|
826
828
|
}
|
|
827
829
|
//#endregion
|
|
828
830
|
//#region packages/local-host/src/router.ts
|
|
829
|
-
/**
|
|
831
|
+
/** Decisions the person makes in the panel while the agent waits for them. */
|
|
830
832
|
const nativePanelRespondSchema = z.object({
|
|
831
833
|
journeyId: nativeRespondSchema.shape.journeyId,
|
|
832
834
|
expectedRevision: nativeRespondSchema.shape.expectedRevision,
|
|
833
835
|
idempotencyKey: nativeRespondSchema.shape.idempotencyKey,
|
|
834
836
|
action: z.union([
|
|
837
|
+
z.object({
|
|
838
|
+
kind: z.literal("consent"),
|
|
839
|
+
allow: z.boolean()
|
|
840
|
+
}).strict(),
|
|
835
841
|
z.object({
|
|
836
842
|
kind: z.literal("work-access"),
|
|
837
843
|
workId: z.uuid(),
|
|
@@ -845,12 +851,17 @@ const nativePanelRespondSchema = z.object({
|
|
|
845
851
|
kind: z.literal("card-answer"),
|
|
846
852
|
cardId: z.uuid(),
|
|
847
853
|
optionId: z.string(),
|
|
848
|
-
remember: z.boolean().optional()
|
|
854
|
+
remember: z.boolean().optional(),
|
|
855
|
+
excludedTermKeys: z.array(z.string().min(1)).max(2e4).optional()
|
|
849
856
|
}).strict(),
|
|
850
|
-
z.object({ kind: z.literal("package-confirm") }).strict()
|
|
857
|
+
z.object({ kind: z.literal("package-confirm") }).strict(),
|
|
858
|
+
z.object({
|
|
859
|
+
kind: z.literal("retry-work"),
|
|
860
|
+
workId: z.uuid()
|
|
861
|
+
}).strict()
|
|
851
862
|
])
|
|
852
863
|
}).strict();
|
|
853
|
-
function createRouter(application, account, stopping, delivery, nativeInspection) {
|
|
864
|
+
function createRouter(application, account, stopping, delivery, nativeInspection, agentPresence) {
|
|
854
865
|
const t = initTRPC.context().create({
|
|
855
866
|
sse: {
|
|
856
867
|
ping: {
|
|
@@ -920,6 +931,7 @@ function createRouter(application, account, stopping, delivery, nativeInspection
|
|
|
920
931
|
respond: t.procedure.input(sendChoiceSchema).mutation(({ input }) => sending().respond(input)),
|
|
921
932
|
pause: t.procedure.input(sendActionSchema).mutation(({ input }) => sending().pause(input)),
|
|
922
933
|
resume: t.procedure.input(sendActionSchema).mutation(({ input }) => sending().resume(input)),
|
|
934
|
+
withdraw: t.procedure.input(sendActionSchema).mutation(({ input }) => sending().withdraw(input)),
|
|
923
935
|
advance: t.procedure.input(sendAddress).mutation(({ input }) => sending().advance(input, stopping))
|
|
924
936
|
}),
|
|
925
937
|
manualUpload: t.router({
|
|
@@ -937,11 +949,13 @@ function createRouter(application, account, stopping, delivery, nativeInspection
|
|
|
937
949
|
cardsRead: t.procedure.input(nativeGetSchema.required()).query(({ input }) => native().cardsRead(input)),
|
|
938
950
|
files: t.procedure.input(journeyRenderSchema).query(({ input, signal }) => native().files(input, signal)),
|
|
939
951
|
searchResult: t.procedure.input(nativeGetSchema.required()).query(({ input }) => native().searchResult(input)),
|
|
952
|
+
prepare: t.procedure.input(nativeGetSchema.required()).mutation(({ input }) => native().prepare(input, stopping)),
|
|
940
953
|
render: t.procedure.input(journeyRenderInputSchema).mutation(({ input, signal }) => native().render(input, signal)),
|
|
941
954
|
findings: t.procedure.input(journeyFindingsSchema).query(({ input, signal }) => native().findings(input, signal)),
|
|
942
955
|
findingImpact: t.procedure.input(journeyFindingImpactSchema).query(({ input, signal }) => native().findingImpact(input, signal)),
|
|
943
956
|
preview: t.procedure.input(journeyPreviewSchema).query(({ input, signal }) => native().preview(input, signal)),
|
|
944
957
|
get: t.procedure.input(nativeGetSchema).query(({ input, signal }) => readThroughContention(() => native().get(input), signal)),
|
|
958
|
+
presence: t.procedure.input(z.object({ journeyId: z.uuid() }).strict()).query(async ({ input }) => ({ agent: await agentPresence?.(input.journeyId) ?? null })),
|
|
945
959
|
inventory: t.procedure.input(journeyInventoryQuerySchema).query(({ input }) => native().inventory(input)),
|
|
946
960
|
events: t.procedure.input(nativeEventsSchema).query(({ input, signal }) => readThroughContention(() => native().events(input, signal), signal))
|
|
947
961
|
}),
|
|
@@ -2827,7 +2841,7 @@ async function startHost(options) {
|
|
|
2827
2841
|
reply.raw.once("close", () => streams.delete(reply.raw));
|
|
2828
2842
|
});
|
|
2829
2843
|
const trpcOptions = {
|
|
2830
|
-
router: createRouter(options.application, options.account, stopping.signal, delivery, options.nativeInspection),
|
|
2844
|
+
router: createRouter(options.application, options.account, stopping.signal, delivery, options.nativeInspection, options.agentPresence),
|
|
2831
2845
|
createContext: ({ req }) => {
|
|
2832
2846
|
const currentView = requestObservations.get(req);
|
|
2833
2847
|
return {
|
|
@@ -4714,6 +4728,19 @@ function createStagedJourney(access, root, databaseId) {
|
|
|
4714
4728
|
});
|
|
4715
4729
|
});
|
|
4716
4730
|
},
|
|
4731
|
+
/** BL-216: as `commit`, for a proposal the store applies to its current state. */
|
|
4732
|
+
commitClassification(input) {
|
|
4733
|
+
return access.staged(async (leg, signal) => {
|
|
4734
|
+
const first = await leg.run("journey.classification.commit", input);
|
|
4735
|
+
if (first.status !== "hide-delta") return first;
|
|
4736
|
+
const key = await files.hideKey(first.state.journeyId, signal);
|
|
4737
|
+
const delta = await readConfirmedHideChange({ journeyTerms: (query) => leg.run("journey.terms.read", query) }, key, first.state, first.next);
|
|
4738
|
+
return leg.run("journey.commit", {
|
|
4739
|
+
...classificationJourneyCommit(input, first.next, first.state.revision),
|
|
4740
|
+
confirmedHideDelta: delta
|
|
4741
|
+
});
|
|
4742
|
+
});
|
|
4743
|
+
},
|
|
4717
4744
|
resources: createStagedResources(access, files, (input) => inspection.protectedBase(input)),
|
|
4718
4745
|
saveResource: createStagedResourceSave(access, files),
|
|
4719
4746
|
async putBatch(batch) {
|
|
@@ -4988,6 +5015,7 @@ async function openStore(options, backupObsolete) {
|
|
|
4988
5015
|
acknowledgeSessionUsefulness: (input) => stagedJourney.acknowledgeSessionUsefulness(input),
|
|
4989
5016
|
current: (journeyId) => access.run("journey.current", journeyId ? { journeyId } : {}),
|
|
4990
5017
|
commit: (input) => stagedJourney.commit(input),
|
|
5018
|
+
commitClassification: (input) => stagedJourney.commitClassification(input),
|
|
4991
5019
|
hideIndexKey: (journeyId) => stagedJourney.hideIndexKey(journeyId),
|
|
4992
5020
|
recordTerms: (input) => stagedJourney.recordTerms(input),
|
|
4993
5021
|
journeyTerms: (input) => stagedJourney.journeyTerms(input),
|
|
@@ -5458,6 +5486,10 @@ function parseOptions(args, source = sourceCheckout) {
|
|
|
5458
5486
|
type: "boolean",
|
|
5459
5487
|
default: false
|
|
5460
5488
|
},
|
|
5489
|
+
detach: {
|
|
5490
|
+
type: "boolean",
|
|
5491
|
+
default: false
|
|
5492
|
+
},
|
|
5461
5493
|
"state-dir": { type: "string" },
|
|
5462
5494
|
"bundle-dir": { type: "string" },
|
|
5463
5495
|
"claude-home": { type: "string" },
|
|
@@ -5507,6 +5539,7 @@ function parseOptions(args, source = sourceCheckout) {
|
|
|
5507
5539
|
"all"
|
|
5508
5540
|
].includes(parsed.values.agent) || parsed.values.scope !== void 0 && !["user", "project"].includes(parsed.values.scope) || parsed.values["cli-path"] !== void 0 && !isAbsolute(parsed.values["cli-path"]) || parsed.values["configure-agents"] !== void 0 && parsed.values["skip-agent-config"] !== void 0) throw new Error("INVALID_ARGUMENTS");
|
|
5509
5541
|
if (parsed.values.async && (!command || !longCommands.has(command))) throw new Error("INVALID_ARGUMENTS");
|
|
5542
|
+
if (parsed.values.detach && command !== "journey.serve") throw new Error("INVALID_ARGUMENTS");
|
|
5510
5543
|
const timeoutMs = parsed.values["timeout-ms"] === void 0 ? 3e4 : Number(parsed.values["timeout-ms"]);
|
|
5511
5544
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647) throw new Error("INVALID_ARGUMENTS");
|
|
5512
5545
|
if (parsed.values.output && !stateDump) throw new Error("INVALID_ARGUMENTS");
|
|
@@ -5552,6 +5585,9 @@ ${Array.from(businessCommands).join("\n")}
|
|
|
5552
5585
|
|
|
5553
5586
|
Business commands invoke the shared Application directly without an HTTP server.
|
|
5554
5587
|
journey.serve starts a focused confirmation host, emits JSON readiness, and waits until stopped.
|
|
5588
|
+
journey.serve --detach starts that host in its own process, prints its readiness with the host's
|
|
5589
|
+
pid, and exits; stop the host with that pid. A detached host already serving the journey is
|
|
5590
|
+
reused and reported with reused: true.
|
|
5555
5591
|
It does not discover sessions or open a browser. The agent opens its private URL in the right pane.
|
|
5556
5592
|
Workflow mutations require protocolVersion, runId and expectedRevision in JSON input.
|
|
5557
5593
|
Long commands emit the operation ID immediately as NDJSON, then wait up to 30000 ms.
|
|
@@ -5760,105 +5796,6 @@ function createJourneyFindings(storage) {
|
|
|
5760
5796
|
};
|
|
5761
5797
|
}
|
|
5762
5798
|
//#endregion
|
|
5763
|
-
//#region apps/cli/src/journey/plan.ts
|
|
5764
|
-
async function prepareSourcePlan(storage, harnesses, journeyId, sensitiveTerms, signal, period = {}) {
|
|
5765
|
-
const saved = (await storage.resources({
|
|
5766
|
-
journeyId,
|
|
5767
|
-
kind: "plan"
|
|
5768
|
-
}))[0];
|
|
5769
|
-
if (saved?.kind === "plan") return saved;
|
|
5770
|
-
const discovered = await harnesses.discover(signal);
|
|
5771
|
-
if (!discovered.complete) throw new ApplicationError("SCOPE_NOT_READY");
|
|
5772
|
-
const inventory = filterInventoryByPeriod(discovered, period);
|
|
5773
|
-
if (inventory.sessions.length === 0) throw new ApplicationError("INVALID_SELECTION");
|
|
5774
|
-
const families = reviewFamilies(inventory);
|
|
5775
|
-
const defaults = defaultScopedSettings();
|
|
5776
|
-
const settings = {
|
|
5777
|
-
...defaults,
|
|
5778
|
-
overall: {
|
|
5779
|
-
...defaults.overall,
|
|
5780
|
-
cleanup: {
|
|
5781
|
-
ref: {
|
|
5782
|
-
id: "native-evidence",
|
|
5783
|
-
version: "2"
|
|
5784
|
-
},
|
|
5785
|
-
options: { assessedSessionsOnly: true }
|
|
5786
|
-
}
|
|
5787
|
-
},
|
|
5788
|
-
workspaces: Object.fromEntries(inventory.workspaces.map((work) => [work.id, {
|
|
5789
|
-
parameters: {
|
|
5790
|
-
"screened-identifiers": { enabled: false },
|
|
5791
|
-
"ip-addresses": { enabled: false },
|
|
5792
|
-
hostnames: { enabled: false },
|
|
5793
|
-
"cloud-resources": { enabled: false },
|
|
5794
|
-
"mac-addresses": { enabled: false },
|
|
5795
|
-
"public-key-containers": { enabled: false },
|
|
5796
|
-
"geographic-coordinates": { enabled: false },
|
|
5797
|
-
urls: { enabled: false },
|
|
5798
|
-
"filesystem-paths": { mode: "identity-roots" }
|
|
5799
|
-
},
|
|
5800
|
-
optionalSteps: { "phone-numbers": true }
|
|
5801
|
-
}]))
|
|
5802
|
-
};
|
|
5803
|
-
const configuration = freezeConfiguration(settings, inventory.sessions.flatMap((session) => {
|
|
5804
|
-
const owner = families.owners.get(session.id);
|
|
5805
|
-
return owner ? [{
|
|
5806
|
-
sessionId: session.id,
|
|
5807
|
-
workspaceId: session.workspaceId,
|
|
5808
|
-
settingsOwner: {
|
|
5809
|
-
sessionId: owner.id,
|
|
5810
|
-
workspaceId: owner.workspaceId
|
|
5811
|
-
}
|
|
5812
|
-
}] : [];
|
|
5813
|
-
}));
|
|
5814
|
-
const resource = {
|
|
5815
|
-
kind: "plan",
|
|
5816
|
-
journeyId,
|
|
5817
|
-
approval: {
|
|
5818
|
-
runId: journeyId,
|
|
5819
|
-
revision: 0,
|
|
5820
|
-
approvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5821
|
-
inventory,
|
|
5822
|
-
sourceChangePolicy: "fail",
|
|
5823
|
-
treatment: {
|
|
5824
|
-
kind: "configured",
|
|
5825
|
-
configuration,
|
|
5826
|
-
dictionaryRevision: sensitiveTerms.revision
|
|
5827
|
-
}
|
|
5828
|
-
},
|
|
5829
|
-
replacementKey: randomBytes(32).toString("hex"),
|
|
5830
|
-
sensitiveTerms: {
|
|
5831
|
-
revision: sensitiveTerms.revision,
|
|
5832
|
-
terms: [...sensitiveTerms.terms]
|
|
5833
|
-
}
|
|
5834
|
-
};
|
|
5835
|
-
await storage.saveResource(resource);
|
|
5836
|
-
return resource;
|
|
5837
|
-
}
|
|
5838
|
-
function workApproval(approval, rootIds) {
|
|
5839
|
-
if (approval.treatment?.kind !== "configured") throw new ApplicationError("PROCESSING_PLAN_UNAVAILABLE");
|
|
5840
|
-
const families = reviewFamilies(approval.inventory);
|
|
5841
|
-
const ids = new Set(rootIds.flatMap((id) => {
|
|
5842
|
-
const members = families.members.get(id);
|
|
5843
|
-
if (!members) throw new ApplicationError("UNKNOWN_ITEM");
|
|
5844
|
-
return members.map((session) => session.id);
|
|
5845
|
-
}));
|
|
5846
|
-
const sessions = approval.inventory.sessions.filter((session) => ids.has(session.id));
|
|
5847
|
-
const workspaces = new Set(sessions.map((session) => session.workspaceId));
|
|
5848
|
-
return {
|
|
5849
|
-
...approval,
|
|
5850
|
-
inventory: {
|
|
5851
|
-
...approval.inventory,
|
|
5852
|
-
sessions,
|
|
5853
|
-
workspaces: approval.inventory.workspaces.filter((workspace) => workspaces.has(workspace.id))
|
|
5854
|
-
},
|
|
5855
|
-
treatment: {
|
|
5856
|
-
...approval.treatment,
|
|
5857
|
-
configuration: selectFrozenConfiguration(approval.treatment.configuration, ids)
|
|
5858
|
-
}
|
|
5859
|
-
};
|
|
5860
|
-
}
|
|
5861
|
-
//#endregion
|
|
5862
5799
|
//#region apps/cli/src/journey/metadata/processing.ts
|
|
5863
5800
|
/** Load the frozen plan and active terms once; each Work then resolves its own policy. */
|
|
5864
5801
|
async function metadataPolicy(storage, state, processing) {
|
|
@@ -6060,6 +5997,10 @@ function bindJourneyDestination(storage, destination) {
|
|
|
6060
5997
|
await guard(input.journeyId);
|
|
6061
5998
|
return storage.saveResource(input);
|
|
6062
5999
|
},
|
|
6000
|
+
async commitClassification(input) {
|
|
6001
|
+
await guard(input.journeyId);
|
|
6002
|
+
return storage.commitClassification(input);
|
|
6003
|
+
},
|
|
6063
6004
|
async respondPolicy(input) {
|
|
6064
6005
|
await guard(input.request.journeyId);
|
|
6065
6006
|
return storage.respondPolicy(input);
|
|
@@ -6784,6 +6725,14 @@ async function publishWorkBatches(storage, plan, work, resources, signal) {
|
|
|
6784
6725
|
}
|
|
6785
6726
|
//#endregion
|
|
6786
6727
|
//#region apps/cli/src/journey/prepare/scan-sessions.ts
|
|
6728
|
+
/** Scan refusals caused by one session's own sources, the codes admission refusals also isolate. */
|
|
6729
|
+
const rootScanRefusals = /* @__PURE__ */ new Set([
|
|
6730
|
+
"ARTIFACT_PROCESSING_UNSUPPORTED",
|
|
6731
|
+
"PROCESSING_UNSUPPORTED",
|
|
6732
|
+
"ARTIFACT_SOURCE_CHANGED",
|
|
6733
|
+
"ARTIFACT_LIMIT",
|
|
6734
|
+
"PROCESSING_LIMIT"
|
|
6735
|
+
]);
|
|
6787
6736
|
/**
|
|
6788
6737
|
* Track A (ADR 0172, ADR 0177): every session of the Work is submitted to the shared pool at once,
|
|
6789
6738
|
* ahead of Track B, and each completed session's receipt is saved as it arrives. Once every session
|
|
@@ -6847,7 +6796,11 @@ async function publishScanSessions(storage, plan, work, prepared, binding, direc
|
|
|
6847
6796
|
volumes.scanInputFields += resource.input.fieldCount;
|
|
6848
6797
|
volumes.scanFileBytes += resource.input.file.byteLength;
|
|
6849
6798
|
}, (error) => {
|
|
6850
|
-
|
|
6799
|
+
const rootSessionId = families.owners.get(selected.session.id)?.id;
|
|
6800
|
+
failure ??= rootSessionId ? {
|
|
6801
|
+
error,
|
|
6802
|
+
rootSessionId
|
|
6803
|
+
} : { error };
|
|
6851
6804
|
withdraw.abort();
|
|
6852
6805
|
});
|
|
6853
6806
|
if (pooled) {
|
|
@@ -6885,7 +6838,11 @@ async function publishScanSessions(storage, plan, work, prepared, binding, direc
|
|
|
6885
6838
|
...paragraphs ? { scanHostedCopies: paragraphs.hosted } : {}
|
|
6886
6839
|
}
|
|
6887
6840
|
});
|
|
6888
|
-
if (failure)
|
|
6841
|
+
if (failure) {
|
|
6842
|
+
const code = safeCode(failure.error);
|
|
6843
|
+
if (!hooks.signal.aborted && failure.rootSessionId && rootScanRefusals.has(code)) throw new RootPreparationFailure(code, /* @__PURE__ */ new Map([[failure.rootSessionId, code]]), { cause: failure.error });
|
|
6844
|
+
throw failure.error;
|
|
6845
|
+
}
|
|
6889
6846
|
hooks.signal.throwIfAborted();
|
|
6890
6847
|
if (resources.length !== prepared.sessions.length) throw new ApplicationError("SCOPE_NOT_READY");
|
|
6891
6848
|
return {
|
|
@@ -7111,223 +7068,6 @@ async function prepareJourneyWork(storage, plan, work, directory, processing, ho
|
|
|
7111
7068
|
}
|
|
7112
7069
|
}
|
|
7113
7070
|
//#endregion
|
|
7114
|
-
//#region apps/cli/src/journey/static/production.ts
|
|
7115
|
-
/**
|
|
7116
|
-
* The single production both Track B and packaging use for one Work generation (ADR 0172):
|
|
7117
|
-
* frozen captures, built-in static detection only (no custom terms, no Git seeds), the Work's
|
|
7118
|
-
* exceptions and a replacement key bound to this generation. Identical inputs give identical
|
|
7119
|
-
* session render jobs, so packaging can require the stored Track B result by job digest.
|
|
7120
|
-
*/
|
|
7121
|
-
async function staticBaseProduction(storage, state, work, processing, hooks) {
|
|
7122
|
-
const excluded = work.excludedReview?.snapshotId === state.snapshotId && work.excludedReview.inputRevision === work.inputRevision ? new Set(work.excludedReview.rootSessionIds) : /* @__PURE__ */ new Set();
|
|
7123
|
-
const roots = work.rootSessionIds.filter((id) => !excluded.has(id));
|
|
7124
|
-
if (!roots.length) return void 0;
|
|
7125
|
-
const plan = (await storage.resources({
|
|
7126
|
-
journeyId: state.journeyId,
|
|
7127
|
-
kind: "plan"
|
|
7128
|
-
}))[0];
|
|
7129
|
-
if (plan?.kind !== "plan") throw new ApplicationError("CORRUPTED_STATE");
|
|
7130
|
-
const approval = workApproval(plan.approval, roots);
|
|
7131
|
-
if (approval.treatment?.kind !== "configured") throw new ApplicationError("CORRUPTED_STATE");
|
|
7132
|
-
const parts = new Set(approval.inventory.sessions.flatMap((session) => session.parts.map((part) => part.id)));
|
|
7133
|
-
const captures = (await storage.resources({
|
|
7134
|
-
journeyId: state.journeyId,
|
|
7135
|
-
kind: "capture"
|
|
7136
|
-
})).flatMap((item) => item.kind === "capture" && parts.has(item.capture.partId) ? [item.capture] : []);
|
|
7137
|
-
if (captures.length !== parts.size) throw new ApplicationError("SCOPE_NOT_READY");
|
|
7138
|
-
const { sensitiveTerms: _live, customTerms: _terms, staticExceptions: _unscoped, ...local } = processing;
|
|
7139
|
-
const production = await prepareCollectionProduction(approval, state.journeyId, {
|
|
7140
|
-
configured: {
|
|
7141
|
-
...local,
|
|
7142
|
-
gitNames: async () => ({
|
|
7143
|
-
names: [],
|
|
7144
|
-
snapshots: []
|
|
7145
|
-
}),
|
|
7146
|
-
replacementKey: createHash("sha256").update(JSON.stringify([
|
|
7147
|
-
"static-base-v1",
|
|
7148
|
-
plan.replacementKey,
|
|
7149
|
-
work.id,
|
|
7150
|
-
work.inputRevision
|
|
7151
|
-
])).digest("hex"),
|
|
7152
|
-
staticExceptions: {
|
|
7153
|
-
workId: work.id,
|
|
7154
|
-
values: state.exceptions.filter((item) => item.workId === work.id).map(({ kind, value }) => ({
|
|
7155
|
-
kind,
|
|
7156
|
-
value
|
|
7157
|
-
}))
|
|
7158
|
-
}
|
|
7159
|
-
},
|
|
7160
|
-
captures,
|
|
7161
|
-
everyAdmissionRefusal: true
|
|
7162
|
-
}, hooks);
|
|
7163
|
-
if (production.kind !== "configured" || production.workspaceFailures.length) throw new ApplicationError("SCOPE_NOT_READY");
|
|
7164
|
-
const families = reviewFamilies(approval.inventory);
|
|
7165
|
-
const admitted = new Set(production.selection.sessions.map((item) => item.session.id));
|
|
7166
|
-
return {
|
|
7167
|
-
production,
|
|
7168
|
-
families,
|
|
7169
|
-
work: {
|
|
7170
|
-
...work,
|
|
7171
|
-
rootSessionIds: roots.filter((id) => families.members.get(id)?.some((member) => admitted.has(member.id))),
|
|
7172
|
-
access: work.access === "private" ? "private" : "public"
|
|
7173
|
-
}
|
|
7174
|
-
};
|
|
7175
|
-
}
|
|
7176
|
-
//#endregion
|
|
7177
|
-
//#region apps/cli/src/journey/static/store.ts
|
|
7178
|
-
/** Identity, length and complete digest of one owner-only file; a symlink is never followed. */
|
|
7179
|
-
async function describe(path, signal) {
|
|
7180
|
-
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
7181
|
-
try {
|
|
7182
|
-
const before = await handle.stat({ bigint: true });
|
|
7183
|
-
if (!before.isFile()) throw new ApplicationError("CORRUPTED_STATE");
|
|
7184
|
-
const digest = createHash("sha256");
|
|
7185
|
-
let byteLength = 0;
|
|
7186
|
-
const buffer = Buffer.allocUnsafe(1048576);
|
|
7187
|
-
for (;;) {
|
|
7188
|
-
signal.throwIfAborted();
|
|
7189
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.length, byteLength);
|
|
7190
|
-
if (!bytesRead) break;
|
|
7191
|
-
digest.update(buffer.subarray(0, bytesRead));
|
|
7192
|
-
byteLength += bytesRead;
|
|
7193
|
-
}
|
|
7194
|
-
const after = await handle.stat({ bigint: true });
|
|
7195
|
-
if (after.dev !== before.dev || after.ino !== before.ino || after.size !== before.size || after.mtimeNs !== before.mtimeNs || BigInt(byteLength) !== after.size) throw new ApplicationError("CORRUPTED_STATE");
|
|
7196
|
-
return {
|
|
7197
|
-
path,
|
|
7198
|
-
identity: {
|
|
7199
|
-
dev: String(after.dev),
|
|
7200
|
-
ino: String(after.ino)
|
|
7201
|
-
},
|
|
7202
|
-
sha256: digest.digest("hex"),
|
|
7203
|
-
byteLength
|
|
7204
|
-
};
|
|
7205
|
-
} finally {
|
|
7206
|
-
await handle.close();
|
|
7207
|
-
}
|
|
7208
|
-
}
|
|
7209
|
-
/** A stored file must still be the exact recorded file and bytes. */
|
|
7210
|
-
async function authenticate(file, signal) {
|
|
7211
|
-
const actual = await describe(file.path, signal);
|
|
7212
|
-
if (JSON.stringify(actual) !== JSON.stringify(file)) throw new ApplicationError("CORRUPTED_STATE");
|
|
7213
|
-
}
|
|
7214
|
-
/**
|
|
7215
|
-
* Persist one Track B session as owner-only private state beside the journey's other scan
|
|
7216
|
-
* files: its three spools and a digest-bound result description. Nothing here is uploaded.
|
|
7217
|
-
*/
|
|
7218
|
-
async function saveStaticSession(storage, binding, item, result, signal) {
|
|
7219
|
-
const owned = [
|
|
7220
|
-
result.spoolPath,
|
|
7221
|
-
result.companionSpoolPath,
|
|
7222
|
-
result.maskSpoolPath
|
|
7223
|
-
];
|
|
7224
|
-
const resultPath = join(binding.directory, `${randomUUID()}.static.json`);
|
|
7225
|
-
let saved = false;
|
|
7226
|
-
try {
|
|
7227
|
-
if (result.kind !== "render" || result.maskSpoolPath === void 0 || result.maskEntries === void 0 || result.metadataMasks === void 0 || result.reviewSpoolPath !== void 0) throw new ApplicationError("CORRUPTED_STATE");
|
|
7228
|
-
const stored = {
|
|
7229
|
-
kind: "render",
|
|
7230
|
-
processingMs: result.processingMs,
|
|
7231
|
-
maskEntries: result.maskEntries,
|
|
7232
|
-
metadataMasks: result.metadataMasks,
|
|
7233
|
-
metadata: result.metadata,
|
|
7234
|
-
parts: result.parts,
|
|
7235
|
-
entries: result.entries,
|
|
7236
|
-
agentSetup: result.agentSetup
|
|
7237
|
-
};
|
|
7238
|
-
const file = await open(resultPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 384);
|
|
7239
|
-
owned.push(resultPath);
|
|
7240
|
-
try {
|
|
7241
|
-
await file.writeFile(JSON.stringify(stored));
|
|
7242
|
-
await file.sync();
|
|
7243
|
-
} finally {
|
|
7244
|
-
await file.close();
|
|
7245
|
-
}
|
|
7246
|
-
const resource = staticSessionResourceSchema.parse({
|
|
7247
|
-
kind: "static-session",
|
|
7248
|
-
journeyId: binding.journeyId,
|
|
7249
|
-
workId: binding.work.id,
|
|
7250
|
-
inputRevision: binding.work.inputRevision,
|
|
7251
|
-
snapshotId: binding.snapshotId,
|
|
7252
|
-
sessionId: item.sessionId,
|
|
7253
|
-
jobDigest: item.digest,
|
|
7254
|
-
result: await describe(resultPath, signal),
|
|
7255
|
-
primary: await describe(result.spoolPath, signal),
|
|
7256
|
-
companion: await describe(result.companionSpoolPath, signal),
|
|
7257
|
-
masks: await describe(result.maskSpoolPath, signal)
|
|
7258
|
-
});
|
|
7259
|
-
await storage.saveResource(resource);
|
|
7260
|
-
saved = true;
|
|
7261
|
-
return resource;
|
|
7262
|
-
} finally {
|
|
7263
|
-
if (!saved) {
|
|
7264
|
-
for (const path of owned) if (path !== void 0) await unlink(path).catch(() => {});
|
|
7265
|
-
}
|
|
7266
|
-
}
|
|
7267
|
-
}
|
|
7268
|
-
/** Current-generation Track B receipts of one Work, keyed by application session ID. */
|
|
7269
|
-
async function staticSessions(storage, journeyId, work) {
|
|
7270
|
-
const resources = (await storage.resources({
|
|
7271
|
-
journeyId,
|
|
7272
|
-
kind: "static-session",
|
|
7273
|
-
idPrefix: `${work.id}:${work.inputRevision}:`
|
|
7274
|
-
})).filter((item) => item.kind === "static-session" && item.workId === work.id && item.inputRevision === work.inputRevision);
|
|
7275
|
-
return new Map(resources.map((item) => [item.sessionId, item]));
|
|
7276
|
-
}
|
|
7277
|
-
/**
|
|
7278
|
-
* Shape guard like the worker transport's: the bytes are bound by the recorded digest, and the
|
|
7279
|
-
* archive writer checks parts, entries and masks again when it consumes them.
|
|
7280
|
-
*/
|
|
7281
|
-
function isStoredResult(value) {
|
|
7282
|
-
return typeof value === "object" && value !== null && Reflect.get(value, "kind") === "render" && Array.isArray(Reflect.get(value, "maskEntries")) && Array.isArray(Reflect.get(value, "metadataMasks")) && Array.isArray(Reflect.get(value, "parts")) && Array.isArray(Reflect.get(value, "entries")) && Array.isArray(Reflect.get(value, "agentSetup")) && typeof Reflect.get(value, "metadata") === "string" && Number.isSafeInteger(Reflect.get(value, "processingMs"));
|
|
7283
|
-
}
|
|
7284
|
-
/** Authenticated result description of one stored Track B session. */
|
|
7285
|
-
async function readStaticResult(resource, signal) {
|
|
7286
|
-
await authenticate(resource.result, signal);
|
|
7287
|
-
const handle = await open(resource.result.path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
7288
|
-
try {
|
|
7289
|
-
const text = await handle.readFile("utf8");
|
|
7290
|
-
if (createHash("sha256").update(text).digest("hex") !== resource.result.sha256) throw new ApplicationError("CORRUPTED_STATE");
|
|
7291
|
-
const value = JSON.parse(text);
|
|
7292
|
-
if (!isStoredResult(value)) throw new ApplicationError("CORRUPTED_STATE");
|
|
7293
|
-
return value;
|
|
7294
|
-
} finally {
|
|
7295
|
-
await handle.close();
|
|
7296
|
-
}
|
|
7297
|
-
}
|
|
7298
|
-
/**
|
|
7299
|
-
* Packaging from the Track B base: the stored result of exactly this job, with fresh hard links
|
|
7300
|
-
* to its authenticated spools that the archive writer may consume. Static detection never runs.
|
|
7301
|
-
*/
|
|
7302
|
-
async function linkedStaticResult(resource, job, signal) {
|
|
7303
|
-
if (!resource || resource.jobDigest !== job.digest) throw new ApplicationError("SCOPE_NOT_READY");
|
|
7304
|
-
const result = await readStaticResult(resource, signal);
|
|
7305
|
-
const links = [];
|
|
7306
|
-
try {
|
|
7307
|
-
const linked = async (file) => {
|
|
7308
|
-
const path = join(job.spoolDirectory, `${randomUUID()}.session`);
|
|
7309
|
-
await link(file.path, path);
|
|
7310
|
-
links.push(path);
|
|
7311
|
-
const stat = await lstat(path, { bigint: true });
|
|
7312
|
-
if (String(stat.dev) !== file.identity.dev || String(stat.ino) !== file.identity.ino) throw new ApplicationError("CORRUPTED_STATE");
|
|
7313
|
-
await authenticate({
|
|
7314
|
-
...file,
|
|
7315
|
-
path
|
|
7316
|
-
}, signal);
|
|
7317
|
-
return path;
|
|
7318
|
-
};
|
|
7319
|
-
return {
|
|
7320
|
-
...result,
|
|
7321
|
-
spoolPath: await linked(resource.primary),
|
|
7322
|
-
companionSpoolPath: await linked(resource.companion),
|
|
7323
|
-
maskSpoolPath: await linked(resource.masks)
|
|
7324
|
-
};
|
|
7325
|
-
} catch (error) {
|
|
7326
|
-
for (const path of links) await unlink(path).catch(() => {});
|
|
7327
|
-
throw error;
|
|
7328
|
-
}
|
|
7329
|
-
}
|
|
7330
|
-
//#endregion
|
|
7331
7071
|
//#region apps/cli/src/journey/static/run.ts
|
|
7332
7072
|
/**
|
|
7333
7073
|
* Track B for one Work generation (ADR 0172): render every admitted session with all built-in
|
|
@@ -7413,6 +7153,7 @@ async function prepareStaticWork(options) {
|
|
|
7413
7153
|
const target = current.works.find((item) => item.id === work.id);
|
|
7414
7154
|
if (current.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
|
|
7415
7155
|
if (current.snapshotId !== snapshotId || target?.inputRevision !== work.inputRevision) throw new ApplicationError("REVISION_CONFLICT");
|
|
7156
|
+
const images = resources.map((resource) => resource.imagesOmitted);
|
|
7416
7157
|
return {
|
|
7417
7158
|
...current,
|
|
7418
7159
|
works: current.works.map((item) => item.id === work.id ? {
|
|
@@ -7420,7 +7161,8 @@ async function prepareStaticWork(options) {
|
|
|
7420
7161
|
staticReview: {
|
|
7421
7162
|
snapshotId,
|
|
7422
7163
|
inputRevision: work.inputRevision,
|
|
7423
|
-
digest: staticReviewDigest(resources)
|
|
7164
|
+
digest: staticReviewDigest(resources),
|
|
7165
|
+
...images.every((count) => count !== void 0) ? { imagesOmitted: images.reduce((sum, count) => sum + count, 0) } : {}
|
|
7424
7166
|
}
|
|
7425
7167
|
} : item)
|
|
7426
7168
|
};
|
|
@@ -7657,8 +7399,8 @@ function createJourneyPreparation(options) {
|
|
|
7657
7399
|
}));
|
|
7658
7400
|
if (!state.works.length) throw new ApplicationError("INVALID_SELECTION");
|
|
7659
7401
|
if (!scanRequested) await captureSortingScope(storage, journeyId, workApproval(plan.approval, state.works.flatMap((work) => work.rootSessionIds)), directory, abort.signal);
|
|
7660
|
-
const
|
|
7661
|
-
if (
|
|
7402
|
+
const scanWorks = scanRequested && state.scope.status === "confirmed" ? state.works : [];
|
|
7403
|
+
if (scanWorks.length) run = await runWorkers(options.workers);
|
|
7662
7404
|
const frozen = state;
|
|
7663
7405
|
const blockWork = (work, error) => changeJourney(storage, journeyId, "snapshot", (before) => {
|
|
7664
7406
|
if (before.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
|
|
@@ -7672,8 +7414,8 @@ function createJourneyPreparation(options) {
|
|
|
7672
7414
|
});
|
|
7673
7415
|
}, 100);
|
|
7674
7416
|
let submittedWorks = 0;
|
|
7675
|
-
|
|
7676
|
-
|
|
7417
|
+
let works = [];
|
|
7418
|
+
let submitted = Promise.withResolvers();
|
|
7677
7419
|
const submission = () => {
|
|
7678
7420
|
let done = false;
|
|
7679
7421
|
return () => {
|
|
@@ -7694,6 +7436,14 @@ function createJourneyPreparation(options) {
|
|
|
7694
7436
|
return false;
|
|
7695
7437
|
}
|
|
7696
7438
|
};
|
|
7439
|
+
/** D2: the person dropped this Work while it was prepared; the other Works go on. */
|
|
7440
|
+
const droppedMidScan = async (workId) => {
|
|
7441
|
+
const now = await storage.current(journeyId);
|
|
7442
|
+
const work = now?.works.find((item) => item.id === workId);
|
|
7443
|
+
return Boolean(now && now.phase !== "paused" && now.scope.status === "confirmed" && work && !isSelectedWork(work));
|
|
7444
|
+
};
|
|
7445
|
+
/** Scan generations this run prepared, including those its own isolation started. */
|
|
7446
|
+
const prepared = /* @__PURE__ */ new Set();
|
|
7697
7447
|
/** True when failing roots left the Work, which is then prepared again (ADR 0188). */
|
|
7698
7448
|
const prepareWorkOnce = async (planned, onSubmitted) => {
|
|
7699
7449
|
abort.signal.throwIfAborted();
|
|
@@ -7707,6 +7457,7 @@ function createJourneyPreparation(options) {
|
|
|
7707
7457
|
"reading",
|
|
7708
7458
|
"review"
|
|
7709
7459
|
].includes(work.status)) return false;
|
|
7460
|
+
prepared.add(`${work.id}:${work.inputRevision}`);
|
|
7710
7461
|
await prepareClassificationMetadata(storage, latest, work, options.processing, abort.signal);
|
|
7711
7462
|
let failures = collectPreparationFailures(plan.approval.inventory, work.rootSessionIds, {
|
|
7712
7463
|
journeyId,
|
|
@@ -7723,6 +7474,7 @@ function createJourneyPreparation(options) {
|
|
|
7723
7474
|
onTiming: (timing) => options.onTiming?.(planned.id, timing)
|
|
7724
7475
|
}, run.workers, (revised) => {
|
|
7725
7476
|
work = revised;
|
|
7477
|
+
prepared.add(`${revised.id}:${revised.inputRevision}`);
|
|
7726
7478
|
failures = collectPreparationFailures(plan.approval.inventory, revised.rootSessionIds, {
|
|
7727
7479
|
journeyId,
|
|
7728
7480
|
snapshotId: latest.snapshotId,
|
|
@@ -7735,7 +7487,10 @@ function createJourneyPreparation(options) {
|
|
|
7735
7487
|
const code = safeCode(error);
|
|
7736
7488
|
if (code !== "OPERATION_CANCELED") options.onFailure(error, failures.read());
|
|
7737
7489
|
abort.signal.throwIfAborted();
|
|
7738
|
-
if (code === "OPERATION_CANCELED")
|
|
7490
|
+
if (code === "OPERATION_CANCELED") {
|
|
7491
|
+
if (await droppedMidScan(planned.id)) return false;
|
|
7492
|
+
throw error;
|
|
7493
|
+
}
|
|
7739
7494
|
if (work && await isolateRoots(work, error)) return true;
|
|
7740
7495
|
if (work) await blockWork(work, error);
|
|
7741
7496
|
}
|
|
@@ -7745,14 +7500,14 @@ function createJourneyPreparation(options) {
|
|
|
7745
7500
|
for (let attempt = 0; attempt < planned.rootSessionIds.length; attempt++) if (!await prepareWorkOnce(planned, onSubmitted)) return;
|
|
7746
7501
|
};
|
|
7747
7502
|
/** Track B for one Work; its failure blocks that Work like a Track A failure would. */
|
|
7748
|
-
const staticWork = async (planned) => {
|
|
7749
|
-
await
|
|
7503
|
+
const staticWork = async (planned, passSubmitted) => {
|
|
7504
|
+
await passSubmitted;
|
|
7750
7505
|
abort.signal.throwIfAborted();
|
|
7751
7506
|
const latest = await storage.current(journeyId);
|
|
7752
7507
|
abort.signal.throwIfAborted();
|
|
7753
7508
|
if (!latest?.consent || latest.phase === "paused" || latest.scope.status !== "confirmed") throw new ApplicationError("OPERATION_CANCELED");
|
|
7754
7509
|
const work = latest.works.find((item) => item.id === planned.id);
|
|
7755
|
-
if (!work) return;
|
|
7510
|
+
if (!work || !isSelectedWork(work)) return;
|
|
7756
7511
|
try {
|
|
7757
7512
|
await inBackgroundStoreLane(() => prepareStaticWork({
|
|
7758
7513
|
storage,
|
|
@@ -7767,6 +7522,7 @@ function createJourneyPreparation(options) {
|
|
|
7767
7522
|
} catch (error) {
|
|
7768
7523
|
abort.signal.throwIfAborted();
|
|
7769
7524
|
const code = safeCode(error);
|
|
7525
|
+
if (code === "OPERATION_CANCELED" && await droppedMidScan(work.id)) return;
|
|
7770
7526
|
if (code === "COORDINATION_BUSY" || code === "OPERATION_CANCELED") throw error;
|
|
7771
7527
|
options.onFailure(error);
|
|
7772
7528
|
await blockWork(work, error);
|
|
@@ -7779,23 +7535,50 @@ function createJourneyPreparation(options) {
|
|
|
7779
7535
|
abort.abort(error);
|
|
7780
7536
|
};
|
|
7781
7537
|
const statics = [];
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7538
|
+
const pass = async (batch) => {
|
|
7539
|
+
works = batch;
|
|
7540
|
+
next = 0;
|
|
7541
|
+
submittedWorks = 0;
|
|
7542
|
+
submitted = Promise.withResolvers();
|
|
7543
|
+
if (!works.length) submitted.resolve();
|
|
7544
|
+
const passSubmitted = submitted.promise;
|
|
7545
|
+
await Promise.all(Array.from({ length: run.pool ? works.length : Math.min(1, works.length) }, async () => {
|
|
7546
|
+
for (let work = works[next++]; work && !fatal; work = works[next++]) {
|
|
7547
|
+
const planned = work;
|
|
7548
|
+
const onSubmitted = submission();
|
|
7549
|
+
try {
|
|
7550
|
+
await retryContention(() => prepareWork(planned, onSubmitted), (error) => options.onFailure(error), abort.signal);
|
|
7551
|
+
} catch (error) {
|
|
7552
|
+
stop(error);
|
|
7553
|
+
} finally {
|
|
7554
|
+
onSubmitted();
|
|
7555
|
+
}
|
|
7556
|
+
statics.push(retryContention(() => staticWork(planned, passSubmitted), (error) => options.onFailure(error), abort.signal).catch(stop));
|
|
7792
7557
|
}
|
|
7793
|
-
|
|
7558
|
+
}));
|
|
7559
|
+
while (submittedWorks < works.length) submission()();
|
|
7560
|
+
};
|
|
7561
|
+
/** Selected Works this run has not prepared yet, waiting while grouping continues (E2). */
|
|
7562
|
+
const later = async () => {
|
|
7563
|
+
for (;;) {
|
|
7564
|
+
abort.signal.throwIfAborted();
|
|
7565
|
+
const latest = await storage.current(journeyId);
|
|
7566
|
+
if (!latest || latest.phase === "paused" || latest.scope.status !== "confirmed") return [];
|
|
7567
|
+
const ready = latest.works.filter((work) => isSelectedWork(work) && [
|
|
7568
|
+
"pending",
|
|
7569
|
+
"reading",
|
|
7570
|
+
"review"
|
|
7571
|
+
].includes(work.status) && !prepared.has(`${work.id}:${work.inputRevision}`));
|
|
7572
|
+
if (ready.length || !isClassifying(latest)) return ready;
|
|
7573
|
+
await setTimeout$1(laterWorksPollMs, void 0, { signal: abort.signal }).catch(() => abort.signal.throwIfAborted());
|
|
7794
7574
|
}
|
|
7795
|
-
}
|
|
7796
|
-
|
|
7575
|
+
};
|
|
7576
|
+
for (let batch = scanWorks; batch.length && !fatal; batch = await later()) {
|
|
7577
|
+
if (!run.workers && !run.pool) run = await runWorkers(options.workers);
|
|
7578
|
+
await pass(batch);
|
|
7579
|
+
}
|
|
7797
7580
|
await Promise.all(statics);
|
|
7798
|
-
if (
|
|
7581
|
+
if (scanWorks.length) options.onTiming?.(journeyId, {
|
|
7799
7582
|
scope: "preparation",
|
|
7800
7583
|
phases: { "journey-prepare": Math.round(performance$1.now() - startedAt) },
|
|
7801
7584
|
volumes: {
|
|
@@ -7820,6 +7603,8 @@ function createJourneyPreparation(options) {
|
|
|
7820
7603
|
}
|
|
7821
7604
|
};
|
|
7822
7605
|
}
|
|
7606
|
+
/** How often a run waiting for grouping to publish more Works checks the journey (E2). */
|
|
7607
|
+
const laterWorksPollMs = 2e3;
|
|
7823
7608
|
/**
|
|
7824
7609
|
* Another process can hold the store past one admission deadline (ADR 0082); the Work runs
|
|
7825
7610
|
* again after a bounded backoff instead of stopping the pass. A pending Work re-prepares
|
|
@@ -7863,7 +7648,7 @@ async function runWorkers(workers) {
|
|
|
7863
7648
|
//#endregion
|
|
7864
7649
|
//#region apps/cli/src/journey/package-base.ts
|
|
7865
7650
|
/** Package-only staging from the complete Track B base; scanning never calls this. */
|
|
7866
|
-
async function packageBase(storage, state, work, processing, directory, signal, readState = () => storage.current(state.journeyId)) {
|
|
7651
|
+
async function packageBase(storage, state, work, processing, directory, signal, readState = () => storage.current(state.journeyId), write = writeJourneyBase) {
|
|
7867
7652
|
assertJourneyPrivacyReady(state, true);
|
|
7868
7653
|
const existing = (await storage.resources({
|
|
7869
7654
|
journeyId: state.journeyId,
|
|
@@ -7871,53 +7656,18 @@ async function packageBase(storage, state, work, processing, directory, signal,
|
|
|
7871
7656
|
id: `${work.id}:${work.inputRevision}`
|
|
7872
7657
|
})).find((item) => item.kind === "base" && item.workId === work.id && item.inputRevision === work.inputRevision);
|
|
7873
7658
|
if (existing?.kind === "base") return existing;
|
|
7874
|
-
const
|
|
7875
|
-
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
|
|
7879
|
-
const prepared = await staticBaseProduction(storage, state, work, processing, hooks);
|
|
7880
|
-
if (!prepared) throw new ApplicationError("ARTIFACT_EMPTY_SCOPE");
|
|
7881
|
-
const { families } = prepared;
|
|
7882
|
-
const stored = await staticSessions(storage, state.journeyId, work);
|
|
7883
|
-
const written = await writeLogicalJourneyBase({
|
|
7884
|
-
production: prepared.production,
|
|
7659
|
+
const inputs = await staticBaseInputs(storage, state, work);
|
|
7660
|
+
if (!inputs) throw new ApplicationError("ARTIFACT_EMPTY_SCOPE");
|
|
7661
|
+
const base = await write({
|
|
7662
|
+
inputs,
|
|
7663
|
+
stored: [...(await staticSessions(storage, state.journeyId, work)).values()],
|
|
7885
7664
|
directory,
|
|
7886
|
-
|
|
7887
|
-
|
|
7888
|
-
prerendered: async (job) => {
|
|
7889
|
-
if (job.spoolDirectory === void 0) throw new ApplicationError("CORRUPTED_STATE");
|
|
7890
|
-
return linkedStaticResult(stored.get(job.session.id), {
|
|
7891
|
-
digest: renderJobDigest(job),
|
|
7892
|
-
spoolDirectory: job.spoolDirectory
|
|
7893
|
-
}, signal);
|
|
7894
|
-
}
|
|
7895
|
-
}, hooks);
|
|
7665
|
+
processing
|
|
7666
|
+
}, signal);
|
|
7896
7667
|
const after = await readState();
|
|
7897
7668
|
const current = after?.works.find((item) => item.id === work.id);
|
|
7898
7669
|
if (!after || after.snapshotId !== state.snapshotId || after.configurationId !== state.configurationId || current?.revision !== work.revision || current.inputRevision !== work.inputRevision || after.pendingHideId) throw new ApplicationError("REVISION_CONFLICT");
|
|
7899
7670
|
assertJourneyPrivacyReady(after, true);
|
|
7900
|
-
const base = {
|
|
7901
|
-
kind: "base",
|
|
7902
|
-
journeyId: state.journeyId,
|
|
7903
|
-
workId: work.id,
|
|
7904
|
-
inputRevision: work.inputRevision,
|
|
7905
|
-
bundleId: written.bundleId,
|
|
7906
|
-
primary: written.primary,
|
|
7907
|
-
companion: written.companion,
|
|
7908
|
-
maskLedger: written.maskLedger,
|
|
7909
|
-
sourceBindings: [...written.sourceBindings],
|
|
7910
|
-
sessions: written.sessionBindings.map((binding) => {
|
|
7911
|
-
const root = families.owners.get(binding.applicationSessionId);
|
|
7912
|
-
if (!root) throw new ApplicationError("CORRUPTED_STATE");
|
|
7913
|
-
return {
|
|
7914
|
-
applicationSessionId: binding.applicationSessionId,
|
|
7915
|
-
archiveSessionId: binding.sessionId,
|
|
7916
|
-
rootSessionId: root.id,
|
|
7917
|
-
role: root.id === binding.applicationSessionId ? "main" : "worker"
|
|
7918
|
-
};
|
|
7919
|
-
})
|
|
7920
|
-
};
|
|
7921
7671
|
await storage.saveResource(base);
|
|
7922
7672
|
return base;
|
|
7923
7673
|
}
|
|
@@ -7931,12 +7681,21 @@ const priorities = [
|
|
|
7931
7681
|
];
|
|
7932
7682
|
//#endregion
|
|
7933
7683
|
//#region apps/cli/src/journey/keywords.ts
|
|
7684
|
+
/** The journey's frozen source plan; it is saved once and never rewritten. */
|
|
7685
|
+
async function readJourneyPlan(storage, journeyId) {
|
|
7686
|
+
const plan = (await storage.resources({
|
|
7687
|
+
journeyId,
|
|
7688
|
+
kind: "plan"
|
|
7689
|
+
}))[0];
|
|
7690
|
+
if (plan?.kind !== "plan") throw new ApplicationError("CORRUPTED_STATE");
|
|
7691
|
+
return plan;
|
|
7692
|
+
}
|
|
7934
7693
|
/**
|
|
7935
7694
|
* ADR 0172: the confirmed dictionary applied once, as the last step before the ZIP — custom
|
|
7936
7695
|
* terms, the snapshot-frozen Git seeds of the given sessions' workspaces (ADR 0169, never live
|
|
7937
7696
|
* Git) and AI-confirmed Hidden values. Neither scan track applies any of these.
|
|
7938
7697
|
*/
|
|
7939
|
-
async function finalKeywordDictionary(storage, state, sessionIds) {
|
|
7698
|
+
async function finalKeywordDictionary(storage, state, sessionIds, plan) {
|
|
7940
7699
|
if (!state.snapshotId) throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
7941
7700
|
const dictionary = /* @__PURE__ */ new Map();
|
|
7942
7701
|
const add = (term, kind, bounded) => {
|
|
@@ -7951,16 +7710,12 @@ async function finalKeywordDictionary(storage, state, sessionIds) {
|
|
|
7951
7710
|
const admitted = admitCustomTerm(value);
|
|
7952
7711
|
if (admitted) add(admitted.value, "private", false);
|
|
7953
7712
|
}
|
|
7954
|
-
const
|
|
7955
|
-
journeyId: state.journeyId,
|
|
7956
|
-
kind: "plan"
|
|
7957
|
-
}))[0];
|
|
7958
|
-
if (plan?.kind !== "plan") throw new ApplicationError("CORRUPTED_STATE");
|
|
7713
|
+
const { approval } = plan ?? await readJourneyPlan(storage, state.journeyId);
|
|
7959
7714
|
const sessions = new Set(sessionIds);
|
|
7960
7715
|
const roots = /* @__PURE__ */ new Map();
|
|
7961
|
-
for (const session of
|
|
7716
|
+
for (const session of approval.inventory.sessions) {
|
|
7962
7717
|
if (!sessions.has(session.id)) continue;
|
|
7963
|
-
const workspace =
|
|
7718
|
+
const workspace = approval.inventory.workspaces.find((item) => item.id === session.workspaceId);
|
|
7964
7719
|
if (!workspace) throw new ApplicationError("CORRUPTED_STATE");
|
|
7965
7720
|
const paths = workspace.path === null ? [] : [workspace.path];
|
|
7966
7721
|
roots.set(JSON.stringify(paths), paths);
|
|
@@ -8307,8 +8062,26 @@ function createRenderWorkers(workers, size) {
|
|
|
8307
8062
|
return pool.serial(() => writeJourneyOutput(work, signal));
|
|
8308
8063
|
}
|
|
8309
8064
|
};
|
|
8065
|
+
const writeBase = async (work, signal) => {
|
|
8066
|
+
const named = /* @__PURE__ */ new Set();
|
|
8067
|
+
try {
|
|
8068
|
+
const result = await pool.acquire().request({
|
|
8069
|
+
kind: "journey-base",
|
|
8070
|
+
job: journeyBaseJob(work)
|
|
8071
|
+
}, signal, { onSpool: (path) => named.add(path) });
|
|
8072
|
+
if (result.kind !== "journey-base") throw new Error("ARTIFACT_PROCESSING_FAILED");
|
|
8073
|
+
return result.base;
|
|
8074
|
+
} catch (error) {
|
|
8075
|
+
await removeNamedTemporarySpools(named).catch((cleanup) => workers.onFallback?.(cleanup));
|
|
8076
|
+
if (error instanceof SessionWorkerError) throw new ApplicationError(error.message, void 0, { cause: error });
|
|
8077
|
+
if (signal.aborted) throw error;
|
|
8078
|
+
workers.onFallback?.(error);
|
|
8079
|
+
return pool.serial(() => writeJourneyBase(work, signal));
|
|
8080
|
+
}
|
|
8081
|
+
};
|
|
8310
8082
|
return {
|
|
8311
8083
|
write,
|
|
8084
|
+
writeBase,
|
|
8312
8085
|
close: () => pool.discard()
|
|
8313
8086
|
};
|
|
8314
8087
|
}
|
|
@@ -8486,7 +8259,7 @@ function createJourneyRenderer(options) {
|
|
|
8486
8259
|
work
|
|
8487
8260
|
};
|
|
8488
8261
|
}
|
|
8489
|
-
async function produce(state, work, signal, write, session) {
|
|
8262
|
+
async function produce(state, work, signal, write, session, plan, writeBase) {
|
|
8490
8263
|
const { journeyId } = state;
|
|
8491
8264
|
const workId = work.id;
|
|
8492
8265
|
assertJourneyPrivacyReady(state, true);
|
|
@@ -8518,12 +8291,12 @@ function createJourneyRenderer(options) {
|
|
|
8518
8291
|
});
|
|
8519
8292
|
const next = moved.works.find((item) => item.id === workId);
|
|
8520
8293
|
if (!next) throw new ApplicationError("REVISION_CONFLICT");
|
|
8521
|
-
return produce(moved, next, signal, write, session);
|
|
8294
|
+
return produce(moved, next, signal, write, session, plan, writeBase);
|
|
8522
8295
|
}
|
|
8523
8296
|
if (!output) {
|
|
8524
8297
|
const directory = await journeyDirectory(options.stateDir, journeyId);
|
|
8525
|
-
await packageBase(storage, state, work, options.processing, directory, signal, () => session.current());
|
|
8526
|
-
const { base, tokens, confirmed, request } = await journeyOutputRequest(storage, state, work, options.processing, signal);
|
|
8298
|
+
await packageBase(storage, state, work, options.processing, directory, signal, () => session.current(), writeBase);
|
|
8299
|
+
const { base, tokens, confirmed, request } = await journeyOutputRequest(storage, state, work, options.processing, signal, await plan());
|
|
8527
8300
|
const { output: written, tokens: collected } = await write({
|
|
8528
8301
|
request: {
|
|
8529
8302
|
...request,
|
|
@@ -8614,10 +8387,10 @@ function createJourneyRenderer(options) {
|
|
|
8614
8387
|
};
|
|
8615
8388
|
});
|
|
8616
8389
|
}
|
|
8617
|
-
async function render(journeyId, workId, signal, write = writeJourneyOutput, session = directRenderSession(storage, journeyId)) {
|
|
8390
|
+
async function render(journeyId, workId, signal, write = writeJourneyOutput, session = directRenderSession(storage, journeyId), plan = () => readJourneyPlan(storage, journeyId), writeBase = writeJourneyBase) {
|
|
8618
8391
|
const { state, work } = await admit(workId, session);
|
|
8619
8392
|
try {
|
|
8620
|
-
return await produce(state, work, signal, write, session);
|
|
8393
|
+
return await produce(state, work, signal, write, session, plan, writeBase);
|
|
8621
8394
|
} catch (error) {
|
|
8622
8395
|
if (signal.aborted) throw error;
|
|
8623
8396
|
const renderFailure = {
|
|
@@ -8640,19 +8413,26 @@ function createJourneyRenderer(options) {
|
|
|
8640
8413
|
}
|
|
8641
8414
|
}
|
|
8642
8415
|
/**
|
|
8643
|
-
* A several-Work render writes each Work's output in packaging workers; state,
|
|
8644
|
-
* resource writes stay on this thread under the cross-process state lock
|
|
8645
|
-
* may be split into shards, so the pool is not limited to one
|
|
8416
|
+
* A several-Work render writes each Work's base and output in packaging workers; state,
|
|
8417
|
+
* database and resource writes stay on this thread under the cross-process state lock
|
|
8418
|
+
* (ADR 0181, ADR 0228). Any Work may be split into shards, so the pool is not limited to one
|
|
8419
|
+
* worker per Work (ADR 0199). A `workIds` call of one Work uses workers too, so a panel batch
|
|
8420
|
+
* never packages on the serving thread.
|
|
8646
8421
|
*/
|
|
8647
8422
|
async function renderScope(journeyId, works, signal) {
|
|
8648
8423
|
const workers = options.workers;
|
|
8649
8424
|
const size = workers ? workers.parallelism ?? renderWorkerCount() : 0;
|
|
8650
|
-
if (!workers || works <
|
|
8425
|
+
if (!workers || works < 1 || size < 1) return void 0;
|
|
8651
8426
|
const pool = createRenderWorkers(workers, size);
|
|
8652
8427
|
const session = batchedRenderSession(storage, journeyId);
|
|
8428
|
+
let plan;
|
|
8429
|
+
const readPlan = () => plan ??= readJourneyPlan(storage, journeyId).catch((error) => {
|
|
8430
|
+
plan = void 0;
|
|
8431
|
+
throw error;
|
|
8432
|
+
});
|
|
8653
8433
|
return {
|
|
8654
8434
|
concurrency: size + 1,
|
|
8655
|
-
render: (workId) => render(journeyId, workId, signal, pool.write, session),
|
|
8435
|
+
render: (workId) => render(journeyId, workId, signal, pool.write, session, readPlan, pool.writeBase),
|
|
8656
8436
|
close: pool.close
|
|
8657
8437
|
};
|
|
8658
8438
|
}
|
|
@@ -8662,7 +8442,7 @@ function createJourneyRenderer(options) {
|
|
|
8662
8442
|
};
|
|
8663
8443
|
}
|
|
8664
8444
|
/** Package-only native output request: the final keyword dictionary covers all retained fields. */
|
|
8665
|
-
async function journeyOutputRequest(storage, state, work, processingConfig, _signal) {
|
|
8445
|
+
async function journeyOutputRequest(storage, state, work, processingConfig, _signal, plan) {
|
|
8666
8446
|
const { journeyId } = state;
|
|
8667
8447
|
const workId = work.id;
|
|
8668
8448
|
const base = (await storage.resources({
|
|
@@ -8670,14 +8450,11 @@ async function journeyOutputRequest(storage, state, work, processingConfig, _sig
|
|
|
8670
8450
|
kind: "base",
|
|
8671
8451
|
id: `${workId}:${work.inputRevision}`
|
|
8672
8452
|
})).find((item) => item.kind === "base" && item.workId === workId && item.inputRevision === work.inputRevision);
|
|
8673
|
-
|
|
8674
|
-
|
|
8675
|
-
kind: "plan"
|
|
8676
|
-
}))[0];
|
|
8677
|
-
if (base?.kind !== "base" || plan?.kind !== "plan") throw new ApplicationError("CORRUPTED_STATE");
|
|
8453
|
+
if (base?.kind !== "base") throw new ApplicationError("CORRUPTED_STATE");
|
|
8454
|
+
const { replacementKey } = plan ?? await readJourneyPlan(storage, journeyId);
|
|
8678
8455
|
const hides = createFamilyHides();
|
|
8679
8456
|
const confirmed = await journeyConfirmedHides(storage, state);
|
|
8680
|
-
const dictionary = await finalKeywordDictionary(storage, state, base.sessions.map((session) => session.applicationSessionId));
|
|
8457
|
+
const dictionary = await finalKeywordDictionary(storage, state, base.sessions.map((session) => session.applicationSessionId), plan);
|
|
8681
8458
|
const exceptions = workExceptions(state, workId);
|
|
8682
8459
|
const tokens = createHideIndexTokens();
|
|
8683
8460
|
const title = await renderTitle(storage, state, work);
|
|
@@ -8692,7 +8469,7 @@ async function journeyOutputRequest(storage, state, work, processingConfig, _sig
|
|
|
8692
8469
|
companion: base.companion,
|
|
8693
8470
|
workId,
|
|
8694
8471
|
baseBundleId: base.bundleId,
|
|
8695
|
-
replacementKey
|
|
8472
|
+
replacementKey,
|
|
8696
8473
|
edits: [],
|
|
8697
8474
|
families: hides.request(base.sessions, dictionary, exceptions),
|
|
8698
8475
|
metadata: {
|
|
@@ -9411,73 +9188,73 @@ function createClassificationReader(storage, stateDir) {
|
|
|
9411
9188
|
}
|
|
9412
9189
|
//#endregion
|
|
9413
9190
|
//#region apps/cli/src/journey/works/regroup.ts
|
|
9414
|
-
/**
|
|
9415
|
-
|
|
9416
|
-
|
|
9191
|
+
/**
|
|
9192
|
+
* The session of every accepted finding chunk outside the proposal's retained Works, read from
|
|
9193
|
+
* frozen batches before the commit so the store transaction can rebind them without I/O.
|
|
9194
|
+
*/
|
|
9195
|
+
async function resolveChunkSessions(storage, state, works, signal) {
|
|
9417
9196
|
const retained = new Set(works.map((work) => work.id));
|
|
9418
9197
|
const batches = /* @__PURE__ */ new Map();
|
|
9419
|
-
const
|
|
9420
|
-
for (const term of state.privacy.terms) {
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
}
|
|
9432
|
-
let batch = batches.get(source.batchId);
|
|
9433
|
-
if (!batch) {
|
|
9434
|
-
const record = await storage.inspectReview({
|
|
9435
|
-
kind: "attempt",
|
|
9436
|
-
journeyId: state.journeyId,
|
|
9437
|
-
batchId: source.batchId
|
|
9438
|
-
});
|
|
9439
|
-
if (!record || record.batch.snapshotId !== state.snapshotId) throw new ApplicationError("CORRUPTED_STATE");
|
|
9440
|
-
batch = record.batch;
|
|
9441
|
-
if (batches.size >= 16) batches.clear();
|
|
9442
|
-
batches.set(source.batchId, batch);
|
|
9443
|
-
}
|
|
9444
|
-
const chunk = batch.chunks.find((item) => item.id === source.chunkId) ?? batch.duplicates?.find((item) => item.id === source.chunkId);
|
|
9445
|
-
const workId = chunk && owners.get(chunk.sessionId);
|
|
9446
|
-
if (!workId) throw new ApplicationError("CORRUPTED_STATE");
|
|
9447
|
-
sources.push({
|
|
9448
|
-
...source,
|
|
9449
|
-
workId
|
|
9198
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
9199
|
+
for (const term of state.privacy.terms) for (const source of term.sources) {
|
|
9200
|
+
signal.throwIfAborted();
|
|
9201
|
+
if (source.kind === "metadata" || retained.has(source.workId)) continue;
|
|
9202
|
+
const key = `${source.batchId}:${source.chunkId}`;
|
|
9203
|
+
if (resolved.has(key)) continue;
|
|
9204
|
+
let batch = batches.get(source.batchId);
|
|
9205
|
+
if (!batch) {
|
|
9206
|
+
const record = await storage.inspectReview({
|
|
9207
|
+
kind: "attempt",
|
|
9208
|
+
journeyId: state.journeyId,
|
|
9209
|
+
batchId: source.batchId
|
|
9450
9210
|
});
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9211
|
+
if (!record || record.batch.snapshotId !== state.snapshotId) throw new ApplicationError("CORRUPTED_STATE");
|
|
9212
|
+
batch = record.batch;
|
|
9213
|
+
if (batches.size >= 16) batches.clear();
|
|
9214
|
+
batches.set(source.batchId, batch);
|
|
9215
|
+
}
|
|
9216
|
+
const chunk = batch.chunks.find((item) => item.id === source.chunkId) ?? batch.duplicates?.find((item) => item.id === source.chunkId);
|
|
9217
|
+
if (!chunk) throw new ApplicationError("CORRUPTED_STATE");
|
|
9218
|
+
resolved.set(key, {
|
|
9219
|
+
batchId: source.batchId,
|
|
9220
|
+
chunkId: source.chunkId,
|
|
9221
|
+
sessionId: chunk.sessionId
|
|
9455
9222
|
});
|
|
9456
9223
|
}
|
|
9457
|
-
return
|
|
9224
|
+
return [...resolved.values()];
|
|
9458
9225
|
}
|
|
9459
9226
|
//#endregion
|
|
9460
9227
|
//#region apps/cli/src/journey/works/proposal.ts
|
|
9461
|
-
/**
|
|
9228
|
+
/**
|
|
9229
|
+
* One atomic root partition; recommendations have no access authority. Roots still being grouped
|
|
9230
|
+
* wait in one unread Work while the other Works are confirmed and read; the submission that
|
|
9231
|
+
* groups them may arrive after confirmation and keeps it (E2). The proposal is prepared from one
|
|
9232
|
+
* read and applied by the store against the current state, so scan writes in between do not
|
|
9233
|
+
* refuse it (BL-216).
|
|
9234
|
+
*/
|
|
9462
9235
|
function createClassificationProposal(storage) {
|
|
9463
9236
|
return async (input, signal) => {
|
|
9464
9237
|
signal.throwIfAborted();
|
|
9465
9238
|
const state = await storage.current(input.journeyId);
|
|
9466
9239
|
if (!state?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
9467
|
-
const
|
|
9240
|
+
const classifying = isClassifying(state);
|
|
9241
|
+
const late = state.scope.status === "confirmed";
|
|
9242
|
+
const bound = input.expectedClassificationRevision !== void 0 && classifying && input.expectedClassificationRevision === state.classificationRevision;
|
|
9243
|
+
const requestHash = textDigest(JSON.stringify(input));
|
|
9244
|
+
const event = {
|
|
9245
|
+
journeyId: state.journeyId,
|
|
9246
|
+
kind: "response",
|
|
9247
|
+
source: "conversation",
|
|
9248
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9249
|
+
};
|
|
9250
|
+
if (input.expectedClassificationRevision !== void 0 ? !bound : state.revision !== input.expectedRevision) return storage.commit({
|
|
9468
9251
|
expectedRevision: input.expectedRevision,
|
|
9469
9252
|
requestKey: input.idempotencyKey,
|
|
9470
|
-
requestHash
|
|
9471
|
-
state
|
|
9472
|
-
event
|
|
9473
|
-
journeyId: state.journeyId,
|
|
9474
|
-
kind: "response",
|
|
9475
|
-
source: "conversation",
|
|
9476
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9477
|
-
}
|
|
9253
|
+
requestHash,
|
|
9254
|
+
state,
|
|
9255
|
+
event
|
|
9478
9256
|
});
|
|
9479
|
-
if (
|
|
9480
|
-
if (state.scope.status === "confirmed") throw new ApplicationError("INVALID_TRANSITION");
|
|
9257
|
+
if (late && !classifying) throw new ApplicationError("INVALID_TRANSITION");
|
|
9481
9258
|
if (state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
|
|
9482
9259
|
if (!state.snapshotId || state.pendingHideId) throw new ApplicationError("SCOPE_NOT_READY");
|
|
9483
9260
|
const plan = (await storage.resources({
|
|
@@ -9503,6 +9280,11 @@ function createClassificationProposal(storage) {
|
|
|
9503
9280
|
});
|
|
9504
9281
|
} else merged.set(key, item);
|
|
9505
9282
|
}
|
|
9283
|
+
const pending = input.pendingRootSessionIds ?? [];
|
|
9284
|
+
for (const root of pending) {
|
|
9285
|
+
if (!roots.has(root) || seen.has(root)) throw new ApplicationError("INVALID_SELECTION");
|
|
9286
|
+
seen.add(root);
|
|
9287
|
+
}
|
|
9506
9288
|
if (seen.size !== roots.size) throw new ApplicationError("INVALID_SELECTION");
|
|
9507
9289
|
const families = reviewFamilies(plan.approval.inventory);
|
|
9508
9290
|
const captures = await storage.resources({
|
|
@@ -9510,75 +9292,43 @@ function createClassificationProposal(storage) {
|
|
|
9510
9292
|
kind: "capture"
|
|
9511
9293
|
});
|
|
9512
9294
|
const captured = new Set(captures.flatMap((resource) => resource.kind === "capture" ? [resource.capture.partId] : []));
|
|
9513
|
-
const
|
|
9295
|
+
const workspacesOf = (rootSessionIds) => {
|
|
9296
|
+
const sessions = rootSessionIds.flatMap((root) => families.members.get(root) ?? []);
|
|
9297
|
+
if (sessions.some((session) => session.parts.some((part) => !captured.has(part.id)))) throw new ApplicationError("SCOPE_NOT_READY");
|
|
9298
|
+
return [...new Set(sessions.map((session) => session.workspaceId))];
|
|
9299
|
+
};
|
|
9300
|
+
const items = [];
|
|
9514
9301
|
for (const item of merged.values()) {
|
|
9515
9302
|
signal.throwIfAborted();
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
works.push({
|
|
9521
|
-
...original,
|
|
9522
|
-
projectName: item.projectName,
|
|
9523
|
-
title: base.titleOrigin === "explicit" ? base.title : item.projectName,
|
|
9524
|
-
description: item.description,
|
|
9525
|
-
projectType: {
|
|
9526
|
-
type: item.projectType,
|
|
9527
|
-
reason: item.recommendation.reason,
|
|
9528
|
-
origin: "model"
|
|
9529
|
-
},
|
|
9530
|
-
placement: "placed",
|
|
9531
|
-
classificationRecommendation: item.recommendation,
|
|
9532
|
-
suggestion: null,
|
|
9533
|
-
artifact: null,
|
|
9534
|
-
revision: base.revision + 1
|
|
9303
|
+
items.push({
|
|
9304
|
+
...item,
|
|
9305
|
+
workId: randomUUID(),
|
|
9306
|
+
workspaceIds: workspacesOf(item.rootSessionIds)
|
|
9535
9307
|
});
|
|
9536
9308
|
}
|
|
9537
|
-
const
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
const
|
|
9556
|
-
const
|
|
9557
|
-
|
|
9558
|
-
...
|
|
9559
|
-
|
|
9560
|
-
}));
|
|
9561
|
-
return commit({
|
|
9562
|
-
...state,
|
|
9563
|
-
works,
|
|
9564
|
-
classificationRevision: state.revision + 1,
|
|
9565
|
-
scope: {
|
|
9566
|
-
status: "pending",
|
|
9567
|
-
actor: null,
|
|
9568
|
-
at: null
|
|
9569
|
-
},
|
|
9570
|
-
privacy: {
|
|
9571
|
-
...state.privacy,
|
|
9572
|
-
terms,
|
|
9573
|
-
packageApproval: null,
|
|
9574
|
-
...regrouped ? {
|
|
9575
|
-
cards: [],
|
|
9576
|
-
answersRevision: state.privacy.answersRevision + 1,
|
|
9577
|
-
confirmedAnswersRevision: null
|
|
9578
|
-
} : {}
|
|
9579
|
-
},
|
|
9580
|
-
readingComplete: works.filter((work) => work.access !== "drop").every((work) => work.totalBatches > 0 && work.completedBatches === work.totalBatches),
|
|
9581
|
-
phase: "processing"
|
|
9309
|
+
const prepared = {
|
|
9310
|
+
journeyId: state.journeyId,
|
|
9311
|
+
requestKey: input.idempotencyKey,
|
|
9312
|
+
requestHash,
|
|
9313
|
+
at: event.at,
|
|
9314
|
+
expectedRevision: input.expectedRevision,
|
|
9315
|
+
expectedClassificationRevision: bound ? input.expectedClassificationRevision ?? null : null,
|
|
9316
|
+
snapshotId: state.snapshotId,
|
|
9317
|
+
partition: classificationPartition(state),
|
|
9318
|
+
late,
|
|
9319
|
+
items,
|
|
9320
|
+
pending: pending.length ? {
|
|
9321
|
+
workId: randomUUID(),
|
|
9322
|
+
rootSessionIds: pending,
|
|
9323
|
+
workspaceIds: [...new Set(pending.flatMap((root) => (families.members.get(root) ?? []).map((session) => session.workspaceId)))]
|
|
9324
|
+
} : null,
|
|
9325
|
+
chunkSessions: []
|
|
9326
|
+
};
|
|
9327
|
+
const { works } = classificationWorks(state, prepared);
|
|
9328
|
+
const chunkSessions = await resolveChunkSessions(storage, state, works, signal);
|
|
9329
|
+
return storage.commitClassification({
|
|
9330
|
+
...prepared,
|
|
9331
|
+
chunkSessions
|
|
9582
9332
|
});
|
|
9583
9333
|
};
|
|
9584
9334
|
}
|
|
@@ -10055,16 +9805,19 @@ function createJourneySendArtifacts(storage) {
|
|
|
10055
9805
|
sha256: output.primary.sha256
|
|
10056
9806
|
});
|
|
10057
9807
|
},
|
|
10058
|
-
async upload(journeyId, artifact,
|
|
9808
|
+
async upload(journeyId, artifact, upload, signal) {
|
|
10059
9809
|
const output = await find(journeyId, artifact.workId, artifact.revision);
|
|
10060
9810
|
if (output.bundleId !== artifact.bundleId || output.primary.sha256 !== artifact.sha256 || output.primary.archiveBytes !== artifact.byteLength || artifact.byteLength > 5368709120) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
|
|
10061
|
-
await putPrimary(output.primary, artifact.byteLength,
|
|
9811
|
+
await putPrimary(output.primary, artifact.byteLength, upload, signal);
|
|
10062
9812
|
}
|
|
10063
9813
|
};
|
|
10064
9814
|
}
|
|
10065
|
-
/**
|
|
10066
|
-
|
|
10067
|
-
|
|
9815
|
+
/**
|
|
9816
|
+
* Stream a verified primary to a signed HTTPS URL; shared by native Send and Manual upload. A
|
|
9817
|
+
* checksum upload also sends the SHA-256 header the URL signs, so storage refuses other bytes.
|
|
9818
|
+
*/
|
|
9819
|
+
async function putPrimary(primary, byteLength, upload, signal) {
|
|
9820
|
+
const url = new URL(upload.uploadUrl);
|
|
10068
9821
|
if (url.protocol !== "https:" || url.username || url.password || url.hash) throw new ApplicationError("INVALID_ARGUMENTS");
|
|
10069
9822
|
const source = await openConfiguredUpload(primary, signal);
|
|
10070
9823
|
const cleanup = createCleanup();
|
|
@@ -10076,7 +9829,8 @@ async function putPrimary(primary, byteLength, uploadUrl, signal) {
|
|
|
10076
9829
|
redirect: "error",
|
|
10077
9830
|
headers: {
|
|
10078
9831
|
"Content-Type": "application/zip",
|
|
10079
|
-
"Content-Length": String(byteLength)
|
|
9832
|
+
"Content-Length": String(byteLength),
|
|
9833
|
+
...upload.headers
|
|
10080
9834
|
},
|
|
10081
9835
|
duplex: "half"
|
|
10082
9836
|
};
|
|
@@ -10246,7 +10000,7 @@ const completeTimeoutMs = 6e5;
|
|
|
10246
10000
|
function createJourneyOwnerClient(options) {
|
|
10247
10001
|
const base = apiBase(options.apiUrl);
|
|
10248
10002
|
const requestFetch = options.fetch ?? fetch;
|
|
10249
|
-
async function request(path, schema, signal, body, method = "POST", key, timeoutMs = requestTimeoutMs$2) {
|
|
10003
|
+
async function request(path, schema, signal, body, method = "POST", key, timeoutMs = requestTimeoutMs$2, checksumUpload = false) {
|
|
10250
10004
|
const token = await options.authorize();
|
|
10251
10005
|
signal.throwIfAborted();
|
|
10252
10006
|
const response = await requestFetch(new URL(path, base), {
|
|
@@ -10255,7 +10009,8 @@ function createJourneyOwnerClient(options) {
|
|
|
10255
10009
|
accept: "application/json",
|
|
10256
10010
|
authorization: `Bearer ${token}`,
|
|
10257
10011
|
...body === void 0 ? {} : { "content-type": "application/json" },
|
|
10258
|
-
...key === void 0 ? {} : { "idempotency-key": key }
|
|
10012
|
+
...key === void 0 ? {} : { "idempotency-key": key },
|
|
10013
|
+
...checksumUpload ? { "upload-checksum": "sha256" } : {}
|
|
10259
10014
|
},
|
|
10260
10015
|
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
10261
10016
|
credentials: "omit",
|
|
@@ -10310,14 +10065,14 @@ function createJourneyOwnerClient(options) {
|
|
|
10310
10065
|
membersDigest: preview.membersDigest
|
|
10311
10066
|
}, "PUT");
|
|
10312
10067
|
},
|
|
10313
|
-
reserve(key, input, signal) {
|
|
10314
|
-
return request("/me/work-bundles/uploads", sendReceiptSchema.refine((value) => receiptMatches(input, value)), signal, input, "POST", key);
|
|
10068
|
+
reserve(key, input, signal, checksumUpload = true) {
|
|
10069
|
+
return request("/me/work-bundles/uploads", sendReceiptSchema.refine((value) => receiptMatches(input, value)), signal, input, "POST", key, requestTimeoutMs$2, checksumUpload);
|
|
10315
10070
|
},
|
|
10316
10071
|
complete(id, signal) {
|
|
10317
10072
|
return request(`/me/work-bundles/uploads/${encodeURIComponent(id)}/complete`, receipt(id), signal, {}, "POST", void 0, completeTimeoutMs);
|
|
10318
10073
|
},
|
|
10319
|
-
status(id, signal) {
|
|
10320
|
-
return request(`/me/work-bundles/uploads/${encodeURIComponent(id)}`, receipt(id), signal, void 0, "GET");
|
|
10074
|
+
status(id, signal, checksumUpload = true) {
|
|
10075
|
+
return request(`/me/work-bundles/uploads/${encodeURIComponent(id)}`, receipt(id), signal, void 0, "GET", void 0, requestTimeoutMs$2, checksumUpload);
|
|
10321
10076
|
},
|
|
10322
10077
|
async replace(keepWorkIds, signal) {
|
|
10323
10078
|
if (keepWorkIds.length === 0) throw new ApplicationError("INVALID_STATE");
|
|
@@ -10507,16 +10262,72 @@ async function perform(context, item, connection, signal) {
|
|
|
10507
10262
|
const auto = context.snapshot === true;
|
|
10508
10263
|
await guard();
|
|
10509
10264
|
if (item.settings && item.settingsConfirmed) return applySettings(item, connection, signal, guard, auto);
|
|
10510
|
-
if (!item.reservation)
|
|
10511
|
-
|
|
10512
|
-
|
|
10265
|
+
if (!item.reservation) {
|
|
10266
|
+
const next = await resolveItem(item, context.bindingId, connection, signal, auto, context.name);
|
|
10267
|
+
if (next.reservation) context.fresh?.set(item.workId, null);
|
|
10268
|
+
return next;
|
|
10269
|
+
}
|
|
10270
|
+
if (!item.receipt) {
|
|
10271
|
+
const reserved = await connection.reserve(item.operationKey, item.reservation, signal, context.checksumUploads !== false);
|
|
10272
|
+
const next = await observe(item, reserved, connection, signal, auto);
|
|
10273
|
+
if (context.fresh?.has(item.workId) && reserved.upload && next.status === "uploading") context.fresh.set(item.workId, {
|
|
10274
|
+
receiptId: reserved.receiptId,
|
|
10275
|
+
upload: reserved.upload
|
|
10276
|
+
});
|
|
10277
|
+
return next;
|
|
10278
|
+
}
|
|
10279
|
+
const fresh = context.fresh?.get(item.workId);
|
|
10280
|
+
context.fresh?.delete(item.workId);
|
|
10281
|
+
if (fresh?.receiptId === item.receipt.receiptId) return put(context, item, fresh.receiptId, fresh.upload, connection, signal, guard);
|
|
10282
|
+
const receipt = await connection.status(item.receipt.receiptId, signal, context.checksumUploads !== false);
|
|
10513
10283
|
const observed = await observe(item, receipt, connection, signal, auto);
|
|
10514
|
-
if (receipt.state !== "awaiting_upload") return observed;
|
|
10284
|
+
if (receipt.state !== "awaiting_upload") return withoutUpload(observed);
|
|
10515
10285
|
if (!receipt.upload) throw new ApplicationError("INVALID_STATE");
|
|
10286
|
+
if (item.uploadedReceiptId === receipt.receiptId) {
|
|
10287
|
+
await guard();
|
|
10288
|
+
try {
|
|
10289
|
+
return withoutUpload(await observe(item, await connection.complete(receipt.receiptId, signal), connection, signal, auto));
|
|
10290
|
+
} catch (error) {
|
|
10291
|
+
if (!(error instanceof JourneyRemoteError) || error.status === 401 || [
|
|
10292
|
+
"settings_changed",
|
|
10293
|
+
"target_conflict",
|
|
10294
|
+
"work_removed"
|
|
10295
|
+
].includes(error.code)) throw error;
|
|
10296
|
+
return withoutUpload(item);
|
|
10297
|
+
}
|
|
10298
|
+
}
|
|
10299
|
+
const current = await connection.resolve(item.reservation.work, signal);
|
|
10300
|
+
if (current.scopeVersion !== item.reservation.expectedScopeVersion || current.accessVersion !== item.reservation.expectedAccessVersion) return renewed(item, null);
|
|
10301
|
+
return put(context, item, receipt.receiptId, receipt.upload, connection, signal, guard);
|
|
10302
|
+
}
|
|
10303
|
+
/** Uploads the bytes to the receipt's signed URL and completes it. */
|
|
10304
|
+
async function put(context, item, receiptId, upload, connection, signal, guard) {
|
|
10516
10305
|
await guard();
|
|
10517
|
-
await context.upload(item.artifact,
|
|
10306
|
+
await context.upload(item.artifact, upload, signal);
|
|
10518
10307
|
await guard();
|
|
10519
|
-
|
|
10308
|
+
try {
|
|
10309
|
+
return observe(item, await connection.complete(receiptId, signal), connection, signal, context.snapshot === true);
|
|
10310
|
+
} catch (error) {
|
|
10311
|
+
if (upload.headers !== void 0 && error instanceof JourneyRemoteError && error.code === "bundle_mismatch") return {
|
|
10312
|
+
...renewed(withoutUpload(item), null),
|
|
10313
|
+
checksumUploadRefused: true
|
|
10314
|
+
};
|
|
10315
|
+
if (error instanceof Object) uploaded.set(error, {
|
|
10316
|
+
...item,
|
|
10317
|
+
uploadedReceiptId: receiptId
|
|
10318
|
+
});
|
|
10319
|
+
throw error;
|
|
10320
|
+
}
|
|
10321
|
+
}
|
|
10322
|
+
const uploaded = /* @__PURE__ */ new WeakMap();
|
|
10323
|
+
/** The item to save for a failed step whose bytes were already uploaded, if any. */
|
|
10324
|
+
function uploadedItem(error) {
|
|
10325
|
+
return error instanceof Object ? uploaded.get(error) : void 0;
|
|
10326
|
+
}
|
|
10327
|
+
function withoutUpload(item) {
|
|
10328
|
+
if (item.uploadedReceiptId === void 0) return item;
|
|
10329
|
+
const { uploadedReceiptId: _, ...rest } = item;
|
|
10330
|
+
return rest;
|
|
10520
10331
|
}
|
|
10521
10332
|
/** A definite stale refusal can renew the intent; unknown outcomes always retry the same key. */
|
|
10522
10333
|
async function stepItem(context, item, connection, signal) {
|
|
@@ -10546,31 +10357,41 @@ async function stepItem(context, item, connection, signal) {
|
|
|
10546
10357
|
status: "blocked",
|
|
10547
10358
|
error: "WORK_REMOVED"
|
|
10548
10359
|
};
|
|
10549
|
-
return
|
|
10550
|
-
...item,
|
|
10551
|
-
operationKey: randomUUID(),
|
|
10552
|
-
reservation: null,
|
|
10553
|
-
resolution: null,
|
|
10554
|
-
previousReceipts: item.receipt ? [...item.previousReceipts, item.receipt] : item.previousReceipts,
|
|
10555
|
-
receipt: null,
|
|
10556
|
-
status: "ready",
|
|
10557
|
-
error: "SETTINGS_CHANGED"
|
|
10558
|
-
};
|
|
10360
|
+
return renewed(item, "SETTINGS_CHANGED");
|
|
10559
10361
|
}
|
|
10560
10362
|
}
|
|
10363
|
+
/** A fresh intent for the same Work: the old receipt is kept as history and resolve runs again. */
|
|
10364
|
+
function renewed(item, error) {
|
|
10365
|
+
return {
|
|
10366
|
+
...withoutUpload(item),
|
|
10367
|
+
operationKey: randomUUID(),
|
|
10368
|
+
reservation: null,
|
|
10369
|
+
resolution: null,
|
|
10370
|
+
previousReceipts: item.receipt ? [...item.previousReceipts, item.receipt] : item.previousReceipts,
|
|
10371
|
+
receipt: null,
|
|
10372
|
+
status: "ready",
|
|
10373
|
+
error
|
|
10374
|
+
};
|
|
10375
|
+
}
|
|
10561
10376
|
const confirmedSettings = (item) => item.settings !== null && item.settingsConfirmed;
|
|
10562
10377
|
/**
|
|
10563
|
-
* The Works one advance steps together.
|
|
10564
|
-
*
|
|
10565
|
-
*
|
|
10378
|
+
* The Works one advance steps together. Upload work comes before assessment checks, smallest
|
|
10379
|
+
* ZIP first. Completing
|
|
10380
|
+
* an upload advances the owner's scope version, which every resolution and reservation carries,
|
|
10381
|
+
* so one Work's resolve-to-complete steps never overlap another's: uploads step one Work at a
|
|
10382
|
+
* time (ADR 0214). Assessment checks change nothing and step up to three together; a confirmed
|
|
10383
|
+
* linked Settings change alters access versions, so it always steps alone.
|
|
10566
10384
|
*/
|
|
10567
|
-
function pickBatch(items) {
|
|
10568
|
-
const uploading = items.
|
|
10569
|
-
|
|
10570
|
-
const
|
|
10385
|
+
function pickBatch(items, now = Date.now()) {
|
|
10386
|
+
const uploading = items.find((item) => item.status === "uploading") ?? items.filter((item) => item.status === "ready").reduce((smallest, item) => !smallest || item.artifact.byteLength < smallest.artifact.byteLength ? item : smallest, void 0);
|
|
10387
|
+
if (uploading) return [uploading];
|
|
10388
|
+
const assessing = items.filter((item) => item.status === "assessing");
|
|
10389
|
+
const first = assessing[0];
|
|
10571
10390
|
if (!first) return [];
|
|
10572
10391
|
if (confirmedSettings(first)) return [first];
|
|
10573
|
-
|
|
10392
|
+
const checks = assessing.filter((item) => !confirmedSettings(item));
|
|
10393
|
+
const start = Math.floor(now / 1e3) * 3 % checks.length;
|
|
10394
|
+
return [...checks.slice(start), ...checks.slice(0, start)].slice(0, 3);
|
|
10574
10395
|
}
|
|
10575
10396
|
function isCode(error, code) {
|
|
10576
10397
|
return error instanceof ApplicationError && error.code === code;
|
|
@@ -10664,7 +10485,11 @@ async function advanceItems(host, signal) {
|
|
|
10664
10485
|
if (finished !== null) return finished;
|
|
10665
10486
|
}
|
|
10666
10487
|
if (!batch.length) return host.applied();
|
|
10667
|
-
const
|
|
10488
|
+
const checksumUploads = !send.items.some((item) => item.checksumUploadRefused);
|
|
10489
|
+
const results = await Promise.allSettled(batch.map((item) => stepItem({
|
|
10490
|
+
...host.context(item),
|
|
10491
|
+
checksumUploads
|
|
10492
|
+
}, item, connection, signal)));
|
|
10668
10493
|
const failures = results.flatMap((result) => result.status === "rejected" ? [result] : []);
|
|
10669
10494
|
if (failures.some(({ reason }) => isCode(reason, "REVISION_CONFLICT"))) return host.stale();
|
|
10670
10495
|
const first = failures[0];
|
|
@@ -10681,9 +10506,11 @@ async function advanceItems(host, signal) {
|
|
|
10681
10506
|
continue;
|
|
10682
10507
|
}
|
|
10683
10508
|
const cause = result.reason;
|
|
10509
|
+
const progressed = uploadedItem(cause);
|
|
10510
|
+
if (progressed) next.set(item.workId, progressed);
|
|
10684
10511
|
if (cause instanceof JourneyRemoteError && cause.status === 401 || isCode(cause, "ACCOUNT_SIGNED_OUT")) signIn = true;
|
|
10685
10512
|
else if (isCode(cause, "IDENTITY_REJECTED")) changed = true;
|
|
10686
|
-
else if (isInterrupted(cause)) {
|
|
10513
|
+
else if (isInterrupted(cause) || isTransientRefusal(cause)) {
|
|
10687
10514
|
host.failure("SEND_INTERRUPTED", cause);
|
|
10688
10515
|
error = "SEND_INTERRUPTED";
|
|
10689
10516
|
} else {
|
|
@@ -10717,7 +10544,7 @@ async function advanceItems(host, signal) {
|
|
|
10717
10544
|
items
|
|
10718
10545
|
}));
|
|
10719
10546
|
} catch (error) {
|
|
10720
|
-
if (!signal.aborted && (isInterrupted(error) || error instanceof Error && isInterrupted(error.cause))) {
|
|
10547
|
+
if (!signal.aborted && (isInterrupted(error) || error instanceof Error && isInterrupted(error.cause) || isCode(error, "IDENTITY_UNAVAILABLE"))) {
|
|
10721
10548
|
host.failure("SEND_INTERRUPTED", error);
|
|
10722
10549
|
return send.error === "SEND_INTERRUPTED" ? host.applied() : host.save({
|
|
10723
10550
|
...send,
|
|
@@ -10791,8 +10618,9 @@ function withoutQuestions(send) {
|
|
|
10791
10618
|
};
|
|
10792
10619
|
return send.status === "review" ? aggregate(next) : next;
|
|
10793
10620
|
}
|
|
10794
|
-
async function advanceSend(dependencies, address, signal) {
|
|
10621
|
+
async function advanceSend(dependencies, address, signal, chain) {
|
|
10795
10622
|
const snapshot = requireSend(await dependencies.storage.loadSend(address.journeyId, address.sendId));
|
|
10623
|
+
if (chain) chain.revision = snapshot.send.revision;
|
|
10796
10624
|
if ((await dependencies.storage.loadSend(address.journeyId))?.send?.id !== snapshot.send.id) return {
|
|
10797
10625
|
...snapshot,
|
|
10798
10626
|
status: "stale"
|
|
@@ -10824,20 +10652,25 @@ async function advanceSend(dependencies, address, signal) {
|
|
|
10824
10652
|
return advanceItems({
|
|
10825
10653
|
send,
|
|
10826
10654
|
failure: dependencies.failure,
|
|
10827
|
-
connect: dependencies.auth.connect,
|
|
10655
|
+
connect: chain?.connect ?? dependencies.auth.connect,
|
|
10828
10656
|
profile: (connection, profileSignal) => prepareProfile(dependencies, snapshot, connection, profileSignal),
|
|
10829
10657
|
context: (item) => {
|
|
10830
10658
|
const binding = snapshot.journey.works.find((value) => value.id === item.workId)?.binding;
|
|
10831
10659
|
const retired = !item.receipt && binding?.sentBundleId === item.artifact.bundleId && binding.sentAccess === item.access;
|
|
10832
10660
|
return {
|
|
10833
10661
|
snapshot: true,
|
|
10662
|
+
...chain ? { fresh: chain.fresh } : {},
|
|
10834
10663
|
active: async () => {
|
|
10835
10664
|
const current = requireSend(await dependencies.storage.loadSend(snapshot.journey.journeyId));
|
|
10836
10665
|
if (current.send.id !== snapshot.send.id || !sameItem(current.send.items, item) || current.send.status === "paused" || current.journey.phase === "paused") throw new ApplicationError("REVISION_CONFLICT");
|
|
10837
10666
|
assertScope(current.journey, current.send);
|
|
10838
10667
|
},
|
|
10839
10668
|
bindingId: binding?.subject === send.subject && !retired ? binding.bundleRecordId : void 0,
|
|
10840
|
-
upload: (artifact,
|
|
10669
|
+
upload: async (artifact, upload, uploadSignal) => {
|
|
10670
|
+
const started = performance.now();
|
|
10671
|
+
await dependencies.artifacts.upload(snapshot.journey.journeyId, artifact, upload, uploadSignal);
|
|
10672
|
+
dependencies.uploaded?.(artifact.workId, artifact.byteLength, Math.round(performance.now() - started));
|
|
10673
|
+
}
|
|
10841
10674
|
};
|
|
10842
10675
|
},
|
|
10843
10676
|
finish: async (connection, finishSignal) => {
|
|
@@ -10881,6 +10714,14 @@ async function advanceSend(dependencies, address, signal) {
|
|
|
10881
10714
|
})
|
|
10882
10715
|
}, signal);
|
|
10883
10716
|
}
|
|
10717
|
+
/**
|
|
10718
|
+
* A refusal Passport answers while it or Identity is briefly unavailable. It says nothing about
|
|
10719
|
+
* the Work, so the step is retried instead of blocking it (the 2026-11-01 recorded run blocked a
|
|
10720
|
+
* Work on one `identity_unavailable`).
|
|
10721
|
+
*/
|
|
10722
|
+
function isTransientRefusal(error) {
|
|
10723
|
+
return error instanceof JourneyRemoteError && (error.status >= 500 || error.status === 429 || error.code === "identity_unavailable");
|
|
10724
|
+
}
|
|
10884
10725
|
//#endregion
|
|
10885
10726
|
//#region apps/cli/src/account/journey/decisions.ts
|
|
10886
10727
|
function decide(item, input) {
|
|
@@ -11151,10 +10992,10 @@ function createManualUploader(dependencies) {
|
|
|
11151
10992
|
active: guard(upload, item),
|
|
11152
10993
|
name: item.title,
|
|
11153
10994
|
bindingId: bindings.find((value) => value.subject === upload.subject && value.pathHash === pathHash(item))?.bundleRecordId,
|
|
11154
|
-
upload: (artifact,
|
|
10995
|
+
upload: (artifact, signedUpload, uploadSignal) => {
|
|
11155
10996
|
const slot = upload.slots[item.workId];
|
|
11156
10997
|
if (slot === void 0) throw new ApplicationError("ARTIFACT_MISSING");
|
|
11157
|
-
return dependencies.artifacts.upload(upload.resultId, slot, artifact,
|
|
10998
|
+
return dependencies.artifacts.upload(upload.resultId, slot, artifact, signedUpload, uploadSignal);
|
|
11158
10999
|
}
|
|
11159
11000
|
}),
|
|
11160
11001
|
applied: () => ({
|
|
@@ -11224,10 +11065,10 @@ function createManualUploadArtifacts(application) {
|
|
|
11224
11065
|
})
|
|
11225
11066
|
};
|
|
11226
11067
|
},
|
|
11227
|
-
async upload(resultId, slot, artifact,
|
|
11068
|
+
async upload(resultId, slot, artifact, upload, signal) {
|
|
11228
11069
|
const { row, primary } = await find(resultId, slot);
|
|
11229
11070
|
if (row.bundleId !== artifact.bundleId || primary.sha256 !== artifact.sha256 || primary.archiveBytes !== artifact.byteLength || artifact.byteLength > 5368709120) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
|
|
11230
|
-
await putPrimary(primary, artifact.byteLength,
|
|
11071
|
+
await putPrimary(primary, artifact.byteLength, upload, signal);
|
|
11231
11072
|
}
|
|
11232
11073
|
};
|
|
11233
11074
|
}
|
|
@@ -12390,13 +12231,42 @@ function createJourneyFiles(storage) {
|
|
|
12390
12231
|
};
|
|
12391
12232
|
};
|
|
12392
12233
|
}
|
|
12234
|
+
/**
|
|
12235
|
+
* One advance carries the Work being uploaded through its phases with one verified connection,
|
|
12236
|
+
* instead of one phase per panel round trip, and uploads a receipt it just resolved and reserved
|
|
12237
|
+
* without reading it again (ADR 0229). Each phase is still fenced and saved on its own, so Pause, a
|
|
12238
|
+
* revision change or a failure stops the chain at the next phase exactly as between advances.
|
|
12239
|
+
* The advance returns once that Work leaves its upload phases, so the next Work starts from the
|
|
12240
|
+
* caller's next advance as before.
|
|
12241
|
+
*/
|
|
12242
|
+
async function advanceWork(dependencies, address, signal, phases = 3) {
|
|
12243
|
+
let connection;
|
|
12244
|
+
const chain = {
|
|
12245
|
+
fresh: /* @__PURE__ */ new Map(),
|
|
12246
|
+
async connect(connectSignal) {
|
|
12247
|
+
if (connection === void 0) connection = await dependencies.auth.connect(connectSignal);
|
|
12248
|
+
return connection;
|
|
12249
|
+
}
|
|
12250
|
+
};
|
|
12251
|
+
let result = await advanceSend(dependencies, address, signal, chain);
|
|
12252
|
+
for (let phase = 1; phase < phases && uploading(result, chain.revision); phase += 1) result = await advanceSend(dependencies, address, signal, chain);
|
|
12253
|
+
return result;
|
|
12254
|
+
}
|
|
12255
|
+
/** The phase saved progress and left its Work mid-upload, with nothing else to report. */
|
|
12256
|
+
function uploading(result, before) {
|
|
12257
|
+
const send = result.send;
|
|
12258
|
+
return result.status === "applied" && send !== null && before !== void 0 && send.revision > before && send.status === "sending" && send.error === null && pickBatch(send.items)[0]?.status === "uploading";
|
|
12259
|
+
}
|
|
12393
12260
|
//#endregion
|
|
12394
12261
|
//#region apps/cli/src/account/journey/send.ts
|
|
12395
12262
|
function sameIds(left, right) {
|
|
12396
12263
|
return left.length === right.length && new Set(left).size === left.length && left.every((id) => right.includes(id));
|
|
12397
12264
|
}
|
|
12398
|
-
/**
|
|
12399
|
-
|
|
12265
|
+
/**
|
|
12266
|
+
* Durable, bounded orchestration; native chat and panel use the same commands. One advance runs
|
|
12267
|
+
* at most `phases` upload phases of one Work (ADR 0229); tests pass 1 to observe each phase.
|
|
12268
|
+
*/
|
|
12269
|
+
function createJourneySender(dependencies, phases = 3) {
|
|
12400
12270
|
const running = /* @__PURE__ */ new Map();
|
|
12401
12271
|
const runKey = (address) => `${address.journeyId}:${address.sendId}`;
|
|
12402
12272
|
const load = async (address) => {
|
|
@@ -12481,6 +12351,20 @@ function createJourneySender(dependencies) {
|
|
|
12481
12351
|
if (result.status !== "stale") running.get(runKey(input))?.stop.abort();
|
|
12482
12352
|
return result;
|
|
12483
12353
|
},
|
|
12354
|
+
/** BL-201: Cancel sign-in returns a confirmed Send that uploaded nothing to Ready to send. */
|
|
12355
|
+
async withdraw(raw) {
|
|
12356
|
+
const input = sendActionSchema.parse(raw);
|
|
12357
|
+
const snapshot = await load(input);
|
|
12358
|
+
if (!matchesAction(snapshot, input)) return saveSend(dependencies, snapshot, snapshot.send, input);
|
|
12359
|
+
if (snapshot.send.status !== "sign-in" || snapshot.send.items.some((item) => item.receipt)) throw new ApplicationError("INVALID_TRANSITION");
|
|
12360
|
+
const result = await saveSend(dependencies, snapshot, {
|
|
12361
|
+
...snapshot.send,
|
|
12362
|
+
status: "prepared",
|
|
12363
|
+
confirmedAt: null
|
|
12364
|
+
}, input);
|
|
12365
|
+
if (result.status !== "stale") running.get(runKey(input))?.stop.abort();
|
|
12366
|
+
return result;
|
|
12367
|
+
},
|
|
12484
12368
|
async resume(raw) {
|
|
12485
12369
|
const input = sendActionSchema.parse(raw);
|
|
12486
12370
|
const snapshot = await load(input);
|
|
@@ -12495,7 +12379,7 @@ function createJourneySender(dependencies) {
|
|
|
12495
12379
|
const current = running.get(key);
|
|
12496
12380
|
if (current) return current.result;
|
|
12497
12381
|
const stop = new AbortController();
|
|
12498
|
-
const result =
|
|
12382
|
+
const result = advanceWork(dependencies, address, AbortSignal.any([signal, stop.signal]), phases).catch(async (error) => {
|
|
12499
12383
|
if (!stop.signal.aborted || signal.aborted) throw error;
|
|
12500
12384
|
return {
|
|
12501
12385
|
...requireSend(await dependencies.storage.loadSend(address.journeyId, address.sendId)),
|
|
@@ -12512,6 +12396,10 @@ function createJourneySender(dependencies) {
|
|
|
12512
12396
|
}
|
|
12513
12397
|
//#endregion
|
|
12514
12398
|
//#region apps/cli/src/account/journey/auth.ts
|
|
12399
|
+
/** An access token this close to expiry is renewed first, so a step never starts on it. */
|
|
12400
|
+
const renewBeforeMs = 12e4;
|
|
12401
|
+
const renewTimeoutMs = 3e4;
|
|
12402
|
+
const renewals = /* @__PURE__ */ new WeakMap();
|
|
12515
12403
|
/** Reuses the existing device sign-in credential; a token is never returned to the panel or Agent. */
|
|
12516
12404
|
function createJourneySendAuth(account, requestFetch) {
|
|
12517
12405
|
/**
|
|
@@ -12528,31 +12416,87 @@ function createJourneySendAuth(account, requestFetch) {
|
|
|
12528
12416
|
return state;
|
|
12529
12417
|
});
|
|
12530
12418
|
}
|
|
12419
|
+
/** A refused access token with a refresh token is renewed on the next use instead (BL-210). */
|
|
12420
|
+
async function refuse(accessToken) {
|
|
12421
|
+
if (!(await account.credentials.update((value) => {
|
|
12422
|
+
if (value.credential?.accessToken === accessToken && value.credential.refreshToken) value.credential = {
|
|
12423
|
+
...value.credential,
|
|
12424
|
+
expiresAt: account.now()
|
|
12425
|
+
};
|
|
12426
|
+
return value;
|
|
12427
|
+
})).credential?.refreshToken) await expire(accessToken);
|
|
12428
|
+
}
|
|
12429
|
+
function renew(credential) {
|
|
12430
|
+
const running = renewals.get(account);
|
|
12431
|
+
if (running) return running;
|
|
12432
|
+
const renewing = (async () => {
|
|
12433
|
+
try {
|
|
12434
|
+
const token = await account.identity.refresh(credential.refreshToken, AbortSignal.timeout(renewTimeoutMs));
|
|
12435
|
+
if (!token) {
|
|
12436
|
+
await expire(credential.accessToken);
|
|
12437
|
+
return null;
|
|
12438
|
+
}
|
|
12439
|
+
return (await account.credentials.update((value) => {
|
|
12440
|
+
if (value.credential?.refreshToken === credential.refreshToken) value.credential = {
|
|
12441
|
+
...value.credential,
|
|
12442
|
+
accessToken: token.accessToken,
|
|
12443
|
+
refreshToken: token.refreshToken ?? credential.refreshToken,
|
|
12444
|
+
scope: token.scope,
|
|
12445
|
+
issuedAt: account.now(),
|
|
12446
|
+
expiresAt: token.expiresAt
|
|
12447
|
+
};
|
|
12448
|
+
return value;
|
|
12449
|
+
})).credential;
|
|
12450
|
+
} finally {
|
|
12451
|
+
renewals.delete(account);
|
|
12452
|
+
}
|
|
12453
|
+
})();
|
|
12454
|
+
renewals.set(account, renewing);
|
|
12455
|
+
return renewing;
|
|
12456
|
+
}
|
|
12531
12457
|
async function current() {
|
|
12532
12458
|
const credential = (await account.credentials.update((value) => value)).credential;
|
|
12533
12459
|
if (!credential || credential.identityUrl !== account.configuration.identityUrl || credential.clientId !== account.configuration.clientId) return null;
|
|
12534
|
-
|
|
12460
|
+
const left = Date.parse(credential.expiresAt) - Date.parse(account.now());
|
|
12461
|
+
if (Number.isFinite(left) && left > renewBeforeMs) return credential;
|
|
12462
|
+
const { refreshToken } = credential;
|
|
12463
|
+
if (!refreshToken) {
|
|
12464
|
+
if (Number.isFinite(left) && left > 0) return credential;
|
|
12535
12465
|
await expire(credential.accessToken);
|
|
12536
12466
|
return null;
|
|
12537
12467
|
}
|
|
12538
|
-
|
|
12468
|
+
try {
|
|
12469
|
+
return await renew({
|
|
12470
|
+
...credential,
|
|
12471
|
+
refreshToken
|
|
12472
|
+
});
|
|
12473
|
+
} catch (error) {
|
|
12474
|
+
if (Number.isFinite(left) && left > 0) return credential;
|
|
12475
|
+
throw error;
|
|
12476
|
+
}
|
|
12539
12477
|
}
|
|
12540
12478
|
const fetchOwner = requestFetch ?? fetch;
|
|
12541
12479
|
const ownerFetch = async (input, init) => {
|
|
12542
12480
|
const response = await fetchOwner(input, init);
|
|
12543
12481
|
if (response.status === 401) {
|
|
12544
12482
|
const header = new Headers(init?.headers).get("authorization");
|
|
12545
|
-
if (header?.startsWith("Bearer ")) await
|
|
12483
|
+
if (header?.startsWith("Bearer ")) await refuse(header.slice(7));
|
|
12546
12484
|
}
|
|
12547
12485
|
return response;
|
|
12548
12486
|
};
|
|
12549
12487
|
return { async connect(signal) {
|
|
12550
|
-
|
|
12488
|
+
let credential = await current();
|
|
12551
12489
|
if (!credential) return null;
|
|
12552
|
-
|
|
12490
|
+
let verified = await account.identity.readIdentity(credential.accessToken, signal);
|
|
12553
12491
|
if (!verified) {
|
|
12554
|
-
await
|
|
12555
|
-
|
|
12492
|
+
await refuse(credential.accessToken);
|
|
12493
|
+
credential = await current();
|
|
12494
|
+
if (!credential) return null;
|
|
12495
|
+
verified = await account.identity.readIdentity(credential.accessToken, signal);
|
|
12496
|
+
if (!verified) {
|
|
12497
|
+
await expire(credential.accessToken);
|
|
12498
|
+
return null;
|
|
12499
|
+
}
|
|
12556
12500
|
}
|
|
12557
12501
|
if (verified.identityId !== credential.identityId) throw new ApplicationError("IDENTITY_REJECTED");
|
|
12558
12502
|
return createJourneyOwnerClient({
|
|
@@ -13097,6 +13041,7 @@ const deviceCodeResponse = z.object({
|
|
|
13097
13041
|
});
|
|
13098
13042
|
const tokenResponse = z.object({
|
|
13099
13043
|
access_token: z.string().min(1).max(4096),
|
|
13044
|
+
refresh_token: z.string().min(1).max(4096).optional(),
|
|
13100
13045
|
token_type: z.string(),
|
|
13101
13046
|
expires_in: z.number().int().positive(),
|
|
13102
13047
|
scope: deviceProfileScopeSchema
|
|
@@ -13108,13 +13053,24 @@ const userInfoResponse = z.object({
|
|
|
13108
13053
|
picture: z.unknown().optional(),
|
|
13109
13054
|
profile: z.string()
|
|
13110
13055
|
});
|
|
13111
|
-
|
|
13112
|
-
|
|
13056
|
+
/**
|
|
13057
|
+
* `detail` names the endpoint and what failed (HTTP status, network error name, schema paths),
|
|
13058
|
+
* never a response value, so the authorization log can say why without exposing a token.
|
|
13059
|
+
*/
|
|
13060
|
+
function unavailable(detail, cause) {
|
|
13061
|
+
const error = Object.assign(new ApplicationError("IDENTITY_UNAVAILABLE"), { detail });
|
|
13113
13062
|
if (cause !== void 0) error.cause = cause;
|
|
13114
13063
|
return error;
|
|
13115
13064
|
}
|
|
13116
|
-
|
|
13117
|
-
|
|
13065
|
+
const issuePaths = (error) => error.issues.map((issue) => issue.path.join(".") || "(root)").join(",");
|
|
13066
|
+
function networkDetail(error) {
|
|
13067
|
+
if (!(error instanceof Error)) return "Error";
|
|
13068
|
+
const cause = error.cause;
|
|
13069
|
+
const code = typeof cause === "object" && cause !== null && "code" in cause ? cause.code : null;
|
|
13070
|
+
return typeof code === "string" ? `${error.name} ${code}` : error.name;
|
|
13071
|
+
}
|
|
13072
|
+
async function readJson(response, where) {
|
|
13073
|
+
if (!response.body) throw unavailable(`${where} empty body`);
|
|
13118
13074
|
const reader = response.body.getReader();
|
|
13119
13075
|
const chunks = [];
|
|
13120
13076
|
let bytes = 0;
|
|
@@ -13126,7 +13082,7 @@ async function readJson(response) {
|
|
|
13126
13082
|
bytes += next.value.byteLength;
|
|
13127
13083
|
if (bytes > maximumResponseBytes) {
|
|
13128
13084
|
await reader.cancel().catch(() => void 0);
|
|
13129
|
-
throw unavailable();
|
|
13085
|
+
throw unavailable(`${where} body too large`);
|
|
13130
13086
|
}
|
|
13131
13087
|
chunks.push(next.value);
|
|
13132
13088
|
}
|
|
@@ -13136,7 +13092,7 @@ async function readJson(response) {
|
|
|
13136
13092
|
try {
|
|
13137
13093
|
return JSON.parse(Buffer.concat(chunks, bytes).toString("utf8"));
|
|
13138
13094
|
} catch (error) {
|
|
13139
|
-
throw unavailable(error);
|
|
13095
|
+
throw unavailable(`${where} invalid JSON`, error);
|
|
13140
13096
|
}
|
|
13141
13097
|
}
|
|
13142
13098
|
function verificationUrl(value, identity) {
|
|
@@ -13144,9 +13100,9 @@ function verificationUrl(value, identity) {
|
|
|
13144
13100
|
try {
|
|
13145
13101
|
url = new URL(value);
|
|
13146
13102
|
} catch (error) {
|
|
13147
|
-
throw unavailable(error);
|
|
13103
|
+
throw unavailable("device-code verification URL invalid", error);
|
|
13148
13104
|
}
|
|
13149
|
-
if (!["http:", "https:"].includes(url.protocol) || url.origin !== identity.origin || url.username !== "" || url.password !== "") throw unavailable();
|
|
13105
|
+
if (!["http:", "https:"].includes(url.protocol) || url.origin !== identity.origin || url.username !== "" || url.password !== "") throw unavailable("device-code verification URL outside Identity");
|
|
13150
13106
|
return url;
|
|
13151
13107
|
}
|
|
13152
13108
|
/**
|
|
@@ -13157,7 +13113,7 @@ function createIdentityDeviceClient(options) {
|
|
|
13157
13113
|
const requestFetch = options.fetch ?? fetch;
|
|
13158
13114
|
const now = options.now ?? Date.now;
|
|
13159
13115
|
const base = new URL(options.identityUrl);
|
|
13160
|
-
if (!["http:", "https:"].includes(base.protocol)) throw unavailable();
|
|
13116
|
+
if (!["http:", "https:"].includes(base.protocol)) throw unavailable("identity URL protocol");
|
|
13161
13117
|
async function send(path, init, signal) {
|
|
13162
13118
|
try {
|
|
13163
13119
|
return await requestFetch(new URL(path, base), {
|
|
@@ -13167,9 +13123,18 @@ function createIdentityDeviceClient(options) {
|
|
|
13167
13123
|
});
|
|
13168
13124
|
} catch (error) {
|
|
13169
13125
|
if (signal.aborted) throw error;
|
|
13170
|
-
throw unavailable(error);
|
|
13126
|
+
throw unavailable(`${path} ${networkDetail(error)}`, error);
|
|
13171
13127
|
}
|
|
13172
13128
|
}
|
|
13129
|
+
function issued(token) {
|
|
13130
|
+
return {
|
|
13131
|
+
kind: "issued",
|
|
13132
|
+
accessToken: token.access_token,
|
|
13133
|
+
...token.refresh_token ? { refreshToken: token.refresh_token } : {},
|
|
13134
|
+
expiresAt: new Date(now() + token.expires_in * 1e3).toISOString(),
|
|
13135
|
+
scope: token.scope
|
|
13136
|
+
};
|
|
13137
|
+
}
|
|
13173
13138
|
function form(fields) {
|
|
13174
13139
|
return {
|
|
13175
13140
|
method: "POST",
|
|
@@ -13182,14 +13147,18 @@ function createIdentityDeviceClient(options) {
|
|
|
13182
13147
|
}
|
|
13183
13148
|
return {
|
|
13184
13149
|
async requestDeviceCode(signal) {
|
|
13185
|
-
|
|
13150
|
+
let response = await send("/api/auth/device/code", form({
|
|
13151
|
+
client_id: options.clientId,
|
|
13152
|
+
scope: "openid profile offline_access"
|
|
13153
|
+
}), signal);
|
|
13154
|
+
if (response.status === 400 && errorResponse.safeParse(await readJson(response, "device-code")).data?.error === "invalid_scope") response = await send("/api/auth/device/code", form({
|
|
13186
13155
|
client_id: options.clientId,
|
|
13187
13156
|
scope: "openid profile"
|
|
13188
13157
|
}), signal);
|
|
13189
13158
|
if (response.status === 400 || response.status === 401) throw new ApplicationError("IDENTITY_REJECTED");
|
|
13190
|
-
if (response.status !== 200) throw unavailable();
|
|
13191
|
-
const parsed = deviceCodeResponse.safeParse(await readJson(response));
|
|
13192
|
-
if (!parsed.success) throw unavailable(parsed.error);
|
|
13159
|
+
if (response.status !== 200) throw unavailable(`device-code HTTP ${response.status}`);
|
|
13160
|
+
const parsed = deviceCodeResponse.safeParse(await readJson(response, "device-code"));
|
|
13161
|
+
if (!parsed.success) throw unavailable(`device-code schema ${issuePaths(parsed.error)}`, parsed.error);
|
|
13193
13162
|
const verification = verificationUrl(parsed.data.verification_uri, base);
|
|
13194
13163
|
const complete = verificationUrl(parsed.data.verification_uri_complete, base);
|
|
13195
13164
|
return {
|
|
@@ -13208,20 +13177,16 @@ function createIdentityDeviceClient(options) {
|
|
|
13208
13177
|
grant_type: deviceCodeGrantType
|
|
13209
13178
|
}), signal);
|
|
13210
13179
|
if (response.status === 200) {
|
|
13211
|
-
const parsed = tokenResponse.safeParse(await readJson(response));
|
|
13212
|
-
if (!parsed.success
|
|
13213
|
-
|
|
13214
|
-
|
|
13215
|
-
accessToken: parsed.data.access_token,
|
|
13216
|
-
expiresAt: new Date(now() + parsed.data.expires_in * 1e3).toISOString(),
|
|
13217
|
-
scope: parsed.data.scope
|
|
13218
|
-
};
|
|
13180
|
+
const parsed = tokenResponse.safeParse(await readJson(response, "token"));
|
|
13181
|
+
if (!parsed.success) throw unavailable(`token schema ${issuePaths(parsed.error)}`, parsed.error);
|
|
13182
|
+
if (parsed.data.token_type.toLowerCase() !== "bearer") throw unavailable("token type not bearer");
|
|
13183
|
+
return issued(parsed.data);
|
|
13219
13184
|
}
|
|
13220
13185
|
if (response.status !== 400) {
|
|
13221
13186
|
if (response.status === 401) throw new ApplicationError("IDENTITY_REJECTED");
|
|
13222
|
-
throw unavailable();
|
|
13187
|
+
throw unavailable(`token HTTP ${response.status}`);
|
|
13223
13188
|
}
|
|
13224
|
-
const parsed = errorResponse.safeParse(await readJson(response));
|
|
13189
|
+
const parsed = errorResponse.safeParse(await readJson(response, "token"));
|
|
13225
13190
|
const code = parsed.success ? parsed.data.error : "";
|
|
13226
13191
|
if (code === "authorization_pending") return { kind: "pending" };
|
|
13227
13192
|
if (code === "slow_down") return { kind: "slow_down" };
|
|
@@ -13229,6 +13194,19 @@ function createIdentityDeviceClient(options) {
|
|
|
13229
13194
|
if (code === "access_denied") return { kind: "denied" };
|
|
13230
13195
|
throw new ApplicationError("IDENTITY_REJECTED");
|
|
13231
13196
|
},
|
|
13197
|
+
async refresh(refreshToken, signal) {
|
|
13198
|
+
const response = await send("/api/auth/oauth2/token", form({
|
|
13199
|
+
client_id: options.clientId,
|
|
13200
|
+
grant_type: "refresh_token",
|
|
13201
|
+
refresh_token: refreshToken
|
|
13202
|
+
}), signal);
|
|
13203
|
+
if (response.status === 400 || response.status === 401) return null;
|
|
13204
|
+
if (response.status !== 200) throw unavailable(`refresh HTTP ${response.status}`);
|
|
13205
|
+
const parsed = tokenResponse.safeParse(await readJson(response, "refresh"));
|
|
13206
|
+
if (!parsed.success) throw unavailable(`refresh schema ${issuePaths(parsed.error)}`, parsed.error);
|
|
13207
|
+
if (parsed.data.token_type.toLowerCase() !== "bearer") throw unavailable("token type not bearer");
|
|
13208
|
+
return issued(parsed.data);
|
|
13209
|
+
},
|
|
13232
13210
|
async readIdentity(accessToken, signal) {
|
|
13233
13211
|
const response = await send("/api/auth/oauth2/userinfo", {
|
|
13234
13212
|
method: "GET",
|
|
@@ -13238,17 +13216,17 @@ function createIdentityDeviceClient(options) {
|
|
|
13238
13216
|
}
|
|
13239
13217
|
}, signal);
|
|
13240
13218
|
if (response.status === 401 || response.status === 403) return null;
|
|
13241
|
-
if (response.status !== 200) throw unavailable();
|
|
13242
|
-
const parsed = userInfoResponse.safeParse(await readJson(response));
|
|
13243
|
-
if (!parsed.success) throw unavailable(parsed.error);
|
|
13219
|
+
if (response.status !== 200) throw unavailable(`userinfo HTTP ${response.status}`);
|
|
13220
|
+
const parsed = userInfoResponse.safeParse(await readJson(response, "userinfo"));
|
|
13221
|
+
if (!parsed.success) throw unavailable(`userinfo schema ${issuePaths(parsed.error)}`, parsed.error);
|
|
13244
13222
|
const profile = accountProfileSchema.safeParse({
|
|
13245
13223
|
identityId: parsed.data.sub,
|
|
13246
13224
|
displayName: parsed.data.name,
|
|
13247
13225
|
avatarUrl: readAccountAvatarUrl(parsed.data.picture),
|
|
13248
13226
|
profileUrl: parsed.data.profile
|
|
13249
13227
|
});
|
|
13250
|
-
if (!profile.success) throw unavailable(profile.error);
|
|
13251
|
-
if (!profileUrlMatchesIdentity(profile.data.profileUrl, options.identityUrl)) throw unavailable();
|
|
13228
|
+
if (!profile.success) throw unavailable(`userinfo profile ${issuePaths(profile.error)}`, profile.error);
|
|
13229
|
+
if (!profileUrlMatchesIdentity(profile.data.profileUrl, options.identityUrl)) throw unavailable("userinfo profile URL outside Identity");
|
|
13252
13230
|
return profile.data;
|
|
13253
13231
|
}
|
|
13254
13232
|
};
|
|
@@ -13367,6 +13345,8 @@ function errorDetail(error) {
|
|
|
13367
13345
|
if (typeof code === "number" && Number.isFinite(code)) detail.code = code;
|
|
13368
13346
|
const syscall = data(value, "syscall");
|
|
13369
13347
|
if (typeof syscall === "string") detail.syscall = text(syscall);
|
|
13348
|
+
const reason = data(value, "detail");
|
|
13349
|
+
if (typeof reason === "string") detail.detail = text(reason);
|
|
13370
13350
|
const cause = data(value, "cause");
|
|
13371
13351
|
if (cause !== void 0) detail.cause = visit(cause, depth + 1);
|
|
13372
13352
|
const errors = data(value, "errors");
|
|
@@ -13849,6 +13829,61 @@ function createConfiguredPreviewPort(workerUrl, environment) {
|
|
|
13849
13829
|
}) };
|
|
13850
13830
|
}
|
|
13851
13831
|
//#endregion
|
|
13832
|
+
//#region apps/cli/src/inspection/index.ts
|
|
13833
|
+
/**
|
|
13834
|
+
* Runs each inspection scan in its own Worker so static detection never holds the host's event
|
|
13835
|
+
* loop, which also serves requests and store accesses (BL-199).
|
|
13836
|
+
*/
|
|
13837
|
+
function createInspectionPort(workerUrl, environment, onAnomaly) {
|
|
13838
|
+
return { async *scan(request, signal, onFailure) {
|
|
13839
|
+
const worker = new Worker(workerUrl, { workerData: {
|
|
13840
|
+
request,
|
|
13841
|
+
environment
|
|
13842
|
+
} });
|
|
13843
|
+
const events = [];
|
|
13844
|
+
let wake;
|
|
13845
|
+
const push = (event) => {
|
|
13846
|
+
events.push(event);
|
|
13847
|
+
wake?.();
|
|
13848
|
+
};
|
|
13849
|
+
const fail = (cause) => push({ error: new Error("INSPECTION_WORKER_FAILED", { cause }) });
|
|
13850
|
+
worker.on("message", (message) => {
|
|
13851
|
+
const parsed = workerMessageSchema.safeParse(message);
|
|
13852
|
+
if (!parsed.success) return fail(parsed.error);
|
|
13853
|
+
const value = parsed.data;
|
|
13854
|
+
try {
|
|
13855
|
+
if (value.kind === "batch") push({ batch: validateInspectionBatch(value.batch) });
|
|
13856
|
+
else if (value.kind === "done") push({ done: true });
|
|
13857
|
+
else if (value.kind === "anomaly") onAnomaly?.(value.event);
|
|
13858
|
+
else onFailure?.(value.stage, Object.assign(new Error(value.message), value.code ? { code: value.code } : {}));
|
|
13859
|
+
} catch (error) {
|
|
13860
|
+
fail(error);
|
|
13861
|
+
}
|
|
13862
|
+
});
|
|
13863
|
+
worker.on("error", fail);
|
|
13864
|
+
worker.on("exit", (code) => fail(/* @__PURE__ */ new Error(`Inspection Worker exited: code=${code}`)));
|
|
13865
|
+
const abort = () => worker.postMessage({ kind: "abort" });
|
|
13866
|
+
if (signal.aborted) abort();
|
|
13867
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
13868
|
+
try {
|
|
13869
|
+
for (;;) {
|
|
13870
|
+
const event = events.shift();
|
|
13871
|
+
if (!event) {
|
|
13872
|
+
await new Promise((resolve) => wake = resolve);
|
|
13873
|
+
wake = void 0;
|
|
13874
|
+
continue;
|
|
13875
|
+
}
|
|
13876
|
+
if ("done" in event) return;
|
|
13877
|
+
if ("error" in event) throw event.error;
|
|
13878
|
+
yield event.batch;
|
|
13879
|
+
}
|
|
13880
|
+
} finally {
|
|
13881
|
+
signal.removeEventListener("abort", abort);
|
|
13882
|
+
await worker.terminate();
|
|
13883
|
+
}
|
|
13884
|
+
} };
|
|
13885
|
+
}
|
|
13886
|
+
//#endregion
|
|
13852
13887
|
//#region apps/cli/src/bundle-directory.ts
|
|
13853
13888
|
/** The platform cache folder; Windows has none, so its default library is the launch directory. */
|
|
13854
13889
|
function cacheRoot(options) {
|
|
@@ -14080,7 +14115,14 @@ async function compose(options, callbacks, bundles) {
|
|
|
14080
14115
|
artifacts: createJourneySendArtifacts(journeyStorage),
|
|
14081
14116
|
auth: createJourneySendAuth(accountPorts),
|
|
14082
14117
|
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
14083
|
-
failure: (code, cause) => diagnostics.onFailure(code, cause)
|
|
14118
|
+
failure: (code, cause) => diagnostics.onFailure(code, cause),
|
|
14119
|
+
uploaded: (workId, bytes, elapsedMs) => diagnostics.onActivity({
|
|
14120
|
+
event: "journey.send_upload_timing",
|
|
14121
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14122
|
+
operationId: workId,
|
|
14123
|
+
phases: { upload: elapsedMs },
|
|
14124
|
+
volumes: { bytes }
|
|
14125
|
+
})
|
|
14084
14126
|
}),
|
|
14085
14127
|
manualUploader: createManualUploader({
|
|
14086
14128
|
storage: store.manualUpload,
|
|
@@ -14152,10 +14194,7 @@ async function compose(options, callbacks, bundles) {
|
|
|
14152
14194
|
onCreated: (path) => process.stderr.write(`Temporary file may contain unredacted originals: ${path}\n`),
|
|
14153
14195
|
onRetained: (path) => process.stderr.write(`Retained temporary file may contain unredacted originals: ${path}\n`)
|
|
14154
14196
|
}),
|
|
14155
|
-
inspection:
|
|
14156
|
-
...processing,
|
|
14157
|
-
onAnomaly: producers.onInspectionAnomaly
|
|
14158
|
-
}),
|
|
14197
|
+
inspection: createInspectionPort(bundles?.url("inspection-worker") ?? new URL("./inspection-worker.mjs", import.meta.url), processing.environment, producers.onInspectionAnomaly),
|
|
14159
14198
|
systemResources,
|
|
14160
14199
|
onDiagnostic: diagnostics.onDiagnostic,
|
|
14161
14200
|
onPackagingActivity: diagnostics.onActivity,
|
|
@@ -14249,6 +14288,46 @@ async function compose(options, callbacks, bundles) {
|
|
|
14249
14288
|
}
|
|
14250
14289
|
}
|
|
14251
14290
|
//#endregion
|
|
14291
|
+
//#region apps/cli/src/agent-presence.ts
|
|
14292
|
+
const journey = z.object({ journeyId: z.uuid() }).loose();
|
|
14293
|
+
const presence = z.object({ at: z.iso.datetime() }).strict();
|
|
14294
|
+
const written = /* @__PURE__ */ new Map();
|
|
14295
|
+
/** At most one presence write per journey this often, from one CLI process. */
|
|
14296
|
+
const presenceIntervalMs = 5e3;
|
|
14297
|
+
const presencePath = (stateDir, journeyId) => join(stateDir, "presence", `${journeyId}.json`);
|
|
14298
|
+
/**
|
|
14299
|
+
* Records that the agent just called the CLI for this journey (E16). It lives beside the journey,
|
|
14300
|
+
* not in it, so a heartbeat never moves the journey revision; a failed write is ignored.
|
|
14301
|
+
*/
|
|
14302
|
+
async function markAgentPresence(stateDir, value) {
|
|
14303
|
+
const parsed = journey.safeParse(value);
|
|
14304
|
+
if (!parsed.success) return;
|
|
14305
|
+
const { journeyId } = parsed.data;
|
|
14306
|
+
const now = Date.now();
|
|
14307
|
+
if (now - (written.get(journeyId) ?? 0) < presenceIntervalMs) return;
|
|
14308
|
+
written.set(journeyId, now);
|
|
14309
|
+
try {
|
|
14310
|
+
const path = presencePath(stateDir, journeyId);
|
|
14311
|
+
await mkdir(join(stateDir, "presence"), {
|
|
14312
|
+
recursive: true,
|
|
14313
|
+
mode: 448
|
|
14314
|
+
});
|
|
14315
|
+
const temporary = `${path}.${now}.tmp`;
|
|
14316
|
+
await writeFile(temporary, JSON.stringify({ at: new Date(now).toISOString() }), { mode: 384 });
|
|
14317
|
+
await rename(temporary, path);
|
|
14318
|
+
} catch {
|
|
14319
|
+
written.delete(journeyId);
|
|
14320
|
+
}
|
|
14321
|
+
}
|
|
14322
|
+
/** When the agent last called the CLI for this journey, or null when it never did. */
|
|
14323
|
+
async function readAgentPresence(stateDir, journeyId) {
|
|
14324
|
+
try {
|
|
14325
|
+
return presence.parse(JSON.parse(await readFile(presencePath(stateDir, journeyId), "utf8")));
|
|
14326
|
+
} catch {
|
|
14327
|
+
return null;
|
|
14328
|
+
}
|
|
14329
|
+
}
|
|
14330
|
+
//#endregion
|
|
14252
14331
|
//#region apps/cli/src/commands.ts
|
|
14253
14332
|
const sendAddress = z.object({
|
|
14254
14333
|
journeyId: z.uuid(),
|
|
@@ -14274,6 +14353,7 @@ const delivery = z.object({
|
|
|
14274
14353
|
/** `signal` carries the process's SIGINT/SIGTERM to long procedures such as journey.prepare. */
|
|
14275
14354
|
async function invoke(runtime, command, value, signal) {
|
|
14276
14355
|
const app = runtime.application;
|
|
14356
|
+
markAgentPresence(runtime.stateDir, value);
|
|
14277
14357
|
switch (command) {
|
|
14278
14358
|
case "send.get": return app.journeySend?.get(sendAddress.partial({ sendId: true }).parse(value));
|
|
14279
14359
|
case "send.prepare": return app.journeySend?.prepare(sendPrepareSchema.parse(value));
|
|
@@ -14508,4 +14588,4 @@ async function cancelOperation(runtime, id) {
|
|
|
14508
14588
|
return readOperation(runtime, id);
|
|
14509
14589
|
}
|
|
14510
14590
|
//#endregion
|
|
14511
|
-
export {
|
|
14591
|
+
export { createExecutionPort as _, readAgentPresence as a, executorRequestSchema as c, parseOptions as d, businessCommands as f, readStateDump as g, startupFailure as h, invoke as i, createRenderActivity as l, permissionDiagnostic as m, readOperation as n, createRuntime as o, longCommands as p, waitOperation as r, executorEventSchema as s, cancelOperation as t, helpText as u, startHost as v, traceStartup as y };
|