@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
|
@@ -3,9 +3,10 @@ import { markYuiRunInput } from "../run/runIdentity.js";
|
|
|
3
3
|
import { taskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
4
4
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
5
5
|
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain } from "../executor/effectiveLaunch.js";
|
|
6
|
-
import { hasRuntimeLifecycleWork, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
6
|
+
import { hasRuntimeLifecycleWork, RuntimeLifecycleBusyError, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
7
7
|
import { recordLeaderFailure } from "./leaderFailure.js";
|
|
8
8
|
import { createLeaderRecoveryNotification } from "./operatorNotification.js";
|
|
9
|
+
import { evaluateSessionContextBudget } from "../context/sessionContextBudget.js";
|
|
9
10
|
import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
10
11
|
import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
11
12
|
export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
@@ -49,8 +50,22 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
49
50
|
results.push({ taskId: task.id, status: "skipped", reason: "busy" });
|
|
50
51
|
continue;
|
|
51
52
|
}
|
|
53
|
+
// Single-flight: a Role runtime lifecycle lane that already holds a
|
|
54
|
+
// launch reservation or cleanup obligation must not be entered by a
|
|
55
|
+
// second wake. The wake stays durable (pendingWakeup is not consumed) and
|
|
56
|
+
// is retried after the lane settles; the suppression is recorded for the
|
|
57
|
+
// audit instead of manufacturing a failed Run.
|
|
58
|
+
if (hasRuntimeLifecycleWork(store.getWorkMailbox(runtimeLifecycleTarget({
|
|
59
|
+
scope: "task",
|
|
60
|
+
taskId: task.id,
|
|
61
|
+
roleName: role.name
|
|
62
|
+
})))) {
|
|
63
|
+
store.recordWakeSuppression?.(task.id, "lifecycle-busy", now);
|
|
64
|
+
results.push({ taskId: task.id, status: "skipped", reason: "recovery-blocked" });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
52
67
|
const reopening = wakeup.reasons.includes("task-reopened");
|
|
53
|
-
|
|
68
|
+
let existingSession = store.getRoleSession(task.id, role.name, reopening ? undefined : role.effective.agentId);
|
|
54
69
|
let effectiveSession = existingSession;
|
|
55
70
|
let claimed = false;
|
|
56
71
|
let deliveryAttempted = false;
|
|
@@ -107,9 +122,51 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
107
122
|
&& existingSession.status !== "stopped" && existingSession.status !== "broken") {
|
|
108
123
|
throw new Error(`Leader Session is incompatible with desired effective launch: ${task.id}/${role.name}.`);
|
|
109
124
|
}
|
|
125
|
+
// Issue 04: a native Session generation whose observed per-request
|
|
126
|
+
// input peak crossed the hard context budget is retired before dispatch
|
|
127
|
+
// so the wake starts a fresh generation instead of waiting for
|
|
128
|
+
// provider-side auto-compaction. Durable Task records are the checkpoint;
|
|
129
|
+
// the bounded snapshot below re-establishes working context.
|
|
130
|
+
let contextBudgetAdvisory;
|
|
131
|
+
if (hasNativeSession(existingSession)
|
|
132
|
+
&& compatibleSession
|
|
133
|
+
&& existingSession.status !== "stopped"
|
|
134
|
+
&& existingSession.status !== "broken"
|
|
135
|
+
&& typeof store.listEvents === "function"
|
|
136
|
+
&& typeof store.getContextBudget === "function"
|
|
137
|
+
&& typeof store.rolloverTaskRoleSessionForContextBudget === "function") {
|
|
138
|
+
const budget = evaluateSessionContextBudget(store.listEvents(task.id), {
|
|
139
|
+
taskId: task.id,
|
|
140
|
+
roleName: role.name,
|
|
141
|
+
...(existingSession.nativeSessionId === undefined
|
|
142
|
+
? {}
|
|
143
|
+
: { nativeSessionId: existingSession.nativeSessionId }),
|
|
144
|
+
...(existingSession.launchId === undefined
|
|
145
|
+
? {}
|
|
146
|
+
: { launchId: existingSession.launchId })
|
|
147
|
+
}, store.getContextBudget());
|
|
148
|
+
if (budget.state === "hard") {
|
|
149
|
+
const rollover = store.rolloverTaskRoleSessionForContextBudget({
|
|
150
|
+
taskId: task.id,
|
|
151
|
+
roleName: role.name,
|
|
152
|
+
peakTokens: budget.peakTokens,
|
|
153
|
+
hardTokens: budget.budget.hardTokens,
|
|
154
|
+
now
|
|
155
|
+
});
|
|
156
|
+
if (rollover !== null) {
|
|
157
|
+
existingSession = null;
|
|
158
|
+
effectiveSession = null;
|
|
159
|
+
contextBudgetAdvisory = `Previous Session generation ${rollover.retiredNativeSessionId ?? "unknown"} was retired at the hard context budget (peak ${budget.peakTokens} >= ${budget.budget.hardTokens} tokens). This wake starts a fresh generation; durable Task records preserve all state.`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (budget.state === "soft") {
|
|
163
|
+
contextBudgetAdvisory = `Session context budget advisory: observed per-request input peak ${budget.peakTokens} tokens crossed the soft threshold (${budget.budget.softTokens}). Durable Yui records are the checkpoint; a fresh generation starts automatically at the hard threshold (${budget.budget.hardTokens}).`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
110
166
|
const mode = hasNativeSession(existingSession) && compatibleSession ? "resume" : "new";
|
|
111
167
|
const runId = store.peekNextAgentRunId(task.id);
|
|
112
|
-
const
|
|
168
|
+
const wakeEnvelope = resolveLeaderWakeEnvelope(store, task.id);
|
|
169
|
+
const input = markYuiRunInput(leaderWakeupInput(task.id, runId, wakeEnvelope, contextBudgetAdvisory), runId, taskRoleSessionTitle(task, role.name));
|
|
113
170
|
run = createAgentRun(runId, task.id, role.name, mode, input, now, {
|
|
114
171
|
...(role.managedWorkspace === undefined
|
|
115
172
|
? {}
|
|
@@ -126,6 +183,10 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
126
183
|
// durable, retryable recovery obligation before any provider call.
|
|
127
184
|
session: reopenIdentityDrift ? null : existingSession,
|
|
128
185
|
wakeup,
|
|
186
|
+
...(wakeEnvelope === null ? {} : {
|
|
187
|
+
wakeId: wakeEnvelope.wakeId,
|
|
188
|
+
wakeFromCursor: wakeEnvelope.fromCursor
|
|
189
|
+
}),
|
|
129
190
|
now
|
|
130
191
|
});
|
|
131
192
|
if (claim !== "claimed") {
|
|
@@ -266,6 +327,26 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
266
327
|
const detail = error instanceof Error ? error.message : String(error);
|
|
267
328
|
const message = `Leader dispatch failed: ${detail}`;
|
|
268
329
|
if (claimed && run !== null) {
|
|
330
|
+
// Scheduler single-flight backpressure: the Role runtime lifecycle
|
|
331
|
+
// lane was busy when the launch was reserved. The claimed Run stays
|
|
332
|
+
// active-unpushed; the active-Run delivery path retries it after the
|
|
333
|
+
// lane settles. This is contention before any semantic launch, not a
|
|
334
|
+
// Run failure and not grounds for a Leader failure record.
|
|
335
|
+
if (error instanceof RuntimeLifecycleBusyError) {
|
|
336
|
+
delivery.forgetPrepared?.({
|
|
337
|
+
taskId: task.id,
|
|
338
|
+
roleName: role.name,
|
|
339
|
+
runId: run.id
|
|
340
|
+
});
|
|
341
|
+
results.push({
|
|
342
|
+
taskId: task.id,
|
|
343
|
+
runId: run.id,
|
|
344
|
+
status: "skipped",
|
|
345
|
+
reason: "not-ready",
|
|
346
|
+
error: message
|
|
347
|
+
});
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
269
350
|
// A finite managed provider from the preceding Run may still be
|
|
270
351
|
// exiting after its durable yield. Keep this newly-claimed Run as the
|
|
271
352
|
// sole owner of the Role mailbox; active-Run delivery will retry it
|
|
@@ -396,22 +477,31 @@ function hasNativeSession(session) {
|
|
|
396
477
|
typeof session.nativeSessionId === "string" &&
|
|
397
478
|
session.nativeSessionId.trim().length > 0;
|
|
398
479
|
}
|
|
399
|
-
|
|
480
|
+
/**
|
|
481
|
+
* Issue 04 (long-term design): resolves the minimal wake envelope for a
|
|
482
|
+
* Leader wake. The envelope carries only the aggregated reason tags, the
|
|
483
|
+
* delta window, and read pointers; the Agent reads delta content on demand
|
|
484
|
+
* with `yui task wake show`. Returns null when the adapter lacks the
|
|
485
|
+
* projection, so the wake falls back to the full `yui task context`
|
|
486
|
+
* instruction.
|
|
487
|
+
*/
|
|
488
|
+
function resolveLeaderWakeEnvelope(store, taskId) {
|
|
489
|
+
if (typeof store.getTaskWakeEnvelope !== "function")
|
|
490
|
+
return null;
|
|
491
|
+
return store.getTaskWakeEnvelope(taskId);
|
|
492
|
+
}
|
|
493
|
+
function leaderWakeupInput(taskId, runId, wakeEnvelope, contextBudgetAdvisory) {
|
|
400
494
|
const lines = [
|
|
401
495
|
"Follow the injected yui-leader Skill for this Yui wakeup.",
|
|
402
496
|
`Current Leader Run: ${runId}.`,
|
|
403
497
|
`For every Leader decision, milestone, or Work Item lifecycle command that is meaningful progress, carry this exact current-turn assertion on that command: YUI_LEADER_ACTION_RUN_ID=${runId} YUI_LEADER_ACTION_RECEIPT_ID=${formatAgentRunReceiptId(taskId, runId)}. The native Session environment may retain an older YUI_RUN_ID/launch; never copy those values, and never reuse this assertion after the turn changes.`,
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
"Use narrower Task message, WorkItem, decision, milestone, and input commands only when a specific record needs closer inspection.",
|
|
412
|
-
`When the requested outcome is finished and there are no active Worker Runs or unresolved inputs, complete the Task with yui task complete ${taskId} --summary-file - and a quoted heredoc containing the final outcome and evidence.`,
|
|
413
|
-
"Provider-native subagents remain inside this Leader AgentRun. Yui observes their structured lifecycle and keeps the Run active across intermediate provider Turn boundaries while children remain active; let the provider deliver completion notifications and continue synthesis without polling or yielding merely for that native wait.",
|
|
414
|
-
`A Provider Turn ending does not end this Yui Run. Leave the Run active when later native-subagent results, managed Role results, reviewer results, or user corrections still belong to the same objective; Yui will aggregate those durable events and resume this Session with another Turn. Use yui task run yield ${runId} --summary-file - only for a deliberate Yui-level handoff that should close this Run rather than for ordinary waiting. If used, the yield command must be the final tool action: after it succeeds, stop immediately.`
|
|
498
|
+
...(contextBudgetAdvisory === undefined ? [] : [contextBudgetAdvisory]),
|
|
499
|
+
...(wakeEnvelope === null
|
|
500
|
+
? [`Read the authoritative context with yui task context ${taskId}.`]
|
|
501
|
+
: [
|
|
502
|
+
"Wake envelope (Issue 04):",
|
|
503
|
+
wakeEnvelope.text
|
|
504
|
+
]),
|
|
415
505
|
];
|
|
416
506
|
return lines.join("\n");
|
|
417
507
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { selectedSchedulerRoles, selectedActiveSchedulerTasks } from "./ports.js";
|
|
2
2
|
import { formatTaskRecordReference } from "../task/taskRecordReference.js";
|
|
3
3
|
import { queueLeaderWakeup } from "./wakeupQueue.js";
|
|
4
|
+
import { wakeReason } from "./wakeReason.js";
|
|
4
5
|
import { currentRoleRunProgressAt, DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS } from "./roleRunStall.js";
|
|
5
6
|
export const EXITED_ROLE_RUN_SUMMARY = "The role's tmux session exited before the run yielded.";
|
|
6
7
|
/**
|
|
@@ -109,7 +110,7 @@ export async function reconcileExitedRoleRuns(store, delivery, now, selection, e
|
|
|
109
110
|
// adapter's atomic failure+wake transition. Production returns
|
|
110
111
|
// "failed" and already enqueued this wake in the same transaction.
|
|
111
112
|
if (persisted === undefined && task.status === "active") {
|
|
112
|
-
queueLeaderWakeup(store, task.id, role.name === "leader" ? "leader-run-failed" : "role-run-failed", now);
|
|
113
|
+
queueLeaderWakeup(store, task.id, wakeReason(role.name === "leader" ? "leader-run-failed" : "role-run-failed"), now);
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
return failed;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { selectedSchedulerRoles, selectedActiveSchedulerTasks } from "./ports.js";
|
|
2
2
|
import { nextPendingBatch } from "../coordination/workMailbox.js";
|
|
3
|
+
import { RUNTIME_DIAGNOSTIC_AFTER_MS, SEMANTIC_STALL_WINDOW_MS } from "../runtime/runtimeHealthPolicy.js";
|
|
3
4
|
/**
|
|
4
5
|
* Default window of no durable progress before a live-but-idle Run becomes a
|
|
5
6
|
* traceable needs-attention signal. It is deliberately long: a healthy Run that
|
|
6
7
|
* is simply slow keeps its structured checkpoint fresh and never crosses it.
|
|
7
8
|
*/
|
|
8
|
-
export const DEFAULT_STALL_WINDOW_MS =
|
|
9
|
+
export const DEFAULT_STALL_WINDOW_MS = SEMANTIC_STALL_WINDOW_MS;
|
|
9
10
|
/** Cheap workflow-stall candidate filter; the real threshold remains 30m. */
|
|
10
|
-
export const DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS =
|
|
11
|
+
export const DEFAULT_WORKFLOW_STALL_CANDIDATE_AGE_MS = RUNTIME_DIAGNOSTIC_AFTER_MS;
|
|
11
12
|
export const RUN_PROGRESS_EVENT = "run.progress";
|
|
12
13
|
export const RUN_STALLED_EVENT = "run.stalled";
|
|
13
14
|
export const RUN_RECOVERED_EVENT = "run.recovered";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { requireIdentity, requirePositiveInteger, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
|
+
import { TASK_RECORD_ID_PREFIXES } from "../task/taskRecordReference.js";
|
|
3
|
+
export const CURRENT_TASK_WAKE_SCHEMA_VERSION = 1;
|
|
4
|
+
export function createTaskWake(input) {
|
|
5
|
+
const wake = {
|
|
6
|
+
schemaVersion: CURRENT_TASK_WAKE_SCHEMA_VERSION,
|
|
7
|
+
id: requireIdentity(input.id, "Task wake id"),
|
|
8
|
+
taskId: requireIdentity(input.taskId, "Task id"),
|
|
9
|
+
seq: wakeSequence(input.id),
|
|
10
|
+
reasons: Object.freeze(input.reasons.map((reason) => requireText(reason, "Wake reason"))),
|
|
11
|
+
fromCursor: requireTimestamp(input.fromCursor, "Wake fromCursor"),
|
|
12
|
+
toCursor: requireTimestamp(input.toCursor, "Wake toCursor"),
|
|
13
|
+
status: "dispatched",
|
|
14
|
+
runId: requireIdentity(input.runId, "Wake run id"),
|
|
15
|
+
createdAt: input.now.toISOString()
|
|
16
|
+
};
|
|
17
|
+
validateTaskWake(wake);
|
|
18
|
+
return wake;
|
|
19
|
+
}
|
|
20
|
+
export function validateTaskWake(wake) {
|
|
21
|
+
if (wake.schemaVersion !== CURRENT_TASK_WAKE_SCHEMA_VERSION) {
|
|
22
|
+
throw new Error(`Task wake ${wake.id} must use schemaVersion ${CURRENT_TASK_WAKE_SCHEMA_VERSION}.`);
|
|
23
|
+
}
|
|
24
|
+
if (wake.seq !== wakeSequence(wake.id)) {
|
|
25
|
+
throw new Error(`Task wake id/seq mismatch: ${wake.id} vs ${wake.seq}.`);
|
|
26
|
+
}
|
|
27
|
+
if (wake.reasons.length === 0) {
|
|
28
|
+
throw new Error(`Task wake ${wake.id} must carry at least one reason.`);
|
|
29
|
+
}
|
|
30
|
+
if (Date.parse(wake.fromCursor) > Date.parse(wake.toCursor)) {
|
|
31
|
+
throw new Error(`Task wake ${wake.id} cursor window is inverted.`);
|
|
32
|
+
}
|
|
33
|
+
if (wake.status !== "dispatched" && wake.status !== "consumed") {
|
|
34
|
+
throw new Error(`Task wake ${wake.id} has invalid status: ${String(wake.status)}.`);
|
|
35
|
+
}
|
|
36
|
+
if (wake.status === "consumed" && wake.consumedAt === undefined) {
|
|
37
|
+
throw new Error(`Task wake ${wake.id} is consumed without consumedAt.`);
|
|
38
|
+
}
|
|
39
|
+
if (wake.status === "dispatched" && wake.consumedAt !== undefined) {
|
|
40
|
+
throw new Error(`Task wake ${wake.id} is dispatched but carries consumedAt.`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function markTaskWakeConsumed(wake, now) {
|
|
44
|
+
if (wake.status === "consumed")
|
|
45
|
+
return wake;
|
|
46
|
+
return Object.freeze({ ...wake, status: "consumed", consumedAt: now.toISOString() });
|
|
47
|
+
}
|
|
48
|
+
/** Latest wake by sequence, or null when the ledger is empty. */
|
|
49
|
+
export function latestTaskWake(wakes) {
|
|
50
|
+
let latest = null;
|
|
51
|
+
for (const wake of wakes) {
|
|
52
|
+
if (latest === null || wake.seq > latest.seq)
|
|
53
|
+
latest = wake;
|
|
54
|
+
}
|
|
55
|
+
return latest;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The delta lower bound for a Task's first wake: the last Leader Run's
|
|
59
|
+
* creation time, preserving the pre-ledger watermark semantics. When no
|
|
60
|
+
* Leader Run exists, the Task's own creation time bounds the window.
|
|
61
|
+
*/
|
|
62
|
+
export function fallbackWakeCursor(input) {
|
|
63
|
+
return input.leaderRunCreatedAt ?? input.taskCreatedAt;
|
|
64
|
+
}
|
|
65
|
+
function wakeSequence(id) {
|
|
66
|
+
const prefix = `${TASK_RECORD_ID_PREFIXES.taskWake}-`;
|
|
67
|
+
if (!id.startsWith(prefix)) {
|
|
68
|
+
throw new Error(`Task wake id is invalid: ${id}.`);
|
|
69
|
+
}
|
|
70
|
+
const seq = Number(id.slice(prefix.length));
|
|
71
|
+
return requirePositiveInteger(seq, "Task wake seq");
|
|
72
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical taxonomy for Leader wake reasons.
|
|
3
|
+
*
|
|
4
|
+
* The leader-role WorkMailbox pending batch is the durable aggregation bucket;
|
|
5
|
+
* it stores reason tags as strings. This module is the single taxonomy that
|
|
6
|
+
* producers use to build those tags and that the wake envelope uses to render
|
|
7
|
+
* them together with their record references. The mailbox stays a generic
|
|
8
|
+
* queue — the wake layer owns the meaning of its tags.
|
|
9
|
+
*
|
|
10
|
+
* Tag wire format: `<kind>` or `<kind>:<ref>`. The ref is an exact record
|
|
11
|
+
* identifier (InputRequest id, AgentRun id, ...) or, for `force-wake`, the
|
|
12
|
+
* operator-supplied text. Unknown kinds parse losslessly so a newer producer
|
|
13
|
+
* can never wedge an older reader.
|
|
14
|
+
*/
|
|
15
|
+
export const WAKE_REASON_KINDS = Object.freeze([
|
|
16
|
+
"role-result",
|
|
17
|
+
"task-orphaned",
|
|
18
|
+
"input-timeout",
|
|
19
|
+
"role-run-stalled",
|
|
20
|
+
"review-failed",
|
|
21
|
+
"task-reopened",
|
|
22
|
+
"leader-run-failed",
|
|
23
|
+
"role-run-failed",
|
|
24
|
+
"job-finished",
|
|
25
|
+
"force-wake"
|
|
26
|
+
]);
|
|
27
|
+
const IMMEDIATE_KINDS = new Set([
|
|
28
|
+
"task-reopened",
|
|
29
|
+
"leader-run-failed",
|
|
30
|
+
"role-run-failed"
|
|
31
|
+
]);
|
|
32
|
+
export function wakeReason(kind, ref) {
|
|
33
|
+
if (ref === undefined)
|
|
34
|
+
return kind;
|
|
35
|
+
const normalized = ref.trim();
|
|
36
|
+
if (normalized.length === 0)
|
|
37
|
+
throw new Error(`Wake reason ref is required: ${kind}.`);
|
|
38
|
+
if (normalized.includes(":")) {
|
|
39
|
+
throw new Error(`Wake reason ref must not contain ':': ${normalized}.`);
|
|
40
|
+
}
|
|
41
|
+
return `${kind}:${normalized}`;
|
|
42
|
+
}
|
|
43
|
+
export function parseWakeReason(tag) {
|
|
44
|
+
const normalized = tag.trim();
|
|
45
|
+
if (normalized.length === 0)
|
|
46
|
+
throw new Error("Wake reason tag is required.");
|
|
47
|
+
const separator = normalized.indexOf(":");
|
|
48
|
+
const kind = separator < 0 ? normalized : normalized.slice(0, separator);
|
|
49
|
+
const ref = separator < 0 ? undefined : normalized.slice(separator + 1);
|
|
50
|
+
return {
|
|
51
|
+
kind,
|
|
52
|
+
...(ref === undefined || ref.length === 0 ? {} : { ref }),
|
|
53
|
+
immediate: IMMEDIATE_KINDS.has(kind)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** One-line rendering for the wake envelope: `kind (ref)` or `kind`. */
|
|
57
|
+
export function renderWakeReason(tag) {
|
|
58
|
+
const reason = parseWakeReason(tag);
|
|
59
|
+
return reason.ref === undefined ? reason.kind : `${reason.kind} (${reason.ref})`;
|
|
60
|
+
}
|
|
61
|
+
/** True when any aggregated reason demands prompt dispatch. */
|
|
62
|
+
export function hasImmediateWakeReason(tags) {
|
|
63
|
+
return tags.some((tag) => parseWakeReason(tag).immediate);
|
|
64
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { mergePendingWakeup } from "./pendingWakeup.js";
|
|
2
|
+
import { wakeReason } from "./wakeReason.js";
|
|
2
3
|
export function queueLeaderWakeup(store, taskId, reason, now) {
|
|
3
4
|
if (store.enqueueLeaderWakeup !== undefined) {
|
|
4
5
|
return store.enqueueLeaderWakeup(taskId, reason, now);
|
|
@@ -12,5 +13,5 @@ export function queueLeaderWakeupAfterYield(store, task, run, now) {
|
|
|
12
13
|
throw new Error(`AgentRun belongs to another Task: ${run.taskId}.`);
|
|
13
14
|
if (task.status !== "active" || run.roleName === "leader")
|
|
14
15
|
return null;
|
|
15
|
-
return queueLeaderWakeup(store, task.id, "role-result", now);
|
|
16
|
+
return queueLeaderWakeup(store, task.id, wakeReason("role-result"), now);
|
|
16
17
|
}
|
|
@@ -518,7 +518,7 @@ function parseSetupOptions(args) {
|
|
|
518
518
|
throw usageError("Setup usage: yui setup [tmux]");
|
|
519
519
|
}
|
|
520
520
|
async function setupTmux(env, executor, question) {
|
|
521
|
-
const command =
|
|
521
|
+
const command = "tmux";
|
|
522
522
|
if (hasExecutable(command, ["-V"], executor))
|
|
523
523
|
return ["Tmux already installed."];
|
|
524
524
|
const plan = detectTmuxInstallPlan(env, executor);
|