@zq-silk/yui 0.8.3 → 0.8.7
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 +66 -19
- package/dist/cli/commandCatalog.js +43 -14
- package/dist/cli/operatorWizard.js +10 -20
- package/dist/cli/updatePorts.js +6 -0
- package/dist/cli.js +252 -37
- package/dist/commands/executionAuditCommands.js +30 -0
- package/dist/commands/globalRoleCommands.js +8 -4
- package/dist/commands/operatorCommands.js +42 -1
- package/dist/commands/taskCommands.js +527 -147
- package/dist/commands/taskCompletionGate.js +36 -24
- package/dist/commands/taskContextCommand.js +11 -4
- package/dist/commands/taskInputCommands.js +48 -10
- package/dist/commands/taskNextActionCommand.js +38 -3
- 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 +158 -11
- package/dist/context/wakeNotification.js +5 -3
- package/dist/controller/clientRuntime.js +15 -15
- package/dist/controller/controller.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +67 -7
- 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 +29 -9
- package/dist/executor/fileRoleLaunchPlanner.js +37 -45
- 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/taskWorkspaceCoordinator.js +13 -10
- package/dist/review/deltaRecheck.js +3 -2
- package/dist/review/reviewFindingLedger.js +5 -4
- package/dist/review/reviewOutcomeClassifier.js +263 -54
- package/dist/review/taskFinalReviewContractEvent.js +1 -0
- package/dist/review/taskFinalReviewContractRebind.js +367 -0
- package/dist/run/runIdentity.js +10 -70
- package/dist/runtime/agentHost.js +3 -4
- package/dist/runtime/codexAppServerRuntime.js +6 -0
- package/dist/runtime/exactControlPlane.js +47 -37
- package/dist/runtime/firstProgressStopLoss.js +54 -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/actionability.js +4 -2
- package/dist/scheduler/activeRoleRunDelivery.js +20 -18
- package/dist/scheduler/activeTaskProgress.js +2 -1
- package/dist/scheduler/leaderWakeupProcessor.js +33 -2
- package/dist/scheduler/taskExecutionProjection.js +13 -4
- package/dist/scheduler/wakeReason.js +1 -0
- package/dist/storage/sqliteStore.js +18 -3
- package/dist/storage/taskStore.js +14 -3
- package/dist/task/completionReadiness.js +48 -19
- package/dist/task/deliveryGuard.js +3 -1
- package/dist/task/nextAction.js +153 -55
- package/dist/task/repairWave.js +14 -1
- package/dist/task/task.js +10 -0
- package/dist/task/taskRecordRetirement.js +72 -0
- package/dist/web/webSnapshot.js +7 -1
- package/dist/workItem/workItem.js +6 -4
- package/i18n/README.zh-CN.md +48 -9
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +73 -31
- package/skills/yui-operator/SKILL.md +58 -10
- package/skills/yui-reviewer/SKILL.md +23 -0
- package/skills/yui-runtime/SKILL.md +6 -6
|
@@ -0,0 +1,54 @@
|
|
|
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
|
|
25
|
+
.filter((item) => item.status !== "retired")
|
|
26
|
+
.map((item) => ({ at: item.createdAt, ref: `work-item:${item.id}` })),
|
|
27
|
+
...input.reviewRounds.map((round) => ({ at: round.createdAt, ref: `review-round:${round.id}` })),
|
|
28
|
+
...input.integrations.map((attempt) => ({ at: attempt.createdAt, ref: `integration-attempt:${attempt.id}` }))
|
|
29
|
+
]
|
|
30
|
+
.filter(({ at }) => at >= firstGenerationAt)
|
|
31
|
+
.sort((left, right) => left.at.localeCompare(right.at) || left.ref.localeCompare(right.ref));
|
|
32
|
+
const firstProgressAt = progress[0]?.at;
|
|
33
|
+
const generationsBeforeFirstProgress = unique.filter((session) => (firstProgressAt === undefined || session.createdAt <= firstProgressAt)).length;
|
|
34
|
+
const generationRefs = unique.map((session) => (`${session.nativeSessionId}@${session.launchId ?? session.createdAt}`));
|
|
35
|
+
const progressRefs = progress.map(({ ref }) => ref);
|
|
36
|
+
const exhausted = firstProgressAt === undefined && generationsBeforeFirstProgress >= 2;
|
|
37
|
+
const fingerprint = createHash("sha256")
|
|
38
|
+
.update(JSON.stringify({ generationRefs, progressRefs }))
|
|
39
|
+
.digest("hex");
|
|
40
|
+
return Object.freeze({
|
|
41
|
+
exhausted,
|
|
42
|
+
generationsBeforeFirstProgress,
|
|
43
|
+
...(firstGenerationAt === undefined ? {} : { firstGenerationAt }),
|
|
44
|
+
...(firstProgressAt === undefined ? {} : { firstProgressAt }),
|
|
45
|
+
generationRefs,
|
|
46
|
+
progressRefs,
|
|
47
|
+
fingerprint,
|
|
48
|
+
reason: exhausted
|
|
49
|
+
? `${generationsBeforeFirstProgress} fresh Leader generations produced no first durable progress; stop before creating another generation and hand off to the Operator.`
|
|
50
|
+
: firstProgressAt !== undefined
|
|
51
|
+
? `First durable progress was recorded at ${firstProgressAt}.`
|
|
52
|
+
: `Fewer than two Leader generations exist before first durable progress.`
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -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,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { isDurableJobTerminal } from "../job/durableJob.js";
|
|
3
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
3
4
|
/**
|
|
4
5
|
* Canonical SHA-256 digest over the normalized actionable facts. Pure and
|
|
5
6
|
* deterministic: the same facts always produce the same digest regardless of
|
|
@@ -46,7 +47,8 @@ export function collectTaskActionability(store, taskId) {
|
|
|
46
47
|
throw new Error(`Task not found for actionability projection: ${taskId}.`);
|
|
47
48
|
}
|
|
48
49
|
const facts = [];
|
|
49
|
-
|
|
50
|
+
const events = store.listEvents?.(taskId) ?? [];
|
|
51
|
+
for (const run of operationalTaskRecords(store.listAgentRuns(taskId), events, "agent-run").filter((candidate) => candidate.status === "active")) {
|
|
50
52
|
facts.push({
|
|
51
53
|
key: `active-run:${run.id}`,
|
|
52
54
|
value: [
|
|
@@ -95,7 +97,7 @@ export function collectTaskActionability(store, taskId) {
|
|
|
95
97
|
value: `${request.status}|${request.updatedAt}`
|
|
96
98
|
});
|
|
97
99
|
}
|
|
98
|
-
for (const message of store.listMessages?.(taskId) ?? []) {
|
|
100
|
+
for (const message of operationalTaskRecords(store.listMessages?.(taskId) ?? [], events, "message")) {
|
|
99
101
|
if (message.wakePolicy !== "leader")
|
|
100
102
|
continue;
|
|
101
103
|
facts.push({
|
|
@@ -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
|
};
|
|
@@ -3,6 +3,7 @@ import { queueLeaderWakeup } from "./wakeupQueue.js";
|
|
|
3
3
|
import { wakeReason } from "./wakeReason.js";
|
|
4
4
|
import { projectTaskExecution } from "./taskExecutionProjection.js";
|
|
5
5
|
import { collectTaskActionability, computeActionabilityDigest, decideOrphanWake } from "./actionability.js";
|
|
6
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
6
7
|
/**
|
|
7
8
|
* Repairs an active Task that has no durable owner capable of advancing it.
|
|
8
9
|
* This is a low-frequency safety net; normal transitions enqueue their own
|
|
@@ -96,7 +97,7 @@ function admitOrphanWake(store, taskId) {
|
|
|
96
97
|
* so the admission check never suppresses while a Leader is still running.
|
|
97
98
|
*/
|
|
98
99
|
function findLastLeaderRun(store, taskId) {
|
|
99
|
-
const runs = store.listAgentRuns?.(taskId) ?? [];
|
|
100
|
+
const runs = operationalTaskRecords(store.listAgentRuns?.(taskId) ?? [], store.listEvents?.(taskId) ?? [], "agent-run");
|
|
100
101
|
let latest = null;
|
|
101
102
|
for (const run of runs) {
|
|
102
103
|
if (run.roleName !== "leader")
|
|
@@ -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
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
1
2
|
import { currentWorkItemExecutionGroup } from "../workItem/workItem.js";
|
|
2
3
|
import { mailboxBatches } from "../coordination/workMailbox.js";
|
|
3
4
|
import { summarizeExecutionGroup } from "../execution/executionGroup.js";
|
|
@@ -12,7 +13,8 @@ export function buildTaskExecutionProjection(store, taskId, taskOverride) {
|
|
|
12
13
|
if (task === null)
|
|
13
14
|
return null;
|
|
14
15
|
const roles = store.listRoles?.(taskId) ?? [];
|
|
15
|
-
const
|
|
16
|
+
const events = store.listEvents?.(taskId) ?? [];
|
|
17
|
+
const runs = operationalTaskRecords(store.listAgentRuns?.(taskId) ?? [], events, "agent-run");
|
|
16
18
|
const leaderMailbox = store.getWorkMailbox?.({
|
|
17
19
|
kind: "role",
|
|
18
20
|
taskId,
|
|
@@ -42,7 +44,7 @@ export function buildTaskExecutionProjection(store, taskId, taskOverride) {
|
|
|
42
44
|
...(store.listIntegrationAttempts === undefined
|
|
43
45
|
? {}
|
|
44
46
|
: { integrations: store.listIntegrationAttempts(taskId) }),
|
|
45
|
-
...(store.listEvents === undefined ? {} : { events
|
|
47
|
+
...(store.listEvents === undefined ? {} : { events }),
|
|
46
48
|
...(store.getTaskBrief === undefined ? {} : { brief: store.getTaskBrief(taskId) }),
|
|
47
49
|
pendingWakeup: store.getPendingWakeup?.(taskId) ?? null,
|
|
48
50
|
leaderMailbox,
|
|
@@ -60,7 +62,11 @@ export function buildTaskExecutionProjection(store, taskId, taskOverride) {
|
|
|
60
62
|
export function projectTaskExecutionFromFacts(facts) {
|
|
61
63
|
const executionGroups = facts.executionGroups
|
|
62
64
|
?? collectExecutionGroups(facts.workItems ?? [], facts.reviewRounds ?? []);
|
|
63
|
-
return projectTaskExecution({
|
|
65
|
+
return projectTaskExecution({
|
|
66
|
+
...facts,
|
|
67
|
+
runs: operationalTaskRecords(facts.runs, facts.events ?? [], "agent-run"),
|
|
68
|
+
executionGroups
|
|
69
|
+
});
|
|
64
70
|
}
|
|
65
71
|
/** Alias kept intentionally small for scheduler callers and external read models. */
|
|
66
72
|
export const deriveTaskExecutionProjection = projectTaskExecution;
|
|
@@ -471,7 +477,10 @@ function collectBlockers(workItems, reviewRounds, integrations, openInputs, task
|
|
|
471
477
|
summary: item.outcome ?? `WorkItem ${item.id} is ${item.status}.`
|
|
472
478
|
});
|
|
473
479
|
}
|
|
474
|
-
if (item.status === "pending" && (item.dependsOn ?? []).some((id) =>
|
|
480
|
+
if (item.status === "pending" && (item.dependsOn ?? []).some((id) => {
|
|
481
|
+
const status = byId.get(id)?.status;
|
|
482
|
+
return status !== "completed" && status !== "retired";
|
|
483
|
+
})) {
|
|
475
484
|
blockers.push({
|
|
476
485
|
kind: "work",
|
|
477
486
|
id: item.id,
|
|
@@ -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";
|
|
@@ -47,6 +48,7 @@ import { generateHomeIdentity, validateHomeIdentity } from "../repository/homeId
|
|
|
47
48
|
import { validateIntegrationQueueEntry } from "../integration/integrationQueueEntry.js";
|
|
48
49
|
import { validDurableJobTransition, validateDurableJob } from "../job/durableJob.js";
|
|
49
50
|
import { validateTaskWake } from "../scheduler/taskWake.js";
|
|
51
|
+
import { operationalTaskRecords, TASK_RECORD_RETIRED_EVENT } from "../task/taskRecordRetirement.js";
|
|
50
52
|
import { TASK_RECORD_ID_PREFIXES } from "../task/taskRecordReference.js";
|
|
51
53
|
import { managedWorkspaceKey } from "../worktree/managedWorkspace.js";
|
|
52
54
|
import { assertHomeWritable } from "./upgradeFence.js";
|
|
@@ -708,7 +710,13 @@ export class SqliteTaskStore {
|
|
|
708
710
|
return null;
|
|
709
711
|
// One indexed query (idx_agent_runs_role_status) covers both the active
|
|
710
712
|
// Runs the projection waits on and the Leader Runs the budget consumes.
|
|
711
|
-
const
|
|
713
|
+
const events = this.#sortById(this.#listPayload("events", "task_id = ? AND type IN (?, ?, ?)", [
|
|
714
|
+
taskId,
|
|
715
|
+
TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT,
|
|
716
|
+
TASK_RECORD_RETIRED_EVENT,
|
|
717
|
+
"review.completed"
|
|
718
|
+
]), (event) => event.id);
|
|
719
|
+
const runs = operationalTaskRecords(this.#sortById(this.#listPayload("agent_runs", "task_id = ? AND (status = 'active' OR role_name = 'leader')", [taskId]), (run) => run.id), events, "agent-run");
|
|
712
720
|
return {
|
|
713
721
|
task: {
|
|
714
722
|
id: task.id,
|
|
@@ -720,10 +728,17 @@ export class SqliteTaskStore {
|
|
|
720
728
|
changeSets: this.#sortById(this.#listPayload("change_sets", "task_id = ?", [taskId]), (changeSet) => changeSet.id),
|
|
721
729
|
integrations: this.#sortById(this.#listPayload("integration_attempts", "task_id = ?", [taskId]), (attempt) => attempt.id),
|
|
722
730
|
reviewRounds: this.#sortById(this.#listPayload("review_rounds", "task_id = ?", [taskId]), (round) => round.id),
|
|
731
|
+
taskFinalReviewContractEvents: events
|
|
732
|
+
.filter((event) => event.type === TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT),
|
|
723
733
|
reviewConfig: this.getReviewConfig(),
|
|
724
734
|
openInputRequests: this.#sortById(this.#listPayload("input_requests", "task_id = ? AND status = 'open'", [taskId]), (request) => request.id),
|
|
725
735
|
activeRuns: runs.filter((run) => run.status === "active"),
|
|
726
|
-
leaderRuns: runs.filter((run) => run.roleName === "leader")
|
|
736
|
+
leaderRuns: runs.filter((run) => run.roleName === "leader"),
|
|
737
|
+
reviewOutcomeEvidence: {
|
|
738
|
+
agentRuns: this.#sortById(this.#listPayload("agent_runs", "task_id = ?", [taskId]).filter((run) => run.purpose === "review"), (run) => run.id),
|
|
739
|
+
reviewFindings: this.listReviewFindings(taskId),
|
|
740
|
+
events: events.filter((event) => event.type === "review.completed")
|
|
741
|
+
}
|
|
727
742
|
};
|
|
728
743
|
}
|
|
729
744
|
readCompletionReadinessFacts(taskId) {
|
|
@@ -732,7 +747,7 @@ export class SqliteTaskStore {
|
|
|
732
747
|
return null;
|
|
733
748
|
return {
|
|
734
749
|
...base,
|
|
735
|
-
agentRuns: this.listAgentRuns(taskId),
|
|
750
|
+
agentRuns: operationalTaskRecords(this.listAgentRuns(taskId), this.listEvents(taskId), "agent-run"),
|
|
736
751
|
roleSessionSets: this.listRoleSessionSets(taskId),
|
|
737
752
|
managedWorkspaces: this.#sortById(this.#listPayload("managed_workspaces", "task_id = ?", [taskId]), (workspace) => managedWorkspaceKey(workspace.owner)),
|
|
738
753
|
durableJobs: this.#sortById(this.#listPayload("durable_jobs", "task_id = ?", [taskId]), (job) => job.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";
|
|
@@ -33,6 +34,7 @@ import { CURRENT_LEADER_FAILURE_SCHEMA_VERSION } from "../scheduler/leaderFailur
|
|
|
33
34
|
import { CURRENT_OPERATOR_NOTIFICATION_SCHEMA_VERSION } from "../scheduler/operatorNotification.js";
|
|
34
35
|
import { CURRENT_TASK_WAKE_SCHEMA_VERSION, validateTaskWake } from "../scheduler/taskWake.js";
|
|
35
36
|
import { validateTask } from "../task/task.js";
|
|
37
|
+
import { operationalTaskRecords } from "../task/taskRecordRetirement.js";
|
|
36
38
|
import { TASK_RECORD_ID_PREFIXES, validateTaskRecordReference } from "../task/taskRecordReference.js";
|
|
37
39
|
import { workItemExecutionGroupById, validateWorkItem } from "../workItem/workItem.js";
|
|
38
40
|
import { isExecutionGroupTransition, validateExecutionGroup } from "../execution/executionGroup.js";
|
|
@@ -408,7 +410,8 @@ export class FileTaskStore {
|
|
|
408
410
|
const aggregate = this.#state().tasks[taskId];
|
|
409
411
|
if (aggregate === undefined)
|
|
410
412
|
return null;
|
|
411
|
-
const
|
|
413
|
+
const events = values(aggregate.events, "id");
|
|
414
|
+
const agentRuns = operationalTaskRecords(values(aggregate.agentRuns, "id"), events, "agent-run");
|
|
412
415
|
return {
|
|
413
416
|
task: {
|
|
414
417
|
id: aggregate.task.id,
|
|
@@ -420,11 +423,19 @@ export class FileTaskStore {
|
|
|
420
423
|
changeSets: values(aggregate.changeSets, "id"),
|
|
421
424
|
integrations: values(aggregate.integrationAttempts, "id"),
|
|
422
425
|
reviewRounds: values(aggregate.reviewRounds, "id"),
|
|
426
|
+
taskFinalReviewContractEvents: events
|
|
427
|
+
.filter((event) => event.type === TASK_FINAL_REVIEW_CONTRACT_REBOUND_EVENT),
|
|
423
428
|
reviewConfig: this.getReviewConfig(),
|
|
424
429
|
openInputRequests: values(aggregate.inputRequests, "id")
|
|
425
430
|
.filter((request) => request.status === "open"),
|
|
426
431
|
activeRuns: agentRuns.filter((run) => run.status === "active"),
|
|
427
|
-
leaderRuns: agentRuns.filter((run) => run.roleName === "leader")
|
|
432
|
+
leaderRuns: agentRuns.filter((run) => run.roleName === "leader"),
|
|
433
|
+
reviewOutcomeEvidence: {
|
|
434
|
+
agentRuns: agentRuns.filter((run) => run.purpose === "review"),
|
|
435
|
+
// The rollback file backend has no finding-ledger records.
|
|
436
|
+
reviewFindings: [],
|
|
437
|
+
events: events.filter((event) => (event.type === "review.completed"))
|
|
438
|
+
}
|
|
428
439
|
};
|
|
429
440
|
}
|
|
430
441
|
readCompletionReadinessFacts(taskId) {
|
|
@@ -442,7 +453,7 @@ export class FileTaskStore {
|
|
|
442
453
|
}
|
|
443
454
|
return {
|
|
444
455
|
...base,
|
|
445
|
-
agentRuns: this.listAgentRuns(taskId),
|
|
456
|
+
agentRuns: operationalTaskRecords(this.listAgentRuns(taskId), values(aggregate.events, "id"), "agent-run"),
|
|
446
457
|
roleSessionSets: this.listRoleSessionSets(taskId),
|
|
447
458
|
managedWorkspaces: values(aggregate.managedWorkspaces, (workspace) => managedWorkspaceKey(workspace.owner)),
|
|
448
459
|
durableJobs: values(aggregate.durableJobs, "id"),
|