@gitroll/tacitt-cli 0.1.0 → 0.2.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-B8DAiQ6Z.mjs} +2 -2
- package/dist/agent-setup-worker.mjs +5 -5
- package/dist/cli.mjs +108 -9
- package/dist/codex-title-worker.mjs +1 -1
- package/dist/configured-preview-worker.mjs +5 -5
- package/dist/configured-session-worker.mjs +4 -4
- package/dist/{frame-D1j9R8xX.mjs → frame-waS5FnN5.mjs} +1 -1
- package/dist/{heap-tfU61Ehy.mjs → heap-BHcvxWCw.mjs} +20 -8
- package/dist/inspection-worker.mjs +57 -0
- package/dist/{job--TceKk0l.mjs → job-J3NFw3MH.mjs} +2 -2
- package/dist/{locate-Bn32k2se.mjs → locate-U3-nkird.mjs} +29 -6
- package/dist/messages-D3m3bhva.mjs +40 -0
- package/dist/{model-7Nu7szNP.mjs → model-BJdTLNjT.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-BA0XCwnR.mjs} +486 -128
- package/dist/{run-CCATsHS2.mjs → run-BeLHsZWb.mjs} +4 -4
- package/dist/{src-DynBRDBc.mjs → src-B5SArpVY.mjs} +20 -9
- package/dist/{src-CPPjOyHZ.mjs → src-CC95LYYp.mjs} +271 -68
- package/dist/{src-BngGpFKd.mjs → src-k0Ya1Bri.mjs} +13 -7
- package/dist/store-database-worker.mjs +1 -1
- package/dist/web/assets/index-Bjos73wK.js +67 -0
- package/dist/web/assets/index-Bl4C2b2_.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/skills/tacitt/SKILL.md +13 -11
- package/skills/tacitt/references/b-method.md +1 -1
- package/skills/tacitt/references/classification.md +3 -3
- package/skills/tacitt/references/commands.md +6 -6
- package/skills/tacitt/references/journey.md +13 -11
- package/skills/tacitt/references/parallel-review.md +3 -3
- package/skills/tacitt/references/send.md +5 -5
- 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 +81 -18
- 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-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-state.d.mts +1 -0
- package/skills/tacitt/scripts/journey-state.mjs +29 -3
- package/skills/tacitt/scripts/journey-tasks.mjs +4 -2
- package/skills/tacitt/scripts/journey.d.mts +2 -0
- package/skills/tacitt/scripts/journey.mjs +154 -13
- package/skills/tacitt/scripts/privacy-validator.mjs +25 -6
- package/skills/tacitt/scripts/review.mjs +24 -3
- 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, E as prepareCollectionProduction, 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, R as gitSeedsProvider, S as readOriginalSearchFields, T as checkReusableBundle, U as readConfiguredOutputInfo, V as privateJourneyDirectory, W as processJourneyMetadataFields, X as createTemporary,
|
|
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, zt as filterInventoryByPeriod } from "./locate-
|
|
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 schemaVersion, A as inspectionQuerySchema$1, B as privateReviewHash, C as loadAuthenticatedReview, D as sessionUsefulnessRecommendation, E as aggregateUsefulness, F as reuseQuerySchema, G as initializeAndClaim, H as createJourneyPayloadFiles, I as batchQuery, J as withDatabase, K as ownership, L as isPrivateResourceKind, M as sameCandidateRequest, N as validateCandidateAttempt, O as sessionQuery, P as validateUnitCandidate, Q as checkVersion, R as privateResourceHeader, S as loadAcceptedReview, T as loadPrivateResource, U as validateBatchBody, V as batchHeader, W as requireActiveBatch, X as readStoredState, Y as readBundleDirectory, Z as validateState, _ as loadRereadBodies, _t as isSqliteCorruption, a as decodeError, at as inspectStateFiles, b as validateSessionReconciliation, c as dropsAnchor, ct as readResetMarker, d as parseDelivery, dt as syncDirectory, et as beginReset, f as validateCandidate, ft as withStateSnapshot, g as loadReconciliation, gt as isCoordinationBusy, h as loadPreparedBodies, ht as errorCode$1, i as createExecutor, it as hasResetMarker, j as evaluateUnitEvidence, k as resourceSelectionQuerySchema, l as replySchema, lt as recheckPrivatePath, m as loadPreparation, mt as StoreError, n as releaseInheritedHeapFlags, nt as databaseName, o as ProtocolError, ot as present, p as loadStatus, pt as withFileCheckTimings, q as validateDatabase, r as storeHeapLimits, rt as finishReset, s as allowedReplies, st as privatePath, tt as createDatabaseFile, u as operations, ut as stateDirectory, v as loadSessionBodies, w as loadBatch, x as validateSessionBindings, y as validatePreparationSelection, z as termReuseQuerySchema } from "./heap-
|
|
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, E as prepareCollectionProduction, 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, R as gitSeedsProvider, S as readOriginalSearchFields, T as checkReusableBundle, U as readConfiguredOutputInfo, V as privateJourneyDirectory, W as processJourneyMetadataFields, X as createTemporary, Z as createCleanup, _ as readStaticSessionMasks, a as scanReviewFields, at as windowsAclCodes, c as readSessionScanRecords, et as makePrivateDirectory, g as processJourneyMetadata, h as readCapturedOriginalRows, i as buildWorkScanBatches, it as isWindowsAclFailure, j as readCollectionIndex, k as renderJobDigest, l as scanPreparedSession, n as verifyPreparedSessionSources, nt as restrictToOwner, o as createWorkPieces, p as openConfiguredUpload, r as prepareSessionScans, rt as windowsOwnerOnly, s as readSessionScanFields, t as preparedScanConfiguration, tt as openPrivateFile, v as staticSessionJobs, w as createCollectionArtifacts, y as writeLogicalJourneyBase, z as parallelismLimits } from "./src-k0Ya1Bri.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, zt as filterInventoryByPeriod } 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-B5SArpVY.mjs";
|
|
4
|
+
import { $n as reviewRepairResourceSchema, $t as journeyPolicyRespondSchema, A as createAccount, Ar as hasCurrentStaticReview, At as sendReplaceResultSchema, B as createApplication, Bn as confirmedHideKinds, C as bReviewScope, Cn as confirmedHideAdditions, Cr as journeyClassificationSessionsSchema, Ct as sendActionSchema, D as reviewBatchReuse, Dr as nativeEventsSchema, Dt as journeySendSchema, E as rebindReviewSubmit, Er as privacyDecisionDigest, Et as sendStartSchema, F as readAccountAvatarUrl, Fr as isClassifying, Ft as obsoleteJourney, Gr as journeyRenderSchema, Gt as journeyContextQuerySchema, H as inspectionStartSchema, Hr as journeyFindingsSchema, I as storedCredentialSchema, In as bTermMatcher, Ir as isReadingComplete, Jr as selectionBatchSchema, K as inspectionQuerySchema, Kn as retireWorkDescription, Kr as commandSchema, Lr as isSelectedWork, M as deviceAuthorizationSchema, Mt as sendSettingsSchema, N as deviceProfileScopeSchema, Nr as staticSessionResourceSchema, O as guardedFindingText, On as termHideAdditions, Or as nativeGetSchema, Ot as sendArtifactSchema, P as profileUrlMatchesIdentity, Pr as hasCompleteWorkDescription, Pt as currentJourneyAdapters, Qn as reviewRepairInputSchema, Qr as stateSchema, Qt as searchScopeDigest, Rn as confirmedHideGroupIds, S as isGenericPublicTerm, St as replaceSensitiveTermsSchema, T as rebindReviewOverlay, Tn as readConfirmedHideChange, Tr as assertJourneyPrivacyReady, Tt as sendPrepareSchema, U as validateInspectionBatch, Un as journeyConfirmedHides, Ur as journeyPreviewSchema, Ut as journeyReviewStorageQuerySchema, V as inspectionCancelSchema, Vr as journeyFindingImpactSchema, Vt as applyClassification, Wn as maskedHitsStale, Wr as journeyRenderInputSchema, Wt as reviewCompileSchema, Xn as reviewCandidateInputSchema, Xt as journeySearchResultSchema, Yn as workCorrectionBinding, Yr as selectionSchema, Zn as reviewCandidateResourceSchema, Zr as producerVersion, _ as hideScopeDigest, _t as manualUploadStartSchema, a as locateTermEvidence, ai as selectFrozenConfiguration, at as appendSensitiveTerms, bt as maximumTermsFileBytes, c as bRereadDigest, cr as scanGitSeedsId, d as newJourneyWork, dr as scanExclusionResourceSchema, dt as validateFailureLocation, ei as recommendParallelism, fn as sessionRereadInputSchema, ft as manualUploadActionSchema, g as activeJourneyTerms, gt as manualUploadSchema, h as splitSelectorValues, hn as sessionReviewQuerySchema, ht as manualUploadPrepareSchema, i as scanBatchEquivalenceDigest, ii as freezeConfiguration, in as journeyPlaceSchema, j as accountProfileSchema, jr as staticReviewDigest, jt as sendResolutionSchema, k as staticallyHiddenFindings, kr as nativeRespondSchema, kt as sendReceiptSchema, l as reconcileBSession, ln as sessionReconcileInputSchema, lr as scanGitSeedsResource, lt as artifactFailureDetailSchema, m as selectedValueMembers, mn as sessionReviewInputSchema, mr as journeyInventoryQuerySchema, mt as manualUploadChoiceSchema, n as retireScanGeneration, nt as settingsQuerySchema, o as validateBCrossSegmentReview, on as reviewAttemptResourceSchema, or as scanPreparationDigest, qn as workDecisionDigest, qr as packageCommandSchema, r as scanBatchContextDigest, ri as defaultScopedSettings, rr as planBRepair, rt as readWorkspaceOutcomes, s as bCrossSegmentReviewSchema, sr as scanSessionResourceSchema, t as resetWorkScan, tn as journeyResourceQuerySchema, tr as bReviewDigest, tt as settingsBatchSchema, u as reconstructBFields, ur as verifiedScanGitSeeds, ut as resolveFailureLocation, vn as sessionUsefulnessInputSchema, vr as emptyPreparationFacts, w as rebindReviewAlignment, wt as sendChoiceSchema, xt as normalizeSensitiveTerms, y as changeJourney, yt as emptyTermsRevision, zr as withReadingProgress } from "./src-CC95LYYp.mjs";
|
|
5
|
+
import { $ as schemaVersion, A as inspectionQuerySchema$1, B as privateReviewHash, C as loadAuthenticatedReview, D as sessionUsefulnessRecommendation, E as aggregateUsefulness, F as reuseQuerySchema, G as initializeAndClaim, H as createJourneyPayloadFiles, I as batchQuery, J as withDatabase, K as ownership, L as isPrivateResourceKind, M as sameCandidateRequest, N as validateCandidateAttempt, O as sessionQuery, P as validateUnitCandidate, Q as checkVersion, R as privateResourceHeader, S as loadAcceptedReview, T as loadPrivateResource, U as validateBatchBody, V as batchHeader, W as requireActiveBatch, X as readStoredState, Y as readBundleDirectory, Z as validateState, _ as loadRereadBodies, _t as isSqliteCorruption, a as decodeError, at as inspectStateFiles, b as validateSessionReconciliation, c as dropsAnchor, ct as readResetMarker, d as parseDelivery, dt as syncDirectory, et as beginReset, f as validateCandidate, ft as withStateSnapshot, g as loadReconciliation, gt as isCoordinationBusy, h as loadPreparedBodies, ht as errorCode$1, i as createExecutor, it as hasResetMarker, j as evaluateUnitEvidence, k as resourceSelectionQuerySchema, l as replySchema, lt as recheckPrivatePath, m as loadPreparation, mt as StoreError, n as releaseInheritedHeapFlags, nt as databaseName, o as ProtocolError, ot as present, p as loadStatus, pt as withFileCheckTimings, q as validateDatabase, r as storeHeapLimits, rt as finishReset, s as allowedReplies, st as privatePath, tt as createDatabaseFile, u as operations, ut as stateDirectory, v as loadSessionBodies, w as loadBatch, x as validateSessionBindings, y as validatePreparationSelection, z as termReuseQuerySchema } from "./heap-BHcvxWCw.mjs";
|
|
6
|
+
import { a as writeJourneyOutput, c as mergeHideIndexTokens, l as packRenderHideIndex, o as createHideIndexTokens, s as hideIndexTexts, t as journeyOutputJob } from "./job-J3NFw3MH.mjs";
|
|
7
|
+
import { t as createAgentSetupPort } from "./agent-setup-B8DAiQ6Z.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";
|
|
@@ -826,12 +827,16 @@ function runObserved(frame, leg, work) {
|
|
|
826
827
|
}
|
|
827
828
|
//#endregion
|
|
828
829
|
//#region packages/local-host/src/router.ts
|
|
829
|
-
/**
|
|
830
|
+
/** Decisions the person makes in the panel while the agent waits for them. */
|
|
830
831
|
const nativePanelRespondSchema = z.object({
|
|
831
832
|
journeyId: nativeRespondSchema.shape.journeyId,
|
|
832
833
|
expectedRevision: nativeRespondSchema.shape.expectedRevision,
|
|
833
834
|
idempotencyKey: nativeRespondSchema.shape.idempotencyKey,
|
|
834
835
|
action: z.union([
|
|
836
|
+
z.object({
|
|
837
|
+
kind: z.literal("consent"),
|
|
838
|
+
allow: z.boolean()
|
|
839
|
+
}).strict(),
|
|
835
840
|
z.object({
|
|
836
841
|
kind: z.literal("work-access"),
|
|
837
842
|
workId: z.uuid(),
|
|
@@ -845,12 +850,13 @@ const nativePanelRespondSchema = z.object({
|
|
|
845
850
|
kind: z.literal("card-answer"),
|
|
846
851
|
cardId: z.uuid(),
|
|
847
852
|
optionId: z.string(),
|
|
848
|
-
remember: z.boolean().optional()
|
|
853
|
+
remember: z.boolean().optional(),
|
|
854
|
+
excludedTermKeys: z.array(z.string().min(1)).max(2e4).optional()
|
|
849
855
|
}).strict(),
|
|
850
856
|
z.object({ kind: z.literal("package-confirm") }).strict()
|
|
851
857
|
])
|
|
852
858
|
}).strict();
|
|
853
|
-
function createRouter(application, account, stopping, delivery, nativeInspection) {
|
|
859
|
+
function createRouter(application, account, stopping, delivery, nativeInspection, agentPresence) {
|
|
854
860
|
const t = initTRPC.context().create({
|
|
855
861
|
sse: {
|
|
856
862
|
ping: {
|
|
@@ -920,6 +926,7 @@ function createRouter(application, account, stopping, delivery, nativeInspection
|
|
|
920
926
|
respond: t.procedure.input(sendChoiceSchema).mutation(({ input }) => sending().respond(input)),
|
|
921
927
|
pause: t.procedure.input(sendActionSchema).mutation(({ input }) => sending().pause(input)),
|
|
922
928
|
resume: t.procedure.input(sendActionSchema).mutation(({ input }) => sending().resume(input)),
|
|
929
|
+
withdraw: t.procedure.input(sendActionSchema).mutation(({ input }) => sending().withdraw(input)),
|
|
923
930
|
advance: t.procedure.input(sendAddress).mutation(({ input }) => sending().advance(input, stopping))
|
|
924
931
|
}),
|
|
925
932
|
manualUpload: t.router({
|
|
@@ -942,6 +949,7 @@ function createRouter(application, account, stopping, delivery, nativeInspection
|
|
|
942
949
|
findingImpact: t.procedure.input(journeyFindingImpactSchema).query(({ input, signal }) => native().findingImpact(input, signal)),
|
|
943
950
|
preview: t.procedure.input(journeyPreviewSchema).query(({ input, signal }) => native().preview(input, signal)),
|
|
944
951
|
get: t.procedure.input(nativeGetSchema).query(({ input, signal }) => readThroughContention(() => native().get(input), signal)),
|
|
952
|
+
presence: t.procedure.input(z.object({ journeyId: z.uuid() }).strict()).query(async ({ input }) => ({ agent: await agentPresence?.(input.journeyId) ?? null })),
|
|
945
953
|
inventory: t.procedure.input(journeyInventoryQuerySchema).query(({ input }) => native().inventory(input)),
|
|
946
954
|
events: t.procedure.input(nativeEventsSchema).query(({ input, signal }) => readThroughContention(() => native().events(input, signal), signal))
|
|
947
955
|
}),
|
|
@@ -2827,7 +2835,7 @@ async function startHost(options) {
|
|
|
2827
2835
|
reply.raw.once("close", () => streams.delete(reply.raw));
|
|
2828
2836
|
});
|
|
2829
2837
|
const trpcOptions = {
|
|
2830
|
-
router: createRouter(options.application, options.account, stopping.signal, delivery, options.nativeInspection),
|
|
2838
|
+
router: createRouter(options.application, options.account, stopping.signal, delivery, options.nativeInspection, options.agentPresence),
|
|
2831
2839
|
createContext: ({ req }) => {
|
|
2832
2840
|
const currentView = requestObservations.get(req);
|
|
2833
2841
|
return {
|
|
@@ -5458,6 +5466,10 @@ function parseOptions(args, source = sourceCheckout) {
|
|
|
5458
5466
|
type: "boolean",
|
|
5459
5467
|
default: false
|
|
5460
5468
|
},
|
|
5469
|
+
detach: {
|
|
5470
|
+
type: "boolean",
|
|
5471
|
+
default: false
|
|
5472
|
+
},
|
|
5461
5473
|
"state-dir": { type: "string" },
|
|
5462
5474
|
"bundle-dir": { type: "string" },
|
|
5463
5475
|
"claude-home": { type: "string" },
|
|
@@ -5507,6 +5519,7 @@ function parseOptions(args, source = sourceCheckout) {
|
|
|
5507
5519
|
"all"
|
|
5508
5520
|
].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
5521
|
if (parsed.values.async && (!command || !longCommands.has(command))) throw new Error("INVALID_ARGUMENTS");
|
|
5522
|
+
if (parsed.values.detach && command !== "journey.serve") throw new Error("INVALID_ARGUMENTS");
|
|
5510
5523
|
const timeoutMs = parsed.values["timeout-ms"] === void 0 ? 3e4 : Number(parsed.values["timeout-ms"]);
|
|
5511
5524
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2147483647) throw new Error("INVALID_ARGUMENTS");
|
|
5512
5525
|
if (parsed.values.output && !stateDump) throw new Error("INVALID_ARGUMENTS");
|
|
@@ -5552,6 +5565,8 @@ ${Array.from(businessCommands).join("\n")}
|
|
|
5552
5565
|
|
|
5553
5566
|
Business commands invoke the shared Application directly without an HTTP server.
|
|
5554
5567
|
journey.serve starts a focused confirmation host, emits JSON readiness, and waits until stopped.
|
|
5568
|
+
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.
|
|
5555
5570
|
It does not discover sessions or open a browser. The agent opens its private URL in the right pane.
|
|
5556
5571
|
Workflow mutations require protocolVersion, runId and expectedRevision in JSON input.
|
|
5557
5572
|
Long commands emit the operation ID immediately as NDJSON, then wait up to 30000 ms.
|
|
@@ -7254,7 +7269,8 @@ async function saveStaticSession(storage, binding, item, result, signal) {
|
|
|
7254
7269
|
result: await describe(resultPath, signal),
|
|
7255
7270
|
primary: await describe(result.spoolPath, signal),
|
|
7256
7271
|
companion: await describe(result.companionSpoolPath, signal),
|
|
7257
|
-
masks: await describe(result.maskSpoolPath, signal)
|
|
7272
|
+
masks: await describe(result.maskSpoolPath, signal),
|
|
7273
|
+
...result.images === void 0 ? {} : { imagesOmitted: result.images }
|
|
7258
7274
|
});
|
|
7259
7275
|
await storage.saveResource(resource);
|
|
7260
7276
|
saved = true;
|
|
@@ -7413,6 +7429,7 @@ async function prepareStaticWork(options) {
|
|
|
7413
7429
|
const target = current.works.find((item) => item.id === work.id);
|
|
7414
7430
|
if (current.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
|
|
7415
7431
|
if (current.snapshotId !== snapshotId || target?.inputRevision !== work.inputRevision) throw new ApplicationError("REVISION_CONFLICT");
|
|
7432
|
+
const images = resources.map((resource) => resource.imagesOmitted);
|
|
7416
7433
|
return {
|
|
7417
7434
|
...current,
|
|
7418
7435
|
works: current.works.map((item) => item.id === work.id ? {
|
|
@@ -7420,7 +7437,8 @@ async function prepareStaticWork(options) {
|
|
|
7420
7437
|
staticReview: {
|
|
7421
7438
|
snapshotId,
|
|
7422
7439
|
inputRevision: work.inputRevision,
|
|
7423
|
-
digest: staticReviewDigest(resources)
|
|
7440
|
+
digest: staticReviewDigest(resources),
|
|
7441
|
+
...images.every((count) => count !== void 0) ? { imagesOmitted: images.reduce((sum, count) => sum + count, 0) } : {}
|
|
7424
7442
|
}
|
|
7425
7443
|
} : item)
|
|
7426
7444
|
};
|
|
@@ -7657,8 +7675,8 @@ function createJourneyPreparation(options) {
|
|
|
7657
7675
|
}));
|
|
7658
7676
|
if (!state.works.length) throw new ApplicationError("INVALID_SELECTION");
|
|
7659
7677
|
if (!scanRequested) await captureSortingScope(storage, journeyId, workApproval(plan.approval, state.works.flatMap((work) => work.rootSessionIds)), directory, abort.signal);
|
|
7660
|
-
const
|
|
7661
|
-
if (
|
|
7678
|
+
const scanWorks = scanRequested && state.scope.status === "confirmed" ? state.works : [];
|
|
7679
|
+
if (scanWorks.length) run = await runWorkers(options.workers);
|
|
7662
7680
|
const frozen = state;
|
|
7663
7681
|
const blockWork = (work, error) => changeJourney(storage, journeyId, "snapshot", (before) => {
|
|
7664
7682
|
if (before.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
|
|
@@ -7672,8 +7690,8 @@ function createJourneyPreparation(options) {
|
|
|
7672
7690
|
});
|
|
7673
7691
|
}, 100);
|
|
7674
7692
|
let submittedWorks = 0;
|
|
7675
|
-
|
|
7676
|
-
|
|
7693
|
+
let works = [];
|
|
7694
|
+
let submitted = Promise.withResolvers();
|
|
7677
7695
|
const submission = () => {
|
|
7678
7696
|
let done = false;
|
|
7679
7697
|
return () => {
|
|
@@ -7694,6 +7712,14 @@ function createJourneyPreparation(options) {
|
|
|
7694
7712
|
return false;
|
|
7695
7713
|
}
|
|
7696
7714
|
};
|
|
7715
|
+
/** D2: the person dropped this Work while it was prepared; the other Works go on. */
|
|
7716
|
+
const droppedMidScan = async (workId) => {
|
|
7717
|
+
const now = await storage.current(journeyId);
|
|
7718
|
+
const work = now?.works.find((item) => item.id === workId);
|
|
7719
|
+
return Boolean(now && now.phase !== "paused" && now.scope.status === "confirmed" && work && !isSelectedWork(work));
|
|
7720
|
+
};
|
|
7721
|
+
/** Scan generations this run prepared, including those its own isolation started. */
|
|
7722
|
+
const prepared = /* @__PURE__ */ new Set();
|
|
7697
7723
|
/** True when failing roots left the Work, which is then prepared again (ADR 0188). */
|
|
7698
7724
|
const prepareWorkOnce = async (planned, onSubmitted) => {
|
|
7699
7725
|
abort.signal.throwIfAborted();
|
|
@@ -7707,6 +7733,7 @@ function createJourneyPreparation(options) {
|
|
|
7707
7733
|
"reading",
|
|
7708
7734
|
"review"
|
|
7709
7735
|
].includes(work.status)) return false;
|
|
7736
|
+
prepared.add(`${work.id}:${work.inputRevision}`);
|
|
7710
7737
|
await prepareClassificationMetadata(storage, latest, work, options.processing, abort.signal);
|
|
7711
7738
|
let failures = collectPreparationFailures(plan.approval.inventory, work.rootSessionIds, {
|
|
7712
7739
|
journeyId,
|
|
@@ -7723,6 +7750,7 @@ function createJourneyPreparation(options) {
|
|
|
7723
7750
|
onTiming: (timing) => options.onTiming?.(planned.id, timing)
|
|
7724
7751
|
}, run.workers, (revised) => {
|
|
7725
7752
|
work = revised;
|
|
7753
|
+
prepared.add(`${revised.id}:${revised.inputRevision}`);
|
|
7726
7754
|
failures = collectPreparationFailures(plan.approval.inventory, revised.rootSessionIds, {
|
|
7727
7755
|
journeyId,
|
|
7728
7756
|
snapshotId: latest.snapshotId,
|
|
@@ -7735,7 +7763,10 @@ function createJourneyPreparation(options) {
|
|
|
7735
7763
|
const code = safeCode(error);
|
|
7736
7764
|
if (code !== "OPERATION_CANCELED") options.onFailure(error, failures.read());
|
|
7737
7765
|
abort.signal.throwIfAborted();
|
|
7738
|
-
if (code === "OPERATION_CANCELED")
|
|
7766
|
+
if (code === "OPERATION_CANCELED") {
|
|
7767
|
+
if (await droppedMidScan(planned.id)) return false;
|
|
7768
|
+
throw error;
|
|
7769
|
+
}
|
|
7739
7770
|
if (work && await isolateRoots(work, error)) return true;
|
|
7740
7771
|
if (work) await blockWork(work, error);
|
|
7741
7772
|
}
|
|
@@ -7745,14 +7776,14 @@ function createJourneyPreparation(options) {
|
|
|
7745
7776
|
for (let attempt = 0; attempt < planned.rootSessionIds.length; attempt++) if (!await prepareWorkOnce(planned, onSubmitted)) return;
|
|
7746
7777
|
};
|
|
7747
7778
|
/** Track B for one Work; its failure blocks that Work like a Track A failure would. */
|
|
7748
|
-
const staticWork = async (planned) => {
|
|
7749
|
-
await
|
|
7779
|
+
const staticWork = async (planned, passSubmitted) => {
|
|
7780
|
+
await passSubmitted;
|
|
7750
7781
|
abort.signal.throwIfAborted();
|
|
7751
7782
|
const latest = await storage.current(journeyId);
|
|
7752
7783
|
abort.signal.throwIfAborted();
|
|
7753
7784
|
if (!latest?.consent || latest.phase === "paused" || latest.scope.status !== "confirmed") throw new ApplicationError("OPERATION_CANCELED");
|
|
7754
7785
|
const work = latest.works.find((item) => item.id === planned.id);
|
|
7755
|
-
if (!work) return;
|
|
7786
|
+
if (!work || !isSelectedWork(work)) return;
|
|
7756
7787
|
try {
|
|
7757
7788
|
await inBackgroundStoreLane(() => prepareStaticWork({
|
|
7758
7789
|
storage,
|
|
@@ -7767,6 +7798,7 @@ function createJourneyPreparation(options) {
|
|
|
7767
7798
|
} catch (error) {
|
|
7768
7799
|
abort.signal.throwIfAborted();
|
|
7769
7800
|
const code = safeCode(error);
|
|
7801
|
+
if (code === "OPERATION_CANCELED" && await droppedMidScan(work.id)) return;
|
|
7770
7802
|
if (code === "COORDINATION_BUSY" || code === "OPERATION_CANCELED") throw error;
|
|
7771
7803
|
options.onFailure(error);
|
|
7772
7804
|
await blockWork(work, error);
|
|
@@ -7779,23 +7811,50 @@ function createJourneyPreparation(options) {
|
|
|
7779
7811
|
abort.abort(error);
|
|
7780
7812
|
};
|
|
7781
7813
|
const statics = [];
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7814
|
+
const pass = async (batch) => {
|
|
7815
|
+
works = batch;
|
|
7816
|
+
next = 0;
|
|
7817
|
+
submittedWorks = 0;
|
|
7818
|
+
submitted = Promise.withResolvers();
|
|
7819
|
+
if (!works.length) submitted.resolve();
|
|
7820
|
+
const passSubmitted = submitted.promise;
|
|
7821
|
+
await Promise.all(Array.from({ length: run.pool ? works.length : Math.min(1, works.length) }, async () => {
|
|
7822
|
+
for (let work = works[next++]; work && !fatal; work = works[next++]) {
|
|
7823
|
+
const planned = work;
|
|
7824
|
+
const onSubmitted = submission();
|
|
7825
|
+
try {
|
|
7826
|
+
await retryContention(() => prepareWork(planned, onSubmitted), (error) => options.onFailure(error), abort.signal);
|
|
7827
|
+
} catch (error) {
|
|
7828
|
+
stop(error);
|
|
7829
|
+
} finally {
|
|
7830
|
+
onSubmitted();
|
|
7831
|
+
}
|
|
7832
|
+
statics.push(retryContention(() => staticWork(planned, passSubmitted), (error) => options.onFailure(error), abort.signal).catch(stop));
|
|
7792
7833
|
}
|
|
7793
|
-
|
|
7834
|
+
}));
|
|
7835
|
+
while (submittedWorks < works.length) submission()();
|
|
7836
|
+
};
|
|
7837
|
+
/** Selected Works this run has not prepared yet, waiting while grouping continues (E2). */
|
|
7838
|
+
const later = async () => {
|
|
7839
|
+
for (;;) {
|
|
7840
|
+
abort.signal.throwIfAborted();
|
|
7841
|
+
const latest = await storage.current(journeyId);
|
|
7842
|
+
if (!latest || latest.phase === "paused" || latest.scope.status !== "confirmed") return [];
|
|
7843
|
+
const ready = latest.works.filter((work) => isSelectedWork(work) && [
|
|
7844
|
+
"pending",
|
|
7845
|
+
"reading",
|
|
7846
|
+
"review"
|
|
7847
|
+
].includes(work.status) && !prepared.has(`${work.id}:${work.inputRevision}`));
|
|
7848
|
+
if (ready.length || !isClassifying(latest)) return ready;
|
|
7849
|
+
await setTimeout$1(laterWorksPollMs, void 0, { signal: abort.signal }).catch(() => abort.signal.throwIfAborted());
|
|
7794
7850
|
}
|
|
7795
|
-
}
|
|
7796
|
-
|
|
7851
|
+
};
|
|
7852
|
+
for (let batch = scanWorks; batch.length && !fatal; batch = await later()) {
|
|
7853
|
+
if (!run.workers && !run.pool) run = await runWorkers(options.workers);
|
|
7854
|
+
await pass(batch);
|
|
7855
|
+
}
|
|
7797
7856
|
await Promise.all(statics);
|
|
7798
|
-
if (
|
|
7857
|
+
if (scanWorks.length) options.onTiming?.(journeyId, {
|
|
7799
7858
|
scope: "preparation",
|
|
7800
7859
|
phases: { "journey-prepare": Math.round(performance$1.now() - startedAt) },
|
|
7801
7860
|
volumes: {
|
|
@@ -7820,6 +7879,8 @@ function createJourneyPreparation(options) {
|
|
|
7820
7879
|
}
|
|
7821
7880
|
};
|
|
7822
7881
|
}
|
|
7882
|
+
/** How often a run waiting for grouping to publish more Works checks the journey (E2). */
|
|
7883
|
+
const laterWorksPollMs = 2e3;
|
|
7823
7884
|
/**
|
|
7824
7885
|
* Another process can hold the store past one admission deadline (ADR 0082); the Work runs
|
|
7825
7886
|
* again after a bounded backoff instead of stopping the pass. A pending Work re-prepares
|
|
@@ -7931,12 +7992,21 @@ const priorities = [
|
|
|
7931
7992
|
];
|
|
7932
7993
|
//#endregion
|
|
7933
7994
|
//#region apps/cli/src/journey/keywords.ts
|
|
7995
|
+
/** The journey's frozen source plan; it is saved once and never rewritten. */
|
|
7996
|
+
async function readJourneyPlan(storage, journeyId) {
|
|
7997
|
+
const plan = (await storage.resources({
|
|
7998
|
+
journeyId,
|
|
7999
|
+
kind: "plan"
|
|
8000
|
+
}))[0];
|
|
8001
|
+
if (plan?.kind !== "plan") throw new ApplicationError("CORRUPTED_STATE");
|
|
8002
|
+
return plan;
|
|
8003
|
+
}
|
|
7934
8004
|
/**
|
|
7935
8005
|
* ADR 0172: the confirmed dictionary applied once, as the last step before the ZIP — custom
|
|
7936
8006
|
* terms, the snapshot-frozen Git seeds of the given sessions' workspaces (ADR 0169, never live
|
|
7937
8007
|
* Git) and AI-confirmed Hidden values. Neither scan track applies any of these.
|
|
7938
8008
|
*/
|
|
7939
|
-
async function finalKeywordDictionary(storage, state, sessionIds) {
|
|
8009
|
+
async function finalKeywordDictionary(storage, state, sessionIds, plan) {
|
|
7940
8010
|
if (!state.snapshotId) throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
7941
8011
|
const dictionary = /* @__PURE__ */ new Map();
|
|
7942
8012
|
const add = (term, kind, bounded) => {
|
|
@@ -7951,16 +8021,12 @@ async function finalKeywordDictionary(storage, state, sessionIds) {
|
|
|
7951
8021
|
const admitted = admitCustomTerm(value);
|
|
7952
8022
|
if (admitted) add(admitted.value, "private", false);
|
|
7953
8023
|
}
|
|
7954
|
-
const
|
|
7955
|
-
journeyId: state.journeyId,
|
|
7956
|
-
kind: "plan"
|
|
7957
|
-
}))[0];
|
|
7958
|
-
if (plan?.kind !== "plan") throw new ApplicationError("CORRUPTED_STATE");
|
|
8024
|
+
const { approval } = plan ?? await readJourneyPlan(storage, state.journeyId);
|
|
7959
8025
|
const sessions = new Set(sessionIds);
|
|
7960
8026
|
const roots = /* @__PURE__ */ new Map();
|
|
7961
|
-
for (const session of
|
|
8027
|
+
for (const session of approval.inventory.sessions) {
|
|
7962
8028
|
if (!sessions.has(session.id)) continue;
|
|
7963
|
-
const workspace =
|
|
8029
|
+
const workspace = approval.inventory.workspaces.find((item) => item.id === session.workspaceId);
|
|
7964
8030
|
if (!workspace) throw new ApplicationError("CORRUPTED_STATE");
|
|
7965
8031
|
const paths = workspace.path === null ? [] : [workspace.path];
|
|
7966
8032
|
roots.set(JSON.stringify(paths), paths);
|
|
@@ -8486,7 +8552,7 @@ function createJourneyRenderer(options) {
|
|
|
8486
8552
|
work
|
|
8487
8553
|
};
|
|
8488
8554
|
}
|
|
8489
|
-
async function produce(state, work, signal, write, session) {
|
|
8555
|
+
async function produce(state, work, signal, write, session, plan) {
|
|
8490
8556
|
const { journeyId } = state;
|
|
8491
8557
|
const workId = work.id;
|
|
8492
8558
|
assertJourneyPrivacyReady(state, true);
|
|
@@ -8518,12 +8584,12 @@ function createJourneyRenderer(options) {
|
|
|
8518
8584
|
});
|
|
8519
8585
|
const next = moved.works.find((item) => item.id === workId);
|
|
8520
8586
|
if (!next) throw new ApplicationError("REVISION_CONFLICT");
|
|
8521
|
-
return produce(moved, next, signal, write, session);
|
|
8587
|
+
return produce(moved, next, signal, write, session, plan);
|
|
8522
8588
|
}
|
|
8523
8589
|
if (!output) {
|
|
8524
8590
|
const directory = await journeyDirectory(options.stateDir, journeyId);
|
|
8525
8591
|
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);
|
|
8592
|
+
const { base, tokens, confirmed, request } = await journeyOutputRequest(storage, state, work, options.processing, signal, await plan());
|
|
8527
8593
|
const { output: written, tokens: collected } = await write({
|
|
8528
8594
|
request: {
|
|
8529
8595
|
...request,
|
|
@@ -8614,10 +8680,10 @@ function createJourneyRenderer(options) {
|
|
|
8614
8680
|
};
|
|
8615
8681
|
});
|
|
8616
8682
|
}
|
|
8617
|
-
async function render(journeyId, workId, signal, write = writeJourneyOutput, session = directRenderSession(storage, journeyId)) {
|
|
8683
|
+
async function render(journeyId, workId, signal, write = writeJourneyOutput, session = directRenderSession(storage, journeyId), plan = () => readJourneyPlan(storage, journeyId)) {
|
|
8618
8684
|
const { state, work } = await admit(workId, session);
|
|
8619
8685
|
try {
|
|
8620
|
-
return await produce(state, work, signal, write, session);
|
|
8686
|
+
return await produce(state, work, signal, write, session, plan);
|
|
8621
8687
|
} catch (error) {
|
|
8622
8688
|
if (signal.aborted) throw error;
|
|
8623
8689
|
const renderFailure = {
|
|
@@ -8650,9 +8716,14 @@ function createJourneyRenderer(options) {
|
|
|
8650
8716
|
if (!workers || works < 2 || size < 1) return void 0;
|
|
8651
8717
|
const pool = createRenderWorkers(workers, size);
|
|
8652
8718
|
const session = batchedRenderSession(storage, journeyId);
|
|
8719
|
+
let plan;
|
|
8720
|
+
const readPlan = () => plan ??= readJourneyPlan(storage, journeyId).catch((error) => {
|
|
8721
|
+
plan = void 0;
|
|
8722
|
+
throw error;
|
|
8723
|
+
});
|
|
8653
8724
|
return {
|
|
8654
8725
|
concurrency: size + 1,
|
|
8655
|
-
render: (workId) => render(journeyId, workId, signal, pool.write, session),
|
|
8726
|
+
render: (workId) => render(journeyId, workId, signal, pool.write, session, readPlan),
|
|
8656
8727
|
close: pool.close
|
|
8657
8728
|
};
|
|
8658
8729
|
}
|
|
@@ -8662,7 +8733,7 @@ function createJourneyRenderer(options) {
|
|
|
8662
8733
|
};
|
|
8663
8734
|
}
|
|
8664
8735
|
/** Package-only native output request: the final keyword dictionary covers all retained fields. */
|
|
8665
|
-
async function journeyOutputRequest(storage, state, work, processingConfig, _signal) {
|
|
8736
|
+
async function journeyOutputRequest(storage, state, work, processingConfig, _signal, plan) {
|
|
8666
8737
|
const { journeyId } = state;
|
|
8667
8738
|
const workId = work.id;
|
|
8668
8739
|
const base = (await storage.resources({
|
|
@@ -8670,14 +8741,11 @@ async function journeyOutputRequest(storage, state, work, processingConfig, _sig
|
|
|
8670
8741
|
kind: "base",
|
|
8671
8742
|
id: `${workId}:${work.inputRevision}`
|
|
8672
8743
|
})).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");
|
|
8744
|
+
if (base?.kind !== "base") throw new ApplicationError("CORRUPTED_STATE");
|
|
8745
|
+
const { replacementKey } = plan ?? await readJourneyPlan(storage, journeyId);
|
|
8678
8746
|
const hides = createFamilyHides();
|
|
8679
8747
|
const confirmed = await journeyConfirmedHides(storage, state);
|
|
8680
|
-
const dictionary = await finalKeywordDictionary(storage, state, base.sessions.map((session) => session.applicationSessionId));
|
|
8748
|
+
const dictionary = await finalKeywordDictionary(storage, state, base.sessions.map((session) => session.applicationSessionId), plan);
|
|
8681
8749
|
const exceptions = workExceptions(state, workId);
|
|
8682
8750
|
const tokens = createHideIndexTokens();
|
|
8683
8751
|
const title = await renderTitle(storage, state, work);
|
|
@@ -8692,7 +8760,7 @@ async function journeyOutputRequest(storage, state, work, processingConfig, _sig
|
|
|
8692
8760
|
companion: base.companion,
|
|
8693
8761
|
workId,
|
|
8694
8762
|
baseBundleId: base.bundleId,
|
|
8695
|
-
replacementKey
|
|
8763
|
+
replacementKey,
|
|
8696
8764
|
edits: [],
|
|
8697
8765
|
families: hides.request(base.sessions, dictionary, exceptions),
|
|
8698
8766
|
metadata: {
|
|
@@ -9458,14 +9526,21 @@ async function regroupPrivacySources(storage, state, works, changedMetadata, sig
|
|
|
9458
9526
|
}
|
|
9459
9527
|
//#endregion
|
|
9460
9528
|
//#region apps/cli/src/journey/works/proposal.ts
|
|
9461
|
-
/**
|
|
9529
|
+
/**
|
|
9530
|
+
* One atomic root partition; recommendations have no access authority. Roots still being grouped
|
|
9531
|
+
* 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).
|
|
9533
|
+
*/
|
|
9462
9534
|
function createClassificationProposal(storage) {
|
|
9463
9535
|
return async (input, signal) => {
|
|
9464
9536
|
signal.throwIfAborted();
|
|
9465
9537
|
const state = await storage.current(input.journeyId);
|
|
9466
9538
|
if (!state?.consent) throw new ApplicationError("CONFIRMATION_REQUIRED");
|
|
9539
|
+
const classifying = isClassifying(state);
|
|
9540
|
+
const late = state.scope.status === "confirmed";
|
|
9541
|
+
const bound = input.expectedClassificationRevision !== void 0 && classifying && input.expectedClassificationRevision === state.classificationRevision;
|
|
9467
9542
|
const commit = (next = state) => storage.commit({
|
|
9468
|
-
expectedRevision: input.expectedRevision,
|
|
9543
|
+
expectedRevision: bound ? state.revision : input.expectedRevision,
|
|
9469
9544
|
requestKey: input.idempotencyKey,
|
|
9470
9545
|
requestHash: textDigest(JSON.stringify(input)),
|
|
9471
9546
|
state: next,
|
|
@@ -9476,8 +9551,8 @@ function createClassificationProposal(storage) {
|
|
|
9476
9551
|
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
9477
9552
|
}
|
|
9478
9553
|
});
|
|
9479
|
-
if (state.revision !== input.expectedRevision) return commit();
|
|
9480
|
-
if (
|
|
9554
|
+
if (input.expectedClassificationRevision !== void 0 ? !bound : state.revision !== input.expectedRevision) return commit();
|
|
9555
|
+
if (late && !classifying) throw new ApplicationError("INVALID_TRANSITION");
|
|
9481
9556
|
if (state.phase === "paused") throw new ApplicationError("OPERATION_CANCELED");
|
|
9482
9557
|
if (!state.snapshotId || state.pendingHideId) throw new ApplicationError("SCOPE_NOT_READY");
|
|
9483
9558
|
const plan = (await storage.resources({
|
|
@@ -9503,6 +9578,11 @@ function createClassificationProposal(storage) {
|
|
|
9503
9578
|
});
|
|
9504
9579
|
} else merged.set(key, item);
|
|
9505
9580
|
}
|
|
9581
|
+
const pending = input.pendingRootSessionIds ?? [];
|
|
9582
|
+
for (const root of pending) {
|
|
9583
|
+
if (!roots.has(root) || seen.has(root)) throw new ApplicationError("INVALID_SELECTION");
|
|
9584
|
+
seen.add(root);
|
|
9585
|
+
}
|
|
9506
9586
|
if (seen.size !== roots.size) throw new ApplicationError("INVALID_SELECTION");
|
|
9507
9587
|
const families = reviewFamilies(plan.approval.inventory);
|
|
9508
9588
|
const captures = await storage.resources({
|
|
@@ -9515,7 +9595,18 @@ function createClassificationProposal(storage) {
|
|
|
9515
9595
|
signal.throwIfAborted();
|
|
9516
9596
|
const sessions = item.rootSessionIds.flatMap((root) => families.members.get(root) ?? []);
|
|
9517
9597
|
if (sessions.some((session) => session.parts.some((part) => !captured.has(part.id)))) throw new ApplicationError("SCOPE_NOT_READY");
|
|
9518
|
-
const
|
|
9598
|
+
const existing = state.works.find((work) => work.placement !== "classifying" && work.rootSessionIds.length === item.rootSessionIds.length && work.rootSessionIds.every((root) => item.rootSessionIds.includes(root)));
|
|
9599
|
+
const extended = late ? state.works.find((work) => work.placement === "placed" && work.rootSessionIds.every((root) => item.rootSessionIds.includes(root))) : void 0;
|
|
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"));
|
|
9519
9610
|
const { classificationMetadata: _display, ...original } = base;
|
|
9520
9611
|
works.push({
|
|
9521
9612
|
...original,
|
|
@@ -9528,12 +9619,22 @@ function createClassificationProposal(storage) {
|
|
|
9528
9619
|
origin: "model"
|
|
9529
9620
|
},
|
|
9530
9621
|
placement: "placed",
|
|
9622
|
+
...late && !extended ? {
|
|
9623
|
+
access: item.recommendation.access,
|
|
9624
|
+
accessOrigin: "model",
|
|
9625
|
+
accessConfirmed: true
|
|
9626
|
+
} : {},
|
|
9531
9627
|
classificationRecommendation: item.recommendation,
|
|
9532
9628
|
suggestion: null,
|
|
9533
9629
|
artifact: null,
|
|
9534
9630
|
revision: base.revision + 1
|
|
9535
9631
|
});
|
|
9536
9632
|
}
|
|
9633
|
+
if (pending.length) {
|
|
9634
|
+
const prior = state.works.find((work) => work.placement === "classifying" && work.rootSessionIds.length === pending.length && work.rootSessionIds.every((root) => pending.includes(root)));
|
|
9635
|
+
const sessions = pending.flatMap((root) => families.members.get(root) ?? []);
|
|
9636
|
+
works.push(prior ?? newJourneyWork(randomUUID(), "Still being grouped", pending, [...new Set(sessions.map((session) => session.workspaceId))], "classifying"));
|
|
9637
|
+
}
|
|
9537
9638
|
const retained = new Set(works.map((work) => work.id));
|
|
9538
9639
|
const changedMetadata = new Set(works.filter((work) => {
|
|
9539
9640
|
const old = state.works.find((item) => item.id === work.id);
|
|
@@ -9549,7 +9650,7 @@ function createClassificationProposal(storage) {
|
|
|
9549
9650
|
old.classificationRecommendation?.reason
|
|
9550
9651
|
]);
|
|
9551
9652
|
}).map((work) => work.id));
|
|
9552
|
-
const regrouped = state.works.some((work) => !retained.has(work.id)) || changedMetadata.size > 0;
|
|
9653
|
+
const regrouped = !late && (state.works.some((work) => !retained.has(work.id)) || changedMetadata.size > 0);
|
|
9553
9654
|
const cardTerms = new Set(state.privacy.cards.flatMap((card) => card.termKeys));
|
|
9554
9655
|
const preserved = await regroupPrivacySources(storage, state, works, changedMetadata, signal);
|
|
9555
9656
|
const forced = new Set(preserved.filter((term) => !termTypeInfo[term.type].asked).map((term) => term.value));
|
|
@@ -9562,7 +9663,7 @@ function createClassificationProposal(storage) {
|
|
|
9562
9663
|
...state,
|
|
9563
9664
|
works,
|
|
9564
9665
|
classificationRevision: state.revision + 1,
|
|
9565
|
-
scope: {
|
|
9666
|
+
scope: late ? state.scope : {
|
|
9566
9667
|
status: "pending",
|
|
9567
9668
|
actor: null,
|
|
9568
9669
|
at: null
|
|
@@ -9577,7 +9678,7 @@ function createClassificationProposal(storage) {
|
|
|
9577
9678
|
confirmedAnswersRevision: null
|
|
9578
9679
|
} : {}
|
|
9579
9680
|
},
|
|
9580
|
-
readingComplete: works.filter((work) => work.access !== "drop").every((work) => work.totalBatches > 0 && work.completedBatches === work.totalBatches),
|
|
9681
|
+
readingComplete: !pending.length && works.filter((work) => work.access !== "drop").every((work) => work.totalBatches > 0 && work.completedBatches === work.totalBatches),
|
|
9581
9682
|
phase: "processing"
|
|
9582
9683
|
});
|
|
9583
9684
|
};
|
|
@@ -10511,12 +10612,45 @@ async function perform(context, item, connection, signal) {
|
|
|
10511
10612
|
if (!item.receipt) return observe(item, await connection.reserve(item.operationKey, item.reservation, signal), connection, signal, auto);
|
|
10512
10613
|
const receipt = await connection.status(item.receipt.receiptId, signal);
|
|
10513
10614
|
const observed = await observe(item, receipt, connection, signal, auto);
|
|
10514
|
-
if (receipt.state !== "awaiting_upload") return observed;
|
|
10615
|
+
if (receipt.state !== "awaiting_upload") return withoutUpload(observed);
|
|
10515
10616
|
if (!receipt.upload) throw new ApplicationError("INVALID_STATE");
|
|
10617
|
+
if (item.uploadedReceiptId === receipt.receiptId) {
|
|
10618
|
+
await guard();
|
|
10619
|
+
try {
|
|
10620
|
+
return withoutUpload(await observe(item, await connection.complete(receipt.receiptId, signal), connection, signal, auto));
|
|
10621
|
+
} catch (error) {
|
|
10622
|
+
if (!(error instanceof JourneyRemoteError) || error.status === 401 || [
|
|
10623
|
+
"settings_changed",
|
|
10624
|
+
"target_conflict",
|
|
10625
|
+
"work_removed"
|
|
10626
|
+
].includes(error.code)) throw error;
|
|
10627
|
+
return withoutUpload(item);
|
|
10628
|
+
}
|
|
10629
|
+
}
|
|
10630
|
+
const current = await connection.resolve(item.reservation.work, signal);
|
|
10631
|
+
if (current.scopeVersion !== item.reservation.expectedScopeVersion || current.accessVersion !== item.reservation.expectedAccessVersion) return renewed(item, null);
|
|
10516
10632
|
await guard();
|
|
10517
10633
|
await context.upload(item.artifact, receipt.upload.uploadUrl, signal);
|
|
10518
10634
|
await guard();
|
|
10519
|
-
|
|
10635
|
+
try {
|
|
10636
|
+
return observe(item, await connection.complete(receipt.receiptId, signal), connection, signal, auto);
|
|
10637
|
+
} catch (error) {
|
|
10638
|
+
if (error instanceof Object) uploaded.set(error, {
|
|
10639
|
+
...item,
|
|
10640
|
+
uploadedReceiptId: receipt.receiptId
|
|
10641
|
+
});
|
|
10642
|
+
throw error;
|
|
10643
|
+
}
|
|
10644
|
+
}
|
|
10645
|
+
const uploaded = /* @__PURE__ */ new WeakMap();
|
|
10646
|
+
/** The item to save for a failed step whose bytes were already uploaded, if any. */
|
|
10647
|
+
function uploadedItem(error) {
|
|
10648
|
+
return error instanceof Object ? uploaded.get(error) : void 0;
|
|
10649
|
+
}
|
|
10650
|
+
function withoutUpload(item) {
|
|
10651
|
+
if (item.uploadedReceiptId === void 0) return item;
|
|
10652
|
+
const { uploadedReceiptId: _, ...rest } = item;
|
|
10653
|
+
return rest;
|
|
10520
10654
|
}
|
|
10521
10655
|
/** A definite stale refusal can renew the intent; unknown outcomes always retry the same key. */
|
|
10522
10656
|
async function stepItem(context, item, connection, signal) {
|
|
@@ -10546,31 +10680,39 @@ async function stepItem(context, item, connection, signal) {
|
|
|
10546
10680
|
status: "blocked",
|
|
10547
10681
|
error: "WORK_REMOVED"
|
|
10548
10682
|
};
|
|
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
|
-
};
|
|
10683
|
+
return renewed(item, "SETTINGS_CHANGED");
|
|
10559
10684
|
}
|
|
10560
10685
|
}
|
|
10686
|
+
/** A fresh intent for the same Work: the old receipt is kept as history and resolve runs again. */
|
|
10687
|
+
function renewed(item, error) {
|
|
10688
|
+
return {
|
|
10689
|
+
...withoutUpload(item),
|
|
10690
|
+
operationKey: randomUUID(),
|
|
10691
|
+
reservation: null,
|
|
10692
|
+
resolution: null,
|
|
10693
|
+
previousReceipts: item.receipt ? [...item.previousReceipts, item.receipt] : item.previousReceipts,
|
|
10694
|
+
receipt: null,
|
|
10695
|
+
status: "ready",
|
|
10696
|
+
error
|
|
10697
|
+
};
|
|
10698
|
+
}
|
|
10561
10699
|
const confirmedSettings = (item) => item.settings !== null && item.settingsConfirmed;
|
|
10562
10700
|
/**
|
|
10563
|
-
* The Works one advance steps together.
|
|
10564
|
-
*
|
|
10565
|
-
*
|
|
10701
|
+
* The Works one advance steps together. Upload work comes before assessment checks, smallest
|
|
10702
|
+
* ZIP first. Completing
|
|
10703
|
+
* an upload advances the owner's scope version, which every resolution and reservation carries,
|
|
10704
|
+
* so one Work's resolve-to-complete steps never overlap another's: uploads step one Work at a
|
|
10705
|
+
* time (ADR 0214). Assessment checks change nothing and step up to three together; a confirmed
|
|
10706
|
+
* linked Settings change alters access versions, so it always steps alone.
|
|
10566
10707
|
*/
|
|
10567
10708
|
function pickBatch(items) {
|
|
10568
|
-
const uploading = items.
|
|
10569
|
-
|
|
10570
|
-
const
|
|
10709
|
+
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
|
+
if (uploading) return [uploading];
|
|
10711
|
+
const assessing = items.filter((item) => item.status === "assessing");
|
|
10712
|
+
const first = assessing[0];
|
|
10571
10713
|
if (!first) return [];
|
|
10572
10714
|
if (confirmedSettings(first)) return [first];
|
|
10573
|
-
return
|
|
10715
|
+
return assessing.filter((item) => !confirmedSettings(item)).slice(0, 3);
|
|
10574
10716
|
}
|
|
10575
10717
|
function isCode(error, code) {
|
|
10576
10718
|
return error instanceof ApplicationError && error.code === code;
|
|
@@ -10681,6 +10823,8 @@ async function advanceItems(host, signal) {
|
|
|
10681
10823
|
continue;
|
|
10682
10824
|
}
|
|
10683
10825
|
const cause = result.reason;
|
|
10826
|
+
const progressed = uploadedItem(cause);
|
|
10827
|
+
if (progressed) next.set(item.workId, progressed);
|
|
10684
10828
|
if (cause instanceof JourneyRemoteError && cause.status === 401 || isCode(cause, "ACCOUNT_SIGNED_OUT")) signIn = true;
|
|
10685
10829
|
else if (isCode(cause, "IDENTITY_REJECTED")) changed = true;
|
|
10686
10830
|
else if (isInterrupted(cause)) {
|
|
@@ -10717,7 +10861,7 @@ async function advanceItems(host, signal) {
|
|
|
10717
10861
|
items
|
|
10718
10862
|
}));
|
|
10719
10863
|
} catch (error) {
|
|
10720
|
-
if (!signal.aborted && (isInterrupted(error) || error instanceof Error && isInterrupted(error.cause))) {
|
|
10864
|
+
if (!signal.aborted && (isInterrupted(error) || error instanceof Error && isInterrupted(error.cause) || isCode(error, "IDENTITY_UNAVAILABLE"))) {
|
|
10721
10865
|
host.failure("SEND_INTERRUPTED", error);
|
|
10722
10866
|
return send.error === "SEND_INTERRUPTED" ? host.applied() : host.save({
|
|
10723
10867
|
...send,
|
|
@@ -10837,7 +10981,11 @@ async function advanceSend(dependencies, address, signal) {
|
|
|
10837
10981
|
assertScope(current.journey, current.send);
|
|
10838
10982
|
},
|
|
10839
10983
|
bindingId: binding?.subject === send.subject && !retired ? binding.bundleRecordId : void 0,
|
|
10840
|
-
upload: (artifact, uploadUrl, uploadSignal) =>
|
|
10984
|
+
upload: async (artifact, uploadUrl, uploadSignal) => {
|
|
10985
|
+
const started = performance.now();
|
|
10986
|
+
await dependencies.artifacts.upload(snapshot.journey.journeyId, artifact, uploadUrl, uploadSignal);
|
|
10987
|
+
dependencies.uploaded?.(artifact.workId, artifact.byteLength, Math.round(performance.now() - started));
|
|
10988
|
+
}
|
|
10841
10989
|
};
|
|
10842
10990
|
},
|
|
10843
10991
|
finish: async (connection, finishSignal) => {
|
|
@@ -12481,6 +12629,20 @@ function createJourneySender(dependencies) {
|
|
|
12481
12629
|
if (result.status !== "stale") running.get(runKey(input))?.stop.abort();
|
|
12482
12630
|
return result;
|
|
12483
12631
|
},
|
|
12632
|
+
/** BL-201: Cancel sign-in returns a confirmed Send that uploaded nothing to Ready to send. */
|
|
12633
|
+
async withdraw(raw) {
|
|
12634
|
+
const input = sendActionSchema.parse(raw);
|
|
12635
|
+
const snapshot = await load(input);
|
|
12636
|
+
if (!matchesAction(snapshot, input)) return saveSend(dependencies, snapshot, snapshot.send, input);
|
|
12637
|
+
if (snapshot.send.status !== "sign-in" || snapshot.send.items.some((item) => item.receipt)) throw new ApplicationError("INVALID_TRANSITION");
|
|
12638
|
+
const result = await saveSend(dependencies, snapshot, {
|
|
12639
|
+
...snapshot.send,
|
|
12640
|
+
status: "prepared",
|
|
12641
|
+
confirmedAt: null
|
|
12642
|
+
}, input);
|
|
12643
|
+
if (result.status !== "stale") running.get(runKey(input))?.stop.abort();
|
|
12644
|
+
return result;
|
|
12645
|
+
},
|
|
12484
12646
|
async resume(raw) {
|
|
12485
12647
|
const input = sendActionSchema.parse(raw);
|
|
12486
12648
|
const snapshot = await load(input);
|
|
@@ -12512,6 +12674,10 @@ function createJourneySender(dependencies) {
|
|
|
12512
12674
|
}
|
|
12513
12675
|
//#endregion
|
|
12514
12676
|
//#region apps/cli/src/account/journey/auth.ts
|
|
12677
|
+
/** An access token this close to expiry is renewed first, so a step never starts on it. */
|
|
12678
|
+
const renewBeforeMs = 12e4;
|
|
12679
|
+
const renewTimeoutMs = 3e4;
|
|
12680
|
+
const renewals = /* @__PURE__ */ new WeakMap();
|
|
12515
12681
|
/** Reuses the existing device sign-in credential; a token is never returned to the panel or Agent. */
|
|
12516
12682
|
function createJourneySendAuth(account, requestFetch) {
|
|
12517
12683
|
/**
|
|
@@ -12528,31 +12694,87 @@ function createJourneySendAuth(account, requestFetch) {
|
|
|
12528
12694
|
return state;
|
|
12529
12695
|
});
|
|
12530
12696
|
}
|
|
12697
|
+
/** A refused access token with a refresh token is renewed on the next use instead (BL-210). */
|
|
12698
|
+
async function refuse(accessToken) {
|
|
12699
|
+
if (!(await account.credentials.update((value) => {
|
|
12700
|
+
if (value.credential?.accessToken === accessToken && value.credential.refreshToken) value.credential = {
|
|
12701
|
+
...value.credential,
|
|
12702
|
+
expiresAt: account.now()
|
|
12703
|
+
};
|
|
12704
|
+
return value;
|
|
12705
|
+
})).credential?.refreshToken) await expire(accessToken);
|
|
12706
|
+
}
|
|
12707
|
+
function renew(credential) {
|
|
12708
|
+
const running = renewals.get(account);
|
|
12709
|
+
if (running) return running;
|
|
12710
|
+
const renewing = (async () => {
|
|
12711
|
+
try {
|
|
12712
|
+
const token = await account.identity.refresh(credential.refreshToken, AbortSignal.timeout(renewTimeoutMs));
|
|
12713
|
+
if (!token) {
|
|
12714
|
+
await expire(credential.accessToken);
|
|
12715
|
+
return null;
|
|
12716
|
+
}
|
|
12717
|
+
return (await account.credentials.update((value) => {
|
|
12718
|
+
if (value.credential?.refreshToken === credential.refreshToken) value.credential = {
|
|
12719
|
+
...value.credential,
|
|
12720
|
+
accessToken: token.accessToken,
|
|
12721
|
+
refreshToken: token.refreshToken ?? credential.refreshToken,
|
|
12722
|
+
scope: token.scope,
|
|
12723
|
+
issuedAt: account.now(),
|
|
12724
|
+
expiresAt: token.expiresAt
|
|
12725
|
+
};
|
|
12726
|
+
return value;
|
|
12727
|
+
})).credential;
|
|
12728
|
+
} finally {
|
|
12729
|
+
renewals.delete(account);
|
|
12730
|
+
}
|
|
12731
|
+
})();
|
|
12732
|
+
renewals.set(account, renewing);
|
|
12733
|
+
return renewing;
|
|
12734
|
+
}
|
|
12531
12735
|
async function current() {
|
|
12532
12736
|
const credential = (await account.credentials.update((value) => value)).credential;
|
|
12533
12737
|
if (!credential || credential.identityUrl !== account.configuration.identityUrl || credential.clientId !== account.configuration.clientId) return null;
|
|
12534
|
-
|
|
12738
|
+
const left = Date.parse(credential.expiresAt) - Date.parse(account.now());
|
|
12739
|
+
if (Number.isFinite(left) && left > renewBeforeMs) return credential;
|
|
12740
|
+
const { refreshToken } = credential;
|
|
12741
|
+
if (!refreshToken) {
|
|
12742
|
+
if (Number.isFinite(left) && left > 0) return credential;
|
|
12535
12743
|
await expire(credential.accessToken);
|
|
12536
12744
|
return null;
|
|
12537
12745
|
}
|
|
12538
|
-
|
|
12746
|
+
try {
|
|
12747
|
+
return await renew({
|
|
12748
|
+
...credential,
|
|
12749
|
+
refreshToken
|
|
12750
|
+
});
|
|
12751
|
+
} catch (error) {
|
|
12752
|
+
if (Number.isFinite(left) && left > 0) return credential;
|
|
12753
|
+
throw error;
|
|
12754
|
+
}
|
|
12539
12755
|
}
|
|
12540
12756
|
const fetchOwner = requestFetch ?? fetch;
|
|
12541
12757
|
const ownerFetch = async (input, init) => {
|
|
12542
12758
|
const response = await fetchOwner(input, init);
|
|
12543
12759
|
if (response.status === 401) {
|
|
12544
12760
|
const header = new Headers(init?.headers).get("authorization");
|
|
12545
|
-
if (header?.startsWith("Bearer ")) await
|
|
12761
|
+
if (header?.startsWith("Bearer ")) await refuse(header.slice(7));
|
|
12546
12762
|
}
|
|
12547
12763
|
return response;
|
|
12548
12764
|
};
|
|
12549
12765
|
return { async connect(signal) {
|
|
12550
|
-
|
|
12766
|
+
let credential = await current();
|
|
12551
12767
|
if (!credential) return null;
|
|
12552
|
-
|
|
12768
|
+
let verified = await account.identity.readIdentity(credential.accessToken, signal);
|
|
12553
12769
|
if (!verified) {
|
|
12554
|
-
await
|
|
12555
|
-
|
|
12770
|
+
await refuse(credential.accessToken);
|
|
12771
|
+
credential = await current();
|
|
12772
|
+
if (!credential) return null;
|
|
12773
|
+
verified = await account.identity.readIdentity(credential.accessToken, signal);
|
|
12774
|
+
if (!verified) {
|
|
12775
|
+
await expire(credential.accessToken);
|
|
12776
|
+
return null;
|
|
12777
|
+
}
|
|
12556
12778
|
}
|
|
12557
12779
|
if (verified.identityId !== credential.identityId) throw new ApplicationError("IDENTITY_REJECTED");
|
|
12558
12780
|
return createJourneyOwnerClient({
|
|
@@ -13097,6 +13319,7 @@ const deviceCodeResponse = z.object({
|
|
|
13097
13319
|
});
|
|
13098
13320
|
const tokenResponse = z.object({
|
|
13099
13321
|
access_token: z.string().min(1).max(4096),
|
|
13322
|
+
refresh_token: z.string().min(1).max(4096).optional(),
|
|
13100
13323
|
token_type: z.string(),
|
|
13101
13324
|
expires_in: z.number().int().positive(),
|
|
13102
13325
|
scope: deviceProfileScopeSchema
|
|
@@ -13108,13 +13331,24 @@ const userInfoResponse = z.object({
|
|
|
13108
13331
|
picture: z.unknown().optional(),
|
|
13109
13332
|
profile: z.string()
|
|
13110
13333
|
});
|
|
13111
|
-
|
|
13112
|
-
|
|
13334
|
+
/**
|
|
13335
|
+
* `detail` names the endpoint and what failed (HTTP status, network error name, schema paths),
|
|
13336
|
+
* never a response value, so the authorization log can say why without exposing a token.
|
|
13337
|
+
*/
|
|
13338
|
+
function unavailable(detail, cause) {
|
|
13339
|
+
const error = Object.assign(new ApplicationError("IDENTITY_UNAVAILABLE"), { detail });
|
|
13113
13340
|
if (cause !== void 0) error.cause = cause;
|
|
13114
13341
|
return error;
|
|
13115
13342
|
}
|
|
13116
|
-
|
|
13117
|
-
|
|
13343
|
+
const issuePaths = (error) => error.issues.map((issue) => issue.path.join(".") || "(root)").join(",");
|
|
13344
|
+
function networkDetail(error) {
|
|
13345
|
+
if (!(error instanceof Error)) return "Error";
|
|
13346
|
+
const cause = error.cause;
|
|
13347
|
+
const code = typeof cause === "object" && cause !== null && "code" in cause ? cause.code : null;
|
|
13348
|
+
return typeof code === "string" ? `${error.name} ${code}` : error.name;
|
|
13349
|
+
}
|
|
13350
|
+
async function readJson(response, where) {
|
|
13351
|
+
if (!response.body) throw unavailable(`${where} empty body`);
|
|
13118
13352
|
const reader = response.body.getReader();
|
|
13119
13353
|
const chunks = [];
|
|
13120
13354
|
let bytes = 0;
|
|
@@ -13126,7 +13360,7 @@ async function readJson(response) {
|
|
|
13126
13360
|
bytes += next.value.byteLength;
|
|
13127
13361
|
if (bytes > maximumResponseBytes) {
|
|
13128
13362
|
await reader.cancel().catch(() => void 0);
|
|
13129
|
-
throw unavailable();
|
|
13363
|
+
throw unavailable(`${where} body too large`);
|
|
13130
13364
|
}
|
|
13131
13365
|
chunks.push(next.value);
|
|
13132
13366
|
}
|
|
@@ -13136,7 +13370,7 @@ async function readJson(response) {
|
|
|
13136
13370
|
try {
|
|
13137
13371
|
return JSON.parse(Buffer.concat(chunks, bytes).toString("utf8"));
|
|
13138
13372
|
} catch (error) {
|
|
13139
|
-
throw unavailable(error);
|
|
13373
|
+
throw unavailable(`${where} invalid JSON`, error);
|
|
13140
13374
|
}
|
|
13141
13375
|
}
|
|
13142
13376
|
function verificationUrl(value, identity) {
|
|
@@ -13144,9 +13378,9 @@ function verificationUrl(value, identity) {
|
|
|
13144
13378
|
try {
|
|
13145
13379
|
url = new URL(value);
|
|
13146
13380
|
} catch (error) {
|
|
13147
|
-
throw unavailable(error);
|
|
13381
|
+
throw unavailable("device-code verification URL invalid", error);
|
|
13148
13382
|
}
|
|
13149
|
-
if (!["http:", "https:"].includes(url.protocol) || url.origin !== identity.origin || url.username !== "" || url.password !== "") throw unavailable();
|
|
13383
|
+
if (!["http:", "https:"].includes(url.protocol) || url.origin !== identity.origin || url.username !== "" || url.password !== "") throw unavailable("device-code verification URL outside Identity");
|
|
13150
13384
|
return url;
|
|
13151
13385
|
}
|
|
13152
13386
|
/**
|
|
@@ -13157,7 +13391,7 @@ function createIdentityDeviceClient(options) {
|
|
|
13157
13391
|
const requestFetch = options.fetch ?? fetch;
|
|
13158
13392
|
const now = options.now ?? Date.now;
|
|
13159
13393
|
const base = new URL(options.identityUrl);
|
|
13160
|
-
if (!["http:", "https:"].includes(base.protocol)) throw unavailable();
|
|
13394
|
+
if (!["http:", "https:"].includes(base.protocol)) throw unavailable("identity URL protocol");
|
|
13161
13395
|
async function send(path, init, signal) {
|
|
13162
13396
|
try {
|
|
13163
13397
|
return await requestFetch(new URL(path, base), {
|
|
@@ -13167,9 +13401,18 @@ function createIdentityDeviceClient(options) {
|
|
|
13167
13401
|
});
|
|
13168
13402
|
} catch (error) {
|
|
13169
13403
|
if (signal.aborted) throw error;
|
|
13170
|
-
throw unavailable(error);
|
|
13404
|
+
throw unavailable(`${path} ${networkDetail(error)}`, error);
|
|
13171
13405
|
}
|
|
13172
13406
|
}
|
|
13407
|
+
function issued(token) {
|
|
13408
|
+
return {
|
|
13409
|
+
kind: "issued",
|
|
13410
|
+
accessToken: token.access_token,
|
|
13411
|
+
...token.refresh_token ? { refreshToken: token.refresh_token } : {},
|
|
13412
|
+
expiresAt: new Date(now() + token.expires_in * 1e3).toISOString(),
|
|
13413
|
+
scope: token.scope
|
|
13414
|
+
};
|
|
13415
|
+
}
|
|
13173
13416
|
function form(fields) {
|
|
13174
13417
|
return {
|
|
13175
13418
|
method: "POST",
|
|
@@ -13182,14 +13425,18 @@ function createIdentityDeviceClient(options) {
|
|
|
13182
13425
|
}
|
|
13183
13426
|
return {
|
|
13184
13427
|
async requestDeviceCode(signal) {
|
|
13185
|
-
|
|
13428
|
+
let response = await send("/api/auth/device/code", form({
|
|
13429
|
+
client_id: options.clientId,
|
|
13430
|
+
scope: "openid profile offline_access"
|
|
13431
|
+
}), signal);
|
|
13432
|
+
if (response.status === 400 && errorResponse.safeParse(await readJson(response, "device-code")).data?.error === "invalid_scope") response = await send("/api/auth/device/code", form({
|
|
13186
13433
|
client_id: options.clientId,
|
|
13187
13434
|
scope: "openid profile"
|
|
13188
13435
|
}), signal);
|
|
13189
13436
|
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);
|
|
13437
|
+
if (response.status !== 200) throw unavailable(`device-code HTTP ${response.status}`);
|
|
13438
|
+
const parsed = deviceCodeResponse.safeParse(await readJson(response, "device-code"));
|
|
13439
|
+
if (!parsed.success) throw unavailable(`device-code schema ${issuePaths(parsed.error)}`, parsed.error);
|
|
13193
13440
|
const verification = verificationUrl(parsed.data.verification_uri, base);
|
|
13194
13441
|
const complete = verificationUrl(parsed.data.verification_uri_complete, base);
|
|
13195
13442
|
return {
|
|
@@ -13208,20 +13455,16 @@ function createIdentityDeviceClient(options) {
|
|
|
13208
13455
|
grant_type: deviceCodeGrantType
|
|
13209
13456
|
}), signal);
|
|
13210
13457
|
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
|
-
};
|
|
13458
|
+
const parsed = tokenResponse.safeParse(await readJson(response, "token"));
|
|
13459
|
+
if (!parsed.success) throw unavailable(`token schema ${issuePaths(parsed.error)}`, parsed.error);
|
|
13460
|
+
if (parsed.data.token_type.toLowerCase() !== "bearer") throw unavailable("token type not bearer");
|
|
13461
|
+
return issued(parsed.data);
|
|
13219
13462
|
}
|
|
13220
13463
|
if (response.status !== 400) {
|
|
13221
13464
|
if (response.status === 401) throw new ApplicationError("IDENTITY_REJECTED");
|
|
13222
|
-
throw unavailable();
|
|
13465
|
+
throw unavailable(`token HTTP ${response.status}`);
|
|
13223
13466
|
}
|
|
13224
|
-
const parsed = errorResponse.safeParse(await readJson(response));
|
|
13467
|
+
const parsed = errorResponse.safeParse(await readJson(response, "token"));
|
|
13225
13468
|
const code = parsed.success ? parsed.data.error : "";
|
|
13226
13469
|
if (code === "authorization_pending") return { kind: "pending" };
|
|
13227
13470
|
if (code === "slow_down") return { kind: "slow_down" };
|
|
@@ -13229,6 +13472,19 @@ function createIdentityDeviceClient(options) {
|
|
|
13229
13472
|
if (code === "access_denied") return { kind: "denied" };
|
|
13230
13473
|
throw new ApplicationError("IDENTITY_REJECTED");
|
|
13231
13474
|
},
|
|
13475
|
+
async refresh(refreshToken, signal) {
|
|
13476
|
+
const response = await send("/api/auth/oauth2/token", form({
|
|
13477
|
+
client_id: options.clientId,
|
|
13478
|
+
grant_type: "refresh_token",
|
|
13479
|
+
refresh_token: refreshToken
|
|
13480
|
+
}), signal);
|
|
13481
|
+
if (response.status === 400 || response.status === 401) return null;
|
|
13482
|
+
if (response.status !== 200) throw unavailable(`refresh HTTP ${response.status}`);
|
|
13483
|
+
const parsed = tokenResponse.safeParse(await readJson(response, "refresh"));
|
|
13484
|
+
if (!parsed.success) throw unavailable(`refresh schema ${issuePaths(parsed.error)}`, parsed.error);
|
|
13485
|
+
if (parsed.data.token_type.toLowerCase() !== "bearer") throw unavailable("token type not bearer");
|
|
13486
|
+
return issued(parsed.data);
|
|
13487
|
+
},
|
|
13232
13488
|
async readIdentity(accessToken, signal) {
|
|
13233
13489
|
const response = await send("/api/auth/oauth2/userinfo", {
|
|
13234
13490
|
method: "GET",
|
|
@@ -13238,17 +13494,17 @@ function createIdentityDeviceClient(options) {
|
|
|
13238
13494
|
}
|
|
13239
13495
|
}, signal);
|
|
13240
13496
|
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);
|
|
13497
|
+
if (response.status !== 200) throw unavailable(`userinfo HTTP ${response.status}`);
|
|
13498
|
+
const parsed = userInfoResponse.safeParse(await readJson(response, "userinfo"));
|
|
13499
|
+
if (!parsed.success) throw unavailable(`userinfo schema ${issuePaths(parsed.error)}`, parsed.error);
|
|
13244
13500
|
const profile = accountProfileSchema.safeParse({
|
|
13245
13501
|
identityId: parsed.data.sub,
|
|
13246
13502
|
displayName: parsed.data.name,
|
|
13247
13503
|
avatarUrl: readAccountAvatarUrl(parsed.data.picture),
|
|
13248
13504
|
profileUrl: parsed.data.profile
|
|
13249
13505
|
});
|
|
13250
|
-
if (!profile.success) throw unavailable(profile.error);
|
|
13251
|
-
if (!profileUrlMatchesIdentity(profile.data.profileUrl, options.identityUrl)) throw unavailable();
|
|
13506
|
+
if (!profile.success) throw unavailable(`userinfo profile ${issuePaths(profile.error)}`, profile.error);
|
|
13507
|
+
if (!profileUrlMatchesIdentity(profile.data.profileUrl, options.identityUrl)) throw unavailable("userinfo profile URL outside Identity");
|
|
13252
13508
|
return profile.data;
|
|
13253
13509
|
}
|
|
13254
13510
|
};
|
|
@@ -13367,6 +13623,8 @@ function errorDetail(error) {
|
|
|
13367
13623
|
if (typeof code === "number" && Number.isFinite(code)) detail.code = code;
|
|
13368
13624
|
const syscall = data(value, "syscall");
|
|
13369
13625
|
if (typeof syscall === "string") detail.syscall = text(syscall);
|
|
13626
|
+
const reason = data(value, "detail");
|
|
13627
|
+
if (typeof reason === "string") detail.detail = text(reason);
|
|
13370
13628
|
const cause = data(value, "cause");
|
|
13371
13629
|
if (cause !== void 0) detail.cause = visit(cause, depth + 1);
|
|
13372
13630
|
const errors = data(value, "errors");
|
|
@@ -13849,6 +14107,61 @@ function createConfiguredPreviewPort(workerUrl, environment) {
|
|
|
13849
14107
|
}) };
|
|
13850
14108
|
}
|
|
13851
14109
|
//#endregion
|
|
14110
|
+
//#region apps/cli/src/inspection/index.ts
|
|
14111
|
+
/**
|
|
14112
|
+
* Runs each inspection scan in its own Worker so static detection never holds the host's event
|
|
14113
|
+
* loop, which also serves requests and store accesses (BL-199).
|
|
14114
|
+
*/
|
|
14115
|
+
function createInspectionPort(workerUrl, environment, onAnomaly) {
|
|
14116
|
+
return { async *scan(request, signal, onFailure) {
|
|
14117
|
+
const worker = new Worker(workerUrl, { workerData: {
|
|
14118
|
+
request,
|
|
14119
|
+
environment
|
|
14120
|
+
} });
|
|
14121
|
+
const events = [];
|
|
14122
|
+
let wake;
|
|
14123
|
+
const push = (event) => {
|
|
14124
|
+
events.push(event);
|
|
14125
|
+
wake?.();
|
|
14126
|
+
};
|
|
14127
|
+
const fail = (cause) => push({ error: new Error("INSPECTION_WORKER_FAILED", { cause }) });
|
|
14128
|
+
worker.on("message", (message) => {
|
|
14129
|
+
const parsed = workerMessageSchema.safeParse(message);
|
|
14130
|
+
if (!parsed.success) return fail(parsed.error);
|
|
14131
|
+
const value = parsed.data;
|
|
14132
|
+
try {
|
|
14133
|
+
if (value.kind === "batch") push({ batch: validateInspectionBatch(value.batch) });
|
|
14134
|
+
else if (value.kind === "done") push({ done: true });
|
|
14135
|
+
else if (value.kind === "anomaly") onAnomaly?.(value.event);
|
|
14136
|
+
else onFailure?.(value.stage, Object.assign(new Error(value.message), value.code ? { code: value.code } : {}));
|
|
14137
|
+
} catch (error) {
|
|
14138
|
+
fail(error);
|
|
14139
|
+
}
|
|
14140
|
+
});
|
|
14141
|
+
worker.on("error", fail);
|
|
14142
|
+
worker.on("exit", (code) => fail(/* @__PURE__ */ new Error(`Inspection Worker exited: code=${code}`)));
|
|
14143
|
+
const abort = () => worker.postMessage({ kind: "abort" });
|
|
14144
|
+
if (signal.aborted) abort();
|
|
14145
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
14146
|
+
try {
|
|
14147
|
+
for (;;) {
|
|
14148
|
+
const event = events.shift();
|
|
14149
|
+
if (!event) {
|
|
14150
|
+
await new Promise((resolve) => wake = resolve);
|
|
14151
|
+
wake = void 0;
|
|
14152
|
+
continue;
|
|
14153
|
+
}
|
|
14154
|
+
if ("done" in event) return;
|
|
14155
|
+
if ("error" in event) throw event.error;
|
|
14156
|
+
yield event.batch;
|
|
14157
|
+
}
|
|
14158
|
+
} finally {
|
|
14159
|
+
signal.removeEventListener("abort", abort);
|
|
14160
|
+
await worker.terminate();
|
|
14161
|
+
}
|
|
14162
|
+
} };
|
|
14163
|
+
}
|
|
14164
|
+
//#endregion
|
|
13852
14165
|
//#region apps/cli/src/bundle-directory.ts
|
|
13853
14166
|
/** The platform cache folder; Windows has none, so its default library is the launch directory. */
|
|
13854
14167
|
function cacheRoot(options) {
|
|
@@ -14080,7 +14393,14 @@ async function compose(options, callbacks, bundles) {
|
|
|
14080
14393
|
artifacts: createJourneySendArtifacts(journeyStorage),
|
|
14081
14394
|
auth: createJourneySendAuth(accountPorts),
|
|
14082
14395
|
now: () => (/* @__PURE__ */ new Date()).toISOString(),
|
|
14083
|
-
failure: (code, cause) => diagnostics.onFailure(code, cause)
|
|
14396
|
+
failure: (code, cause) => diagnostics.onFailure(code, cause),
|
|
14397
|
+
uploaded: (workId, bytes, elapsedMs) => diagnostics.onActivity({
|
|
14398
|
+
event: "journey.send_upload_timing",
|
|
14399
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
14400
|
+
operationId: workId,
|
|
14401
|
+
phases: { upload: elapsedMs },
|
|
14402
|
+
volumes: { bytes }
|
|
14403
|
+
})
|
|
14084
14404
|
}),
|
|
14085
14405
|
manualUploader: createManualUploader({
|
|
14086
14406
|
storage: store.manualUpload,
|
|
@@ -14152,10 +14472,7 @@ async function compose(options, callbacks, bundles) {
|
|
|
14152
14472
|
onCreated: (path) => process.stderr.write(`Temporary file may contain unredacted originals: ${path}\n`),
|
|
14153
14473
|
onRetained: (path) => process.stderr.write(`Retained temporary file may contain unredacted originals: ${path}\n`)
|
|
14154
14474
|
}),
|
|
14155
|
-
inspection:
|
|
14156
|
-
...processing,
|
|
14157
|
-
onAnomaly: producers.onInspectionAnomaly
|
|
14158
|
-
}),
|
|
14475
|
+
inspection: createInspectionPort(bundles?.url("inspection-worker") ?? new URL("./inspection-worker.mjs", import.meta.url), processing.environment, producers.onInspectionAnomaly),
|
|
14159
14476
|
systemResources,
|
|
14160
14477
|
onDiagnostic: diagnostics.onDiagnostic,
|
|
14161
14478
|
onPackagingActivity: diagnostics.onActivity,
|
|
@@ -14249,6 +14566,46 @@ async function compose(options, callbacks, bundles) {
|
|
|
14249
14566
|
}
|
|
14250
14567
|
}
|
|
14251
14568
|
//#endregion
|
|
14569
|
+
//#region apps/cli/src/agent-presence.ts
|
|
14570
|
+
const journey = z.object({ journeyId: z.uuid() }).loose();
|
|
14571
|
+
const presence = z.object({ at: z.iso.datetime() }).strict();
|
|
14572
|
+
const written = /* @__PURE__ */ new Map();
|
|
14573
|
+
/** At most one presence write per journey this often, from one CLI process. */
|
|
14574
|
+
const presenceIntervalMs = 5e3;
|
|
14575
|
+
const presencePath = (stateDir, journeyId) => join(stateDir, "presence", `${journeyId}.json`);
|
|
14576
|
+
/**
|
|
14577
|
+
* Records that the agent just called the CLI for this journey (E16). It lives beside the journey,
|
|
14578
|
+
* not in it, so a heartbeat never moves the journey revision; a failed write is ignored.
|
|
14579
|
+
*/
|
|
14580
|
+
async function markAgentPresence(stateDir, value) {
|
|
14581
|
+
const parsed = journey.safeParse(value);
|
|
14582
|
+
if (!parsed.success) return;
|
|
14583
|
+
const { journeyId } = parsed.data;
|
|
14584
|
+
const now = Date.now();
|
|
14585
|
+
if (now - (written.get(journeyId) ?? 0) < presenceIntervalMs) return;
|
|
14586
|
+
written.set(journeyId, now);
|
|
14587
|
+
try {
|
|
14588
|
+
const path = presencePath(stateDir, journeyId);
|
|
14589
|
+
await mkdir(join(stateDir, "presence"), {
|
|
14590
|
+
recursive: true,
|
|
14591
|
+
mode: 448
|
|
14592
|
+
});
|
|
14593
|
+
const temporary = `${path}.${now}.tmp`;
|
|
14594
|
+
await writeFile(temporary, JSON.stringify({ at: new Date(now).toISOString() }), { mode: 384 });
|
|
14595
|
+
await rename(temporary, path);
|
|
14596
|
+
} catch {
|
|
14597
|
+
written.delete(journeyId);
|
|
14598
|
+
}
|
|
14599
|
+
}
|
|
14600
|
+
/** When the agent last called the CLI for this journey, or null when it never did. */
|
|
14601
|
+
async function readAgentPresence(stateDir, journeyId) {
|
|
14602
|
+
try {
|
|
14603
|
+
return presence.parse(JSON.parse(await readFile(presencePath(stateDir, journeyId), "utf8")));
|
|
14604
|
+
} catch {
|
|
14605
|
+
return null;
|
|
14606
|
+
}
|
|
14607
|
+
}
|
|
14608
|
+
//#endregion
|
|
14252
14609
|
//#region apps/cli/src/commands.ts
|
|
14253
14610
|
const sendAddress = z.object({
|
|
14254
14611
|
journeyId: z.uuid(),
|
|
@@ -14274,6 +14631,7 @@ const delivery = z.object({
|
|
|
14274
14631
|
/** `signal` carries the process's SIGINT/SIGTERM to long procedures such as journey.prepare. */
|
|
14275
14632
|
async function invoke(runtime, command, value, signal) {
|
|
14276
14633
|
const app = runtime.application;
|
|
14634
|
+
markAgentPresence(runtime.stateDir, value);
|
|
14277
14635
|
switch (command) {
|
|
14278
14636
|
case "send.get": return app.journeySend?.get(sendAddress.partial({ sendId: true }).parse(value));
|
|
14279
14637
|
case "send.prepare": return app.journeySend?.prepare(sendPrepareSchema.parse(value));
|
|
@@ -14508,4 +14866,4 @@ async function cancelOperation(runtime, id) {
|
|
|
14508
14866
|
return readOperation(runtime, id);
|
|
14509
14867
|
}
|
|
14510
14868
|
//#endregion
|
|
14511
|
-
export {
|
|
14869
|
+
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 };
|