@zq-silk/yui 0.8.6 → 0.8.8
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 +20 -3
- package/dist/cli/commandCatalog.js +20 -3
- package/dist/cli/updatePorts.js +6 -0
- package/dist/cli.js +147 -44
- package/dist/commands/globalRoleCommands.js +8 -4
- package/dist/commands/releaseCommands.js +44 -8
- package/dist/commands/taskCommands.js +141 -9
- package/dist/commands/taskContextCommand.js +5 -3
- package/dist/commands/taskNextActionCommand.js +4 -2
- package/dist/commands/taskOverviewCommand.js +2 -1
- package/dist/commands/taskRoleRuntimeStatus.js +2 -1
- package/dist/context/runContextPack.js +9 -5
- package/dist/context/sessionBootstrapManifest.js +77 -11
- package/dist/context/wakeNotification.js +5 -3
- package/dist/controller/clientRuntime.js +8 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +3 -2
- package/dist/executor/agentExecutor.js +4 -4
- package/dist/executor/fileRoleLaunchPlanner.js +21 -34
- package/dist/release/releaseHandover.js +2 -2
- package/dist/release/runtimeRelease.js +4 -3
- package/dist/review/reviewOutcomeClassifier.js +15 -4
- package/dist/review/taskFinalReviewContractRebind.js +28 -11
- package/dist/runtime/exactControlPlane.js +47 -37
- package/dist/runtime/firstProgressStopLoss.js +3 -1
- package/dist/scheduler/actionability.js +4 -2
- package/dist/scheduler/activeTaskProgress.js +2 -1
- package/dist/scheduler/taskExecutionProjection.js +13 -4
- package/dist/storage/sqliteStore.js +12 -4
- package/dist/storage/taskStore.js +6 -4
- package/dist/task/nextAction.js +8 -3
- package/dist/task/taskRecordRetirement.js +72 -0
- package/dist/workItem/workItem.js +6 -4
- package/i18n/README.zh-CN.md +20 -1
- package/package.json +1 -1
- package/skills/yui-operator/SKILL.md +7 -0
- package/skills/yui-runtime/SKILL.md +6 -6
|
@@ -5,6 +5,7 @@ import { buildRunContextPack, buildRunContextDelta, contextSnapshotDeltaRefIds,
|
|
|
5
5
|
import { contextSnapshotRef } from "../context/contextSnapshot.js";
|
|
6
6
|
import { CliError, dataError, roleNotFound, runtimeError, taskNotFound, usageError } from "../errors/cliError.js";
|
|
7
7
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
8
|
+
import { createTaskRecordRetirement, isTaskRecordRetired, taskRecordRetirement } from "../task/taskRecordRetirement.js";
|
|
8
9
|
import { clearMatchingLeaderStallAttention, isRoleRunStalled, RUN_PROGRESS_EVENT, RUN_RECOVERED_EVENT } from "../scheduler/roleRunStall.js";
|
|
9
10
|
import { readCommandText } from "./textInput.js";
|
|
10
11
|
import { assertTaskCompletionPublishedTreeProof } from "./taskCompletionGate.js";
|
|
@@ -1348,23 +1349,79 @@ function taskMessageCommand(args, store, options) {
|
|
|
1348
1349
|
}
|
|
1349
1350
|
if (messages.length === 0)
|
|
1350
1351
|
return "No messages found.\n";
|
|
1352
|
+
const retirements = new Map(store.listEvents(task.id).flatMap((event) => {
|
|
1353
|
+
const retirement = taskRecordRetirement(event);
|
|
1354
|
+
return retirement?.recordKind === "message"
|
|
1355
|
+
? [[retirement.recordId, retirement]]
|
|
1356
|
+
: [];
|
|
1357
|
+
}));
|
|
1351
1358
|
const timeZone = store.getConfig().timeZone;
|
|
1352
1359
|
return `${renderTable(`Task messages: ${task.id}`, [
|
|
1353
1360
|
{ header: "Message", minWidth: 7, maxWidth: 18 },
|
|
1361
|
+
{ header: "Status", minWidth: 6, maxWidth: 9 },
|
|
1354
1362
|
{ header: "Author", minWidth: 6, maxWidth: 18 },
|
|
1355
1363
|
{ header: "Created", minWidth: 10, maxWidth: 28 },
|
|
1356
1364
|
{ header: "Body", minWidth: 8, maxWidth: 72 }
|
|
1357
1365
|
], messages.map((message) => [
|
|
1358
1366
|
message.id,
|
|
1367
|
+
retirements.has(message.id) ? "retired" : "active",
|
|
1359
1368
|
taskMessageAuthorLabel(message.author),
|
|
1360
1369
|
presentTime(message.createdAt, timeZone),
|
|
1361
1370
|
message.body
|
|
1362
1371
|
]), defaultTableWidth())}\n`;
|
|
1363
1372
|
}
|
|
1373
|
+
if (command === "retire") {
|
|
1374
|
+
return retireMessage(rest, store, options);
|
|
1375
|
+
}
|
|
1364
1376
|
throw usageError(command === undefined
|
|
1365
1377
|
? "Task message command is required."
|
|
1366
1378
|
: `Unknown command: task message ${command}`);
|
|
1367
1379
|
}
|
|
1380
|
+
function retireMessage(args, store, options) {
|
|
1381
|
+
const usage = "Task message retire usage: yui task message retire <task>/<message> --reason <text>.";
|
|
1382
|
+
const parsed = parseTail(args, new Set(["--reason"]), usage);
|
|
1383
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
1384
|
+
const reason = requiredOption(parsed.options, "--reason");
|
|
1385
|
+
const reference = taskRecordReference(parsed.positionals[0], "message", "Message reference", options);
|
|
1386
|
+
const now = clock(options);
|
|
1387
|
+
const result = store.transaction((tx) => {
|
|
1388
|
+
const task = requireTask(tx, reference.taskId);
|
|
1389
|
+
assertTaskOpen(task);
|
|
1390
|
+
const actor = taskActor(options, task.id);
|
|
1391
|
+
if (actor === "leader") {
|
|
1392
|
+
throw usageError("Only the user or global Operator may retire a Task Message.");
|
|
1393
|
+
}
|
|
1394
|
+
const message = tx.listMessages(task.id).find(({ id }) => id === reference.localId);
|
|
1395
|
+
if (message === undefined) {
|
|
1396
|
+
throw dataError(`Task Message not found: ${task.id}/${reference.localId}.`);
|
|
1397
|
+
}
|
|
1398
|
+
const events = tx.listEvents(task.id);
|
|
1399
|
+
if (isTaskRecordRetired(events, "message", message.id)) {
|
|
1400
|
+
return { task, message, changed: false };
|
|
1401
|
+
}
|
|
1402
|
+
// Remove an isolated pending wake for this exact directive. A merged batch
|
|
1403
|
+
// is retained because its other signals remain actionable; context and
|
|
1404
|
+
// actionability projections still filter the retired message below.
|
|
1405
|
+
try {
|
|
1406
|
+
settleExactWorkExecution(tx, leaderMailbox(task.id), messageRef(task.id, message.id));
|
|
1407
|
+
}
|
|
1408
|
+
catch {
|
|
1409
|
+
// Merged pending work cannot be split without losing unrelated signals.
|
|
1410
|
+
}
|
|
1411
|
+
tx.saveEvent(task.id, createTaskRecordRetirement({
|
|
1412
|
+
eventId: tx.nextEventId(task.id),
|
|
1413
|
+
taskId: task.id,
|
|
1414
|
+
recordKind: "message",
|
|
1415
|
+
recordId: message.id,
|
|
1416
|
+
reason,
|
|
1417
|
+
retiredBy: actor
|
|
1418
|
+
}, now));
|
|
1419
|
+
return { task, message, changed: true };
|
|
1420
|
+
});
|
|
1421
|
+
if (result.changed)
|
|
1422
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
1423
|
+
return `Retired Task Message ${result.task.id}/${result.message.id}\n`;
|
|
1424
|
+
}
|
|
1368
1425
|
/**
|
|
1369
1426
|
* Issue 05: force-wake escape hatch. Bypasses the actionability digest and
|
|
1370
1427
|
* enqueues exactly one Leader wakeup with an auditable reason. The reason is
|
|
@@ -2659,9 +2716,7 @@ function retireWork(args, store, options) {
|
|
|
2659
2716
|
if (task.status !== "active") {
|
|
2660
2717
|
throw usageError(`Task is not active: ${task.id}/${task.status}.`);
|
|
2661
2718
|
}
|
|
2662
|
-
|
|
2663
|
-
throw usageError("Only the Task Leader may retire a Work Item.");
|
|
2664
|
-
}
|
|
2719
|
+
const actor = taskActor(options, task.id);
|
|
2665
2720
|
if (replacementWorkItemId !== undefined) {
|
|
2666
2721
|
const replacement = requireWorkItem(tx, replacementWorkItemId, options);
|
|
2667
2722
|
if (replacement.taskId !== task.id) {
|
|
@@ -2701,7 +2756,7 @@ function retireWork(args, store, options) {
|
|
|
2701
2756
|
}
|
|
2702
2757
|
}
|
|
2703
2758
|
const next = retireWorkItem(item, {
|
|
2704
|
-
by:
|
|
2759
|
+
by: actor,
|
|
2705
2760
|
summary,
|
|
2706
2761
|
...(replacementWorkItemId === undefined ? {} : { replacementWorkItemId })
|
|
2707
2762
|
}, now);
|
|
@@ -2713,8 +2768,16 @@ function retireWork(args, store, options) {
|
|
|
2713
2768
|
...(replacementWorkItemId === undefined
|
|
2714
2769
|
? {}
|
|
2715
2770
|
: { replacementWorkItemId }),
|
|
2716
|
-
...leaderActionEventPayload(tx, task.id, options)
|
|
2771
|
+
...(actor === "leader" ? leaderActionEventPayload(tx, task.id, options) : { retiredBy: actor })
|
|
2717
2772
|
}, now);
|
|
2773
|
+
tx.saveEvent(task.id, createTaskRecordRetirement({
|
|
2774
|
+
eventId: tx.nextEventId(task.id),
|
|
2775
|
+
taskId: task.id,
|
|
2776
|
+
recordKind: "work-item",
|
|
2777
|
+
recordId: next.id,
|
|
2778
|
+
reason: summary,
|
|
2779
|
+
retiredBy: actor
|
|
2780
|
+
}, now));
|
|
2718
2781
|
}
|
|
2719
2782
|
return next;
|
|
2720
2783
|
});
|
|
@@ -3801,10 +3864,64 @@ function taskRunCommand(args, store, options) {
|
|
|
3801
3864
|
return yieldRunStatus(rest, store, options);
|
|
3802
3865
|
if (command === "checkpoint")
|
|
3803
3866
|
return output(checkpointRun(rest, store, options));
|
|
3867
|
+
if (command === "retire")
|
|
3868
|
+
return retireRun(rest, store, options);
|
|
3804
3869
|
throw usageError(command === undefined
|
|
3805
3870
|
? "Task run command is required."
|
|
3806
3871
|
: `Unknown command: task run ${command}`);
|
|
3807
3872
|
}
|
|
3873
|
+
function retireRun(args, store, options) {
|
|
3874
|
+
const usage = "Task run retire usage: yui task run retire <task>/<run> --reason <text>.";
|
|
3875
|
+
const parsed = parseTail(args, new Set(["--reason"]), usage);
|
|
3876
|
+
exactPositionals(parsed.positionals, 1, usage);
|
|
3877
|
+
const reason = requiredOption(parsed.options, "--reason");
|
|
3878
|
+
const reference = taskRecordReference(parsed.positionals[0], "agentRun", "Agent Run reference", options);
|
|
3879
|
+
const now = clock(options);
|
|
3880
|
+
const result = store.transaction((tx) => {
|
|
3881
|
+
const task = requireTask(tx, reference.taskId);
|
|
3882
|
+
assertTaskOpen(task);
|
|
3883
|
+
const actor = taskActor(options, task.id);
|
|
3884
|
+
if (actor === "leader") {
|
|
3885
|
+
throw usageError("Only the user or global Operator may retire an Agent Run.");
|
|
3886
|
+
}
|
|
3887
|
+
let run = tx.getAgentRun(task.id, reference.localId);
|
|
3888
|
+
if (run === null)
|
|
3889
|
+
throw dataError(`Agent Run not found: ${task.id}/${reference.localId}.`);
|
|
3890
|
+
const events = tx.listEvents(task.id);
|
|
3891
|
+
if (isTaskRecordRetired(events, "agent-run", run.id)) {
|
|
3892
|
+
return { task, run, changed: false };
|
|
3893
|
+
}
|
|
3894
|
+
if (run.status === "active") {
|
|
3895
|
+
const terminal = terminalizeExactTaskRun(tx, {
|
|
3896
|
+
taskId: task.id,
|
|
3897
|
+
roleName: run.roleName,
|
|
3898
|
+
agentId: run.effective.agentId,
|
|
3899
|
+
runId: run.id,
|
|
3900
|
+
receiptId: agentRunDeliveryReceiptId(run),
|
|
3901
|
+
outcome: { status: "failed", summary: `Agent Run retired: ${reason}` }
|
|
3902
|
+
}, now);
|
|
3903
|
+
if (terminal.disposition !== "applied" || terminal.run === null) {
|
|
3904
|
+
throw usageError(`Agent Run changed during retirement: ${run.id}/${terminal.reason ?? "obsolete"}.`);
|
|
3905
|
+
}
|
|
3906
|
+
run = terminal.run;
|
|
3907
|
+
}
|
|
3908
|
+
tx.saveEvent(task.id, createTaskRecordRetirement({
|
|
3909
|
+
eventId: tx.nextEventId(task.id),
|
|
3910
|
+
taskId: task.id,
|
|
3911
|
+
recordKind: "agent-run",
|
|
3912
|
+
recordId: run.id,
|
|
3913
|
+
reason,
|
|
3914
|
+
retiredBy: actor
|
|
3915
|
+
}, now));
|
|
3916
|
+
return { task, run, changed: true };
|
|
3917
|
+
});
|
|
3918
|
+
if (result.changed)
|
|
3919
|
+
options.runtime?.notifyStateChanged(result.task.id);
|
|
3920
|
+
return output(`Retired Agent Run ${result.task.id}/${result.run.id}\n`, {
|
|
3921
|
+
agentRun: result.run,
|
|
3922
|
+
retired: true
|
|
3923
|
+
});
|
|
3924
|
+
}
|
|
3808
3925
|
function runContextCommand(args, store, options) {
|
|
3809
3926
|
const [first, ...rest] = args;
|
|
3810
3927
|
if (first === "expand") {
|
|
@@ -3878,6 +3995,7 @@ function listRuns(args, store, options) {
|
|
|
3878
3995
|
const runs = store.listAgentRuns(item.taskId).filter((run) => run.workItemId === item.id);
|
|
3879
3996
|
if (runs.length === 0)
|
|
3880
3997
|
return "No runs found.\n";
|
|
3998
|
+
const events = store.listEvents(item.taskId);
|
|
3881
3999
|
return `${renderTable(`Runs: ${item.id}`, [
|
|
3882
4000
|
{ header: "Run", minWidth: 6, maxWidth: 20 },
|
|
3883
4001
|
{ header: "Role", minWidth: 4, maxWidth: 22 },
|
|
@@ -3887,6 +4005,7 @@ function listRuns(args, store, options) {
|
|
|
3887
4005
|
{ header: "Profile", minWidth: 7, maxWidth: 8 },
|
|
3888
4006
|
{ header: "Permission", minWidth: 8, maxWidth: 16 },
|
|
3889
4007
|
{ header: "Status", minWidth: 6, maxWidth: 12 },
|
|
4008
|
+
{ header: "History", minWidth: 7, maxWidth: 9 },
|
|
3890
4009
|
{ header: "Summary", minWidth: 8, maxWidth: 58 }
|
|
3891
4010
|
], runs.map((run) => [
|
|
3892
4011
|
run.id,
|
|
@@ -3897,6 +4016,7 @@ function listRuns(args, store, options) {
|
|
|
3897
4016
|
run.effective.profileAccess,
|
|
3898
4017
|
run.effective.permission.strategy,
|
|
3899
4018
|
run.status,
|
|
4019
|
+
isTaskRecordRetired(events, "agent-run", run.id) ? "retired" : "active",
|
|
3900
4020
|
run.summary ?? "-"
|
|
3901
4021
|
]), defaultTableWidth())}\n`;
|
|
3902
4022
|
}
|
|
@@ -4907,14 +5027,21 @@ function showRun(args, store, options) {
|
|
|
4907
5027
|
const facts = readRunRecoveryFacts(tx, run.taskId, run.id);
|
|
4908
5028
|
if (facts === null)
|
|
4909
5029
|
throw usageError(`Agent Run not found: ${run.taskId}/${run.id}.`, usage);
|
|
4910
|
-
|
|
5030
|
+
const retirement = tx.listEvents(run.taskId)
|
|
5031
|
+
.map(taskRecordRetirement)
|
|
5032
|
+
.find((entry) => entry?.recordKind === "agent-run" && entry.recordId === run.id) ?? null;
|
|
5033
|
+
return { run, recovery: projectRunRecovery(facts), retirement };
|
|
4911
5034
|
});
|
|
4912
5035
|
if (asJson) {
|
|
4913
5036
|
return { kind: "output", output: `${JSON.stringify(data, null, 2)}\n`, data };
|
|
4914
5037
|
}
|
|
4915
|
-
return {
|
|
5038
|
+
return {
|
|
5039
|
+
kind: "output",
|
|
5040
|
+
output: renderRunShow(data.run, data.recovery, data.retirement),
|
|
5041
|
+
data
|
|
5042
|
+
};
|
|
4916
5043
|
}
|
|
4917
|
-
function renderRunShow(run, recovery) {
|
|
5044
|
+
function renderRunShow(run, recovery, retirement) {
|
|
4918
5045
|
const lines = [
|
|
4919
5046
|
`Run: ${run.id}`,
|
|
4920
5047
|
`Task: ${run.taskId}`,
|
|
@@ -4922,6 +5049,10 @@ function renderRunShow(run, recovery) {
|
|
|
4922
5049
|
`Purpose: ${run.purpose}`,
|
|
4923
5050
|
`Mode: ${run.mode}`,
|
|
4924
5051
|
`Status: ${run.status}`,
|
|
5052
|
+
...(retirement === null ? [] : [
|
|
5053
|
+
`History: retired by ${retirement.retiredBy}`,
|
|
5054
|
+
`Retirement reason: ${retirement.reason}`
|
|
5055
|
+
]),
|
|
4925
5056
|
`Effective: ${run.effective.agentId}/${run.effective.adapterId} r${run.effective.sourceDesiredRevision}`,
|
|
4926
5057
|
`Created: ${run.createdAt}`,
|
|
4927
5058
|
...(run.pushedAt === undefined ? [] : [`Pushed: ${run.pushedAt}`]),
|
|
@@ -6065,7 +6196,8 @@ function taskRecordReference(value, kind, label, options) {
|
|
|
6065
6196
|
function assertWorkItemDependenciesCompleted(store, item) {
|
|
6066
6197
|
for (const dependencyId of item.dependsOn) {
|
|
6067
6198
|
const dependency = store.getWorkItem(item.taskId, dependencyId);
|
|
6068
|
-
if (dependency === null
|
|
6199
|
+
if (dependency === null
|
|
6200
|
+
|| (dependency.status !== "completed" && dependency.status !== "retired")) {
|
|
6069
6201
|
throw usageError(`Work Item dependency is not completed: ${dependencyId}.`);
|
|
6070
6202
|
}
|
|
6071
6203
|
}
|
|
@@ -8,6 +8,7 @@ import { taskDeliveryPath } from "../task/task.js";
|
|
|
8
8
|
import { inspectTaskRoleSessionRecovery } from "./taskRoleRuntimeStatus.js";
|
|
9
9
|
import { summarizeExecutionGroup } from "../execution/executionGroup.js";
|
|
10
10
|
import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
|
|
11
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
11
12
|
const RECENT_RECORD_LIMIT = 5;
|
|
12
13
|
const RELATED_RECORD_LIMIT = 5;
|
|
13
14
|
const SUMMARY_TEXT_LIMIT = 400;
|
|
@@ -41,7 +42,8 @@ export function runTaskContextCommand(args, store) {
|
|
|
41
42
|
roleName: role.name
|
|
42
43
|
}))
|
|
43
44
|
].filter((mailbox) => mailbox !== null);
|
|
44
|
-
const
|
|
45
|
+
const events = reader.listEvents(task.id);
|
|
46
|
+
const agentRuns = chronological(operationalTaskRecords(reader.listAgentRuns(task.id), events, "agent-run"));
|
|
45
47
|
const reviewRounds = chronological(reader.listReviewRounds(task.id));
|
|
46
48
|
const changeSets = chronological(reader.listChangeSets(task.id));
|
|
47
49
|
const integrations = chronological(reader.listIntegrationAttempts(task.id));
|
|
@@ -70,10 +72,10 @@ export function runTaskContextCommand(args, store) {
|
|
|
70
72
|
changeSets,
|
|
71
73
|
integrations,
|
|
72
74
|
publications,
|
|
73
|
-
messages: reader.listMessages(task.id),
|
|
75
|
+
messages: operationalTaskRecords(reader.listMessages(task.id), events, "message"),
|
|
74
76
|
openInputRequests: inputRequests.filter((request) => request.status === "open"),
|
|
75
77
|
resolvedInputRequests: inputRequests.filter((request) => request.status !== "open"),
|
|
76
|
-
events
|
|
78
|
+
events,
|
|
77
79
|
nextAction: projectNextAction(nextActionFacts)
|
|
78
80
|
};
|
|
79
81
|
});
|
|
@@ -4,6 +4,7 @@ import { projectCompletionReadiness } from "../task/completionReadiness.js";
|
|
|
4
4
|
import { extractReviewFindings, planRepairWave } from "../task/repairWave.js";
|
|
5
5
|
import { taskDeliveryPath } from "../task/task.js";
|
|
6
6
|
import { projectTaskOrchestration } from "../observability/orchestrationMetrics.js";
|
|
7
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
7
8
|
/**
|
|
8
9
|
* Issue 07 (Leader convergence): read-only `yui task next-action <task>`.
|
|
9
10
|
* Folds the existing durable records into exactly one protocol-level next
|
|
@@ -57,9 +58,10 @@ export function runTaskNextActionCommand(args, store) {
|
|
|
57
58
|
throw taskNotFound(taskId);
|
|
58
59
|
completionReadiness = projectCompletionReadiness(readinessFacts);
|
|
59
60
|
}
|
|
61
|
+
const events = reader.listEvents(taskId);
|
|
60
62
|
const orchestration = projectTaskOrchestration({
|
|
61
63
|
task: reader.getTask(taskId),
|
|
62
|
-
runs: reader.listAgentRuns(taskId),
|
|
64
|
+
runs: operationalTaskRecords(reader.listAgentRuns(taskId), events, "agent-run"),
|
|
63
65
|
roleSessionSets: reader.listRoleSessionSets(taskId),
|
|
64
66
|
workItems: reader.listWorkItems(taskId),
|
|
65
67
|
changeSets: reader.listChangeSets(taskId),
|
|
@@ -69,7 +71,7 @@ export function runTaskNextActionCommand(args, store) {
|
|
|
69
71
|
durableJobs: reader.listDurableJobs(taskId),
|
|
70
72
|
publications: reader.listPublicationReferences(taskId),
|
|
71
73
|
decisions: reader.listDecisions(taskId),
|
|
72
|
-
events
|
|
74
|
+
events,
|
|
73
75
|
managedWorkspaces: reader.listManagedWorkspaces(taskId)
|
|
74
76
|
});
|
|
75
77
|
return {
|
|
@@ -226,7 +226,8 @@ function collectBlockers(workItems, openInputRequests, attention) {
|
|
|
226
226
|
}
|
|
227
227
|
if (item.status !== "pending")
|
|
228
228
|
continue;
|
|
229
|
-
const dependencies = item.dependsOn.filter((dependency) => (workById.get(dependency)?.status !== "completed"
|
|
229
|
+
const dependencies = item.dependsOn.filter((dependency) => (workById.get(dependency)?.status !== "completed"
|
|
230
|
+
&& workById.get(dependency)?.status !== "retired"));
|
|
230
231
|
if (dependencies.length === 0)
|
|
231
232
|
continue;
|
|
232
233
|
blockers.push({
|
|
@@ -7,6 +7,7 @@ import { classifyRuntimeHealth, projectRuntimeMailbox, projectRuntimeObservation
|
|
|
7
7
|
import { latestRunDurableProgressAt } from "../scheduler/roleRunStall.js";
|
|
8
8
|
import { resolveRuntimeHealth } from "../config/yuiConfig.js";
|
|
9
9
|
import { builtinDriverIdForAdapter } from "../runtime/builtinAgentDrivers.js";
|
|
10
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
10
11
|
export function inspectTaskRoleRuntimeStatuses(taskId, roles, store, panes, now = new Date()) {
|
|
11
12
|
const taskOpenInputRequestCount = store.listInputRequests(taskId)
|
|
12
13
|
.filter((request) => request.status === "open").length;
|
|
@@ -140,7 +141,7 @@ function inspectTaskRoleRuntimeStatus(taskId, role, store, pane, openInputReques
|
|
|
140
141
|
// Issue 09: the last Run outcome is a separate axis from the Session
|
|
141
142
|
// lifecycle. A Session that stops after its Run yielded must not retroactively
|
|
142
143
|
// turn that Run into a failure; the status display keeps both visible.
|
|
143
|
-
const lastRun = store.listAgentRuns(taskId)
|
|
144
|
+
const lastRun = operationalTaskRecords(store.listAgentRuns(taskId), store.listEvents(taskId), "agent-run")
|
|
144
145
|
.filter((candidate) => candidate.roleName === role.name)
|
|
145
146
|
.sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt))[0]
|
|
146
147
|
?? null;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
1
2
|
import { TASK_COMPLETION_PUBLISHED_TREE_AUTHORIZED_EVENT } from "../task/publicationReference.js";
|
|
2
3
|
import { RUN_BOOTSTRAP_MAX_DELTAS } from "./runContextContract.js";
|
|
3
4
|
import { contextContentDigest, contextSnapshotRef, createContextSnapshot, validateContextSnapshot } from "./contextSnapshot.js";
|
|
@@ -213,9 +214,12 @@ function collectAuthorizedContext(store, run) {
|
|
|
213
214
|
if (view === "worker") {
|
|
214
215
|
for (const dependencyId of item.dependsOn) {
|
|
215
216
|
const dependency = store.getWorkItem(task.id, dependencyId);
|
|
216
|
-
if (dependency === null
|
|
217
|
+
if (dependency === null
|
|
218
|
+
|| (dependency.status !== "completed" && dependency.status !== "retired")) {
|
|
217
219
|
throw new Error(`Run WorkItem dependency is not accepted: ${dependencyId}.`);
|
|
218
220
|
}
|
|
221
|
+
if (dependency.status === "retired")
|
|
222
|
+
continue;
|
|
219
223
|
result.push(materialize("L3", "accepted-work-item", dependency.id, dependency));
|
|
220
224
|
}
|
|
221
225
|
}
|
|
@@ -242,7 +246,8 @@ function collectAuthorizedContext(store, run) {
|
|
|
242
246
|
}
|
|
243
247
|
}
|
|
244
248
|
if (view === "leader") {
|
|
245
|
-
|
|
249
|
+
const events = store.listEvents(task.id);
|
|
250
|
+
for (const item of store.listWorkItems(task.id).filter(({ status }) => status !== "retired")) {
|
|
246
251
|
result.push(materialize("L3", "work-item", item.id, item));
|
|
247
252
|
}
|
|
248
253
|
for (const decision of store.listDecisions(task.id)) {
|
|
@@ -257,14 +262,13 @@ function collectAuthorizedContext(store, run) {
|
|
|
257
262
|
for (const finding of store.listReviewFindings(task.id)) {
|
|
258
263
|
result.push(materialize("L3", "review-finding", finding.id, finding));
|
|
259
264
|
}
|
|
260
|
-
for (const agentRun of store.listAgentRuns(task.id).slice(-24)) {
|
|
265
|
+
for (const agentRun of operationalTaskRecords(store.listAgentRuns(task.id), events, "agent-run").slice(-24)) {
|
|
261
266
|
result.push(materialize("L4", "agent-run", agentRun.id, agentRun));
|
|
262
267
|
}
|
|
263
|
-
for (const message of store.listMessages(task.id).slice(-16)) {
|
|
268
|
+
for (const message of operationalTaskRecords(store.listMessages(task.id), events, "message").slice(-16)) {
|
|
264
269
|
result.push(materialize("L4", "task-message", message.id, message));
|
|
265
270
|
}
|
|
266
271
|
const publishedTreeAuthorizations = [];
|
|
267
|
-
const events = store.listEvents(task.id);
|
|
268
272
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
269
273
|
const event = events[index];
|
|
270
274
|
if (event.type === "task.completed" || event.type === "task.reopened")
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { chmodSync, readFileSync } from "node:fs";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
4
|
-
import {
|
|
2
|
+
import { chmodSync, existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { exactControlPlaneDigest, serializeExactDescriptor } from "../runtime/exactControlPlane.js";
|
|
5
5
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
6
6
|
import { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
|
|
7
7
|
export { SESSION_BOOTSTRAP_MANIFEST_SCHEMA_VERSION, SESSION_CONTEXT_PROTOCOL, sessionManifestCompatibilityDigest } from "./sessionProtocolIdentity.js";
|
|
8
|
+
const ORDINARY_SESSION_CLI = [
|
|
9
|
+
"#!/bin/sh",
|
|
10
|
+
"exec yui \"$@\"",
|
|
11
|
+
""
|
|
12
|
+
].join("\n");
|
|
8
13
|
/** Read back one immutable Session Manifest and verify its content digest. */
|
|
9
14
|
export function readSessionBootstrapManifest(path) {
|
|
10
15
|
const source = resolve(path);
|
|
@@ -79,11 +84,10 @@ export function materializeSessionBootstrap(input) {
|
|
|
79
84
|
const controlDigest = exactControlPlaneDigest(input.controlPlane);
|
|
80
85
|
const descriptorPath = resolve(join(home, "runtime", "control-plane", `${controlDigest}.json`));
|
|
81
86
|
writeImmutableText(descriptorPath, `${serializeExactDescriptor(input.controlPlane)}\n`);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
].join("\n");
|
|
87
|
+
// Session identity is carried by the immutable Manifest and durable Role/
|
|
88
|
+
// Run fences. Resolve the ordinary CLI on every invocation so package or
|
|
89
|
+
// release upgrades do not invalidate a still-current native Session.
|
|
90
|
+
const sessionCliContent = ORDINARY_SESSION_CLI;
|
|
87
91
|
const sessionCliDigest = digest(sessionCliContent);
|
|
88
92
|
const sessionCliPath = resolve(join(home, "runtime", "session-cli", `yui-${sessionCliDigest}.sh`));
|
|
89
93
|
writeImmutableText(sessionCliPath, sessionCliContent);
|
|
@@ -122,11 +126,11 @@ export function materializeSessionBootstrap(input) {
|
|
|
122
126
|
roleProfileRef: { digest: profileDigest, path: roleProfilePath },
|
|
123
127
|
contextProtocol: input.owner.scope === "global"
|
|
124
128
|
? {
|
|
125
|
-
loadCommand:
|
|
129
|
+
loadCommand: "yui session context \"$YUI_ROLE\" --json"
|
|
126
130
|
}
|
|
127
131
|
: {
|
|
128
|
-
loadCommand:
|
|
129
|
-
expandCommand:
|
|
132
|
+
loadCommand: "yui task run context \"$YUI_TASK_ID/<run-id>\" --json",
|
|
133
|
+
expandCommand: "yui task run context expand \"$YUI_TASK_ID/<run-id>\" <ref-id> --store <store> --mode full --json"
|
|
130
134
|
}
|
|
131
135
|
};
|
|
132
136
|
const manifest = Object.freeze({ ...body, digest: digest(body) });
|
|
@@ -140,6 +144,68 @@ export function materializeSessionBootstrap(input) {
|
|
|
140
144
|
descriptorPath
|
|
141
145
|
});
|
|
142
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Converts wrappers produced before the protocol-compatible Session CLI to an
|
|
149
|
+
* ordinary `yui` invocation. Only a valid Session Manifest may nominate a
|
|
150
|
+
* wrapper, and only the exact legacy two-line wrapper shape is changed. The
|
|
151
|
+
* Manifest and its frozen descriptor stay immutable and continue to
|
|
152
|
+
* authenticate the Session; repeated refreshes are no-ops.
|
|
153
|
+
*/
|
|
154
|
+
export function refreshManagedSessionCliWrappers(homeInput) {
|
|
155
|
+
const home = resolve(homeInput);
|
|
156
|
+
const manifestDirectory = resolve(join(home, "runtime", "session-manifests"));
|
|
157
|
+
const sessionCliDirectory = resolve(join(home, "runtime", "session-cli"));
|
|
158
|
+
if (!existsSync(manifestDirectory)) {
|
|
159
|
+
return Object.freeze({ refreshed: 0, current: 0, skipped: 0 });
|
|
160
|
+
}
|
|
161
|
+
const wrapperPaths = new Set();
|
|
162
|
+
let skipped = 0;
|
|
163
|
+
for (const name of readdirSync(manifestDirectory).filter((entry) => entry.endsWith(".json"))) {
|
|
164
|
+
const manifestPath = resolve(join(manifestDirectory, name));
|
|
165
|
+
try {
|
|
166
|
+
const manifest = readSessionBootstrapManifest(manifestPath);
|
|
167
|
+
if (manifestPath !== resolve(join(manifestDirectory, `${manifest.digest}.json`))) {
|
|
168
|
+
skipped += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const wrapperPath = resolve(manifest.controlPlane.sessionCliPath);
|
|
172
|
+
if (dirname(wrapperPath) !== sessionCliDirectory) {
|
|
173
|
+
skipped += 1;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
wrapperPaths.add(wrapperPath);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Historical or incomplete manifests are audit material. They must not
|
|
180
|
+
// block current Sessions or an otherwise compatible package update.
|
|
181
|
+
skipped += 1;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let refreshed = 0;
|
|
185
|
+
let current = 0;
|
|
186
|
+
for (const wrapperPath of wrapperPaths) {
|
|
187
|
+
if (!existsSync(wrapperPath)) {
|
|
188
|
+
skipped += 1;
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const content = readFileSync(wrapperPath, "utf8");
|
|
192
|
+
if (content === ORDINARY_SESSION_CLI) {
|
|
193
|
+
current += 1;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (!isLegacyExactSessionCli(content)) {
|
|
197
|
+
skipped += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
writeTextFileAtomically(wrapperPath, ORDINARY_SESSION_CLI);
|
|
201
|
+
chmodSync(wrapperPath, 0o700);
|
|
202
|
+
refreshed += 1;
|
|
203
|
+
}
|
|
204
|
+
return Object.freeze({ refreshed, current, skipped });
|
|
205
|
+
}
|
|
206
|
+
function isLegacyExactSessionCli(content) {
|
|
207
|
+
return /^#!\/bin\/sh\nexec [^\n]+ '--yui-control' '[a-f0-9]{64}' "\$@"\n$/u.test(content);
|
|
208
|
+
}
|
|
143
209
|
function writeImmutableText(path, content) {
|
|
144
210
|
writeTextFileAtomically(path, content);
|
|
145
211
|
chmodSync(path, 0o600);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderWakeReason } from "../scheduler/wakeReason.js";
|
|
2
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
2
3
|
/**
|
|
3
4
|
* Issue 04 (context token budget) — long-term design:
|
|
4
5
|
*
|
|
@@ -25,12 +26,13 @@ export function buildTaskWakeEnvelope(reader, request) {
|
|
|
25
26
|
throw new Error(`Wake envelope ${request.wakeId} must carry at least one reason.`);
|
|
26
27
|
}
|
|
27
28
|
const fromTime = Date.parse(request.fromCursor);
|
|
29
|
+
const events = reader.listEvents(request.taskId);
|
|
28
30
|
const counts = {
|
|
29
|
-
events:
|
|
31
|
+
events: events
|
|
30
32
|
.filter((record) => Date.parse(record.createdAt) > fromTime).length,
|
|
31
|
-
messages: reader.listMessages(request.taskId)
|
|
33
|
+
messages: operationalTaskRecords(reader.listMessages(request.taskId), events, "message")
|
|
32
34
|
.filter((record) => Date.parse(record.createdAt) > fromTime).length,
|
|
33
|
-
runs: reader.listAgentRuns(request.taskId)
|
|
35
|
+
runs: operationalTaskRecords(reader.listAgentRuns(request.taskId), events, "agent-run")
|
|
34
36
|
.filter((record) => Date.parse(record.createdAt) > fromTime).length
|
|
35
37
|
};
|
|
36
38
|
const lines = [
|
|
@@ -7,7 +7,7 @@ import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
|
|
|
7
7
|
import { hasRuntimeLifecycleWork } from "../runtime/lifecycleReservation.js";
|
|
8
8
|
import { assertControllerStatusIdentity } from "../runtime/exactControlPlane.js";
|
|
9
9
|
import { EPHEMERAL_DOMAIN_ENVIRONMENT_NAMES } from "./domainIdentity.js";
|
|
10
|
-
import {
|
|
10
|
+
import { yuiVersionIdentity } from "../version.js";
|
|
11
11
|
import { SessionOwnerReconciliation } from "./sessionOwnerReconciliation.js";
|
|
12
12
|
import { WorkspaceCleanupBlockedError } from "../repository/taskWorkspacePreparer.js";
|
|
13
13
|
import { CONTROLLER_SHUTDOWN_TIMEOUT_MS, LIFECYCLE_REQUEST_TIMEOUT_MS } from "../runtime/runtimeDeadlines.js";
|
|
@@ -137,12 +137,8 @@ function assertCompatibleControllerStatus(status, expectedVersion) {
|
|
|
137
137
|
+ "Run `yui controller restart` before writing new task records.");
|
|
138
138
|
}
|
|
139
139
|
const actualVersion = statusRecord.version;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
? typeof actualVersion === "string" && actualVersion !== expected
|
|
143
|
-
: actualVersion !== expected;
|
|
144
|
-
if (versionMismatch) {
|
|
145
|
-
throw new Error(`Controller version is incompatible (expected ${expected}, found ${typeof actualVersion === "string" ? actualVersion : "unknown"}). `
|
|
140
|
+
if (expectedVersion !== undefined && actualVersion !== expectedVersion) {
|
|
141
|
+
throw new Error(`Controller version is incompatible (expected ${expectedVersion}, found ${typeof actualVersion === "string" ? actualVersion : "unknown"}). `
|
|
146
142
|
+ "Run `yui controller restart` before writing new task records.");
|
|
147
143
|
}
|
|
148
144
|
// Ordinary callers must authenticate the complete control-plane identity.
|
|
@@ -151,7 +147,11 @@ function assertCompatibleControllerStatus(status, expectedVersion) {
|
|
|
151
147
|
// path authenticates its executable, argv, and version immediately after
|
|
152
148
|
// readiness in ensureFileTaskControllerIdentity.
|
|
153
149
|
if (expectedVersion === undefined) {
|
|
154
|
-
|
|
150
|
+
const identity = yuiVersionIdentity();
|
|
151
|
+
assertControllerStatusIdentity(status, {
|
|
152
|
+
...identity,
|
|
153
|
+
version: typeof actualVersion === "string" ? actualVersion : identity.version
|
|
154
|
+
});
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
function spawnDetachedFileTaskController(home, environment) {
|
|
@@ -6,6 +6,7 @@ import { decideProviderRecovery } from "../runtime/providerRecoveryDecision.js";
|
|
|
6
6
|
import { boundProviderRetryBeforeFirstProgress, projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
|
|
7
7
|
import { hasRecentTurnId } from "../executor/turnCompletion.js";
|
|
8
8
|
import { createTaskEvent } from "../event/taskEvent.js";
|
|
9
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
9
10
|
import { buildTaskWakeEnvelope } from "../context/wakeNotification.js";
|
|
10
11
|
import { createTaskWake, fallbackWakeCursor, latestTaskWake } from "../scheduler/taskWake.js";
|
|
11
12
|
import { rolloverTaskRoleSessionForContextBudget } from "../lifecycle/contextBudgetRollover.js";
|
|
@@ -596,7 +597,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
596
597
|
const latest = latestTaskWake(reader.listTaskWakes(taskId));
|
|
597
598
|
const fromCursor = latest?.toCursor ?? fallbackWakeCursor({
|
|
598
599
|
taskCreatedAt: task.createdAt,
|
|
599
|
-
leaderRunCreatedAt: reader.listAgentRuns(taskId)
|
|
600
|
+
leaderRunCreatedAt: operationalTaskRecords(reader.listAgentRuns(taskId), reader.listEvents(taskId), "agent-run")
|
|
600
601
|
.filter((run) => run.roleName === "leader")
|
|
601
602
|
.at(-1)?.createdAt
|
|
602
603
|
});
|
|
@@ -3706,7 +3707,7 @@ function bindTaskRoleRunInFlight(store, role, run, now) {
|
|
|
3706
3707
|
agentId,
|
|
3707
3708
|
runId: run.id,
|
|
3708
3709
|
receiptId: agentRunDeliveryReceiptId(run)
|
|
3709
|
-
}, now);
|
|
3710
|
+
}, now, run.mode);
|
|
3710
3711
|
store.saveRoleSessionSet(updated);
|
|
3711
3712
|
}
|
|
3712
3713
|
function markTaskRoleRunPushedInFlight(store, role, run, now) {
|
|
@@ -292,7 +292,7 @@ export function roleAgentSessionResumeMode(set, agentId, desired, workspace) {
|
|
|
292
292
|
throw new Error(`Role Agent session is incompatible with the next effective launch: ${agentId}. `
|
|
293
293
|
+ "Stop the existing native process before starting a fresh Session.");
|
|
294
294
|
}
|
|
295
|
-
export function bindTaskRoleRun(set, fence, preparedAt) {
|
|
295
|
+
export function bindTaskRoleRun(set, fence, preparedAt, mode) {
|
|
296
296
|
validateRoleSessionSet(set);
|
|
297
297
|
assertTaskRoleSessionSet(set);
|
|
298
298
|
const normalized = normalizeTaskRoleRunFence(fence);
|
|
@@ -305,9 +305,9 @@ export function bindTaskRoleRun(set, fence, preparedAt) {
|
|
|
305
305
|
throw new Error("Task Role session set already has an in-flight Run.");
|
|
306
306
|
}
|
|
307
307
|
const timestamp = requireDate(preparedAt, "Task Role Run preparedAt");
|
|
308
|
-
const providerBinding = set.providerBinding
|
|
309
|
-
?
|
|
310
|
-
:
|
|
308
|
+
const providerBinding = mode === "resume" && set.providerBinding !== null
|
|
309
|
+
? rebindProviderRuntimeRun(set.providerBinding, normalized.runId)
|
|
310
|
+
: null;
|
|
311
311
|
const updated = {
|
|
312
312
|
...set,
|
|
313
313
|
inFlight: { ...normalized, preparedAt: timestamp },
|