@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
|
@@ -96,7 +96,7 @@ export function assertExecutionTargetUnchanged(group, target) {
|
|
|
96
96
|
* Enforce the only durable Group/Lane evolution accepted by both domain
|
|
97
97
|
* helpers and storage. Identity, target, prior results, and a final Leader
|
|
98
98
|
* resolution never move backwards. A terminal Lane may only reopen as an
|
|
99
|
-
* explicit retry with a fresh
|
|
99
|
+
* explicit retry with a fresh Turn identity, or reset to pending for a
|
|
100
100
|
* Task-final Review execution retry.
|
|
101
101
|
*/
|
|
102
102
|
export function assertExecutionGroupTransition(existing, candidate) {
|
|
@@ -153,11 +153,11 @@ export function updateExecutionLane(group, laneId, patch, now) {
|
|
|
153
153
|
}
|
|
154
154
|
const status = patch.status ?? existing.status;
|
|
155
155
|
const timestamp = now.toISOString();
|
|
156
|
-
const terminal = status === "completed" || status === "failed"
|
|
156
|
+
const terminal = status === "completed" || status === "failed";
|
|
157
157
|
const next = {
|
|
158
158
|
...existing,
|
|
159
159
|
...(patch.effective === undefined ? {} : { effective: validateEffectiveLaunchSnapshot(patch.effective) }),
|
|
160
|
-
...(patch.
|
|
160
|
+
...(patch.turnId === undefined ? {} : { turnId: requireIdentity(patch.turnId, "Turn id") }),
|
|
161
161
|
...(patch.sessionId === undefined ? {} : { sessionId: requireIdentity(patch.sessionId, "Session id") }),
|
|
162
162
|
...(patch.reviewRoundId === undefined ? {} : { reviewRoundId: requireIdentity(patch.reviewRoundId, "ReviewRound id") }),
|
|
163
163
|
...(patch.workspace === undefined ? {} : { workspace: validateLaneWorkspace(patch.workspace) }),
|
|
@@ -173,8 +173,8 @@ export function updateExecutionLane(group, laneId, patch, now) {
|
|
|
173
173
|
});
|
|
174
174
|
}
|
|
175
175
|
/**
|
|
176
|
-
* Reopens only a failed Lane for an explicit WorkItem retry. Completed
|
|
177
|
-
*
|
|
176
|
+
* Reopens only a failed Lane for an explicit WorkItem retry. Completed
|
|
177
|
+
* results are immutable reusable outputs; retrying them would erase
|
|
178
178
|
* the Group's durable evidence instead of recovering unfinished work.
|
|
179
179
|
*/
|
|
180
180
|
export function restartExecutionLane(group, laneId, patch, now) {
|
|
@@ -187,14 +187,14 @@ export function restartExecutionLane(group, laneId, patch, now) {
|
|
|
187
187
|
if (existing === undefined)
|
|
188
188
|
throw new Error(`ExecutionLane not found: ${group.id}/${id}.`);
|
|
189
189
|
if (existing.status !== "failed") {
|
|
190
|
-
throw new Error(`Only failed ExecutionLanes can start a new
|
|
190
|
+
throw new Error(`Only failed ExecutionLanes can start a new Turn: ${group.id}/${id}.`);
|
|
191
191
|
}
|
|
192
192
|
const timestamp = now.toISOString();
|
|
193
193
|
const { result: _result, endedAt: _endedAt, ...base } = existing;
|
|
194
194
|
const next = {
|
|
195
195
|
...base,
|
|
196
196
|
...(patch.effective === undefined ? {} : { effective: validateEffectiveLaunchSnapshot(patch.effective) }),
|
|
197
|
-
...(patch.
|
|
197
|
+
...(patch.turnId === undefined ? {} : { turnId: requireIdentity(patch.turnId, "Turn id") }),
|
|
198
198
|
...(patch.sessionId === undefined ? {} : { sessionId: requireIdentity(patch.sessionId, "Session id") }),
|
|
199
199
|
...(patch.reviewRoundId === undefined ? {} : { reviewRoundId: requireIdentity(patch.reviewRoundId, "ReviewRound id") }),
|
|
200
200
|
...(patch.workspace === undefined ? {} : { workspace: validateLaneWorkspace(patch.workspace) }),
|
|
@@ -209,8 +209,8 @@ export function restartExecutionLane(group, laneId, patch, now) {
|
|
|
209
209
|
});
|
|
210
210
|
}
|
|
211
211
|
/**
|
|
212
|
-
* Reopen a failed execution Lane without starting its next
|
|
213
|
-
* Broker capacity is unavailable. The failed attempt remains in
|
|
212
|
+
* Reopen a failed execution Lane without starting its next Turn when Resource
|
|
213
|
+
* Broker capacity is unavailable. The failed attempt remains in Turn
|
|
214
214
|
* history; the Lane freezes the next launch input until ordinary dispatch can
|
|
215
215
|
* admit it.
|
|
216
216
|
*/
|
|
@@ -229,7 +229,7 @@ export function queueExecutionLaneRetry(group, laneId, patch, now) {
|
|
|
229
229
|
throw new Error(`Only a failed ExecutionLane can queue a retry: ${group.id}/${laneId}.`);
|
|
230
230
|
}
|
|
231
231
|
const timestamp = now.toISOString();
|
|
232
|
-
const {
|
|
232
|
+
const { turnId: _turnId, sessionId: _sessionId, result: _result, endedAt: _endedAt, ...base } = existing;
|
|
233
233
|
const next = {
|
|
234
234
|
...base,
|
|
235
235
|
effective: validateEffectiveLaunchSnapshot(patch.effective),
|
|
@@ -246,7 +246,7 @@ export function queueExecutionLaneRetry(group, laneId, patch, now) {
|
|
|
246
246
|
}
|
|
247
247
|
/**
|
|
248
248
|
* Resets a terminal Reviewer Lane to pending without replacing its ExecutionGroup.
|
|
249
|
-
* The old
|
|
249
|
+
* The old Turn remains the attempt trail; clearing the Lane's Turn/Session and
|
|
250
250
|
* result lets the same semantic ReviewRound be dispatched again.
|
|
251
251
|
*/
|
|
252
252
|
export function resetReviewExecutionLane(group, laneId, now) {
|
|
@@ -263,7 +263,7 @@ export function resetReviewExecutionLane(group, laneId, now) {
|
|
|
263
263
|
if (!isTerminalLane(existing.status))
|
|
264
264
|
return existing;
|
|
265
265
|
const timestamp = now.toISOString();
|
|
266
|
-
const { effective: _effective,
|
|
266
|
+
const { effective: _effective, turnId: _turnId, sessionId: _sessionId, result: _result, endedAt: _endedAt, ...base } = existing;
|
|
267
267
|
return validateExecutionLane({
|
|
268
268
|
...base,
|
|
269
269
|
status: "pending",
|
|
@@ -289,7 +289,7 @@ export function recordExecutionLaneResult(group, laneId, result, status, now) {
|
|
|
289
289
|
});
|
|
290
290
|
}
|
|
291
291
|
/**
|
|
292
|
-
* Stop only work that has never started. Running Lanes retain their exact
|
|
292
|
+
* Stop only work that has never started. Running Lanes retain their exact Turn
|
|
293
293
|
* and Session; the Resource Broker never kills a straggler merely to save
|
|
294
294
|
* budget. Skipped Lanes are terminal but never usable Candidate inputs.
|
|
295
295
|
*/
|
|
@@ -309,7 +309,7 @@ export function skipPendingExecutionLanes(group, laneIds, summary, now) {
|
|
|
309
309
|
const lane = group.lanes.find(({ id }) => id === laneId);
|
|
310
310
|
if (lane === undefined)
|
|
311
311
|
throw new Error(`ExecutionLane not found: ${group.id}/${laneId}.`);
|
|
312
|
-
if (lane.status !== "pending" || lane.
|
|
312
|
+
if (lane.status !== "pending" || lane.turnId !== undefined) {
|
|
313
313
|
throw new Error(`Only an unstarted pending ExecutionLane can be skipped: ${group.id}/${laneId}.`);
|
|
314
314
|
}
|
|
315
315
|
}
|
|
@@ -339,7 +339,7 @@ export function resolveExecutionGroup(group, input, now) {
|
|
|
339
339
|
const summary = requireText(input.summary, "Execution resolution summary");
|
|
340
340
|
const defaultSelectedLaneIds = input.decision === "accept"
|
|
341
341
|
? group.lanes
|
|
342
|
-
.filter((lane) => lane.status === "
|
|
342
|
+
.filter((lane) => lane.status === "completed")
|
|
343
343
|
.map(({ id }) => id)
|
|
344
344
|
: group.lanes.map(({ id }) => id);
|
|
345
345
|
const selected = [...new Set((input.selectedLaneIds ?? defaultSelectedLaneIds)
|
|
@@ -349,12 +349,12 @@ export function resolveExecutionGroup(group, input, now) {
|
|
|
349
349
|
}
|
|
350
350
|
if (input.decision === "accept" && selected.some((id) => {
|
|
351
351
|
const lane = group.lanes.find((candidate) => candidate.id === id);
|
|
352
|
-
return lane?.status !== "
|
|
352
|
+
return lane?.status !== "completed";
|
|
353
353
|
})) {
|
|
354
354
|
throw new Error(`ExecutionGroup accept selects a Lane without usable terminal output: ${group.id}.`);
|
|
355
355
|
}
|
|
356
356
|
if (input.decision === "accept" && group.stage?.resources !== undefined) {
|
|
357
|
-
const usable = group.lanes.filter(({ status }) =>
|
|
357
|
+
const usable = group.lanes.filter(({ status }) => status === "completed").length;
|
|
358
358
|
if (usable < group.stage.resources.quorum) {
|
|
359
359
|
throw new Error(`ExecutionGroup quorum is not met: ${usable}/${group.stage.resources.quorum} usable Lanes.`);
|
|
360
360
|
}
|
|
@@ -398,7 +398,7 @@ export function summarizeExecutionGroup(group) {
|
|
|
398
398
|
laneId: lane.id,
|
|
399
399
|
roleName: lane.roleName,
|
|
400
400
|
ordinal: lane.ordinal,
|
|
401
|
-
...(lane.
|
|
401
|
+
...(lane.turnId === undefined ? {} : { turnId: lane.turnId }),
|
|
402
402
|
...(lane.sessionId === undefined ? {} : { sessionId: lane.sessionId }),
|
|
403
403
|
...(lane.effective === undefined ? {} : { effective: lane.effective }),
|
|
404
404
|
...(lane.directive === undefined ? {} : { directive: lane.directive }),
|
|
@@ -571,7 +571,7 @@ export function validateExecutionLane(lane, group) {
|
|
|
571
571
|
throw new Error(`ExecutionLane belongs to another Group: ${lane.id}.`);
|
|
572
572
|
if (group.purpose === "review" && lane.reviewRoundId === undefined) {
|
|
573
573
|
// A review lane may be pending before its ReviewRound is dispatched, but
|
|
574
|
-
// it must acquire that identity before it can
|
|
574
|
+
// it must acquire that identity before it can turn.
|
|
575
575
|
if (lane.status === "running"
|
|
576
576
|
|| lane.status === "completed"
|
|
577
577
|
|| lane.status === "failed"
|
|
@@ -584,8 +584,8 @@ export function validateExecutionLane(lane, group) {
|
|
|
584
584
|
requireIdentity(lane.roleName, "ExecutionLane Role");
|
|
585
585
|
if (lane.effective !== undefined)
|
|
586
586
|
validateEffectiveLaunchSnapshot(lane.effective);
|
|
587
|
-
if (lane.
|
|
588
|
-
requireIdentity(lane.
|
|
587
|
+
if (lane.turnId !== undefined)
|
|
588
|
+
requireIdentity(lane.turnId, "Turn id");
|
|
589
589
|
if (lane.sessionId !== undefined)
|
|
590
590
|
requireIdentity(lane.sessionId, "Session id");
|
|
591
591
|
if (lane.reviewRoundId !== undefined)
|
|
@@ -594,14 +594,14 @@ export function validateExecutionLane(lane, group) {
|
|
|
594
594
|
validateLaneWorkspace(lane.workspace);
|
|
595
595
|
if (lane.directive !== undefined)
|
|
596
596
|
requireText(lane.directive, "ExecutionLane directive");
|
|
597
|
-
if (!["pending", "running", "
|
|
597
|
+
if (!["pending", "running", "completed", "failed", "skipped"].includes(lane.status)) {
|
|
598
598
|
throw new Error(`ExecutionLane status is invalid: ${String(lane.status)}.`);
|
|
599
599
|
}
|
|
600
600
|
if (lane.status === "skipped") {
|
|
601
601
|
if (group !== undefined && group.purpose !== "execution") {
|
|
602
602
|
throw new Error(`Only WorkItem ExecutionLanes can be skipped: ${lane.id}.`);
|
|
603
603
|
}
|
|
604
|
-
if (lane.
|
|
604
|
+
if (lane.turnId !== undefined || lane.sessionId !== undefined) {
|
|
605
605
|
throw new Error(`Skipped ExecutionLane must never have started: ${lane.id}.`);
|
|
606
606
|
}
|
|
607
607
|
}
|
|
@@ -627,7 +627,7 @@ function createLane(id, groupId, input, ordinal, timestamp) {
|
|
|
627
627
|
ordinal: positiveInteger(input.ordinal ?? ordinal, "ExecutionLane ordinal"),
|
|
628
628
|
roleName: requireIdentity(input.roleName, "ExecutionLane Role"),
|
|
629
629
|
...(input.effective === undefined ? {} : { effective: validateEffectiveLaunchSnapshot(input.effective) }),
|
|
630
|
-
...(input.
|
|
630
|
+
...(input.turnId === undefined ? {} : { turnId: requireIdentity(input.turnId, "Turn id") }),
|
|
631
631
|
...(input.sessionId === undefined ? {} : { sessionId: requireIdentity(input.sessionId, "Session id") }),
|
|
632
632
|
...(input.reviewRoundId === undefined ? {} : { reviewRoundId: requireIdentity(input.reviewRoundId, "ReviewRound id") }),
|
|
633
633
|
...(input.workspace === undefined ? {} : { workspace: validateLaneWorkspace(input.workspace) }),
|
|
@@ -745,7 +745,7 @@ function validateResolution(resolution, group) {
|
|
|
745
745
|
&& (resolution.selectedLaneIds.length === 0
|
|
746
746
|
|| resolution.selectedLaneIds.some((id) => {
|
|
747
747
|
const lane = group.lanes.find((candidate) => candidate.id === id);
|
|
748
|
-
return lane?.status !== "
|
|
748
|
+
return lane?.status !== "completed";
|
|
749
749
|
}))) {
|
|
750
750
|
throw new Error("Execution accept resolution must select usable Lane output.");
|
|
751
751
|
}
|
|
@@ -780,7 +780,7 @@ function assertExecutionLaneTransition(existing, candidate, groupId, groupPurpos
|
|
|
780
780
|
if (groupPurpose === "review"
|
|
781
781
|
&& candidate.status === "pending"
|
|
782
782
|
&& candidate.effective === undefined
|
|
783
|
-
&& candidate.
|
|
783
|
+
&& candidate.turnId === undefined
|
|
784
784
|
&& candidate.sessionId === undefined
|
|
785
785
|
&& candidate.result === undefined
|
|
786
786
|
&& candidate.endedAt === undefined
|
|
@@ -791,7 +791,7 @@ function assertExecutionLaneTransition(existing, candidate, groupId, groupPurpos
|
|
|
791
791
|
&& existing.status === "failed"
|
|
792
792
|
&& candidate.status === "pending"
|
|
793
793
|
&& candidate.effective !== undefined
|
|
794
|
-
&& candidate.
|
|
794
|
+
&& candidate.turnId === undefined
|
|
795
795
|
&& candidate.sessionId === undefined
|
|
796
796
|
&& candidate.result === undefined
|
|
797
797
|
&& candidate.endedAt === undefined
|
|
@@ -799,16 +799,16 @@ function assertExecutionLaneTransition(existing, candidate, groupId, groupPurpos
|
|
|
799
799
|
return;
|
|
800
800
|
}
|
|
801
801
|
if (candidate.status !== "running"
|
|
802
|
-
|| candidate.
|
|
803
|
-
|| candidate.
|
|
804
|
-
throw new Error(`Terminal ExecutionLane is immutable without a fresh retry
|
|
802
|
+
|| candidate.turnId === undefined
|
|
803
|
+
|| candidate.turnId === existing.turnId) {
|
|
804
|
+
throw new Error(`Terminal ExecutionLane is immutable without a fresh retry Turn: ${existing.id}.`);
|
|
805
805
|
}
|
|
806
806
|
return;
|
|
807
807
|
}
|
|
808
808
|
if (existing.status === "running" && candidate.status === "pending") {
|
|
809
809
|
throw new Error(`Running ExecutionLane cannot return to pending: ${existing.id}.`);
|
|
810
810
|
}
|
|
811
|
-
for (const key of ["effective", "
|
|
811
|
+
for (const key of ["effective", "turnId", "sessionId", "workspace", "directive"]) {
|
|
812
812
|
if (existing[key] !== undefined
|
|
813
813
|
&& !isDeepStrictEqual(existing[key], candidate[key])) {
|
|
814
814
|
throw new Error(`ExecutionLane ${key} changed without retry: ${existing.id}.`);
|
|
@@ -837,8 +837,7 @@ function validatePurpose(purpose) {
|
|
|
837
837
|
return purpose;
|
|
838
838
|
}
|
|
839
839
|
function isTerminalLane(status) {
|
|
840
|
-
return status === "
|
|
841
|
-
|| status === "completed"
|
|
840
|
+
return status === "completed"
|
|
842
841
|
|| status === "failed"
|
|
843
842
|
|| status === "skipped";
|
|
844
843
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isRoleTurnStalled, latestStallProgressAt } from "../scheduler/roleTurnStall.js";
|
|
2
2
|
import { validateRuntimeProcessExitObservation } from "../runtime/processExitObservation.js";
|
|
3
3
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
4
4
|
import { projectRuntimeTaskEvents } from "../runtime/runtimeProjection.js";
|
|
@@ -7,9 +7,9 @@ import { runOwnsBlockingProviderContinuation } from "../runtime/runtimeContinuat
|
|
|
7
7
|
import { summarizeExecutionGroup } from "./executionGroup.js";
|
|
8
8
|
import { executionStageSpendClosed, observedExecutionResourceUsage, projectExecutionStageResources } from "./resourceBroker.js";
|
|
9
9
|
/**
|
|
10
|
-
* Fold existing
|
|
10
|
+
* Fold existing Turn, runtime, Session, process-exit, and stall facts into the
|
|
11
11
|
* four Lane health states. This is a read model only: it never advances a
|
|
12
|
-
* Lane, terminalizes a
|
|
12
|
+
* Lane, terminalizes a Turn, or guesses that silence means death.
|
|
13
13
|
*/
|
|
14
14
|
export function projectExecutionGroupHealth(input) {
|
|
15
15
|
const policy = input.policy ?? DEFAULT_RUNTIME_HEALTH_POLICY;
|
|
@@ -23,7 +23,7 @@ export function projectExecutionGroupHealth(input) {
|
|
|
23
23
|
confirmedDeadLaneCount: countHealth(lanes, "confirmed-dead"),
|
|
24
24
|
reusableLaneIds: lanes.filter(({ resultReusable }) => resultReusable)
|
|
25
25
|
.map(({ laneId }) => laneId),
|
|
26
|
-
retryableLaneIds: lanes.filter(({ recovery }) => recovery === "retry-new-
|
|
26
|
+
retryableLaneIds: lanes.filter(({ recovery }) => recovery === "retry-new-turn")
|
|
27
27
|
.map(({ laneId }) => laneId)
|
|
28
28
|
});
|
|
29
29
|
}
|
|
@@ -39,7 +39,7 @@ export function summarizeExecutionGroupHealth(input) {
|
|
|
39
39
|
usage: observedExecutionResourceUsage({
|
|
40
40
|
group: input.group,
|
|
41
41
|
...(input.stageGroups === undefined ? {} : { stageGroups: input.stageGroups }),
|
|
42
|
-
|
|
42
|
+
turns: input.turns,
|
|
43
43
|
events: input.events
|
|
44
44
|
}),
|
|
45
45
|
now: input.now
|
|
@@ -67,45 +67,44 @@ export function actionableExecutionLaneRecoveries(groups) {
|
|
|
67
67
|
return groups
|
|
68
68
|
.filter(({ resolution }) => resolution === undefined)
|
|
69
69
|
.flatMap((group) => group.laneSummaries.flatMap((lane) => {
|
|
70
|
-
if (lane.recovery === "retry-new-
|
|
70
|
+
if (lane.recovery === "retry-new-turn"
|
|
71
71
|
&& group.resources !== undefined
|
|
72
72
|
&& executionStageSpendClosed(group.resources))
|
|
73
73
|
return [];
|
|
74
|
-
if (lane.recovery !== "retry-new-
|
|
74
|
+
if (lane.recovery !== "retry-new-turn")
|
|
75
75
|
return [];
|
|
76
76
|
return [{
|
|
77
77
|
groupId: group.groupId,
|
|
78
78
|
laneId: lane.laneId,
|
|
79
|
-
...(lane.
|
|
79
|
+
...(lane.turnId === undefined ? {} : { turnId: lane.turnId }),
|
|
80
80
|
...(lane.runtimeHealth === undefined ? {} : { runtimeHealth: lane.runtimeHealth }),
|
|
81
81
|
recovery: lane.recovery
|
|
82
82
|
}];
|
|
83
83
|
}));
|
|
84
84
|
}
|
|
85
85
|
function projectExecutionLaneHealth(lane, input, policy) {
|
|
86
|
-
const run = lane.
|
|
86
|
+
const run = lane.turnId === undefined
|
|
87
87
|
? undefined
|
|
88
|
-
: input.
|
|
88
|
+
: input.turns.find((candidate) => exactLaneTurn(candidate, input.group, lane));
|
|
89
89
|
const continuationAgentId = run?.effective.agentId ?? lane.effective?.agentId;
|
|
90
90
|
if (lane.status !== "completed"
|
|
91
|
-
&& lane.
|
|
92
|
-
&& lane.runId !== undefined
|
|
91
|
+
&& lane.turnId !== undefined
|
|
93
92
|
&& continuationAgentId !== undefined
|
|
94
93
|
&& runOwnsBlockingProviderContinuation(input.events, {
|
|
95
94
|
taskId: input.group.taskId,
|
|
96
95
|
roleName: lane.roleName,
|
|
97
|
-
|
|
96
|
+
turnId: lane.turnId,
|
|
98
97
|
agentId: continuationAgentId
|
|
99
98
|
})) {
|
|
100
99
|
return projection(lane, {
|
|
101
100
|
runtimeHealth: "active",
|
|
102
101
|
recovery: "none",
|
|
103
102
|
resultReusable: false,
|
|
104
|
-
reason: "the exact
|
|
103
|
+
reason: "the exact Turn still owns an unsettled Provider continuation writer",
|
|
105
104
|
evidence: ["runtime-continuation-writer-owned"]
|
|
106
105
|
});
|
|
107
106
|
}
|
|
108
|
-
if (lane.status === "completed"
|
|
107
|
+
if (lane.status === "completed") {
|
|
109
108
|
return projection(lane, {
|
|
110
109
|
recovery: "reuse-result",
|
|
111
110
|
resultReusable: true,
|
|
@@ -116,9 +115,9 @@ function projectExecutionLaneHealth(lane, input, policy) {
|
|
|
116
115
|
if (lane.status === "failed") {
|
|
117
116
|
return projection(lane, {
|
|
118
117
|
runtimeHealth: "confirmed-dead",
|
|
119
|
-
recovery: "retry-new-
|
|
118
|
+
recovery: "retry-new-turn",
|
|
120
119
|
resultReusable: false,
|
|
121
|
-
reason: "the exact Lane attempt is durably failed; a retry must create a new
|
|
120
|
+
reason: "the exact Lane attempt is durably failed; a retry must create a new Turn",
|
|
122
121
|
evidence: ["execution-lane-terminal-failure"]
|
|
123
122
|
});
|
|
124
123
|
}
|
|
@@ -143,17 +142,17 @@ function projectExecutionLaneHealth(lane, input, policy) {
|
|
|
143
142
|
runtimeHealth: "suspected-stalled",
|
|
144
143
|
recovery: "inspect",
|
|
145
144
|
resultReusable: false,
|
|
146
|
-
reason: "the running Lane has no exact
|
|
145
|
+
reason: "the running Lane has no exact Turn record",
|
|
147
146
|
evidence: ["execution-lineage-missing"]
|
|
148
147
|
});
|
|
149
148
|
}
|
|
150
149
|
if (run.status === "failed") {
|
|
151
150
|
return projection(lane, {
|
|
152
151
|
runtimeHealth: "confirmed-dead",
|
|
153
|
-
recovery: "retry-new-
|
|
152
|
+
recovery: "retry-new-turn",
|
|
154
153
|
resultReusable: false,
|
|
155
|
-
reason: "the exact
|
|
156
|
-
evidence: ["
|
|
154
|
+
reason: "the exact Turn is durably failed",
|
|
155
|
+
evidence: ["turn-terminal-failure"]
|
|
157
156
|
});
|
|
158
157
|
}
|
|
159
158
|
if (run.status !== "active") {
|
|
@@ -161,14 +160,14 @@ function projectExecutionLaneHealth(lane, input, policy) {
|
|
|
161
160
|
runtimeHealth: "suspected-stalled",
|
|
162
161
|
recovery: "inspect",
|
|
163
162
|
resultReusable: false,
|
|
164
|
-
reason: "the Lane is running but its exact
|
|
163
|
+
reason: "the Lane is running but its exact Turn is terminal without a Lane result",
|
|
165
164
|
evidence: ["execution-lineage-inconsistent"]
|
|
166
165
|
});
|
|
167
166
|
}
|
|
168
167
|
const session = input.sessions.find((candidate) => (candidate.roleName === run.roleName
|
|
169
168
|
&& candidate.agentId === run.effective.agentId
|
|
170
169
|
&& candidate.adapterId === run.effective.adapterId));
|
|
171
|
-
const observations =
|
|
170
|
+
const observations = exactTurnObservations(input.events, run, session);
|
|
172
171
|
const runtime = runtimeProjection(observations, input.events, run);
|
|
173
172
|
const unsettledContinuation = runtime !== null
|
|
174
173
|
&& Object.values(runtime.continuations).some((continuation) => (continuation.execution === "active"
|
|
@@ -178,13 +177,13 @@ function projectExecutionLaneHealth(lane, input, policy) {
|
|
|
178
177
|
&& (Object.values(runtime.operations).some(({ kind }) => kind === "subagent")
|
|
179
178
|
|| unsettledContinuation);
|
|
180
179
|
if (observations.some((observation) => (observation.kind === "turn.failed"
|
|
181
|
-
&& observation.payload.failure?.
|
|
180
|
+
&& observation.payload.failure?.turnTerminal === true)) && !unsettledChildWork) {
|
|
182
181
|
return projection(lane, {
|
|
183
182
|
runtimeHealth: "confirmed-dead",
|
|
184
183
|
recovery: "inspect",
|
|
185
184
|
resultReusable: false,
|
|
186
|
-
reason: "the Provider reported an exact
|
|
187
|
-
evidence: ["provider-
|
|
185
|
+
reason: "the Provider reported an exact Turn-terminal failure",
|
|
186
|
+
evidence: ["provider-turn-terminal"]
|
|
188
187
|
});
|
|
189
188
|
}
|
|
190
189
|
const runtimeTerminalEvidence = runtime === null
|
|
@@ -218,20 +217,19 @@ function projectExecutionLaneHealth(lane, input, policy) {
|
|
|
218
217
|
evidence: ["native-session-terminal", `process-exit:${exit.classification}`]
|
|
219
218
|
});
|
|
220
219
|
}
|
|
221
|
-
if (
|
|
220
|
+
if (isRoleTurnStalled(input.events, run.id)) {
|
|
222
221
|
return projection(lane, {
|
|
223
222
|
runtimeHealth: "suspected-stalled",
|
|
224
223
|
recovery: "inspect",
|
|
225
224
|
resultReusable: false,
|
|
226
225
|
reason: `the durable progress clock has not advanced since ${latestStallProgressAt(input.events, run.id) ?? run.updatedAt}; no death proof exists`,
|
|
227
|
-
evidence: ["
|
|
226
|
+
evidence: ["turn-stalled"]
|
|
228
227
|
});
|
|
229
228
|
}
|
|
230
229
|
const activeOperation = runtime !== null
|
|
231
230
|
&& (Object.keys(runtime.operations).length > 0 || unsettledContinuation);
|
|
232
231
|
const lastActivityAt = runtime?.lastRuntimeActivityAt
|
|
233
|
-
?? run.
|
|
234
|
-
?? run.pushedAt
|
|
232
|
+
?? run.updatedAt
|
|
235
233
|
?? run.createdAt;
|
|
236
234
|
const recentActivity = input.now.getTime() - Date.parse(lastActivityAt) < policy.quietAfterMs;
|
|
237
235
|
if (activeOperation || recentActivity) {
|
|
@@ -243,7 +241,7 @@ function projectExecutionLaneHealth(lane, input, policy) {
|
|
|
243
241
|
? "the exact runtime reports unsettled continuation work"
|
|
244
242
|
: activeOperation
|
|
245
243
|
? "the exact runtime reports an active operation"
|
|
246
|
-
: "the exact
|
|
244
|
+
: "the exact Turn has recent structured runtime activity",
|
|
247
245
|
evidence: unsettledContinuation
|
|
248
246
|
? ["runtime-continuation-unsettled"]
|
|
249
247
|
: activeOperation ? ["runtime-operation-active"] : ["runtime-activity-recent"]
|
|
@@ -253,25 +251,25 @@ function projectExecutionLaneHealth(lane, input, policy) {
|
|
|
253
251
|
runtimeHealth: "silent",
|
|
254
252
|
recovery: "none",
|
|
255
253
|
resultReusable: false,
|
|
256
|
-
reason: "the exact
|
|
257
|
-
evidence: ["
|
|
254
|
+
reason: "the exact Turn remains active without recent structured activity; silence alone is not death",
|
|
255
|
+
evidence: ["turn-active"]
|
|
258
256
|
});
|
|
259
257
|
}
|
|
260
258
|
function projection(lane, value) {
|
|
261
259
|
return Object.freeze({ laneId: lane.id, ...value });
|
|
262
260
|
}
|
|
263
|
-
function
|
|
264
|
-
return run.id === lane.
|
|
261
|
+
function exactLaneTurn(run, group, lane) {
|
|
262
|
+
return run.id === lane.turnId
|
|
265
263
|
&& run.taskId === group.taskId
|
|
266
264
|
&& run.roleName === lane.roleName
|
|
267
265
|
&& run.executionGroupId === group.id
|
|
268
266
|
&& run.executionLaneId === lane.id;
|
|
269
267
|
}
|
|
270
|
-
function
|
|
268
|
+
function exactTurnObservations(events, run, session) {
|
|
271
269
|
return events.map(runtimeObservationFromTaskEvent)
|
|
272
270
|
.filter((observation) => (observation !== null
|
|
273
271
|
&& observation.fence.taskId === run.taskId
|
|
274
|
-
&& observation.fence.
|
|
272
|
+
&& observation.fence.turnId === run.id
|
|
275
273
|
&& observation.fence.roleName === run.roleName
|
|
276
274
|
&& observation.fence.agentId === run.effective.agentId
|
|
277
275
|
&& (session?.launchId === undefined || observation.fence.launchId === session.launchId)
|
|
@@ -291,7 +289,7 @@ function latestExactProcessExit(events, run, session) {
|
|
|
291
289
|
try {
|
|
292
290
|
const observation = validateRuntimeProcessExitObservation(JSON.parse(event.payload.observation ?? ""));
|
|
293
291
|
if (observation.taskId !== run.taskId
|
|
294
|
-
|| observation.
|
|
292
|
+
|| observation.turnId !== run.id
|
|
295
293
|
|| observation.roleName !== run.roleName
|
|
296
294
|
|| (session?.launchId !== undefined && observation.launchId !== session.launchId)
|
|
297
295
|
|| (session?.nativeSessionId !== undefined
|
|
@@ -300,23 +300,23 @@ export function observedExecutionResourceUsage(input) {
|
|
|
300
300
|
const group = validateExecutionGroup(input.group);
|
|
301
301
|
const stageGroups = executionStageRetryLineage(group, input.stageGroups);
|
|
302
302
|
const groupIds = new Set(stageGroups.map(({ id }) => id));
|
|
303
|
-
const
|
|
303
|
+
const turnIds = new Set([
|
|
304
304
|
...stageGroups.flatMap((candidate) => candidate.lanes
|
|
305
|
-
.flatMap(({
|
|
306
|
-
...(input.
|
|
307
|
-
&&
|
|
308
|
-
&&
|
|
309
|
-
&& groupIds.has(
|
|
305
|
+
.flatMap(({ turnId }) => turnId === undefined ? [] : [turnId])),
|
|
306
|
+
...(input.turns ?? []).filter((turn) => (turn.taskId === group.taskId
|
|
307
|
+
&& turn.purpose === "execution"
|
|
308
|
+
&& turn.executionGroupId !== undefined
|
|
309
|
+
&& groupIds.has(turn.executionGroupId))).map(({ id }) => id)
|
|
310
310
|
]);
|
|
311
311
|
const observations = input.events.map(runtimeObservationFromTaskEvent)
|
|
312
312
|
.filter((value) => (value !== null
|
|
313
313
|
&& value.fence.taskId === group.taskId
|
|
314
|
-
&& value.fence.
|
|
315
|
-
&&
|
|
314
|
+
&& value.fence.turnId !== undefined
|
|
315
|
+
&& turnIds.has(value.fence.turnId)));
|
|
316
316
|
let tokens = 0;
|
|
317
317
|
let tokensObservable = true;
|
|
318
|
-
for (const
|
|
319
|
-
const usage = observations.filter(({ fence, payload }) => (fence.
|
|
318
|
+
for (const turnId of turnIds) {
|
|
319
|
+
const usage = observations.filter(({ fence, payload }) => (fence.turnId === turnId && payload.usage !== undefined)).sort((left, right) => (left.receivedAt.localeCompare(right.receivedAt)
|
|
320
320
|
|| (left.sequence ?? 0) - (right.sequence ?? 0)
|
|
321
321
|
|| (left.ordinal ?? 0) - (right.ordinal ?? 0)));
|
|
322
322
|
if (usage.length === 0) {
|
|
@@ -345,7 +345,7 @@ export function observedExecutionResourceUsage(input) {
|
|
|
345
345
|
tokensObservable = false;
|
|
346
346
|
}
|
|
347
347
|
const toolOperationIds = new Set(observations.flatMap((observation) => (observation.kind === "operation.started" && observation.payload.operation === "tool"
|
|
348
|
-
? [`${observation.fence.
|
|
348
|
+
? [`${observation.fence.turnId}\0${observation.payload.operationId ?? observation.semanticKey}`]
|
|
349
349
|
: [])));
|
|
350
350
|
const toolCallsObservable = observations.some(({ kind, payload }) => (kind === "operation.started" && payload.operation === "tool"));
|
|
351
351
|
return Object.freeze({
|
|
@@ -422,5 +422,5 @@ function percentage(value, label) {
|
|
|
422
422
|
return value;
|
|
423
423
|
}
|
|
424
424
|
function isUsable(status) {
|
|
425
|
-
return status === "
|
|
425
|
+
return status === "completed";
|
|
426
426
|
}
|