@zq-silk/yui 0.11.3 → 0.12.1
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 +23 -4
- package/README.md +44 -22
- package/dist/cli/commandCatalog.js +13 -10
- package/dist/cli/interactionPolicy.js +5 -6
- package/dist/cli/updateCommand.js +3 -1
- package/dist/cli/updateOrchestrator.js +173 -28
- package/dist/cli/updatePorts.js +137 -8
- package/dist/cli/upgradeCommand.js +19 -9
- package/dist/cli.js +156 -57
- package/dist/commands/configCommands.js +13 -46
- package/dist/commands/executionAuditCommands.js +3 -2
- package/dist/commands/jobCommands.js +4 -10
- package/dist/commands/taskCommands.js +234 -200
- package/dist/commands/taskCompletionGate.js +29 -54
- package/dist/commands/taskContextCommand.js +61 -13
- package/dist/commands/taskInputCommands.js +8 -5
- package/dist/commands/taskNextActionCommand.js +56 -6
- package/dist/commands/taskOverviewCommand.js +17 -31
- package/dist/commands/taskRoleRuntimeStatus.js +55 -7
- package/dist/commands/taskWorkspaceCommands.js +5 -2
- package/dist/config/configCatalog.js +3 -3
- package/dist/config/yuiConfig.js +3 -7
- package/dist/context/wakeNotification.js +20 -5
- package/dist/controller/agentRuntimeObserver.js +247 -51
- package/dist/controller/clientRuntime.js +39 -3
- package/dist/controller/controller.js +31 -27
- package/dist/controller/fileSchedulerStoreAdapter.js +390 -332
- package/dist/controller/runtime.js +58 -3
- package/dist/controller/runtimeEventInbox.js +17 -7
- package/dist/controller/runtimeEventProcessor.js +12 -19
- package/dist/controller/runtimeHookRunFence.js +19 -4
- package/dist/controller/runtimeObservationHook.js +45 -0
- package/dist/controller/structuredProviderObservation.js +20 -3
- package/dist/core/controllerClient.js +20 -2
- package/dist/core/controllerServer.js +1 -0
- package/dist/execution/resourceBroker.js +5 -4
- package/dist/executor/agentExecutor.js +52 -50
- package/dist/executor/effectiveLaunch.js +8 -48
- package/dist/executor/fileRoleLaunchPlanner.js +129 -45
- package/dist/executor/workspacePreflightClassification.js +23 -2
- package/dist/interaction/operatorPresentation.js +33 -89
- package/dist/lifecycle/exactRunTerminalization.js +69 -5
- package/dist/observability/executionAudit.js +5 -0
- package/dist/observability/orchestrationMetrics.js +8 -3
- package/dist/profile/agentProfile.js +1 -1
- package/dist/release/cliHomeReleaseFence.js +123 -0
- package/dist/release/runtimeRelease.js +20 -0
- package/dist/repository/taskWorkspacePreparer.js +176 -60
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/review/deltaRecheck.js +12 -51
- package/dist/review/reviewAcceptance.js +26 -0
- package/dist/review/reviewConfig.js +0 -31
- package/dist/review/reviewDecision.js +113 -0
- package/dist/review/reviewOutcomeClassifier.js +1 -1
- package/dist/review/reviewRound.js +1 -1
- package/dist/review/reviewerAvailability.js +69 -0
- package/dist/run/recoveryProjection.js +45 -6
- package/dist/runtime/agentDriverObservation.js +24 -10
- package/dist/runtime/agentHost.js +159 -85
- package/dist/runtime/builtinAgentDrivers.js +4 -3
- package/dist/runtime/builtinTranscriptObserver.js +301 -64
- package/dist/runtime/builtinTranscriptUsage.js +9 -7
- package/dist/runtime/conversationSwitch.js +277 -0
- package/dist/runtime/index.js +2 -1
- package/dist/runtime/launchBroker.js +12 -0
- package/dist/runtime/processExitOutbox.js +88 -0
- package/dist/runtime/providerRuntimeIdentity.js +29 -1
- package/dist/runtime/runtimeHealthPolicy.js +5 -5
- package/dist/runtime/runtimeObservation.js +28 -0
- package/dist/runtime/runtimeProjection.js +22 -32
- package/dist/runtime/sessionTokenMetrics.js +181 -0
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +4 -1
- package/dist/scheduler/activeRoleRunDelivery.js +34 -199
- package/dist/scheduler/activeTaskProgress.js +7 -6
- package/dist/scheduler/leaderWakeupProcessor.js +42 -138
- package/dist/scheduler/operatorEvent.js +34 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +54 -94
- package/dist/scheduler/roleRunStall.js +53 -31
- package/dist/scheduler/taskExecutionProjection.js +97 -76
- package/dist/scheduler/taskObservabilityProjection.js +10 -11
- package/dist/storage/migration/productionRegistry.js +379 -0
- package/dist/storage/sqliteSchema.js +66 -23
- package/dist/storage/sqliteStore.js +46 -23
- package/dist/storage/storeRpc.js +0 -1
- package/dist/storage/taskStore.js +15 -30
- package/dist/storage/upgrade/homeClassification.js +52 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
- package/dist/storage/upgrade/recordVersions.js +3 -2
- package/dist/storage/upgrade/sqliteMigrationTarget.js +30 -8
- package/dist/storage/upgrade/sqliteRecordMigrationTarget.js +23 -11
- package/dist/storage/upgrade/sqliteStateMigration.js +66 -8
- package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
- package/dist/task/completionReadiness.js +10 -6
- package/dist/task/nextAction.js +82 -58
- package/dist/telemetry/sqliteTelemetryStore.js +1 -1
- package/dist/web/assets/client/components.js +13 -3
- package/dist/web/assets/client/i18n.js +6 -2
- package/dist/web/webSnapshot.js +7 -1
- package/i18n/README.zh-CN.md +2 -2
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +60 -20
- package/skills/yui-operator/SKILL.md +13 -4
- package/skills/yui-reviewer/SKILL.md +35 -11
- package/dist/context/sessionContextBudget.js +0 -71
- package/dist/lifecycle/contextBudgetRollover.js +0 -81
- package/dist/lifecycle/taskRoleSessionReset.js +0 -126
- package/dist/scheduler/operatorNotification.js +0 -59
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
3
|
-
import { createRuntimeObservation,
|
|
3
|
+
import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
4
4
|
const DEFAULT_MAX_CONCURRENT_SAMPLES = 8;
|
|
5
5
|
const MAX_CONCURRENT_SAMPLES = 64;
|
|
6
6
|
/**
|
|
@@ -31,20 +31,30 @@ export class AgentRuntimeObserver {
|
|
|
31
31
|
const dirty = new Set();
|
|
32
32
|
const sequenceBase = this.#sequence;
|
|
33
33
|
this.#sequence += active.length;
|
|
34
|
-
await forEachConcurrent(active, this.#maxConcurrentSamples, async ({ key, fence, source,
|
|
34
|
+
await forEachConcurrent(active, this.#maxConcurrentSamples, async ({ key, fence, source, persistedState }, index) => {
|
|
35
35
|
const existingState = this.#states.get(key);
|
|
36
36
|
// Cursor state is intentionally process-local, but the latest canonical
|
|
37
37
|
// usage/activity baseline is durable. Rehydrate it after Controller
|
|
38
38
|
// restart so rereading the bounded transcript tail cannot manufacture a
|
|
39
39
|
// fresh activity edge from tokens that were already observed.
|
|
40
40
|
const state = existingState ?? { ...persistedState };
|
|
41
|
+
const restoredUsageEventId = existingState === undefined
|
|
42
|
+
? persistedState.usageEventId
|
|
43
|
+
: undefined;
|
|
41
44
|
const driver = this.drivers.require(fence.driverId);
|
|
42
45
|
const observer = driver.runtime.observer;
|
|
43
46
|
if (observer === undefined)
|
|
44
47
|
return;
|
|
45
48
|
let sample;
|
|
46
49
|
try {
|
|
47
|
-
sample = await observer.sample(source, state.cursor
|
|
50
|
+
sample = await observer.sample(source, state.cursor, {
|
|
51
|
+
...(state.usageOccurrenceId === undefined
|
|
52
|
+
? {}
|
|
53
|
+
: { latestOccurrenceId: state.usageOccurrenceId }),
|
|
54
|
+
...(state.usageOccurrenceCheckpoint === undefined
|
|
55
|
+
? {}
|
|
56
|
+
: { latestCheckpoint: state.usageOccurrenceCheckpoint })
|
|
57
|
+
});
|
|
48
58
|
}
|
|
49
59
|
catch (error) {
|
|
50
60
|
sample = Object.freeze({
|
|
@@ -59,37 +69,70 @@ export class AgentRuntimeObserver {
|
|
|
59
69
|
// latency can change completion order without changing observation
|
|
60
70
|
// identity or the canonical sequence assigned to a source.
|
|
61
71
|
const sequence = sequenceBase + index;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
let usages = sample.usages ?? [];
|
|
73
|
+
if (state.usage === undefined) {
|
|
74
|
+
// Cumulative counters need a lower bound. An exact initial sample
|
|
75
|
+
// proves the complete Session-generation history even when the
|
|
76
|
+
// current Run resumed that Session, so preserve every occurrence and
|
|
77
|
+
// anchor it at zero. A partial sample cannot prove the omitted request
|
|
78
|
+
// boundaries; retain its latest total as a partial baseline, plus the
|
|
79
|
+
// smallest ordered witness if the visible counter already rolled back.
|
|
80
|
+
// Request snapshots already describe one complete occurrence and must
|
|
81
|
+
// not be mixed with a synthetic cumulative fact.
|
|
82
|
+
const usageOccurrences = usages.filter(hasUsageSnapshot);
|
|
83
|
+
const latestOccurrence = usageOccurrences.at(-1);
|
|
84
|
+
const completeHistory = latestOccurrence?.usage.semantics === "cumulative-session"
|
|
85
|
+
&& usages.every(({ observationQuality }) => observationQuality !== "partial");
|
|
86
|
+
const rollbackWitness = latestOccurrence?.usage.semantics === "cumulative-session"
|
|
87
|
+
&& !completeHistory
|
|
88
|
+
? cumulativeRollbackWitness(usageOccurrences)
|
|
89
|
+
: [];
|
|
90
|
+
const baseline = latestOccurrence?.usage.semantics !== "cumulative-session"
|
|
91
|
+
? undefined
|
|
92
|
+
: completeHistory
|
|
93
|
+
? Object.freeze({
|
|
94
|
+
semantics: "cumulative-session",
|
|
95
|
+
inputTokens: 0,
|
|
96
|
+
outputTokens: 0
|
|
97
|
+
})
|
|
98
|
+
: latestOccurrence.usage;
|
|
99
|
+
if (baseline !== undefined && rollbackWitness.length === 0) {
|
|
100
|
+
const baselineKey = completeHistory ? "zero" : latestOccurrence.occurrenceId;
|
|
101
|
+
const identity = completeHistory
|
|
102
|
+
? tokenObservationIdentity("baseline", fence, source.sourceId, baselineKey)
|
|
103
|
+
: usageObservationIdentity(fence, source.sourceId, latestOccurrence);
|
|
78
104
|
this.inbox.enqueueObservation(createRuntimeObservation({
|
|
79
105
|
schemaVersion: 2,
|
|
80
|
-
eventId:
|
|
81
|
-
semanticKey:
|
|
106
|
+
eventId: identity.eventId,
|
|
107
|
+
semanticKey: identity.semanticKey,
|
|
82
108
|
kind: "activity.observed",
|
|
83
|
-
authority:
|
|
109
|
+
authority: completeHistory ? "controller" : "driver-inferred",
|
|
84
110
|
receivedAt: at,
|
|
85
111
|
sequence,
|
|
86
112
|
ordinal: 1,
|
|
87
113
|
fence,
|
|
88
|
-
payload: {
|
|
114
|
+
payload: {
|
|
115
|
+
activity: "model",
|
|
116
|
+
sourceId: source.sourceId,
|
|
117
|
+
usage: baseline,
|
|
118
|
+
...(completeHistory
|
|
119
|
+
? {}
|
|
120
|
+
: { observationQuality: "partial" })
|
|
121
|
+
}
|
|
89
122
|
}));
|
|
90
123
|
state.usage = baseline;
|
|
91
|
-
|
|
124
|
+
state.usageEventId = identity.eventId;
|
|
125
|
+
state.usageOccurrenceId = completeHistory
|
|
126
|
+
? undefined
|
|
127
|
+
: latestOccurrence.occurrenceId;
|
|
128
|
+
state.usageOccurrenceCheckpoint = completeHistory
|
|
129
|
+
? undefined
|
|
130
|
+
: latestOccurrence.resumeCheckpoint;
|
|
131
|
+
if (!completeHistory)
|
|
132
|
+
usages = [];
|
|
92
133
|
}
|
|
134
|
+
if (rollbackWitness.length > 0)
|
|
135
|
+
usages = rollbackWitness;
|
|
93
136
|
}
|
|
94
137
|
const health = JSON.stringify([sample.status, sample.detail ?? null]);
|
|
95
138
|
if (state.health !== health) {
|
|
@@ -112,32 +155,81 @@ export class AgentRuntimeObserver {
|
|
|
112
155
|
state.health = health;
|
|
113
156
|
dirty.add(`role:${fence.taskId}/${fence.roleName}`);
|
|
114
157
|
}
|
|
115
|
-
const usageChanged = sample.usage !== undefined
|
|
116
|
-
&& !sameUsage(state.usage, sample.usage);
|
|
117
158
|
const activityChanged = sample.activityId !== undefined
|
|
118
159
|
&& sample.activityId !== state.activityId;
|
|
119
|
-
if (
|
|
120
|
-
|
|
160
|
+
if (restoredUsageEventId !== undefined) {
|
|
161
|
+
let persistedIndex = -1;
|
|
162
|
+
for (let usageIndex = usages.length - 1; usageIndex >= 0; usageIndex -= 1) {
|
|
163
|
+
if (usageObservationIdentity(fence, source.sourceId, usages[usageIndex]).eventId
|
|
164
|
+
=== restoredUsageEventId) {
|
|
165
|
+
persistedIndex = usageIndex;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (persistedIndex >= 0) {
|
|
170
|
+
usages = usages.slice(persistedIndex + 1);
|
|
171
|
+
}
|
|
172
|
+
else if (state.usage?.semantics === "cumulative-session") {
|
|
173
|
+
// The durable counter proves the latest cumulative total, but an
|
|
174
|
+
// initial bounded read that cannot recover that occurrence may
|
|
175
|
+
// have skipped intermediate request boundaries while the
|
|
176
|
+
// Controller was stopped. Preserve the total and fail closed only
|
|
177
|
+
// for the derived maximum-request metric.
|
|
178
|
+
usages = usages.map((occurrence) => (occurrence.usage?.semantics !== "cumulative-session"
|
|
179
|
+
? occurrence
|
|
180
|
+
: Object.freeze({ ...occurrence, observationQuality: "partial" })));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
usages.forEach((occurrence, usageIndex) => {
|
|
184
|
+
const identity = usageObservationIdentity(fence, source.sourceId, occurrence);
|
|
121
185
|
this.inbox.enqueueObservation(createRuntimeObservation({
|
|
122
186
|
schemaVersion: 2,
|
|
123
|
-
eventId:
|
|
124
|
-
semanticKey:
|
|
187
|
+
eventId: identity.eventId,
|
|
188
|
+
semanticKey: identity.semanticKey,
|
|
125
189
|
kind: "activity.observed",
|
|
126
190
|
authority: "driver-inferred",
|
|
127
191
|
receivedAt: at,
|
|
128
192
|
sequence,
|
|
129
|
-
ordinal: 2,
|
|
193
|
+
ordinal: 2 + usageIndex,
|
|
130
194
|
fence,
|
|
131
195
|
payload: {
|
|
132
196
|
activity: sample.activity ?? "model",
|
|
133
|
-
|
|
134
|
-
...(usage === undefined
|
|
197
|
+
sourceId: source.sourceId,
|
|
198
|
+
...(occurrence.usage === undefined || occurrence.activityId === undefined
|
|
199
|
+
? {}
|
|
200
|
+
: { activityId: occurrence.activityId }),
|
|
201
|
+
...(occurrence.observationQuality === undefined
|
|
202
|
+
? {}
|
|
203
|
+
: { observationQuality: occurrence.observationQuality }),
|
|
204
|
+
...(occurrence.usage === undefined ? {} : { usage: occurrence.usage })
|
|
205
|
+
}
|
|
206
|
+
}));
|
|
207
|
+
if (occurrence.usage !== undefined) {
|
|
208
|
+
state.usage = occurrence.usage;
|
|
209
|
+
state.usageEventId = identity.eventId;
|
|
210
|
+
state.usageOccurrenceId = occurrence.occurrenceId;
|
|
211
|
+
state.usageOccurrenceCheckpoint = occurrence.resumeCheckpoint;
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
if (activityChanged && state.cursor !== undefined) {
|
|
215
|
+
this.inbox.enqueueObservation(createRuntimeObservation({
|
|
216
|
+
schemaVersion: 2,
|
|
217
|
+
eventId: observationId("activity", fence, source.sourceId, sample.activityId),
|
|
218
|
+
semanticKey: observationId("activity", fence, source.sourceId, sample.activityId),
|
|
219
|
+
kind: "activity.observed",
|
|
220
|
+
authority: "driver-inferred",
|
|
221
|
+
receivedAt: at,
|
|
222
|
+
sequence,
|
|
223
|
+
ordinal: 2 + usages.length,
|
|
224
|
+
fence,
|
|
225
|
+
payload: {
|
|
226
|
+
activity: sample.activity ?? "model",
|
|
227
|
+
sourceId: source.sourceId,
|
|
228
|
+
activityId: sample.activityId
|
|
135
229
|
}
|
|
136
230
|
}));
|
|
137
231
|
dirty.add(`role:${fence.taskId}/${fence.roleName}`);
|
|
138
232
|
}
|
|
139
|
-
if (sample.usage !== undefined)
|
|
140
|
-
state.usage = sample.usage;
|
|
141
233
|
if (sample.activityId !== undefined)
|
|
142
234
|
state.activityId = sample.activityId;
|
|
143
235
|
this.#states.set(key, state);
|
|
@@ -160,11 +252,13 @@ export class AgentRuntimeObserver {
|
|
|
160
252
|
// by Run and sort each group once so every active Run can reuse the same
|
|
161
253
|
// ordered slice instead of repeatedly filtering/sorting all E events.
|
|
162
254
|
const observationsByRunId = new Map();
|
|
255
|
+
const taskObservations = [];
|
|
163
256
|
for (const event of this.store.listEvents(task.id)) {
|
|
164
257
|
const observation = runtimeObservationFromTaskEvent(event);
|
|
165
258
|
const runId = observation?.fence.runId;
|
|
166
259
|
if (observation === null || runId === undefined)
|
|
167
260
|
continue;
|
|
261
|
+
taskObservations.push(observation);
|
|
168
262
|
const grouped = observationsByRunId.get(runId);
|
|
169
263
|
if (grouped === undefined) {
|
|
170
264
|
observationsByRunId.set(runId, [observation]);
|
|
@@ -176,6 +270,7 @@ export class AgentRuntimeObserver {
|
|
|
176
270
|
for (const observations of observationsByRunId.values()) {
|
|
177
271
|
observations.sort(compareObservations);
|
|
178
272
|
}
|
|
273
|
+
taskObservations.sort(compareObservations);
|
|
179
274
|
for (const run of this.store.listAgentRuns(task.id)) {
|
|
180
275
|
if (run.status !== "active"
|
|
181
276
|
|| this.store.getActiveAgentRun(task.id, run.roleName)?.id !== run.id)
|
|
@@ -200,12 +295,18 @@ export class AgentRuntimeObserver {
|
|
|
200
295
|
continue;
|
|
201
296
|
}
|
|
202
297
|
const fence = accepted.fence;
|
|
203
|
-
const
|
|
204
|
-
.
|
|
205
|
-
const persistedUsage =
|
|
298
|
+
const generation = taskObservations.filter((observation) => (sessionGenerationFenceMatches(fence, observation.fence)
|
|
299
|
+
&& observation.payload.sourceId === source.sourceId));
|
|
300
|
+
const persistedUsage = generation.filter((observation) => (observation.kind === "activity.observed"
|
|
206
301
|
&& observation.payload.usage !== undefined)).at(-1);
|
|
207
|
-
const
|
|
302
|
+
const persistedActivity = generation.filter((observation) => (observation.kind === "activity.observed"
|
|
303
|
+
&& observation.payload.activityId !== undefined
|
|
304
|
+
&& observation.payload.usage === undefined)).at(-1);
|
|
305
|
+
const persistedHealth = generation.filter((observation) => (observation.kind === "observer.health"
|
|
208
306
|
&& observation.payload.sourceId === source.sourceId)).at(-1);
|
|
307
|
+
const persistedUsageResume = persistedUsage === undefined
|
|
308
|
+
? undefined
|
|
309
|
+
: usageObservationResume(persistedUsage.semanticKey);
|
|
209
310
|
result.push(Object.freeze({
|
|
210
311
|
key: JSON.stringify([
|
|
211
312
|
fence.taskId,
|
|
@@ -216,20 +317,30 @@ export class AgentRuntimeObserver {
|
|
|
216
317
|
fence.launchId,
|
|
217
318
|
fence.sessionGenerationId,
|
|
218
319
|
fence.nativeSessionId,
|
|
219
|
-
fence.nativeTurnId,
|
|
220
|
-
fence.receiptId,
|
|
221
320
|
source.sourceId
|
|
222
321
|
]),
|
|
223
322
|
fence,
|
|
224
323
|
source,
|
|
225
|
-
freshSession: run.mode === "new",
|
|
226
324
|
persistedState: Object.freeze({
|
|
227
325
|
...(persistedUsage?.payload.usage === undefined
|
|
228
326
|
? {}
|
|
229
327
|
: { usage: persistedUsage.payload.usage }),
|
|
230
|
-
...(persistedUsage
|
|
328
|
+
...(persistedUsage === undefined
|
|
231
329
|
? {}
|
|
232
|
-
: {
|
|
330
|
+
: {
|
|
331
|
+
usageEventId: persistedUsage.eventId,
|
|
332
|
+
...(persistedUsageResume === undefined
|
|
333
|
+
? {}
|
|
334
|
+
: {
|
|
335
|
+
usageOccurrenceId: persistedUsageResume.occurrenceId,
|
|
336
|
+
...(persistedUsageResume.checkpoint === undefined
|
|
337
|
+
? {}
|
|
338
|
+
: { usageOccurrenceCheckpoint: persistedUsageResume.checkpoint })
|
|
339
|
+
})
|
|
340
|
+
}),
|
|
341
|
+
...(persistedActivity?.payload.activityId === undefined
|
|
342
|
+
? {}
|
|
343
|
+
: { activityId: persistedActivity.payload.activityId }),
|
|
233
344
|
...(persistedHealth === undefined
|
|
234
345
|
? {}
|
|
235
346
|
: {
|
|
@@ -265,6 +376,28 @@ function sampleConcurrency(value) {
|
|
|
265
376
|
}
|
|
266
377
|
return resolved;
|
|
267
378
|
}
|
|
379
|
+
function hasUsageSnapshot(occurrence) {
|
|
380
|
+
return occurrence.usage !== undefined;
|
|
381
|
+
}
|
|
382
|
+
function cumulativeRollbackWitness(occurrences) {
|
|
383
|
+
const cumulative = occurrences.filter(({ usage }) => (usage.semantics === "cumulative-session"));
|
|
384
|
+
const latest = cumulative.at(-1);
|
|
385
|
+
for (let index = 1; index < cumulative.length; index += 1) {
|
|
386
|
+
const previous = cumulative[index - 1];
|
|
387
|
+
const current = cumulative[index];
|
|
388
|
+
if (current.usage.inputTokens >= previous.usage.inputTokens
|
|
389
|
+
&& current.usage.outputTokens >= previous.usage.outputTokens)
|
|
390
|
+
continue;
|
|
391
|
+
const witness = latest === current
|
|
392
|
+
? [previous, current]
|
|
393
|
+
: [previous, current, latest];
|
|
394
|
+
return Object.freeze(witness.map((occurrence) => Object.freeze({
|
|
395
|
+
...occurrence,
|
|
396
|
+
observationQuality: "partial"
|
|
397
|
+
})));
|
|
398
|
+
}
|
|
399
|
+
return Object.freeze([]);
|
|
400
|
+
}
|
|
268
401
|
function numericCompare(left, right) {
|
|
269
402
|
return left.localeCompare(right, undefined, { numeric: true });
|
|
270
403
|
}
|
|
@@ -279,11 +412,74 @@ function observationId(kind, fence, sourceId, value) {
|
|
|
279
412
|
.update(JSON.stringify([kind, fence, sourceId, value]))
|
|
280
413
|
.digest("hex")}`;
|
|
281
414
|
}
|
|
282
|
-
function
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
415
|
+
function usageObservationIdentity(fence, sourceId, occurrence) {
|
|
416
|
+
const eventId = tokenObservationId("usage", fence, sourceId, occurrence.occurrenceId);
|
|
417
|
+
const encodedOccurrenceId = Buffer.from(occurrence.occurrenceId, "utf8").toString("base64url");
|
|
418
|
+
return Object.freeze({
|
|
419
|
+
eventId,
|
|
420
|
+
semanticKey: `${eventId}:occurrence:${encodedOccurrenceId}${occurrence.resumeCheckpoint === undefined
|
|
421
|
+
? ""
|
|
422
|
+
: `:checkpoint:${Buffer.from(occurrence.resumeCheckpoint, "utf8").toString("base64url")}`}`
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
function usageObservationResume(semanticKey) {
|
|
426
|
+
const occurrenceMarker = ":occurrence:";
|
|
427
|
+
const checkpointMarker = ":checkpoint:";
|
|
428
|
+
const occurrenceIndex = semanticKey.lastIndexOf(occurrenceMarker);
|
|
429
|
+
if (occurrenceIndex < 0)
|
|
430
|
+
return undefined;
|
|
431
|
+
const checkpointIndex = semanticKey.indexOf(checkpointMarker, occurrenceIndex + occurrenceMarker.length);
|
|
432
|
+
const encodedOccurrence = semanticKey.slice(occurrenceIndex + occurrenceMarker.length, checkpointIndex < 0 ? undefined : checkpointIndex);
|
|
433
|
+
if (encodedOccurrence.length === 0)
|
|
434
|
+
return undefined;
|
|
435
|
+
try {
|
|
436
|
+
const occurrenceId = Buffer.from(encodedOccurrence, "base64url").toString("utf8");
|
|
437
|
+
if (occurrenceId.length === 0)
|
|
438
|
+
return undefined;
|
|
439
|
+
if (checkpointIndex < 0)
|
|
440
|
+
return Object.freeze({ occurrenceId });
|
|
441
|
+
const encodedCheckpoint = semanticKey.slice(checkpointIndex + checkpointMarker.length);
|
|
442
|
+
if (encodedCheckpoint.length === 0)
|
|
443
|
+
return undefined;
|
|
444
|
+
const checkpoint = Buffer.from(encodedCheckpoint, "base64url").toString("utf8");
|
|
445
|
+
return checkpoint.length === 0
|
|
446
|
+
? undefined
|
|
447
|
+
: Object.freeze({ occurrenceId, checkpoint });
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
return undefined;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function tokenObservationIdentity(kind, fence, sourceId, value) {
|
|
454
|
+
const eventId = tokenObservationId(kind, fence, sourceId, value);
|
|
455
|
+
return Object.freeze({ eventId, semanticKey: eventId });
|
|
456
|
+
}
|
|
457
|
+
function tokenObservationId(kind, fence, sourceId, value) {
|
|
458
|
+
return `runtime-observer-${createHash("sha256").update(JSON.stringify([
|
|
459
|
+
kind,
|
|
460
|
+
fence.taskId ?? null,
|
|
461
|
+
fence.roleName,
|
|
462
|
+
fence.agentId,
|
|
463
|
+
fence.driverId,
|
|
464
|
+
fence.launchId,
|
|
465
|
+
fence.sessionGenerationId,
|
|
466
|
+
fence.nativeSessionId ?? null,
|
|
467
|
+
sourceId,
|
|
468
|
+
value
|
|
469
|
+
])).digest("hex")}`;
|
|
470
|
+
}
|
|
471
|
+
function sessionGenerationFenceMatches(expected, actual) {
|
|
472
|
+
for (const field of [
|
|
473
|
+
"taskId",
|
|
474
|
+
"roleName",
|
|
475
|
+
"agentId",
|
|
476
|
+
"driverId",
|
|
477
|
+
"launchId",
|
|
478
|
+
"sessionGenerationId",
|
|
479
|
+
"nativeSessionId"
|
|
480
|
+
]) {
|
|
481
|
+
if (expected[field] !== actual[field])
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
return true;
|
|
289
485
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
|
-
import { callController, readControllerDiscovery, stopOrphanedFileTaskController, stopPreviousFileTaskController } from "../core/controllerClient.js";
|
|
3
|
+
import { callController, ControllerClientError, readControllerDiscovery, stopOrphanedFileTaskController, stopPreviousFileTaskController } from "../core/controllerClient.js";
|
|
4
4
|
import { controllerSocketPath } from "../core/controllerEndpoint.js";
|
|
5
5
|
import { FILE_TASK_CONTROLLER_PROTOCOL_VERSION } from "../core/protocol.js";
|
|
6
6
|
import { AGENT_OPERATIONAL_ENVIRONMENT_NAMES, nativeAgentEnvironmentNames, operationalAgentEnvironment, selectEnvironment, YUI_MANAGED_RUNTIME_ENVIRONMENT_NAMES } from "../agent/launchEnvironment.js";
|
|
@@ -14,6 +14,7 @@ import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
|
|
|
14
14
|
import { WorkspaceCleanupBlockedError } from "../repository/taskWorkspacePreparer.js";
|
|
15
15
|
import { CONTROLLER_SHUTDOWN_TIMEOUT_MS, LIFECYCLE_REQUEST_TIMEOUT_MS } from "../runtime/runtimeDeadlines.js";
|
|
16
16
|
import { FileTaskRuntimeIsolation } from "../runtime/taskRuntimeIsolation.js";
|
|
17
|
+
import { isForeignHandoverLockHeld } from "../release/runtimeRelease.js";
|
|
17
18
|
const STARTUP_TIMEOUT_MS = 5_000;
|
|
18
19
|
const POLL_INTERVAL_MS = 50;
|
|
19
20
|
const ENVIRONMENT_REFRESH_TIMEOUT_MS = 500;
|
|
@@ -54,6 +55,7 @@ export async function assertFileTaskControllerStorageCompatible(home, options =
|
|
|
54
55
|
/** Starts the per-home FileTask Controller on demand and waits until callable. */
|
|
55
56
|
export async function ensureFileTaskController(home, options = {}) {
|
|
56
57
|
const call = options.call ?? callController;
|
|
58
|
+
await waitForForeignHandover(home, options);
|
|
57
59
|
try {
|
|
58
60
|
const status = await call(home, "controller.status", {});
|
|
59
61
|
assertCompatibleControllerStatus(status, options.expectedVersion);
|
|
@@ -63,6 +65,10 @@ export async function ensureFileTaskController(home, options = {}) {
|
|
|
63
65
|
if (!isUnavailable(error))
|
|
64
66
|
throw error;
|
|
65
67
|
}
|
|
68
|
+
// A handover may have started after the first lock check and before the
|
|
69
|
+
// failed discovery/socket call. Re-check before spawning so a managed
|
|
70
|
+
// Session can never resurrect the old Controller during an update.
|
|
71
|
+
await waitForForeignHandover(home, options);
|
|
66
72
|
const timeoutMs = positive(options.startupTimeoutMs, STARTUP_TIMEOUT_MS, "startupTimeoutMs");
|
|
67
73
|
const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
|
|
68
74
|
const spawnController = options.spawnController ?? spawnDetachedFileTaskController;
|
|
@@ -322,15 +328,22 @@ export async function callFileTaskController(home, method, params = {}, options
|
|
|
322
328
|
: { stopped: false, alreadyStopped: true };
|
|
323
329
|
}
|
|
324
330
|
}
|
|
331
|
+
await waitForForeignHandover(home, options);
|
|
325
332
|
try {
|
|
326
333
|
return await call(home, method, params, {
|
|
327
334
|
timeoutMs: options.requestTimeoutMs
|
|
328
335
|
});
|
|
329
336
|
}
|
|
330
337
|
catch (error) {
|
|
331
|
-
if (!isUnavailable(error)
|
|
338
|
+
if (!isUnavailable(error)
|
|
339
|
+
&& !(isControllerDraining(error)
|
|
340
|
+
&& isForeignHandoverLockHeld(home, options.handoverOwnerPid ?? process.pid))) {
|
|
332
341
|
throw error;
|
|
342
|
+
}
|
|
333
343
|
}
|
|
344
|
+
// A pre-connect unavailable result is safe to retry. Ambiguous post-send
|
|
345
|
+
// failures are classified separately by the socket client and surface.
|
|
346
|
+
await waitForForeignHandover(home, options);
|
|
334
347
|
await ensureFileTaskController(home, options);
|
|
335
348
|
return call(home, method, params, {
|
|
336
349
|
timeoutMs: options.requestTimeoutMs
|
|
@@ -582,7 +595,7 @@ export class FileTaskWorkflowRuntime {
|
|
|
582
595
|
async #prepareAndScan(taskId) {
|
|
583
596
|
const task = this.store.getTask(taskId);
|
|
584
597
|
if (task !== null
|
|
585
|
-
&&
|
|
598
|
+
&& task.status === "active"
|
|
586
599
|
&& this.workspacePreparer !== undefined) {
|
|
587
600
|
await this.workspacePreparer.prepareTaskWorkspace(taskId);
|
|
588
601
|
}
|
|
@@ -631,6 +644,29 @@ function isUnavailable(error) {
|
|
|
631
644
|
return code === "CONTROLLER_NOT_RUNNING"
|
|
632
645
|
|| code === "CONTROLLER_UNAVAILABLE";
|
|
633
646
|
}
|
|
647
|
+
function isControllerDraining(error) {
|
|
648
|
+
return typeof error === "object"
|
|
649
|
+
&& error !== null
|
|
650
|
+
&& "code" in error
|
|
651
|
+
&& error.code === "CONTROLLER_DRAINING";
|
|
652
|
+
}
|
|
653
|
+
async function waitForForeignHandover(home, options) {
|
|
654
|
+
const allowedOwnerPid = options.handoverOwnerPid ?? process.pid;
|
|
655
|
+
if (!Number.isSafeInteger(allowedOwnerPid) || allowedOwnerPid < 1) {
|
|
656
|
+
throw new TypeError("handoverOwnerPid must be a positive integer");
|
|
657
|
+
}
|
|
658
|
+
if (!isForeignHandoverLockHeld(home, allowedOwnerPid))
|
|
659
|
+
return;
|
|
660
|
+
const timeoutMs = positive(options.handoverWaitTimeoutMs, LIFECYCLE_REQUEST_TIMEOUT_MS, "handoverWaitTimeoutMs");
|
|
661
|
+
const pollMs = positive(options.pollIntervalMs, POLL_INTERVAL_MS, "pollIntervalMs");
|
|
662
|
+
const deadline = Date.now() + timeoutMs;
|
|
663
|
+
do {
|
|
664
|
+
await delay(pollMs);
|
|
665
|
+
if (!isForeignHandoverLockHeld(home, allowedOwnerPid))
|
|
666
|
+
return;
|
|
667
|
+
} while (Date.now() < deadline);
|
|
668
|
+
throw new ControllerClientError("CONTROLLER_HANDOVER_TIMEOUT", `Controller handover did not finish within ${timeoutMs} ms.`);
|
|
669
|
+
}
|
|
634
670
|
function isDefinitelyNotRunning(error) {
|
|
635
671
|
if (typeof error !== "object" || error === null || !("code" in error))
|
|
636
672
|
return false;
|
|
@@ -282,23 +282,27 @@ async function processSelectedRoleRuntimeCleanups(store, delivery, lifecycleHost
|
|
|
282
282
|
const reservedLaunchId = isRuntimeLaunchReservation(mailbox.processing)
|
|
283
283
|
? mailbox.processing.batchId
|
|
284
284
|
: undefined;
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
285
|
+
// A failed fresh-Conversation launch can leave two exact resource
|
|
286
|
+
// generations: the detached Session's last committed launch and the
|
|
287
|
+
// replacement reservation. They are not competing authorities. The
|
|
288
|
+
// owner-wide stop has already proven physical zero, so clean both
|
|
289
|
+
// exact launch roots idempotently before settling the mailbox.
|
|
290
|
+
const launchIds = new Set([
|
|
291
|
+
reservedLaunchId,
|
|
292
|
+
session?.launchId
|
|
293
|
+
].filter((launchId) => launchId !== undefined));
|
|
294
|
+
if (lifecycleHost.cleanupTaskLaunch !== undefined) {
|
|
293
295
|
const task = store.getTask(target.taskId);
|
|
294
296
|
const reason = task?.status === "completed"
|
|
295
297
|
? "completion"
|
|
296
298
|
: "interruption";
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
299
|
+
for (const launchId of launchIds) {
|
|
300
|
+
lifecycleHost.cleanupTaskLaunch({
|
|
301
|
+
taskId: target.taskId,
|
|
302
|
+
launchId,
|
|
303
|
+
reason
|
|
304
|
+
});
|
|
305
|
+
}
|
|
302
306
|
}
|
|
303
307
|
}
|
|
304
308
|
if (store.completeRuntimeCleanup === undefined
|
|
@@ -1184,11 +1188,12 @@ export class FileTaskController {
|
|
|
1184
1188
|
this.#clearDeliveryRetry(key);
|
|
1185
1189
|
this.#scheduleTaskPassRetry({ taskId, keys: [key] });
|
|
1186
1190
|
}
|
|
1191
|
+
const runtimeAdvanced = (firstRuntimeDrain?.acknowledgedEventIds.length ?? 0) > 0
|
|
1192
|
+
|| (secondRuntimeDrain?.acknowledgedEventIds.length ?? 0) > 0;
|
|
1187
1193
|
if (scope.kind === "full"
|
|
1188
1194
|
|| scope.keys.some((key) => key !== "operator")
|
|
1189
|
-
||
|
|
1190
|
-
|
|
1191
|
-
this.#signalOperatorMailbox();
|
|
1195
|
+
|| runtimeAdvanced) {
|
|
1196
|
+
this.#signalOperatorMailbox(runtimeAdvanced);
|
|
1192
1197
|
}
|
|
1193
1198
|
}
|
|
1194
1199
|
catch (error) {
|
|
@@ -1311,7 +1316,7 @@ export class FileTaskController {
|
|
|
1311
1316
|
this.#clearEmptyOperatorRetry();
|
|
1312
1317
|
this.#scheduleNextInputDeadline();
|
|
1313
1318
|
}
|
|
1314
|
-
#signalOperatorMailbox() {
|
|
1319
|
+
#signalOperatorMailbox(retryReady = false) {
|
|
1315
1320
|
if (this.#stopped)
|
|
1316
1321
|
return;
|
|
1317
1322
|
const mailbox = this.store.getWorkMailbox({ kind: "operator" });
|
|
@@ -1320,7 +1325,8 @@ export class FileTaskController {
|
|
|
1320
1325
|
this.#lastOperatorSignalIdentity = undefined;
|
|
1321
1326
|
return;
|
|
1322
1327
|
}
|
|
1323
|
-
if (identity !== this.#lastOperatorSignalIdentity
|
|
1328
|
+
if (identity !== this.#lastOperatorSignalIdentity
|
|
1329
|
+
|| (retryReady && this.store.getOperatorDeliveryTarget() !== null)) {
|
|
1324
1330
|
this.#operatorSignalScheduler.signal("operator");
|
|
1325
1331
|
this.#lastOperatorSignalIdentity = identity;
|
|
1326
1332
|
}
|
|
@@ -1502,7 +1508,11 @@ export class FileTaskController {
|
|
|
1502
1508
|
}
|
|
1503
1509
|
for (const wakeup of result.wakeups) {
|
|
1504
1510
|
const key = `role:${encodeURIComponent(wakeup.taskId)}/leader`;
|
|
1505
|
-
if (wakeup.
|
|
1511
|
+
if (wakeup.terminalized === true) {
|
|
1512
|
+
settled.add(key);
|
|
1513
|
+
resignal.add(key);
|
|
1514
|
+
}
|
|
1515
|
+
else if (wakeup.reason === "writer-attached") {
|
|
1506
1516
|
writerBlocked.add(key);
|
|
1507
1517
|
}
|
|
1508
1518
|
else if (wakeup.reason === "not-ready") {
|
|
@@ -1516,13 +1526,7 @@ export class FileTaskController {
|
|
|
1516
1526
|
&& this.#operatorStartupRetryArmed));
|
|
1517
1527
|
if (operatorRetries.length > 0) {
|
|
1518
1528
|
retry.set("operator", {
|
|
1519
|
-
identity: operatorRetries.map((notification) => ("
|
|
1520
|
-
? `input:${notification.inputRequestId}`
|
|
1521
|
-
: "recoveryTaskId" in notification
|
|
1522
|
-
? `recovery:${notification.recoveryTaskId}`
|
|
1523
|
-
: "stallTaskId" in notification
|
|
1524
|
-
? `stall:${notification.stallTaskId}`
|
|
1525
|
-
: `terminal:${notification.terminalTaskId}`)).join("|")
|
|
1529
|
+
identity: operatorRetries.map((notification) => (`${notification.sourceKind}:${notification.taskId}:${notification.sourceId}`)).join("|")
|
|
1526
1530
|
});
|
|
1527
1531
|
}
|
|
1528
1532
|
else if (result.inputNotifications.some((notification) => notification.status === "sent"
|
|
@@ -1694,7 +1698,7 @@ export async function startFileTaskController(home, store, delivery, dispatcher,
|
|
|
1694
1698
|
dispatcherServiceTime.started();
|
|
1695
1699
|
try {
|
|
1696
1700
|
if (stopping) {
|
|
1697
|
-
throw controllerApplicationError("
|
|
1701
|
+
throw controllerApplicationError("CONTROLLER_DRAINING", "Controller is draining.");
|
|
1698
1702
|
}
|
|
1699
1703
|
if (method === "scheduler.signal") {
|
|
1700
1704
|
runtime.signal(signalMailboxKey(params));
|