@gitroll/tacitt-cli 0.2.0 → 0.4.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-B8DAiQ6Z.mjs → agent-setup-CQRWVUWR.mjs} +1 -1
- package/dist/agent-setup-worker.mjs +4 -4
- package/dist/cli.mjs +66 -9
- package/dist/codex-title-worker.mjs +1 -1
- package/dist/configured-preview-worker.mjs +4 -4
- package/dist/configured-session-worker.mjs +25 -4
- package/dist/{heap-BHcvxWCw.mjs → heap-BaTNC9kf.mjs} +45 -3
- package/dist/inspection-worker.mjs +3 -3
- package/dist/job-DrcpSL_9.mjs +615 -0
- package/dist/{model-BJdTLNjT.mjs → model-D7UnGeag.mjs} +1 -1
- package/dist/operation-executor.mjs +2 -2
- package/dist/preview-worker.mjs +1 -1
- package/dist/{query-BA0XCwnR.mjs → query-ByD0Rp_o.mjs} +391 -551
- package/dist/{run-BeLHsZWb.mjs → run-3UIkr7GZ.mjs} +2 -2
- package/dist/session-process.mjs +6 -1
- package/dist/{src-k0Ya1Bri.mjs → src-CmP73EcD.mjs} +510 -161
- package/dist/{src-B5SArpVY.mjs → src-DES7hBAP.mjs} +17 -5
- package/dist/{src-CC95LYYp.mjs → src-DNn7SifS.mjs} +763 -528
- 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 +8 -6
- package/skills/tacitt/references/commands.md +14 -9
- package/skills/tacitt/references/journey.md +10 -6
- package/skills/tacitt/references/send.md +1 -1
- package/skills/tacitt/scripts/INDEX.md +6 -2
- package/skills/tacitt/scripts/journey-card-consolidation.mjs +90 -4
- 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.mjs +6 -2
- 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.mjs +10 -4
- package/skills/tacitt/scripts/journey-tasks.mjs +1 -1
- package/skills/tacitt/scripts/journey.d.mts +2 -0
- package/skills/tacitt/scripts/journey.mjs +120 -53
- package/skills/tacitt/scripts/review-stream-client.mjs +1 -1
- package/dist/job-J3NFw3MH.mjs +0 -194
- package/dist/web/assets/index-Bjos73wK.js +0 -67
- package/dist/web/assets/index-Bl4C2b2_.css +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
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 { $
|
|
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-CmP73EcD.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-DES7hBAP.mjs";
|
|
4
|
+
import { $ as settingsQuerySchema, $r as packageCommandSchema, A as deviceProfileScopeSchema, B as validateInspectionBatch, Bn as journeyConfirmedHides, Br as hasCompleteWorkDescription, Bt as newJourneyWork, C as rebindReviewSubmit, Cr as privacyDecisionDigest, Ct as sendStartSchema, D as createAccount, Dr as hasCurrentStaticReview, E as staticallyHiddenFindings, Er as nativeRespondSchema, Et as obsoleteJourney, Fn as confirmedHideGroupIds, Fr as sendArtifactSchema, Gr as withReadingProgress, Hr as isReadingComplete, Ht as journeyContextQuerySchema, Ir as sendReceiptSchema, Jn as reviewCandidateResourceSchema, Jr as journeyFindingsSchema, Kn as workCorrectionBinding, L as createApplication, Ln as confirmedHideKinds, Lr as sendReplaceResultSchema, Lt as classificationPartition, M as readAccountAvatarUrl, Mr as agentSourceSchema, Mt as applyClassification, N as storedCredentialSchema, Nn as bTermMatcher, Nr as agentSourcesSchema, O as accountProfileSchema, Or as staticReviewDigest, Pr as journeySendSchema, Pt as journeyReviewStorageQuerySchema, Q as settingsBatchSchema, Qn as bReviewDigest, Qr as commandSchema, Qt as journeyResourceQuerySchema, R as inspectionCancelSchema, Rr as sendResolutionSchema, Rt as classificationWorks, S as rebindReviewOverlay, Sn as readConfirmedHideChange, Sr as assertJourneyPrivacyReady, St as sendPrepareSchema, T as guardedFindingText, Tn as termHideAdditions, Tr as nativeGetSchema, Tt as currentJourneyAdapters, U as inspectionQuerySchema, Un as retireWorkDescription, Ur as isSelectedWork, Vn as maskedHitsStale, Vr as isClassifying, Vt as reviewCompileSchema, Wn as workDecisionDigest, Xn as reviewRepairResourceSchema, Xr as journeyRenderInputSchema, Xt as journeyPolicyRespondSchema, Yn as reviewRepairInputSchema, Yr as journeyPreviewSchema, Yt as searchScopeDigest, Zr as journeyRenderSchema, _t as maximumTermsFileBytes, a as bCrossSegmentReviewSchema, ar as scanGitSeedsId, b as bReviewScope, bn as confirmedHideAdditions, br as journeyClassificationSessionsSchema, bt as sendActionSchema, c as reconstructBFields, cr as scanExclusionResourceSchema, ct as validateFailureLocation, d as selectedValueMembers, dn as sessionReviewInputSchema, dr as journeyInventoryQuerySchema, dt as manualUploadChoiceSchema, ei as selectionBatchSchema, er as planBRepair, et as readWorkspaceOutcomes, f as splitSelectorValues, fn as sessionReviewQuerySchema, ft as manualUploadPrepareSchema, g as changeJourney, gt as emptyTermsRevision, hn as sessionUsefulnessInputSchema, hr as emptyPreparationFacts, i as validateBCrossSegmentReview, ii as stateSchema, ir as scanSessionResourceSchema, j as profileUrlMatchesIdentity, k as deviceAuthorizationSchema, ln as sessionRereadInputSchema, lt as manualUploadActionSchema, m as hideScopeDigest, mt as manualUploadStartSchema, n as scanBatchEquivalenceDigest, nt as appendSensitiveTerms, o as bRereadDigest, oi as recommendParallelism, on as sessionReconcileInputSchema, or as scanGitSeedsResource, ot as artifactFailureDetailSchema, p as activeJourneyTerms, pt as manualUploadSchema, qn as reviewCandidateInputSchema, qr as journeyFindingImpactSchema, qt as journeySearchResultSchema, r as locateTermEvidence, ri as producerVersion, rn as reviewAttemptResourceSchema, rr as scanPreparationDigest, s as reconcileBSession, sr as verifiedScanGitSeeds, st as resolveFailureLocation, t as scanBatchContextDigest, ti as selectionSchema, tn as journeyPlaceSchema, vt as normalizeSensitiveTerms, w as reviewBatchReuse, wr as nativeEventsSchema, x as rebindReviewAlignment, xt as sendChoiceSchema, y as isGenericPublicTerm, yt as replaceSensitiveTermsSchema, z as inspectionStartSchema, zr as sendSettingsSchema, zt as retireScanGeneration } from "./src-DNn7SifS.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-BaTNC9kf.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-DrcpSL_9.mjs";
|
|
7
|
+
import { t as createAgentSetupPort } from "./agent-setup-CQRWVUWR.mjs";
|
|
8
8
|
import { t as workerMessageSchema } from "./messages-D3m3bhva.mjs";
|
|
9
9
|
import { constants, write } from "node:fs";
|
|
10
10
|
import { z } from "zod";
|
|
@@ -651,6 +651,7 @@ const operation = z.enum([
|
|
|
651
651
|
"journey.terms.select-aliases",
|
|
652
652
|
"journey.commit",
|
|
653
653
|
"journey.commit.checked",
|
|
654
|
+
"journey.classification.commit",
|
|
654
655
|
"journey.events",
|
|
655
656
|
"journey.inventory",
|
|
656
657
|
"journey.preparation.progress",
|
|
@@ -853,7 +854,11 @@ const nativePanelRespondSchema = z.object({
|
|
|
853
854
|
remember: z.boolean().optional(),
|
|
854
855
|
excludedTermKeys: z.array(z.string().min(1)).max(2e4).optional()
|
|
855
856
|
}).strict(),
|
|
856
|
-
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()
|
|
857
862
|
])
|
|
858
863
|
}).strict();
|
|
859
864
|
function createRouter(application, account, stopping, delivery, nativeInspection, agentPresence) {
|
|
@@ -944,6 +949,7 @@ function createRouter(application, account, stopping, delivery, nativeInspection
|
|
|
944
949
|
cardsRead: t.procedure.input(nativeGetSchema.required()).query(({ input }) => native().cardsRead(input)),
|
|
945
950
|
files: t.procedure.input(journeyRenderSchema).query(({ input, signal }) => native().files(input, signal)),
|
|
946
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)),
|
|
947
953
|
render: t.procedure.input(journeyRenderInputSchema).mutation(({ input, signal }) => native().render(input, signal)),
|
|
948
954
|
findings: t.procedure.input(journeyFindingsSchema).query(({ input, signal }) => native().findings(input, signal)),
|
|
949
955
|
findingImpact: t.procedure.input(journeyFindingImpactSchema).query(({ input, signal }) => native().findingImpact(input, signal)),
|
|
@@ -4722,6 +4728,19 @@ function createStagedJourney(access, root, databaseId) {
|
|
|
4722
4728
|
});
|
|
4723
4729
|
});
|
|
4724
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
|
+
},
|
|
4725
4744
|
resources: createStagedResources(access, files, (input) => inspection.protectedBase(input)),
|
|
4726
4745
|
saveResource: createStagedResourceSave(access, files),
|
|
4727
4746
|
async putBatch(batch) {
|
|
@@ -4996,6 +5015,7 @@ async function openStore(options, backupObsolete) {
|
|
|
4996
5015
|
acknowledgeSessionUsefulness: (input) => stagedJourney.acknowledgeSessionUsefulness(input),
|
|
4997
5016
|
current: (journeyId) => access.run("journey.current", journeyId ? { journeyId } : {}),
|
|
4998
5017
|
commit: (input) => stagedJourney.commit(input),
|
|
5018
|
+
commitClassification: (input) => stagedJourney.commitClassification(input),
|
|
4999
5019
|
hideIndexKey: (journeyId) => stagedJourney.hideIndexKey(journeyId),
|
|
5000
5020
|
recordTerms: (input) => stagedJourney.recordTerms(input),
|
|
5001
5021
|
journeyTerms: (input) => stagedJourney.journeyTerms(input),
|
|
@@ -5566,7 +5586,8 @@ ${Array.from(businessCommands).join("\n")}
|
|
|
5566
5586
|
Business commands invoke the shared Application directly without an HTTP server.
|
|
5567
5587
|
journey.serve starts a focused confirmation host, emits JSON readiness, and waits until stopped.
|
|
5568
5588
|
journey.serve --detach starts that host in its own process, prints its readiness with the host's
|
|
5569
|
-
pid, and exits; stop the host with that pid.
|
|
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.
|
|
5570
5591
|
It does not discover sessions or open a browser. The agent opens its private URL in the right pane.
|
|
5571
5592
|
Workflow mutations require protocolVersion, runId and expectedRevision in JSON input.
|
|
5572
5593
|
Long commands emit the operation ID immediately as NDJSON, then wait up to 30000 ms.
|
|
@@ -5775,105 +5796,6 @@ function createJourneyFindings(storage) {
|
|
|
5775
5796
|
};
|
|
5776
5797
|
}
|
|
5777
5798
|
//#endregion
|
|
5778
|
-
//#region apps/cli/src/journey/plan.ts
|
|
5779
|
-
async function prepareSourcePlan(storage, harnesses, journeyId, sensitiveTerms, signal, period = {}) {
|
|
5780
|
-
const saved = (await storage.resources({
|
|
5781
|
-
journeyId,
|
|
5782
|
-
kind: "plan"
|
|
5783
|
-
}))[0];
|
|
5784
|
-
if (saved?.kind === "plan") return saved;
|
|
5785
|
-
const discovered = await harnesses.discover(signal);
|
|
5786
|
-
if (!discovered.complete) throw new ApplicationError("SCOPE_NOT_READY");
|
|
5787
|
-
const inventory = filterInventoryByPeriod(discovered, period);
|
|
5788
|
-
if (inventory.sessions.length === 0) throw new ApplicationError("INVALID_SELECTION");
|
|
5789
|
-
const families = reviewFamilies(inventory);
|
|
5790
|
-
const defaults = defaultScopedSettings();
|
|
5791
|
-
const settings = {
|
|
5792
|
-
...defaults,
|
|
5793
|
-
overall: {
|
|
5794
|
-
...defaults.overall,
|
|
5795
|
-
cleanup: {
|
|
5796
|
-
ref: {
|
|
5797
|
-
id: "native-evidence",
|
|
5798
|
-
version: "2"
|
|
5799
|
-
},
|
|
5800
|
-
options: { assessedSessionsOnly: true }
|
|
5801
|
-
}
|
|
5802
|
-
},
|
|
5803
|
-
workspaces: Object.fromEntries(inventory.workspaces.map((work) => [work.id, {
|
|
5804
|
-
parameters: {
|
|
5805
|
-
"screened-identifiers": { enabled: false },
|
|
5806
|
-
"ip-addresses": { enabled: false },
|
|
5807
|
-
hostnames: { enabled: false },
|
|
5808
|
-
"cloud-resources": { enabled: false },
|
|
5809
|
-
"mac-addresses": { enabled: false },
|
|
5810
|
-
"public-key-containers": { enabled: false },
|
|
5811
|
-
"geographic-coordinates": { enabled: false },
|
|
5812
|
-
urls: { enabled: false },
|
|
5813
|
-
"filesystem-paths": { mode: "identity-roots" }
|
|
5814
|
-
},
|
|
5815
|
-
optionalSteps: { "phone-numbers": true }
|
|
5816
|
-
}]))
|
|
5817
|
-
};
|
|
5818
|
-
const configuration = freezeConfiguration(settings, inventory.sessions.flatMap((session) => {
|
|
5819
|
-
const owner = families.owners.get(session.id);
|
|
5820
|
-
return owner ? [{
|
|
5821
|
-
sessionId: session.id,
|
|
5822
|
-
workspaceId: session.workspaceId,
|
|
5823
|
-
settingsOwner: {
|
|
5824
|
-
sessionId: owner.id,
|
|
5825
|
-
workspaceId: owner.workspaceId
|
|
5826
|
-
}
|
|
5827
|
-
}] : [];
|
|
5828
|
-
}));
|
|
5829
|
-
const resource = {
|
|
5830
|
-
kind: "plan",
|
|
5831
|
-
journeyId,
|
|
5832
|
-
approval: {
|
|
5833
|
-
runId: journeyId,
|
|
5834
|
-
revision: 0,
|
|
5835
|
-
approvedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5836
|
-
inventory,
|
|
5837
|
-
sourceChangePolicy: "fail",
|
|
5838
|
-
treatment: {
|
|
5839
|
-
kind: "configured",
|
|
5840
|
-
configuration,
|
|
5841
|
-
dictionaryRevision: sensitiveTerms.revision
|
|
5842
|
-
}
|
|
5843
|
-
},
|
|
5844
|
-
replacementKey: randomBytes(32).toString("hex"),
|
|
5845
|
-
sensitiveTerms: {
|
|
5846
|
-
revision: sensitiveTerms.revision,
|
|
5847
|
-
terms: [...sensitiveTerms.terms]
|
|
5848
|
-
}
|
|
5849
|
-
};
|
|
5850
|
-
await storage.saveResource(resource);
|
|
5851
|
-
return resource;
|
|
5852
|
-
}
|
|
5853
|
-
function workApproval(approval, rootIds) {
|
|
5854
|
-
if (approval.treatment?.kind !== "configured") throw new ApplicationError("PROCESSING_PLAN_UNAVAILABLE");
|
|
5855
|
-
const families = reviewFamilies(approval.inventory);
|
|
5856
|
-
const ids = new Set(rootIds.flatMap((id) => {
|
|
5857
|
-
const members = families.members.get(id);
|
|
5858
|
-
if (!members) throw new ApplicationError("UNKNOWN_ITEM");
|
|
5859
|
-
return members.map((session) => session.id);
|
|
5860
|
-
}));
|
|
5861
|
-
const sessions = approval.inventory.sessions.filter((session) => ids.has(session.id));
|
|
5862
|
-
const workspaces = new Set(sessions.map((session) => session.workspaceId));
|
|
5863
|
-
return {
|
|
5864
|
-
...approval,
|
|
5865
|
-
inventory: {
|
|
5866
|
-
...approval.inventory,
|
|
5867
|
-
sessions,
|
|
5868
|
-
workspaces: approval.inventory.workspaces.filter((workspace) => workspaces.has(workspace.id))
|
|
5869
|
-
},
|
|
5870
|
-
treatment: {
|
|
5871
|
-
...approval.treatment,
|
|
5872
|
-
configuration: selectFrozenConfiguration(approval.treatment.configuration, ids)
|
|
5873
|
-
}
|
|
5874
|
-
};
|
|
5875
|
-
}
|
|
5876
|
-
//#endregion
|
|
5877
5799
|
//#region apps/cli/src/journey/metadata/processing.ts
|
|
5878
5800
|
/** Load the frozen plan and active terms once; each Work then resolves its own policy. */
|
|
5879
5801
|
async function metadataPolicy(storage, state, processing) {
|
|
@@ -6075,6 +5997,10 @@ function bindJourneyDestination(storage, destination) {
|
|
|
6075
5997
|
await guard(input.journeyId);
|
|
6076
5998
|
return storage.saveResource(input);
|
|
6077
5999
|
},
|
|
6000
|
+
async commitClassification(input) {
|
|
6001
|
+
await guard(input.journeyId);
|
|
6002
|
+
return storage.commitClassification(input);
|
|
6003
|
+
},
|
|
6078
6004
|
async respondPolicy(input) {
|
|
6079
6005
|
await guard(input.request.journeyId);
|
|
6080
6006
|
return storage.respondPolicy(input);
|
|
@@ -6799,6 +6725,14 @@ async function publishWorkBatches(storage, plan, work, resources, signal) {
|
|
|
6799
6725
|
}
|
|
6800
6726
|
//#endregion
|
|
6801
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
|
+
]);
|
|
6802
6736
|
/**
|
|
6803
6737
|
* Track A (ADR 0172, ADR 0177): every session of the Work is submitted to the shared pool at once,
|
|
6804
6738
|
* ahead of Track B, and each completed session's receipt is saved as it arrives. Once every session
|
|
@@ -6862,7 +6796,11 @@ async function publishScanSessions(storage, plan, work, prepared, binding, direc
|
|
|
6862
6796
|
volumes.scanInputFields += resource.input.fieldCount;
|
|
6863
6797
|
volumes.scanFileBytes += resource.input.file.byteLength;
|
|
6864
6798
|
}, (error) => {
|
|
6865
|
-
|
|
6799
|
+
const rootSessionId = families.owners.get(selected.session.id)?.id;
|
|
6800
|
+
failure ??= rootSessionId ? {
|
|
6801
|
+
error,
|
|
6802
|
+
rootSessionId
|
|
6803
|
+
} : { error };
|
|
6866
6804
|
withdraw.abort();
|
|
6867
6805
|
});
|
|
6868
6806
|
if (pooled) {
|
|
@@ -6900,7 +6838,11 @@ async function publishScanSessions(storage, plan, work, prepared, binding, direc
|
|
|
6900
6838
|
...paragraphs ? { scanHostedCopies: paragraphs.hosted } : {}
|
|
6901
6839
|
}
|
|
6902
6840
|
});
|
|
6903
|
-
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
|
+
}
|
|
6904
6846
|
hooks.signal.throwIfAborted();
|
|
6905
6847
|
if (resources.length !== prepared.sessions.length) throw new ApplicationError("SCOPE_NOT_READY");
|
|
6906
6848
|
return {
|
|
@@ -7126,224 +7068,6 @@ async function prepareJourneyWork(storage, plan, work, directory, processing, ho
|
|
|
7126
7068
|
}
|
|
7127
7069
|
}
|
|
7128
7070
|
//#endregion
|
|
7129
|
-
//#region apps/cli/src/journey/static/production.ts
|
|
7130
|
-
/**
|
|
7131
|
-
* The single production both Track B and packaging use for one Work generation (ADR 0172):
|
|
7132
|
-
* frozen captures, built-in static detection only (no custom terms, no Git seeds), the Work's
|
|
7133
|
-
* exceptions and a replacement key bound to this generation. Identical inputs give identical
|
|
7134
|
-
* session render jobs, so packaging can require the stored Track B result by job digest.
|
|
7135
|
-
*/
|
|
7136
|
-
async function staticBaseProduction(storage, state, work, processing, hooks) {
|
|
7137
|
-
const excluded = work.excludedReview?.snapshotId === state.snapshotId && work.excludedReview.inputRevision === work.inputRevision ? new Set(work.excludedReview.rootSessionIds) : /* @__PURE__ */ new Set();
|
|
7138
|
-
const roots = work.rootSessionIds.filter((id) => !excluded.has(id));
|
|
7139
|
-
if (!roots.length) return void 0;
|
|
7140
|
-
const plan = (await storage.resources({
|
|
7141
|
-
journeyId: state.journeyId,
|
|
7142
|
-
kind: "plan"
|
|
7143
|
-
}))[0];
|
|
7144
|
-
if (plan?.kind !== "plan") throw new ApplicationError("CORRUPTED_STATE");
|
|
7145
|
-
const approval = workApproval(plan.approval, roots);
|
|
7146
|
-
if (approval.treatment?.kind !== "configured") throw new ApplicationError("CORRUPTED_STATE");
|
|
7147
|
-
const parts = new Set(approval.inventory.sessions.flatMap((session) => session.parts.map((part) => part.id)));
|
|
7148
|
-
const captures = (await storage.resources({
|
|
7149
|
-
journeyId: state.journeyId,
|
|
7150
|
-
kind: "capture"
|
|
7151
|
-
})).flatMap((item) => item.kind === "capture" && parts.has(item.capture.partId) ? [item.capture] : []);
|
|
7152
|
-
if (captures.length !== parts.size) throw new ApplicationError("SCOPE_NOT_READY");
|
|
7153
|
-
const { sensitiveTerms: _live, customTerms: _terms, staticExceptions: _unscoped, ...local } = processing;
|
|
7154
|
-
const production = await prepareCollectionProduction(approval, state.journeyId, {
|
|
7155
|
-
configured: {
|
|
7156
|
-
...local,
|
|
7157
|
-
gitNames: async () => ({
|
|
7158
|
-
names: [],
|
|
7159
|
-
snapshots: []
|
|
7160
|
-
}),
|
|
7161
|
-
replacementKey: createHash("sha256").update(JSON.stringify([
|
|
7162
|
-
"static-base-v1",
|
|
7163
|
-
plan.replacementKey,
|
|
7164
|
-
work.id,
|
|
7165
|
-
work.inputRevision
|
|
7166
|
-
])).digest("hex"),
|
|
7167
|
-
staticExceptions: {
|
|
7168
|
-
workId: work.id,
|
|
7169
|
-
values: state.exceptions.filter((item) => item.workId === work.id).map(({ kind, value }) => ({
|
|
7170
|
-
kind,
|
|
7171
|
-
value
|
|
7172
|
-
}))
|
|
7173
|
-
}
|
|
7174
|
-
},
|
|
7175
|
-
captures,
|
|
7176
|
-
everyAdmissionRefusal: true
|
|
7177
|
-
}, hooks);
|
|
7178
|
-
if (production.kind !== "configured" || production.workspaceFailures.length) throw new ApplicationError("SCOPE_NOT_READY");
|
|
7179
|
-
const families = reviewFamilies(approval.inventory);
|
|
7180
|
-
const admitted = new Set(production.selection.sessions.map((item) => item.session.id));
|
|
7181
|
-
return {
|
|
7182
|
-
production,
|
|
7183
|
-
families,
|
|
7184
|
-
work: {
|
|
7185
|
-
...work,
|
|
7186
|
-
rootSessionIds: roots.filter((id) => families.members.get(id)?.some((member) => admitted.has(member.id))),
|
|
7187
|
-
access: work.access === "private" ? "private" : "public"
|
|
7188
|
-
}
|
|
7189
|
-
};
|
|
7190
|
-
}
|
|
7191
|
-
//#endregion
|
|
7192
|
-
//#region apps/cli/src/journey/static/store.ts
|
|
7193
|
-
/** Identity, length and complete digest of one owner-only file; a symlink is never followed. */
|
|
7194
|
-
async function describe(path, signal) {
|
|
7195
|
-
const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
7196
|
-
try {
|
|
7197
|
-
const before = await handle.stat({ bigint: true });
|
|
7198
|
-
if (!before.isFile()) throw new ApplicationError("CORRUPTED_STATE");
|
|
7199
|
-
const digest = createHash("sha256");
|
|
7200
|
-
let byteLength = 0;
|
|
7201
|
-
const buffer = Buffer.allocUnsafe(1048576);
|
|
7202
|
-
for (;;) {
|
|
7203
|
-
signal.throwIfAborted();
|
|
7204
|
-
const { bytesRead } = await handle.read(buffer, 0, buffer.length, byteLength);
|
|
7205
|
-
if (!bytesRead) break;
|
|
7206
|
-
digest.update(buffer.subarray(0, bytesRead));
|
|
7207
|
-
byteLength += bytesRead;
|
|
7208
|
-
}
|
|
7209
|
-
const after = await handle.stat({ bigint: true });
|
|
7210
|
-
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");
|
|
7211
|
-
return {
|
|
7212
|
-
path,
|
|
7213
|
-
identity: {
|
|
7214
|
-
dev: String(after.dev),
|
|
7215
|
-
ino: String(after.ino)
|
|
7216
|
-
},
|
|
7217
|
-
sha256: digest.digest("hex"),
|
|
7218
|
-
byteLength
|
|
7219
|
-
};
|
|
7220
|
-
} finally {
|
|
7221
|
-
await handle.close();
|
|
7222
|
-
}
|
|
7223
|
-
}
|
|
7224
|
-
/** A stored file must still be the exact recorded file and bytes. */
|
|
7225
|
-
async function authenticate(file, signal) {
|
|
7226
|
-
const actual = await describe(file.path, signal);
|
|
7227
|
-
if (JSON.stringify(actual) !== JSON.stringify(file)) throw new ApplicationError("CORRUPTED_STATE");
|
|
7228
|
-
}
|
|
7229
|
-
/**
|
|
7230
|
-
* Persist one Track B session as owner-only private state beside the journey's other scan
|
|
7231
|
-
* files: its three spools and a digest-bound result description. Nothing here is uploaded.
|
|
7232
|
-
*/
|
|
7233
|
-
async function saveStaticSession(storage, binding, item, result, signal) {
|
|
7234
|
-
const owned = [
|
|
7235
|
-
result.spoolPath,
|
|
7236
|
-
result.companionSpoolPath,
|
|
7237
|
-
result.maskSpoolPath
|
|
7238
|
-
];
|
|
7239
|
-
const resultPath = join(binding.directory, `${randomUUID()}.static.json`);
|
|
7240
|
-
let saved = false;
|
|
7241
|
-
try {
|
|
7242
|
-
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");
|
|
7243
|
-
const stored = {
|
|
7244
|
-
kind: "render",
|
|
7245
|
-
processingMs: result.processingMs,
|
|
7246
|
-
maskEntries: result.maskEntries,
|
|
7247
|
-
metadataMasks: result.metadataMasks,
|
|
7248
|
-
metadata: result.metadata,
|
|
7249
|
-
parts: result.parts,
|
|
7250
|
-
entries: result.entries,
|
|
7251
|
-
agentSetup: result.agentSetup
|
|
7252
|
-
};
|
|
7253
|
-
const file = await open(resultPath, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, 384);
|
|
7254
|
-
owned.push(resultPath);
|
|
7255
|
-
try {
|
|
7256
|
-
await file.writeFile(JSON.stringify(stored));
|
|
7257
|
-
await file.sync();
|
|
7258
|
-
} finally {
|
|
7259
|
-
await file.close();
|
|
7260
|
-
}
|
|
7261
|
-
const resource = staticSessionResourceSchema.parse({
|
|
7262
|
-
kind: "static-session",
|
|
7263
|
-
journeyId: binding.journeyId,
|
|
7264
|
-
workId: binding.work.id,
|
|
7265
|
-
inputRevision: binding.work.inputRevision,
|
|
7266
|
-
snapshotId: binding.snapshotId,
|
|
7267
|
-
sessionId: item.sessionId,
|
|
7268
|
-
jobDigest: item.digest,
|
|
7269
|
-
result: await describe(resultPath, signal),
|
|
7270
|
-
primary: await describe(result.spoolPath, signal),
|
|
7271
|
-
companion: await describe(result.companionSpoolPath, signal),
|
|
7272
|
-
masks: await describe(result.maskSpoolPath, signal),
|
|
7273
|
-
...result.images === void 0 ? {} : { imagesOmitted: result.images }
|
|
7274
|
-
});
|
|
7275
|
-
await storage.saveResource(resource);
|
|
7276
|
-
saved = true;
|
|
7277
|
-
return resource;
|
|
7278
|
-
} finally {
|
|
7279
|
-
if (!saved) {
|
|
7280
|
-
for (const path of owned) if (path !== void 0) await unlink(path).catch(() => {});
|
|
7281
|
-
}
|
|
7282
|
-
}
|
|
7283
|
-
}
|
|
7284
|
-
/** Current-generation Track B receipts of one Work, keyed by application session ID. */
|
|
7285
|
-
async function staticSessions(storage, journeyId, work) {
|
|
7286
|
-
const resources = (await storage.resources({
|
|
7287
|
-
journeyId,
|
|
7288
|
-
kind: "static-session",
|
|
7289
|
-
idPrefix: `${work.id}:${work.inputRevision}:`
|
|
7290
|
-
})).filter((item) => item.kind === "static-session" && item.workId === work.id && item.inputRevision === work.inputRevision);
|
|
7291
|
-
return new Map(resources.map((item) => [item.sessionId, item]));
|
|
7292
|
-
}
|
|
7293
|
-
/**
|
|
7294
|
-
* Shape guard like the worker transport's: the bytes are bound by the recorded digest, and the
|
|
7295
|
-
* archive writer checks parts, entries and masks again when it consumes them.
|
|
7296
|
-
*/
|
|
7297
|
-
function isStoredResult(value) {
|
|
7298
|
-
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"));
|
|
7299
|
-
}
|
|
7300
|
-
/** Authenticated result description of one stored Track B session. */
|
|
7301
|
-
async function readStaticResult(resource, signal) {
|
|
7302
|
-
await authenticate(resource.result, signal);
|
|
7303
|
-
const handle = await open(resource.result.path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
7304
|
-
try {
|
|
7305
|
-
const text = await handle.readFile("utf8");
|
|
7306
|
-
if (createHash("sha256").update(text).digest("hex") !== resource.result.sha256) throw new ApplicationError("CORRUPTED_STATE");
|
|
7307
|
-
const value = JSON.parse(text);
|
|
7308
|
-
if (!isStoredResult(value)) throw new ApplicationError("CORRUPTED_STATE");
|
|
7309
|
-
return value;
|
|
7310
|
-
} finally {
|
|
7311
|
-
await handle.close();
|
|
7312
|
-
}
|
|
7313
|
-
}
|
|
7314
|
-
/**
|
|
7315
|
-
* Packaging from the Track B base: the stored result of exactly this job, with fresh hard links
|
|
7316
|
-
* to its authenticated spools that the archive writer may consume. Static detection never runs.
|
|
7317
|
-
*/
|
|
7318
|
-
async function linkedStaticResult(resource, job, signal) {
|
|
7319
|
-
if (!resource || resource.jobDigest !== job.digest) throw new ApplicationError("SCOPE_NOT_READY");
|
|
7320
|
-
const result = await readStaticResult(resource, signal);
|
|
7321
|
-
const links = [];
|
|
7322
|
-
try {
|
|
7323
|
-
const linked = async (file) => {
|
|
7324
|
-
const path = join(job.spoolDirectory, `${randomUUID()}.session`);
|
|
7325
|
-
await link(file.path, path);
|
|
7326
|
-
links.push(path);
|
|
7327
|
-
const stat = await lstat(path, { bigint: true });
|
|
7328
|
-
if (String(stat.dev) !== file.identity.dev || String(stat.ino) !== file.identity.ino) throw new ApplicationError("CORRUPTED_STATE");
|
|
7329
|
-
await authenticate({
|
|
7330
|
-
...file,
|
|
7331
|
-
path
|
|
7332
|
-
}, signal);
|
|
7333
|
-
return path;
|
|
7334
|
-
};
|
|
7335
|
-
return {
|
|
7336
|
-
...result,
|
|
7337
|
-
spoolPath: await linked(resource.primary),
|
|
7338
|
-
companionSpoolPath: await linked(resource.companion),
|
|
7339
|
-
maskSpoolPath: await linked(resource.masks)
|
|
7340
|
-
};
|
|
7341
|
-
} catch (error) {
|
|
7342
|
-
for (const path of links) await unlink(path).catch(() => {});
|
|
7343
|
-
throw error;
|
|
7344
|
-
}
|
|
7345
|
-
}
|
|
7346
|
-
//#endregion
|
|
7347
7071
|
//#region apps/cli/src/journey/static/run.ts
|
|
7348
7072
|
/**
|
|
7349
7073
|
* Track B for one Work generation (ADR 0172): render every admitted session with all built-in
|
|
@@ -7924,7 +7648,7 @@ async function runWorkers(workers) {
|
|
|
7924
7648
|
//#endregion
|
|
7925
7649
|
//#region apps/cli/src/journey/package-base.ts
|
|
7926
7650
|
/** Package-only staging from the complete Track B base; scanning never calls this. */
|
|
7927
|
-
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) {
|
|
7928
7652
|
assertJourneyPrivacyReady(state, true);
|
|
7929
7653
|
const existing = (await storage.resources({
|
|
7930
7654
|
journeyId: state.journeyId,
|
|
@@ -7932,53 +7656,18 @@ async function packageBase(storage, state, work, processing, directory, signal,
|
|
|
7932
7656
|
id: `${work.id}:${work.inputRevision}`
|
|
7933
7657
|
})).find((item) => item.kind === "base" && item.workId === work.id && item.inputRevision === work.inputRevision);
|
|
7934
7658
|
if (existing?.kind === "base") return existing;
|
|
7935
|
-
const
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
const prepared = await staticBaseProduction(storage, state, work, processing, hooks);
|
|
7941
|
-
if (!prepared) throw new ApplicationError("ARTIFACT_EMPTY_SCOPE");
|
|
7942
|
-
const { families } = prepared;
|
|
7943
|
-
const stored = await staticSessions(storage, state.journeyId, work);
|
|
7944
|
-
const written = await writeLogicalJourneyBase({
|
|
7945
|
-
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()],
|
|
7946
7664
|
directory,
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
prerendered: async (job) => {
|
|
7950
|
-
if (job.spoolDirectory === void 0) throw new ApplicationError("CORRUPTED_STATE");
|
|
7951
|
-
return linkedStaticResult(stored.get(job.session.id), {
|
|
7952
|
-
digest: renderJobDigest(job),
|
|
7953
|
-
spoolDirectory: job.spoolDirectory
|
|
7954
|
-
}, signal);
|
|
7955
|
-
}
|
|
7956
|
-
}, hooks);
|
|
7665
|
+
processing
|
|
7666
|
+
}, signal);
|
|
7957
7667
|
const after = await readState();
|
|
7958
7668
|
const current = after?.works.find((item) => item.id === work.id);
|
|
7959
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");
|
|
7960
7670
|
assertJourneyPrivacyReady(after, true);
|
|
7961
|
-
const base = {
|
|
7962
|
-
kind: "base",
|
|
7963
|
-
journeyId: state.journeyId,
|
|
7964
|
-
workId: work.id,
|
|
7965
|
-
inputRevision: work.inputRevision,
|
|
7966
|
-
bundleId: written.bundleId,
|
|
7967
|
-
primary: written.primary,
|
|
7968
|
-
companion: written.companion,
|
|
7969
|
-
maskLedger: written.maskLedger,
|
|
7970
|
-
sourceBindings: [...written.sourceBindings],
|
|
7971
|
-
sessions: written.sessionBindings.map((binding) => {
|
|
7972
|
-
const root = families.owners.get(binding.applicationSessionId);
|
|
7973
|
-
if (!root) throw new ApplicationError("CORRUPTED_STATE");
|
|
7974
|
-
return {
|
|
7975
|
-
applicationSessionId: binding.applicationSessionId,
|
|
7976
|
-
archiveSessionId: binding.sessionId,
|
|
7977
|
-
rootSessionId: root.id,
|
|
7978
|
-
role: root.id === binding.applicationSessionId ? "main" : "worker"
|
|
7979
|
-
};
|
|
7980
|
-
})
|
|
7981
|
-
};
|
|
7982
7671
|
await storage.saveResource(base);
|
|
7983
7672
|
return base;
|
|
7984
7673
|
}
|
|
@@ -8373,8 +8062,26 @@ function createRenderWorkers(workers, size) {
|
|
|
8373
8062
|
return pool.serial(() => writeJourneyOutput(work, signal));
|
|
8374
8063
|
}
|
|
8375
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
|
+
};
|
|
8376
8082
|
return {
|
|
8377
8083
|
write,
|
|
8084
|
+
writeBase,
|
|
8378
8085
|
close: () => pool.discard()
|
|
8379
8086
|
};
|
|
8380
8087
|
}
|
|
@@ -8552,7 +8259,7 @@ function createJourneyRenderer(options) {
|
|
|
8552
8259
|
work
|
|
8553
8260
|
};
|
|
8554
8261
|
}
|
|
8555
|
-
async function produce(state, work, signal, write, session, plan) {
|
|
8262
|
+
async function produce(state, work, signal, write, session, plan, writeBase) {
|
|
8556
8263
|
const { journeyId } = state;
|
|
8557
8264
|
const workId = work.id;
|
|
8558
8265
|
assertJourneyPrivacyReady(state, true);
|
|
@@ -8584,11 +8291,11 @@ function createJourneyRenderer(options) {
|
|
|
8584
8291
|
});
|
|
8585
8292
|
const next = moved.works.find((item) => item.id === workId);
|
|
8586
8293
|
if (!next) throw new ApplicationError("REVISION_CONFLICT");
|
|
8587
|
-
return produce(moved, next, signal, write, session, plan);
|
|
8294
|
+
return produce(moved, next, signal, write, session, plan, writeBase);
|
|
8588
8295
|
}
|
|
8589
8296
|
if (!output) {
|
|
8590
8297
|
const directory = await journeyDirectory(options.stateDir, journeyId);
|
|
8591
|
-
await packageBase(storage, state, work, options.processing, directory, signal, () => session.current());
|
|
8298
|
+
await packageBase(storage, state, work, options.processing, directory, signal, () => session.current(), writeBase);
|
|
8592
8299
|
const { base, tokens, confirmed, request } = await journeyOutputRequest(storage, state, work, options.processing, signal, await plan());
|
|
8593
8300
|
const { output: written, tokens: collected } = await write({
|
|
8594
8301
|
request: {
|
|
@@ -8680,10 +8387,10 @@ function createJourneyRenderer(options) {
|
|
|
8680
8387
|
};
|
|
8681
8388
|
});
|
|
8682
8389
|
}
|
|
8683
|
-
async function render(journeyId, workId, signal, write = writeJourneyOutput, session = directRenderSession(storage, journeyId), plan = () => readJourneyPlan(storage, journeyId)) {
|
|
8390
|
+
async function render(journeyId, workId, signal, write = writeJourneyOutput, session = directRenderSession(storage, journeyId), plan = () => readJourneyPlan(storage, journeyId), writeBase = writeJourneyBase) {
|
|
8684
8391
|
const { state, work } = await admit(workId, session);
|
|
8685
8392
|
try {
|
|
8686
|
-
return await produce(state, work, signal, write, session, plan);
|
|
8393
|
+
return await produce(state, work, signal, write, session, plan, writeBase);
|
|
8687
8394
|
} catch (error) {
|
|
8688
8395
|
if (signal.aborted) throw error;
|
|
8689
8396
|
const renderFailure = {
|
|
@@ -8706,14 +8413,16 @@ function createJourneyRenderer(options) {
|
|
|
8706
8413
|
}
|
|
8707
8414
|
}
|
|
8708
8415
|
/**
|
|
8709
|
-
* A several-Work render writes each Work's output in packaging workers; state,
|
|
8710
|
-
* resource writes stay on this thread under the cross-process state lock
|
|
8711
|
-
* 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.
|
|
8712
8421
|
*/
|
|
8713
8422
|
async function renderScope(journeyId, works, signal) {
|
|
8714
8423
|
const workers = options.workers;
|
|
8715
8424
|
const size = workers ? workers.parallelism ?? renderWorkerCount() : 0;
|
|
8716
|
-
if (!workers || works <
|
|
8425
|
+
if (!workers || works < 1 || size < 1) return void 0;
|
|
8717
8426
|
const pool = createRenderWorkers(workers, size);
|
|
8718
8427
|
const session = batchedRenderSession(storage, journeyId);
|
|
8719
8428
|
let plan;
|
|
@@ -8723,7 +8432,7 @@ function createJourneyRenderer(options) {
|
|
|
8723
8432
|
});
|
|
8724
8433
|
return {
|
|
8725
8434
|
concurrency: size + 1,
|
|
8726
|
-
render: (workId) => render(journeyId, workId, signal, pool.write, session, readPlan),
|
|
8435
|
+
render: (workId) => render(journeyId, workId, signal, pool.write, session, readPlan, pool.writeBase),
|
|
8727
8436
|
close: pool.close
|
|
8728
8437
|
};
|
|
8729
8438
|
}
|
|
@@ -9479,57 +9188,49 @@ function createClassificationReader(storage, stateDir) {
|
|
|
9479
9188
|
}
|
|
9480
9189
|
//#endregion
|
|
9481
9190
|
//#region apps/cli/src/journey/works/regroup.ts
|
|
9482
|
-
/**
|
|
9483
|
-
|
|
9484
|
-
|
|
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) {
|
|
9485
9196
|
const retained = new Set(works.map((work) => work.id));
|
|
9486
9197
|
const batches = /* @__PURE__ */ new Map();
|
|
9487
|
-
const
|
|
9488
|
-
for (const term of state.privacy.terms) {
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
|
|
9492
|
-
|
|
9493
|
-
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
}
|
|
9500
|
-
let batch = batches.get(source.batchId);
|
|
9501
|
-
if (!batch) {
|
|
9502
|
-
const record = await storage.inspectReview({
|
|
9503
|
-
kind: "attempt",
|
|
9504
|
-
journeyId: state.journeyId,
|
|
9505
|
-
batchId: source.batchId
|
|
9506
|
-
});
|
|
9507
|
-
if (!record || record.batch.snapshotId !== state.snapshotId) throw new ApplicationError("CORRUPTED_STATE");
|
|
9508
|
-
batch = record.batch;
|
|
9509
|
-
if (batches.size >= 16) batches.clear();
|
|
9510
|
-
batches.set(source.batchId, batch);
|
|
9511
|
-
}
|
|
9512
|
-
const chunk = batch.chunks.find((item) => item.id === source.chunkId) ?? batch.duplicates?.find((item) => item.id === source.chunkId);
|
|
9513
|
-
const workId = chunk && owners.get(chunk.sessionId);
|
|
9514
|
-
if (!workId) throw new ApplicationError("CORRUPTED_STATE");
|
|
9515
|
-
sources.push({
|
|
9516
|
-
...source,
|
|
9517
|
-
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
|
|
9518
9210
|
});
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
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
|
|
9523
9222
|
});
|
|
9524
9223
|
}
|
|
9525
|
-
return
|
|
9224
|
+
return [...resolved.values()];
|
|
9526
9225
|
}
|
|
9527
9226
|
//#endregion
|
|
9528
9227
|
//#region apps/cli/src/journey/works/proposal.ts
|
|
9529
9228
|
/**
|
|
9530
9229
|
* One atomic root partition; recommendations have no access authority. Roots still being grouped
|
|
9531
9230
|
* wait in one unread Work while the other Works are confirmed and read; the submission that
|
|
9532
|
-
* groups them may arrive after confirmation and keeps it (E2).
|
|
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).
|
|
9533
9234
|
*/
|
|
9534
9235
|
function createClassificationProposal(storage) {
|
|
9535
9236
|
return async (input, signal) => {
|
|
@@ -9539,19 +9240,20 @@ function createClassificationProposal(storage) {
|
|
|
9539
9240
|
const classifying = isClassifying(state);
|
|
9540
9241
|
const late = state.scope.status === "confirmed";
|
|
9541
9242
|
const bound = input.expectedClassificationRevision !== void 0 && classifying && input.expectedClassificationRevision === state.classificationRevision;
|
|
9542
|
-
const
|
|
9543
|
-
|
|
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({
|
|
9251
|
+
expectedRevision: input.expectedRevision,
|
|
9544
9252
|
requestKey: input.idempotencyKey,
|
|
9545
|
-
requestHash
|
|
9546
|
-
state
|
|
9547
|
-
event
|
|
9548
|
-
journeyId: state.journeyId,
|
|
9549
|
-
kind: "response",
|
|
9550
|
-
source: "conversation",
|
|
9551
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9552
|
-
}
|
|
9253
|
+
requestHash,
|
|
9254
|
+
state,
|
|
9255
|
+
event
|
|
9553
9256
|
});
|
|
9554
|
-
if (input.expectedClassificationRevision !== void 0 ? !bound : state.revision !== input.expectedRevision) return commit();
|
|
9555
9257
|
if (late && !classifying) throw new ApplicationError("INVALID_TRANSITION");
|
|
9556
9258
|
if (state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
|
|
9557
9259
|
if (!state.snapshotId || state.pendingHideId) throw new ApplicationError("SCOPE_NOT_READY");
|
|
@@ -9590,96 +9292,43 @@ function createClassificationProposal(storage) {
|
|
|
9590
9292
|
kind: "capture"
|
|
9591
9293
|
});
|
|
9592
9294
|
const captured = new Set(captures.flatMap((resource) => resource.kind === "capture" ? [resource.capture.partId] : []));
|
|
9593
|
-
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 = [];
|
|
9594
9301
|
for (const item of merged.values()) {
|
|
9595
9302
|
signal.throwIfAborted();
|
|
9596
|
-
|
|
9597
|
-
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
if (late && existing) {
|
|
9601
|
-
works.push(existing);
|
|
9602
|
-
continue;
|
|
9603
|
-
}
|
|
9604
|
-
const workspaces = [...new Set(sessions.map((session) => session.workspaceId))];
|
|
9605
|
-
const base = existing ?? (extended ? {
|
|
9606
|
-
...resetWorkScan(extended),
|
|
9607
|
-
rootSessionIds: item.rootSessionIds,
|
|
9608
|
-
sourceWorkspaceIds: [.../* @__PURE__ */ new Set([...extended.sourceWorkspaceIds, ...workspaces])]
|
|
9609
|
-
} : newJourneyWork(randomUUID(), item.projectName, item.rootSessionIds, workspaces, "placed"));
|
|
9610
|
-
const { classificationMetadata: _display, ...original } = base;
|
|
9611
|
-
works.push({
|
|
9612
|
-
...original,
|
|
9613
|
-
projectName: item.projectName,
|
|
9614
|
-
title: base.titleOrigin === "explicit" ? base.title : item.projectName,
|
|
9615
|
-
description: item.description,
|
|
9616
|
-
projectType: {
|
|
9617
|
-
type: item.projectType,
|
|
9618
|
-
reason: item.recommendation.reason,
|
|
9619
|
-
origin: "model"
|
|
9620
|
-
},
|
|
9621
|
-
placement: "placed",
|
|
9622
|
-
...late && !extended ? {
|
|
9623
|
-
access: item.recommendation.access,
|
|
9624
|
-
accessOrigin: "model",
|
|
9625
|
-
accessConfirmed: true
|
|
9626
|
-
} : {},
|
|
9627
|
-
classificationRecommendation: item.recommendation,
|
|
9628
|
-
suggestion: null,
|
|
9629
|
-
artifact: null,
|
|
9630
|
-
revision: base.revision + 1
|
|
9303
|
+
items.push({
|
|
9304
|
+
...item,
|
|
9305
|
+
workId: randomUUID(),
|
|
9306
|
+
workspaceIds: workspacesOf(item.rootSessionIds)
|
|
9631
9307
|
});
|
|
9632
9308
|
}
|
|
9633
|
-
|
|
9634
|
-
|
|
9635
|
-
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
const forced = new Set(preserved.filter((term) => !termTypeInfo[term.type].asked).map((term) => term.value));
|
|
9657
|
-
const overrides = new Map(state.valueOverrides?.map((item) => [item.value, item.choice]) ?? []);
|
|
9658
|
-
const terms = preserved.map((term) => ({
|
|
9659
|
-
...term,
|
|
9660
|
-
disposition: forced.has(term.value) ? "hidden" : overrides.has(term.value) ? overrides.get(term.value) === "hide" ? "hidden" : "keep" : regrouped && cardTerms.has(privacyTermKey(term)) ? "pending" : term.disposition
|
|
9661
|
-
}));
|
|
9662
|
-
return commit({
|
|
9663
|
-
...state,
|
|
9664
|
-
works,
|
|
9665
|
-
classificationRevision: state.revision + 1,
|
|
9666
|
-
scope: late ? state.scope : {
|
|
9667
|
-
status: "pending",
|
|
9668
|
-
actor: null,
|
|
9669
|
-
at: null
|
|
9670
|
-
},
|
|
9671
|
-
privacy: {
|
|
9672
|
-
...state.privacy,
|
|
9673
|
-
terms,
|
|
9674
|
-
packageApproval: null,
|
|
9675
|
-
...regrouped ? {
|
|
9676
|
-
cards: [],
|
|
9677
|
-
answersRevision: state.privacy.answersRevision + 1,
|
|
9678
|
-
confirmedAnswersRevision: null
|
|
9679
|
-
} : {}
|
|
9680
|
-
},
|
|
9681
|
-
readingComplete: !pending.length && works.filter((work) => work.access !== "drop").every((work) => work.totalBatches > 0 && work.completedBatches === work.totalBatches),
|
|
9682
|
-
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
|
|
9683
9332
|
});
|
|
9684
9333
|
};
|
|
9685
9334
|
}
|
|
@@ -10143,7 +9792,33 @@ function createJourneySendArtifacts(storage) {
|
|
|
10143
9792
|
const output = await find(journey.journeyId, work.id, work.revision);
|
|
10144
9793
|
const info = await readConfiguredOutputInfo(output.primary, signal);
|
|
10145
9794
|
if (info.work.id !== work.id || info.work.access !== work.access) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
|
|
9795
|
+
let agentSources = [];
|
|
9796
|
+
try {
|
|
9797
|
+
const base = (await storage.resources({
|
|
9798
|
+
journeyId: journey.journeyId,
|
|
9799
|
+
kind: "base"
|
|
9800
|
+
})).find((item) => item.kind === "base" && item.workId === work.id && item.inputRevision === output.finalMaskLedger.inputRevision && item.primary.sha256 === output.finalMaskLedger.basePrimarySha256);
|
|
9801
|
+
if (base?.kind === "base") {
|
|
9802
|
+
const included = new Set(base.sessions.map((item) => item.applicationSessionId));
|
|
9803
|
+
const resources = await storage.resources({
|
|
9804
|
+
journeyId: journey.journeyId,
|
|
9805
|
+
kind: "static-session"
|
|
9806
|
+
});
|
|
9807
|
+
const unique = /* @__PURE__ */ new Map();
|
|
9808
|
+
for (const resource of resources) {
|
|
9809
|
+
if (resource.kind !== "static-session" || resource.workId !== work.id || resource.inputRevision !== base.inputRevision || !included.has(resource.sessionId)) continue;
|
|
9810
|
+
for (const candidate of resource.agentSources ?? []) {
|
|
9811
|
+
const parsed = agentSourceSchema.safeParse(candidate);
|
|
9812
|
+
if (parsed.success && unique.size < 500) unique.set(JSON.stringify(parsed.data), parsed.data);
|
|
9813
|
+
}
|
|
9814
|
+
}
|
|
9815
|
+
agentSources = [...unique.values()];
|
|
9816
|
+
}
|
|
9817
|
+
} catch {
|
|
9818
|
+
console.error("AGENT_SOURCES_METADATA_UNAVAILABLE");
|
|
9819
|
+
}
|
|
10146
9820
|
return sendArtifactSchema.parse({
|
|
9821
|
+
...agentSources.length === 0 ? {} : { agentSources },
|
|
10147
9822
|
workId: work.id,
|
|
10148
9823
|
revision: work.revision,
|
|
10149
9824
|
bundleId: info.manifest.bundleId,
|
|
@@ -10156,16 +9831,19 @@ function createJourneySendArtifacts(storage) {
|
|
|
10156
9831
|
sha256: output.primary.sha256
|
|
10157
9832
|
});
|
|
10158
9833
|
},
|
|
10159
|
-
async upload(journeyId, artifact,
|
|
9834
|
+
async upload(journeyId, artifact, upload, signal) {
|
|
10160
9835
|
const output = await find(journeyId, artifact.workId, artifact.revision);
|
|
10161
9836
|
if (output.bundleId !== artifact.bundleId || output.primary.sha256 !== artifact.sha256 || output.primary.archiveBytes !== artifact.byteLength || artifact.byteLength > 5368709120) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
|
|
10162
|
-
await putPrimary(output.primary, artifact.byteLength,
|
|
9837
|
+
await putPrimary(output.primary, artifact.byteLength, upload, signal);
|
|
10163
9838
|
}
|
|
10164
9839
|
};
|
|
10165
9840
|
}
|
|
10166
|
-
/**
|
|
10167
|
-
|
|
10168
|
-
|
|
9841
|
+
/**
|
|
9842
|
+
* Stream a verified primary to a signed HTTPS URL; shared by native Send and Manual upload. A
|
|
9843
|
+
* checksum upload also sends the SHA-256 header the URL signs, so storage refuses other bytes.
|
|
9844
|
+
*/
|
|
9845
|
+
async function putPrimary(primary, byteLength, upload, signal) {
|
|
9846
|
+
const url = new URL(upload.uploadUrl);
|
|
10169
9847
|
if (url.protocol !== "https:" || url.username || url.password || url.hash) throw new ApplicationError("INVALID_ARGUMENTS");
|
|
10170
9848
|
const source = await openConfiguredUpload(primary, signal);
|
|
10171
9849
|
const cleanup = createCleanup();
|
|
@@ -10177,7 +9855,8 @@ async function putPrimary(primary, byteLength, uploadUrl, signal) {
|
|
|
10177
9855
|
redirect: "error",
|
|
10178
9856
|
headers: {
|
|
10179
9857
|
"Content-Type": "application/zip",
|
|
10180
|
-
"Content-Length": String(byteLength)
|
|
9858
|
+
"Content-Length": String(byteLength),
|
|
9859
|
+
...upload.headers
|
|
10181
9860
|
},
|
|
10182
9861
|
duplex: "half"
|
|
10183
9862
|
};
|
|
@@ -10299,6 +9978,85 @@ async function readOwnerJson(response) {
|
|
|
10299
9978
|
return JSON.parse(Buffer.concat(chunks, bytes).toString("utf8"));
|
|
10300
9979
|
}
|
|
10301
9980
|
//#endregion
|
|
9981
|
+
//#region apps/cli/src/account/journey/public-sources.ts
|
|
9982
|
+
/** Anonymous evidence only. Expected absence, refusal and lookup failure all omit enrichment. */
|
|
9983
|
+
async function publicSource(source, requestFetch, signal) {
|
|
9984
|
+
const controller = new AbortController();
|
|
9985
|
+
let onAbort = () => {};
|
|
9986
|
+
const cancelled = new Promise((resolve) => {
|
|
9987
|
+
onAbort = () => {
|
|
9988
|
+
controller.abort();
|
|
9989
|
+
resolve(false);
|
|
9990
|
+
};
|
|
9991
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
9992
|
+
if (signal?.aborted) onAbort();
|
|
9993
|
+
});
|
|
9994
|
+
let timer;
|
|
9995
|
+
const expired = new Promise((resolve) => {
|
|
9996
|
+
timer = setTimeout(() => {
|
|
9997
|
+
controller.abort();
|
|
9998
|
+
resolve(false);
|
|
9999
|
+
}, 3e3);
|
|
10000
|
+
});
|
|
10001
|
+
const check = async () => {
|
|
10002
|
+
const response = await requestFetch(source.type === "github" ? `https://api.github.com/repos/${source.repo}` : `https://registry.npmjs.org/${encodeURIComponent(source.package)}`, {
|
|
10003
|
+
method: "GET",
|
|
10004
|
+
credentials: "omit",
|
|
10005
|
+
redirect: "error",
|
|
10006
|
+
referrerPolicy: "no-referrer",
|
|
10007
|
+
cache: "no-store",
|
|
10008
|
+
signal: controller.signal
|
|
10009
|
+
});
|
|
10010
|
+
if (response.status !== 200 || source.type === "npm") {
|
|
10011
|
+
await response.body?.cancel();
|
|
10012
|
+
return response.status === 200;
|
|
10013
|
+
}
|
|
10014
|
+
const value = await readOwnerJson(response);
|
|
10015
|
+
return typeof value === "object" && value !== null && "private" in value && value.private === false;
|
|
10016
|
+
};
|
|
10017
|
+
try {
|
|
10018
|
+
return await Promise.race([
|
|
10019
|
+
check(),
|
|
10020
|
+
expired,
|
|
10021
|
+
cancelled
|
|
10022
|
+
]);
|
|
10023
|
+
} catch {
|
|
10024
|
+
return false;
|
|
10025
|
+
} finally {
|
|
10026
|
+
signal?.removeEventListener("abort", onAbort);
|
|
10027
|
+
clearTimeout(timer);
|
|
10028
|
+
controller.abort();
|
|
10029
|
+
}
|
|
10030
|
+
}
|
|
10031
|
+
/** One upload, at most twenty distinct checks in parallel and a three-second I/O deadline. */
|
|
10032
|
+
async function checkPublicAgentSources(candidates, requestFetch = fetch, signal) {
|
|
10033
|
+
signal?.throwIfAborted();
|
|
10034
|
+
if (!Array.isArray(candidates) || candidates.length > 500) return [];
|
|
10035
|
+
const admittedShape = candidates.flatMap((item) => {
|
|
10036
|
+
const parsed = agentSourcesSchema.element.safeParse(item);
|
|
10037
|
+
return parsed.success ? [parsed.data] : [];
|
|
10038
|
+
});
|
|
10039
|
+
const cache = /* @__PURE__ */ new Map();
|
|
10040
|
+
const selected = [];
|
|
10041
|
+
for (const item of admittedShape) {
|
|
10042
|
+
const source = item.source;
|
|
10043
|
+
const key = source.type === "github" ? `github:${source.repo.toLowerCase()}` : `npm:${source.package}`;
|
|
10044
|
+
let result = cache.get(key);
|
|
10045
|
+
if (!result) {
|
|
10046
|
+
if (cache.size === 20) continue;
|
|
10047
|
+
result = publicSource(source, requestFetch, signal);
|
|
10048
|
+
cache.set(key, result);
|
|
10049
|
+
}
|
|
10050
|
+
selected.push({
|
|
10051
|
+
item,
|
|
10052
|
+
result
|
|
10053
|
+
});
|
|
10054
|
+
}
|
|
10055
|
+
const results = await Promise.all(selected.map(async ({ item, result }) => await result ? item : null));
|
|
10056
|
+
signal?.throwIfAborted();
|
|
10057
|
+
return results.filter((item) => item !== null);
|
|
10058
|
+
}
|
|
10059
|
+
//#endregion
|
|
10302
10060
|
//#region apps/cli/src/account/journey/client.ts
|
|
10303
10061
|
const refusalSchema = z.object({
|
|
10304
10062
|
error: z.string(),
|
|
@@ -10347,7 +10105,7 @@ const completeTimeoutMs = 6e5;
|
|
|
10347
10105
|
function createJourneyOwnerClient(options) {
|
|
10348
10106
|
const base = apiBase(options.apiUrl);
|
|
10349
10107
|
const requestFetch = options.fetch ?? fetch;
|
|
10350
|
-
async function request(path, schema, signal, body, method = "POST", key, timeoutMs = requestTimeoutMs$2) {
|
|
10108
|
+
async function request(path, schema, signal, body, method = "POST", key, timeoutMs = requestTimeoutMs$2, checksumUpload = false) {
|
|
10351
10109
|
const token = await options.authorize();
|
|
10352
10110
|
signal.throwIfAborted();
|
|
10353
10111
|
const response = await requestFetch(new URL(path, base), {
|
|
@@ -10356,7 +10114,8 @@ function createJourneyOwnerClient(options) {
|
|
|
10356
10114
|
accept: "application/json",
|
|
10357
10115
|
authorization: `Bearer ${token}`,
|
|
10358
10116
|
...body === void 0 ? {} : { "content-type": "application/json" },
|
|
10359
|
-
...key === void 0 ? {} : { "idempotency-key": key }
|
|
10117
|
+
...key === void 0 ? {} : { "idempotency-key": key },
|
|
10118
|
+
...checksumUpload ? { "upload-checksum": "sha256" } : {}
|
|
10360
10119
|
},
|
|
10361
10120
|
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
10362
10121
|
credentials: "omit",
|
|
@@ -10387,6 +10146,9 @@ function createJourneyOwnerClient(options) {
|
|
|
10387
10146
|
const receipt = (id) => sendReceiptSchema.refine((value) => value.receiptId === id);
|
|
10388
10147
|
return {
|
|
10389
10148
|
subject: options.subject,
|
|
10149
|
+
checkAgentSources(candidates, signal) {
|
|
10150
|
+
return checkPublicAgentSources(candidates, requestFetch, signal);
|
|
10151
|
+
},
|
|
10390
10152
|
async assertCurrent(signal) {
|
|
10391
10153
|
await options.authorize();
|
|
10392
10154
|
signal.throwIfAborted();
|
|
@@ -10411,14 +10173,21 @@ function createJourneyOwnerClient(options) {
|
|
|
10411
10173
|
membersDigest: preview.membersDigest
|
|
10412
10174
|
}, "PUT");
|
|
10413
10175
|
},
|
|
10414
|
-
reserve(key, input, signal) {
|
|
10415
|
-
|
|
10176
|
+
async reserve(key, input, signal, checksumUpload = true) {
|
|
10177
|
+
const reserve = (body) => request("/me/work-bundles/uploads", sendReceiptSchema.refine((value) => receiptMatches(input, value)), signal, body, "POST", key, requestTimeoutMs$2, checksumUpload);
|
|
10178
|
+
try {
|
|
10179
|
+
return await reserve(input);
|
|
10180
|
+
} catch (error) {
|
|
10181
|
+
if (!(error instanceof JourneyRemoteError) || error.status !== 400 || error.code !== "invalid_request" || input.agentSources === void 0) throw error;
|
|
10182
|
+
const { agentSources: _, ...withoutSources } = input;
|
|
10183
|
+
return reserve(withoutSources);
|
|
10184
|
+
}
|
|
10416
10185
|
},
|
|
10417
10186
|
complete(id, signal) {
|
|
10418
10187
|
return request(`/me/work-bundles/uploads/${encodeURIComponent(id)}/complete`, receipt(id), signal, {}, "POST", void 0, completeTimeoutMs);
|
|
10419
10188
|
},
|
|
10420
|
-
status(id, signal) {
|
|
10421
|
-
return request(`/me/work-bundles/uploads/${encodeURIComponent(id)}`, receipt(id), signal, void 0, "GET");
|
|
10189
|
+
status(id, signal, checksumUpload = true) {
|
|
10190
|
+
return request(`/me/work-bundles/uploads/${encodeURIComponent(id)}`, receipt(id), signal, void 0, "GET", void 0, requestTimeoutMs$2, checksumUpload);
|
|
10422
10191
|
},
|
|
10423
10192
|
async replace(keepWorkIds, signal) {
|
|
10424
10193
|
if (keepWorkIds.length === 0) throw new ApplicationError("INVALID_STATE");
|
|
@@ -10565,6 +10334,8 @@ async function resolveItem(item, bindingId, connection, signal, snapshot = false
|
|
|
10565
10334
|
resolution
|
|
10566
10335
|
}, settings, null, snapshot);
|
|
10567
10336
|
}
|
|
10337
|
+
const agentSources = await connection.checkAgentSources?.(item.artifact.agentSources, signal);
|
|
10338
|
+
signal.throwIfAborted();
|
|
10568
10339
|
return {
|
|
10569
10340
|
...item,
|
|
10570
10341
|
target,
|
|
@@ -10572,6 +10343,7 @@ async function resolveItem(item, bindingId, connection, signal, snapshot = false
|
|
|
10572
10343
|
status: "uploading",
|
|
10573
10344
|
error: null,
|
|
10574
10345
|
reservation: {
|
|
10346
|
+
...agentSources?.length ? { agentSources } : {},
|
|
10575
10347
|
work,
|
|
10576
10348
|
target,
|
|
10577
10349
|
bundleId: item.artifact.bundleId,
|
|
@@ -10608,9 +10380,24 @@ async function perform(context, item, connection, signal) {
|
|
|
10608
10380
|
const auto = context.snapshot === true;
|
|
10609
10381
|
await guard();
|
|
10610
10382
|
if (item.settings && item.settingsConfirmed) return applySettings(item, connection, signal, guard, auto);
|
|
10611
|
-
if (!item.reservation)
|
|
10612
|
-
|
|
10613
|
-
|
|
10383
|
+
if (!item.reservation) {
|
|
10384
|
+
const next = await resolveItem(item, context.bindingId, connection, signal, auto, context.name);
|
|
10385
|
+
if (next.reservation) context.fresh?.set(item.workId, null);
|
|
10386
|
+
return next;
|
|
10387
|
+
}
|
|
10388
|
+
if (!item.receipt) {
|
|
10389
|
+
const reserved = await connection.reserve(item.operationKey, item.reservation, signal, context.checksumUploads !== false);
|
|
10390
|
+
const next = await observe(item, reserved, connection, signal, auto);
|
|
10391
|
+
if (context.fresh?.has(item.workId) && reserved.upload && next.status === "uploading") context.fresh.set(item.workId, {
|
|
10392
|
+
receiptId: reserved.receiptId,
|
|
10393
|
+
upload: reserved.upload
|
|
10394
|
+
});
|
|
10395
|
+
return next;
|
|
10396
|
+
}
|
|
10397
|
+
const fresh = context.fresh?.get(item.workId);
|
|
10398
|
+
context.fresh?.delete(item.workId);
|
|
10399
|
+
if (fresh?.receiptId === item.receipt.receiptId) return put(context, item, fresh.receiptId, fresh.upload, connection, signal, guard);
|
|
10400
|
+
const receipt = await connection.status(item.receipt.receiptId, signal, context.checksumUploads !== false);
|
|
10614
10401
|
const observed = await observe(item, receipt, connection, signal, auto);
|
|
10615
10402
|
if (receipt.state !== "awaiting_upload") return withoutUpload(observed);
|
|
10616
10403
|
if (!receipt.upload) throw new ApplicationError("INVALID_STATE");
|
|
@@ -10629,15 +10416,23 @@ async function perform(context, item, connection, signal) {
|
|
|
10629
10416
|
}
|
|
10630
10417
|
const current = await connection.resolve(item.reservation.work, signal);
|
|
10631
10418
|
if (current.scopeVersion !== item.reservation.expectedScopeVersion || current.accessVersion !== item.reservation.expectedAccessVersion) return renewed(item, null);
|
|
10419
|
+
return put(context, item, receipt.receiptId, receipt.upload, connection, signal, guard);
|
|
10420
|
+
}
|
|
10421
|
+
/** Uploads the bytes to the receipt's signed URL and completes it. */
|
|
10422
|
+
async function put(context, item, receiptId, upload, connection, signal, guard) {
|
|
10632
10423
|
await guard();
|
|
10633
|
-
await context.upload(item.artifact,
|
|
10424
|
+
await context.upload(item.artifact, upload, signal);
|
|
10634
10425
|
await guard();
|
|
10635
10426
|
try {
|
|
10636
|
-
return observe(item, await connection.complete(
|
|
10427
|
+
return observe(item, await connection.complete(receiptId, signal), connection, signal, context.snapshot === true);
|
|
10637
10428
|
} catch (error) {
|
|
10429
|
+
if (upload.headers !== void 0 && error instanceof JourneyRemoteError && error.code === "bundle_mismatch") return {
|
|
10430
|
+
...renewed(withoutUpload(item), null),
|
|
10431
|
+
checksumUploadRefused: true
|
|
10432
|
+
};
|
|
10638
10433
|
if (error instanceof Object) uploaded.set(error, {
|
|
10639
10434
|
...item,
|
|
10640
|
-
uploadedReceiptId:
|
|
10435
|
+
uploadedReceiptId: receiptId
|
|
10641
10436
|
});
|
|
10642
10437
|
throw error;
|
|
10643
10438
|
}
|
|
@@ -10705,14 +10500,16 @@ const confirmedSettings = (item) => item.settings !== null && item.settingsConfi
|
|
|
10705
10500
|
* time (ADR 0214). Assessment checks change nothing and step up to three together; a confirmed
|
|
10706
10501
|
* linked Settings change alters access versions, so it always steps alone.
|
|
10707
10502
|
*/
|
|
10708
|
-
function pickBatch(items) {
|
|
10503
|
+
function pickBatch(items, now = Date.now()) {
|
|
10709
10504
|
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);
|
|
10710
10505
|
if (uploading) return [uploading];
|
|
10711
10506
|
const assessing = items.filter((item) => item.status === "assessing");
|
|
10712
10507
|
const first = assessing[0];
|
|
10713
10508
|
if (!first) return [];
|
|
10714
10509
|
if (confirmedSettings(first)) return [first];
|
|
10715
|
-
|
|
10510
|
+
const checks = assessing.filter((item) => !confirmedSettings(item));
|
|
10511
|
+
const start = Math.floor(now / 1e3) * 3 % checks.length;
|
|
10512
|
+
return [...checks.slice(start), ...checks.slice(0, start)].slice(0, 3);
|
|
10716
10513
|
}
|
|
10717
10514
|
function isCode(error, code) {
|
|
10718
10515
|
return error instanceof ApplicationError && error.code === code;
|
|
@@ -10806,7 +10603,11 @@ async function advanceItems(host, signal) {
|
|
|
10806
10603
|
if (finished !== null) return finished;
|
|
10807
10604
|
}
|
|
10808
10605
|
if (!batch.length) return host.applied();
|
|
10809
|
-
const
|
|
10606
|
+
const checksumUploads = !send.items.some((item) => item.checksumUploadRefused);
|
|
10607
|
+
const results = await Promise.allSettled(batch.map((item) => stepItem({
|
|
10608
|
+
...host.context(item),
|
|
10609
|
+
checksumUploads
|
|
10610
|
+
}, item, connection, signal)));
|
|
10810
10611
|
const failures = results.flatMap((result) => result.status === "rejected" ? [result] : []);
|
|
10811
10612
|
if (failures.some(({ reason }) => isCode(reason, "REVISION_CONFLICT"))) return host.stale();
|
|
10812
10613
|
const first = failures[0];
|
|
@@ -10827,7 +10628,7 @@ async function advanceItems(host, signal) {
|
|
|
10827
10628
|
if (progressed) next.set(item.workId, progressed);
|
|
10828
10629
|
if (cause instanceof JourneyRemoteError && cause.status === 401 || isCode(cause, "ACCOUNT_SIGNED_OUT")) signIn = true;
|
|
10829
10630
|
else if (isCode(cause, "IDENTITY_REJECTED")) changed = true;
|
|
10830
|
-
else if (isInterrupted(cause)) {
|
|
10631
|
+
else if (isInterrupted(cause) || isTransientRefusal(cause)) {
|
|
10831
10632
|
host.failure("SEND_INTERRUPTED", cause);
|
|
10832
10633
|
error = "SEND_INTERRUPTED";
|
|
10833
10634
|
} else {
|
|
@@ -10935,8 +10736,9 @@ function withoutQuestions(send) {
|
|
|
10935
10736
|
};
|
|
10936
10737
|
return send.status === "review" ? aggregate(next) : next;
|
|
10937
10738
|
}
|
|
10938
|
-
async function advanceSend(dependencies, address, signal) {
|
|
10739
|
+
async function advanceSend(dependencies, address, signal, chain) {
|
|
10939
10740
|
const snapshot = requireSend(await dependencies.storage.loadSend(address.journeyId, address.sendId));
|
|
10741
|
+
if (chain) chain.revision = snapshot.send.revision;
|
|
10940
10742
|
if ((await dependencies.storage.loadSend(address.journeyId))?.send?.id !== snapshot.send.id) return {
|
|
10941
10743
|
...snapshot,
|
|
10942
10744
|
status: "stale"
|
|
@@ -10968,22 +10770,23 @@ async function advanceSend(dependencies, address, signal) {
|
|
|
10968
10770
|
return advanceItems({
|
|
10969
10771
|
send,
|
|
10970
10772
|
failure: dependencies.failure,
|
|
10971
|
-
connect: dependencies.auth.connect,
|
|
10773
|
+
connect: chain?.connect ?? dependencies.auth.connect,
|
|
10972
10774
|
profile: (connection, profileSignal) => prepareProfile(dependencies, snapshot, connection, profileSignal),
|
|
10973
10775
|
context: (item) => {
|
|
10974
10776
|
const binding = snapshot.journey.works.find((value) => value.id === item.workId)?.binding;
|
|
10975
10777
|
const retired = !item.receipt && binding?.sentBundleId === item.artifact.bundleId && binding.sentAccess === item.access;
|
|
10976
10778
|
return {
|
|
10977
10779
|
snapshot: true,
|
|
10780
|
+
...chain ? { fresh: chain.fresh } : {},
|
|
10978
10781
|
active: async () => {
|
|
10979
10782
|
const current = requireSend(await dependencies.storage.loadSend(snapshot.journey.journeyId));
|
|
10980
10783
|
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");
|
|
10981
10784
|
assertScope(current.journey, current.send);
|
|
10982
10785
|
},
|
|
10983
10786
|
bindingId: binding?.subject === send.subject && !retired ? binding.bundleRecordId : void 0,
|
|
10984
|
-
upload: async (artifact,
|
|
10787
|
+
upload: async (artifact, upload, uploadSignal) => {
|
|
10985
10788
|
const started = performance.now();
|
|
10986
|
-
await dependencies.artifacts.upload(snapshot.journey.journeyId, artifact,
|
|
10789
|
+
await dependencies.artifacts.upload(snapshot.journey.journeyId, artifact, upload, uploadSignal);
|
|
10987
10790
|
dependencies.uploaded?.(artifact.workId, artifact.byteLength, Math.round(performance.now() - started));
|
|
10988
10791
|
}
|
|
10989
10792
|
};
|
|
@@ -11029,6 +10832,14 @@ async function advanceSend(dependencies, address, signal) {
|
|
|
11029
10832
|
})
|
|
11030
10833
|
}, signal);
|
|
11031
10834
|
}
|
|
10835
|
+
/**
|
|
10836
|
+
* A refusal Passport answers while it or Identity is briefly unavailable. It says nothing about
|
|
10837
|
+
* the Work, so the step is retried instead of blocking it (the 2026-11-01 recorded run blocked a
|
|
10838
|
+
* Work on one `identity_unavailable`).
|
|
10839
|
+
*/
|
|
10840
|
+
function isTransientRefusal(error) {
|
|
10841
|
+
return error instanceof JourneyRemoteError && (error.status >= 500 || error.status === 429 || error.code === "identity_unavailable");
|
|
10842
|
+
}
|
|
11032
10843
|
//#endregion
|
|
11033
10844
|
//#region apps/cli/src/account/journey/decisions.ts
|
|
11034
10845
|
function decide(item, input) {
|
|
@@ -11299,10 +11110,10 @@ function createManualUploader(dependencies) {
|
|
|
11299
11110
|
active: guard(upload, item),
|
|
11300
11111
|
name: item.title,
|
|
11301
11112
|
bindingId: bindings.find((value) => value.subject === upload.subject && value.pathHash === pathHash(item))?.bundleRecordId,
|
|
11302
|
-
upload: (artifact,
|
|
11113
|
+
upload: (artifact, signedUpload, uploadSignal) => {
|
|
11303
11114
|
const slot = upload.slots[item.workId];
|
|
11304
11115
|
if (slot === void 0) throw new ApplicationError("ARTIFACT_MISSING");
|
|
11305
|
-
return dependencies.artifacts.upload(upload.resultId, slot, artifact,
|
|
11116
|
+
return dependencies.artifacts.upload(upload.resultId, slot, artifact, signedUpload, uploadSignal);
|
|
11306
11117
|
}
|
|
11307
11118
|
}),
|
|
11308
11119
|
applied: () => ({
|
|
@@ -11372,10 +11183,10 @@ function createManualUploadArtifacts(application) {
|
|
|
11372
11183
|
})
|
|
11373
11184
|
};
|
|
11374
11185
|
},
|
|
11375
|
-
async upload(resultId, slot, artifact,
|
|
11186
|
+
async upload(resultId, slot, artifact, upload, signal) {
|
|
11376
11187
|
const { row, primary } = await find(resultId, slot);
|
|
11377
11188
|
if (row.bundleId !== artifact.bundleId || primary.sha256 !== artifact.sha256 || primary.archiveBytes !== artifact.byteLength || artifact.byteLength > 5368709120) throw new ApplicationError("ARTIFACT_IDENTITY_MISMATCH");
|
|
11378
|
-
await putPrimary(primary, artifact.byteLength,
|
|
11189
|
+
await putPrimary(primary, artifact.byteLength, upload, signal);
|
|
11379
11190
|
}
|
|
11380
11191
|
};
|
|
11381
11192
|
}
|
|
@@ -12538,13 +12349,42 @@ function createJourneyFiles(storage) {
|
|
|
12538
12349
|
};
|
|
12539
12350
|
};
|
|
12540
12351
|
}
|
|
12352
|
+
/**
|
|
12353
|
+
* One advance carries the Work being uploaded through its phases with one verified connection,
|
|
12354
|
+
* instead of one phase per panel round trip, and uploads a receipt it just resolved and reserved
|
|
12355
|
+
* without reading it again (ADR 0229). Each phase is still fenced and saved on its own, so Pause, a
|
|
12356
|
+
* revision change or a failure stops the chain at the next phase exactly as between advances.
|
|
12357
|
+
* The advance returns once that Work leaves its upload phases, so the next Work starts from the
|
|
12358
|
+
* caller's next advance as before.
|
|
12359
|
+
*/
|
|
12360
|
+
async function advanceWork(dependencies, address, signal, phases = 3) {
|
|
12361
|
+
let connection;
|
|
12362
|
+
const chain = {
|
|
12363
|
+
fresh: /* @__PURE__ */ new Map(),
|
|
12364
|
+
async connect(connectSignal) {
|
|
12365
|
+
if (connection === void 0) connection = await dependencies.auth.connect(connectSignal);
|
|
12366
|
+
return connection;
|
|
12367
|
+
}
|
|
12368
|
+
};
|
|
12369
|
+
let result = await advanceSend(dependencies, address, signal, chain);
|
|
12370
|
+
for (let phase = 1; phase < phases && uploading(result, chain.revision); phase += 1) result = await advanceSend(dependencies, address, signal, chain);
|
|
12371
|
+
return result;
|
|
12372
|
+
}
|
|
12373
|
+
/** The phase saved progress and left its Work mid-upload, with nothing else to report. */
|
|
12374
|
+
function uploading(result, before) {
|
|
12375
|
+
const send = result.send;
|
|
12376
|
+
return result.status === "applied" && send !== null && before !== void 0 && send.revision > before && send.status === "sending" && send.error === null && pickBatch(send.items)[0]?.status === "uploading";
|
|
12377
|
+
}
|
|
12541
12378
|
//#endregion
|
|
12542
12379
|
//#region apps/cli/src/account/journey/send.ts
|
|
12543
12380
|
function sameIds(left, right) {
|
|
12544
12381
|
return left.length === right.length && new Set(left).size === left.length && left.every((id) => right.includes(id));
|
|
12545
12382
|
}
|
|
12546
|
-
/**
|
|
12547
|
-
|
|
12383
|
+
/**
|
|
12384
|
+
* Durable, bounded orchestration; native chat and panel use the same commands. One advance runs
|
|
12385
|
+
* at most `phases` upload phases of one Work (ADR 0229); tests pass 1 to observe each phase.
|
|
12386
|
+
*/
|
|
12387
|
+
function createJourneySender(dependencies, phases = 3) {
|
|
12548
12388
|
const running = /* @__PURE__ */ new Map();
|
|
12549
12389
|
const runKey = (address) => `${address.journeyId}:${address.sendId}`;
|
|
12550
12390
|
const load = async (address) => {
|
|
@@ -12657,7 +12497,7 @@ function createJourneySender(dependencies) {
|
|
|
12657
12497
|
const current = running.get(key);
|
|
12658
12498
|
if (current) return current.result;
|
|
12659
12499
|
const stop = new AbortController();
|
|
12660
|
-
const result =
|
|
12500
|
+
const result = advanceWork(dependencies, address, AbortSignal.any([signal, stop.signal]), phases).catch(async (error) => {
|
|
12661
12501
|
if (!stop.signal.aborted || signal.aborted) throw error;
|
|
12662
12502
|
return {
|
|
12663
12503
|
...requireSend(await dependencies.storage.loadSend(address.journeyId, address.sendId)),
|