@zq-silk/yui 0.6.2 → 0.6.4
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 +28 -4
- package/README.md +60 -97
- package/dist/agent/argumentPolicy.js +1 -1
- package/dist/agent/managedRuntimeEnvironment.js +1 -0
- package/dist/cli/commandCatalog.js +19 -9
- package/dist/cli/interactionPolicy.js +4 -2
- package/dist/cli.js +77 -32
- package/dist/commands/taskCommands.js +46 -11
- package/dist/commands/taskContextCommand.js +1 -1
- package/dist/commands/taskRoleRuntimeStatus.js +170 -10
- package/dist/controller/agentRuntimeObserver.js +210 -0
- package/dist/controller/clientRuntime.js +3 -21
- package/dist/controller/controller.js +47 -7
- package/dist/controller/fileSchedulerStoreAdapter.js +522 -388
- package/dist/controller/runtime.js +9 -3
- package/dist/controller/runtimeEventInbox.js +49 -295
- package/dist/controller/runtimeEventProcessor.js +184 -321
- package/dist/controller/runtimeHookRunFence.js +226 -0
- package/dist/controller/runtimeLaunchCoordinator.js +91 -26
- package/dist/controller/runtimeObservationHook.js +112 -0
- package/dist/core/controllerServer.js +5 -0
- package/dist/executor/agentAdapter.js +18 -3
- package/dist/executor/fileRoleLaunchPlanner.js +64 -15
- package/dist/executor/managedClaudeRunner.js +121 -0
- package/dist/observability/executionAudit.js +6 -3
- package/dist/repository/taskWorkspacePreparer.js +1 -4
- package/dist/run/providerRetryConfig.js +8 -3
- package/dist/runtime/agentDriver.js +229 -0
- package/dist/runtime/agentDriverObservation.js +57 -0
- package/dist/runtime/builtinAgentDrivers.js +235 -0
- package/dist/runtime/builtinTranscriptObserver.js +290 -0
- package/dist/runtime/builtinTranscriptUsage.js +97 -0
- package/dist/runtime/exactControlPlane.js +2 -2
- package/dist/runtime/index.js +1 -1
- package/dist/runtime/ports.js +12 -1
- package/dist/runtime/runtimeObservation.js +297 -0
- package/dist/runtime/runtimeProjection.js +277 -0
- package/dist/runtime/sessionTerminationGuard.js +78 -22
- package/dist/runtime/tmuxAdapters.js +35 -0
- package/dist/scheduler/activeRoleRunDelivery.js +28 -13
- package/dist/scheduler/leaderWakeupProcessor.js +21 -2
- package/dist/scheduler/roleRunLiveness.js +2 -2
- package/dist/scheduler/roleRunStall.js +62 -114
- package/dist/storage/migration/productionRegistry.js +85 -0
- package/dist/storage/sqliteStore.js +3 -3
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +2 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +123 -0
- package/dist/telemetry/sqliteTelemetryStore.js +0 -28
- package/dist/telemetry/telemetryCompaction.js +1 -0
- package/dist/telemetry/telemetryConfig.js +4 -5
- package/dist/tmux/tmuxManager.js +136 -22
- package/dist/web/assets/client/view.js +1 -1
- package/dist/web/tmuxWebTerminal.js +17 -12
- package/dist/web/webSnapshot.js +1 -1
- package/dist/worktree/managedWorkspace.js +14 -0
- package/i18n/README.zh-CN.md +12 -7
- package/package.json +1 -1
- package/dist/controller/claudeLifecycleHook.js +0 -203
- package/dist/controller/codexLifecycleHook.js +0 -108
- package/dist/controller/providerHookRunFence.js +0 -156
- package/dist/lifecycle/providerLifecycleMapping.js +0 -190
- package/dist/telemetry/telemetryRouter.js +0 -32
|
@@ -5,22 +5,17 @@ import { selectedSchedulerRoles, selectedSchedulerTasks } from "./ports.js";
|
|
|
5
5
|
* is simply slow keeps its structured checkpoint fresh and never crosses it.
|
|
6
6
|
*/
|
|
7
7
|
export const DEFAULT_STALL_WINDOW_MS = 30 * 60_000;
|
|
8
|
-
/** Cheap
|
|
9
|
-
export const
|
|
10
|
-
/** Resource activity may postpone attention only within this semantic gap. */
|
|
11
|
-
export const DEFAULT_RESOURCE_ONLY_SEMANTIC_GAP_MS = 90 * 60_000;
|
|
8
|
+
/** Cheap workflow-stall candidate filter; the real threshold remains 30m. */
|
|
9
|
+
export const DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS = 10 * 60_000;
|
|
12
10
|
export const RUN_PROGRESS_EVENT = "run.progress";
|
|
13
11
|
export const RUN_STALLED_EVENT = "run.stalled";
|
|
14
12
|
export const RUN_RECOVERED_EVENT = "run.recovered";
|
|
15
|
-
/** Durable one-shot advisory resource evidence; never a progress fact. */
|
|
16
|
-
export const RUN_RESOURCE_SUPPRESSED_EVENT = "run.resource-suppressed";
|
|
17
13
|
/** Structured, non-Message recovery evidence written by an explicit Leader. */
|
|
18
14
|
export const RUN_RECOVERY_REQUESTED_EVENT = "run.recovery-requested";
|
|
19
15
|
export const RUN_RECOVERY_APPLIED_EVENT = "run.recovery-applied";
|
|
20
|
-
/**
|
|
16
|
+
/** Workflow-semantic events that count for the durable progress clock. */
|
|
21
17
|
const ACTIVITY_EVENT_TYPES = new Set([
|
|
22
18
|
RUN_PROGRESS_EVENT,
|
|
23
|
-
"runtime.provider-turn-progress",
|
|
24
19
|
"message.sent",
|
|
25
20
|
"input.answered",
|
|
26
21
|
"input.auto-answered",
|
|
@@ -48,10 +43,9 @@ export function clearMatchingLeaderStallAttention(store, taskId, runId) {
|
|
|
48
43
|
return true;
|
|
49
44
|
}
|
|
50
45
|
/**
|
|
51
|
-
* One pure projection used by every Role. Resource activity
|
|
52
|
-
*
|
|
53
|
-
* or
|
|
54
|
-
* it never changes the durable progress clock or authorizes recovery.
|
|
46
|
+
* One pure projection used by every Role. Resource activity is retained only
|
|
47
|
+
* as exact-generation diagnostic evidence; it never changes the durable
|
|
48
|
+
* progress clock, suppresses workflow attention, or authorizes recovery.
|
|
55
49
|
*/
|
|
56
50
|
export function projectRoleRunHealth(input) {
|
|
57
51
|
const windowMs = input.windowMs ?? DEFAULT_STALL_WINDOW_MS;
|
|
@@ -69,7 +63,7 @@ export function projectRoleRunHealth(input) {
|
|
|
69
63
|
const candidate = Number.isFinite(candidateAge)
|
|
70
64
|
&& (input.deliveredAt === undefined
|
|
71
65
|
? candidateAge >= windowMs
|
|
72
|
-
: candidateAge >=
|
|
66
|
+
: candidateAge >= DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS);
|
|
73
67
|
const providerAcceptance = input.providerAcceptance
|
|
74
68
|
?? (input.deliveredAt === undefined ? "ambiguous" : "accepted");
|
|
75
69
|
const hostLiveness = input.hostLiveness;
|
|
@@ -99,14 +93,13 @@ export function projectRoleRunHealth(input) {
|
|
|
99
93
|
// evidence is still actionable in that case. Only an explicit stopped or
|
|
100
94
|
// broken Session blocks the projection; identity mismatches are fenced by
|
|
101
95
|
// reconcileStalledRoleRuns below before this projection is routed.
|
|
102
|
-
const
|
|
96
|
+
const workflowStall = candidate
|
|
103
97
|
&& evaluation.stalled
|
|
104
98
|
&& hostLiveness === "present"
|
|
105
99
|
&& nativeSession !== "stopped"
|
|
106
100
|
&& nativeSession !== "broken"
|
|
107
|
-
&& providerAcceptance !== "ambiguous"
|
|
108
|
-
|
|
109
|
-
const stalled = executionStall
|
|
101
|
+
&& providerAcceptance !== "ambiguous";
|
|
102
|
+
const stalled = workflowStall
|
|
110
103
|
&& !waitingUser
|
|
111
104
|
&& !waitingOnWorkers;
|
|
112
105
|
const classification = waitingUser
|
|
@@ -151,9 +144,9 @@ export function evaluateRoleRunStall(input) {
|
|
|
151
144
|
}
|
|
152
145
|
/**
|
|
153
146
|
* Latest durable progress timestamp for an active Run. Provider acceptance
|
|
154
|
-
* (deliveredAt), explicit
|
|
155
|
-
*
|
|
156
|
-
*
|
|
147
|
+
* (deliveredAt), explicit workflow checkpoints, and semantic domain activity
|
|
148
|
+
* count as progress. Provider operations, tokens, CPU/memory and bookkeeping
|
|
149
|
+
* timestamps are intentionally excluded.
|
|
157
150
|
*/
|
|
158
151
|
export function latestDurableProgressAt(input) {
|
|
159
152
|
const candidates = [
|
|
@@ -170,15 +163,10 @@ export function latestDurableProgressAt(input) {
|
|
|
170
163
|
export function latestRunProgressAt(events, runId) {
|
|
171
164
|
let latest;
|
|
172
165
|
for (const event of events) {
|
|
173
|
-
if (event.type !== RUN_PROGRESS_EVENT
|
|
174
|
-
&& event.type !== "runtime.provider-turn-progress")
|
|
166
|
+
if (event.type !== RUN_PROGRESS_EVENT)
|
|
175
167
|
continue;
|
|
176
168
|
if (event.payload.runId !== runId)
|
|
177
169
|
continue;
|
|
178
|
-
if (event.type === "runtime.provider-turn-progress"
|
|
179
|
-
&& (typeof event.payload.progressAt !== "string"
|
|
180
|
-
|| !Number.isFinite(Date.parse(event.payload.progressAt))))
|
|
181
|
-
continue;
|
|
182
170
|
const progressAt = typeof event.payload.progressAt === "string"
|
|
183
171
|
&& Number.isFinite(Date.parse(event.payload.progressAt))
|
|
184
172
|
? event.payload.progressAt
|
|
@@ -320,12 +308,7 @@ export function latestRunActivityAt(events, runId) {
|
|
|
320
308
|
|| event.type === RUN_STALLED_EVENT
|
|
321
309
|
|| event.type === RUN_RECOVERED_EVENT)
|
|
322
310
|
continue;
|
|
323
|
-
|
|
324
|
-
&& (typeof event.payload.progressAt !== "string"
|
|
325
|
-
|| !Number.isFinite(Date.parse(event.payload.progressAt))))
|
|
326
|
-
continue;
|
|
327
|
-
const activityAt = (event.type === RUN_PROGRESS_EVENT
|
|
328
|
-
|| event.type === "runtime.provider-turn-progress")
|
|
311
|
+
const activityAt = event.type === RUN_PROGRESS_EVENT
|
|
329
312
|
&& typeof event.payload.progressAt === "string"
|
|
330
313
|
&& Number.isFinite(Date.parse(event.payload.progressAt))
|
|
331
314
|
? event.payload.progressAt
|
|
@@ -386,13 +369,10 @@ export function foldRunProgressFacts(events) {
|
|
|
386
369
|
byRun.set(runId, facts);
|
|
387
370
|
}
|
|
388
371
|
const type = event.type;
|
|
389
|
-
const isProgress = type === RUN_PROGRESS_EVENT
|
|
390
|
-
|| type === "runtime.provider-turn-progress";
|
|
372
|
+
const isProgress = type === RUN_PROGRESS_EVENT;
|
|
391
373
|
if (isProgress) {
|
|
392
374
|
const validProgress = typeof event.payload.progressAt === "string"
|
|
393
375
|
&& Number.isFinite(Date.parse(event.payload.progressAt));
|
|
394
|
-
if (type === "runtime.provider-turn-progress" && !validProgress)
|
|
395
|
-
continue;
|
|
396
376
|
const progressAt = validProgress
|
|
397
377
|
? event.payload.progressAt
|
|
398
378
|
: event.createdAt;
|
|
@@ -400,9 +380,7 @@ export function foldRunProgressFacts(events) {
|
|
|
400
380
|
|| Date.parse(progressAt) > Date.parse(facts.latestCheckpointAt)) {
|
|
401
381
|
facts.latestCheckpointAt = progressAt;
|
|
402
382
|
}
|
|
403
|
-
//
|
|
404
|
-
// exclusion in latestRunActivityAt is structural here because a progress
|
|
405
|
-
// event can never be either type.
|
|
383
|
+
// Workflow checkpoints are also semantic activity.
|
|
406
384
|
if (facts.latestActivityAt === undefined
|
|
407
385
|
|| Date.parse(progressAt) > Date.parse(facts.latestActivityAt)) {
|
|
408
386
|
facts.latestActivityAt = progressAt;
|
|
@@ -444,34 +422,59 @@ function yieldEventLoop() {
|
|
|
444
422
|
* projection the runtime-health view reads to surface needs-attention.
|
|
445
423
|
*/
|
|
446
424
|
export function isRoleRunStalled(events, runId) {
|
|
447
|
-
|
|
448
|
-
|
|
425
|
+
let stalled;
|
|
426
|
+
let recoveredAt;
|
|
427
|
+
let progressAt;
|
|
428
|
+
for (const event of events) {
|
|
429
|
+
if (event.payload.runId !== runId)
|
|
430
|
+
continue;
|
|
431
|
+
if (event.type === RUN_STALLED_EVENT) {
|
|
432
|
+
if (stalled === undefined
|
|
433
|
+
|| Date.parse(event.createdAt) > Date.parse(stalled.createdAt)) {
|
|
434
|
+
stalled = event;
|
|
435
|
+
}
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
if (event.type === RUN_RECOVERED_EVENT) {
|
|
439
|
+
if (recoveredAt === undefined
|
|
440
|
+
|| Date.parse(event.createdAt) > Date.parse(recoveredAt)) {
|
|
441
|
+
recoveredAt = event.createdAt;
|
|
442
|
+
}
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
if (event.type !== RUN_PROGRESS_EVENT)
|
|
446
|
+
continue;
|
|
447
|
+
const candidate = typeof event.payload.progressAt === "string"
|
|
448
|
+
&& Number.isFinite(Date.parse(event.payload.progressAt))
|
|
449
|
+
? event.payload.progressAt
|
|
450
|
+
: event.createdAt;
|
|
451
|
+
if (progressAt === undefined || Date.parse(candidate) > Date.parse(progressAt)) {
|
|
452
|
+
progressAt = candidate;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (stalled === undefined)
|
|
449
456
|
return false;
|
|
450
|
-
const
|
|
451
|
-
|
|
452
|
-
.sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
|
|
453
|
-
const stalledProgressAt = typeof stalled?.payload.progressAt === "string"
|
|
457
|
+
const stalledAt = stalled.createdAt;
|
|
458
|
+
const stalledProgressAt = typeof stalled.payload.progressAt === "string"
|
|
454
459
|
&& Number.isFinite(Date.parse(stalled.payload.progressAt))
|
|
455
460
|
? stalled.payload.progressAt
|
|
456
461
|
: stalledAt;
|
|
457
|
-
const recoveredAt = latestRunEventTime(events, RUN_RECOVERED_EVENT, runId);
|
|
458
462
|
if (recoveredAt !== undefined && Date.parse(recoveredAt) > Date.parse(stalledAt)) {
|
|
459
463
|
return false;
|
|
460
464
|
}
|
|
461
|
-
const progressAt = latestRunProgressAt(events, runId);
|
|
462
465
|
return progressAt === undefined || Date.parse(progressAt) <= Date.parse(stalledProgressAt);
|
|
463
466
|
}
|
|
464
467
|
/**
|
|
465
468
|
* Low-frequency health pass for active Task Role Runs. An unaccepted Run is
|
|
466
469
|
* watched as delivery-stalled after the reasonable delivery window; an
|
|
467
|
-
* accepted Run enters the
|
|
470
|
+
* accepted Run enters the workflow-stall candidate scan after ten minutes,
|
|
468
471
|
* while the actual no-progress threshold remains thirty minutes.
|
|
469
472
|
* Leader Runs are only persisted when classification reaches truly-stalled —
|
|
470
473
|
* healthy downstream work, open user input, and recent own progress remain
|
|
471
474
|
* structured waiting/working facts. No branch sends terminal bytes, retries,
|
|
472
475
|
* replaces a Session, or changes Run status.
|
|
473
476
|
*/
|
|
474
|
-
export async function reconcileStalledRoleRuns(store, delivery, now, selection, windowMs = DEFAULT_STALL_WINDOW_MS, liveStatuses, resourceEvidence
|
|
477
|
+
export async function reconcileStalledRoleRuns(store, delivery, now, selection, windowMs = DEFAULT_STALL_WINDOW_MS, liveStatuses, resourceEvidence) {
|
|
475
478
|
// Dirty mailbox passes are intentionally not a second scheduler. Full
|
|
476
479
|
// reconcile owns the all-active-Run scan; dirty passes may still route the
|
|
477
480
|
// existing mailbox work without manufacturing another episode.
|
|
@@ -655,15 +658,13 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
655
658
|
: { launchId: candidate.session.launchId })
|
|
656
659
|
};
|
|
657
660
|
const resourceSnapshot = resourceForRun(resourceEvidence, candidate.task.id, candidate.role.name, candidate.run.id);
|
|
658
|
-
const
|
|
661
|
+
const resourceIsCurrent = live === "present"
|
|
659
662
|
&& sessionMatchesRun
|
|
660
663
|
&& sessionUsable
|
|
661
664
|
&& expectedResourceIdentity !== undefined
|
|
662
665
|
&& resourceEvidenceMatchesCurrentRun(resourceSnapshot, expectedResourceIdentity, progressAt)
|
|
663
666
|
&& resourceEvidenceIsFresh(resourceSnapshot, now, windowMs);
|
|
664
|
-
const resource =
|
|
665
|
-
? await consumeResourceEvidence(store, resourceEvidence, candidate.task.id, candidate.role.name, candidate.run.id, progressAt, expectedResourceIdentity, resourceSuppressionKeys, now)
|
|
666
|
-
: undefined;
|
|
667
|
+
const resource = resourceIsCurrent ? resourceSnapshot : undefined;
|
|
667
668
|
const health = projectRoleRunHealth({
|
|
668
669
|
progressAt,
|
|
669
670
|
createdAt: candidate.run.createdAt,
|
|
@@ -682,8 +683,7 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
682
683
|
});
|
|
683
684
|
// Delivery-stalled Runs retain the existing delivery clock and immediate
|
|
684
685
|
// provider-uncertainty path. Accepted execution Runs use the shared
|
|
685
|
-
// projection
|
|
686
|
-
// conditions.
|
|
686
|
+
// projection. Exact Session/host resource evidence remains diagnostic.
|
|
687
687
|
const resourceActivity = health.resourceActivity;
|
|
688
688
|
const stalled = candidate.run.deliveredAt === undefined
|
|
689
689
|
? evaluation.stalled && live === "present"
|
|
@@ -693,9 +693,8 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
693
693
|
live,
|
|
694
694
|
progressAt,
|
|
695
695
|
idleMs: evaluation.idleMs,
|
|
696
|
-
// Resource activity is advisory
|
|
697
|
-
//
|
|
698
|
-
// never advances progress or creates a recovered event by itself.
|
|
696
|
+
// Resource activity is advisory and never advances workflow progress or
|
|
697
|
+
// suppresses a workflow-not-progressing episode.
|
|
699
698
|
stalled,
|
|
700
699
|
resourceActivity,
|
|
701
700
|
evidence: [
|
|
@@ -728,7 +727,7 @@ export async function reconcileStalledRoleRuns(store, delivery, now, selection,
|
|
|
728
727
|
}
|
|
729
728
|
const kind = candidate.run.deliveredAt === undefined
|
|
730
729
|
? "delivery-stalled"
|
|
731
|
-
: "
|
|
730
|
+
: "workflow-not-progressing";
|
|
732
731
|
const classification = candidate.role.name === "leader"
|
|
733
732
|
? classifyLeaderStall(store, candidate.task.id, observed, now, windowMs)
|
|
734
733
|
: "truly-stalled";
|
|
@@ -806,10 +805,10 @@ function isStallCandidate(run, now, windowMs) {
|
|
|
806
805
|
if (!Number.isFinite(ageMs))
|
|
807
806
|
return false;
|
|
808
807
|
// Undelivered Runs are watched for a delivery stall on the same reasonable
|
|
809
|
-
// window, but they never enter
|
|
808
|
+
// window, but they never enter workflow-stall candidate filtering.
|
|
810
809
|
return run.deliveredAt === undefined
|
|
811
810
|
? ageMs >= windowMs
|
|
812
|
-
: ageMs >=
|
|
811
|
+
: ageMs >= DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS;
|
|
813
812
|
}
|
|
814
813
|
function classifyLeaderStall(store, taskId, observed, now, windowMs) {
|
|
815
814
|
if (store.hasOpenInputRequest(taskId))
|
|
@@ -850,7 +849,6 @@ function classifyLeaderStall(store, taskId, observed, now, windowMs) {
|
|
|
850
849
|
}
|
|
851
850
|
const LEADER_ACTION_PROGRESS_TYPES = new Set([
|
|
852
851
|
RUN_PROGRESS_EVENT,
|
|
853
|
-
"runtime.provider-turn-progress",
|
|
854
852
|
"input.answered",
|
|
855
853
|
"input.auto-answered",
|
|
856
854
|
"input.cancelled",
|
|
@@ -884,8 +882,7 @@ function latestLeaderActionProgressAt(store, taskId, runId, startedAt, batch, no
|
|
|
884
882
|
|| createdMs < startedMs
|
|
885
883
|
|| createdMs > now.getTime())
|
|
886
884
|
continue;
|
|
887
|
-
const value =
|
|
888
|
-
|| event.type === "runtime.provider-turn-progress")
|
|
885
|
+
const value = event.type === RUN_PROGRESS_EVENT
|
|
889
886
|
&& typeof event.payload.progressAt === "string"
|
|
890
887
|
&& Number.isFinite(Date.parse(event.payload.progressAt))
|
|
891
888
|
? event.payload.progressAt
|
|
@@ -996,55 +993,6 @@ function resourceEvidenceMatchesCurrentRun(resource, expected, progressAt) {
|
|
|
996
993
|
function hasResourceIdentityText(value) {
|
|
997
994
|
return typeof value === "string" && value.trim().length > 0;
|
|
998
995
|
}
|
|
999
|
-
/** Persist the first advisory sample and keep it bounded by the semantic gap. */
|
|
1000
|
-
async function consumeResourceEvidence(store, snapshot, taskId, roleName, runId, progressAt, expectedIdentity, suppressionKeys, now) {
|
|
1001
|
-
const resource = resourceForRun(snapshot, taskId, roleName, runId);
|
|
1002
|
-
if (resource === undefined)
|
|
1003
|
-
return undefined;
|
|
1004
|
-
if (!resourceEvidenceMatchesCurrentRun(resource, expectedIdentity, progressAt)) {
|
|
1005
|
-
return resource;
|
|
1006
|
-
}
|
|
1007
|
-
if (resource.active !== true || resource.changed !== true)
|
|
1008
|
-
return resource;
|
|
1009
|
-
const key = `${taskId}\0${roleName}\0${runId}\0${progressAt}`;
|
|
1010
|
-
if (store.recordRoleRunResourceSuppression !== undefined) {
|
|
1011
|
-
const persisted = store.recordRoleRunResourceSuppression({
|
|
1012
|
-
taskId,
|
|
1013
|
-
roleName,
|
|
1014
|
-
runId,
|
|
1015
|
-
agentId: expectedIdentity.agentId,
|
|
1016
|
-
adapterId: expectedIdentity.adapterId,
|
|
1017
|
-
...(expectedIdentity.nativeSessionId === undefined
|
|
1018
|
-
? {}
|
|
1019
|
-
: { nativeSessionId: expectedIdentity.nativeSessionId }),
|
|
1020
|
-
...(expectedIdentity.launchId === undefined
|
|
1021
|
-
? {}
|
|
1022
|
-
: { launchId: expectedIdentity.launchId }),
|
|
1023
|
-
progressAt,
|
|
1024
|
-
observedAt: resource.observedAt,
|
|
1025
|
-
now
|
|
1026
|
-
});
|
|
1027
|
-
if (persisted === "recorded" || persisted === "already-recorded") {
|
|
1028
|
-
return resourceFallsWithinSemanticGap(progressAt, now)
|
|
1029
|
-
? resource
|
|
1030
|
-
: { ...resource, active: false, changed: false };
|
|
1031
|
-
}
|
|
1032
|
-
// A concurrent Run/session change invalidates this sample. Do not let the
|
|
1033
|
-
// stale changed bit suppress the current Run's attention episode.
|
|
1034
|
-
return { ...resource, active: false, changed: false };
|
|
1035
|
-
}
|
|
1036
|
-
if (suppressionKeys === undefined || !suppressionKeys.has(key)) {
|
|
1037
|
-
suppressionKeys?.add(key);
|
|
1038
|
-
return resource;
|
|
1039
|
-
}
|
|
1040
|
-
return { ...resource, active: false, changed: false };
|
|
1041
|
-
}
|
|
1042
|
-
function resourceFallsWithinSemanticGap(progressAt, now) {
|
|
1043
|
-
const progressMs = Date.parse(progressAt);
|
|
1044
|
-
return Number.isFinite(progressMs)
|
|
1045
|
-
&& progressMs <= now.getTime()
|
|
1046
|
-
&& now.getTime() - progressMs < DEFAULT_RESOURCE_ONLY_SEMANTIC_GAP_MS;
|
|
1047
|
-
}
|
|
1048
996
|
function resourceEvidenceIsFresh(evidence, now, windowMs) {
|
|
1049
997
|
if (evidence === undefined)
|
|
1050
998
|
return false;
|
|
@@ -1053,5 +1001,5 @@ function resourceEvidenceIsFresh(evidence, now, windowMs) {
|
|
|
1053
1001
|
return false;
|
|
1054
1002
|
// A sample from an earlier scheduler window is not a current health signal.
|
|
1055
1003
|
return observedAt <= now.getTime()
|
|
1056
|
-
&& now.getTime() - observedAt < Math.max(windowMs,
|
|
1004
|
+
&& now.getTime() - observedAt < Math.max(windowMs, DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS);
|
|
1057
1005
|
}
|
|
@@ -8,6 +8,8 @@ const FINAL_REVIEW_AGGREGATE_FROM_VERSION = 16;
|
|
|
8
8
|
const FINAL_REVIEW_AGGREGATE_TO_VERSION = 17;
|
|
9
9
|
const HOME_IDENTITY_AGGREGATE_FROM_VERSION = 17;
|
|
10
10
|
const HOME_IDENTITY_AGGREGATE_TO_VERSION = 18;
|
|
11
|
+
const RUNTIME_OBSERVATION_AGGREGATE_FROM_VERSION = 18;
|
|
12
|
+
const RUNTIME_OBSERVATION_AGGREGATE_TO_VERSION = 19;
|
|
11
13
|
const SQLITE_LAYOUT_FROM_VERSION = 6;
|
|
12
14
|
const SQLITE_LAYOUT_TO_VERSION = 7;
|
|
13
15
|
const PROJECT_FROM_VERSION = 2;
|
|
@@ -87,6 +89,14 @@ export function createProductionStorageRegistry() {
|
|
|
87
89
|
preconditions: requireAggregateV17Snapshot,
|
|
88
90
|
transform: migrateAggregateV17ToV18,
|
|
89
91
|
declaredEffects: []
|
|
92
|
+
})
|
|
93
|
+
.registerOfflineMigration({
|
|
94
|
+
axis: "aggregate",
|
|
95
|
+
fromVersion: RUNTIME_OBSERVATION_AGGREGATE_FROM_VERSION,
|
|
96
|
+
toVersion: RUNTIME_OBSERVATION_AGGREGATE_TO_VERSION,
|
|
97
|
+
preconditions: requireAggregateV18Snapshot,
|
|
98
|
+
transform: migrateAggregateV18ToV19,
|
|
99
|
+
declaredEffects: []
|
|
90
100
|
})
|
|
91
101
|
.registerOfflineMigration(projectOwnershipStep())
|
|
92
102
|
.registerOfflineMigration(taskWorkspaceIdentityStep())
|
|
@@ -1290,6 +1300,81 @@ function requireAggregateV17Snapshot(snapshot) {
|
|
|
1290
1300
|
throw new Error("Aggregate 17->18 migration requires state.json schemaVersion 17 to match schema.json.");
|
|
1291
1301
|
}
|
|
1292
1302
|
}
|
|
1303
|
+
/**
|
|
1304
|
+
* Runtime state is now projected exclusively from canonical
|
|
1305
|
+
* `runtime.observation` events. Offline upgrade inventory proves there are no
|
|
1306
|
+
* active Runs or live Sessions. A retired Task has also explicitly abandoned
|
|
1307
|
+
* its runtime, so its stored runtime inconsistencies no longer block the Home;
|
|
1308
|
+
* the anomalous records themselves remain available as history. Non-retired
|
|
1309
|
+
* Tasks still fail closed and receive the supported retirement command.
|
|
1310
|
+
*/
|
|
1311
|
+
function migrateAggregateV18ToV19(snapshot) {
|
|
1312
|
+
requireAggregateV18Snapshot(snapshot);
|
|
1313
|
+
const schemaManifest = {
|
|
1314
|
+
...snapshot.schemaManifest,
|
|
1315
|
+
aggregateSchemaVersion: RUNTIME_OBSERVATION_AGGREGATE_TO_VERSION
|
|
1316
|
+
};
|
|
1317
|
+
if (snapshot.state === null)
|
|
1318
|
+
return { schemaManifest, state: null };
|
|
1319
|
+
const tasks = asObject(snapshot.state.tasks, "state tasks");
|
|
1320
|
+
const nextTasks = {};
|
|
1321
|
+
for (const [taskId, rawStoredTask] of Object.entries(tasks)) {
|
|
1322
|
+
const storedTask = asObject(rawStoredTask, `Task aggregate ${taskId}`);
|
|
1323
|
+
const rawTask = storedTask.task;
|
|
1324
|
+
const task = rawTask !== null && typeof rawTask === "object" && !Array.isArray(rawTask)
|
|
1325
|
+
? rawTask
|
|
1326
|
+
: undefined;
|
|
1327
|
+
if (task?.status === "retired") {
|
|
1328
|
+
nextTasks[taskId] = { ...storedTask };
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
requireResolvableActiveRunPointers(taskId, storedTask);
|
|
1332
|
+
nextTasks[taskId] = { ...storedTask };
|
|
1333
|
+
}
|
|
1334
|
+
return {
|
|
1335
|
+
schemaManifest,
|
|
1336
|
+
state: {
|
|
1337
|
+
...snapshot.state,
|
|
1338
|
+
schemaVersion: RUNTIME_OBSERVATION_AGGREGATE_TO_VERSION,
|
|
1339
|
+
tasks: nextTasks
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
function requireResolvableActiveRunPointers(taskId, storedTask) {
|
|
1344
|
+
if (storedTask.activeRuns === undefined)
|
|
1345
|
+
return;
|
|
1346
|
+
const activeRuns = asObject(storedTask.activeRuns, `activeRunPointer map ${taskId}`);
|
|
1347
|
+
if (Object.keys(activeRuns).length === 0)
|
|
1348
|
+
return;
|
|
1349
|
+
const agentRuns = asObject(storedTask.agentRuns, `agentRun map ${taskId}`);
|
|
1350
|
+
for (const [pointer, rawActiveRun] of Object.entries(activeRuns)) {
|
|
1351
|
+
const activeRun = asObject(rawActiveRun, `Active run ${taskId}/${pointer}`);
|
|
1352
|
+
const runId = typeof activeRun.runId === "string" ? activeRun.runId.trim() : "";
|
|
1353
|
+
if (runId.length === 0) {
|
|
1354
|
+
throw new Error(`Active run pointer ${taskId}/${pointer} has an invalid runId. `
|
|
1355
|
+
+ taskRetirementUpgradeHint(taskId));
|
|
1356
|
+
}
|
|
1357
|
+
if (agentRuns[runId] === undefined) {
|
|
1358
|
+
throw new Error(`Active run pointer ${taskId}/${pointer} references missing agent run ${runId}. `
|
|
1359
|
+
+ taskRetirementUpgradeHint(taskId));
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
function taskRetirementUpgradeHint(taskId) {
|
|
1364
|
+
return `Retire Task ${taskId} with `
|
|
1365
|
+
+ `\`yui task retire ${taskId} --summary "abandon inconsistent runtime state"\`, `
|
|
1366
|
+
+ "then retry `yui update`.";
|
|
1367
|
+
}
|
|
1368
|
+
function requireAggregateV18Snapshot(snapshot) {
|
|
1369
|
+
if (snapshot.schemaManifest.aggregateSchemaVersion
|
|
1370
|
+
!== RUNTIME_OBSERVATION_AGGREGATE_FROM_VERSION) {
|
|
1371
|
+
throw new Error("Aggregate 18->19 migration requires schema.json aggregateSchemaVersion 18.");
|
|
1372
|
+
}
|
|
1373
|
+
if (snapshot.state !== null
|
|
1374
|
+
&& snapshot.state.schemaVersion !== RUNTIME_OBSERVATION_AGGREGATE_FROM_VERSION) {
|
|
1375
|
+
throw new Error("Aggregate 18->19 migration requires state.json schemaVersion 18 to match schema.json.");
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1293
1378
|
/**
|
|
1294
1379
|
* Project ownership is a new required field. Every pre-v3 Project is a
|
|
1295
1380
|
* user-registered checkout, so the historical binding is `external`; a managed
|
|
@@ -25,8 +25,8 @@
|
|
|
25
25
|
*
|
|
26
26
|
* Records are stored as full versioned JSON in `payload` columns, with typed
|
|
27
27
|
* columns for the fields that are queried/filtered/used-for-CAS (§4). A
|
|
28
|
-
* high-frequency
|
|
29
|
-
*
|
|
28
|
+
* high-frequency runtime telemetry observation is a single-row upsert into
|
|
29
|
+
* `telemetry` scoped by its primary key — it never rewrites global
|
|
30
30
|
* state and never touches another Task's rows (§4.4).
|
|
31
31
|
*
|
|
32
32
|
* The in-process store is phase 1 of §6. It does not re-run the heavy record
|
|
@@ -1542,7 +1542,7 @@ export class SqliteTaskStore {
|
|
|
1542
1542
|
/**
|
|
1543
1543
|
* Upsert one progress row. The PK is (task_id, role_name, run_id, generation,
|
|
1544
1544
|
* progress_id): a repeated progress id updates in place, so a high-frequency
|
|
1545
|
-
*
|
|
1545
|
+
* runtime telemetry observation is a single-row write that never
|
|
1546
1546
|
* rewrites global state or another Task's rows.
|
|
1547
1547
|
*/
|
|
1548
1548
|
upsertTelemetryProgress(entry) {
|
|
@@ -37,7 +37,7 @@ import { planMigration } from "../migration/planner.js";
|
|
|
37
37
|
import { AmbiguousSwitchError } from "../migration/index.js";
|
|
38
38
|
import { describeActiveRuntime, homeRuntimeIsActive, inspectHomeRuntime, inspectSourceVersionState, inspectSnapshotVersionState } from "./homeMigrationTarget.js";
|
|
39
39
|
import { writeSwitchProgress } from "./switchProgress.js";
|
|
40
|
-
import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState, readStateFromSqlite } from "./sqliteStateMigration.js";
|
|
40
|
+
import { COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME, copySqlitePassthroughState, computeDbFamilyChecksums, computeStateFamilyChecksums, populateSqliteFromState, readStateFromSqlite } from "./sqliteStateMigration.js";
|
|
41
41
|
/** Build the SQLite-backed record-migration target. */
|
|
42
42
|
export function createSqliteRecordMigrationTarget(options) {
|
|
43
43
|
const home = options.home;
|
|
@@ -92,6 +92,7 @@ export function createSqliteRecordMigrationTarget(options) {
|
|
|
92
92
|
updatedAt: now().toISOString()
|
|
93
93
|
};
|
|
94
94
|
populateSqliteFromState(home, snapshot.state ?? {}, STAGED_DATABASE_FILENAME);
|
|
95
|
+
copySqlitePassthroughState(home, COMMITTED_DATABASE_FILENAME, STAGED_DATABASE_FILENAME);
|
|
95
96
|
},
|
|
96
97
|
rebuildDerivedState(effects) {
|
|
97
98
|
// The SQLite database is fully normalised by populateSqliteFromState;
|
|
@@ -33,6 +33,90 @@ import { CURRENT_STORED_TASK_SCHEMA_VERSION } from "../taskStore.js";
|
|
|
33
33
|
export const STAGED_DATABASE_FILENAME = "yui.db.staged";
|
|
34
34
|
/** The committed database filename. */
|
|
35
35
|
export const COMMITTED_DATABASE_FILENAME = "yui.db";
|
|
36
|
+
/**
|
|
37
|
+
* Preserve SQLite-owned durable state that intentionally sits outside the
|
|
38
|
+
* state.json-shaped Task aggregate snapshot. Volatile coordination locks and
|
|
39
|
+
* derived projections are rebuilt or dropped at the offline boundary.
|
|
40
|
+
*/
|
|
41
|
+
export function copySqlitePassthroughState(home, sourceDatabaseFilename, targetDatabaseFilename) {
|
|
42
|
+
if (sourceDatabaseFilename === targetDatabaseFilename) {
|
|
43
|
+
throw new Error("SQLite passthrough copy requires distinct source and target databases.");
|
|
44
|
+
}
|
|
45
|
+
const source = new Database(join(home, sourceDatabaseFilename), { readonly: true });
|
|
46
|
+
const target = new Database(join(home, targetDatabaseFilename));
|
|
47
|
+
try {
|
|
48
|
+
target.pragma("foreign_keys = ON");
|
|
49
|
+
target.transaction(() => {
|
|
50
|
+
mergeGlobalSequences(source, target);
|
|
51
|
+
copyTableRows(source, target, "outbox");
|
|
52
|
+
copyMailboxSignals(source, target);
|
|
53
|
+
copyTableRows(source, target, "work_item_candidates");
|
|
54
|
+
copyTableRows(source, target, "review_findings");
|
|
55
|
+
copyTableRows(source, target, "telemetry");
|
|
56
|
+
if (sqliteTableExists(source, "telemetry_aggregate")) {
|
|
57
|
+
target.exec("DELETE FROM telemetry_aggregate");
|
|
58
|
+
copyTableRows(source, target, "telemetry_aggregate");
|
|
59
|
+
}
|
|
60
|
+
copyTableRows(source, target, "session_owners");
|
|
61
|
+
copyTableRows(source, target, "resource_registry");
|
|
62
|
+
copyTableRows(source, target, "gate_artifacts");
|
|
63
|
+
copyTableRows(source, target, "gate_artifact_logs");
|
|
64
|
+
})();
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
source.close();
|
|
68
|
+
target.close();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
function mergeGlobalSequences(source, target) {
|
|
72
|
+
if (!sqliteTableExists(source, "global_sequences"))
|
|
73
|
+
return;
|
|
74
|
+
const rows = source.prepare("SELECT name, high_water FROM global_sequences").all();
|
|
75
|
+
const merge = target.prepare(`INSERT INTO global_sequences (name, high_water) VALUES (?, ?)
|
|
76
|
+
ON CONFLICT(name) DO UPDATE SET
|
|
77
|
+
high_water = MAX(global_sequences.high_water, excluded.high_water)`);
|
|
78
|
+
for (const row of rows)
|
|
79
|
+
merge.run(row.name, row.high_water);
|
|
80
|
+
}
|
|
81
|
+
function copyMailboxSignals(source, target) {
|
|
82
|
+
if (!sqliteTableExists(source, "mailbox_signals"))
|
|
83
|
+
return;
|
|
84
|
+
const rows = source.prepare(`SELECT m.target_key, s.sequence, s.reason, s.ref_type, s.ref_task_id,
|
|
85
|
+
s.ref_id, s.occurred_at, s.request_id
|
|
86
|
+
FROM mailbox_signals s
|
|
87
|
+
JOIN mailboxes m ON m.mailbox_id = s.mailbox_id
|
|
88
|
+
ORDER BY m.target_key, s.sequence`).iterate();
|
|
89
|
+
const findMailbox = target.prepare("SELECT mailbox_id FROM mailboxes WHERE target_key = ?");
|
|
90
|
+
const insert = target.prepare(`INSERT INTO mailbox_signals
|
|
91
|
+
(mailbox_id, sequence, reason, ref_type, ref_task_id, ref_id, occurred_at, request_id)
|
|
92
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
93
|
+
for (const row of rows) {
|
|
94
|
+
const mailbox = findMailbox.get(row.target_key);
|
|
95
|
+
if (mailbox === undefined) {
|
|
96
|
+
throw new Error(`SQLite migration cannot preserve signals for missing mailbox ${row.target_key}.`);
|
|
97
|
+
}
|
|
98
|
+
insert.run(mailbox.mailbox_id, row.sequence, row.reason, row.ref_type, row.ref_task_id, row.ref_id, row.occurred_at, row.request_id);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function copyTableRows(source, target, table) {
|
|
102
|
+
if (!sqliteTableExists(source, table) || !sqliteTableExists(target, table))
|
|
103
|
+
return;
|
|
104
|
+
const columns = source.prepare(`PRAGMA table_info(${quoteSqliteIdentifier(table)})`).all().map((row) => row.name);
|
|
105
|
+
if (columns.length === 0)
|
|
106
|
+
return;
|
|
107
|
+
const quotedColumns = columns.map(quoteSqliteIdentifier);
|
|
108
|
+
const rows = source.prepare(`SELECT ${quotedColumns.join(", ")} FROM ${quoteSqliteIdentifier(table)}`).iterate();
|
|
109
|
+
const insert = target.prepare(`INSERT INTO ${quoteSqliteIdentifier(table)} (${quotedColumns.join(", ")}) `
|
|
110
|
+
+ `VALUES (${columns.map(() => "?").join(", ")})`);
|
|
111
|
+
for (const row of rows)
|
|
112
|
+
insert.run(...columns.map((column) => row[column]));
|
|
113
|
+
}
|
|
114
|
+
function sqliteTableExists(db, table) {
|
|
115
|
+
return db.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) !== undefined;
|
|
116
|
+
}
|
|
117
|
+
function quoteSqliteIdentifier(value) {
|
|
118
|
+
return `"${value.replaceAll("\"", "\"\"")}"`;
|
|
119
|
+
}
|
|
36
120
|
// ---------------------------------------------------------------------------
|
|
37
121
|
// Canonical JSON and hashing
|
|
38
122
|
// ---------------------------------------------------------------------------
|
|
@@ -157,6 +241,7 @@ function tasksOf(state) {
|
|
|
157
241
|
* sidecar database file.
|
|
158
242
|
*/
|
|
159
243
|
export function populateSqliteFromState(home, state, databaseFilename) {
|
|
244
|
+
const retiredActiveRuns = [];
|
|
160
245
|
const store = new SqliteTaskStore(home, { databaseFilename, migration: true });
|
|
161
246
|
try {
|
|
162
247
|
store.transaction(() => {
|
|
@@ -234,6 +319,17 @@ export function populateSqliteFromState(home, state, databaseFilename) {
|
|
|
234
319
|
// Active-run pointers: the document stores { schemaVersion, runId }
|
|
235
320
|
// keyed by pointer; the store derives the pointer from the Run.
|
|
236
321
|
for (const [pointer, value] of Object.entries(stored.activeRuns)) {
|
|
322
|
+
if (stored.task.status === "retired") {
|
|
323
|
+
retiredActiveRuns.push({
|
|
324
|
+
taskId,
|
|
325
|
+
pointer,
|
|
326
|
+
value,
|
|
327
|
+
updatedAt: typeof stored.task.updatedAt === "string"
|
|
328
|
+
? stored.task.updatedAt
|
|
329
|
+
: new Date().toISOString()
|
|
330
|
+
});
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
237
333
|
const run = stored.agentRuns[value.runId];
|
|
238
334
|
if (run === undefined) {
|
|
239
335
|
throw new Error(`Active run pointer ${taskId}/${pointer} references missing agent run ${value.runId}.`);
|
|
@@ -291,6 +387,33 @@ export function populateSqliteFromState(home, state, databaseFilename) {
|
|
|
291
387
|
finally {
|
|
292
388
|
store.close();
|
|
293
389
|
}
|
|
390
|
+
persistRetiredActiveRunPointers(home, databaseFilename, retiredActiveRuns);
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* A retired Task is an explicit isolation boundary. Its active-run rows are
|
|
394
|
+
* retained byte-for-byte at the logical record level even when their Run is
|
|
395
|
+
* missing; normal Tasks continue through the referentially strict store path.
|
|
396
|
+
*/
|
|
397
|
+
function persistRetiredActiveRunPointers(home, databaseFilename, pointers) {
|
|
398
|
+
if (pointers.length === 0)
|
|
399
|
+
return;
|
|
400
|
+
const db = new Database(join(home, databaseFilename));
|
|
401
|
+
try {
|
|
402
|
+
const insert = db.prepare(`INSERT INTO active_runs (task_id, pointer, run_id, payload, updated_at)
|
|
403
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
404
|
+
db.transaction(() => {
|
|
405
|
+
for (const entry of pointers) {
|
|
406
|
+
if (typeof entry.value.runId !== "string") {
|
|
407
|
+
throw new Error(`Retired Task active run pointer ${entry.taskId}/${entry.pointer} `
|
|
408
|
+
+ "cannot be represented because runId is not a string.");
|
|
409
|
+
}
|
|
410
|
+
insert.run(entry.taskId, entry.pointer, entry.value.runId, JSON.stringify(entry.value), entry.updatedAt);
|
|
411
|
+
}
|
|
412
|
+
})();
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
db.close();
|
|
416
|
+
}
|
|
294
417
|
}
|
|
295
418
|
/**
|
|
296
419
|
* Seed `global_sequences` from the numeric suffixes of existing task and
|