@zq-silk/yui 0.8.2 → 0.8.6
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/ARCHITECTURE.md +40 -22
- package/README.md +61 -9
- package/dist/cli/commandCatalog.js +31 -14
- package/dist/cli/operatorWizard.js +10 -20
- package/dist/cli.js +187 -22
- package/dist/commands/executionAuditCommands.js +30 -0
- package/dist/commands/operatorCommands.js +42 -1
- package/dist/commands/taskCommands.js +648 -74
- package/dist/commands/taskCompletionGate.js +166 -2
- package/dist/commands/taskContextCommand.js +6 -1
- package/dist/commands/taskInputCommands.js +48 -10
- package/dist/commands/taskNextActionCommand.js +36 -3
- package/dist/context/runContextPack.js +19 -4
- package/dist/context/sessionBootstrapManifest.js +83 -2
- package/dist/controller/clientRuntime.js +7 -7
- package/dist/controller/controller.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +64 -5
- package/dist/controller/handoverCandidate.js +10 -3
- package/dist/controller/sessionNotify.js +4 -22
- package/dist/executor/agentAdapter.js +2 -2
- package/dist/executor/agentExecutor.js +25 -5
- package/dist/executor/fileRoleLaunchPlanner.js +16 -11
- package/dist/integration/gitIntegrationService.js +50 -2
- package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
- package/dist/observability/executionAudit.js +47 -1
- package/dist/observability/faultClassification.js +6 -4
- package/dist/observability/orchestrationMetrics.js +196 -0
- package/dist/operator/operatorSessionHistory.js +36 -0
- package/dist/release/releaseHandover.js +7 -5
- package/dist/release/runtimeRelease.js +15 -0
- package/dist/repository/gitWorkspace.js +7 -4
- package/dist/repository/taskBaseFreshness.js +4 -2
- package/dist/repository/taskWorkspaceCoordinator.js +13 -10
- package/dist/review/deltaRecheck.js +3 -2
- package/dist/review/reviewFindingLedger.js +5 -4
- package/dist/review/reviewOutcomeClassifier.js +252 -54
- package/dist/review/taskFinalReviewContractEvent.js +1 -0
- package/dist/review/taskFinalReviewContractRebind.js +350 -0
- package/dist/run/agentRun.js +2 -2
- package/dist/run/runIdentity.js +10 -70
- package/dist/runtime/agentHost.js +3 -4
- package/dist/runtime/codexAppServerRuntime.js +6 -0
- package/dist/runtime/firstProgressStopLoss.js +52 -0
- package/dist/runtime/launchBroker.js +10 -2
- package/dist/runtime/runtimeDeadlines.js +14 -0
- package/dist/runtime/sessionTitle.js +24 -12
- package/dist/runtime/structuredProviderHost.js +7 -1
- package/dist/runtime/tmuxAdapters.js +10 -3
- package/dist/scheduler/activeRoleRunDelivery.js +20 -18
- package/dist/scheduler/leaderWakeupProcessor.js +33 -2
- package/dist/scheduler/wakeReason.js +2 -0
- package/dist/storage/sqliteStore.js +10 -1
- package/dist/storage/taskStore.js +12 -1
- package/dist/task/completionReadiness.js +91 -19
- package/dist/task/deliveryGuard.js +3 -1
- package/dist/task/nextAction.js +146 -51
- package/dist/task/publicationReference.js +1 -0
- package/dist/task/repairWave.js +14 -1
- package/dist/task/task.js +10 -0
- package/dist/web/webSnapshot.js +7 -1
- package/dist/workItem/workItem.js +12 -0
- package/dist/workspace/workItemChangeSetManager.js +2 -1
- package/i18n/README.zh-CN.md +28 -8
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +79 -32
- package/skills/yui-operator/SKILL.md +51 -10
- package/skills/yui-reviewer/SKILL.md +23 -0
- package/skills/yui-runtime/SKILL.md +7 -2
package/dist/run/agentRun.js
CHANGED
|
@@ -67,11 +67,11 @@ export function withAgentRunContextSnapshot(run, snapshot, deltaRefIds = []) {
|
|
|
67
67
|
if (run.status !== "active" || run.pushedAt !== undefined || run.deliveredAt !== undefined) {
|
|
68
68
|
throw new Error(`Cannot bind Context Snapshot after Run delivery: ${run.id}.`);
|
|
69
69
|
}
|
|
70
|
-
const assignment =
|
|
70
|
+
const assignment = createRunAssignment({
|
|
71
71
|
...run.assignment,
|
|
72
72
|
contextSnapshotRef: snapshot,
|
|
73
73
|
deltaRefIds
|
|
74
|
-
})
|
|
74
|
+
});
|
|
75
75
|
return validateAgentRun(Object.freeze({
|
|
76
76
|
...run,
|
|
77
77
|
assignment,
|
package/dist/run/runIdentity.js
CHANGED
|
@@ -1,82 +1,22 @@
|
|
|
1
1
|
import { MAX_SESSION_TITLE_LENGTH } from "../runtime/sessionTitle.js";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Prefixes managed launch input with its session title. The bootstrap body
|
|
4
|
+
* exposes the exact current Run identity to the Agent, while structured
|
|
5
|
+
* delivery metadata and control hooks retain the delivery fence.
|
|
6
|
+
*/
|
|
7
|
+
export function prefixYuiTitleInput(input, title) {
|
|
5
8
|
const text = input.replace(/\r/g, "").trim();
|
|
6
|
-
return managedRunInput(text, runId, title);
|
|
7
|
-
}
|
|
8
|
-
/** Replaces the causal token on an input already owned by a persisted Yui Run. */
|
|
9
|
-
export function retagYuiRunInput(input, runId, title) {
|
|
10
|
-
const lines = input.replace(/\r/g, "").split("\n");
|
|
11
|
-
if (lines.length < 3
|
|
12
|
-
|| lines[1] !== ""
|
|
13
|
-
|| runIdFromHeader(lines[0]) === undefined) {
|
|
14
|
-
throw new Error("Managed Run input header is required.");
|
|
15
|
-
}
|
|
16
|
-
return managedRunInput(lines.slice(2).join("\n").trim(), runId, title);
|
|
17
|
-
}
|
|
18
|
-
function managedRunInput(text, runId, title) {
|
|
19
|
-
const id = requireRunId(runId);
|
|
20
9
|
if (text.length === 0)
|
|
21
|
-
throw new Error("
|
|
22
|
-
return `${requireRunTitle(title)}
|
|
23
|
-
}
|
|
24
|
-
/** Reads the last managed Yui header from Codex's structured input-messages. */
|
|
25
|
-
export function yuiRunIdFromInputMessages(value) {
|
|
26
|
-
if (!Array.isArray(value))
|
|
27
|
-
return undefined;
|
|
28
|
-
let found;
|
|
29
|
-
for (const entry of value) {
|
|
30
|
-
if (typeof entry !== "string")
|
|
31
|
-
continue;
|
|
32
|
-
const lines = entry.replace(/\r/g, "").split("\n");
|
|
33
|
-
if (lines.length < 3 || lines[1] !== "")
|
|
34
|
-
continue;
|
|
35
|
-
const runId = runIdFromHeader(lines[0]);
|
|
36
|
-
if (runId !== undefined)
|
|
37
|
-
found = runId;
|
|
38
|
-
}
|
|
39
|
-
return found;
|
|
40
|
-
}
|
|
41
|
-
/** Removes only a valid managed Run header, preserving user-authored lookalikes. */
|
|
42
|
-
export function yuiRunBodyFromInputMessage(value) {
|
|
43
|
-
const lines = value.replace(/\r/g, "").trim().split("\n");
|
|
44
|
-
return lines.length >= 3
|
|
45
|
-
&& lines[1] === ""
|
|
46
|
-
&& runIdFromHeader(lines[0]) !== undefined
|
|
47
|
-
? lines.slice(2).join("\n")
|
|
48
|
-
: lines.join("\n");
|
|
49
|
-
}
|
|
50
|
-
function runIdFromHeader(line) {
|
|
51
|
-
const separator = line.lastIndexOf(TITLED_RUN_SEPARATOR);
|
|
52
|
-
if (separator > 0) {
|
|
53
|
-
try {
|
|
54
|
-
requireRunTitle(line.slice(0, separator));
|
|
55
|
-
return requireRunId(line.slice(separator + TITLED_RUN_SEPARATOR.length));
|
|
56
|
-
}
|
|
57
|
-
catch {
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return undefined;
|
|
10
|
+
throw new Error("Managed input body is required.");
|
|
11
|
+
return `${requireRunTitle(title)}\n\n${text}`;
|
|
62
12
|
}
|
|
63
13
|
function requireRunTitle(value) {
|
|
64
14
|
const title = value.trim();
|
|
65
15
|
if (title.length === 0
|
|
66
16
|
|| title.length > MAX_SESSION_TITLE_LENGTH
|
|
67
|
-
||
|
|
68
|
-
||
|
|
17
|
+
|| /[\r\n\0]/u.test(title)
|
|
18
|
+
|| !title.startsWith("Yui ")) {
|
|
69
19
|
throw new Error("Run title is invalid.");
|
|
70
20
|
}
|
|
71
21
|
return title;
|
|
72
22
|
}
|
|
73
|
-
function requireRunId(value) {
|
|
74
|
-
const id = value.trim();
|
|
75
|
-
if (id.length === 0
|
|
76
|
-
|| id.length > 1_024
|
|
77
|
-
|| ["__proto__", "prototype", "constructor", ".", ".."].includes(id)
|
|
78
|
-
|| /[\/\\\0\s]/.test(id)) {
|
|
79
|
-
throw new Error("Run id is invalid.");
|
|
80
|
-
}
|
|
81
|
-
return id;
|
|
82
|
-
}
|
|
@@ -13,9 +13,8 @@ import { ProviderDeliveryUnknownError, ProviderConversationMissingError, Provide
|
|
|
13
13
|
import { sameProviderAuthorityFence, validateProviderAuthorityFence } from "./providerAuthorityFence.js";
|
|
14
14
|
import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
|
|
15
15
|
import { readRuntimeStopReceipt, removeRuntimeStopReceipt } from "./runtimeStopReceipt.js";
|
|
16
|
+
import { AGENT_HOST_CONTROL_TIMEOUT_MS, AGENT_HOST_READY_TIMEOUT_MS } from "./runtimeDeadlines.js";
|
|
16
17
|
export const AGENT_HOST_CONTROL_PROTOCOL = "yui-agent-host/v2";
|
|
17
|
-
const HOST_CONTROL_TIMEOUT_MS = 35_000;
|
|
18
|
-
const HOST_READY_TIMEOUT_MS = 35_000;
|
|
19
18
|
const HOST_CONTROL_MAX_BYTES = 32 * 1024;
|
|
20
19
|
export function serializeAgentHostLaunchControl(control) {
|
|
21
20
|
return JSON.stringify(validateControl(control));
|
|
@@ -599,7 +598,7 @@ export async function inspectAgentHost(input) {
|
|
|
599
598
|
return result.snapshot;
|
|
600
599
|
}
|
|
601
600
|
export async function waitForAgentHostLaunchAck(input) {
|
|
602
|
-
const deadline = Date.now() + (input.timeoutMs ??
|
|
601
|
+
const deadline = Date.now() + (input.timeoutMs ?? AGENT_HOST_READY_TIMEOUT_MS);
|
|
603
602
|
let lastError;
|
|
604
603
|
while (Date.now() < deadline) {
|
|
605
604
|
try {
|
|
@@ -634,7 +633,7 @@ async function sendAgentHostControl(input) {
|
|
|
634
633
|
const timer = setTimeout(() => {
|
|
635
634
|
client.destroy();
|
|
636
635
|
reject(new Error("Agent Host control request timed out."));
|
|
637
|
-
},
|
|
636
|
+
}, AGENT_HOST_CONTROL_TIMEOUT_MS);
|
|
638
637
|
let settled = false;
|
|
639
638
|
const settle = (callback, value) => {
|
|
640
639
|
if (settled)
|
|
@@ -39,6 +39,12 @@ export class CodexAppServerRuntime {
|
|
|
39
39
|
const result = await this.transport.request("thread/resume", { threadId: id });
|
|
40
40
|
return parseThreadSnapshot(result, id, true);
|
|
41
41
|
}
|
|
42
|
+
async setConversationName(input) {
|
|
43
|
+
await this.transport.request("thread/name/set", {
|
|
44
|
+
threadId: text(input.conversationId, "Codex thread id"),
|
|
45
|
+
name: text(input.name, "Codex thread name")
|
|
46
|
+
});
|
|
47
|
+
}
|
|
42
48
|
async readConversation(conversationId) {
|
|
43
49
|
const id = text(conversationId, "Codex thread id");
|
|
44
50
|
const result = await this.transport.request("thread/read", {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
/** One automatic same-Session continuation is allowed before first progress. */
|
|
3
|
+
export function boundProviderRetryBeforeFirstProgress(policy, projection) {
|
|
4
|
+
return projection.firstProgressAt === undefined
|
|
5
|
+
? Object.freeze({ delaysMs: policy.delaysMs.slice(0, 1), maxWindowMs: policy.maxWindowMs })
|
|
6
|
+
: policy;
|
|
7
|
+
}
|
|
8
|
+
export function projectFirstProgressStopLoss(input) {
|
|
9
|
+
const sessions = input.sessions === null
|
|
10
|
+
? []
|
|
11
|
+
: [...(input.sessions.history ?? []), ...Object.values(input.sessions.sessions)]
|
|
12
|
+
.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
13
|
+
const unique = [...new Map(sessions.map((session) => [
|
|
14
|
+
`${session.nativeSessionId}\0${session.launchId ?? ""}`,
|
|
15
|
+
session
|
|
16
|
+
])).values()];
|
|
17
|
+
const firstGenerationAt = unique[0]?.createdAt;
|
|
18
|
+
const progress = firstGenerationAt === undefined
|
|
19
|
+
? []
|
|
20
|
+
: [
|
|
21
|
+
...input.events
|
|
22
|
+
.filter((event) => typeof event.payload.leaderRunId === "string")
|
|
23
|
+
.map((event) => ({ at: event.createdAt, ref: `event:${event.id}` })),
|
|
24
|
+
...input.workItems.map((item) => ({ at: item.createdAt, ref: `work-item:${item.id}` })),
|
|
25
|
+
...input.reviewRounds.map((round) => ({ at: round.createdAt, ref: `review-round:${round.id}` })),
|
|
26
|
+
...input.integrations.map((attempt) => ({ at: attempt.createdAt, ref: `integration-attempt:${attempt.id}` }))
|
|
27
|
+
]
|
|
28
|
+
.filter(({ at }) => at >= firstGenerationAt)
|
|
29
|
+
.sort((left, right) => left.at.localeCompare(right.at) || left.ref.localeCompare(right.ref));
|
|
30
|
+
const firstProgressAt = progress[0]?.at;
|
|
31
|
+
const generationsBeforeFirstProgress = unique.filter((session) => (firstProgressAt === undefined || session.createdAt <= firstProgressAt)).length;
|
|
32
|
+
const generationRefs = unique.map((session) => (`${session.nativeSessionId}@${session.launchId ?? session.createdAt}`));
|
|
33
|
+
const progressRefs = progress.map(({ ref }) => ref);
|
|
34
|
+
const exhausted = firstProgressAt === undefined && generationsBeforeFirstProgress >= 2;
|
|
35
|
+
const fingerprint = createHash("sha256")
|
|
36
|
+
.update(JSON.stringify({ generationRefs, progressRefs }))
|
|
37
|
+
.digest("hex");
|
|
38
|
+
return Object.freeze({
|
|
39
|
+
exhausted,
|
|
40
|
+
generationsBeforeFirstProgress,
|
|
41
|
+
...(firstGenerationAt === undefined ? {} : { firstGenerationAt }),
|
|
42
|
+
...(firstProgressAt === undefined ? {} : { firstProgressAt }),
|
|
43
|
+
generationRefs,
|
|
44
|
+
progressRefs,
|
|
45
|
+
fingerprint,
|
|
46
|
+
reason: exhausted
|
|
47
|
+
? `${generationsBeforeFirstProgress} fresh Leader generations produced no first durable progress; stop before creating another generation and hand off to the Operator.`
|
|
48
|
+
: firstProgressAt !== undefined
|
|
49
|
+
? `First durable progress was recorded at ${firstProgressAt}.`
|
|
50
|
+
: `Fewer than two Leader generations exist before first durable progress.`
|
|
51
|
+
});
|
|
52
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { validateProviderAuthorityFence } from "./providerAuthorityFence.js";
|
|
4
|
+
import { AGENT_HOST_LAUNCH_TICKET_TTL_MS } from "./runtimeDeadlines.js";
|
|
4
5
|
const brokers = new Map();
|
|
5
|
-
const TICKET_TTL_MS = 60_000;
|
|
6
6
|
/** One Controller-process broker per canonical Home. Payloads never hit disk or tmux. */
|
|
7
7
|
export function launchBrokerForHome(home) {
|
|
8
8
|
const key = resolve(home);
|
|
@@ -34,7 +34,7 @@ export class LaunchBroker {
|
|
|
34
34
|
throw new Error("Launch ticket is invalid or already consumed.");
|
|
35
35
|
}
|
|
36
36
|
this.#reservations.delete(launchId);
|
|
37
|
-
if (Date.now() - reservation.createdAt >
|
|
37
|
+
if (Date.now() - reservation.createdAt > AGENT_HOST_LAUNCH_TICKET_TTL_MS) {
|
|
38
38
|
throw new Error("Launch ticket expired before redemption.");
|
|
39
39
|
}
|
|
40
40
|
return reservation.payload;
|
|
@@ -99,6 +99,14 @@ function validateProviderControl(control) {
|
|
|
99
99
|
}
|
|
100
100
|
if (control.nativeSessionId !== undefined)
|
|
101
101
|
text(control.nativeSessionId, "nativeSessionId");
|
|
102
|
+
if (control.sessionTitle !== undefined) {
|
|
103
|
+
const title = control.sessionTitle.trim();
|
|
104
|
+
if (title.length === 0
|
|
105
|
+
|| title.length > 1_024
|
|
106
|
+
|| /[\r\n\0]/u.test(title)) {
|
|
107
|
+
throw new Error("Agent Host Provider session title is invalid.");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
102
110
|
validateProviderAuthorityFence(control.authority);
|
|
103
111
|
if (control.initialTurn !== undefined) {
|
|
104
112
|
text(control.initialTurn.attemptId, "Provider input attemptId");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed-runtime deadline hierarchy. Each outer boundary must outlive the
|
|
3
|
+
* inner operation it awaits, with enough margin to persist observations and
|
|
4
|
+
* return the enclosing acknowledgement.
|
|
5
|
+
*/
|
|
6
|
+
export const PROVIDER_ACCEPT_TIMEOUT_MS = 60_000;
|
|
7
|
+
const RUNTIME_ENVELOPE_MARGIN_MS = 15_000;
|
|
8
|
+
export const AGENT_HOST_CONTROL_TIMEOUT_MS = PROVIDER_ACCEPT_TIMEOUT_MS + RUNTIME_ENVELOPE_MARGIN_MS;
|
|
9
|
+
export const AGENT_HOST_READY_TIMEOUT_MS = AGENT_HOST_CONTROL_TIMEOUT_MS;
|
|
10
|
+
export const AGENT_HOST_LAUNCH_TICKET_TTL_MS = AGENT_HOST_READY_TIMEOUT_MS;
|
|
11
|
+
export const LIFECYCLE_REQUEST_TIMEOUT_MS = AGENT_HOST_CONTROL_TIMEOUT_MS + RUNTIME_ENVELOPE_MARGIN_MS;
|
|
12
|
+
export const CONTROLLER_SHUTDOWN_TIMEOUT_MS = LIFECYCLE_REQUEST_TIMEOUT_MS + RUNTIME_ENVELOPE_MARGIN_MS;
|
|
13
|
+
export const RELEASE_HANDOVER_OLD_OWNER_GRACE_MS = CONTROLLER_SHUTDOWN_TIMEOUT_MS + RUNTIME_ENVELOPE_MARGIN_MS;
|
|
14
|
+
export const RELEASE_HANDOVER_PROMOTION_TIMEOUT_MS = RELEASE_HANDOVER_OLD_OWNER_GRACE_MS + RUNTIME_ENVELOPE_MARGIN_MS;
|
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
const TITLE_SEPARATOR = " · ";
|
|
2
|
-
export const MAX_SESSION_TITLE_LENGTH =
|
|
2
|
+
export const MAX_SESSION_TITLE_LENGTH = 80;
|
|
3
|
+
const TASK_TITLE_MAX_LENGTH = 20;
|
|
3
4
|
export function taskRoleSessionTitle(task, roleName) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const normalized = segments.map(normalizeSegment);
|
|
8
|
-
const full = normalized.join(TITLE_SEPARATOR);
|
|
5
|
+
const prefix = `Yui ${roleLabel(roleName)} ${normalizeSegment(task.id)}`;
|
|
6
|
+
const title = displayTitle(normalizeSegment(task.title), TASK_TITLE_MAX_LENGTH);
|
|
7
|
+
const full = `${prefix}${TITLE_SEPARATOR}${title}`;
|
|
9
8
|
if (full.length <= MAX_SESSION_TITLE_LENGTH)
|
|
10
9
|
return full;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
if (prefix.length + TITLE_SEPARATOR.length + 1 > MAX_SESSION_TITLE_LENGTH) {
|
|
11
|
+
return truncate(prefix, MAX_SESSION_TITLE_LENGTH);
|
|
12
|
+
}
|
|
13
|
+
const titleLength = MAX_SESSION_TITLE_LENGTH - prefix.length - TITLE_SEPARATOR.length - 1;
|
|
14
|
+
return `${prefix}${TITLE_SEPARATOR}${displayTitle(title, Math.max(titleLength, 1))}`;
|
|
15
|
+
}
|
|
16
|
+
export function resolveTaskRoleSessionTitle(existingTitle, task, roleName) {
|
|
17
|
+
if (existingTitle !== undefined
|
|
18
|
+
&& existingTitle.length > 0
|
|
19
|
+
&& existingTitle.length <= MAX_SESSION_TITLE_LENGTH
|
|
20
|
+
&& !/[\r\n\0]/u.test(existingTitle)) {
|
|
21
|
+
return existingTitle;
|
|
22
|
+
}
|
|
23
|
+
return taskRoleSessionTitle(task, roleName);
|
|
24
|
+
}
|
|
25
|
+
function displayTitle(value, maxLength) {
|
|
26
|
+
if (value.length <= maxLength)
|
|
27
|
+
return value;
|
|
28
|
+
return `${truncate(value, maxLength - 1)}…`;
|
|
17
29
|
}
|
|
18
30
|
function normalizeSegment(value) {
|
|
19
31
|
if (typeof value !== "string" || value.includes("\0")) {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { CodexAppServerRequestError, CodexAppServerRuntime, codexAppServerErrorIsMissing } from "./codexAppServerRuntime.js";
|
|
4
|
+
import { PROVIDER_ACCEPT_TIMEOUT_MS } from "./runtimeDeadlines.js";
|
|
4
5
|
import { YUI_VERSION } from "../version.js";
|
|
5
6
|
const PROVIDER_MESSAGE_MAX_BYTES = 16 * 1024 * 1024;
|
|
6
|
-
const PROVIDER_ACCEPT_TIMEOUT_MS = 30_000;
|
|
7
7
|
export class ProviderDeliveryUnknownError extends Error {
|
|
8
8
|
attemptId;
|
|
9
9
|
name = "ProviderDeliveryUnknownError";
|
|
@@ -219,6 +219,12 @@ class CodexStructuredProviderSession {
|
|
|
219
219
|
throw error;
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
+
if (control.sessionTitle !== undefined) {
|
|
223
|
+
await runtime.setConversationName({
|
|
224
|
+
conversationId,
|
|
225
|
+
name: control.sessionTitle
|
|
226
|
+
});
|
|
227
|
+
}
|
|
222
228
|
const session = new CodexStructuredProviderSession(child, exit, processInstanceId, conversationId, runtime);
|
|
223
229
|
session.#activeTurnId = resumedActiveTurnId;
|
|
224
230
|
channel.onMessage((message) => {
|
|
@@ -216,6 +216,9 @@ export class TmuxSessionHost {
|
|
|
216
216
|
agentId: request.agentId,
|
|
217
217
|
adapterId: request.adapterId,
|
|
218
218
|
effective: request.effective,
|
|
219
|
+
...(planned.launch.env.YUI_SESSION_TITLE === undefined
|
|
220
|
+
? {}
|
|
221
|
+
: { sessionTitle: planned.launch.env.YUI_SESSION_TITLE }),
|
|
219
222
|
...(nativeSessionId === undefined ? {} : { nativeSessionId }),
|
|
220
223
|
...(planned.initialTurnRunId === undefined
|
|
221
224
|
? {}
|
|
@@ -223,9 +226,13 @@ export class TmuxSessionHost {
|
|
|
223
226
|
});
|
|
224
227
|
const yuiHome = planned.launch.env.YUI_HOME;
|
|
225
228
|
const childLifecycle = planned.launch.childLifecycle;
|
|
226
|
-
// Interactive/global Roles
|
|
227
|
-
//
|
|
228
|
-
|
|
229
|
+
// Interactive/global Roles remain native TUIs even when their Driver
|
|
230
|
+
// advertises a persistent child lifecycle. Provider control metadata is
|
|
231
|
+
// the discriminator for the structured Agent Host path. A managed Task
|
|
232
|
+
// Run has no terminal-write fallback and must expose that contract.
|
|
233
|
+
if (yuiHome === undefined
|
|
234
|
+
|| childLifecycle === undefined
|
|
235
|
+
|| planned.launch.providerControl === undefined) {
|
|
229
236
|
if (request.owner.scope === "task" && request.runId !== undefined) {
|
|
230
237
|
throw new Error("Managed Task Run is missing its structured Agent Host contract.");
|
|
231
238
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { selectedSchedulerRoles, selectedActiveSchedulerTasks } from "./ports.js";
|
|
2
2
|
import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
3
3
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { prefixYuiTitleInput } from "../run/runIdentity.js";
|
|
5
|
+
import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
6
6
|
import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
|
|
7
7
|
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain } from "../executor/effectiveLaunch.js";
|
|
8
8
|
import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
@@ -298,24 +298,25 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
|
|
|
298
298
|
}
|
|
299
299
|
providerSubmissionBegun = true;
|
|
300
300
|
deliveryAttempted = true;
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
301
|
+
const launchText = run.controlRequest?.state === "dispatching"
|
|
302
|
+
? serializeWorkflowOutcomeRequestEnvelope({
|
|
303
|
+
taskId: task.id,
|
|
304
|
+
runId: run.id,
|
|
305
|
+
roleName: role.name,
|
|
306
|
+
request: run.controlRequest
|
|
307
|
+
})
|
|
308
|
+
: run.providerRetry?.state === "dispatching"
|
|
309
|
+
? serializeProviderRetryEnvelope({
|
|
306
310
|
taskId: task.id,
|
|
307
311
|
runId: run.id,
|
|
308
312
|
roleName: role.name,
|
|
309
|
-
|
|
313
|
+
retry: run.providerRetry
|
|
310
314
|
})
|
|
311
|
-
: run.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
retry: run.providerRetry
|
|
317
|
-
})
|
|
318
|
-
: serializeRunBootstrapEnvelope(run.bootstrapEnvelope)
|
|
315
|
+
: serializeRunBootstrapEnvelope(run.bootstrapEnvelope);
|
|
316
|
+
const outcome = await delivery.sendOnce({
|
|
317
|
+
delivery: ready,
|
|
318
|
+
receiptId,
|
|
319
|
+
text: prefixYuiTitleInput(launchText, resolveTaskRoleSessionTitle(session.title, task, role.name))
|
|
319
320
|
});
|
|
320
321
|
if (outcome === "busy" || outcome === "unavailable") {
|
|
321
322
|
store.resolveRoleRunProviderSubmission?.({
|
|
@@ -791,7 +792,7 @@ function continuationInput(task, role, run, attemptId, batch, resultSummaries) {
|
|
|
791
792
|
const references = batch.refs.map((ref) => ("taskId" in ref
|
|
792
793
|
? `${ref.type}:${ref.taskId}/${ref.id}`
|
|
793
794
|
: `${ref.type}:${ref.id}`));
|
|
794
|
-
return
|
|
795
|
+
return [
|
|
795
796
|
`Yui Task Event Batch: ${attemptId}.`,
|
|
796
797
|
"New durable task events are available for the current Yui Run.",
|
|
797
798
|
"Read the referenced shared context through the Yui CLI, incorporate it, and decide whether to continue work or wait for more results.",
|
|
@@ -805,7 +806,7 @@ function continuationInput(task, role, run, attemptId, batch, resultSummaries) {
|
|
|
805
806
|
"Native child results (bounded excerpts; read the referenced event for the full content):",
|
|
806
807
|
...resultSummaries
|
|
807
808
|
])
|
|
808
|
-
].join("\n")
|
|
809
|
+
].join("\n");
|
|
809
810
|
}
|
|
810
811
|
/**
|
|
811
812
|
* Issue 13: the parent prompt only ever sees a bounded excerpt of a native
|
|
@@ -911,6 +912,7 @@ function preflightSession(role, effective, existing, mode, preflight) {
|
|
|
911
912
|
adapterId: preflight.adapterId,
|
|
912
913
|
nativeSessionId: preflight.nativeSessionId,
|
|
913
914
|
launchId: preflight.launchId,
|
|
915
|
+
...(preflight.sessionTitle === undefined ? {} : { title: preflight.sessionTitle }),
|
|
914
916
|
status: "ready",
|
|
915
917
|
effective: preflight.effective
|
|
916
918
|
};
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { createAgentRun } from "../run/agentRun.js";
|
|
2
2
|
import { createRunAssignment, serializeRunBootstrapEnvelope } from "../context/runContextContract.js";
|
|
3
3
|
import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
|
|
4
|
+
import { prefixYuiTitleInput } from "../run/runIdentity.js";
|
|
5
|
+
import { resolveTaskRoleSessionTitle } from "../runtime/sessionTitle.js";
|
|
4
6
|
import { effectiveLaunchSnapshotsCompatible, effectiveLaunchSnapshotsCompatibleForTaskMain } from "../executor/effectiveLaunch.js";
|
|
5
7
|
import { hasRuntimeLifecycleWork, RuntimeLifecycleBusyError, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
|
|
6
8
|
import { recordLeaderFailure } from "./leaderFailure.js";
|
|
7
9
|
import { createLeaderRecoveryNotification } from "./operatorNotification.js";
|
|
8
10
|
import { isSchedulerTaskWorkspaceReady } from "./ports.js";
|
|
9
11
|
import { RuntimeLaunchError } from "../runtime/ports.js";
|
|
12
|
+
import { projectFirstProgressStopLoss } from "../runtime/firstProgressStopLoss.js";
|
|
10
13
|
export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
11
14
|
const results = [];
|
|
12
15
|
const wakeups = selection === undefined || selection.full
|
|
@@ -121,7 +124,34 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
121
124
|
&& existingSession.status !== "stopped" && existingSession.status !== "broken") {
|
|
122
125
|
throw new Error(`Leader Session is incompatible with desired effective launch: ${task.id}/${role.name}.`);
|
|
123
126
|
}
|
|
124
|
-
const
|
|
127
|
+
const resumableSession = hasNativeSession(existingSession)
|
|
128
|
+
&& existingSession.status !== "stopped"
|
|
129
|
+
&& existingSession.status !== "broken";
|
|
130
|
+
const mode = resumableSession && compatibleSession ? "resume" : "new";
|
|
131
|
+
if (mode === "new" && store.getTaskRoleSessionSet !== undefined) {
|
|
132
|
+
const stopLoss = projectFirstProgressStopLoss({
|
|
133
|
+
sessions: store.getTaskRoleSessionSet(task.id, role.name),
|
|
134
|
+
events: store.listEvents?.(task.id) ?? [],
|
|
135
|
+
workItems: store.listWorkItems?.(task.id) ?? [],
|
|
136
|
+
reviewRounds: store.listReviewRounds?.(task.id) ?? [],
|
|
137
|
+
integrations: store.listIntegrationAttempts?.(task.id) ?? []
|
|
138
|
+
});
|
|
139
|
+
if (stopLoss.exhausted && store.saveLeaderFirstProgressStopLoss !== undefined) {
|
|
140
|
+
const saved = store.saveLeaderFirstProgressStopLoss({
|
|
141
|
+
taskId: task.id,
|
|
142
|
+
roleName: role.name,
|
|
143
|
+
expectedFingerprint: stopLoss.fingerprint,
|
|
144
|
+
now
|
|
145
|
+
});
|
|
146
|
+
results.push({
|
|
147
|
+
taskId: task.id,
|
|
148
|
+
status: "skipped",
|
|
149
|
+
reason: saved === "recorded" ? "recovery-blocked" : "state-changed",
|
|
150
|
+
error: saved === "recorded" ? stopLoss.reason : undefined
|
|
151
|
+
});
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
125
155
|
const runId = store.peekNextAgentRunId(task.id);
|
|
126
156
|
const wakeEnvelope = resolveLeaderWakeEnvelope(store, task.id);
|
|
127
157
|
const contextSnapshot = store.freezeLeaderContextSnapshot?.(task.id, role.name, now);
|
|
@@ -328,7 +358,7 @@ export async function processLeaderWakeups(store, delivery, now, selection) {
|
|
|
328
358
|
const outcome = await delivery.sendOnce({
|
|
329
359
|
delivery: ready,
|
|
330
360
|
receiptId,
|
|
331
|
-
text: serializeRunBootstrapEnvelope(run.bootstrapEnvelope)
|
|
361
|
+
text: prefixYuiTitleInput(serializeRunBootstrapEnvelope(run.bootstrapEnvelope), resolveTaskRoleSessionTitle(effectiveSession.title, task, role.name))
|
|
332
362
|
});
|
|
333
363
|
if (outcome === "busy" || outcome === "unavailable") {
|
|
334
364
|
store.resolveRoleRunProviderSubmission?.({
|
|
@@ -559,6 +589,7 @@ function preflightSession(role, effective, existing, mode, preflight) {
|
|
|
559
589
|
adapterId: preflight.adapterId,
|
|
560
590
|
nativeSessionId: preflight.nativeSessionId,
|
|
561
591
|
launchId: preflight.launchId,
|
|
592
|
+
...(preflight.sessionTitle === undefined ? {} : { title: preflight.sessionTitle }),
|
|
562
593
|
status: "ready",
|
|
563
594
|
effective: preflight.effective
|
|
564
595
|
};
|
|
@@ -40,6 +40,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
40
40
|
import Database from "better-sqlite3";
|
|
41
41
|
import { consumePendingBatch, mailboxTargetKey, pendingLane, validateWorkMailbox } from "../coordination/workMailbox.js";
|
|
42
42
|
import { validateContextSnapshot } from "../context/contextSnapshot.js";
|
|
43
|
+
import { TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT } from "../review/taskFinalReviewContractEvent.js";
|
|
43
44
|
import { compareRuntimeSessionCandidates, projectRuntimeSessionCandidate } from "../runtime/runtimeSessionCandidate.js";
|
|
44
45
|
import { validateReviewFinding } from "../review/reviewFinding.js";
|
|
45
46
|
import { reviewFindingLedgerMode } from "../review/reviewFindingLedger.js";
|
|
@@ -720,10 +721,16 @@ export class SqliteTaskStore {
|
|
|
720
721
|
changeSets: this.#sortById(this.#listPayload("change_sets", "task_id = ?", [taskId]), (changeSet) => changeSet.id),
|
|
721
722
|
integrations: this.#sortById(this.#listPayload("integration_attempts", "task_id = ?", [taskId]), (attempt) => attempt.id),
|
|
722
723
|
reviewRounds: this.#sortById(this.#listPayload("review_rounds", "task_id = ?", [taskId]), (round) => round.id),
|
|
724
|
+
taskFinalReviewContractEvents: this.#sortById(this.#listPayload("events", "task_id = ? AND type = ?", [taskId, TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT]), (event) => event.id),
|
|
723
725
|
reviewConfig: this.getReviewConfig(),
|
|
724
726
|
openInputRequests: this.#sortById(this.#listPayload("input_requests", "task_id = ? AND status = 'open'", [taskId]), (request) => request.id),
|
|
725
727
|
activeRuns: runs.filter((run) => run.status === "active"),
|
|
726
|
-
leaderRuns: runs.filter((run) => run.roleName === "leader")
|
|
728
|
+
leaderRuns: runs.filter((run) => run.roleName === "leader"),
|
|
729
|
+
reviewOutcomeEvidence: {
|
|
730
|
+
agentRuns: this.#sortById(this.#listPayload("agent_runs", "task_id = ?", [taskId]).filter((run) => run.purpose === "review"), (run) => run.id),
|
|
731
|
+
reviewFindings: this.listReviewFindings(taskId),
|
|
732
|
+
events: this.listEvents(taskId).filter((event) => event.type === "review.completed")
|
|
733
|
+
}
|
|
727
734
|
};
|
|
728
735
|
}
|
|
729
736
|
readCompletionReadinessFacts(taskId) {
|
|
@@ -732,6 +739,8 @@ export class SqliteTaskStore {
|
|
|
732
739
|
return null;
|
|
733
740
|
return {
|
|
734
741
|
...base,
|
|
742
|
+
agentRuns: this.listAgentRuns(taskId),
|
|
743
|
+
roleSessionSets: this.listRoleSessionSets(taskId),
|
|
735
744
|
managedWorkspaces: this.#sortById(this.#listPayload("managed_workspaces", "task_id = ?", [taskId]), (workspace) => managedWorkspaceKey(workspace.owner)),
|
|
736
745
|
durableJobs: this.#sortById(this.#listPayload("durable_jobs", "task_id = ?", [taskId]), (job) => job.id),
|
|
737
746
|
integrationQueueEntries: this.#sortById(this.#listPayload("integration_queue", "task_id = ?", [taskId]), (entry) => entry.id),
|
|
@@ -10,6 +10,7 @@ import { reconciliationIntervalMilliseconds, resolveAgentLaunchInactivityTimeout
|
|
|
10
10
|
import { resolveTimeZone } from "../output/timePresentation.js";
|
|
11
11
|
import { mailboxBatches, consumePendingBatch, mailboxHasWork, mailboxTargetKey, pendingLane, validateWorkMailbox } from "../coordination/workMailbox.js";
|
|
12
12
|
import { validateContextSnapshot } from "../context/contextSnapshot.js";
|
|
13
|
+
import { TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT } from "../review/taskFinalReviewContractEvent.js";
|
|
13
14
|
import { validateInputRequest } from "../input/inputRequest.js";
|
|
14
15
|
import { validateRoleSessionSet } from "../executor/agentExecutor.js";
|
|
15
16
|
import { validateTaskMessage } from "../message/message.js";
|
|
@@ -420,11 +421,19 @@ export class FileTaskStore {
|
|
|
420
421
|
changeSets: values(aggregate.changeSets, "id"),
|
|
421
422
|
integrations: values(aggregate.integrationAttempts, "id"),
|
|
422
423
|
reviewRounds: values(aggregate.reviewRounds, "id"),
|
|
424
|
+
taskFinalReviewContractEvents: values(aggregate.events, "id")
|
|
425
|
+
.filter((event) => event.type === TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT),
|
|
423
426
|
reviewConfig: this.getReviewConfig(),
|
|
424
427
|
openInputRequests: values(aggregate.inputRequests, "id")
|
|
425
428
|
.filter((request) => request.status === "open"),
|
|
426
429
|
activeRuns: agentRuns.filter((run) => run.status === "active"),
|
|
427
|
-
leaderRuns: agentRuns.filter((run) => run.roleName === "leader")
|
|
430
|
+
leaderRuns: agentRuns.filter((run) => run.roleName === "leader"),
|
|
431
|
+
reviewOutcomeEvidence: {
|
|
432
|
+
agentRuns: agentRuns.filter((run) => run.purpose === "review"),
|
|
433
|
+
// The rollback file backend has no finding-ledger records.
|
|
434
|
+
reviewFindings: [],
|
|
435
|
+
events: values(aggregate.events, "id").filter((event) => (event.type === "review.completed"))
|
|
436
|
+
}
|
|
428
437
|
};
|
|
429
438
|
}
|
|
430
439
|
readCompletionReadinessFacts(taskId) {
|
|
@@ -442,6 +451,8 @@ export class FileTaskStore {
|
|
|
442
451
|
}
|
|
443
452
|
return {
|
|
444
453
|
...base,
|
|
454
|
+
agentRuns: this.listAgentRuns(taskId),
|
|
455
|
+
roleSessionSets: this.listRoleSessionSets(taskId),
|
|
445
456
|
managedWorkspaces: values(aggregate.managedWorkspaces, (workspace) => managedWorkspaceKey(workspace.owner)),
|
|
446
457
|
durableJobs: values(aggregate.durableJobs, "id"),
|
|
447
458
|
integrationQueueEntries: values(aggregate.integrationQueue, "id"),
|