@zq-silk/yui 0.6.13 → 0.6.14
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/README.md +34 -5
- package/dist/cli/commandCatalog.js +173 -57
- package/dist/cli/helpRenderer.js +3 -1
- package/dist/cli.js +99 -11
- package/dist/commands/configCommands.js +521 -171
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +56 -3
- package/dist/commands/projectCommands.js +504 -2
- package/dist/commands/releaseCommands.js +0 -1
- package/dist/commands/taskActor.js +17 -0
- package/dist/commands/taskBaseCommands.js +29 -0
- package/dist/commands/taskCommands.js +577 -126
- package/dist/commands/taskContextCommand.js +36 -2
- package/dist/commands/taskNextActionCommand.js +48 -3
- package/dist/commands/taskPublicationCommands.js +319 -0
- package/dist/commands/taskRoleRuntimeStatus.js +83 -59
- package/dist/commands/telemetryCommands.js +14 -13
- package/dist/config/yuiConfig.js +161 -8
- package/dist/context/sessionContextBudget.js +68 -0
- package/dist/context/wakeNotification.js +65 -0
- package/dist/controller/clientRuntime.js +2 -1
- package/dist/controller/ephemeralResourceReaper.js +2 -1
- package/dist/controller/fileSchedulerStoreAdapter.js +183 -16
- package/dist/controller/jobSupervisor.js +5 -4
- package/dist/controller/resourceCleanupLinux.js +6 -6
- package/dist/controller/resourceInventoryLinux.js +3 -3
- package/dist/controller/runtime.js +88 -10
- package/dist/controller/updateReconciliation.js +4 -3
- package/dist/doctor/doctor.js +26 -7
- package/dist/executor/agentConfigurationCatalog.js +18 -0
- package/dist/executor/fileRoleLaunchPlanner.js +3 -3
- package/dist/lifecycle/contextBudgetRollover.js +81 -0
- package/dist/lifecycle/exactRunTerminalization.js +11 -1
- package/dist/lifecycle/providerErrorClass.js +33 -12
- package/dist/observability/executionAudit.js +214 -6
- package/dist/output/table.js +18 -0
- package/dist/repository/gitWorkspace.js +92 -0
- package/dist/repository/project.js +218 -4
- package/dist/repository/taskBaseFreshness.js +318 -0
- package/dist/repository/taskWorkspacePreparer.js +16 -2
- package/dist/review/deltaRecheck.js +232 -0
- package/dist/review/reviewConfig.js +31 -0
- package/dist/review/reviewFindingLedger.js +5 -1
- package/dist/review/reviewRound.js +156 -1
- package/dist/run/providerRetry.js +21 -3
- package/dist/run/providerRetryConfig.js +13 -60
- package/dist/run/recoveryProjection.js +199 -0
- package/dist/runtime/builtinAgentDrivers.js +3 -0
- package/dist/runtime/builtinTranscriptUsage.js +76 -32
- package/dist/runtime/continuationManager.js +17 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/launchDiagnostics.js +154 -0
- package/dist/runtime/lifecycleReservation.js +13 -0
- package/dist/runtime/providerContinuation.js +38 -0
- package/dist/runtime/providerContinuationReconciliationService.js +1 -0
- package/dist/runtime/providerErrorCodes.js +278 -0
- package/dist/runtime/runtimeHealthPolicy.js +20 -0
- package/dist/runtime/runtimeObservation.js +1 -0
- package/dist/runtime/runtimeProjection.js +115 -23
- package/dist/runtime/tmuxAdapters.js +242 -48
- package/dist/scheduler/activeRoleRunDelivery.js +139 -3
- package/dist/scheduler/activeTaskProgress.js +4 -3
- package/dist/scheduler/leaderWakeupProcessor.js +105 -15
- package/dist/scheduler/roleRunLiveness.js +2 -1
- package/dist/scheduler/roleRunStall.js +3 -2
- package/dist/scheduler/taskWake.js +72 -0
- package/dist/scheduler/wakeReason.js +64 -0
- package/dist/scheduler/wakeupQueue.js +2 -1
- package/dist/setup/setupCommand.js +1 -1
- package/dist/storage/migration/productionRegistry.js +325 -1
- package/dist/storage/sqliteSchema.js +61 -2
- package/dist/storage/sqliteStore.js +129 -2
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/taskStore.js +262 -5
- package/dist/storage/upgrade/recordVersions.js +6 -1
- package/dist/storage/upgrade/sqliteStateMigration.js +22 -2
- package/dist/task/completionReadiness.js +282 -0
- package/dist/task/publicationReference.js +123 -0
- package/dist/task/taskRecordReference.js +3 -1
- package/dist/telemetry/telemetryConfig.js +23 -18
- package/dist/telemetry/telemetryWiring.js +8 -8
- package/dist/tmux/tmuxManager.js +50 -9
- package/dist/web/assets/client/i18n.js +4 -0
- package/dist/web/assets/client/view.js +18 -0
- package/dist/web/webSnapshot.js +100 -10
- package/i18n/README.zh-CN.md +5 -5
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +49 -10
- package/skills/yui-operator/SKILL.md +13 -3
- package/skills/yui-worker/SKILL.md +8 -0
|
@@ -2,7 +2,8 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
2
2
|
import { hasRuntimeCleanupObligation, hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
3
3
|
import { isRoleRunStalled, latestStallProgressAt } from "../scheduler/roleRunStall.js";
|
|
4
4
|
import { createRuntimeObservation, runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
5
|
-
import {
|
|
5
|
+
import { classifyRuntimeHealth, projectRuntimeMailbox, projectRuntimeObservation, projectRuntimeTaskEvents, runtimeDisplayStatus } from "../runtime/runtimeProjection.js";
|
|
6
|
+
import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
|
|
6
7
|
import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
7
8
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
8
9
|
export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes, now = new Date()) {
|
|
@@ -20,6 +21,9 @@ export function renderTaskRoleRuntimeStatus(status) {
|
|
|
20
21
|
const activeRun = status.activeRun === null
|
|
21
22
|
? "-"
|
|
22
23
|
: `${status.activeRun.id} (${activeRunDeliveryLabel(status.activeRun)})`;
|
|
24
|
+
const lastRun = status.activeRun !== null || status.lastRun === null
|
|
25
|
+
? undefined
|
|
26
|
+
: `${status.lastRun.id} (${status.lastRun.status}${status.lastRun.endedAt === undefined ? "" : ` at ${status.lastRun.endedAt}`})`;
|
|
23
27
|
const activeWork = status.activeWork === null
|
|
24
28
|
? "-"
|
|
25
29
|
: `${status.activeWork.id} (${status.activeWork.status}) ${status.activeWork.title}`;
|
|
@@ -44,10 +48,12 @@ export function renderTaskRoleRuntimeStatus(status) {
|
|
|
44
48
|
? "not observable"
|
|
45
49
|
: [
|
|
46
50
|
`${status.runtime.driverId}: ${status.runtime.status}`,
|
|
47
|
-
`
|
|
51
|
+
`health=${status.runtime.healthLayer}`,
|
|
52
|
+
`health reason=${status.runtime.healthReason}`,
|
|
48
53
|
status.runtime.lastActivityAt === undefined
|
|
49
54
|
? undefined
|
|
50
55
|
: `last activity=${status.runtime.lastActivityAt}`,
|
|
56
|
+
`last semantic progress=${status.runtime.lastSemanticProgressAt}`,
|
|
51
57
|
status.runtime.activeOperations.length === 0
|
|
52
58
|
? undefined
|
|
53
59
|
: `operations=${status.runtime.activeOperations.join(",")}`,
|
|
@@ -73,6 +79,7 @@ export function renderTaskRoleRuntimeStatus(status) {
|
|
|
73
79
|
` Role state ${status.role.status}`,
|
|
74
80
|
` Active work ${activeWork}`,
|
|
75
81
|
` Active run ${activeRun}`,
|
|
82
|
+
...(lastRun === undefined ? [] : [` Last run ${lastRun}`]),
|
|
76
83
|
` Run attention ${status.stall.active
|
|
77
84
|
? `needs-attention (${status.stall.kind ?? "workflow-not-progressing"}; no workflow progress since ${status.stall.progressAt ?? "unknown"})`
|
|
78
85
|
: "none"}`,
|
|
@@ -90,6 +97,14 @@ export function taskRoleActiveWorkLabel(status) {
|
|
|
90
97
|
return `${status.activeWork.id}: ${status.activeWork.title}`;
|
|
91
98
|
return status.activeRun === null ? "-" : status.activeRun.id;
|
|
92
99
|
}
|
|
100
|
+
/** Issue 09: compact last-Run outcome label for the Role list table. */
|
|
101
|
+
export function taskRoleLastRunLabel(status) {
|
|
102
|
+
if (status.activeRun !== null)
|
|
103
|
+
return `${status.activeRun.id} ${status.activeRun.status}`;
|
|
104
|
+
if (status.lastRun === null)
|
|
105
|
+
return "-";
|
|
106
|
+
return `${status.lastRun.id} ${status.lastRun.status}`;
|
|
107
|
+
}
|
|
93
108
|
export function taskRoleNativeSessionLabel(status) {
|
|
94
109
|
if (status.runtimeCleanupPending && status.nativeSession === null)
|
|
95
110
|
return "reset (cleanup-pending)";
|
|
@@ -121,6 +136,13 @@ export function taskRoleTmuxLabel(status) {
|
|
|
121
136
|
}
|
|
122
137
|
function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputRequestCount, now) {
|
|
123
138
|
const activeRun = store.getActiveAgentRun(taskId, role.name);
|
|
139
|
+
// Issue 09: the last Run outcome is a separate axis from the Session
|
|
140
|
+
// lifecycle. A Session that stops after its Run yielded must not retroactively
|
|
141
|
+
// turn that Run into a failure; the status display keeps both visible.
|
|
142
|
+
const lastRun = store.listAgentRuns(taskId)
|
|
143
|
+
.filter((candidate) => candidate.roleName === role.name)
|
|
144
|
+
.sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt))[0]
|
|
145
|
+
?? null;
|
|
124
146
|
const activeWork = activeRun?.workItemId === undefined
|
|
125
147
|
? null
|
|
126
148
|
: store.getWorkItem(taskId, activeRun.workItemId);
|
|
@@ -154,7 +176,7 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
|
|
|
154
176
|
? { managed: false, path: role.workspace }
|
|
155
177
|
: { ...managedWorkspace, managed: true };
|
|
156
178
|
const events = store.listEvents(taskId);
|
|
157
|
-
const runtime = projectTaskRoleRuntime(activeRun, nativeSession, tmux, events, store.getWorkMailbox({ kind: "role", taskId, roleName: role.name }), now);
|
|
179
|
+
const runtime = projectTaskRoleRuntime(activeRun, nativeSession, tmux, events, store.getWorkMailbox({ kind: "role", taskId, roleName: role.name }), store, taskId, role.name, now);
|
|
158
180
|
const stalled = activeRun !== null && isRoleRunStalled(events, activeRun.id);
|
|
159
181
|
const stallProgressAt = activeRun === null
|
|
160
182
|
? undefined
|
|
@@ -162,7 +184,7 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
|
|
|
162
184
|
const stallKind = activeRun === null
|
|
163
185
|
? undefined
|
|
164
186
|
: latestStallKind(events, activeRun.id);
|
|
165
|
-
const health = calculateHealth(role, activeRun, nativeSession, recovery.runtimeCleanupPending, tmux, openInputRequestCount, stalled, runtime);
|
|
187
|
+
const health = calculateHealth(role, activeRun, lastRun, nativeSession, recovery.runtimeCleanupPending, tmux, openInputRequestCount, stalled, runtime);
|
|
166
188
|
const stall = activeRun === null
|
|
167
189
|
? { active: false }
|
|
168
190
|
: {
|
|
@@ -184,6 +206,7 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
|
|
|
184
206
|
openInputRequestCount,
|
|
185
207
|
role,
|
|
186
208
|
activeRun,
|
|
209
|
+
lastRun,
|
|
187
210
|
activeWork,
|
|
188
211
|
nativeSession,
|
|
189
212
|
tmux,
|
|
@@ -200,7 +223,7 @@ function latestStallKind(events, runId) {
|
|
|
200
223
|
? event.payload.kind
|
|
201
224
|
: undefined;
|
|
202
225
|
}
|
|
203
|
-
function calculateHealth(role, activeRun, nativeSession, runtimeCleanupPending, tmux, openInputRequestCount, stalled, runtime) {
|
|
226
|
+
function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanupPending, tmux, openInputRequestCount, stalled, runtime) {
|
|
204
227
|
if (runtimeCleanupPending && nativeSession === null) {
|
|
205
228
|
return {
|
|
206
229
|
health: "needs-attention",
|
|
@@ -211,7 +234,18 @@ function calculateHealth(role, activeRun, nativeSession, runtimeCleanupPending,
|
|
|
211
234
|
return { health: "failed", healthReason: `persisted Role state is ${role.status}` };
|
|
212
235
|
}
|
|
213
236
|
if (nativeSession?.status === "broken") {
|
|
214
|
-
|
|
237
|
+
// Issue 09: a broken Session only fails a live Run. When the last Run
|
|
238
|
+
// already yielded, the Session death is a lifecycle event, not a Run
|
|
239
|
+
// failure — surface it as attention with the persisted Run outcome.
|
|
240
|
+
if (activeRun !== null) {
|
|
241
|
+
return { health: "failed", healthReason: "the active native session is broken" };
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
health: "needs-attention",
|
|
245
|
+
healthReason: lastRun === null
|
|
246
|
+
? "the native session is broken"
|
|
247
|
+
: `the native session is broken; last run ${lastRun.id} ${lastRun.status}`
|
|
248
|
+
};
|
|
215
249
|
}
|
|
216
250
|
const awaitingProviderAcceptance = activeRun?.pushedAt !== undefined
|
|
217
251
|
&& activeRun.deliveredAt === undefined;
|
|
@@ -245,52 +279,36 @@ function calculateHealth(role, activeRun, nativeSession, runtimeCleanupPending,
|
|
|
245
279
|
};
|
|
246
280
|
}
|
|
247
281
|
if (activeRun.deliveredAt !== undefined) {
|
|
248
|
-
if (runtime === null
|
|
249
|
-
|| runtime.status === "runtime-unobservable"
|
|
250
|
-
|| runtime.attention === "unobservable") {
|
|
251
|
-
return {
|
|
252
|
-
health: "needs-attention",
|
|
253
|
-
healthReason: "the host is present but the Agent Driver exposes no current runtime state"
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
if (runtime.attention === "quiet" || runtime.attention === "active-operation-quiet") {
|
|
257
|
-
return {
|
|
258
|
-
health: "needs-attention",
|
|
259
|
-
healthReason: runtime.attention === "active-operation-quiet"
|
|
260
|
-
? "the Agent Driver reports an open operation but no recent structured runtime activity"
|
|
261
|
-
: "the Agent Driver has not reported recent structured runtime activity"
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
if (runtime.status === "broken") {
|
|
265
|
-
return {
|
|
266
|
-
health: "failed",
|
|
267
|
-
healthReason: `the Agent Driver runtime is ${runtime.status}`
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
if (runtime.status === "stopped") {
|
|
282
|
+
if (runtime === null) {
|
|
271
283
|
return {
|
|
272
284
|
health: "needs-attention",
|
|
273
|
-
healthReason: "the
|
|
274
|
-
};
|
|
275
|
-
}
|
|
276
|
-
if (runtime.status.startsWith("waiting-")) {
|
|
277
|
-
return {
|
|
278
|
-
health: runtime.status === "waiting-user" ? "blocked-input" : "waiting",
|
|
279
|
-
healthReason: `the Agent Driver is ${runtime.status.replaceAll("-", " ")}`
|
|
285
|
+
healthReason: "the delivered Run has no authoritative Agent Driver state"
|
|
280
286
|
};
|
|
281
287
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
health: "
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
health: "
|
|
292
|
-
|
|
293
|
-
|
|
288
|
+
switch (runtime.healthLayer) {
|
|
289
|
+
case "broken":
|
|
290
|
+
return { health: "failed", healthReason: runtime.healthReason };
|
|
291
|
+
case "stopped":
|
|
292
|
+
case "diagnostic-needed":
|
|
293
|
+
case "ready":
|
|
294
|
+
case "awaiting-provider-acceptance":
|
|
295
|
+
case "runtime-unobservable":
|
|
296
|
+
case "starting":
|
|
297
|
+
return { health: "needs-attention", healthReason: runtime.healthReason };
|
|
298
|
+
case "waiting-user":
|
|
299
|
+
return { health: "blocked-input", healthReason: runtime.healthReason };
|
|
300
|
+
case "waiting-permission":
|
|
301
|
+
case "waiting-external":
|
|
302
|
+
return { health: "waiting", healthReason: runtime.healthReason };
|
|
303
|
+
case "quiet":
|
|
304
|
+
case "active-quiet":
|
|
305
|
+
case "model-active":
|
|
306
|
+
case "tool-active":
|
|
307
|
+
case "subagent-active":
|
|
308
|
+
default:
|
|
309
|
+
// Short silence is a hint, not a failure. Only deterministic
|
|
310
|
+
// dead/broken evidence or the durable stall window escalates.
|
|
311
|
+
return { health: "running", healthReason: runtime.healthReason };
|
|
294
312
|
}
|
|
295
313
|
}
|
|
296
314
|
}
|
|
@@ -327,7 +345,7 @@ function calculateHealth(role, activeRun, nativeSession, runtimeCleanupPending,
|
|
|
327
345
|
? { health: "ready", healthReason: "the native Agent pane is ready without active work" }
|
|
328
346
|
: { health: "idle", healthReason: "there is no active work or live tmux pane" };
|
|
329
347
|
}
|
|
330
|
-
function projectTaskRoleRuntime(run, session, tmux, events, mailbox, now) {
|
|
348
|
+
function projectTaskRoleRuntime(run, session, tmux, events, mailbox, store, taskId, roleName, now) {
|
|
331
349
|
if (run === null || session?.launchId === undefined)
|
|
332
350
|
return null;
|
|
333
351
|
let driverId;
|
|
@@ -370,21 +388,27 @@ function projectTaskRoleRuntime(run, session, tmux, events, mailbox, now) {
|
|
|
370
388
|
fence,
|
|
371
389
|
payload: { alive: tmux.state === "running" }
|
|
372
390
|
}));
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
391
|
+
// The semantic progress fence is the same durable fold the scheduler stall
|
|
392
|
+
// pass consumes, so CLI/Web/scheduler share one progress clock.
|
|
393
|
+
const semanticProgress = run.deliveredAt === undefined
|
|
394
|
+
? { progressAt: run.createdAt }
|
|
395
|
+
: latestRunDurableProgressAt(store, taskId, roleName, run.id)
|
|
396
|
+
?? { progressAt: run.deliveredAt };
|
|
397
|
+
const classification = classifyRuntimeHealth({
|
|
398
|
+
projection,
|
|
399
|
+
semanticProgressAt: semanticProgress.progressAt,
|
|
400
|
+
now
|
|
379
401
|
});
|
|
380
402
|
return {
|
|
381
403
|
driverId,
|
|
382
404
|
status: runtimeDisplayStatus(projection),
|
|
383
|
-
|
|
384
|
-
|
|
405
|
+
healthLayer: classification.layer,
|
|
406
|
+
healthReason: classification.reason,
|
|
407
|
+
lastSemanticProgressAt: classification.lastSemanticProgressAt,
|
|
408
|
+
...(classification.lastRuntimeActivityAt === undefined
|
|
385
409
|
? {}
|
|
386
|
-
: { lastActivityAt:
|
|
387
|
-
activeOperations:
|
|
410
|
+
: { lastActivityAt: classification.lastRuntimeActivityAt }),
|
|
411
|
+
activeOperations: classification.activeOperations,
|
|
388
412
|
...(projection.waitingReason === undefined
|
|
389
413
|
? {}
|
|
390
414
|
: { waitingReason: projection.waitingReason }),
|
|
@@ -7,6 +7,10 @@ import { COMMITTED_DATABASE_FILENAME } from "../storage/upgrade/sqliteStateMigra
|
|
|
7
7
|
import { DEFAULT_RUN_CAP, DEFAULT_TERMINAL_KEEP, resolveRunCap, resolveTelemetryMode, resolveTerminalKeep } from "../telemetry/telemetryConfig.js";
|
|
8
8
|
import { applyTelemetryCompaction, planTelemetryCompaction } from "../telemetry/telemetryCompaction.js";
|
|
9
9
|
import { SqliteTelemetryStore } from "../telemetry/sqliteTelemetryStore.js";
|
|
10
|
+
/** Read the durable config, falling back to defaults when no store is available. */
|
|
11
|
+
function storeConfig(options) {
|
|
12
|
+
return options.store?.getConfig() ?? { schemaVersion: 1 };
|
|
13
|
+
}
|
|
10
14
|
export async function runTelemetryCommand(args, options) {
|
|
11
15
|
const [command, ...rest] = args;
|
|
12
16
|
if (command === "status")
|
|
@@ -27,12 +31,11 @@ export async function runTelemetryCommand(args, options) {
|
|
|
27
31
|
// -- status ---------------------------------------------------------------------
|
|
28
32
|
function telemetryStatus(args, options) {
|
|
29
33
|
const flags = parseFlags(args, new Set([]));
|
|
30
|
-
const
|
|
31
|
-
const mode = resolveTelemetryMode(env);
|
|
34
|
+
const mode = resolveTelemetryMode(storeConfig(options).telemetryMode);
|
|
32
35
|
const telemetry = new SqliteTelemetryStore(options.home, {
|
|
33
36
|
mode,
|
|
34
|
-
terminalKeep: resolveTerminalKeep(
|
|
35
|
-
runCap: resolveRunCap(
|
|
37
|
+
terminalKeep: resolveTerminalKeep(storeConfig(options).telemetryTerminalKeep),
|
|
38
|
+
runCap: resolveRunCap(storeConfig(options).telemetryRunCap)
|
|
36
39
|
});
|
|
37
40
|
try {
|
|
38
41
|
const health = telemetry.health();
|
|
@@ -52,8 +55,8 @@ function telemetryStatus(args, options) {
|
|
|
52
55
|
coalesced: health.coalesced,
|
|
53
56
|
lastError: health.lastError,
|
|
54
57
|
totalRows: health.rows,
|
|
55
|
-
terminalKeep: resolveTerminalKeep(
|
|
56
|
-
runCap: resolveRunCap(
|
|
58
|
+
terminalKeep: resolveTerminalKeep(storeConfig(options).telemetryTerminalKeep),
|
|
59
|
+
runCap: resolveRunCap(storeConfig(options).telemetryRunCap),
|
|
57
60
|
tasks: perTask
|
|
58
61
|
};
|
|
59
62
|
if (options.json || flags.has("json"))
|
|
@@ -84,11 +87,10 @@ function telemetryPrune(args, options) {
|
|
|
84
87
|
const taskId = stringOption(args, "--task");
|
|
85
88
|
const keep = integerOption(args, "--keep", DEFAULT_TERMINAL_KEEP);
|
|
86
89
|
const dryRun = flags.has("dry-run");
|
|
87
|
-
const
|
|
88
|
-
const cap = resolveRunCap(env);
|
|
90
|
+
const cap = resolveRunCap(storeConfig(options).telemetryRunCap);
|
|
89
91
|
const store = requireStore(options);
|
|
90
92
|
const telemetry = new SqliteTelemetryStore(options.home, {
|
|
91
|
-
mode: resolveTelemetryMode(
|
|
93
|
+
mode: resolveTelemetryMode(storeConfig(options).telemetryMode),
|
|
92
94
|
terminalKeep: keep,
|
|
93
95
|
runCap: cap
|
|
94
96
|
});
|
|
@@ -237,11 +239,10 @@ function telemetryRead(args, options) {
|
|
|
237
239
|
const runId = stringOption(args, "--run");
|
|
238
240
|
const limit = integerOption(args, "--limit", 100);
|
|
239
241
|
const offset = integerOption(args, "--offset", 0);
|
|
240
|
-
const env = options.environment ?? process.env;
|
|
241
242
|
const telemetry = new SqliteTelemetryStore(options.home, {
|
|
242
|
-
mode: resolveTelemetryMode(
|
|
243
|
-
terminalKeep: resolveTerminalKeep(
|
|
244
|
-
runCap: resolveRunCap(
|
|
243
|
+
mode: resolveTelemetryMode(storeConfig(options).telemetryMode),
|
|
244
|
+
terminalKeep: resolveTerminalKeep(storeConfig(options).telemetryTerminalKeep),
|
|
245
|
+
runCap: resolveRunCap(storeConfig(options).telemetryRunCap)
|
|
245
246
|
});
|
|
246
247
|
try {
|
|
247
248
|
if (flags.has("aggregate")) {
|
package/dist/config/yuiConfig.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { supportedAgentAdapterIds } from "../agent/adapterCatalog.js";
|
|
2
|
+
import { PROVIDER_RETRY_MAX_WINDOW_MS } from "../run/providerRetry.js";
|
|
3
|
+
import { DEFAULT_RUN_CAP, DEFAULT_TELEMETRY_MODE, DEFAULT_TERMINAL_KEEP, MAX_RUN_CAP } from "../telemetry/telemetryConfig.js";
|
|
1
4
|
export const DEFAULT_RECONCILIATION_INTERVAL_SECONDS = 120;
|
|
2
5
|
export const MIN_RECONCILIATION_INTERVAL_SECONDS = 5;
|
|
3
6
|
export const MAX_RECONCILIATION_INTERVAL_SECONDS = 300;
|
|
@@ -66,15 +69,165 @@ export function resolveLeaderNextActionMode(value) {
|
|
|
66
69
|
}
|
|
67
70
|
return normalized;
|
|
68
71
|
}
|
|
72
|
+
// ── Issue 01: Provider retry ──────────────────────────────────────────────
|
|
73
|
+
export const PROVIDER_RETRY_MODES = ["off", "shadow", "enforce"];
|
|
74
|
+
export const DEFAULT_PROVIDER_RETRY_MODE = "enforce";
|
|
75
|
+
export function resolveProviderRetryMode(value) {
|
|
76
|
+
if (value === undefined || value === null)
|
|
77
|
+
return DEFAULT_PROVIDER_RETRY_MODE;
|
|
78
|
+
if (typeof value !== "string") {
|
|
79
|
+
throw new TypeError("providerRetryMode must be off, shadow, or enforce.");
|
|
80
|
+
}
|
|
81
|
+
const normalized = value.trim().toLowerCase();
|
|
82
|
+
if (normalized.length === 0)
|
|
83
|
+
return DEFAULT_PROVIDER_RETRY_MODE;
|
|
84
|
+
if (!PROVIDER_RETRY_MODES.includes(normalized)) {
|
|
85
|
+
throw new TypeError("providerRetryMode must be off, shadow, or enforce.");
|
|
86
|
+
}
|
|
87
|
+
return normalized;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Resolves the adapter list. `["all"]` or undefined means every supported
|
|
91
|
+
* adapter; an empty array disables in-place retry.
|
|
92
|
+
*/
|
|
93
|
+
export function resolveProviderRetryAdapters(value) {
|
|
94
|
+
if (value === undefined || value === null) {
|
|
95
|
+
return [...supportedAgentAdapterIds()];
|
|
96
|
+
}
|
|
97
|
+
if (!Array.isArray(value)) {
|
|
98
|
+
throw new TypeError("providerRetryAdapters must be an array of adapter ids.");
|
|
99
|
+
}
|
|
100
|
+
const supported = new Set(supportedAgentAdapterIds());
|
|
101
|
+
const adapters = [];
|
|
102
|
+
for (const raw of value) {
|
|
103
|
+
if (typeof raw !== "string") {
|
|
104
|
+
throw new TypeError("providerRetryAdapters entries must be strings.");
|
|
105
|
+
}
|
|
106
|
+
const token = raw.trim().toLowerCase();
|
|
107
|
+
if (token === "all") {
|
|
108
|
+
for (const adapter of supportedAgentAdapterIds()) {
|
|
109
|
+
if (!adapters.includes(adapter))
|
|
110
|
+
adapters.push(adapter);
|
|
111
|
+
}
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/u.test(token)) {
|
|
115
|
+
throw new TypeError(`Invalid Provider retry adapter: ${token}.`);
|
|
116
|
+
}
|
|
117
|
+
if (!supported.has(token)) {
|
|
118
|
+
throw new TypeError(`Unknown Provider retry adapter: ${token}.`);
|
|
119
|
+
}
|
|
120
|
+
if (!adapters.includes(token))
|
|
121
|
+
adapters.push(token);
|
|
122
|
+
}
|
|
123
|
+
return adapters;
|
|
124
|
+
}
|
|
125
|
+
export function resolveProviderRetryMaxWindowMs(value) {
|
|
126
|
+
if (value === undefined || value === null)
|
|
127
|
+
return PROVIDER_RETRY_MAX_WINDOW_MS;
|
|
128
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
129
|
+
throw new TypeError("providerRetryMaxWindowMs must be a positive integer.");
|
|
130
|
+
}
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
export function resolveYieldReceiptReplay(value) {
|
|
134
|
+
if (value === undefined || value === null)
|
|
135
|
+
return true;
|
|
136
|
+
if (typeof value !== "boolean") {
|
|
137
|
+
throw new TypeError("yieldReceiptReplay must be a boolean.");
|
|
138
|
+
}
|
|
139
|
+
return value;
|
|
140
|
+
}
|
|
141
|
+
// ── Executable paths ──────────────────────────────────────────────────────
|
|
142
|
+
export function resolveTmuxBin(value) {
|
|
143
|
+
if (value === undefined || value === null)
|
|
144
|
+
return "tmux";
|
|
145
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
146
|
+
throw new TypeError("tmuxBin must be a non-empty string.");
|
|
147
|
+
}
|
|
148
|
+
return value.trim();
|
|
149
|
+
}
|
|
150
|
+
export function resolveGitBin(value) {
|
|
151
|
+
if (value === undefined || value === null)
|
|
152
|
+
return "git";
|
|
153
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
154
|
+
throw new TypeError("gitBin must be a non-empty string.");
|
|
155
|
+
}
|
|
156
|
+
return value.trim();
|
|
157
|
+
}
|
|
158
|
+
// ── Telemetry ─────────────────────────────────────────────────────────────
|
|
159
|
+
const TELEMETRY_MODES = ["legacy", "dual", "bounded"];
|
|
160
|
+
export function resolveTelemetryMode(value) {
|
|
161
|
+
if (value === undefined || value === null)
|
|
162
|
+
return DEFAULT_TELEMETRY_MODE;
|
|
163
|
+
if (typeof value !== "string") {
|
|
164
|
+
throw new TypeError("telemetryMode must be legacy, dual, or bounded.");
|
|
165
|
+
}
|
|
166
|
+
const normalized = value.trim().toLowerCase();
|
|
167
|
+
if (normalized.length === 0)
|
|
168
|
+
return DEFAULT_TELEMETRY_MODE;
|
|
169
|
+
if (!TELEMETRY_MODES.includes(normalized)) {
|
|
170
|
+
throw new TypeError(`telemetryMode must be one of ${TELEMETRY_MODES.join(", ")}; got ${JSON.stringify(normalized)}.`);
|
|
171
|
+
}
|
|
172
|
+
return normalized;
|
|
173
|
+
}
|
|
174
|
+
export function resolveTelemetryTerminalKeep(value) {
|
|
175
|
+
if (value === undefined || value === null)
|
|
176
|
+
return DEFAULT_TERMINAL_KEEP;
|
|
177
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
|
|
178
|
+
throw new TypeError("telemetryTerminalKeep must be a positive integer.");
|
|
179
|
+
}
|
|
180
|
+
return value;
|
|
181
|
+
}
|
|
182
|
+
export function resolveTelemetryRunCap(value) {
|
|
183
|
+
if (value === undefined || value === null)
|
|
184
|
+
return DEFAULT_RUN_CAP;
|
|
185
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
|
|
186
|
+
throw new TypeError("telemetryRunCap must be a positive integer.");
|
|
187
|
+
}
|
|
188
|
+
if (value > MAX_RUN_CAP) {
|
|
189
|
+
throw new TypeError(`telemetryRunCap must not exceed ${MAX_RUN_CAP.toLocaleString("en-US")} (retention cannot be disabled).`);
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
}
|
|
69
193
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
194
|
+
* Issue 04 (context token budget): thresholds for one native Session
|
|
195
|
+
* generation's observed per-request input peak, measured in tokens. When the
|
|
196
|
+
* peak crosses the soft threshold the Leader wake carries a checkpoint
|
|
197
|
+
* advisory; when it crosses the hard threshold the scheduler retires the
|
|
198
|
+
* generation and starts a fresh one instead of waiting for provider-side
|
|
199
|
+
* auto-compaction. The fields are additive and optional — Homes without them
|
|
200
|
+
* keep these defaults, so no config migration is required.
|
|
73
201
|
*/
|
|
74
|
-
export
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
202
|
+
export const DEFAULT_CONTEXT_SOFT_TOKENS = 100_000;
|
|
203
|
+
export const DEFAULT_CONTEXT_HARD_TOKENS = 120_000;
|
|
204
|
+
export const MIN_CONTEXT_BUDGET_TOKENS = 1_000;
|
|
205
|
+
export const MAX_CONTEXT_BUDGET_TOKENS = 1_000_000;
|
|
206
|
+
function resolveContextBudgetToken(value, label) {
|
|
207
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value)) {
|
|
208
|
+
throw new TypeError(`${label} must be a safe integer.`);
|
|
209
|
+
}
|
|
210
|
+
if (value < MIN_CONTEXT_BUDGET_TOKENS || value > MAX_CONTEXT_BUDGET_TOKENS) {
|
|
211
|
+
throw new TypeError(`${label} must be between ${MIN_CONTEXT_BUDGET_TOKENS} and ${MAX_CONTEXT_BUDGET_TOKENS}.`);
|
|
212
|
+
}
|
|
213
|
+
return value;
|
|
214
|
+
}
|
|
215
|
+
export function resolveContextBudget(configured) {
|
|
216
|
+
if (configured === undefined || configured === null) {
|
|
217
|
+
return { softTokens: DEFAULT_CONTEXT_SOFT_TOKENS, hardTokens: DEFAULT_CONTEXT_HARD_TOKENS };
|
|
218
|
+
}
|
|
219
|
+
if (typeof configured !== "object" || Array.isArray(configured)) {
|
|
220
|
+
throw new TypeError("contextBudget must be an object.");
|
|
221
|
+
}
|
|
222
|
+
const record = configured;
|
|
223
|
+
const softTokens = record.softTokens === undefined
|
|
224
|
+
? DEFAULT_CONTEXT_SOFT_TOKENS
|
|
225
|
+
: resolveContextBudgetToken(record.softTokens, "contextBudget.softTokens");
|
|
226
|
+
const hardTokens = record.hardTokens === undefined
|
|
227
|
+
? DEFAULT_CONTEXT_HARD_TOKENS
|
|
228
|
+
: resolveContextBudgetToken(record.hardTokens, "contextBudget.hardTokens");
|
|
229
|
+
if (softTokens >= hardTokens) {
|
|
230
|
+
throw new TypeError("contextBudget.softTokens must be smaller than contextBudget.hardTokens.");
|
|
78
231
|
}
|
|
79
|
-
return
|
|
232
|
+
return { softTokens, hardTokens };
|
|
80
233
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { resolveContextBudget } from "../config/yuiConfig.js";
|
|
2
|
+
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
3
|
+
export function evaluateSessionContextBudget(events, session, configured) {
|
|
4
|
+
const budget = resolveContextBudget(configured);
|
|
5
|
+
const peak = observedPeakInputTokens(events, session);
|
|
6
|
+
const state = peak >= budget.hardTokens
|
|
7
|
+
? "hard"
|
|
8
|
+
: peak >= budget.softTokens
|
|
9
|
+
? "soft"
|
|
10
|
+
: "within";
|
|
11
|
+
return Object.freeze({ state, peakTokens: peak, budget });
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Largest per-request input peak observed for one Session generation. Usage
|
|
15
|
+
* observations carry cumulative session totals; consecutive deltas bound the
|
|
16
|
+
* per-request input of the window between them. The first observation for a
|
|
17
|
+
* generation is treated as one full request so a long-lived resumed Session
|
|
18
|
+
* that was only sampled late still counts its inherited context.
|
|
19
|
+
*/
|
|
20
|
+
export function observedPeakInputTokens(events, session) {
|
|
21
|
+
let previous = 0;
|
|
22
|
+
let peak = 0;
|
|
23
|
+
for (const event of events) {
|
|
24
|
+
const observation = runtimeObservationFromTaskEvent(event);
|
|
25
|
+
if (observation === null)
|
|
26
|
+
continue;
|
|
27
|
+
if (!matchesSession(observation.fence, session))
|
|
28
|
+
continue;
|
|
29
|
+
const usage = usageTotal(observation.payload.usage);
|
|
30
|
+
if (usage === null)
|
|
31
|
+
continue;
|
|
32
|
+
const delta = Math.max(0, usage - previous);
|
|
33
|
+
if (delta > peak)
|
|
34
|
+
peak = delta;
|
|
35
|
+
previous = usage;
|
|
36
|
+
}
|
|
37
|
+
return peak;
|
|
38
|
+
}
|
|
39
|
+
function matchesSession(fence, session) {
|
|
40
|
+
if (fence.taskId !== undefined && fence.taskId !== session.taskId)
|
|
41
|
+
return false;
|
|
42
|
+
if (fence.roleName !== session.roleName)
|
|
43
|
+
return false;
|
|
44
|
+
if (session.nativeSessionId !== undefined
|
|
45
|
+
&& fence.nativeSessionId !== undefined
|
|
46
|
+
&& fence.nativeSessionId !== session.nativeSessionId) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (session.launchId !== undefined
|
|
50
|
+
&& fence.launchId !== undefined
|
|
51
|
+
&& fence.launchId !== session.launchId) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
function usageTotal(usage) {
|
|
57
|
+
if (typeof usage !== "object" || usage === null || Array.isArray(usage))
|
|
58
|
+
return null;
|
|
59
|
+
const record = usage;
|
|
60
|
+
const inputTokens = integer(record.inputTokens);
|
|
61
|
+
if (inputTokens === null)
|
|
62
|
+
return null;
|
|
63
|
+
const cachedInputTokens = integer(record.cachedInputTokens) ?? 0;
|
|
64
|
+
return inputTokens + cachedInputTokens;
|
|
65
|
+
}
|
|
66
|
+
function integer(value) {
|
|
67
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : null;
|
|
68
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { renderWakeReason } from "../scheduler/wakeReason.js";
|
|
2
|
+
/**
|
|
3
|
+
* Issue 04 (context token budget) — long-term design:
|
|
4
|
+
*
|
|
5
|
+
* A Leader wake is a NOTIFICATION, not a context dump. The wake envelope
|
|
6
|
+
* carries only what the Agent cannot reconstruct from durable records:
|
|
7
|
+
* the wake id, the aggregated reason tags, and the delta window. The Agent
|
|
8
|
+
* reads the delta content on demand with `yui task wake show <wake-id>` and
|
|
9
|
+
* the full projection with `yui task context <task>`.
|
|
10
|
+
*
|
|
11
|
+
* The envelope is mode-agnostic: fresh generations and resumed generations
|
|
12
|
+
* receive the same minimal text. The native Session is a disposable cache of
|
|
13
|
+
* working context; Yui's durable Task records (including the wake ledger) are
|
|
14
|
+
* the checkpoint.
|
|
15
|
+
*/
|
|
16
|
+
/** Structural guardrail: the envelope must stay an order of magnitude below any context budget. */
|
|
17
|
+
export const WAKE_ENVELOPE_HARD_BYTES = 2_000;
|
|
18
|
+
/** Maximum reason tags rendered before elision to a count. */
|
|
19
|
+
const REASON_DISPLAY_LIMIT = 6;
|
|
20
|
+
export function buildTaskWakeEnvelope(reader, request) {
|
|
21
|
+
const task = reader.getTask(request.taskId);
|
|
22
|
+
if (task === null)
|
|
23
|
+
throw new Error(`Task not found: ${request.taskId}.`);
|
|
24
|
+
if (request.reasons.length === 0) {
|
|
25
|
+
throw new Error(`Wake envelope ${request.wakeId} must carry at least one reason.`);
|
|
26
|
+
}
|
|
27
|
+
const fromTime = Date.parse(request.fromCursor);
|
|
28
|
+
const counts = {
|
|
29
|
+
events: reader.listEvents(request.taskId)
|
|
30
|
+
.filter((record) => Date.parse(record.createdAt) > fromTime).length,
|
|
31
|
+
messages: reader.listMessages(request.taskId)
|
|
32
|
+
.filter((record) => Date.parse(record.createdAt) > fromTime).length,
|
|
33
|
+
runs: reader.listAgentRuns(request.taskId)
|
|
34
|
+
.filter((record) => Date.parse(record.createdAt) > fromTime).length
|
|
35
|
+
};
|
|
36
|
+
const lines = [
|
|
37
|
+
`Wake: ${request.wakeId} — delta since ${request.fromCursor}`,
|
|
38
|
+
` Reasons: ${renderReasons(request.reasons)}`,
|
|
39
|
+
` Changed: ${counts.events} events, ${counts.messages} messages, ${counts.runs} runs`
|
|
40
|
+
+ ` → yui task wake show ${request.taskId} ${request.wakeId}`,
|
|
41
|
+
`Full context: yui task context ${request.taskId}`
|
|
42
|
+
];
|
|
43
|
+
const body = lines.join("\n");
|
|
44
|
+
const totalBytes = byteLength(body) + 1;
|
|
45
|
+
if (totalBytes > WAKE_ENVELOPE_HARD_BYTES) {
|
|
46
|
+
throw new Error(`Wake envelope ${request.wakeId} exceeds the structural hard budget of`
|
|
47
|
+
+ ` ${WAKE_ENVELOPE_HARD_BYTES} bytes (${totalBytes}).`);
|
|
48
|
+
}
|
|
49
|
+
return Object.freeze({
|
|
50
|
+
taskId: request.taskId,
|
|
51
|
+
wakeId: request.wakeId,
|
|
52
|
+
fromCursor: request.fromCursor,
|
|
53
|
+
totalBytes,
|
|
54
|
+
text: `${body}\n`
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function renderReasons(reasons) {
|
|
58
|
+
const selected = reasons.slice(0, REASON_DISPLAY_LIMIT);
|
|
59
|
+
const elided = reasons.length - selected.length;
|
|
60
|
+
const rendered = selected.map(renderWakeReason).join(", ");
|
|
61
|
+
return elided === 0 ? rendered : `${rendered}, … (+${elided} more)`;
|
|
62
|
+
}
|
|
63
|
+
function byteLength(value) {
|
|
64
|
+
return Buffer.byteLength(value, "utf8");
|
|
65
|
+
}
|
|
@@ -20,7 +20,6 @@ const ENVIRONMENT_REFRESH_TIMEOUT_MS = 500;
|
|
|
20
20
|
const CONFIGURATION_REFRESH_TIMEOUT_MS = 500;
|
|
21
21
|
const CONTROLLER_OPERATIONAL_ENVIRONMENT = [
|
|
22
22
|
...AGENT_OPERATIONAL_ENVIRONMENT_NAMES,
|
|
23
|
-
"YUI_TMUX_BIN",
|
|
24
23
|
// Issue 01: YUI_STORE_BACKEND/YUI_STORE_WORKER are reserved for tests and
|
|
25
24
|
// explicit recovery commands. A Controller spawned by such a command must
|
|
26
25
|
// inherit the forced backend; otherwise a layout-7 test Home silently opens
|
|
@@ -30,6 +29,8 @@ const CONTROLLER_OPERATIONAL_ENVIRONMENT = [
|
|
|
30
29
|
// rr13/test: Forward the liveness seam so an integration test's Controller
|
|
31
30
|
// subprocess does not reap a saved active Leader Run without a real tmux role.
|
|
32
31
|
"YUI_TEST_ROLE_LIVENESS_PRESENT",
|
|
32
|
+
// Issue 02: agent-signal-driven launch monitoring is Controller-owned.
|
|
33
|
+
"YUI_LAUNCH_INACTIVITY_TIMEOUT_MS",
|
|
33
34
|
...EPHEMERAL_DOMAIN_ENVIRONMENT_NAMES
|
|
34
35
|
];
|
|
35
36
|
/**
|
|
@@ -84,7 +84,8 @@ export function createEphemeralResourceReaper(options) {
|
|
|
84
84
|
return () => reapExpiredEphemeralResources({
|
|
85
85
|
scan,
|
|
86
86
|
clean: (resource) => cleanControllerResource(resource, {
|
|
87
|
-
environment: options.environment
|
|
87
|
+
environment: options.environment,
|
|
88
|
+
tmuxBin: options.tmuxBin
|
|
88
89
|
}),
|
|
89
90
|
onExpiredDomain: options.onExpiredDomain
|
|
90
91
|
});
|