@zq-silk/yui 2.6.0 → 2.7.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 +23 -0
- package/dist/cli/commandCatalog.js +28 -2
- package/dist/commands/globalRoleCommands.js +22 -2
- package/dist/commands/inboxCommands.js +352 -0
- package/dist/commands/roleSkillValidation.js +2 -0
- package/dist/commands/taskCommands.js +32 -4
- package/dist/context/currentSkills.js +48 -0
- package/dist/context/roleSessionContext.js +13 -16
- package/dist/context/runContextPack.js +2 -0
- package/dist/context/runInputContract.js +1 -1
- package/dist/context/sessionBootstrapManifest.js +27 -4
- package/dist/context/skillCatalog.js +2 -3
- package/dist/context/taskContext.js +2 -0
- package/dist/controlPlaneCli.js +18 -1
- package/dist/controller/controller.js +3 -2
- package/dist/controller/fileSchedulerStoreAdapter.js +19 -4
- package/dist/controller/globalInputDelivery.js +73 -15
- package/dist/controller/globalRuntimeAttention.js +1 -1
- package/dist/controller/operatorNotification.js +6 -1
- package/dist/controller/providerRetryDelivery.js +3 -5
- package/dist/controller/runtime.js +2 -2
- package/dist/doctor/doctor.js +25 -2
- package/dist/executor/effectiveLaunch.js +2 -2
- package/dist/executor/fileRoleLaunchPlanner.js +4 -6
- package/dist/interaction/notificationPresentation.js +2 -2
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/message/globalInputBacklog.js +69 -0
- package/dist/message/globalMessageQuery.js +1 -0
- package/dist/message/inboxReceipt.js +32 -0
- package/dist/message/message.js +6 -1
- package/dist/message/messageContinuation.js +34 -13
- package/dist/message/taskInboxOwnership.js +21 -0
- package/dist/nativeAgent/session/backends.js +1 -1
- package/dist/observability/runtimeIdentity.js +1 -1
- package/dist/resources/sqliteResourceRegistry.js +1 -1
- package/dist/runtime/agentHost.js +105 -16
- package/dist/scheduler/operatorEvent.js +49 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +5 -4
- package/dist/scheduler/taskWake.js +5 -0
- package/dist/sqlite/driver.js +108 -0
- package/dist/storage/sqliteSchema.js +11 -1
- package/dist/storage/sqliteStore.js +60 -1
- package/dist/storage/storageSchema.js +1 -1
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +1 -0
- package/dist/storage/upgrade/upgradeOrchestrator.js +1 -1
- package/dist/task/completionReadiness.js +11 -4
- package/dist/telemetry/sqliteTelemetryStore.js +1 -1
- package/docs/managed-turn-and-session-runtime.md +81 -5
- package/docs/managed-turn-and-session-runtime.zh-CN.md +51 -3
- package/docs/release-workflow.md +47 -2
- package/docs/release-workflow.zh-CN.md +34 -1
- package/docs/storage-baseline.md +17 -7
- package/docs/storage-baseline.zh-CN.md +11 -5
- package/docs/task-delivery.md +17 -8
- package/docs/task-delivery.zh-CN.md +11 -5
- package/docs/testing/verification-levels.md +22 -4
- package/docs/testing/verification-levels.zh-CN.md +8 -1
- package/i18n/README.zh-CN.md +19 -0
- package/package.json +5 -2
- package/skills/yui-leader/SKILL.md +28 -11
- package/skills/yui-leader/VERSION +1 -0
- package/skills/yui-operator/SKILL.md +10 -2
- package/skills/yui-operator/VERSION +1 -0
- package/skills/yui-operator/references/task-delivery.md +2 -0
- package/skills/yui-reviewer/SKILL.md +8 -1
- package/skills/yui-reviewer/VERSION +1 -0
- package/skills/yui-runtime/SKILL.md +94 -11
- package/skills/yui-runtime/VERSION +1 -0
- package/skills/yui-runtime/references/publication.md +16 -5
- package/skills/yui-worker/SKILL.md +8 -1
- package/skills/yui-worker/VERSION +1 -0
package/dist/doctor/doctor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync, lstatSync, realpathSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
|
-
import Database from "
|
|
3
|
+
import Database, { selectSqliteDriver } from "../sqlite/driver.js";
|
|
4
4
|
import { inspectPty } from "./ptyProbe.js";
|
|
5
5
|
import { gitVersionSupport } from "../repository/gitCompatibility.js";
|
|
6
6
|
import { resolveExecutable } from "../external/pinnedCommandRunner.js";
|
|
@@ -31,6 +31,7 @@ import { CommandExecutionError } from "../tmux/commandExecutor.js";
|
|
|
31
31
|
* and never block an update.
|
|
32
32
|
*/
|
|
33
33
|
export const STORAGE_DOCTOR_CHECK_NAMES = Object.freeze([
|
|
34
|
+
"sqlite driver",
|
|
34
35
|
"storage schema",
|
|
35
36
|
"storage compatibility",
|
|
36
37
|
"storage state"
|
|
@@ -38,7 +39,7 @@ export const STORAGE_DOCTOR_CHECK_NAMES = Object.freeze([
|
|
|
38
39
|
/**
|
|
39
40
|
* Classify the storage checks into a machine-readable health verdict. This is the
|
|
40
41
|
* canonical, parseable signal the update post-verify consumes (P1-3): storage is
|
|
41
|
-
* healthy only when schema, compatibility, and state are all `ok`. Any
|
|
42
|
+
* healthy only when driver, schema, compatibility, and state are all `ok`. Any
|
|
42
43
|
* `unsupported` (version mismatch / needs-new-version), `invalid` (corrupted /
|
|
43
44
|
* unreadable), or `missing` (uninitialized) storage check is blocking — even
|
|
44
45
|
* and the machine-readable command exits non-zero.
|
|
@@ -119,6 +120,7 @@ export async function getDoctorChecks(env, executor) {
|
|
|
119
120
|
async function inspectDoctor(env, executor) {
|
|
120
121
|
const home = resolveYuiHome(env);
|
|
121
122
|
const homeCheck = checkHome(home);
|
|
123
|
+
const driverCheck = checkSqliteDriver();
|
|
122
124
|
const schema = readSchema(home);
|
|
123
125
|
const compatibility = inspectCompatibility(home, homeCheck, schema);
|
|
124
126
|
const schemaCheck = checkSchema(schema);
|
|
@@ -135,6 +137,7 @@ async function inspectDoctor(env, executor) {
|
|
|
135
137
|
return {
|
|
136
138
|
checks: [
|
|
137
139
|
homeCheck,
|
|
140
|
+
driverCheck,
|
|
138
141
|
schemaCheck,
|
|
139
142
|
compatibility.check,
|
|
140
143
|
storage.check,
|
|
@@ -148,6 +151,26 @@ async function inspectDoctor(env, executor) {
|
|
|
148
151
|
review: review.report
|
|
149
152
|
};
|
|
150
153
|
}
|
|
154
|
+
export function checkSqliteDriver() {
|
|
155
|
+
try {
|
|
156
|
+
const selected = selectSqliteDriver();
|
|
157
|
+
return {
|
|
158
|
+
name: "sqlite driver",
|
|
159
|
+
status: "ok",
|
|
160
|
+
detail: `${selected.name}@${selected.version}; SQLite ${selected.sqliteVersion}; `
|
|
161
|
+
+ (selected.fallbackReason === undefined
|
|
162
|
+
? "no fallback"
|
|
163
|
+
: `fallback: ${selected.fallbackReason}`)
|
|
164
|
+
+ ". Isolated native connection probe; does not validate Home data."
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
return {
|
|
169
|
+
name: "sqlite driver", status: "invalid",
|
|
170
|
+
detail: `Native driver probe failed (not a Home diagnosis): ${error instanceof Error ? error.message : String(error)}`
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
151
174
|
/** Query the authenticated Controller endpoint without starting it. */
|
|
152
175
|
export async function checkController(home, probe = callController) {
|
|
153
176
|
try {
|
|
@@ -2,7 +2,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
2
2
|
import { validateExecutionEnvironmentSnapshot } from "../resources/projectResource.js";
|
|
3
3
|
import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
|
|
4
4
|
import { resolveAgentAdapter } from "./agentAdapter.js";
|
|
5
|
-
import {
|
|
5
|
+
import { loadYuiSkillContexts, roleSessionKind, roleSkillIds } from "../context/roleSessionContext.js";
|
|
6
6
|
import { validateSkillPackageRef } from "../context/skillPackage.js";
|
|
7
7
|
import { isAgentAdapterId } from "../agent/adapterCatalog.js";
|
|
8
8
|
import { adapterIdForExecutionComponent, isAgentExecutionComponentId, resolveAgentExecutionComponent } from "../agent/executionComponents.js";
|
|
@@ -24,7 +24,7 @@ export function resolveEffectiveLaunch(input) {
|
|
|
24
24
|
const continuing = existing?.status === "active";
|
|
25
25
|
const skillPackages = continuing ? existing.effective.skillPackages
|
|
26
26
|
: home === undefined ? undefined
|
|
27
|
-
:
|
|
27
|
+
: loadYuiSkillContexts(home, roleSkillIds(input.role, roleSessionKind(input.role, "taskId" in input.role ? { scope: "task", taskId: input.role.taskId } : { scope: "global" }, input.purpose))).map(skill => skill.package);
|
|
28
28
|
return snapshotFromConfig({
|
|
29
29
|
executionAuthority: input.executionAuthority ?? (input.purpose === "planning" ? "planning" : "delivery"),
|
|
30
30
|
sourceDesiredRevision: input.role.launchRevision,
|
|
@@ -337,12 +337,10 @@ export class FileRoleLaunchPlanner {
|
|
|
337
337
|
Object.assign(launchEnvironment, runtimeIsolation === undefined
|
|
338
338
|
? {}
|
|
339
339
|
: taskRuntimeIsolationEnvironment(runtimeIsolation));
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
const baseSessionContext = compileRoleSessionContext(this.home, launchRole, owner, { ...sessionPolicy, skillPackages });
|
|
340
|
+
// Frozen packages describe historical materialization, not Session identity
|
|
341
|
+
// or current instruction availability. Never require or reconstruct them
|
|
342
|
+
// when reconnecting a valid old Session.
|
|
343
|
+
const baseSessionContext = compileRoleSessionContext(this.home, launchRole, owner, sessionPolicy);
|
|
346
344
|
const bootstrap = materializeSessionBootstrap({
|
|
347
345
|
yuiHome: this.home,
|
|
348
346
|
role: launchRole,
|
|
@@ -69,7 +69,7 @@ export function taskNotificationPresentation(wake, store) {
|
|
|
69
69
|
...(messages.length > 4 ? [`${messages.length - 4} more messages in the fixed wake.`] : []),
|
|
70
70
|
`Refresh: yui task context ${wake.taskId} --json`,
|
|
71
71
|
`Read inputs/results: yui task wake show ${wake.taskId} ${wake.id} --json`,
|
|
72
|
-
"
|
|
72
|
+
"Check current Skills each turn: yui session skills --json. Apply current yui-leader; read relevant originals in full and advance authorized work."
|
|
73
73
|
].join("\n");
|
|
74
74
|
}
|
|
75
75
|
export function globalMessagePresentation(roleName, messageId, message) {
|
|
@@ -80,6 +80,6 @@ export function globalMessagePresentation(roleName, messageId, message) {
|
|
|
80
80
|
...(summary === undefined ? [] : [`Author summary (message-body): ${JSON.stringify(summary)}`]),
|
|
81
81
|
"Refresh Session Context using the current Manifest's loadCommand.",
|
|
82
82
|
`Read original: yui role message show ${roleName} ${messageId} --json`,
|
|
83
|
-
`Apply ${roleName === "operator" ? "yui-operator" : "yui-runtime"}
|
|
83
|
+
`The Global loadCommand checks current Skill versions each turn. Apply current ${roleName === "operator" ? "yui-operator" : "yui-runtime"} within existing authority.`
|
|
84
84
|
].join("\n");
|
|
85
85
|
}
|
|
@@ -10,7 +10,7 @@ export function createOperatorBatchPresentation(batchId, items) {
|
|
|
10
10
|
receiptId: `operator-batch:${batchId}`,
|
|
11
11
|
text: [
|
|
12
12
|
"[Yui updates]",
|
|
13
|
-
"
|
|
13
|
+
"Check current Skills each turn via the Global Manifest's loadCommand. Apply current yui-operator; inspect these originals and present material changes or decisions.",
|
|
14
14
|
...items.flatMap(renderItem)
|
|
15
15
|
].join("\n")
|
|
16
16
|
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { providerRetryPending } from "../runtime/providerRetry.js";
|
|
2
|
+
import { hasRuntimeCleanupObligation } from "../runtime/lifecycleReservation.js";
|
|
3
|
+
/** Bounded, derived diagnostics only; never an acknowledgement or recovery authority. */
|
|
4
|
+
export function globalInputBacklog(pending, sessions, now = new Date(), mailboxes = {}) {
|
|
5
|
+
const first = pending[0];
|
|
6
|
+
const run = sessions?.providerBinding?.run;
|
|
7
|
+
const session = sessions?.sessions[sessions.activeAgentId];
|
|
8
|
+
const authority = sessions?.providerBinding?.authority;
|
|
9
|
+
const batches = [mailboxes.attention?.pending, mailboxes.attention?.processing?.batch]
|
|
10
|
+
.filter(batch => batch != null);
|
|
11
|
+
const coalescedFactCount = batches.reduce((count, batch) => count + batch.requestCount, 0);
|
|
12
|
+
let reason = "ready";
|
|
13
|
+
let recovery = "The Controller delivers at the next legal opportunity; do not duplicate the input.";
|
|
14
|
+
if (first === undefined && coalescedFactCount === 0)
|
|
15
|
+
reason = "empty";
|
|
16
|
+
else if (hasRuntimeCleanupObligation(mailboxes.lifecycle ?? null)) {
|
|
17
|
+
reason = "lifecycle-cleanup";
|
|
18
|
+
recovery = "Complete the existing exact-owner stop/replacement; queued input cannot revive the Session.";
|
|
19
|
+
}
|
|
20
|
+
else if (session?.status !== "active") {
|
|
21
|
+
reason = session === undefined ? "session-unavailable" : "session-stopped";
|
|
22
|
+
recovery = "Inspect Session lifecycle. Explicit stop/replacement is not undone by background delivery.";
|
|
23
|
+
}
|
|
24
|
+
else if (authority?.owner !== "controller") {
|
|
25
|
+
reason = "external-or-unknown-writer";
|
|
26
|
+
recovery = "Resolve the current writer/authority explicitly; no background takeover is allowed.";
|
|
27
|
+
}
|
|
28
|
+
else if (providerRetryPending(sessions?.providerBinding)) {
|
|
29
|
+
reason = "provider-retry";
|
|
30
|
+
recovery = "The existing bounded Provider retry owns this input; inspect session retry before intervening.";
|
|
31
|
+
}
|
|
32
|
+
else if (run && ["submitting", "accepted", "delivery-unknown"].includes(run.status)) {
|
|
33
|
+
reason = run.status === "accepted" ? "in-flight-or-unobserved-terminal" : run.status;
|
|
34
|
+
recovery = "Age does not prove a stalled Turn. Periodic reconciliation asks only the original Host for exact retained terminal evidence. Inspect recent runtime-attention Messages if the Host is unobservable; never resend unknown input.";
|
|
35
|
+
}
|
|
36
|
+
else if (first?.inbox) {
|
|
37
|
+
reason = "agent-inbox-handling";
|
|
38
|
+
recovery = "Read the exact inbox original and prior handling claim; explicitly acknowledge after processing. No automatic replay or timeout release.";
|
|
39
|
+
}
|
|
40
|
+
else if (first?.control && first.control.outcome !== "rejected") {
|
|
41
|
+
reason = "message-acceptance-unresolved";
|
|
42
|
+
recovery = "Inspect this original Message's exact control receipt; do not replay or bypass uncertain input.";
|
|
43
|
+
}
|
|
44
|
+
else if (first?.interruptThen) {
|
|
45
|
+
reason = "reserved-interrupt-then";
|
|
46
|
+
recovery = "Only the original Session, authority and exact terminal release the reserved continuation.";
|
|
47
|
+
}
|
|
48
|
+
else if (first?.deliveryTarget && (first.deliveryTarget.agentId !== sessions?.activeAgentId
|
|
49
|
+
|| first.deliveryTarget.nativeSessionId !== session.nativeSessionId)) {
|
|
50
|
+
reason = "stale-target";
|
|
51
|
+
recovery = "Unsubmitted stale messages are retained with a nondelivery reason; no cross-Session input copy.";
|
|
52
|
+
}
|
|
53
|
+
const oldestCreatedAt = [...pending.map(message => message.createdAt), ...batches.map(batch => batch.firstQueuedAt)]
|
|
54
|
+
.reduce((oldest, at) => oldest === null || at < oldest ? at : oldest, null);
|
|
55
|
+
return {
|
|
56
|
+
pendingCount: pending.length, coalescedFactCount, oldestCreatedAt,
|
|
57
|
+
oldestAgeMs: oldestCreatedAt === null ? null : Math.max(0, now.getTime() - Date.parse(oldestCreatedAt)),
|
|
58
|
+
reason, recovery,
|
|
59
|
+
target: session === undefined ? null : {
|
|
60
|
+
agentId: session.agentId, nativeSessionId: session.nativeSessionId,
|
|
61
|
+
authority: authority ?? null, attemptId: run?.attemptId ?? null, nativeTurnId: run?.nativeTurnId ?? null
|
|
62
|
+
},
|
|
63
|
+
messages: pending.slice(0, 8).map(message => ({
|
|
64
|
+
id: message.id, createdAt: message.createdAt, deliveryTarget: message.deliveryTarget ?? null,
|
|
65
|
+
control: message.control ?? null, interruptThen: message.interruptThen ?? null
|
|
66
|
+
})),
|
|
67
|
+
omittedMessages: Math.max(0, pending.length - 8)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { requireKnownFields, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
|
+
import { isDeepStrictEqual } from "node:util";
|
|
3
|
+
export function validateInboxReceipt(receipt) {
|
|
4
|
+
if (receipt === undefined)
|
|
5
|
+
return;
|
|
6
|
+
requireKnownFields(receipt, ["claim", "ack"], "Inbox receipt");
|
|
7
|
+
for (const [kind, entry] of Object.entries(receipt)) {
|
|
8
|
+
requireKnownFields(entry, kind === "claim" ? ["requestId", "actor", "at"]
|
|
9
|
+
: ["requestId", "actor", "at", "reason"], "Inbox action");
|
|
10
|
+
requireText(entry.requestId, "Inbox request id");
|
|
11
|
+
requireTimestamp(entry.at, "Inbox timestamp");
|
|
12
|
+
const actor = entry.actor;
|
|
13
|
+
requireKnownFields(actor, ["scope", "taskId", "roleName", "agentId", "nativeSessionId", "runId"], "Inbox actor");
|
|
14
|
+
if (!["task", "global"].includes(actor.scope)
|
|
15
|
+
|| (actor.scope === "task") !== (actor.taskId !== undefined))
|
|
16
|
+
throw new Error("Inbox actor scope is invalid.");
|
|
17
|
+
for (const value of Object.values(actor))
|
|
18
|
+
requireText(value, "Inbox actor identity");
|
|
19
|
+
if (kind === "ack")
|
|
20
|
+
requireText(entry.reason, "Inbox disposition");
|
|
21
|
+
}
|
|
22
|
+
if (!receipt.claim)
|
|
23
|
+
throw new Error("Inbox receipt requires its original claim.");
|
|
24
|
+
}
|
|
25
|
+
/** A stale automatic writer cannot erase an active claim between its read and
|
|
26
|
+
* Host admission. The original claim and any ack are immutable audit facts. */
|
|
27
|
+
export function assertInboxReceiptPreserved(previous, next) {
|
|
28
|
+
if (previous !== undefined && (!isDeepStrictEqual(previous.claim, next?.claim)
|
|
29
|
+
|| previous.ack !== undefined && !isDeepStrictEqual(previous.ack, next?.ack))) {
|
|
30
|
+
throw new Error("Inbox handling ownership changed; reread the original instead of overwriting its receipt.");
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/message/message.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
2
2
|
import { requireKnownFields } from "../domain/validation.js";
|
|
3
|
+
import { validateInboxReceipt } from "./inboxReceipt.js";
|
|
3
4
|
export const TASK_MESSAGE_KINDS = ["user", "operator", "role-result", "system"];
|
|
4
5
|
/**
|
|
5
6
|
* How a user/operator submission asks its Task to react (task-32 Requirement A).
|
|
@@ -207,8 +208,9 @@ export function validateTaskMessage(message) {
|
|
|
207
208
|
throw new Error("Task Message must use schemaVersion 1.");
|
|
208
209
|
requireKnownFields(message, [
|
|
209
210
|
"schemaVersion", "id", "taskId", "kind", "author", "body", "intent", "submissionKey", "submissionReceipt",
|
|
210
|
-
"inputControl", "interruptThen", "control", "runId", "resultRef", "workItemId", "recipient", "continuation", "handovers", "createdAt"
|
|
211
|
+
"inputControl", "interruptThen", "control", "runId", "resultRef", "workItemId", "recipient", "continuation", "handovers", "createdAt", "inbox"
|
|
211
212
|
], "Task Message");
|
|
213
|
+
validateInboxReceipt(message.inbox);
|
|
212
214
|
validateTaskRecordReference({ taskId: message.taskId, localId: message.id }, "message");
|
|
213
215
|
requireText(message.body, "Message body");
|
|
214
216
|
validateKindAndAuthor(message.kind, message.author);
|
|
@@ -394,6 +396,7 @@ export function createGlobalRoleMessage(id, roleName, body, kind, author, now, c
|
|
|
394
396
|
return message;
|
|
395
397
|
}
|
|
396
398
|
export function validateGlobalRoleMessage(message) {
|
|
399
|
+
validateInboxReceipt(message.inbox);
|
|
397
400
|
if (message.schemaVersion !== 1)
|
|
398
401
|
throw new Error("Global Role Message must use schemaVersion 1.");
|
|
399
402
|
validateGlobalRoleMessageId(message.id);
|
|
@@ -517,6 +520,8 @@ export function markGlobalRoleMessageDelivered(message, now, via = "provider") {
|
|
|
517
520
|
* keeps resolving to this same Message and never creates a second input.
|
|
518
521
|
*/
|
|
519
522
|
export function claimGlobalRoleMessageInterruptThen(message, claim) {
|
|
523
|
+
if (message.inbox !== undefined)
|
|
524
|
+
throw new Error("Agent inbox handling already owns this Message; it cannot be replayed through interrupt-then.");
|
|
520
525
|
const { inputControl, notDelivered: _priorNondelivery, ...rest } = message;
|
|
521
526
|
const claimed = {
|
|
522
527
|
...rest,
|
|
@@ -10,6 +10,7 @@ import { validateExactRunReviewRound } from "../lifecycle/exactRunTerminalizatio
|
|
|
10
10
|
import { sourceRunContextValue } from "../context/sourceRunContext.js";
|
|
11
11
|
import { requireManagedTaskCaller } from "../runtime/managedCaller.js";
|
|
12
12
|
import { runtimeObservationFromTaskEvent } from "../runtime/runtimeObservation.js";
|
|
13
|
+
import { isTaskRecordRetired, operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
13
14
|
export function resolveMessageRecipient(store, taskId, roleName, scope) {
|
|
14
15
|
if (scope.reviewRoundId !== undefined) {
|
|
15
16
|
const round = store.getReviewRound(taskId, scope.reviewRoundId);
|
|
@@ -28,8 +29,11 @@ export function resolveMessageRecipient(store, taskId, roleName, scope) {
|
|
|
28
29
|
throw new Error("No existing Dispatch owns this Role and work scope.");
|
|
29
30
|
return { roleName, ownerRunId: previous.id, ...scope };
|
|
30
31
|
}
|
|
31
|
-
/**
|
|
32
|
-
|
|
32
|
+
/** Current Assignment authority, independent of eligibility to start a new Run.
|
|
33
|
+
* A Reviewer still owns its frozen clarification when Task main advances. */
|
|
34
|
+
export function messageAssignmentBlocker(store, message) {
|
|
35
|
+
if (isTaskRecordRetired(store.listEvents(message.taskId), "message", message.id))
|
|
36
|
+
return "message-retired";
|
|
33
37
|
const target = message.recipient;
|
|
34
38
|
if (target?.ownerRunId === undefined)
|
|
35
39
|
return "recipient-assignment-missing";
|
|
@@ -78,22 +82,29 @@ export function messageContinuationBlocker(store, message) {
|
|
|
78
82
|
&& latest.inputs[0]?.input.source.channel !== "message-continuation")
|
|
79
83
|
return "assignment-changed";
|
|
80
84
|
if (target.reviewRoundId !== undefined) {
|
|
81
|
-
const
|
|
82
|
-
if (
|
|
83
|
-
return "review-
|
|
85
|
+
const validation = validateExactRunReviewRound(store, latest ?? owner, { allowTerminal: true });
|
|
86
|
+
if (validation.disposition !== "applied")
|
|
87
|
+
return validation.reason ?? "review-candidate-stale";
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
/** Starting another Run additionally requires the reviewed candidate to remain current. */
|
|
92
|
+
export function messageContinuationBlocker(store, message) {
|
|
93
|
+
const invalid = messageAssignmentBlocker(store, message);
|
|
94
|
+
if (invalid !== undefined)
|
|
95
|
+
return invalid;
|
|
96
|
+
const reviewRoundId = message.recipient?.reviewRoundId;
|
|
97
|
+
if (reviewRoundId !== undefined) {
|
|
98
|
+
const round = store.getReviewRound(message.taskId, reviewRoundId);
|
|
84
99
|
if (round.scope === "task") {
|
|
100
|
+
const task = store.getTask(message.taskId);
|
|
85
101
|
if (round.taskCandidate?.projects.some((project) => task.projectBindings.find((binding) => binding.projectId === project.projectId)?.currentCommit !== project.commit)) {
|
|
86
102
|
return "review-candidate-stale";
|
|
87
103
|
}
|
|
88
104
|
}
|
|
89
|
-
else {
|
|
90
|
-
|
|
91
|
-
if (item?.candidates.at(-1)?.id !== round.candidateId)
|
|
92
|
-
return "review-candidate-stale";
|
|
105
|
+
else if (store.getWorkItem(message.taskId, round.workItemId)?.candidates.at(-1)?.id !== round.candidateId) {
|
|
106
|
+
return "review-candidate-stale";
|
|
93
107
|
}
|
|
94
|
-
const validation = validateExactRunReviewRound(store, latest ?? owner, { allowTerminal: true });
|
|
95
|
-
if (validation.disposition !== "applied")
|
|
96
|
-
return validation.reason ?? "review-candidate-stale";
|
|
97
108
|
}
|
|
98
109
|
return undefined;
|
|
99
110
|
}
|
|
@@ -189,7 +200,11 @@ export function interruptThenTerminalState(store, message) {
|
|
|
189
200
|
* remain the pending authority; Mailbox is only the existing scheduling hint.
|
|
190
201
|
* Reserving inputs and creating the next Run is one atomic effect. */
|
|
191
202
|
export function prepareMessageContinuations(store, taskId, now, roleName) {
|
|
192
|
-
const
|
|
203
|
+
const messages = operationalTaskRecords(store.listMessages(taskId), store.listEvents(taskId), "message");
|
|
204
|
+
const claims = messages.filter(message => message.recipient?.roleName === roleName
|
|
205
|
+
&& message.inbox !== undefined && message.inbox.ack === undefined);
|
|
206
|
+
const pending = messages.filter((message) => message.recipient?.roleName === roleName && message.recipient.ownerRunId !== undefined
|
|
207
|
+
&& message.inbox === undefined
|
|
193
208
|
&& message.continuation?.runId === undefined
|
|
194
209
|
// A steer targets the exact current native turn, never a queued next Run.
|
|
195
210
|
// If its live attempt did not submit, the message stays saved for the
|
|
@@ -197,6 +212,12 @@ export function prepareMessageContinuations(store, taskId, now, roleName) {
|
|
|
197
212
|
&& message.inputControl?.action !== "steer");
|
|
198
213
|
for (const message of pending) {
|
|
199
214
|
const recipient = message.recipient;
|
|
215
|
+
// Retain ordering within the claimed Assignment, not across unrelated
|
|
216
|
+
// work owned by the same Role. Only ack or explicit retirement settles a
|
|
217
|
+
// claim's scheduling obligation; neither time nor a failed Run does.
|
|
218
|
+
if (claims.some(claim => claim.recipient?.workItemId === recipient.workItemId
|
|
219
|
+
&& claim.recipient?.reviewRoundId === recipient.reviewRoundId))
|
|
220
|
+
continue;
|
|
200
221
|
const blocker = messageContinuationBlocker(store, message);
|
|
201
222
|
if (blocker !== undefined) {
|
|
202
223
|
markNotDelivered(store, message, blocker);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
2
|
+
export function latestMessageUpdate(store, message) {
|
|
3
|
+
return store.listEvents(message.taskId).filter(event => event.type === "message.updated" && event.payload.messageId === message.id).at(-1);
|
|
4
|
+
}
|
|
5
|
+
/** Leader steer has no Assignment recipient; ordinary Task records are not
|
|
6
|
+
* private inbox messages. Queued Leader inputs remain fixed wake windows. */
|
|
7
|
+
export function isLeaderSteer(message) {
|
|
8
|
+
return message.recipient === undefined && message.inputControl?.action === "steer";
|
|
9
|
+
}
|
|
10
|
+
/** Derive ownership from original receipts, never from disposable mailboxes. */
|
|
11
|
+
export function taskMessageInboxReceipt(store, message) {
|
|
12
|
+
const revision = latestMessageUpdate(store, message);
|
|
13
|
+
return message.inbox ?? store.listTaskWakes(message.taskId).find(wake => wake.inbox !== undefined
|
|
14
|
+
&& (revision === undefined || wake.refs?.some(ref => ref.type === "event" && ref.taskId === message.taskId && ref.id === revision.id))
|
|
15
|
+
&& wake.refs?.some(ref => ref.type === "message" && ref.taskId === message.taskId && ref.id === message.id))?.inbox;
|
|
16
|
+
}
|
|
17
|
+
export function unfinishedLeaderInbox(store, taskId) {
|
|
18
|
+
return store.listTaskWakes(taskId).some(wake => wake.inbox !== undefined && wake.inbox.ack === undefined)
|
|
19
|
+
|| operationalTaskRecords(store.listMessages(taskId), store.listEvents(taskId), "message").some(message => isLeaderSteer(message)
|
|
20
|
+
&& message.inbox !== undefined && message.inbox.ack === undefined);
|
|
21
|
+
}
|
|
@@ -12,7 +12,7 @@ import { createHash } from "node:crypto";
|
|
|
12
12
|
import { spawnSync } from "node:child_process";
|
|
13
13
|
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
|
-
import Database from "
|
|
15
|
+
import Database from "../sqlite/driver.js";
|
|
16
16
|
import { CURRENT_STORAGE_VERSION, MIN_SUPPORTED_STORAGE_VERSION } from "../storage/storageVersions.js";
|
|
17
17
|
import { inspectStorageSchema } from "../storage/storageSchema.js";
|
|
18
18
|
import { resolveStoreWorkerEnabledForHome } from "../storage/storeRpc.js";
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { existsSync } from "node:fs";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { isDeepStrictEqual } from "node:util";
|
|
16
|
-
import Database from "
|
|
16
|
+
import Database from "../sqlite/driver.js";
|
|
17
17
|
import { validateSqliteSchema } from "../storage/sqliteSchema.js";
|
|
18
18
|
import { emptyResourceRegistry, parseResourceRegistryState } from "./resourceRegistry.js";
|
|
19
19
|
import { RESOURCE_REGISTRY_SCHEMA_VERSION } from "./resourceTypes.js";
|
|
@@ -93,6 +93,9 @@ export async function runAgentHost(input) {
|
|
|
93
93
|
let activeRunPayload;
|
|
94
94
|
let lastTerminalPayload;
|
|
95
95
|
let lastTerminal;
|
|
96
|
+
// One exact direct Turn witnessed by this Endpoint under its original fence.
|
|
97
|
+
// This is recovery evidence, never ownership of the Desktop client's input.
|
|
98
|
+
let directObservation;
|
|
96
99
|
let activeRunAttemptId;
|
|
97
100
|
let activeNativeTurnId;
|
|
98
101
|
let codexClientAttachedAt;
|
|
@@ -166,6 +169,7 @@ export async function runAgentHost(input) {
|
|
|
166
169
|
const handleStarted = (started, observedPayload = sessionPayload) => {
|
|
167
170
|
if (started.clientOwned || observedPayload === undefined)
|
|
168
171
|
return;
|
|
172
|
+
const observedAuthority = authority === undefined ? undefined : { ...authority };
|
|
169
173
|
updateSnapshot(hostSnapshot("busy", {
|
|
170
174
|
adapterId: observedPayload.providerControl?.adapterId,
|
|
171
175
|
processInstanceId: session?.processInstanceId,
|
|
@@ -175,6 +179,22 @@ export async function runAgentHost(input) {
|
|
|
175
179
|
...authorityFields()
|
|
176
180
|
}));
|
|
177
181
|
void enqueueSerialized(async () => {
|
|
182
|
+
// Desktop can start another Turn before the Controller's periodic pass.
|
|
183
|
+
// Persist the previous exact terminal before replacing its bounded cache
|
|
184
|
+
// or publishing the new start. Never clear the newer native writer.
|
|
185
|
+
const previous = directObservation;
|
|
186
|
+
if (previous?.terminal !== undefined && authority !== undefined
|
|
187
|
+
&& sameProviderAuthorityFence(previous.authority, authority)
|
|
188
|
+
&& previous.started.nativeSessionId === session?.nativeSessionId) {
|
|
189
|
+
await publishStructuredProviderTerminal({
|
|
190
|
+
home: input.home, environment: previous.payload.environment, terminal: previous.terminal
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (session !== undefined && started.nativeSessionId === session.nativeSessionId
|
|
194
|
+
&& started.conversationId === session.conversationId && observedAuthority !== undefined
|
|
195
|
+
&& authority !== undefined && sameProviderAuthorityFence(observedAuthority, authority)) {
|
|
196
|
+
directObservation = { started, payload: observedPayload, authority: observedAuthority };
|
|
197
|
+
}
|
|
178
198
|
await publishStructuredProviderStarted({
|
|
179
199
|
home: input.home,
|
|
180
200
|
environment: observedPayload.environment,
|
|
@@ -194,6 +214,14 @@ export async function runAgentHost(input) {
|
|
|
194
214
|
// AgentRun audit record. It has no WorkItem ownership and no managed input
|
|
195
215
|
// fence; the runtime observer records only visible input/output.
|
|
196
216
|
void enqueueSerialized(async () => {
|
|
217
|
+
const direct = directObservation;
|
|
218
|
+
if (direct !== undefined && authority !== undefined
|
|
219
|
+
&& sameProviderAuthorityFence(direct.authority, authority)
|
|
220
|
+
&& direct.started.nativeSessionId === terminal.nativeSessionId
|
|
221
|
+
&& direct.started.conversationId === terminal.conversationId
|
|
222
|
+
&& direct.started.nativeTurnId === terminal.nativeTurnId) {
|
|
223
|
+
direct.terminal = terminal;
|
|
224
|
+
}
|
|
197
225
|
await publishStructuredProviderTerminal({
|
|
198
226
|
home: input.home,
|
|
199
227
|
environment: busyPayload.environment,
|
|
@@ -274,6 +302,7 @@ export async function runAgentHost(input) {
|
|
|
274
302
|
conversationId: terminal.conversationId,
|
|
275
303
|
...authorityFields()
|
|
276
304
|
}));
|
|
305
|
+
signalRoleMailbox(input.home, currentPayload);
|
|
277
306
|
promptHuman();
|
|
278
307
|
}
|
|
279
308
|
catch (error) {
|
|
@@ -621,6 +650,7 @@ export async function runAgentHost(input) {
|
|
|
621
650
|
await recordProviderConnection(input.home, next, started.session);
|
|
622
651
|
lastTerminalPayload = undefined;
|
|
623
652
|
lastTerminal = undefined;
|
|
653
|
+
directObservation = undefined;
|
|
624
654
|
// Recoverable means a later process can rebind this Conversation by
|
|
625
655
|
// its native id. The Endpoint answers that from the Driver capability
|
|
626
656
|
// or, where the protocol settles it per connection, from what this
|
|
@@ -1157,6 +1187,51 @@ export async function runAgentHost(input) {
|
|
|
1157
1187
|
}
|
|
1158
1188
|
return controlResult("accepted", { ...snapshot, nativeTui: !!nativeTui });
|
|
1159
1189
|
}
|
|
1190
|
+
if (request.type === "reconcile-turn") {
|
|
1191
|
+
if (session === undefined || session.nativeSessionId !== request.nativeSessionId
|
|
1192
|
+
|| authority === undefined || !sameProviderAuthorityFence(authority, request.authority)) {
|
|
1193
|
+
throw new Error("Terminal reconciliation targets a different Session or authority.");
|
|
1194
|
+
}
|
|
1195
|
+
const direct = directObservation;
|
|
1196
|
+
const directMatch = direct?.terminal !== undefined
|
|
1197
|
+
&& request.nativeTurnId !== undefined
|
|
1198
|
+
&& request.attemptId === `direct:${request.nativeTurnId}`
|
|
1199
|
+
&& direct.started.nativeTurnId === request.nativeTurnId
|
|
1200
|
+
&& direct.started.nativeSessionId === request.nativeSessionId
|
|
1201
|
+
&& sameProviderAuthorityFence(direct.authority, request.authority);
|
|
1202
|
+
const terminal = directMatch ? direct.terminal : lastTerminal;
|
|
1203
|
+
const terminalPayload = directMatch ? direct.payload : lastTerminalPayload;
|
|
1204
|
+
if (terminal === undefined || terminalPayload === undefined
|
|
1205
|
+
|| terminal.nativeSessionId !== request.nativeSessionId
|
|
1206
|
+
|| !directMatch && (terminal.clientOwned !== true || terminal.attemptId !== request.attemptId)
|
|
1207
|
+
|| request.nativeTurnId !== undefined && terminal.nativeTurnId !== request.nativeTurnId) {
|
|
1208
|
+
return { ...controlResult("status", snapshot), reconciliation: "unobserved" };
|
|
1209
|
+
}
|
|
1210
|
+
// Serialize with submission/terminal persistence. The original Host and
|
|
1211
|
+
// original launch envelope remain the observer, including the full result.
|
|
1212
|
+
await enqueueSerialized(async () => {
|
|
1213
|
+
if (session?.nativeSessionId !== request.nativeSessionId || authority === undefined
|
|
1214
|
+
|| !sameProviderAuthorityFence(authority, request.authority)
|
|
1215
|
+
|| directMatch && directObservation !== direct) {
|
|
1216
|
+
throw new Error("Session changed before terminal reconciliation.");
|
|
1217
|
+
}
|
|
1218
|
+
await publishStructuredProviderTerminal({
|
|
1219
|
+
home: input.home, environment: terminalPayload.environment, terminal
|
|
1220
|
+
});
|
|
1221
|
+
if (!directMatch && activeRunAttemptId === terminal.attemptId) {
|
|
1222
|
+
activeRunPayload = undefined;
|
|
1223
|
+
activeRunAttemptId = undefined;
|
|
1224
|
+
activeNativeTurnId = undefined;
|
|
1225
|
+
updateSnapshot(hostSnapshot(session.inspect().activeNativeTurnId === undefined ? "idle" : "busy", {
|
|
1226
|
+
adapterId: session.adapterId, processInstanceId: session.processInstanceId,
|
|
1227
|
+
nativeSessionId: session.nativeSessionId, conversationId: session.conversationId,
|
|
1228
|
+
...authorityFields()
|
|
1229
|
+
}));
|
|
1230
|
+
}
|
|
1231
|
+
signalRoleMailbox(input.home, terminalPayload);
|
|
1232
|
+
});
|
|
1233
|
+
return { ...controlResult("status", snapshot), reconciliation: "terminal-republished" };
|
|
1234
|
+
}
|
|
1160
1235
|
if (request.type === "status") {
|
|
1161
1236
|
// Read the Agent's configuration at answer time, not from the stored
|
|
1162
1237
|
// snapshot. The Agent may have changed it since the last state
|
|
@@ -1461,6 +1536,11 @@ export async function inspectAgentHost(input) {
|
|
|
1461
1536
|
});
|
|
1462
1537
|
return result.snapshot;
|
|
1463
1538
|
}
|
|
1539
|
+
export async function reconcileAgentHostTurn(input) {
|
|
1540
|
+
// Periodic observation must not hold the scheduler behind an unavailable Host.
|
|
1541
|
+
// A timeout is only an observation failure, never permission to submit again.
|
|
1542
|
+
return exchangeAgentHostControl(agentHostControlSocketPath(input), input.control, 1_000);
|
|
1543
|
+
}
|
|
1464
1544
|
export async function waitForAgentHostLaunchAck(input) {
|
|
1465
1545
|
const deadline = Date.now() + (input.timeoutMs ?? AGENT_HOST_READY_TIMEOUT_MS);
|
|
1466
1546
|
let lastError;
|
|
@@ -1616,7 +1696,7 @@ export async function openAgentHostControl(home, payload, snapshot, dispatch) {
|
|
|
1616
1696
|
...result,
|
|
1617
1697
|
snapshot: {
|
|
1618
1698
|
...result.snapshot,
|
|
1619
|
-
...(payload.environment.YUI_SESSION_SCOPE
|
|
1699
|
+
...(!["task", "global"].includes(payload.environment.YUI_SESSION_SCOPE ?? "") ? {} : {
|
|
1620
1700
|
eventDelivery: structuredProviderEventDelivery(home, payload.environment, result.snapshot.nativeSessionId)
|
|
1621
1701
|
})
|
|
1622
1702
|
}
|
|
@@ -1691,7 +1771,8 @@ function boundControlResponse(result) {
|
|
|
1691
1771
|
// visible through an otherwise successful status request.
|
|
1692
1772
|
result = {
|
|
1693
1773
|
...controlResult(result.outcome, validateSnapshot(result.snapshot), result.failure === undefined ? undefined : providerDeliveryFailure(result.failure)),
|
|
1694
|
-
...(result.cancellation === undefined ? {} : { cancellation: result.cancellation })
|
|
1774
|
+
...(result.cancellation === undefined ? {} : { cancellation: result.cancellation }),
|
|
1775
|
+
...(result.reconciliation === undefined ? {} : { reconciliation: result.reconciliation })
|
|
1695
1776
|
};
|
|
1696
1777
|
if (withinControlBound(result))
|
|
1697
1778
|
return result;
|
|
@@ -1709,29 +1790,29 @@ function boundControlResponse(result) {
|
|
|
1709
1790
|
};
|
|
1710
1791
|
// Never make an omitted option list look like a complete empty enumeration.
|
|
1711
1792
|
if (result.snapshot.runConfiguration?.status === "observed") {
|
|
1712
|
-
result = controlResult(result.outcome, validateSnapshot({
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1793
|
+
result = { ...result, ...controlResult(result.outcome, validateSnapshot({
|
|
1794
|
+
...result.snapshot,
|
|
1795
|
+
runConfiguration: unknownAgentRunConfiguration("The Agent's reported configuration is too large for the Agent Host "
|
|
1796
|
+
+ "control response, so it could not be carried in full and none of it "
|
|
1797
|
+
+ "is shown rather than part of it.")
|
|
1798
|
+
}), result.failure) };
|
|
1718
1799
|
if (withinControlBound(result))
|
|
1719
1800
|
return result;
|
|
1720
1801
|
}
|
|
1721
|
-
let bounded = controlResult(result.outcome, {
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1802
|
+
let bounded = { ...result, ...controlResult(result.outcome, {
|
|
1803
|
+
...result.snapshot,
|
|
1804
|
+
...(result.snapshot.detail === undefined ? {} : { detail: clip(result.snapshot.detail, 1200) })
|
|
1805
|
+
}, result.failure === undefined ? undefined : { ...result.failure, detail: clip(result.failure.detail, 1200) }) };
|
|
1725
1806
|
const raw = bounded.failure?.raw;
|
|
1726
1807
|
if (withinControlBound(bounded) || raw === undefined)
|
|
1727
1808
|
return bounded;
|
|
1728
1809
|
let chars = raw.length;
|
|
1729
1810
|
while (!withinControlBound(bounded) && chars > 0) {
|
|
1730
1811
|
chars = Math.floor(chars / 2);
|
|
1731
|
-
bounded = controlResult(bounded.outcome, bounded.snapshot, {
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1812
|
+
bounded = { ...bounded, ...controlResult(bounded.outcome, bounded.snapshot, {
|
|
1813
|
+
...bounded.failure,
|
|
1814
|
+
raw: clip(raw, chars)
|
|
1815
|
+
}) };
|
|
1735
1816
|
}
|
|
1736
1817
|
return bounded;
|
|
1737
1818
|
}
|
|
@@ -1764,6 +1845,14 @@ function validateControl(control) {
|
|
|
1764
1845
|
}
|
|
1765
1846
|
if (control.type === "status")
|
|
1766
1847
|
return Object.freeze({ ...control });
|
|
1848
|
+
if (control.type === "reconcile-turn") {
|
|
1849
|
+
validateIdentity(control.nativeSessionId, "native Session id");
|
|
1850
|
+
validateIdentity(control.attemptId, "Provider input attempt id");
|
|
1851
|
+
if (control.nativeTurnId !== undefined)
|
|
1852
|
+
validateIdentity(control.nativeTurnId, "native Turn id");
|
|
1853
|
+
validateProviderAuthorityFence(control.authority);
|
|
1854
|
+
return Object.freeze({ ...control });
|
|
1855
|
+
}
|
|
1767
1856
|
if (control.type === "native-open" || control.type === "native-respond" || control.type === "native-model") {
|
|
1768
1857
|
validateIdentity(control.nativeSessionId, "native Session id");
|
|
1769
1858
|
if (control.type === "native-model" && (typeof control.model !== "string"
|