@zq-silk/yui 0.12.0 → 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/README.md +8 -9
- package/dist/cli/commandCatalog.js +11 -6
- 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 +51 -14
- package/dist/commands/configCommands.js +1 -1
- package/dist/commands/executionAuditCommands.js +2 -1
- package/dist/commands/taskCommands.js +80 -29
- package/dist/commands/taskContextCommand.js +6 -2
- package/dist/commands/taskRoleRuntimeStatus.js +31 -7
- package/dist/config/configCatalog.js +1 -1
- package/dist/controller/clientRuntime.js +38 -2
- package/dist/controller/controller.js +23 -15
- package/dist/controller/fileSchedulerStoreAdapter.js +248 -138
- package/dist/controller/runtime.js +56 -1
- package/dist/controller/runtimeHookRunFence.js +19 -4
- package/dist/controller/structuredProviderObservation.js +20 -3
- package/dist/core/controllerClient.js +20 -2
- package/dist/core/controllerServer.js +1 -0
- package/dist/executor/agentExecutor.js +48 -46
- package/dist/executor/fileRoleLaunchPlanner.js +94 -30
- package/dist/lifecycle/exactRunTerminalization.js +68 -3
- package/dist/observability/executionAudit.js +5 -0
- package/dist/release/runtimeRelease.js +20 -0
- package/dist/run/recoveryProjection.js +45 -6
- package/dist/runtime/agentHost.js +159 -85
- package/dist/runtime/conversationSwitch.js +277 -0
- package/dist/runtime/index.js +1 -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 +15 -0
- package/dist/runtime/runtimeProjection.js +6 -7
- package/dist/runtime/tmuxAdapters.js +4 -1
- package/dist/scheduler/activeRoleRunDelivery.js +31 -196
- package/dist/scheduler/leaderWakeupProcessor.js +39 -133
- package/dist/scheduler/roleRunStall.js +53 -17
- package/dist/storage/sqliteSchema.js +57 -24
- package/dist/storage/sqliteStore.js +23 -4
- package/dist/storage/upgrade/homeClassification.js +52 -0
- package/dist/storage/upgrade/offlineUpgradeInventory.js +145 -7
- package/dist/storage/upgrade/upgradeOrchestrator.js +333 -12
- package/dist/task/nextAction.js +0 -34
- package/dist/web/webSnapshot.js +3 -1
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +7 -4
- package/skills/yui-operator/SKILL.md +4 -4
- package/skills/yui-reviewer/SKILL.md +7 -4
- package/dist/lifecycle/taskRoleSessionReset.js +0 -118
|
@@ -9,6 +9,7 @@ import { resolveRuntimeHealth } from "../config/yuiConfig.js";
|
|
|
9
9
|
import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
10
10
|
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
11
11
|
import { projectSessionTokenMetrics, resolveSessionTokenIdentity } from "../runtime/sessionTokenMetrics.js";
|
|
12
|
+
import { freshConversationLaunchBlockers, projectConversationSwitch } from "../runtime/conversationSwitch.js";
|
|
12
13
|
export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes, now = new Date()) {
|
|
13
14
|
const taskOpenInputRequestCount = store.listInputRequests(taskId)
|
|
14
15
|
.filter((request) => request.status === "open").length;
|
|
@@ -93,10 +94,15 @@ export function renderTaskRoleRuntimeStatus(status) {
|
|
|
93
94
|
? `needs-attention (${status.stall.kind ?? "workflow-not-progressing"}; no workflow progress since ${status.stall.progressAt ?? "unknown"})`
|
|
94
95
|
: "none"}`,
|
|
95
96
|
` Native session ${nativeSession}`,
|
|
97
|
+
` Session switch ${status.conversationSwitch === null
|
|
98
|
+
? "none"
|
|
99
|
+
: `${status.conversationSwitch.status} (${status.conversationSwitch.requestId}; ${status.conversationSwitch.reason})`}`,
|
|
96
100
|
` Agent runtime ${runtime}`,
|
|
97
101
|
` Session tokens ${sessionTokens}`,
|
|
98
102
|
` Runtime cleanup ${status.runtimeCleanupPending ? "pending" : "none"}`,
|
|
99
|
-
` Fresh launch ${status.freshLaunchAllowed
|
|
103
|
+
` Fresh launch ${status.freshLaunchAllowed
|
|
104
|
+
? "allowed"
|
|
105
|
+
: `blocked (${status.freshLaunchBlockers.join(", ")})`}`,
|
|
100
106
|
` tmux pane ${tmux}`,
|
|
101
107
|
` Workspace ${status.workspace.managed ? status.workspace.root : status.workspace.path}`,
|
|
102
108
|
...workspaceDetails
|
|
@@ -117,19 +123,30 @@ export function taskRoleLastRunLabel(status) {
|
|
|
117
123
|
}
|
|
118
124
|
export function taskRoleNativeSessionLabel(status) {
|
|
119
125
|
if (status.runtimeCleanupPending && status.nativeSession === null)
|
|
120
|
-
return "
|
|
126
|
+
return "unbound (cleanup-pending)";
|
|
121
127
|
return status.nativeSession?.status ?? "unbound";
|
|
122
128
|
}
|
|
123
129
|
export function inspectTaskRoleSessionRecovery(taskId, roleName, store) {
|
|
124
130
|
const sessions = store.getTaskRoleSessionSet(taskId, roleName);
|
|
125
131
|
const target = runtimeLifecycleTarget({ scope: "task", taskId, roleName });
|
|
126
132
|
const runtimeMailbox = store.getWorkMailbox(target);
|
|
133
|
+
const roleMailbox = store.getWorkMailbox({ kind: "role", taskId, roleName });
|
|
134
|
+
const switchBlockers = freshConversationLaunchBlockers({
|
|
135
|
+
sessions,
|
|
136
|
+
events: store.listEvents(taskId),
|
|
137
|
+
mailbox: roleMailbox,
|
|
138
|
+
roleName
|
|
139
|
+
});
|
|
140
|
+
const freshLaunchBlockers = [
|
|
141
|
+
...switchBlockers,
|
|
142
|
+
...(hasRuntimeLifecycleWork(runtimeMailbox) ? ["runtime-lifecycle-busy"] : [])
|
|
143
|
+
];
|
|
127
144
|
return {
|
|
128
145
|
taskId,
|
|
129
146
|
roleName,
|
|
130
147
|
runtimeCleanupPending: hasRuntimeCleanupObligation(runtimeMailbox),
|
|
131
|
-
freshLaunchAllowed:
|
|
132
|
-
|
|
148
|
+
freshLaunchAllowed: freshLaunchBlockers.length === 0,
|
|
149
|
+
freshLaunchBlockers
|
|
133
150
|
};
|
|
134
151
|
}
|
|
135
152
|
export function taskRoleOpenInputLabel(status) {
|
|
@@ -186,6 +203,7 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
|
|
|
186
203
|
? { managed: false, path: role.workspace }
|
|
187
204
|
: { ...managedWorkspace, managed: true };
|
|
188
205
|
const events = store.listEvents(taskId);
|
|
206
|
+
const conversationSwitch = projectConversationSwitch(events, role.name, sessions);
|
|
189
207
|
const sessionTokens = projectSessionTokenMetrics(events, resolveSessionTokenIdentity(nativeSession === null
|
|
190
208
|
? null
|
|
191
209
|
: { taskId, roleName: role.name, ...nativeSession }));
|
|
@@ -225,6 +243,7 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
|
|
|
225
243
|
tmux,
|
|
226
244
|
workspace,
|
|
227
245
|
sessionTokens,
|
|
246
|
+
conversationSwitch,
|
|
228
247
|
runtime,
|
|
229
248
|
stall
|
|
230
249
|
};
|
|
@@ -243,7 +262,9 @@ function sessionCumulativeTokenLabel(metrics) {
|
|
|
243
262
|
}
|
|
244
263
|
function latestStallKind(events, runId) {
|
|
245
264
|
const event = [...events]
|
|
246
|
-
.filter((candidate) => candidate.type === "run.stalled"
|
|
265
|
+
.filter((candidate) => candidate.type === "run.stalled"
|
|
266
|
+
&& candidate.payload.runId === runId
|
|
267
|
+
&& candidate.payload.status !== "diagnostic-only")
|
|
247
268
|
.sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
|
|
248
269
|
return event?.payload.kind === "delivery-stalled" || event?.payload.kind === "workflow-not-progressing"
|
|
249
270
|
? event.payload.kind
|
|
@@ -253,7 +274,7 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
|
|
|
253
274
|
if (runtimeCleanupPending && nativeSession === null) {
|
|
254
275
|
return {
|
|
255
276
|
health: "needs-attention",
|
|
256
|
-
healthReason: "the native Session
|
|
277
|
+
healthReason: "the native Session is unbound; verified runtime cleanup is pending"
|
|
257
278
|
};
|
|
258
279
|
}
|
|
259
280
|
if (role.status === "failed" || role.status === "exited") {
|
|
@@ -315,12 +336,15 @@ function calculateHealth(role, activeRun, lastRun, nativeSession, runtimeCleanup
|
|
|
315
336
|
case "broken":
|
|
316
337
|
return { health: "failed", healthReason: runtime.healthReason };
|
|
317
338
|
case "stopped":
|
|
318
|
-
case "diagnostic-needed":
|
|
319
339
|
case "ready":
|
|
320
340
|
case "awaiting-provider-acceptance":
|
|
321
341
|
case "runtime-unobservable":
|
|
322
342
|
case "starting":
|
|
323
343
|
return { health: "needs-attention", healthReason: runtime.healthReason };
|
|
344
|
+
case "diagnostic-needed":
|
|
345
|
+
return runtime.observerStatus === "degraded" || runtime.observerStatus === "unavailable"
|
|
346
|
+
? { health: "needs-attention", healthReason: runtime.healthReason }
|
|
347
|
+
: { health: "running", healthReason: runtime.healthReason };
|
|
324
348
|
case "waiting-user":
|
|
325
349
|
return { health: "blocked-input", healthReason: runtime.healthReason };
|
|
326
350
|
case "waiting-permission":
|
|
@@ -13,7 +13,7 @@ export const CONFIG_DEFINITIONS = Object.freeze([
|
|
|
13
13
|
{ key: "time-zone", domain: "system", property: "timeZone", label: "Time zone", summary: "IANA timezone for human-facing timestamps (default: Asia/Shanghai).", takesEffect: "The next human-readable timestamp rendering; durable JSON remains UTC." },
|
|
14
14
|
{ key: "reconciliation-interval-seconds", domain: "runtime", property: "reconciliationIntervalSeconds", label: "Reconciliation interval", summary: "Recovery reconciliation interval, 5-300 seconds (default: 120).", takesEffect: "The running Controller is refreshed after the value is saved." },
|
|
15
15
|
{ key: "controller-task-concurrency", domain: "runtime", property: "controllerTaskConcurrency", label: "Controller Task concurrency", summary: "Maximum Tasks reconciled concurrently, 1-32 (default: 4).", takesEffect: "After the Controller restarts." },
|
|
16
|
-
{ key: "runtime-health", domain: "runtime", property: "runtimeHealth", label: "Runtime health thresholds", summary: "Quiet,
|
|
16
|
+
{ key: "runtime-health", domain: "runtime", property: "runtimeHealth", label: "Runtime health thresholds", summary: "Quiet, checkpoint-overdue, and read-only diagnostic thresholds in seconds; values must be strictly increasing (defaults: 300/900/1800).", takesEffect: "CLI and Web projections immediately; scheduler diagnostics after the Controller restarts." },
|
|
17
17
|
{ key: "agent-launch-inactivity-timeout-seconds", domain: "runtime", property: "agentLaunchInactivityTimeoutSeconds", label: "Agent launch inactivity timeout", summary: "Maximum launch silence before startup fails, 15-3600 seconds (default: 300).", takesEffect: "After the Controller restarts." },
|
|
18
18
|
{ key: "delivery-timeout-seconds", domain: "runtime", property: "deliveryTimeoutSeconds", label: "Delivery timeout", summary: "Total control-plane delivery retry budget, 5-600 seconds (default: 120).", takesEffect: "After the Controller restarts; internal retry cadence remains automatic." },
|
|
19
19
|
{ key: "provider-retry-mode", domain: "runtime", property: "providerRetryMode", label: "Provider retry mode", summary: "Provider retry mode: off, shadow, or enforce (default: enforce).", takesEffect: "The next eligible provider-failure decision." },
|
|
@@ -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
|
|
@@ -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
|
|
@@ -1504,7 +1508,11 @@ export class FileTaskController {
|
|
|
1504
1508
|
}
|
|
1505
1509
|
for (const wakeup of result.wakeups) {
|
|
1506
1510
|
const key = `role:${encodeURIComponent(wakeup.taskId)}/leader`;
|
|
1507
|
-
if (wakeup.
|
|
1511
|
+
if (wakeup.terminalized === true) {
|
|
1512
|
+
settled.add(key);
|
|
1513
|
+
resignal.add(key);
|
|
1514
|
+
}
|
|
1515
|
+
else if (wakeup.reason === "writer-attached") {
|
|
1508
1516
|
writerBlocked.add(key);
|
|
1509
1517
|
}
|
|
1510
1518
|
else if (wakeup.reason === "not-ready") {
|
|
@@ -1690,7 +1698,7 @@ export async function startFileTaskController(home, store, delivery, dispatcher,
|
|
|
1690
1698
|
dispatcherServiceTime.started();
|
|
1691
1699
|
try {
|
|
1692
1700
|
if (stopping) {
|
|
1693
|
-
throw controllerApplicationError("
|
|
1701
|
+
throw controllerApplicationError("CONTROLLER_DRAINING", "Controller is draining.");
|
|
1694
1702
|
}
|
|
1695
1703
|
if (method === "scheduler.signal") {
|
|
1696
1704
|
runtime.signal(signalMailboxKey(params));
|