@zq-silk/yui 0.13.9 → 0.14.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/ARCHITECTURE.md +69 -428
- package/README.md +108 -298
- package/dist/agent/managedRuntimeEnvironment.js +2 -2
- package/dist/cli/commandCatalog.js +39 -77
- package/dist/cli/interactionCandidates.js +4 -4
- package/dist/cli/interactionPolicy.js +7 -10
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/updateCommand.js +6 -27
- package/dist/cli/updateOrchestrator.js +63 -484
- package/dist/cli/updatePorts.js +15 -333
- package/dist/cli/upgradeCommand.js +8 -65
- package/dist/cli.js +57 -242
- package/dist/commands/configCommands.js +11 -62
- package/dist/commands/executionAuditCommands.js +23 -23
- package/dist/commands/globalRoleCommands.js +2 -7
- package/dist/commands/jobCommands.js +1 -4
- package/dist/commands/projectCommands.js +5 -5
- package/dist/commands/releaseCommands.js +3 -10
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/sessionCommands.js +2 -2
- package/dist/commands/taskActor.js +31 -39
- package/dist/commands/taskChangeSetCommands.js +5 -12
- package/dist/commands/taskCommands.js +464 -1249
- package/dist/commands/taskContextCommand.js +35 -36
- package/dist/commands/taskExecutionCommands.js +33 -46
- package/dist/commands/taskInputCommands.js +20 -42
- package/dist/commands/taskNextActionCommand.js +9 -9
- package/dist/commands/taskOverviewCommand.js +56 -95
- package/dist/commands/taskRoleRuntimeStatus.js +77 -107
- package/dist/commands/taskUpstreamCommands.js +3 -3
- package/dist/commands/taskWorkspaceCommands.js +7 -79
- package/dist/commands/telemetryCommands.js +32 -124
- package/dist/commands/workflowCommands.js +3 -3
- package/dist/completion/fileCompletionManager.js +1 -1
- package/dist/config/configCatalog.js +4 -6
- package/dist/config/yuiConfig.js +6 -43
- package/dist/context/dispatchContext.js +14 -30
- package/dist/context/roleSessionContext.js +2 -2
- package/dist/context/sessionBootstrapManifest.js +2 -2
- package/dist/context/{runContextPack.js → turnContextPack.js} +58 -73
- package/dist/context/turnInputContract.js +204 -0
- package/dist/context/wakeNotification.js +2 -2
- package/dist/controller/agentRuntimeObserver.js +18 -18
- package/dist/controller/clientRuntime.js +13 -20
- package/dist/controller/controller.js +89 -116
- package/dist/controller/fileSchedulerStoreAdapter.js +511 -742
- package/dist/controller/jobControl.js +17 -17
- package/dist/controller/resourceInventory.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +38 -59
- package/dist/controller/resourceInventoryRpc.js +3 -3
- package/dist/controller/resourceInventoryWorker.js +1 -1
- package/dist/controller/runtime.js +46 -53
- package/dist/controller/runtimeEventInbox.js +23 -31
- package/dist/controller/runtimeEventProcessor.js +15 -15
- package/dist/controller/{runtimeHookRunFence.js → runtimeHookTurnFence.js} +87 -81
- package/dist/controller/runtimeLaunchCoordinator.js +3 -3
- package/dist/controller/runtimeObservationHook.js +11 -8
- package/dist/controller/sessionNotify.js +11 -11
- package/dist/controller/structuredProviderObservation.js +89 -16
- package/dist/coordination/workMailbox.js +309 -579
- package/dist/coordination/workMailboxQueue.js +5 -6
- package/dist/core/controllerClient.js +1 -91
- package/dist/core/controllerServer.js +3 -6
- package/dist/doctor/doctor.js +51 -314
- package/dist/execution/candidateConvergence.js +3 -3
- package/dist/execution/executionGroup.js +32 -33
- package/dist/execution/executionHealth.js +36 -38
- package/dist/execution/resourceBroker.js +12 -12
- package/dist/executor/agentExecutor.js +30 -265
- package/dist/executor/effectiveLaunch.js +9 -19
- package/dist/executor/executorRegistry.js +43 -17
- package/dist/executor/fileRoleLaunchPlanner.js +56 -56
- package/dist/executor/launchPlan.js +3 -3
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +20 -25
- package/dist/input/inputRequest.js +12 -12
- package/dist/integration/changeSet.js +9 -13
- package/dist/integration/changeSetManifest.js +1 -1
- package/dist/integration/gitIntegrationService.js +2 -2
- package/dist/integration/integrationQueueService.js +5 -21
- package/dist/integration/overlapDiagnostics.js +2 -2
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/job/jobRunner.js +1 -1
- package/dist/lifecycle/canonicalLifecycleEvent.js +25 -25
- package/dist/lifecycle/{exactRunTerminalization.js → exactTurnTerminalization.js} +116 -232
- package/dist/message/message.js +6 -6
- package/dist/observability/executionAudit.js +74 -86
- package/dist/observability/faultClassification.js +7 -24
- package/dist/observability/orchestrationMetrics.js +5 -5
- package/dist/observability/runtimeIdentity.js +34 -101
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +3 -3
- package/dist/release/releaseWorkflowEngine.js +6 -10
- package/dist/release/releaseWorkflowPorts.js +2 -2
- package/dist/release/workflowFileLock.js +1 -1
- package/dist/repository/checkoutSwap.js +1 -1
- package/dist/repository/gitWorkspace.js +0 -13
- package/dist/repository/project.js +1 -1
- package/dist/repository/projectMaintenanceLock.js +1 -2
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +21 -25
- package/dist/repository/taskWorkspaceIdentity.js +8 -21
- package/dist/repository/taskWorkspacePreparer.js +63 -620
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewFindingLedger.js +1 -1
- package/dist/review/reviewOutcomeClassifier.js +35 -50
- package/dist/review/reviewRound.js +14 -25
- package/dist/review/reviewerAvailability.js +4 -4
- package/dist/review/taskFinalReviewContractResolution.js +41 -0
- package/dist/role/role.js +6 -3
- package/dist/runtime/agentDriver.js +4 -0
- package/dist/runtime/agentHost.js +151 -36
- package/dist/runtime/builtinAgentDrivers.js +79 -2
- package/dist/runtime/codexAppServerRuntime.js +122 -0
- package/dist/runtime/continuationManager.js +3 -5
- package/dist/runtime/exactControlPlane.js +17 -42
- package/dist/runtime/firstProgressAdvisory.js +1 -1
- package/dist/runtime/index.js +2 -2
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +4 -4
- package/dist/runtime/processExitObservation.js +1 -1
- package/dist/runtime/processExitOutbox.js +0 -32
- package/dist/runtime/promptEnvelope.js +9 -9
- package/dist/runtime/providerContinuation.js +2 -2
- package/dist/runtime/providerContinuationReconciliationService.js +2 -2
- package/dist/runtime/providerRuntimeIdentity.js +76 -24
- package/dist/runtime/recentTurnIds.js +38 -0
- package/dist/runtime/runtimeContinuationProjection.js +2 -2
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +58 -21
- package/dist/runtime/runtimeProjection.js +19 -27
- package/dist/runtime/sessionLaunchRequest.js +2 -2
- package/dist/runtime/structuredProviderHost.js +177 -12
- package/dist/runtime/tmuxAdapters.js +46 -11
- package/dist/scheduler/activeRoleTurnDelivery.js +245 -0
- package/dist/scheduler/leaderWakeupProcessor.js +147 -445
- package/dist/scheduler/operatorEvent.js +4 -6
- package/dist/scheduler/resourceQueueProjection.js +71 -0
- package/dist/scheduler/{roleRunLiveness.js → roleTurnLiveness.js} +24 -28
- package/dist/scheduler/{roleRunStall.js → roleTurnStall.js} +162 -247
- package/dist/scheduler/taskExecutionProjection.js +99 -99
- package/dist/scheduler/taskObservabilityProjection.js +10 -10
- package/dist/scheduler/taskWake.js +4 -4
- package/dist/scheduler/wakeReason.js +6 -10
- package/dist/scheduler/wakeupQueue.js +0 -9
- package/dist/setup/setupCommand.js +2 -2
- package/dist/storage/currentTaskStore.js +46 -0
- package/dist/storage/persistenceWorker.js +1 -1
- package/dist/storage/sqliteSchema.js +81 -485
- package/dist/storage/sqliteStore.js +206 -365
- package/dist/storage/storageSchema.js +1 -15
- package/dist/storage/storageVersions.js +8 -9
- package/dist/storage/storeRpc.js +10 -34
- package/dist/storage/taskStore.js +63 -3415
- package/dist/storage/upgrade/recordVersions.js +55 -149
- package/dist/storage/upgrade/upgradeOrchestrator.js +89 -1752
- package/dist/task/completionReadiness.js +5 -5
- package/dist/task/deliveryGuard.js +10 -10
- package/dist/task/nextAction.js +74 -77
- package/dist/task/task.js +2 -4
- package/dist/task/taskRecordReference.js +3 -3
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +53 -53
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +8 -14
- package/dist/turn/turn.js +356 -0
- package/dist/{run/runIdentity.js → turn/turnIdentity.js} +4 -4
- package/dist/verification/gateArtifactStore.js +2 -2
- package/dist/web/assets/client/app.js +1 -1
- package/dist/web/assets/client/components.js +29 -54
- package/dist/web/assets/client/i18n.js +17 -37
- package/dist/web/assets/client/view.js +37 -37
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +5 -5
- package/dist/web/assets/styles/widgets.js +1 -2
- package/dist/web/webSnapshot.js +65 -60
- package/dist/workItem/workItem.js +11 -11
- package/dist/workspace/workItemChangeSetManager.js +1 -1
- package/docs/task-local-identity.md +5 -5
- package/i18n/README.zh-CN.md +41 -54
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +85 -83
- package/skills/yui-operator/SKILL.md +16 -16
- package/skills/yui-reviewer/SKILL.md +10 -10
- package/skills/yui-runtime/SKILL.md +17 -18
- package/skills/yui-worker/SKILL.md +16 -18
- package/dist/context/runContextContract.js +0 -162
- package/dist/executor/turnCompletion.js +0 -126
- package/dist/review/taskFinalReviewContractEvent.js +0 -1
- package/dist/review/taskFinalReviewContractRebind.js +0 -367
- package/dist/run/agentRun.js +0 -356
- package/dist/run/rejectedYieldAttempt.js +0 -221
- package/dist/run/runControlRequest.js +0 -50
- package/dist/run/yieldReceipt.js +0 -65
- package/dist/runtime/turnCompletion.js +0 -3
- package/dist/scheduler/actionability.js +0 -323
- package/dist/scheduler/activeRoleRunDelivery.js +0 -868
- package/dist/scheduler/activeTaskProgress.js +0 -126
- package/dist/storage/compatibleTaskStore.js +0 -248
- package/dist/storage/migration/baseline.js +0 -78
- package/dist/storage/migration/classifier.js +0 -51
- package/dist/storage/migration/compatibleCodec.js +0 -53
- package/dist/storage/migration/engine.js +0 -147
- package/dist/storage/migration/index.js +0 -33
- package/dist/storage/migration/planner.js +0 -154
- package/dist/storage/migration/productionRegistry.js +0 -3931
- package/dist/storage/migration/registry.js +0 -169
- package/dist/storage/migration/report.js +0 -54
- package/dist/storage/migration/types.js +0 -31
- package/dist/storage/upgrade/homeClassification.js +0 -353
- package/dist/storage/upgrade/homeMigrationTarget.js +0 -595
- package/dist/storage/upgrade/migrationReceipt.js +0 -67
- package/dist/storage/upgrade/offlineUpgradeInventory.js +0 -453
- package/dist/storage/upgrade/productionMigrationRegistry.js +0 -6
- package/dist/storage/upgrade/pseudoLayoutRepair.js +0 -241
- package/dist/storage/upgrade/recordVersionScan.js +0 -176
- package/dist/storage/upgrade/sqliteFileSet.js +0 -50
- package/dist/storage/upgrade/sqliteMigrationTarget.js +0 -375
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +0 -305
- package/dist/storage/upgrade/sqliteStateMigration.js +0 -918
- package/dist/storage/upgrade/switchProgress.js +0 -80
- package/dist/storage/upgrade/upgradeReceipt.js +0 -161
- package/dist/storage/upgradeCoordination.js +0 -187
- package/dist/storage/upgradeFence.js +0 -366
- package/dist/telemetry/telemetryCompaction.js +0 -252
|
@@ -1,76 +1,178 @@
|
|
|
1
1
|
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
2
2
|
const RECENT_DEDUPE_KEY_LIMIT = 256;
|
|
3
|
-
function
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
export function createWorkMailbox(target) {
|
|
4
|
+
return {
|
|
5
|
+
schemaVersion: 5,
|
|
6
|
+
target: copyTarget(target),
|
|
7
|
+
nextSequence: 1,
|
|
8
|
+
processing: null,
|
|
9
|
+
pending: null,
|
|
10
|
+
recentDedupeKeys: []
|
|
11
|
+
};
|
|
9
12
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
export function validateWorkMailbox(value) {
|
|
14
|
+
const mailbox = record(value, "WorkMailbox");
|
|
15
|
+
exact(mailbox, ["schemaVersion", "target", "nextSequence", "processing", "pending", "recentDedupeKeys"], "WorkMailbox");
|
|
16
|
+
if (mailbox.schemaVersion !== 5)
|
|
17
|
+
throw new Error("WorkMailbox must use schemaVersion 5");
|
|
18
|
+
const target = parseTarget(mailbox.target);
|
|
19
|
+
const nextSequence = requireInteger(mailbox.nextSequence, 1, "WorkMailbox nextSequence");
|
|
20
|
+
const processing = mailbox.processing === null ? null : parseProcessing(mailbox.processing);
|
|
21
|
+
const pending = mailbox.pending === null ? null : parseBatch(mailbox.pending, "WorkMailbox pending");
|
|
22
|
+
const recentDedupeKeys = requireStringArray(mailbox.recentDedupeKeys, "WorkMailbox recent dedupe keys");
|
|
23
|
+
if (recentDedupeKeys.length > RECENT_DEDUPE_KEY_LIMIT) {
|
|
24
|
+
throw new Error("WorkMailbox recent dedupe keys exceed the bounded limit");
|
|
13
25
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
26
|
+
const activeKeys = new Set();
|
|
27
|
+
for (const batch of [processing?.batch, pending]) {
|
|
28
|
+
if (batch === null || batch === undefined)
|
|
29
|
+
continue;
|
|
30
|
+
if (batch.toSequence >= nextSequence) {
|
|
31
|
+
throw new Error("WorkMailbox batch sequence must be lower than nextSequence");
|
|
32
|
+
}
|
|
33
|
+
for (const key of batch.dedupeKeys) {
|
|
34
|
+
if (activeKeys.has(key) || recentDedupeKeys.includes(key)) {
|
|
35
|
+
throw new Error("WorkMailbox batches contain an already-consumed dedupe key");
|
|
36
|
+
}
|
|
37
|
+
activeKeys.add(key);
|
|
38
|
+
}
|
|
19
39
|
}
|
|
20
|
-
return
|
|
40
|
+
return {
|
|
41
|
+
schemaVersion: 5,
|
|
42
|
+
target,
|
|
43
|
+
nextSequence,
|
|
44
|
+
processing,
|
|
45
|
+
pending,
|
|
46
|
+
recentDedupeKeys
|
|
47
|
+
};
|
|
21
48
|
}
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
49
|
+
export function enqueueSignal(mailbox, signal) {
|
|
50
|
+
validateWorkMailbox(mailbox);
|
|
51
|
+
const reason = requireText(signal.reason, "signal reason");
|
|
52
|
+
const occurredAt = requireTimestamp(signal.occurredAt, "signal occurredAt");
|
|
53
|
+
const source = requireText(signal.source ?? "yui", "signal source");
|
|
54
|
+
const sequence = mailbox.nextSequence;
|
|
55
|
+
const dedupeKey = signal.dedupeKey === undefined
|
|
56
|
+
? `sequence:${sequence}`
|
|
57
|
+
: requireText(signal.dedupeKey, "signal dedupeKey");
|
|
58
|
+
if (mailboxContainsDedupeKey(mailbox, dedupeKey))
|
|
59
|
+
return mailbox;
|
|
60
|
+
const incoming = {
|
|
61
|
+
fromSequence: sequence,
|
|
62
|
+
toSequence: sequence,
|
|
63
|
+
reasons: [reason],
|
|
64
|
+
refs: appendUnique([], signal.refs.map(copyRef), mailboxEntityRefKey),
|
|
65
|
+
requestCount: 1,
|
|
66
|
+
firstQueuedAt: occurredAt,
|
|
67
|
+
lastQueuedAt: occurredAt,
|
|
68
|
+
sources: [source],
|
|
69
|
+
dedupeKeys: [dedupeKey],
|
|
70
|
+
...(signal.factRevision === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: { highestFactRevision: requireInteger(signal.factRevision, 0, "signal factRevision") })
|
|
73
|
+
};
|
|
74
|
+
return validateWorkMailbox({
|
|
75
|
+
...mailbox,
|
|
76
|
+
nextSequence: sequence + 1,
|
|
77
|
+
pending: mailbox.pending === null ? incoming : mergeBatches(mailbox.pending, incoming)
|
|
78
|
+
});
|
|
30
79
|
}
|
|
31
|
-
function
|
|
32
|
-
|
|
33
|
-
throw new Error(`${label} must be a string`);
|
|
34
|
-
return requireText(value, label);
|
|
80
|
+
export function mailboxHasPending(mailbox) {
|
|
81
|
+
return mailbox.pending !== null;
|
|
35
82
|
}
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
if (!Number.isFinite(Date.parse(timestamp)))
|
|
39
|
-
throw new Error(`${label} must be a timestamp`);
|
|
40
|
-
return timestamp;
|
|
83
|
+
export function nextPendingBatch(mailbox) {
|
|
84
|
+
return mailbox.pending;
|
|
41
85
|
}
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
throw new Error(`${label} must be an array`);
|
|
45
|
-
const result = value.map((item, index) => requireString(item, `${label}[${index}]`));
|
|
46
|
-
if (new Set(result).size !== result.length)
|
|
47
|
-
throw new Error(`${label} must not contain duplicates`);
|
|
48
|
-
return result;
|
|
86
|
+
export function mailboxHasWork(mailbox) {
|
|
87
|
+
return mailbox.processing !== null || mailbox.pending !== null;
|
|
49
88
|
}
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
89
|
+
export function mailboxBatches(mailbox) {
|
|
90
|
+
return [mailbox.processing?.batch, mailbox.pending]
|
|
91
|
+
.filter((batch) => batch !== null && batch !== undefined);
|
|
92
|
+
}
|
|
93
|
+
export function claimPending(mailbox, options) {
|
|
94
|
+
validateWorkMailbox(mailbox);
|
|
95
|
+
if (mailbox.processing !== null)
|
|
96
|
+
throw new Error("Mailbox is already processing a batch");
|
|
97
|
+
if (mailbox.pending === null)
|
|
98
|
+
throw new Error("Mailbox has no pending work to claim");
|
|
99
|
+
return validateWorkMailbox({
|
|
100
|
+
...mailbox,
|
|
101
|
+
processing: {
|
|
102
|
+
batchId: requireText(options.batchId, "batch id"),
|
|
103
|
+
batch: mailbox.pending,
|
|
104
|
+
owner: requireText(options.owner, "claim owner"),
|
|
105
|
+
startedAt: requireTimestamp(options.startedAt, "claim startedAt")
|
|
106
|
+
},
|
|
107
|
+
pending: null
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
export function bindExecution(mailbox, batchId, executionRef) {
|
|
111
|
+
const processing = requireProcessing(mailbox, batchId);
|
|
112
|
+
return validateWorkMailbox({
|
|
113
|
+
...mailbox,
|
|
114
|
+
processing: { ...processing, executionRef: copyRef(executionRef) }
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
export function completeProcessing(mailbox, batchId) {
|
|
118
|
+
const processing = requireProcessing(mailbox, batchId);
|
|
119
|
+
return validateWorkMailbox({
|
|
120
|
+
...mailbox,
|
|
121
|
+
processing: null,
|
|
122
|
+
recentDedupeKeys: rememberDedupeKeys(mailbox.recentDedupeKeys, processing.batch.dedupeKeys)
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
export function releaseProcessing(mailbox, batchId) {
|
|
126
|
+
const processing = requireProcessing(mailbox, batchId);
|
|
127
|
+
return validateWorkMailbox({
|
|
128
|
+
...mailbox,
|
|
129
|
+
processing: null,
|
|
130
|
+
pending: mailbox.pending === null
|
|
131
|
+
? processing.batch
|
|
132
|
+
: mergeBatches(processing.batch, mailbox.pending)
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Consumes a whole wake or the exact submitted sequence prefix. Newer signals
|
|
137
|
+
* merged while the Provider request was in flight remain pending.
|
|
138
|
+
*/
|
|
139
|
+
export function consumePendingBatch(mailbox, through) {
|
|
140
|
+
validateWorkMailbox(mailbox);
|
|
141
|
+
if (mailbox.pending === null)
|
|
142
|
+
throw new Error("Mailbox has no pending batch");
|
|
143
|
+
const pending = mailbox.pending;
|
|
144
|
+
if (through !== undefined) {
|
|
145
|
+
if (through.fromSequence !== pending.fromSequence
|
|
146
|
+
|| !Number.isInteger(through.toSequence)
|
|
147
|
+
|| through.toSequence < through.fromSequence
|
|
148
|
+
|| through.toSequence > pending.toSequence) {
|
|
149
|
+
throw new Error("Mailbox consumed sequence does not match the pending prefix");
|
|
150
|
+
}
|
|
151
|
+
if (through.toSequence < pending.toSequence) {
|
|
152
|
+
const consumedCount = through.toSequence - through.fromSequence + 1;
|
|
153
|
+
const keysAreSequenceAligned = pending.dedupeKeys.length === pending.requestCount;
|
|
154
|
+
const consumedKeys = keysAreSequenceAligned
|
|
155
|
+
? pending.dedupeKeys.slice(0, consumedCount)
|
|
156
|
+
: [];
|
|
157
|
+
return validateWorkMailbox({
|
|
158
|
+
...mailbox,
|
|
159
|
+
pending: {
|
|
160
|
+
...pending,
|
|
161
|
+
fromSequence: through.toSequence + 1,
|
|
162
|
+
requestCount: pending.toSequence - through.toSequence,
|
|
163
|
+
dedupeKeys: keysAreSequenceAligned
|
|
164
|
+
? pending.dedupeKeys.slice(consumedCount)
|
|
165
|
+
: pending.dedupeKeys
|
|
166
|
+
},
|
|
167
|
+
recentDedupeKeys: rememberDedupeKeys(mailbox.recentDedupeKeys, consumedKeys)
|
|
168
|
+
});
|
|
169
|
+
}
|
|
73
170
|
}
|
|
171
|
+
return validateWorkMailbox({
|
|
172
|
+
...mailbox,
|
|
173
|
+
recentDedupeKeys: rememberDedupeKeys(mailbox.recentDedupeKeys, pending.dedupeKeys),
|
|
174
|
+
pending: null
|
|
175
|
+
});
|
|
74
176
|
}
|
|
75
177
|
export function mailboxTargetKey(target) {
|
|
76
178
|
const copied = copyTarget(target);
|
|
@@ -84,45 +186,22 @@ export function mailboxTargetKey(target) {
|
|
|
84
186
|
return `global-role-runtime/${encodeURIComponent(copied.roleName)}`;
|
|
85
187
|
}
|
|
86
188
|
}
|
|
87
|
-
function copyRef(ref) {
|
|
88
|
-
if (!("taskId" in ref)) {
|
|
89
|
-
if (ref.type !== "task" && ref.type !== "session") {
|
|
90
|
-
throw new Error(`entity reference taskId is required for ${ref.type}`);
|
|
91
|
-
}
|
|
92
|
-
return {
|
|
93
|
-
type: ref.type,
|
|
94
|
-
id: requireText(ref.id, "entity reference id")
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
const validated = validateTaskRecordReference({
|
|
98
|
-
taskId: ref.taskId,
|
|
99
|
-
localId: ref.id
|
|
100
|
-
}, mailboxTaskRecordKind(ref.type));
|
|
101
|
-
return {
|
|
102
|
-
type: ref.type,
|
|
103
|
-
taskId: validated.taskId,
|
|
104
|
-
id: validated.localId
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
189
|
export function mailboxEntityRefKey(ref) {
|
|
108
190
|
return !("taskId" in ref)
|
|
109
191
|
? `${ref.type}\u0000${ref.id}`
|
|
110
192
|
: `${ref.type}\u0000${ref.taskId}\u0000${ref.id}`;
|
|
111
193
|
}
|
|
112
|
-
function
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return result;
|
|
194
|
+
function mailboxContainsDedupeKey(mailbox, key) {
|
|
195
|
+
return mailbox.recentDedupeKeys.includes(key)
|
|
196
|
+
|| mailbox.pending?.dedupeKeys.includes(key) === true
|
|
197
|
+
|| mailbox.processing?.batch.dedupeKeys.includes(key) === true;
|
|
198
|
+
}
|
|
199
|
+
function rememberDedupeKeys(existing, dedupeKeys) {
|
|
200
|
+
return appendUnique(existing, dedupeKeys, (value) => value)
|
|
201
|
+
.slice(-RECENT_DEDUPE_KEY_LIMIT);
|
|
123
202
|
}
|
|
124
203
|
function mergeBatches(left, right) {
|
|
125
|
-
|
|
204
|
+
return {
|
|
126
205
|
fromSequence: Math.min(left.fromSequence, right.fromSequence),
|
|
127
206
|
toSequence: Math.max(left.toSequence, right.toSequence),
|
|
128
207
|
reasons: appendUnique(left.reasons, right.reasons, (reason) => reason),
|
|
@@ -136,158 +215,12 @@ function mergeBatches(left, right) {
|
|
|
136
215
|
: right.lastQueuedAt,
|
|
137
216
|
sources: appendUnique(left.sources, right.sources, (source) => source),
|
|
138
217
|
dedupeKeys: appendUnique(left.dedupeKeys, right.dedupeKeys, (key) => key),
|
|
139
|
-
deliveryModes: appendUnique(left.deliveryModes, right.deliveryModes, (mode) => mode),
|
|
140
218
|
...((left.highestFactRevision ?? right.highestFactRevision) === undefined
|
|
141
219
|
? {}
|
|
142
220
|
: {
|
|
143
221
|
highestFactRevision: Math.max(left.highestFactRevision ?? 0, right.highestFactRevision ?? 0)
|
|
144
222
|
})
|
|
145
223
|
};
|
|
146
|
-
return merged;
|
|
147
|
-
}
|
|
148
|
-
function requireProcessing(mailbox, batchId) {
|
|
149
|
-
const normalizedBatchId = requireText(batchId, "batch id");
|
|
150
|
-
if (mailbox.processing === null) {
|
|
151
|
-
throw new Error("Mailbox has no processing batch");
|
|
152
|
-
}
|
|
153
|
-
if (mailbox.processing.batchId !== normalizedBatchId) {
|
|
154
|
-
throw new Error(`Mailbox processing batch id does not match ${normalizedBatchId}`);
|
|
155
|
-
}
|
|
156
|
-
return mailbox.processing;
|
|
157
|
-
}
|
|
158
|
-
export function createWorkMailbox(target) {
|
|
159
|
-
return {
|
|
160
|
-
schemaVersion: 3,
|
|
161
|
-
target: copyTarget(target),
|
|
162
|
-
nextSequence: 1,
|
|
163
|
-
processing: null,
|
|
164
|
-
pending: {
|
|
165
|
-
normal: null,
|
|
166
|
-
userCorrection: null,
|
|
167
|
-
cursors: { normal: 0, userCorrection: 0 },
|
|
168
|
-
recentDedupeKeys: []
|
|
169
|
-
},
|
|
170
|
-
inputDelivery: null
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
export function validateWorkMailbox(value) {
|
|
174
|
-
const mailbox = record(value, "WorkMailbox");
|
|
175
|
-
exact(mailbox, ["schemaVersion", "target", "nextSequence", "processing", "pending", "inputDelivery"], "WorkMailbox");
|
|
176
|
-
if (mailbox.schemaVersion !== 3)
|
|
177
|
-
throw new Error("WorkMailbox must use schemaVersion 3");
|
|
178
|
-
const target = parseTarget(mailbox.target);
|
|
179
|
-
const nextSequence = requireInteger(mailbox.nextSequence, 1, "WorkMailbox nextSequence");
|
|
180
|
-
const processing = mailbox.processing === null ? null : parseProcessing(mailbox.processing);
|
|
181
|
-
const pending = parsePendingLanes(mailbox.pending);
|
|
182
|
-
for (const cursor of [pending.cursors.normal, pending.cursors.userCorrection]) {
|
|
183
|
-
if (cursor >= nextSequence) {
|
|
184
|
-
throw new Error("WorkMailbox lane cursor must be lower than nextSequence");
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
if (pending.normal !== null && pending.normal.fromSequence <= pending.cursors.normal) {
|
|
188
|
-
throw new Error("WorkMailbox normal pending batch is behind its cursor");
|
|
189
|
-
}
|
|
190
|
-
if (pending.userCorrection !== null
|
|
191
|
-
&& pending.userCorrection.fromSequence <= pending.cursors.userCorrection) {
|
|
192
|
-
throw new Error("WorkMailbox user-correction pending batch is behind its cursor");
|
|
193
|
-
}
|
|
194
|
-
const inputDelivery = mailbox.inputDelivery === null
|
|
195
|
-
? null
|
|
196
|
-
: parseInputDelivery(mailbox.inputDelivery);
|
|
197
|
-
const batches = [
|
|
198
|
-
processing?.batch,
|
|
199
|
-
pending.normal,
|
|
200
|
-
pending.userCorrection,
|
|
201
|
-
inputDelivery?.batch
|
|
202
|
-
].filter((batch) => batch !== null && batch !== undefined);
|
|
203
|
-
const activeDedupeKeys = new Set();
|
|
204
|
-
for (const batch of batches) {
|
|
205
|
-
if (batch.toSequence >= nextSequence) {
|
|
206
|
-
throw new Error("WorkMailbox batch sequence must be lower than nextSequence");
|
|
207
|
-
}
|
|
208
|
-
for (const dedupeKey of batch.dedupeKeys) {
|
|
209
|
-
if (activeDedupeKeys.has(dedupeKey)) {
|
|
210
|
-
throw new Error("WorkMailbox active batches share a dedupe key");
|
|
211
|
-
}
|
|
212
|
-
activeDedupeKeys.add(dedupeKey);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
if (inputDelivery !== null && target.kind !== "role" && target.kind !== "operator") {
|
|
216
|
-
throw new Error("WorkMailbox inputDelivery requires a Role or Operator target");
|
|
217
|
-
}
|
|
218
|
-
return { schemaVersion: 3, target, nextSequence, processing, pending, inputDelivery };
|
|
219
|
-
}
|
|
220
|
-
function parsePendingLanes(value) {
|
|
221
|
-
const lanes = record(value, "WorkMailbox pending");
|
|
222
|
-
exact(lanes, ["normal", "userCorrection", "cursors", "recentDedupeKeys"], "WorkMailbox pending");
|
|
223
|
-
const cursors = record(lanes.cursors, "WorkMailbox pending cursors");
|
|
224
|
-
exact(cursors, ["normal", "userCorrection"], "WorkMailbox pending cursors");
|
|
225
|
-
const recentDedupeKeys = requireStringArray(lanes.recentDedupeKeys, "WorkMailbox recent dedupe keys");
|
|
226
|
-
if (recentDedupeKeys.length > RECENT_DEDUPE_KEY_LIMIT) {
|
|
227
|
-
throw new Error("WorkMailbox recent dedupe keys exceed the bounded limit");
|
|
228
|
-
}
|
|
229
|
-
return {
|
|
230
|
-
normal: lanes.normal === null ? null : parseBatch(lanes.normal, "WorkMailbox pending normal"),
|
|
231
|
-
userCorrection: lanes.userCorrection === null
|
|
232
|
-
? null
|
|
233
|
-
: parseBatch(lanes.userCorrection, "WorkMailbox pending userCorrection"),
|
|
234
|
-
cursors: {
|
|
235
|
-
normal: requireInteger(cursors.normal, 0, "WorkMailbox normal cursor"),
|
|
236
|
-
userCorrection: requireInteger(cursors.userCorrection, 0, "WorkMailbox user-correction cursor")
|
|
237
|
-
},
|
|
238
|
-
recentDedupeKeys
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
function parseTarget(value) {
|
|
242
|
-
const target = record(value, "WorkMailbox target");
|
|
243
|
-
switch (target.kind) {
|
|
244
|
-
case "operator":
|
|
245
|
-
exact(target, ["kind"], "WorkMailbox operator target");
|
|
246
|
-
return { kind: "operator" };
|
|
247
|
-
case "task":
|
|
248
|
-
exact(target, ["kind", "taskId"], "WorkMailbox task target");
|
|
249
|
-
return { kind: "task", taskId: requireString(target.taskId, "WorkMailbox target taskId") };
|
|
250
|
-
case "role":
|
|
251
|
-
exact(target, ["kind", "taskId", "roleName"], "WorkMailbox role target");
|
|
252
|
-
return {
|
|
253
|
-
kind: "role",
|
|
254
|
-
taskId: requireString(target.taskId, "WorkMailbox target taskId"),
|
|
255
|
-
roleName: requireString(target.roleName, "WorkMailbox target roleName")
|
|
256
|
-
};
|
|
257
|
-
case "role-runtime":
|
|
258
|
-
exact(target, ["kind", "taskId", "roleName"], "WorkMailbox role runtime target");
|
|
259
|
-
return {
|
|
260
|
-
kind: "role-runtime",
|
|
261
|
-
taskId: requireString(target.taskId, "WorkMailbox target taskId"),
|
|
262
|
-
roleName: requireString(target.roleName, "WorkMailbox target roleName")
|
|
263
|
-
};
|
|
264
|
-
case "global-role-runtime":
|
|
265
|
-
exact(target, ["kind", "roleName"], "WorkMailbox global role runtime target");
|
|
266
|
-
return {
|
|
267
|
-
kind: "global-role-runtime",
|
|
268
|
-
roleName: requireString(target.roleName, "WorkMailbox target roleName")
|
|
269
|
-
};
|
|
270
|
-
default:
|
|
271
|
-
throw new Error("WorkMailbox target kind is invalid");
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
function parseRef(value, label) {
|
|
275
|
-
const ref = record(value, label);
|
|
276
|
-
const types = [
|
|
277
|
-
"task", "run", "work-item", "input", "session", "message", "event"
|
|
278
|
-
];
|
|
279
|
-
if (!types.includes(ref.type))
|
|
280
|
-
throw new Error(`${label} type is invalid`);
|
|
281
|
-
if (ref.type === "task" || ref.type === "session") {
|
|
282
|
-
exact(ref, ["type", "id"], label);
|
|
283
|
-
return { type: ref.type, id: requireString(ref.id, `${label} id`) };
|
|
284
|
-
}
|
|
285
|
-
exact(ref, ["type", "taskId", "id"], label);
|
|
286
|
-
return copyRef({
|
|
287
|
-
type: ref.type,
|
|
288
|
-
taskId: requireString(ref.taskId, `${label} taskId`),
|
|
289
|
-
id: requireString(ref.id, `${label} id`)
|
|
290
|
-
});
|
|
291
224
|
}
|
|
292
225
|
function parseBatch(value, label) {
|
|
293
226
|
const batch = record(value, label);
|
|
@@ -300,8 +233,7 @@ function parseBatch(value, label) {
|
|
|
300
233
|
"firstQueuedAt",
|
|
301
234
|
"lastQueuedAt",
|
|
302
235
|
"sources",
|
|
303
|
-
"dedupeKeys"
|
|
304
|
-
"deliveryModes"
|
|
236
|
+
"dedupeKeys"
|
|
305
237
|
];
|
|
306
238
|
exact(batch, batch.highestFactRevision === undefined ? required : [...required, "highestFactRevision"], label);
|
|
307
239
|
const fromSequence = requireInteger(batch.fromSequence, 1, `${label} fromSequence`);
|
|
@@ -316,10 +248,10 @@ function parseBatch(value, label) {
|
|
|
316
248
|
if (!Array.isArray(batch.refs))
|
|
317
249
|
throw new Error(`${label} refs must be an array`);
|
|
318
250
|
const refs = batch.refs.map((ref, index) => parseRef(ref, `${label} refs[${index}]`));
|
|
319
|
-
|
|
320
|
-
if (new Set(refKeys).size !== refKeys.length)
|
|
251
|
+
if (new Set(refs.map(mailboxEntityRefKey)).size !== refs.length) {
|
|
321
252
|
throw new Error(`${label} refs must not contain duplicates`);
|
|
322
|
-
|
|
253
|
+
}
|
|
254
|
+
return {
|
|
323
255
|
fromSequence,
|
|
324
256
|
toSequence,
|
|
325
257
|
reasons,
|
|
@@ -329,384 +261,182 @@ function parseBatch(value, label) {
|
|
|
329
261
|
lastQueuedAt: requireTimestamp(batch.lastQueuedAt, `${label} lastQueuedAt`),
|
|
330
262
|
sources: requireStringArray(batch.sources, `${label} sources`),
|
|
331
263
|
dedupeKeys: requireStringArray(batch.dedupeKeys, `${label} dedupeKeys`),
|
|
332
|
-
deliveryModes: parseDeliveryModes(batch.deliveryModes, `${label} deliveryModes`),
|
|
333
264
|
...(batch.highestFactRevision === undefined
|
|
334
265
|
? {}
|
|
335
266
|
: {
|
|
336
267
|
highestFactRevision: requireInteger(batch.highestFactRevision, 0, `${label} highestFactRevision`)
|
|
337
268
|
})
|
|
338
269
|
};
|
|
339
|
-
return result;
|
|
340
|
-
}
|
|
341
|
-
function parseDeliveryModes(value, label) {
|
|
342
|
-
const modes = requireStringArray(value, label);
|
|
343
|
-
for (const mode of modes) {
|
|
344
|
-
if (mode !== "followup" && mode !== "steer-if-safe" && mode !== "inject") {
|
|
345
|
-
throw new Error(`${label} contains an invalid delivery mode`);
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return modes;
|
|
349
270
|
}
|
|
350
271
|
function parseProcessing(value) {
|
|
351
272
|
const processing = record(value, "WorkMailbox processing");
|
|
352
|
-
const required = ["batchId", "
|
|
273
|
+
const required = ["batchId", "batch", "owner", "startedAt"];
|
|
353
274
|
exact(processing, processing.executionRef === undefined ? required : [...required, "executionRef"], "WorkMailbox processing");
|
|
354
275
|
const result = {
|
|
355
276
|
batchId: requireString(processing.batchId, "WorkMailbox processing batchId"),
|
|
356
|
-
lane: parseLane(processing.lane, "WorkMailbox processing lane"),
|
|
357
277
|
batch: parseBatch(processing.batch, "WorkMailbox processing batch"),
|
|
358
278
|
owner: requireString(processing.owner, "WorkMailbox processing owner"),
|
|
359
279
|
startedAt: requireTimestamp(processing.startedAt, "WorkMailbox processing startedAt")
|
|
360
280
|
};
|
|
361
281
|
return processing.executionRef === undefined
|
|
362
282
|
? result
|
|
363
|
-
: {
|
|
283
|
+
: {
|
|
284
|
+
...result,
|
|
285
|
+
executionRef: parseRef(processing.executionRef, "WorkMailbox processing executionRef")
|
|
286
|
+
};
|
|
364
287
|
}
|
|
365
|
-
function
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
"
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
"
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
"
|
|
375
|
-
"
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
throw new Error("WorkMailbox delivery-unknown requires exactly one unknownReason");
|
|
391
|
-
}
|
|
392
|
-
const result = {
|
|
393
|
-
attemptId: requireString(input.attemptId, "WorkMailbox inputDelivery attemptId"),
|
|
394
|
-
lane,
|
|
395
|
-
mode,
|
|
396
|
-
batch: parseBatch(input.batch, "WorkMailbox inputDelivery batch"),
|
|
397
|
-
owner: requireString(input.owner, "WorkMailbox inputDelivery owner"),
|
|
398
|
-
status,
|
|
399
|
-
startedAt: requireTimestamp(input.startedAt, "WorkMailbox inputDelivery startedAt"),
|
|
400
|
-
executionRef: parseRef(input.executionRef, "WorkMailbox inputDelivery executionRef"),
|
|
401
|
-
...(input.pushedAt === undefined
|
|
402
|
-
? {}
|
|
403
|
-
: { pushedAt: requireTimestamp(input.pushedAt, "WorkMailbox inputDelivery pushedAt") }),
|
|
404
|
-
...(input.unknownReason === undefined
|
|
405
|
-
? {}
|
|
406
|
-
: { unknownReason: requireString(input.unknownReason, "WorkMailbox inputDelivery unknownReason") }),
|
|
407
|
-
...(input.providerFence === undefined
|
|
408
|
-
? {}
|
|
409
|
-
: { providerFence: parseProviderFence(input.providerFence) })
|
|
410
|
-
};
|
|
411
|
-
if (result.pushedAt !== undefined
|
|
412
|
-
&& Date.parse(result.pushedAt) < Date.parse(result.startedAt)) {
|
|
413
|
-
throw new Error("WorkMailbox inputDelivery pushedAt is earlier than startedAt");
|
|
288
|
+
function parseTarget(value) {
|
|
289
|
+
const target = record(value, "WorkMailbox target");
|
|
290
|
+
switch (target.kind) {
|
|
291
|
+
case "operator":
|
|
292
|
+
exact(target, ["kind"], "WorkMailbox operator target");
|
|
293
|
+
return { kind: "operator" };
|
|
294
|
+
case "task":
|
|
295
|
+
exact(target, ["kind", "taskId"], "WorkMailbox task target");
|
|
296
|
+
return { kind: "task", taskId: requireString(target.taskId, "WorkMailbox target taskId") };
|
|
297
|
+
case "role":
|
|
298
|
+
case "role-runtime":
|
|
299
|
+
exact(target, ["kind", "taskId", "roleName"], `WorkMailbox ${target.kind} target`);
|
|
300
|
+
return {
|
|
301
|
+
kind: target.kind,
|
|
302
|
+
taskId: requireString(target.taskId, "WorkMailbox target taskId"),
|
|
303
|
+
roleName: requireString(target.roleName, "WorkMailbox target roleName")
|
|
304
|
+
};
|
|
305
|
+
case "global-role-runtime":
|
|
306
|
+
exact(target, ["kind", "roleName"], "WorkMailbox global role runtime target");
|
|
307
|
+
return {
|
|
308
|
+
kind: "global-role-runtime",
|
|
309
|
+
roleName: requireString(target.roleName, "WorkMailbox target roleName")
|
|
310
|
+
};
|
|
311
|
+
default:
|
|
312
|
+
throw new Error("WorkMailbox target kind is invalid");
|
|
414
313
|
}
|
|
415
|
-
return result;
|
|
416
314
|
}
|
|
417
|
-
function
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
315
|
+
function parseRef(value, label) {
|
|
316
|
+
const ref = record(value, label);
|
|
317
|
+
const types = [
|
|
318
|
+
"task", "turn", "work-item", "input", "session", "message", "event"
|
|
319
|
+
];
|
|
320
|
+
if (!types.includes(ref.type))
|
|
321
|
+
throw new Error(`${label} type is invalid`);
|
|
322
|
+
if (ref.type === "task" || ref.type === "session") {
|
|
323
|
+
exact(ref, ["type", "id"], label);
|
|
324
|
+
return { type: ref.type, id: requireString(ref.id, `${label} id`) };
|
|
325
|
+
}
|
|
326
|
+
exact(ref, ["type", "taskId", "id"], label);
|
|
327
|
+
return copyRef({
|
|
328
|
+
type: ref.type,
|
|
329
|
+
taskId: requireString(ref.taskId, `${label} taskId`),
|
|
330
|
+
id: requireString(ref.id, `${label} id`)
|
|
331
|
+
});
|
|
429
332
|
}
|
|
430
|
-
function
|
|
431
|
-
|
|
432
|
-
|
|
333
|
+
function requireProcessing(mailbox, batchId) {
|
|
334
|
+
validateWorkMailbox(mailbox);
|
|
335
|
+
const normalizedBatchId = requireText(batchId, "batch id");
|
|
336
|
+
if (mailbox.processing === null)
|
|
337
|
+
throw new Error("Mailbox has no processing batch");
|
|
338
|
+
if (mailbox.processing.batchId !== normalizedBatchId) {
|
|
339
|
+
throw new Error(`Mailbox processing batch id does not match ${normalizedBatchId}`);
|
|
433
340
|
}
|
|
434
|
-
return
|
|
341
|
+
return mailbox.processing;
|
|
435
342
|
}
|
|
436
|
-
function
|
|
437
|
-
|
|
438
|
-
|
|
343
|
+
function copyTarget(target) {
|
|
344
|
+
switch (target.kind) {
|
|
345
|
+
case "operator": return { kind: "operator" };
|
|
346
|
+
case "task": return { kind: "task", taskId: requireText(target.taskId, "taskId") };
|
|
347
|
+
case "role":
|
|
348
|
+
case "role-runtime":
|
|
349
|
+
return {
|
|
350
|
+
kind: target.kind,
|
|
351
|
+
taskId: requireText(target.taskId, "taskId"),
|
|
352
|
+
roleName: requireText(target.roleName, "roleName")
|
|
353
|
+
};
|
|
354
|
+
case "global-role-runtime":
|
|
355
|
+
return {
|
|
356
|
+
kind: "global-role-runtime",
|
|
357
|
+
roleName: requireText(target.roleName, "roleName")
|
|
358
|
+
};
|
|
439
359
|
}
|
|
440
|
-
return value;
|
|
441
360
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const laneKey = lane === "normal" ? "normal" : "userCorrection";
|
|
447
|
-
const deliveryMode = signal.deliveryMode ?? "followup";
|
|
448
|
-
const source = requireText(signal.source ?? "yui", "signal source");
|
|
449
|
-
const refs = signal.refs.map(copyRef);
|
|
450
|
-
const sequence = mailbox.nextSequence;
|
|
451
|
-
const dedupeKey = signal.dedupeKey === undefined
|
|
452
|
-
? `sequence:${sequence}`
|
|
453
|
-
: requireText(signal.dedupeKey, "signal dedupeKey");
|
|
454
|
-
if (mailboxContainsDedupeKey(mailbox, dedupeKey))
|
|
455
|
-
return mailbox;
|
|
456
|
-
const incoming = {
|
|
457
|
-
fromSequence: sequence,
|
|
458
|
-
toSequence: sequence,
|
|
459
|
-
reasons: [reason],
|
|
460
|
-
refs: appendUnique([], refs, mailboxEntityRefKey),
|
|
461
|
-
requestCount: 1,
|
|
462
|
-
firstQueuedAt: occurredAt,
|
|
463
|
-
lastQueuedAt: occurredAt,
|
|
464
|
-
sources: [source],
|
|
465
|
-
dedupeKeys: [dedupeKey],
|
|
466
|
-
deliveryModes: [deliveryMode],
|
|
467
|
-
...(signal.factRevision === undefined
|
|
468
|
-
? {}
|
|
469
|
-
: { highestFactRevision: requireInteger(signal.factRevision, 0, "signal factRevision") })
|
|
470
|
-
};
|
|
471
|
-
return {
|
|
472
|
-
...mailbox,
|
|
473
|
-
nextSequence: sequence + 1,
|
|
474
|
-
pending: {
|
|
475
|
-
...mailbox.pending,
|
|
476
|
-
[laneKey]: mailbox.pending[laneKey] === null
|
|
477
|
-
? incoming
|
|
478
|
-
: mergeBatches(mailbox.pending[laneKey], incoming)
|
|
361
|
+
function copyRef(ref) {
|
|
362
|
+
if (!("taskId" in ref)) {
|
|
363
|
+
if (ref.type !== "task" && ref.type !== "session") {
|
|
364
|
+
throw new Error(`entity reference taskId is required for ${ref.type}`);
|
|
479
365
|
}
|
|
480
|
-
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
mailbox.pending.userCorrection,
|
|
488
|
-
mailbox.processing?.batch,
|
|
489
|
-
mailbox.inputDelivery?.batch
|
|
490
|
-
].some((batch) => batch?.dedupeKeys.includes(key) === true);
|
|
491
|
-
}
|
|
492
|
-
export function pendingLane(mailbox, lane) {
|
|
493
|
-
return lane === "normal" ? mailbox.pending.normal : mailbox.pending.userCorrection;
|
|
494
|
-
}
|
|
495
|
-
export function mailboxHasPending(mailbox) {
|
|
496
|
-
return mailbox.pending.normal !== null || mailbox.pending.userCorrection !== null;
|
|
497
|
-
}
|
|
498
|
-
export function nextPendingLane(mailbox) {
|
|
499
|
-
if (mailbox.pending.userCorrection !== null)
|
|
500
|
-
return "user-correction";
|
|
501
|
-
return mailbox.pending.normal === null ? null : "normal";
|
|
502
|
-
}
|
|
503
|
-
export function nextPendingBatch(mailbox) {
|
|
504
|
-
const lane = nextPendingLane(mailbox);
|
|
505
|
-
return lane === null ? null : pendingLane(mailbox, lane);
|
|
506
|
-
}
|
|
507
|
-
export function mailboxHasWork(mailbox) {
|
|
508
|
-
return mailbox.processing !== null
|
|
509
|
-
|| mailbox.inputDelivery !== null
|
|
510
|
-
|| mailboxHasPending(mailbox);
|
|
511
|
-
}
|
|
512
|
-
export function mailboxBatches(mailbox) {
|
|
513
|
-
return [
|
|
514
|
-
mailbox.processing?.batch,
|
|
515
|
-
mailbox.inputDelivery?.batch,
|
|
516
|
-
mailbox.pending.userCorrection,
|
|
517
|
-
mailbox.pending.normal
|
|
518
|
-
].filter((batch) => batch !== null && batch !== undefined);
|
|
366
|
+
return { type: ref.type, id: requireText(ref.id, "entity reference id") };
|
|
367
|
+
}
|
|
368
|
+
const validated = validateTaskRecordReference({
|
|
369
|
+
taskId: ref.taskId,
|
|
370
|
+
localId: ref.id
|
|
371
|
+
}, mailboxTaskRecordKind(ref.type));
|
|
372
|
+
return { type: ref.type, taskId: validated.taskId, id: validated.localId };
|
|
519
373
|
}
|
|
520
374
|
function mailboxTaskRecordKind(type) {
|
|
521
375
|
switch (type) {
|
|
522
|
-
case "
|
|
376
|
+
case "turn": return "turn";
|
|
523
377
|
case "work-item": return "workItem";
|
|
524
378
|
case "input": return "inputRequest";
|
|
525
379
|
case "message": return "message";
|
|
526
380
|
case "event": return "event";
|
|
527
381
|
}
|
|
528
382
|
}
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
if (batch === null)
|
|
539
|
-
throw new Error(`Mailbox has no pending ${lane} work to claim`);
|
|
540
|
-
const processing = {
|
|
541
|
-
batchId: requireText(options.batchId, "batch id"),
|
|
542
|
-
lane,
|
|
543
|
-
batch,
|
|
544
|
-
owner: requireText(options.owner, "claim owner"),
|
|
545
|
-
startedAt: requireText(options.startedAt, "claim startedAt")
|
|
546
|
-
};
|
|
547
|
-
return {
|
|
548
|
-
...mailbox,
|
|
549
|
-
processing,
|
|
550
|
-
pending: {
|
|
551
|
-
...mailbox.pending,
|
|
552
|
-
[lane === "normal" ? "normal" : "userCorrection"]: null
|
|
553
|
-
}
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
export function bindExecution(mailbox, batchId, executionRef) {
|
|
557
|
-
const processing = requireProcessing(mailbox, batchId);
|
|
558
|
-
return {
|
|
559
|
-
...mailbox,
|
|
560
|
-
processing: { ...processing, executionRef: copyRef(executionRef) }
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
export function completeProcessing(mailbox, batchId) {
|
|
564
|
-
const processing = requireProcessing(mailbox, batchId);
|
|
565
|
-
const lane = processing.lane;
|
|
566
|
-
return validateWorkMailbox({
|
|
567
|
-
...mailbox,
|
|
568
|
-
processing: null,
|
|
569
|
-
pending: advanceLaneCursor(mailbox.pending, lane, processing.batch)
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
export function releaseProcessing(mailbox, batchId) {
|
|
573
|
-
const processing = requireProcessing(mailbox, batchId);
|
|
574
|
-
const lane = processing.lane;
|
|
575
|
-
const key = lane === "normal" ? "normal" : "userCorrection";
|
|
576
|
-
const released = mailbox.pending[key] === null
|
|
577
|
-
? processing.batch
|
|
578
|
-
: mergeBatches(processing.batch, mailbox.pending[key]);
|
|
579
|
-
return {
|
|
580
|
-
...mailbox,
|
|
581
|
-
processing: null,
|
|
582
|
-
pending: { ...mailbox.pending, [key]: released }
|
|
583
|
-
};
|
|
584
|
-
}
|
|
585
|
-
function requireInputDelivery(mailbox, attemptId) {
|
|
586
|
-
const id = requireText(attemptId, "input delivery attempt id");
|
|
587
|
-
if (mailbox.inputDelivery === null)
|
|
588
|
-
throw new Error("Mailbox has no input delivery");
|
|
589
|
-
if (mailbox.inputDelivery.attemptId !== id) {
|
|
590
|
-
throw new Error(`Mailbox input delivery does not match ${id}`);
|
|
383
|
+
function appendUnique(existing, incoming, keyOf) {
|
|
384
|
+
const result = [...existing];
|
|
385
|
+
const keys = new Set(existing.map(keyOf));
|
|
386
|
+
for (const value of incoming) {
|
|
387
|
+
const key = keyOf(value);
|
|
388
|
+
if (keys.has(key))
|
|
389
|
+
continue;
|
|
390
|
+
keys.add(key);
|
|
391
|
+
result.push(value);
|
|
591
392
|
}
|
|
592
|
-
return
|
|
393
|
+
return result;
|
|
593
394
|
}
|
|
594
|
-
|
|
595
|
-
if (
|
|
596
|
-
|
|
597
|
-
return mailbox;
|
|
598
|
-
throw new Error("Mailbox already has an input delivery");
|
|
395
|
+
function record(value, label) {
|
|
396
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
397
|
+
throw new Error(`${label} must be an object`);
|
|
599
398
|
}
|
|
600
|
-
|
|
601
|
-
if (batch === null)
|
|
602
|
-
throw new Error(`Mailbox has no pending ${options.lane} input`);
|
|
603
|
-
const inputDelivery = {
|
|
604
|
-
attemptId: requireText(options.attemptId, "input delivery attempt id"),
|
|
605
|
-
lane: options.lane,
|
|
606
|
-
mode: options.mode,
|
|
607
|
-
batch,
|
|
608
|
-
owner: requireText(options.owner, "input delivery owner"),
|
|
609
|
-
status: "dispatching",
|
|
610
|
-
startedAt: requireTimestamp(options.startedAt, "input delivery startedAt"),
|
|
611
|
-
executionRef: copyRef(options.executionRef),
|
|
612
|
-
...(options.providerFence === undefined
|
|
613
|
-
? {}
|
|
614
|
-
: { providerFence: parseProviderFence(options.providerFence) })
|
|
615
|
-
};
|
|
616
|
-
return validateWorkMailbox({
|
|
617
|
-
...mailbox,
|
|
618
|
-
pending: {
|
|
619
|
-
...mailbox.pending,
|
|
620
|
-
[options.lane === "normal" ? "normal" : "userCorrection"]: null
|
|
621
|
-
},
|
|
622
|
-
inputDelivery
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
export function markInputDeliveryPushed(mailbox, attemptId, pushedAt) {
|
|
626
|
-
const delivery = requireInputDelivery(mailbox, attemptId);
|
|
627
|
-
if (delivery.pushedAt !== undefined)
|
|
628
|
-
return mailbox;
|
|
629
|
-
const timestamp = requireTimestamp(pushedAt.toISOString(), "input delivery pushedAt");
|
|
630
|
-
return validateWorkMailbox({
|
|
631
|
-
...mailbox,
|
|
632
|
-
inputDelivery: { ...delivery, pushedAt: timestamp }
|
|
633
|
-
});
|
|
399
|
+
return value;
|
|
634
400
|
}
|
|
635
|
-
|
|
636
|
-
const
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
unknownReason: requireText(reason, "delivery unknown reason"),
|
|
644
|
-
pushedAt: delivery.pushedAt ?? timestamp
|
|
645
|
-
}
|
|
646
|
-
});
|
|
401
|
+
function exact(value, keys, label) {
|
|
402
|
+
const expected = new Set(keys);
|
|
403
|
+
const unknown = Object.keys(value).find((key) => !expected.has(key));
|
|
404
|
+
if (unknown !== undefined)
|
|
405
|
+
throw new Error(`${label} has unknown field: ${unknown}`);
|
|
406
|
+
const missing = keys.find((key) => !Object.hasOwn(value, key));
|
|
407
|
+
if (missing !== undefined)
|
|
408
|
+
throw new Error(`${label} is missing field: ${missing}`);
|
|
647
409
|
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
410
|
+
function requireText(value, label) {
|
|
411
|
+
if (typeof value !== "string" || value.includes("\0"))
|
|
412
|
+
throw new Error(`${label} is invalid`);
|
|
413
|
+
const normalized = value.trim();
|
|
414
|
+
if (normalized.length === 0)
|
|
415
|
+
throw new Error(`${label} must not be empty`);
|
|
416
|
+
return normalized;
|
|
655
417
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
throw new Error(`Mailbox has no pending ${lane} batch`);
|
|
661
|
-
return validateWorkMailbox({
|
|
662
|
-
...mailbox,
|
|
663
|
-
pending: {
|
|
664
|
-
...advanceLaneCursor(mailbox.pending, lane, batch),
|
|
665
|
-
[key]: null
|
|
666
|
-
}
|
|
667
|
-
});
|
|
418
|
+
function requireString(value, label) {
|
|
419
|
+
if (typeof value !== "string")
|
|
420
|
+
throw new Error(`${label} must be a string`);
|
|
421
|
+
return requireText(value, label);
|
|
668
422
|
}
|
|
669
|
-
function
|
|
670
|
-
const
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
cursors: {
|
|
675
|
-
...pending.cursors,
|
|
676
|
-
[key]: Math.max(pending.cursors[key], batch.toSequence)
|
|
677
|
-
},
|
|
678
|
-
recentDedupeKeys
|
|
679
|
-
};
|
|
423
|
+
function requireTimestamp(value, label) {
|
|
424
|
+
const timestamp = requireString(value, label);
|
|
425
|
+
if (!Number.isFinite(Date.parse(timestamp)))
|
|
426
|
+
throw new Error(`${label} must be a timestamp`);
|
|
427
|
+
return timestamp;
|
|
680
428
|
}
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
throw new Error("A pushed or delivery-unknown input cannot return to pending");
|
|
429
|
+
function requireInteger(value, minimum, label) {
|
|
430
|
+
if (!Number.isInteger(value) || value < minimum) {
|
|
431
|
+
throw new Error(`${label} must be an integer greater than or equal to ${minimum}`);
|
|
685
432
|
}
|
|
686
|
-
|
|
687
|
-
return validateWorkMailbox({
|
|
688
|
-
...mailbox,
|
|
689
|
-
pending: {
|
|
690
|
-
...mailbox.pending,
|
|
691
|
-
[key]: mailbox.pending[key] === null
|
|
692
|
-
? delivery.batch
|
|
693
|
-
: mergeBatches(delivery.batch, mailbox.pending[key])
|
|
694
|
-
},
|
|
695
|
-
inputDelivery: null
|
|
696
|
-
});
|
|
433
|
+
return value;
|
|
697
434
|
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
const
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
...mailbox.pending,
|
|
706
|
-
[key]: mailbox.pending[key] === null
|
|
707
|
-
? delivery.batch
|
|
708
|
-
: mergeBatches(delivery.batch, mailbox.pending[key])
|
|
709
|
-
},
|
|
710
|
-
inputDelivery: null
|
|
711
|
-
});
|
|
435
|
+
function requireStringArray(value, label) {
|
|
436
|
+
if (!Array.isArray(value))
|
|
437
|
+
throw new Error(`${label} must be an array`);
|
|
438
|
+
const result = value.map((item, index) => requireString(item, `${label}[${index}]`));
|
|
439
|
+
if (new Set(result).size !== result.length)
|
|
440
|
+
throw new Error(`${label} must not contain duplicates`);
|
|
441
|
+
return result;
|
|
712
442
|
}
|