@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.
Files changed (68) hide show
  1. package/ARCHITECTURE.md +40 -22
  2. package/README.md +61 -9
  3. package/dist/cli/commandCatalog.js +31 -14
  4. package/dist/cli/operatorWizard.js +10 -20
  5. package/dist/cli.js +187 -22
  6. package/dist/commands/executionAuditCommands.js +30 -0
  7. package/dist/commands/operatorCommands.js +42 -1
  8. package/dist/commands/taskCommands.js +648 -74
  9. package/dist/commands/taskCompletionGate.js +166 -2
  10. package/dist/commands/taskContextCommand.js +6 -1
  11. package/dist/commands/taskInputCommands.js +48 -10
  12. package/dist/commands/taskNextActionCommand.js +36 -3
  13. package/dist/context/runContextPack.js +19 -4
  14. package/dist/context/sessionBootstrapManifest.js +83 -2
  15. package/dist/controller/clientRuntime.js +7 -7
  16. package/dist/controller/controller.js +16 -8
  17. package/dist/controller/fileSchedulerStoreAdapter.js +64 -5
  18. package/dist/controller/handoverCandidate.js +10 -3
  19. package/dist/controller/sessionNotify.js +4 -22
  20. package/dist/executor/agentAdapter.js +2 -2
  21. package/dist/executor/agentExecutor.js +25 -5
  22. package/dist/executor/fileRoleLaunchPlanner.js +16 -11
  23. package/dist/integration/gitIntegrationService.js +50 -2
  24. package/dist/integration/integrationCheckEvidenceReuse.js +53 -0
  25. package/dist/observability/executionAudit.js +47 -1
  26. package/dist/observability/faultClassification.js +6 -4
  27. package/dist/observability/orchestrationMetrics.js +196 -0
  28. package/dist/operator/operatorSessionHistory.js +36 -0
  29. package/dist/release/releaseHandover.js +7 -5
  30. package/dist/release/runtimeRelease.js +15 -0
  31. package/dist/repository/gitWorkspace.js +7 -4
  32. package/dist/repository/taskBaseFreshness.js +4 -2
  33. package/dist/repository/taskWorkspaceCoordinator.js +13 -10
  34. package/dist/review/deltaRecheck.js +3 -2
  35. package/dist/review/reviewFindingLedger.js +5 -4
  36. package/dist/review/reviewOutcomeClassifier.js +252 -54
  37. package/dist/review/taskFinalReviewContractEvent.js +1 -0
  38. package/dist/review/taskFinalReviewContractRebind.js +350 -0
  39. package/dist/run/agentRun.js +2 -2
  40. package/dist/run/runIdentity.js +10 -70
  41. package/dist/runtime/agentHost.js +3 -4
  42. package/dist/runtime/codexAppServerRuntime.js +6 -0
  43. package/dist/runtime/firstProgressStopLoss.js +52 -0
  44. package/dist/runtime/launchBroker.js +10 -2
  45. package/dist/runtime/runtimeDeadlines.js +14 -0
  46. package/dist/runtime/sessionTitle.js +24 -12
  47. package/dist/runtime/structuredProviderHost.js +7 -1
  48. package/dist/runtime/tmuxAdapters.js +10 -3
  49. package/dist/scheduler/activeRoleRunDelivery.js +20 -18
  50. package/dist/scheduler/leaderWakeupProcessor.js +33 -2
  51. package/dist/scheduler/wakeReason.js +2 -0
  52. package/dist/storage/sqliteStore.js +10 -1
  53. package/dist/storage/taskStore.js +12 -1
  54. package/dist/task/completionReadiness.js +91 -19
  55. package/dist/task/deliveryGuard.js +3 -1
  56. package/dist/task/nextAction.js +146 -51
  57. package/dist/task/publicationReference.js +1 -0
  58. package/dist/task/repairWave.js +14 -1
  59. package/dist/task/task.js +10 -0
  60. package/dist/web/webSnapshot.js +7 -1
  61. package/dist/workItem/workItem.js +12 -0
  62. package/dist/workspace/workItemChangeSetManager.js +2 -1
  63. package/i18n/README.zh-CN.md +28 -8
  64. package/package.json +1 -1
  65. package/skills/yui-leader/SKILL.md +79 -32
  66. package/skills/yui-operator/SKILL.md +51 -10
  67. package/skills/yui-reviewer/SKILL.md +23 -0
  68. package/skills/yui-runtime/SKILL.md +7 -2
@@ -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 = validateRunAssignment(Object.freeze({
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,
@@ -1,82 +1,22 @@
1
1
  import { MAX_SESSION_TITLE_LENGTH } from "../runtime/sessionTitle.js";
2
- const TITLED_RUN_SEPARATOR = " · Run ";
3
- /** Adds the causal token consumed by the native Turn-complete Hook. */
4
- export function markYuiRunInput(input, runId, title) {
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("Run input is required.");
22
- return `${requireRunTitle(title)}${TITLED_RUN_SEPARATOR}${id}\n\n${text}`;
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
- || !title.startsWith("Yui · ")
68
- || /[\r\n\0]/u.test(title)) {
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 ?? HOST_READY_TIMEOUT_MS);
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
- }, HOST_CONTROL_TIMEOUT_MS);
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 > 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,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
  };
@@ -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
  };
@@ -22,6 +22,8 @@ export const WAKE_REASON_KINDS = Object.freeze([
22
22
  "leader-run-failed",
23
23
  "role-run-failed",
24
24
  "job-finished",
25
+ "published-tree-authorized",
26
+ "review-contract-rebound",
25
27
  "force-wake"
26
28
  ]);
27
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";
@@ -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"),