@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.
Files changed (73) hide show
  1. package/ARCHITECTURE.md +40 -22
  2. package/README.md +66 -19
  3. package/dist/cli/commandCatalog.js +43 -14
  4. package/dist/cli/operatorWizard.js +10 -20
  5. package/dist/cli/updatePorts.js +6 -0
  6. package/dist/cli.js +252 -37
  7. package/dist/commands/executionAuditCommands.js +30 -0
  8. package/dist/commands/globalRoleCommands.js +8 -4
  9. package/dist/commands/operatorCommands.js +42 -1
  10. package/dist/commands/taskCommands.js +527 -147
  11. package/dist/commands/taskCompletionGate.js +36 -24
  12. package/dist/commands/taskContextCommand.js +11 -4
  13. package/dist/commands/taskInputCommands.js +48 -10
  14. package/dist/commands/taskNextActionCommand.js +38 -3
  15. package/dist/commands/taskOverviewCommand.js +2 -1
  16. package/dist/commands/taskRoleRuntimeStatus.js +2 -1
  17. package/dist/context/runContextPack.js +9 -5
  18. package/dist/context/sessionBootstrapManifest.js +158 -11
  19. package/dist/context/wakeNotification.js +5 -3
  20. package/dist/controller/clientRuntime.js +15 -15
  21. package/dist/controller/controller.js +16 -8
  22. package/dist/controller/fileSchedulerStoreAdapter.js +67 -7
  23. package/dist/controller/handoverCandidate.js +10 -3
  24. package/dist/controller/sessionNotify.js +4 -22
  25. package/dist/executor/agentAdapter.js +2 -2
  26. package/dist/executor/agentExecutor.js +29 -9
  27. package/dist/executor/fileRoleLaunchPlanner.js +37 -45
  28. package/dist/integration/gitIntegrationService.js +50 -2
  29. package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
  30. package/dist/observability/executionAudit.js +47 -1
  31. package/dist/observability/faultClassification.js +6 -4
  32. package/dist/observability/orchestrationMetrics.js +196 -0
  33. package/dist/operator/operatorSessionHistory.js +36 -0
  34. package/dist/release/releaseHandover.js +7 -5
  35. package/dist/release/runtimeRelease.js +15 -0
  36. package/dist/repository/taskWorkspaceCoordinator.js +13 -10
  37. package/dist/review/deltaRecheck.js +3 -2
  38. package/dist/review/reviewFindingLedger.js +5 -4
  39. package/dist/review/reviewOutcomeClassifier.js +263 -54
  40. package/dist/review/taskFinalReviewContractEvent.js +1 -0
  41. package/dist/review/taskFinalReviewContractRebind.js +367 -0
  42. package/dist/run/runIdentity.js +10 -70
  43. package/dist/runtime/agentHost.js +3 -4
  44. package/dist/runtime/codexAppServerRuntime.js +6 -0
  45. package/dist/runtime/exactControlPlane.js +47 -37
  46. package/dist/runtime/firstProgressStopLoss.js +54 -0
  47. package/dist/runtime/launchBroker.js +10 -2
  48. package/dist/runtime/runtimeDeadlines.js +14 -0
  49. package/dist/runtime/sessionTitle.js +24 -12
  50. package/dist/runtime/structuredProviderHost.js +7 -1
  51. package/dist/runtime/tmuxAdapters.js +10 -3
  52. package/dist/scheduler/actionability.js +4 -2
  53. package/dist/scheduler/activeRoleRunDelivery.js +20 -18
  54. package/dist/scheduler/activeTaskProgress.js +2 -1
  55. package/dist/scheduler/leaderWakeupProcessor.js +33 -2
  56. package/dist/scheduler/taskExecutionProjection.js +13 -4
  57. package/dist/scheduler/wakeReason.js +1 -0
  58. package/dist/storage/sqliteStore.js +18 -3
  59. package/dist/storage/taskStore.js +14 -3
  60. package/dist/task/completionReadiness.js +48 -19
  61. package/dist/task/deliveryGuard.js +3 -1
  62. package/dist/task/nextAction.js +153 -55
  63. package/dist/task/repairWave.js +14 -1
  64. package/dist/task/task.js +10 -0
  65. package/dist/task/taskRecordRetirement.js +72 -0
  66. package/dist/web/webSnapshot.js +7 -1
  67. package/dist/workItem/workItem.js +6 -4
  68. package/i18n/README.zh-CN.md +48 -9
  69. package/package.json +1 -1
  70. package/skills/yui-leader/SKILL.md +73 -31
  71. package/skills/yui-operator/SKILL.md +58 -10
  72. package/skills/yui-reviewer/SKILL.md +23 -0
  73. 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 > TICKET_TTL_MS) {
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 = 160;
2
+ export const MAX_SESSION_TITLE_LENGTH = 80;
3
+ const TASK_TITLE_MAX_LENGTH = 20;
3
4
  export function taskRoleSessionTitle(task, roleName) {
4
- return sessionTitle(["Yui", task.id, roleLabel(roleName), task.title]);
5
- }
6
- function sessionTitle(segments) {
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
- const head = normalized.slice(0, -1).join(TITLE_SEPARATOR);
12
- const tail = normalized.at(-1);
13
- const tailLength = MAX_SESSION_TITLE_LENGTH - head.length - TITLE_SEPARATOR.length - 1;
14
- if (tailLength < 1)
15
- return truncate(full, MAX_SESSION_TITLE_LENGTH);
16
- return `${head}${TITLE_SEPARATOR}${truncate(tail, tailLength)}…`;
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 may still be native TUIs. A managed Task Run
227
- // has no terminal-write fallback and must expose the Agent Host contract.
228
- if (yuiHome === undefined || childLifecycle === undefined) {
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
- for (const run of store.listAgentRuns(taskId).filter((candidate) => candidate.status === "active")) {
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 { markYuiRunInput } from "../run/runIdentity.js";
5
- import { taskRoleSessionTitle } from "../runtime/sessionTitle.js";
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 outcome = await delivery.sendOnce({
302
- delivery: ready,
303
- receiptId,
304
- text: run.controlRequest?.state === "dispatching"
305
- ? serializeWorkflowOutcomeRequestEnvelope({
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
- request: run.controlRequest
313
+ retry: run.providerRetry
310
314
  })
311
- : run.providerRetry?.state === "dispatching"
312
- ? serializeProviderRetryEnvelope({
313
- taskId: task.id,
314
- runId: run.id,
315
- roleName: role.name,
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 markYuiRunInput([
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"), run.id, taskRoleSessionTitle(task, role.name));
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 mode = hasNativeSession(existingSession) && compatibleSession ? "resume" : "new";
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 runs = store.listAgentRuns?.(taskId) ?? [];
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: store.listEvents(taskId) }),
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({ ...facts, executionGroups });
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) => byId.get(id)?.status !== "completed")) {
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,
@@ -23,6 +23,7 @@ export const WAKE_REASON_KINDS = Object.freeze([
23
23
  "role-run-failed",
24
24
  "job-finished",
25
25
  "published-tree-authorized",
26
+ "review-contract-rebound",
26
27
  "force-wake"
27
28
  ]);
28
29
  const IMMEDIATE_KINDS = new Set([
@@ -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 runs = this.#sortById(this.#listPayload("agent_runs", "task_id = ? AND (status = 'active' OR role_name = 'leader')", [taskId]), (run) => run.id);
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 agentRuns = values(aggregate.agentRuns, "id");
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"),