@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
|
@@ -2,21 +2,21 @@ import { selectedSchedulerRoles, selectedActiveSchedulerTasks } from "./ports.js
|
|
|
2
2
|
import { nextPendingBatch } from "../coordination/workMailbox.js";
|
|
3
3
|
import { RUNTIME_DIAGNOSTIC_AFTER_MS, SEMANTIC_STALL_WINDOW_MS } from "../runtime/runtimeHealthPolicy.js";
|
|
4
4
|
/**
|
|
5
|
-
* Default window of no durable progress before a live-but-idle
|
|
6
|
-
* traceable needs-attention signal. It is deliberately long: a healthy
|
|
5
|
+
* Default window of no durable progress before a live-but-idle Turn becomes a
|
|
6
|
+
* traceable needs-attention signal. It is deliberately long: a healthy Turn that
|
|
7
7
|
* is simply slow keeps its structured checkpoint fresh and never crosses it.
|
|
8
8
|
*/
|
|
9
9
|
export const DEFAULT_STALL_WINDOW_MS = SEMANTIC_STALL_WINDOW_MS;
|
|
10
10
|
/** Cheap workflow-stall candidate filter; the real threshold remains 30m. */
|
|
11
11
|
export const DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS = RUNTIME_DIAGNOSTIC_AFTER_MS;
|
|
12
|
-
export const
|
|
13
|
-
export const
|
|
14
|
-
export const
|
|
15
|
-
export const
|
|
12
|
+
export const TURN_PROGRESS_EVENT = "turn.progress";
|
|
13
|
+
export const TURN_STALLED_EVENT = "turn.stalled";
|
|
14
|
+
export const TURN_RECOVERED_EVENT = "turn.recovered";
|
|
15
|
+
export const TURN_DIAGNOSTIC_FINISHED_EVENT = "runtime.diagnostic-finished";
|
|
16
16
|
/** Structured, non-Message recovery evidence written by an explicit Leader. */
|
|
17
17
|
/** Workflow-semantic events that count for the durable progress clock. */
|
|
18
18
|
const ACTIVITY_EVENT_TYPES = new Set([
|
|
19
|
-
|
|
19
|
+
TURN_PROGRESS_EVENT,
|
|
20
20
|
"message.sent",
|
|
21
21
|
"input.answered",
|
|
22
22
|
"input.auto-answered",
|
|
@@ -34,24 +34,23 @@ const ACTIVITY_EVENT_TYPES = new Set([
|
|
|
34
34
|
* as exact-generation diagnostic evidence; it never changes the durable
|
|
35
35
|
* progress clock, suppresses workflow attention, or authorizes recovery.
|
|
36
36
|
*/
|
|
37
|
-
export function
|
|
37
|
+
export function projectRoleTurnHealth(input) {
|
|
38
38
|
const windowMs = input.windowMs ?? DEFAULT_STALL_WINDOW_MS;
|
|
39
39
|
const diagnosticAfterMs = input.diagnosticAfterMs
|
|
40
40
|
?? DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS;
|
|
41
41
|
if (!Number.isFinite(windowMs) || windowMs <= 0) {
|
|
42
|
-
throw new Error("Role
|
|
42
|
+
throw new Error("Role Turn stall window must be a positive number of milliseconds.");
|
|
43
43
|
}
|
|
44
|
-
const evaluation =
|
|
44
|
+
const evaluation = evaluateRoleTurnStall({
|
|
45
45
|
progressAt: input.progressAt,
|
|
46
46
|
now: input.now,
|
|
47
47
|
windowMs
|
|
48
48
|
});
|
|
49
49
|
const idleMs = evaluation.idleMs;
|
|
50
|
-
const
|
|
51
|
-
const candidateAge = input.now.getTime() - Date.parse(candidateStart);
|
|
50
|
+
const candidateAge = input.now.getTime() - Date.parse(input.createdAt);
|
|
52
51
|
const candidate = Number.isFinite(candidateAge) && candidateAge >= windowMs;
|
|
53
52
|
const providerAcceptance = input.providerAcceptance
|
|
54
|
-
??
|
|
53
|
+
?? "ambiguous";
|
|
55
54
|
const hostLiveness = input.hostLiveness;
|
|
56
55
|
const session = input.nativeSession;
|
|
57
56
|
const nativeSession = session === null || session === undefined
|
|
@@ -74,7 +73,7 @@ export function projectRoleRunHealth(input) {
|
|
|
74
73
|
// no persisted nativeSessionId. With a present host, durable no-progress
|
|
75
74
|
// evidence is still actionable in that case. Only an explicit stopped or
|
|
76
75
|
// broken Session blocks the projection; identity mismatches are fenced by
|
|
77
|
-
//
|
|
76
|
+
// reconcileStalledRoleTurns below before this projection is routed.
|
|
78
77
|
const workflowStall = candidate
|
|
79
78
|
&& evaluation.stalled
|
|
80
79
|
&& hostLiveness === "present"
|
|
@@ -108,16 +107,16 @@ export function projectRoleRunHealth(input) {
|
|
|
108
107
|
/**
|
|
109
108
|
* Pure stall decision. Given the last durable progress timestamp and whatever
|
|
110
109
|
* progress point the previous attention (if any) recorded, decide whether the
|
|
111
|
-
*
|
|
110
|
+
* Turn is stalled and whether this is new evidence worth surfacing again.
|
|
112
111
|
*/
|
|
113
|
-
export function
|
|
112
|
+
export function evaluateRoleTurnStall(input) {
|
|
114
113
|
const windowMs = input.windowMs ?? DEFAULT_STALL_WINDOW_MS;
|
|
115
114
|
if (!Number.isFinite(windowMs) || windowMs <= 0) {
|
|
116
|
-
throw new Error("Role
|
|
115
|
+
throw new Error("Role Turn stall window must be a positive number of milliseconds.");
|
|
117
116
|
}
|
|
118
117
|
const progressMs = Date.parse(input.progressAt);
|
|
119
118
|
if (!Number.isFinite(progressMs)) {
|
|
120
|
-
throw new Error("Role
|
|
119
|
+
throw new Error("Role Turn stall progress timestamp is invalid.");
|
|
121
120
|
}
|
|
122
121
|
const idleMs = input.now.getTime() - progressMs;
|
|
123
122
|
const stalled = idleMs >= windowMs;
|
|
@@ -125,29 +124,29 @@ export function evaluateRoleRunStall(input) {
|
|
|
125
124
|
return { stalled, isNewEpisode, idleMs, progressAt: input.progressAt };
|
|
126
125
|
}
|
|
127
126
|
/**
|
|
128
|
-
* Latest durable progress timestamp for an active
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
127
|
+
* Latest durable progress timestamp for an active Turn. Turn creation,
|
|
128
|
+
* explicit workflow checkpoints, and semantic domain activity count as
|
|
129
|
+
* progress. Provider operations, tokens, CPU/memory and bookkeeping are
|
|
130
|
+
* intentionally excluded.
|
|
132
131
|
*/
|
|
133
132
|
export function latestDurableProgressAt(input) {
|
|
134
133
|
const candidates = [
|
|
135
|
-
input.
|
|
134
|
+
input.startedAt,
|
|
136
135
|
input.latestCheckpointAt,
|
|
137
136
|
input.latestActivityAt
|
|
138
137
|
].filter((value) => (typeof value === "string" && Number.isFinite(Date.parse(value))));
|
|
139
138
|
if (candidates.length === 0) {
|
|
140
|
-
throw new Error("No durable progress timestamp is available for the
|
|
139
|
+
throw new Error("No durable progress timestamp is available for the Turn.");
|
|
141
140
|
}
|
|
142
141
|
return candidates.reduce((latest, value) => (Date.parse(value) > Date.parse(latest) ? value : latest));
|
|
143
142
|
}
|
|
144
|
-
/** Most recent semantic timestamp carried by a
|
|
145
|
-
export function
|
|
143
|
+
/** Most recent semantic timestamp carried by a Turn progress event. */
|
|
144
|
+
export function latestTurnProgressAt(events, turnId) {
|
|
146
145
|
let latest;
|
|
147
146
|
for (const event of events) {
|
|
148
|
-
if (event.type !==
|
|
147
|
+
if (event.type !== TURN_PROGRESS_EVENT)
|
|
149
148
|
continue;
|
|
150
|
-
if (event.payload.
|
|
149
|
+
if (event.payload.turnId !== turnId)
|
|
151
150
|
continue;
|
|
152
151
|
const progressAt = typeof event.payload.progressAt === "string"
|
|
153
152
|
&& Number.isFinite(Date.parse(event.payload.progressAt))
|
|
@@ -164,89 +163,45 @@ export function latestRunProgressAt(events, runId) {
|
|
|
164
163
|
}
|
|
165
164
|
/**
|
|
166
165
|
* Resolve the exact semantic progress fence shared by resource production and
|
|
167
|
-
* stall consumption for one active
|
|
168
|
-
*
|
|
169
|
-
* the provider-neutral fallback. Resource evidence carries this value as an
|
|
170
|
-
* exact fence, but never advances it.
|
|
166
|
+
* stall consumption for one active Turn. Resource evidence carries this value
|
|
167
|
+
* as an exact fence, but never advances it.
|
|
171
168
|
*/
|
|
172
|
-
export function
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
richerProgress = store.getRunDurableProgress?.(taskId, roleName, run.id);
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
// A related-record fold is advisory. The retained Run/Event fence remains
|
|
179
|
-
// usable when that optional read is unavailable.
|
|
180
|
-
richerProgress = undefined;
|
|
181
|
-
}
|
|
182
|
-
if (run.deliveredAt === undefined)
|
|
183
|
-
return { progressAt: run.createdAt };
|
|
184
|
-
// The richer fold is authoritative when present; skip the per-candidate
|
|
185
|
-
// full-history scans that every stall candidate otherwise pays.
|
|
186
|
-
if (richerProgress !== undefined && richerProgress !== null) {
|
|
169
|
+
export function currentRoleTurnProgressAt(store, taskId, roleName, run) {
|
|
170
|
+
const progress = store.getTurnDurableProgress(taskId, roleName, run.id);
|
|
171
|
+
if (progress !== null) {
|
|
187
172
|
return {
|
|
188
|
-
progressAt:
|
|
189
|
-
...(
|
|
173
|
+
progressAt: progress.progressAt,
|
|
174
|
+
...(progress.evidence === undefined
|
|
190
175
|
? {}
|
|
191
|
-
: { evidence:
|
|
176
|
+
: { evidence: progress.evidence })
|
|
192
177
|
};
|
|
193
178
|
}
|
|
194
|
-
|
|
195
|
-
// not evaluate a default history argument or fall back to listEvents in
|
|
196
|
-
// that case: an absent entry is the folded empty fact.
|
|
197
|
-
const folded = store.getRunProgressFacts !== undefined;
|
|
198
|
-
const foldedFacts = folded
|
|
199
|
-
? progressFacts ?? store.getRunProgressFacts?.(taskId, run.id)
|
|
200
|
-
: undefined;
|
|
201
|
-
if (folded) {
|
|
202
|
-
const fallbackProgressAt = latestDurableProgressAt({
|
|
203
|
-
deliveredAt: run.deliveredAt,
|
|
204
|
-
latestCheckpointAt: foldedFacts?.latestCheckpointAt,
|
|
205
|
-
latestActivityAt: foldedFacts?.latestActivityAt
|
|
206
|
-
});
|
|
207
|
-
return { progressAt: fallbackProgressAt };
|
|
208
|
-
}
|
|
209
|
-
// Legacy ports retain the per-Run event-history fallback. Resolve the
|
|
210
|
-
// history only in this branch so fold-backed callers perform zero scans.
|
|
211
|
-
const history = events ?? store.listEvents?.(taskId) ?? [];
|
|
212
|
-
const fallbackProgressAt = latestDurableProgressAt({
|
|
213
|
-
deliveredAt: run.deliveredAt,
|
|
214
|
-
latestCheckpointAt: latestRunProgressAt(history, run.id),
|
|
215
|
-
latestActivityAt: latestRunActivityAt(history, run.id)
|
|
216
|
-
});
|
|
217
|
-
return {
|
|
218
|
-
progressAt: fallbackProgressAt
|
|
219
|
-
};
|
|
179
|
+
return { progressAt: run.createdAt };
|
|
220
180
|
}
|
|
221
181
|
/**
|
|
222
|
-
* Computes the current semantic progress fence for an exact
|
|
223
|
-
* related-record readers
|
|
224
|
-
*
|
|
182
|
+
* Computes the current semantic progress fence for an exact Turn. The optional
|
|
183
|
+
* related-record readers add Work/Review/Integration evidence to the Turn and
|
|
184
|
+
* event facts used by on-demand projections.
|
|
225
185
|
*/
|
|
226
|
-
export function
|
|
227
|
-
const run = store.
|
|
186
|
+
export function latestTurnDurableProgressAt(store, taskId, roleName, turnId, precomputed) {
|
|
187
|
+
const run = store.getTurn(taskId, turnId);
|
|
228
188
|
if (run === null || run.taskId !== taskId || run.roleName !== roleName)
|
|
229
189
|
return null;
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
// its (possibly undefined) values directly and never re-scan the whole
|
|
233
|
-
// history per candidate. Legacy callers without the port omit precomputed
|
|
234
|
-
// and keep the per-candidate full-history scans.
|
|
190
|
+
// Scheduler reconciliation supplies its once-per-revision fold; on-demand
|
|
191
|
+
// views omit it and compute the same facts from the Task event history.
|
|
235
192
|
const folded = precomputed !== undefined;
|
|
236
193
|
const events = folded ? undefined : store.listEvents(taskId);
|
|
237
194
|
const latestCheckpointAt = folded
|
|
238
195
|
? precomputed.latestCheckpointAt
|
|
239
|
-
:
|
|
196
|
+
: latestTurnProgressAt(events, run.id);
|
|
240
197
|
const latestActivityAt = folded
|
|
241
198
|
? precomputed.latestActivityAt
|
|
242
|
-
:
|
|
243
|
-
const baseline =
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
latestActivityAt
|
|
249
|
-
});
|
|
199
|
+
: latestTurnActivityAt(events, run.id);
|
|
200
|
+
const baseline = latestDurableProgressAt({
|
|
201
|
+
startedAt: run.createdAt,
|
|
202
|
+
latestCheckpointAt,
|
|
203
|
+
latestActivityAt
|
|
204
|
+
});
|
|
250
205
|
if (run.workItemId === undefined) {
|
|
251
206
|
return { progressAt: baseline };
|
|
252
207
|
}
|
|
@@ -262,8 +217,8 @@ export function latestRunDurableProgressAt(store, taskId, roleName, runId, preco
|
|
|
262
217
|
.filter(({ changeSetIds: ids }) => ids.some((id) => changeSetIds.has(id)))
|
|
263
218
|
?? [];
|
|
264
219
|
const inputProgress = store.listInputRequests?.(taskId)
|
|
265
|
-
.filter((request) => (request.requester.
|
|
266
|
-
|| request.blockedRefs.some((ref) => ref.type === "
|
|
220
|
+
.filter((request) => (request.requester.turnId === run.id
|
|
221
|
+
|| request.blockedRefs.some((ref) => ref.type === "turn" && ref.id === run.id)))
|
|
267
222
|
?? [];
|
|
268
223
|
const related = [
|
|
269
224
|
workItem?.updatedAt,
|
|
@@ -276,10 +231,7 @@ export function latestRunDurableProgressAt(store, taskId, roleName, runId, preco
|
|
|
276
231
|
const latestRelatedAt = related.reduce((latest, value) => latest === undefined || Date.parse(value) > Date.parse(latest)
|
|
277
232
|
? value
|
|
278
233
|
: latest, undefined);
|
|
279
|
-
|
|
280
|
-
// while related records are retained as diagnostic evidence only.
|
|
281
|
-
return run.deliveredAt !== undefined
|
|
282
|
-
&& latestRelatedAt !== undefined
|
|
234
|
+
return latestRelatedAt !== undefined
|
|
283
235
|
&& Date.parse(latestRelatedAt) > Date.parse(baseline)
|
|
284
236
|
? { progressAt: latestRelatedAt, evidence: "work-review-integration" }
|
|
285
237
|
: {
|
|
@@ -287,11 +239,11 @@ export function latestRunDurableProgressAt(store, taskId, roleName, runId, preco
|
|
|
287
239
|
...(latestRelatedAt === undefined ? {} : { evidence: "work-review-integration" })
|
|
288
240
|
};
|
|
289
241
|
}
|
|
290
|
-
/** Most recent createdAt of a
|
|
291
|
-
export function
|
|
242
|
+
/** Most recent createdAt of a Turn-scoped event of one type, if any. */
|
|
243
|
+
export function latestTurnEventTime(events, type, turnId) {
|
|
292
244
|
let latest;
|
|
293
245
|
for (const event of events) {
|
|
294
|
-
if (event.type !== type || event.payload.
|
|
246
|
+
if (event.type !== type || event.payload.turnId !== turnId)
|
|
295
247
|
continue;
|
|
296
248
|
if (latest === undefined || Date.parse(event.createdAt) > Date.parse(latest)) {
|
|
297
249
|
latest = event.createdAt;
|
|
@@ -299,16 +251,16 @@ export function latestRunEventTime(events, type, runId) {
|
|
|
299
251
|
}
|
|
300
252
|
return latest;
|
|
301
253
|
}
|
|
302
|
-
/** Most recent non-control event carrying a
|
|
303
|
-
export function
|
|
254
|
+
/** Most recent non-control event carrying a Turn identity. */
|
|
255
|
+
export function latestTurnActivityAt(events, turnId) {
|
|
304
256
|
let latest;
|
|
305
257
|
for (const event of events) {
|
|
306
258
|
if (!ACTIVITY_EVENT_TYPES.has(event.type)
|
|
307
|
-
|| event.payload.
|
|
308
|
-
|| event.type ===
|
|
309
|
-
|| event.type ===
|
|
259
|
+
|| event.payload.turnId !== turnId
|
|
260
|
+
|| event.type === TURN_STALLED_EVENT
|
|
261
|
+
|| event.type === TURN_RECOVERED_EVENT)
|
|
310
262
|
continue;
|
|
311
|
-
const activityAt = event.type ===
|
|
263
|
+
const activityAt = event.type === TURN_PROGRESS_EVENT
|
|
312
264
|
&& typeof event.payload.progressAt === "string"
|
|
313
265
|
&& Number.isFinite(Date.parse(event.payload.progressAt))
|
|
314
266
|
? event.payload.progressAt
|
|
@@ -319,11 +271,11 @@ export function latestRunActivityAt(events, runId) {
|
|
|
319
271
|
}
|
|
320
272
|
return latest;
|
|
321
273
|
}
|
|
322
|
-
/** The progress point recorded by the most recent stall attention for a
|
|
323
|
-
export function latestStallProgressAt(events,
|
|
274
|
+
/** The progress point recorded by the most recent stall attention for a Turn. */
|
|
275
|
+
export function latestStallProgressAt(events, turnId) {
|
|
324
276
|
let latest;
|
|
325
277
|
for (const event of events) {
|
|
326
|
-
if (event.type !==
|
|
278
|
+
if (event.type !== TURN_STALLED_EVENT || event.payload.turnId !== turnId)
|
|
327
279
|
continue;
|
|
328
280
|
if (latest === undefined || Date.parse(event.createdAt) > Date.parse(latest.createdAt)) {
|
|
329
281
|
latest = event;
|
|
@@ -332,10 +284,10 @@ export function latestStallProgressAt(events, runId) {
|
|
|
332
284
|
return latest?.payload.progressAt;
|
|
333
285
|
}
|
|
334
286
|
/** Latest stall episode identity used for source-idempotent attention. */
|
|
335
|
-
export function latestStallEvidenceKey(events,
|
|
287
|
+
export function latestStallEvidenceKey(events, turnId) {
|
|
336
288
|
let latest;
|
|
337
289
|
for (const event of events) {
|
|
338
|
-
if (event.type !==
|
|
290
|
+
if (event.type !== TURN_STALLED_EVENT || event.payload.turnId !== turnId)
|
|
339
291
|
continue;
|
|
340
292
|
if (latest === undefined || Date.parse(event.createdAt) > Date.parse(latest.createdAt)) {
|
|
341
293
|
latest = event;
|
|
@@ -349,27 +301,27 @@ export function latestStallEvidenceKey(events, runId) {
|
|
|
349
301
|
};
|
|
350
302
|
}
|
|
351
303
|
/**
|
|
352
|
-
* Folds a Task's event history into per-
|
|
304
|
+
* Folds a Task's event history into per-Turn progress facts in one O(events)
|
|
353
305
|
* pass. The adapter builds this once per durable revision and serves the
|
|
354
306
|
* stall reconciliation from it, replacing the per-candidate full-history
|
|
355
307
|
* clones and scans that turned one pass into an O(candidates x events) hot
|
|
356
|
-
* read. The fold mirrors
|
|
308
|
+
* read. The fold mirrors latestTurnProgressAt/latestTurnActivityAt/
|
|
357
309
|
* latestStallProgressAt/latestStallEvidenceKey exactly.
|
|
358
310
|
*/
|
|
359
|
-
export function
|
|
360
|
-
const
|
|
311
|
+
export function foldTurnProgressFacts(events) {
|
|
312
|
+
const byTurn = new Map();
|
|
361
313
|
const latestStallCreatedAt = new Map();
|
|
362
314
|
for (const event of events) {
|
|
363
|
-
const
|
|
364
|
-
if (typeof
|
|
315
|
+
const turnId = event.payload.turnId;
|
|
316
|
+
if (typeof turnId !== "string")
|
|
365
317
|
continue;
|
|
366
|
-
let facts =
|
|
318
|
+
let facts = byTurn.get(turnId);
|
|
367
319
|
if (facts === undefined) {
|
|
368
320
|
facts = {};
|
|
369
|
-
|
|
321
|
+
byTurn.set(turnId, facts);
|
|
370
322
|
}
|
|
371
323
|
const type = event.type;
|
|
372
|
-
const isProgress = type ===
|
|
324
|
+
const isProgress = type === TURN_PROGRESS_EVENT;
|
|
373
325
|
if (isProgress) {
|
|
374
326
|
const validProgress = typeof event.payload.progressAt === "string"
|
|
375
327
|
&& Number.isFinite(Date.parse(event.payload.progressAt));
|
|
@@ -386,11 +338,11 @@ export function foldRunProgressFacts(events) {
|
|
|
386
338
|
facts.latestActivityAt = progressAt;
|
|
387
339
|
}
|
|
388
340
|
}
|
|
389
|
-
else if (type ===
|
|
390
|
-
const previousCreatedAt = latestStallCreatedAt.get(
|
|
341
|
+
else if (type === TURN_STALLED_EVENT) {
|
|
342
|
+
const previousCreatedAt = latestStallCreatedAt.get(turnId);
|
|
391
343
|
if (previousCreatedAt === undefined
|
|
392
344
|
|| Date.parse(event.createdAt) > Date.parse(previousCreatedAt)) {
|
|
393
|
-
latestStallCreatedAt.set(
|
|
345
|
+
latestStallCreatedAt.set(turnId, event.createdAt);
|
|
394
346
|
facts.latestStall = typeof event.payload.progressAt === "string"
|
|
395
347
|
? {
|
|
396
348
|
progressAt: event.payload.progressAt,
|
|
@@ -402,33 +354,33 @@ export function foldRunProgressFacts(events) {
|
|
|
402
354
|
}
|
|
403
355
|
}
|
|
404
356
|
else if (ACTIVITY_EVENT_TYPES.has(type)
|
|
405
|
-
&& type !==
|
|
406
|
-
&& type !==
|
|
357
|
+
&& type !== TURN_STALLED_EVENT
|
|
358
|
+
&& type !== TURN_RECOVERED_EVENT) {
|
|
407
359
|
if (facts.latestActivityAt === undefined
|
|
408
360
|
|| Date.parse(event.createdAt) > Date.parse(facts.latestActivityAt)) {
|
|
409
361
|
facts.latestActivityAt = event.createdAt;
|
|
410
362
|
}
|
|
411
363
|
}
|
|
412
364
|
}
|
|
413
|
-
return
|
|
365
|
+
return byTurn;
|
|
414
366
|
}
|
|
415
367
|
/** Yield the event loop so already-written control requests stay responsive. */
|
|
416
368
|
function yieldEventLoop() {
|
|
417
369
|
return new Promise((resolve) => setImmediate(resolve));
|
|
418
370
|
}
|
|
419
371
|
/**
|
|
420
|
-
* True while a
|
|
372
|
+
* True while a Turn remains in an unresolved stall episode: a stall was raised
|
|
421
373
|
* and no later checkpoint has advanced its durable progress since. This is the
|
|
422
374
|
* projection the runtime-health view reads to surface needs-attention.
|
|
423
375
|
*/
|
|
424
|
-
export function
|
|
376
|
+
export function isRoleTurnStalled(events, turnId) {
|
|
425
377
|
let stalled;
|
|
426
378
|
let recoveredAt;
|
|
427
379
|
let progressAt;
|
|
428
380
|
for (const event of events) {
|
|
429
|
-
if (event.payload.
|
|
381
|
+
if (event.payload.turnId !== turnId)
|
|
430
382
|
continue;
|
|
431
|
-
if (event.type ===
|
|
383
|
+
if (event.type === TURN_STALLED_EVENT) {
|
|
432
384
|
if (event.payload.status === "diagnostic-only")
|
|
433
385
|
continue;
|
|
434
386
|
if (stalled === undefined
|
|
@@ -437,14 +389,14 @@ export function isRoleRunStalled(events, runId) {
|
|
|
437
389
|
}
|
|
438
390
|
continue;
|
|
439
391
|
}
|
|
440
|
-
if (event.type ===
|
|
392
|
+
if (event.type === TURN_RECOVERED_EVENT) {
|
|
441
393
|
if (recoveredAt === undefined
|
|
442
394
|
|| Date.parse(event.createdAt) > Date.parse(recoveredAt)) {
|
|
443
395
|
recoveredAt = event.createdAt;
|
|
444
396
|
}
|
|
445
397
|
continue;
|
|
446
398
|
}
|
|
447
|
-
if (event.type !==
|
|
399
|
+
if (event.type !== TURN_PROGRESS_EVENT)
|
|
448
400
|
continue;
|
|
449
401
|
const candidate = typeof event.payload.progressAt === "string"
|
|
450
402
|
&& Number.isFinite(Date.parse(event.payload.progressAt))
|
|
@@ -467,32 +419,26 @@ export function isRoleRunStalled(events, runId) {
|
|
|
467
419
|
return progressAt === undefined || Date.parse(progressAt) <= Date.parse(stalledProgressAt);
|
|
468
420
|
}
|
|
469
421
|
/**
|
|
470
|
-
* Low-frequency health pass for active Task Role
|
|
422
|
+
* Low-frequency health pass for active Task Role Turns. An unaccepted Turn is
|
|
471
423
|
* watched as delivery-stalled after the reasonable delivery window; an
|
|
472
|
-
* accepted
|
|
424
|
+
* accepted Turn is displayed as checkpoint-overdue after fifteen minutes, but
|
|
473
425
|
* this scheduler performs no runtime inspection until the thirty-minute
|
|
474
426
|
* diagnostic window is due.
|
|
475
|
-
* Leader
|
|
427
|
+
* Leader Turns are only persisted when classification reaches truly-stalled —
|
|
476
428
|
* healthy downstream work, open user input, and recent own progress remain
|
|
477
429
|
* structured waiting/working facts. No branch sends terminal bytes, retries,
|
|
478
|
-
* replaces a Session, or changes
|
|
430
|
+
* replaces a Session, or changes Turn status.
|
|
479
431
|
*/
|
|
480
|
-
export async function
|
|
432
|
+
export async function reconcileStalledRoleTurns(store, delivery, now, selection, windowMs = DEFAULT_STALL_WINDOW_MS, liveStatuses, resourceEvidence, diagnosticAfterMs = DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS) {
|
|
481
433
|
// Dirty mailbox passes are intentionally not a second scheduler. Full
|
|
482
|
-
// reconcile owns the all-active-
|
|
434
|
+
// reconcile owns the all-active-Turn scan; dirty passes may still route the
|
|
483
435
|
// existing mailbox work without manufacturing another episode.
|
|
484
436
|
if (selection !== undefined && !selection.full)
|
|
485
437
|
return [];
|
|
486
|
-
if (
|
|
487
|
-
|| store.recordRoleRunStall === undefined)
|
|
438
|
+
if (store.recordRoleTurnStall === undefined)
|
|
488
439
|
return [];
|
|
489
|
-
// When the fold port exists, a missing per-Run entry is an authoritative
|
|
490
|
-
// empty fold: the stall reconciliation must not re-scan the whole history
|
|
491
|
-
// per candidate. Legacy stores without the port keep the per-candidate
|
|
492
|
-
// scans.
|
|
493
|
-
const foldPortPresent = store.getRunProgressFacts !== undefined;
|
|
494
440
|
const candidates = selectedActiveSchedulerTasks(store, selection).flatMap((task) => (selectedSchedulerRoles(store, task.id, selection).flatMap((role) => {
|
|
495
|
-
const run = store.
|
|
441
|
+
const run = store.getActiveTurn(task.id, role.name);
|
|
496
442
|
if (run === null || run.status !== "active")
|
|
497
443
|
return [];
|
|
498
444
|
return [{
|
|
@@ -511,23 +457,20 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
511
457
|
eventsByTask.set(taskId, events);
|
|
512
458
|
return events;
|
|
513
459
|
};
|
|
514
|
-
const cadenceCandidates = candidates.filter(({ task, run }) => isStallCandidate(run, now, windowMs,
|
|
460
|
+
const cadenceCandidates = candidates.filter(({ task, run }) => isStallCandidate(run, now, windowMs, latestTurnEventTime(diagnosticEvents(task.id), TURN_DIAGNOSTIC_FINISHED_EVENT, run.id)));
|
|
515
461
|
const stallCandidates = cadenceCandidates.filter(({ task, role, run }) => {
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
: undefined;
|
|
519
|
-
const progressAt = currentRoleRunProgressAt(store, task.id, role.name, run, foldPortPresent ? undefined : diagnosticEvents(task.id), progressFacts).progressAt;
|
|
520
|
-
return isStallCandidate(run, now, windowMs, latestRunEventTime(diagnosticEvents(task.id), RUN_DIAGNOSTIC_FINISHED_EVENT, run.id), progressAt);
|
|
462
|
+
const progressAt = currentRoleTurnProgressAt(store, task.id, role.name, run).progressAt;
|
|
463
|
+
return isStallCandidate(run, now, windowMs, latestTurnEventTime(diagnosticEvents(task.id), TURN_DIAGNOSTIC_FINISHED_EVENT, run.id), progressAt);
|
|
521
464
|
});
|
|
522
465
|
if (stallCandidates.length === 0)
|
|
523
466
|
return [];
|
|
524
467
|
const diagnosticStartedAt = now.toISOString();
|
|
525
468
|
const finishDiagnostics = (outcome) => {
|
|
526
469
|
for (const { task, role, run } of stallCandidates) {
|
|
527
|
-
store.
|
|
470
|
+
store.recordRoleTurnDiagnostic?.({
|
|
528
471
|
taskId: task.id,
|
|
529
472
|
roleName: role.name,
|
|
530
|
-
|
|
473
|
+
turnId: run.id,
|
|
531
474
|
startedAt: diagnosticStartedAt,
|
|
532
475
|
outcome,
|
|
533
476
|
now
|
|
@@ -536,7 +479,7 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
536
479
|
};
|
|
537
480
|
const stallCandidateKeys = new Set(stallCandidates.map(({ task, role }) => (`${task.id}\0${role.name}`)));
|
|
538
481
|
const observed = new Map();
|
|
539
|
-
// Recent active
|
|
482
|
+
// Recent active Turns are known healthy enough for Leader classification from
|
|
540
483
|
// their exact acceptance/creation boundary, but are deliberately not read
|
|
541
484
|
// from tmux or Event history until the 30-minute diagnostic window.
|
|
542
485
|
for (const candidate of candidates) {
|
|
@@ -546,7 +489,7 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
546
489
|
observed.set(key, {
|
|
547
490
|
candidate,
|
|
548
491
|
live: liveStatuses?.get(key) ?? "present",
|
|
549
|
-
progressAt: candidate.run.
|
|
492
|
+
progressAt: candidate.run.createdAt,
|
|
550
493
|
idleMs: 0,
|
|
551
494
|
stalled: false,
|
|
552
495
|
resourceActivity: false,
|
|
@@ -562,32 +505,30 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
562
505
|
statuses = await delivery.inspectRoles(stallCandidates.map(({ task, role, run, session }) => ({
|
|
563
506
|
taskId: task.id,
|
|
564
507
|
roleName: role.name,
|
|
565
|
-
|
|
566
|
-
progressAt: run.
|
|
508
|
+
turnId: run.id,
|
|
509
|
+
progressAt: run.createdAt,
|
|
567
510
|
agentId: session?.agentId ?? run.effective.agentId,
|
|
568
511
|
adapterId: session?.adapterId ?? run.effective.adapterId,
|
|
569
512
|
...(session?.launchId === undefined ? {} : { launchId: session.launchId }),
|
|
570
513
|
...(session?.nativeSessionId === undefined
|
|
571
514
|
? {}
|
|
572
515
|
: { nativeSessionId: session.nativeSessionId })
|
|
573
|
-
})), stallCandidates.
|
|
574
|
-
|
|
575
|
-
:
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
: { nativeSessionId: session.nativeSessionId })
|
|
586
|
-
}])));
|
|
516
|
+
})), stallCandidates.map(({ task, role, run, session }) => ({
|
|
517
|
+
taskId: task.id,
|
|
518
|
+
roleName: role.name,
|
|
519
|
+
turnId: run.id,
|
|
520
|
+
progressAt: currentRoleTurnProgressAt(store, task.id, role.name, run).progressAt,
|
|
521
|
+
agentId: session?.agentId ?? run.effective.agentId,
|
|
522
|
+
adapterId: session?.adapterId ?? run.effective.adapterId,
|
|
523
|
+
...(session?.launchId === undefined ? {} : { launchId: session.launchId }),
|
|
524
|
+
...(session?.nativeSessionId === undefined
|
|
525
|
+
? {}
|
|
526
|
+
: { nativeSessionId: session.nativeSessionId })
|
|
527
|
+
})));
|
|
587
528
|
}
|
|
588
529
|
catch {
|
|
589
530
|
// Health inspection is advisory. Unknown host state must leave the exact
|
|
590
|
-
//
|
|
531
|
+
// Turn/Session fence untouched. Closing this window schedules the next
|
|
591
532
|
// bounded diagnostic thirty minutes later instead of hot-looping.
|
|
592
533
|
finishDiagnostics("observation-error");
|
|
593
534
|
return [];
|
|
@@ -634,7 +575,7 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
634
575
|
: byRole.get(key);
|
|
635
576
|
}
|
|
636
577
|
catch {
|
|
637
|
-
// Without a complete live snapshot, especially for downstream
|
|
578
|
+
// Without a complete live snapshot, especially for downstream Turns,
|
|
638
579
|
// Leader classification would be unsafe. Treat the whole advisory pass
|
|
639
580
|
// as unknown instead of escalating a false stall.
|
|
640
581
|
finishDiagnostics("observation-error");
|
|
@@ -654,30 +595,16 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
654
595
|
});
|
|
655
596
|
continue;
|
|
656
597
|
}
|
|
657
|
-
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
const
|
|
661
|
-
? store.getRunProgressFacts?.(candidate.task.id, candidate.run.id)
|
|
662
|
-
: undefined;
|
|
663
|
-
const events = foldPortPresent
|
|
664
|
-
? undefined
|
|
665
|
-
: store.listEvents?.(candidate.task.id);
|
|
666
|
-
// Before exact acceptance there is no execution progress clock. Keep the
|
|
667
|
-
// delivery watch anchored to the Run creation/transport boundary even if
|
|
668
|
-
// checkpoints, output, or related WorkItem/Review/Integration records are
|
|
669
|
-
// newer; those facts are useful evidence but cannot prove provider
|
|
670
|
-
// acceptance or reset delivery timeout. Once accepted, deliveredAt is the
|
|
671
|
-
// semantic baseline and the durable fold may advance it.
|
|
672
|
-
const progress = currentRoleRunProgressAt(store, candidate.task.id, candidate.role.name, candidate.run, events, progressFacts);
|
|
598
|
+
const progressFacts = store.getTurnProgressFacts(candidate.task.id, candidate.run.id);
|
|
599
|
+
// The Turn creation boundary starts the execution clock; durable workflow
|
|
600
|
+
// facts may advance it independently of Provider transport observations.
|
|
601
|
+
const progress = currentRoleTurnProgressAt(store, candidate.task.id, candidate.role.name, candidate.run);
|
|
673
602
|
const progressAt = progress.progressAt;
|
|
674
|
-
const evaluation =
|
|
603
|
+
const evaluation = evaluateRoleTurnStall({
|
|
675
604
|
progressAt,
|
|
676
605
|
now,
|
|
677
606
|
windowMs,
|
|
678
|
-
lastAttentionProgressAt:
|
|
679
|
-
? progressFacts?.latestStall?.progressAt
|
|
680
|
-
: latestStallProgressAt(events ?? [], candidate.run.id)
|
|
607
|
+
lastAttentionProgressAt: progressFacts?.latestStall?.progressAt
|
|
681
608
|
});
|
|
682
609
|
const runAgentId = candidate.run.effective.agentId;
|
|
683
610
|
const runAdapterId = candidate.run.effective.adapterId;
|
|
@@ -691,7 +618,7 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
691
618
|
: {
|
|
692
619
|
taskId: candidate.task.id,
|
|
693
620
|
roleName: candidate.role.name,
|
|
694
|
-
|
|
621
|
+
turnId: candidate.run.id,
|
|
695
622
|
agentId: runAgentId,
|
|
696
623
|
adapterId: runAdapterId,
|
|
697
624
|
...(candidate.session.nativeSessionId === undefined
|
|
@@ -709,30 +636,23 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
709
636
|
&& resourceEvidenceMatchesCurrentRun(resourceSnapshot, expectedResourceIdentity, progressAt)
|
|
710
637
|
&& resourceEvidenceIsFresh(resourceSnapshot, now, windowMs, diagnosticAfterMs);
|
|
711
638
|
const resource = resourceIsCurrent ? resourceSnapshot : undefined;
|
|
712
|
-
const health =
|
|
639
|
+
const health = projectRoleTurnHealth({
|
|
713
640
|
progressAt,
|
|
714
641
|
createdAt: candidate.run.createdAt,
|
|
715
|
-
...(candidate.run.deliveredAt === undefined
|
|
716
|
-
? {}
|
|
717
|
-
: { deliveredAt: candidate.run.deliveredAt }),
|
|
718
642
|
now,
|
|
719
643
|
windowMs,
|
|
720
644
|
diagnosticAfterMs,
|
|
721
645
|
hostLiveness: live,
|
|
722
646
|
nativeSession: candidate.session,
|
|
723
|
-
providerAcceptance: candidate.
|
|
724
|
-
? "ambiguous"
|
|
725
|
-
: "accepted",
|
|
647
|
+
providerAcceptance: candidate.session === null ? "ambiguous" : "accepted",
|
|
726
648
|
resource,
|
|
727
649
|
roleName: candidate.role.name
|
|
728
650
|
});
|
|
729
|
-
// Delivery-stalled
|
|
730
|
-
//
|
|
651
|
+
// Delivery-stalled Turns retain the existing delivery clock and immediate
|
|
652
|
+
// Provider-uncertainty path. Accepted execution Turns use the shared
|
|
731
653
|
// projection. Exact Session/host resource evidence remains diagnostic.
|
|
732
654
|
const resourceActivity = health.resourceActivity;
|
|
733
|
-
const stalled =
|
|
734
|
-
? evaluation.stalled && live === "present"
|
|
735
|
-
: health.stalled && sessionMatchesRun && sessionUsable;
|
|
655
|
+
const stalled = health.stalled && sessionMatchesRun && sessionUsable;
|
|
736
656
|
observed.set(key, {
|
|
737
657
|
candidate,
|
|
738
658
|
live,
|
|
@@ -753,24 +673,22 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
753
673
|
if (current.live !== "present" || !current.stalled || !current.stallCandidate)
|
|
754
674
|
continue;
|
|
755
675
|
const { candidate, progressAt } = current;
|
|
756
|
-
const previous =
|
|
757
|
-
? store.getRunProgressFacts?.(candidate.task.id, candidate.run.id)?.latestStall
|
|
758
|
-
: latestStallEvidenceKey(store.listEvents?.(candidate.task.id) ?? [], candidate.run.id);
|
|
676
|
+
const previous = store.getTurnProgressFacts(candidate.task.id, candidate.run.id)?.latestStall;
|
|
759
677
|
if (previous !== undefined
|
|
760
678
|
&& Date.parse(progressAt) > Date.parse(previous.progressAt)) {
|
|
761
679
|
// A new semantic progress point closes the previous episode first. It
|
|
762
680
|
// may itself already be older than the window, in which case the same
|
|
763
|
-
// pass records the next
|
|
764
|
-
store.
|
|
681
|
+
// pass records the next Turn+progressAt episode below.
|
|
682
|
+
store.recordRoleTurnProgress?.({
|
|
765
683
|
taskId: candidate.task.id,
|
|
766
684
|
roleName: candidate.role.name,
|
|
767
|
-
|
|
685
|
+
turnId: candidate.run.id,
|
|
768
686
|
progressAt,
|
|
769
687
|
...(current.evidence === undefined ? {} : { evidence: current.evidence }),
|
|
770
688
|
now
|
|
771
689
|
});
|
|
772
690
|
}
|
|
773
|
-
const kind = candidate.
|
|
691
|
+
const kind = candidate.session === null
|
|
774
692
|
? "delivery-stalled"
|
|
775
693
|
: "workflow-not-progressing";
|
|
776
694
|
const classification = candidate.role.name === "leader"
|
|
@@ -789,15 +707,15 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
789
707
|
: []),
|
|
790
708
|
...(current.evidence === undefined ? [] : [current.evidence])
|
|
791
709
|
].join(":");
|
|
792
|
-
// The final contract is one episode per
|
|
710
|
+
// The final contract is one episode per Turn + semantic progress point.
|
|
793
711
|
// New role/session/provider evidence is retained in the Leader's next
|
|
794
712
|
// diagnostic context, not duplicated as another Task-level alert.
|
|
795
713
|
if (previous?.progressAt === progressAt)
|
|
796
714
|
continue;
|
|
797
|
-
const persisted = store.
|
|
715
|
+
const persisted = store.recordRoleTurnStall({
|
|
798
716
|
taskId: candidate.task.id,
|
|
799
717
|
roleName: candidate.role.name,
|
|
800
|
-
|
|
718
|
+
turnId: candidate.run.id,
|
|
801
719
|
agentId: candidate.run.effective.agentId,
|
|
802
720
|
adapterId: candidate.run.effective.adapterId,
|
|
803
721
|
session: candidate.session,
|
|
@@ -812,7 +730,7 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
812
730
|
raised.push({
|
|
813
731
|
taskId: candidate.task.id,
|
|
814
732
|
roleName: candidate.role.name,
|
|
815
|
-
|
|
733
|
+
turnId: candidate.run.id,
|
|
816
734
|
status: persisted,
|
|
817
735
|
kind,
|
|
818
736
|
classification,
|
|
@@ -821,21 +739,19 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
821
739
|
}
|
|
822
740
|
}
|
|
823
741
|
// A related WorkItem/Review/Integration fold may advance progress without
|
|
824
|
-
// carrying the
|
|
742
|
+
// carrying the Turn id. Materialize it once so context/web can clear the
|
|
825
743
|
// attention projection and the event history records the recovery boundary.
|
|
826
744
|
for (const current of observed.values()) {
|
|
827
745
|
if (current.live !== "present" || current.stalled || !current.stallCandidate)
|
|
828
746
|
continue;
|
|
829
747
|
const { candidate, progressAt } = current;
|
|
830
|
-
const previous =
|
|
831
|
-
? store.getRunProgressFacts?.(candidate.task.id, candidate.run.id)?.latestStall
|
|
832
|
-
: latestStallEvidenceKey(store.listEvents?.(candidate.task.id) ?? [], candidate.run.id);
|
|
748
|
+
const previous = store.getTurnProgressFacts(candidate.task.id, candidate.run.id)?.latestStall;
|
|
833
749
|
if (previous !== undefined
|
|
834
750
|
&& Date.parse(progressAt) > Date.parse(previous.progressAt)) {
|
|
835
|
-
store.
|
|
751
|
+
store.recordRoleTurnProgress?.({
|
|
836
752
|
taskId: candidate.task.id,
|
|
837
753
|
roleName: candidate.role.name,
|
|
838
|
-
|
|
754
|
+
turnId: candidate.run.id,
|
|
839
755
|
progressAt,
|
|
840
756
|
...(current.evidence === undefined ? {} : { evidence: current.evidence }),
|
|
841
757
|
now
|
|
@@ -846,8 +762,7 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
846
762
|
return raised;
|
|
847
763
|
}
|
|
848
764
|
function isStallCandidate(run, now, windowMs, lastDiagnosticFinishedAt, latestProgressAt) {
|
|
849
|
-
const
|
|
850
|
-
const baseline = [startAt, lastDiagnosticFinishedAt, latestProgressAt]
|
|
765
|
+
const baseline = [run.createdAt, lastDiagnosticFinishedAt, latestProgressAt]
|
|
851
766
|
.filter((value) => (value !== undefined && Number.isFinite(Date.parse(value))))
|
|
852
767
|
.reduce((latest, value) => (Date.parse(value) > Date.parse(latest) ? value : latest));
|
|
853
768
|
const ageMs = now.getTime() - Date.parse(baseline);
|
|
@@ -859,7 +774,7 @@ function classifyLeaderStall(store, taskId, observed, now, windowMs) {
|
|
|
859
774
|
if (store.hasOpenInputRequest(taskId))
|
|
860
775
|
return "waiting-user";
|
|
861
776
|
const downstream = [...observed.values()].filter((entry) => (entry.candidate.task.id === taskId && entry.candidate.role.name !== "leader"));
|
|
862
|
-
// A present downstream
|
|
777
|
+
// A present downstream Turn keeps recovery Leader-owned. If that Turn is
|
|
863
778
|
// itself stalled, this pass has just routed its structured attention to the
|
|
864
779
|
// Leader; escalating the Leader to the Operator in the same pass would skip
|
|
865
780
|
// the intended recovery owner.
|
|
@@ -868,13 +783,13 @@ function classifyLeaderStall(store, taskId, observed, now, windowMs) {
|
|
|
868
783
|
const mailbox = store.getWorkMailbox({ kind: "role", taskId, roleName: "leader" });
|
|
869
784
|
const pending = mailbox === null || mailbox === undefined ? null : nextPendingBatch(mailbox);
|
|
870
785
|
const processing = mailbox?.processing;
|
|
871
|
-
const processingCurrent = processing?.executionRef?.type === "
|
|
786
|
+
const processingCurrent = processing?.executionRef?.type === "turn"
|
|
872
787
|
&& processing.executionRef.taskId === taskId
|
|
873
788
|
&& leader !== undefined
|
|
874
789
|
&& processing.executionRef.id === leader.candidate.run.id;
|
|
875
|
-
const
|
|
876
|
-
if (processingCurrent &&
|
|
877
|
-
const actionProgressAt = latestLeaderActionProgressAt(store, taskId,
|
|
790
|
+
const currentTurnId = leader?.candidate.run.id;
|
|
791
|
+
if (processingCurrent && currentTurnId !== undefined && processing !== null) {
|
|
792
|
+
const actionProgressAt = latestLeaderActionProgressAt(store, taskId, currentTurnId, processing.startedAt, processing.batch, now);
|
|
878
793
|
const actionProgressMs = Date.parse(actionProgressAt);
|
|
879
794
|
const actionStalled = Number.isFinite(actionProgressMs)
|
|
880
795
|
&& now.getTime() - actionProgressMs >= windowMs;
|
|
@@ -893,7 +808,7 @@ function classifyLeaderStall(store, taskId, observed, now, windowMs) {
|
|
|
893
808
|
return "truly-stalled";
|
|
894
809
|
}
|
|
895
810
|
const LEADER_ACTION_PROGRESS_TYPES = new Set([
|
|
896
|
-
|
|
811
|
+
TURN_PROGRESS_EVENT,
|
|
897
812
|
"input.answered",
|
|
898
813
|
"input.auto-answered",
|
|
899
814
|
"input.cancelled",
|
|
@@ -909,7 +824,7 @@ const LEADER_ACTION_PROGRESS_TYPES = new Set([
|
|
|
909
824
|
"decision.superseded",
|
|
910
825
|
"milestone.added"
|
|
911
826
|
]);
|
|
912
|
-
function latestLeaderActionProgressAt(store, taskId,
|
|
827
|
+
function latestLeaderActionProgressAt(store, taskId, turnId, startedAt, batch, now) {
|
|
913
828
|
const startedMs = Date.parse(startedAt);
|
|
914
829
|
if (!Number.isFinite(startedMs))
|
|
915
830
|
return "";
|
|
@@ -920,14 +835,14 @@ function latestLeaderActionProgressAt(store, taskId, runId, startedAt, batch, no
|
|
|
920
835
|
continue;
|
|
921
836
|
if (event.payload.taskId !== undefined && event.payload.taskId !== taskId)
|
|
922
837
|
continue;
|
|
923
|
-
if (!leaderActionEventMatches(event, taskId,
|
|
838
|
+
if (!leaderActionEventMatches(event, taskId, turnId, batch))
|
|
924
839
|
continue;
|
|
925
840
|
const createdMs = Date.parse(event.createdAt);
|
|
926
841
|
if (!Number.isFinite(createdMs)
|
|
927
842
|
|| createdMs < startedMs
|
|
928
843
|
|| createdMs > now.getTime())
|
|
929
844
|
continue;
|
|
930
|
-
const value = event.type ===
|
|
845
|
+
const value = event.type === TURN_PROGRESS_EVENT
|
|
931
846
|
&& typeof event.payload.progressAt === "string"
|
|
932
847
|
&& Number.isFinite(Date.parse(event.payload.progressAt))
|
|
933
848
|
? event.payload.progressAt
|
|
@@ -943,12 +858,12 @@ function latestLeaderActionProgressAt(store, taskId, runId, startedAt, batch, no
|
|
|
943
858
|
}
|
|
944
859
|
return latest;
|
|
945
860
|
}
|
|
946
|
-
function leaderActionEventMatches(event, taskId,
|
|
861
|
+
function leaderActionEventMatches(event, taskId, turnId, batch) {
|
|
947
862
|
if (event.taskId !== taskId)
|
|
948
863
|
return false;
|
|
949
|
-
if (event.payload.
|
|
864
|
+
if (event.payload.leaderTurnId === turnId)
|
|
950
865
|
return true;
|
|
951
|
-
if (event.payload.
|
|
866
|
+
if (event.payload.turnId === turnId)
|
|
952
867
|
return true;
|
|
953
868
|
const refs = batch.refs ?? [];
|
|
954
869
|
const refMatches = (type, payloadKey) => {
|
|
@@ -957,7 +872,7 @@ function leaderActionEventMatches(event, taskId, runId, batch) {
|
|
|
957
872
|
&& ref.id === id
|
|
958
873
|
&& (ref.taskId === undefined || ref.taskId === taskId)));
|
|
959
874
|
};
|
|
960
|
-
if (refMatches("
|
|
875
|
+
if (refMatches("turn", "turnId"))
|
|
961
876
|
return true;
|
|
962
877
|
if (refMatches("work-item", "workItemId"))
|
|
963
878
|
return true;
|
|
@@ -983,7 +898,7 @@ function leaderStallEvidence(store, taskId, observed, now, windowMs) {
|
|
|
983
898
|
: "recent";
|
|
984
899
|
const processing = store.getWorkMailbox({ kind: "role", taskId, roleName: "leader" })?.processing;
|
|
985
900
|
const leader = [...observed.values()].find((entry) => (entry.candidate.task.id === taskId && entry.candidate.role.name === "leader"));
|
|
986
|
-
const processingCurrent = processing?.executionRef?.type === "
|
|
901
|
+
const processingCurrent = processing?.executionRef?.type === "turn"
|
|
987
902
|
&& processing.executionRef.taskId === taskId
|
|
988
903
|
&& leader !== undefined
|
|
989
904
|
&& processing.executionRef.id === leader.candidate.run.id;
|
|
@@ -1014,10 +929,10 @@ function exactLiveStatuses(statuses, candidates) {
|
|
|
1014
929
|
throw new Error("Tmux Role stall snapshot is incomplete.");
|
|
1015
930
|
return result;
|
|
1016
931
|
}
|
|
1017
|
-
function resourceForRun(snapshot, taskId, roleName,
|
|
932
|
+
function resourceForRun(snapshot, taskId, roleName, turnId) {
|
|
1018
933
|
if (snapshot === undefined)
|
|
1019
934
|
return undefined;
|
|
1020
|
-
return snapshot.get(`${taskId}\0${roleName}\0${
|
|
935
|
+
return snapshot.get(`${taskId}\0${roleName}\0${turnId}`);
|
|
1021
936
|
}
|
|
1022
937
|
function resourceEvidenceMatchesCurrentRun(resource, expected, progressAt) {
|
|
1023
938
|
if (resource === undefined || expected === undefined)
|
|
@@ -1027,7 +942,7 @@ function resourceEvidenceMatchesCurrentRun(resource, expected, progressAt) {
|
|
|
1027
942
|
|| resource.progressAt !== progressAt
|
|
1028
943
|
|| identity.taskId !== expected.taskId
|
|
1029
944
|
|| identity.roleName !== expected.roleName
|
|
1030
|
-
|| identity.
|
|
945
|
+
|| identity.turnId !== expected.turnId
|
|
1031
946
|
|| identity.agentId !== expected.agentId
|
|
1032
947
|
|| identity.adapterId !== expected.adapterId
|
|
1033
948
|
|| identity.nativeSessionId !== expected.nativeSessionId
|