@zq-silk/yui 0.12.3 → 0.13.0

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 (32) hide show
  1. package/dist/cli/commandCatalog.js +2 -2
  2. package/dist/cli/updatePorts.js +18 -2
  3. package/dist/cli.js +3 -6
  4. package/dist/commands/durableJobCommands.js +6 -18
  5. package/dist/commands/executionAuditCommands.js +1 -1
  6. package/dist/commands/taskActor.js +35 -61
  7. package/dist/commands/taskCommands.js +260 -200
  8. package/dist/commands/taskIntegrationCommands.js +33 -24
  9. package/dist/commands/taskIntegrationQueueCommands.js +16 -18
  10. package/dist/commands/taskPublicationCommands.js +0 -4
  11. package/dist/commands/taskWorkspaceCommands.js +1 -1
  12. package/dist/context/runContextPack.js +15 -0
  13. package/dist/controller/fileSchedulerStoreAdapter.js +17 -1
  14. package/dist/controller/jobClient.js +5 -8
  15. package/dist/controller/jobControl.js +59 -105
  16. package/dist/executor/workspacePreflightClassification.js +6 -5
  17. package/dist/integration/gitIntegrationService.js +3 -3
  18. package/dist/integration/integrationAttempt.js +13 -9
  19. package/dist/integration/integrationQueueService.js +7 -7
  20. package/dist/lifecycle/exactRunTerminalization.js +146 -6
  21. package/dist/milestone/milestone.js +9 -3
  22. package/dist/review/deltaRecheck.js +1 -1
  23. package/dist/review/reviewFinding.js +3 -3
  24. package/dist/review/reviewRound.js +11 -8
  25. package/dist/run/rejectedYieldAttempt.js +221 -0
  26. package/dist/runtime/tmuxAdapters.js +12 -3
  27. package/dist/scheduler/activeRoleRunDelivery.js +4 -0
  28. package/dist/storage/migration/productionRegistry.js +9 -0
  29. package/dist/storage/taskStore.js +9 -8
  30. package/package.json +1 -1
  31. package/skills/yui-leader/SKILL.md +4 -0
  32. package/skills/yui-operator/SKILL.md +19 -13
@@ -9,7 +9,7 @@ import { finishReviewRound, updateReviewExecutionGroup } from "../review/reviewR
9
9
  import { reconcileReviewFindingsAfterReview } from "../review/reviewFindingLedger.js";
10
10
  import { agentRunDeliveryReceiptId, failAgentRun, withYieldReceipt, yieldAgentRun } from "../run/agentRun.js";
11
11
  import { createYieldReceipt } from "../run/yieldReceipt.js";
12
- import { recordExecutionLaneResult } from "../execution/executionGroup.js";
12
+ import { recordExecutionLaneResult, resolveExecutionGroup } from "../execution/executionGroup.js";
13
13
  import { isRuntimeLaunchReservation, runtimeLifecycleTarget } from "../runtime/lifecycleReservation.js";
14
14
  import { runOwnsBlockingProviderContinuation } from "../runtime/runtimeContinuationProjection.js";
15
15
  import { runHasActiveRuntimeOperations } from "../runtime/runtimeObservation.js";
@@ -145,7 +145,7 @@ export function terminalizeExactRunReviewRound(store, input, now) {
145
145
  return validation;
146
146
  }
147
147
  const reviewRound = validation.round;
148
- const groupedRound = input.run.executionGroupId !== undefined
148
+ let groupedRound = input.run.executionGroupId !== undefined
149
149
  && input.run.executionLaneId !== undefined
150
150
  && reviewRound.executionGroup !== undefined
151
151
  ? updateReviewExecutionGroup(reviewRound, recordExecutionLaneResult(reviewRound.executionGroup, input.run.executionLaneId, {
@@ -174,10 +174,25 @@ export function terminalizeExactRunReviewRound(store, input, now) {
174
174
  : { gitSnapshot: input.reviewResult.gitSnapshot })
175
175
  }, input.outcome.status === "yielded" ? "completed" : "failed", now))
176
176
  : reviewRound;
177
+ if (input.settleFailedExecutionGroup === true
178
+ && input.outcome.status === "failed"
179
+ && groupedRound.executionGroup !== undefined
180
+ && groupedRound.executionGroup.resolution === undefined
181
+ && groupedRound.executionGroup.lanes.every((lane) => (lane.status === "yielded"
182
+ || lane.status === "completed"
183
+ || lane.status === "failed"
184
+ || lane.status === "skipped"))) {
185
+ groupedRound = updateReviewExecutionGroup(groupedRound, resolveExecutionGroup(groupedRound.executionGroup, {
186
+ decision: "blocked",
187
+ summary: input.outcome.summary
188
+ }, now));
189
+ }
177
190
  const groupedMultiLane = groupedRound.executionGroup !== undefined
178
191
  && (groupedRound.executionGroup.lanes.length > 1
179
192
  || groupedRound.executionGroup.strategy.mode === "adaptive");
180
- if (groupedMultiLane && groupedRound.executionGroup !== undefined) {
193
+ if (groupedMultiLane
194
+ && groupedRound.executionGroup !== undefined
195
+ && groupedRound.executionGroup.resolution === undefined) {
181
196
  // A panel Lane only contributes evidence. The Leader must see every
182
197
  // terminal Lane and explicitly resolve the Group before this ReviewRound
183
198
  // can become terminal.
@@ -193,6 +208,108 @@ export function terminalizeExactRunReviewRound(store, input, now) {
193
208
  }
194
209
  return { disposition: "applied", round: terminal };
195
210
  }
211
+ /**
212
+ * Retire one stranded active Run only after its exact Provider Turn is
213
+ * terminal and every durable execution fence is quiet. The caller owns the
214
+ * surrounding aggregate transaction so the Run, ReviewRound/Lane, mailbox,
215
+ * Session, and append-only retirement record commit together.
216
+ */
217
+ export function retireExactActiveAgentRun(store, input, now) {
218
+ const current = store.getAgentRun(input.taskId, input.runId);
219
+ const stateChanged = (reason) => ({
220
+ disposition: "state-changed",
221
+ run: current,
222
+ ...(current === null ? {} : { progressAt: latestRunDurableProgressAt(store, input.taskId, input.roleName, input.runId)?.progressAt }),
223
+ reason
224
+ });
225
+ const task = store.getTask(input.taskId);
226
+ if (task === null)
227
+ return stateChanged("task-missing");
228
+ if (task.status !== "active")
229
+ return stateChanged("task-terminal");
230
+ if (current === null)
231
+ return stateChanged("run-missing");
232
+ if (current.status !== "active")
233
+ return stateChanged("run-terminal");
234
+ if (current.taskId !== input.taskId || current.roleName !== input.roleName) {
235
+ return stateChanged("run-owner-mismatch");
236
+ }
237
+ if (current.effective.agentId !== input.agentId
238
+ || current.effective.adapterId !== input.adapterId) {
239
+ return stateChanged("run-launch-identity-mismatch");
240
+ }
241
+ const active = current.executionGroupId !== undefined && current.executionLaneId !== undefined
242
+ ? store.getActiveExecutionLaneRun(input.taskId, current.executionGroupId, current.executionLaneId)
243
+ : store.getActiveAgentRun(input.taskId, input.roleName);
244
+ if (active?.id !== current.id)
245
+ return stateChanged("active-run-mismatch");
246
+ const progress = latestRunDurableProgressAt(store, input.taskId, input.roleName, input.runId);
247
+ if (progress === null)
248
+ return stateChanged("progress-unavailable");
249
+ if (progress.progressAt !== input.expectedProgressAt) {
250
+ return { ...stateChanged("progress-fence-mismatch"), progressAt: progress.progressAt };
251
+ }
252
+ if (!matchesRecoverySessionFence(store, {
253
+ ...input,
254
+ action: "terminate",
255
+ providerAcceptance: current.deliveredAt === undefined ? "rejected" : "accepted",
256
+ now
257
+ }))
258
+ return stateChanged("session-or-launch-fence-mismatch");
259
+ const blocker = exactRecoveryExecutionBlocker(store, current);
260
+ if (blocker !== null) {
261
+ return {
262
+ disposition: "blocked",
263
+ run: current,
264
+ progressAt: progress.progressAt,
265
+ reason: blocker
266
+ };
267
+ }
268
+ const sessions = store.getTaskRoleSessionSet(input.taskId, input.roleName);
269
+ const session = sessions?.sessions[input.agentId];
270
+ const providerBinding = sessions?.providerBinding;
271
+ const providerTurn = providerBinding?.turn;
272
+ const providerSettled = providerBinding?.runId === current.id
273
+ && (providerTurn?.status === "completed"
274
+ || providerTurn?.status === "failed"
275
+ || providerTurn?.status === "cancelled"
276
+ || providerTurn?.status === "rejected");
277
+ if (session?.status !== "stopped" && session?.status !== "broken" && !providerSettled) {
278
+ return {
279
+ disposition: "blocked",
280
+ run: current,
281
+ progressAt: progress.progressAt,
282
+ reason: "runtime-not-terminal"
283
+ };
284
+ }
285
+ const terminal = terminalizeExactTaskRun(store, {
286
+ taskId: input.taskId,
287
+ roleName: input.roleName,
288
+ agentId: input.agentId,
289
+ runId: input.runId,
290
+ receiptId: agentRunDeliveryReceiptId(current),
291
+ ...(input.nativeSessionId === undefined ? {} : { nativeSessionId: input.nativeSessionId }),
292
+ ...(input.launchId === undefined ? {} : { launchId: input.launchId }),
293
+ settleFailedExecutionGroup: true,
294
+ outcome: { status: "failed", summary: input.reason }
295
+ }, now);
296
+ if (terminal.disposition !== "applied" || terminal.run === null) {
297
+ return stateChanged(terminal.reason ?? "terminalization-fence-mismatch");
298
+ }
299
+ if (terminal.run.purpose === "execution" && terminal.run.workItemId !== undefined) {
300
+ const item = store.getWorkItem(input.taskId, terminal.run.workItemId);
301
+ if (item !== null
302
+ && !["completed", "failed", "retired"].includes(item.status)
303
+ && !workItemOwnsUnresolvedExecutionLane(item, terminal.run.executionGroupId, terminal.run.executionLaneId)) {
304
+ store.saveWorkItem(input.taskId, updateWorkItemStatus(item, "failed", now, input.reason));
305
+ }
306
+ }
307
+ return {
308
+ disposition: "applied",
309
+ run: terminal.run,
310
+ progressAt: progress.progressAt
311
+ };
312
+ }
196
313
  /**
197
314
  * Applies one exact application-level terminal fact inside the caller's
198
315
  * FileTaskStore transaction. All caller-owned outcome records can therefore
@@ -279,7 +396,10 @@ export function terminalizeExactTaskRun(store, input, now) {
279
396
  taskId: input.taskId,
280
397
  run,
281
398
  outcome: input.outcome,
282
- reviewResult: input.reviewResult
399
+ reviewResult: input.reviewResult,
400
+ ...(input.settleFailedExecutionGroup === undefined
401
+ ? {}
402
+ : { settleFailedExecutionGroup: input.settleFailedExecutionGroup })
283
403
  }, now);
284
404
  if (reviewRoundTerminalization.disposition !== "applied") {
285
405
  return obsolete(run, reviewRoundTerminalization.reason ?? "review-round-mismatch");
@@ -313,7 +433,7 @@ export function terminalizeExactTaskRun(store, input, now) {
313
433
  ? undefined
314
434
  : workItemExecutionGroupById(item, run.executionGroupId);
315
435
  if (item !== null && group !== undefined) {
316
- const grouped = recordExecutionLaneResult(group, run.executionLaneId, {
436
+ let grouped = recordExecutionLaneResult(group, run.executionLaneId, {
317
437
  summary: input.outcome.summary,
318
438
  ...(input.reviewResult?.report === undefined ? {} : { report: input.reviewResult.report }),
319
439
  ...(input.reviewResult?.checks === undefined ? {} : { checks: input.reviewResult.checks }),
@@ -322,6 +442,18 @@ export function terminalizeExactTaskRun(store, input, now) {
322
442
  ...(input.reviewResult?.evidenceCommit === undefined ? {} : { evidenceCommit: input.reviewResult.evidenceCommit }),
323
443
  ...(input.reviewResult?.gitSnapshot === undefined ? {} : { gitSnapshot: input.reviewResult.gitSnapshot })
324
444
  }, input.outcome.status === "yielded" ? "completed" : "failed", now);
445
+ if (input.settleFailedExecutionGroup === true
446
+ && input.outcome.status === "failed"
447
+ && grouped.resolution === undefined
448
+ && grouped.lanes.every((lane) => (lane.status === "yielded"
449
+ || lane.status === "completed"
450
+ || lane.status === "failed"
451
+ || lane.status === "skipped"))) {
452
+ grouped = resolveExecutionGroup(grouped, {
453
+ decision: "blocked",
454
+ summary: input.outcome.summary
455
+ }, now);
456
+ }
325
457
  store.saveWorkItem(input.taskId, updateWorkItemExecutionGroup(item, grouped, now));
326
458
  }
327
459
  }
@@ -643,6 +775,12 @@ function matchesSessionFence(sessions, input) {
643
775
  && inFlight.receiptId === input.receiptId);
644
776
  }
645
777
  function matchesLaunchFence(store, sessions, input) {
778
+ // A resumed Run shares the native Provider Session whose process environment
779
+ // was created for an earlier Run. Once that exact native Session has passed
780
+ // matchesSessionFence, its immutable per-launch environment is not a second
781
+ // Run identity. Opaque hosts still require the launch fence below.
782
+ if (input.nativeSessionId !== undefined)
783
+ return true;
646
784
  if (input.launchId === undefined)
647
785
  return true;
648
786
  const session = sessions?.sessions[input.agentId];
@@ -664,7 +802,9 @@ function settleLaunchReservation(store, sessions, input) {
664
802
  const mailbox = store.getWorkMailbox(target);
665
803
  const reservation = mailbox?.processing;
666
804
  const session = sessions?.sessions[input.agentId];
667
- const launchId = input.launchId ?? session?.launchId;
805
+ const launchId = input.nativeSessionId === undefined
806
+ ? input.launchId ?? session?.launchId
807
+ : session?.launchId;
668
808
  if (launchId === undefined || !isRuntimeLaunchReservation(reservation, launchId))
669
809
  return;
670
810
  const settled = completeProcessing(mailbox, reservation.batchId);
@@ -1,15 +1,21 @@
1
1
  import { validateTaskRecordReference } from "../task/taskRecordReference.js";
2
- export function createMilestone(id, taskId, title, summary, now) {
2
+ export function createMilestone(id, taskId, title, summary, createdBy, now) {
3
3
  return {
4
- schemaVersion: 1,
4
+ schemaVersion: 2,
5
5
  id: validateTaskRecordReference({ taskId, localId: id }, "milestone").localId,
6
6
  taskId: requireSafeIdentity(taskId, "Task id"),
7
7
  title: requireText(title, "Milestone title"),
8
8
  summary: requireText(summary, "Milestone summary"),
9
- createdBy: "leader",
9
+ createdBy: requireTaskControlActor(createdBy),
10
10
  createdAt: now.toISOString()
11
11
  };
12
12
  }
13
+ function requireTaskControlActor(value) {
14
+ if (value !== "user" && value !== "operator" && value !== "leader") {
15
+ throw new Error(`Milestone createdBy is invalid: ${String(value)}.`);
16
+ }
17
+ return value;
18
+ }
13
19
  function requireSafeIdentity(value, label) {
14
20
  const normalized = requireText(value, label);
15
21
  if (["__proto__", "prototype", "constructor", ".", ".."].includes(normalized)
@@ -127,7 +127,7 @@ export function buildDeltaRecheckDispatchContext(input) {
127
127
  "- finding: the diff introduces a material problem; report it as a finding.",
128
128
  "- requires-full-review: you cannot prove equivalence (uncertainty, cross-scope,",
129
129
  " semantic change, evidence doubt). This is the safe default.",
130
- "Yui verified only the technical delta boundary; the Leader selected this mode.",
130
+ "Yui verified only the technical delta boundary; a Task-control Agent selected this mode.",
131
131
  `Previous accepted ReviewRound: ${previousRound.id}@${record.previousBaseCommit}`,
132
132
  `Previous acceptance summary: ${compact(previousRound.summary ?? previousRound.report ?? "")}`,
133
133
  ...(round.taskCandidate?.projects.map((project) => {
@@ -6,8 +6,8 @@ export const BLOCKING_FINDING_DISPOSITIONS = [
6
6
  "open",
7
7
  "fixed-pending-review"
8
8
  ];
9
- /** Dispositions the Leader may set explicitly. */
10
- export const LEADER_FINDING_DISPOSITIONS = [
9
+ /** Dispositions a Task-control Agent may set explicitly. */
10
+ export const TASK_CONTROL_FINDING_DISPOSITIONS = [
11
11
  "fixed-pending-review",
12
12
  "verified-fixed",
13
13
  "accepted-risk",
@@ -86,7 +86,7 @@ export function createReviewFinding(id, taskId, input, now) {
86
86
  */
87
87
  export function disposeReviewFinding(finding, input) {
88
88
  validateReviewFinding(finding);
89
- if (!LEADER_FINDING_DISPOSITIONS.includes(input.disposition)) {
89
+ if (!TASK_CONTROL_FINDING_DISPOSITIONS.includes(input.disposition)) {
90
90
  throw new Error(`ReviewFinding disposition is not a Leader decision: ${input.disposition}.`);
91
91
  }
92
92
  if (finding.disposition !== "open" && finding.disposition !== "fixed-pending-review") {
@@ -5,7 +5,7 @@ import { validateManagedWorkspace } from "../worktree/managedWorkspace.js";
5
5
  import { assertExecutionGroupTransition, resetReviewExecutionLane, validateExecutionGroup } from "../execution/executionGroup.js";
6
6
  export function createReviewRound(id, taskId, workItemId, candidateId, reviewerRoleName, requestedBy, reviewBaseCommit, now, executionGroup) {
7
7
  return validateReviewRound({
8
- schemaVersion: 5,
8
+ schemaVersion: 6,
9
9
  id: requireIdentity(id, "ReviewRound id"),
10
10
  taskId: requireIdentity(taskId, "Task id"),
11
11
  workItemId: requireIdentity(workItemId, "Work Item id"),
@@ -21,7 +21,7 @@ export function createReviewRound(id, taskId, workItemId, candidateId, reviewerR
21
21
  export function createTaskReviewRound(id, taskId, reviewerRoleName, requestedBy, taskCandidate, now, taskFinalReviewContract, executionGroup) {
22
22
  const candidate = validateTaskReviewCandidate(taskCandidate);
23
23
  return validateReviewRound({
24
- schemaVersion: 5,
24
+ schemaVersion: 6,
25
25
  id: requireIdentity(id, "ReviewRound id"),
26
26
  taskId: requireIdentity(taskId, "Task id"),
27
27
  reviewerRoleName: requireIdentity(reviewerRoleName, "Reviewer Role"),
@@ -47,7 +47,7 @@ export function createTaskReviewRound(id, taskId, reviewerRoleName, requestedBy,
47
47
  export function createTaskDeltaReviewRound(id, taskId, reviewerRoleName, requestedBy, taskCandidate, deltaRecheck, now, taskFinalReviewContract, executionGroup) {
48
48
  const candidate = validateTaskReviewCandidate(taskCandidate);
49
49
  return validateReviewRound({
50
- schemaVersion: 5,
50
+ schemaVersion: 6,
51
51
  id: requireIdentity(id, "ReviewRound id"),
52
52
  taskId: requireIdentity(taskId, "Task id"),
53
53
  reviewerRoleName: requireIdentity(reviewerRoleName, "Reviewer Role"),
@@ -138,7 +138,7 @@ export function finishReviewRound(round, status, summary, now, result = {}) {
138
138
  * returns to pending so infrastructure retries do not manufacture a new
139
139
  * semantic ReviewRound or duplicate findings.
140
140
  */
141
- export function retryTaskReviewRound(round, executionLaneId) {
141
+ export function retryTaskReviewRound(round, requestedBy, executionLaneId) {
142
142
  validateReviewRound(round);
143
143
  if ((round.scope ?? "work-item") !== "task") {
144
144
  throw new Error(`Only a Task-final ReviewRound can be retried in place: ${round.id}.`);
@@ -180,7 +180,7 @@ export function retryTaskReviewRound(round, executionLaneId) {
180
180
  // Keep the historical attempt Group and Lane addressable from AgentRun
181
181
  // history while resetting the Lane for another dispatch attempt.
182
182
  ...(retryExecutionGroup === undefined ? {} : { executionGroup: retryExecutionGroup }),
183
- requestedBy: "leader",
183
+ requestedBy: validateReviewRequestSource(requestedBy),
184
184
  status: "pending",
185
185
  ...(round.workspace === undefined ? {} : { workspace: round.workspace }),
186
186
  createdAt: round.createdAt
@@ -359,8 +359,8 @@ export function updateReviewExecutionGroup(round, executionGroup) {
359
359
  return validateReviewRound({ ...round, executionGroup });
360
360
  }
361
361
  export function validateReviewRound(round) {
362
- if (round.schemaVersion !== 5)
363
- throw new Error("ReviewRound must use schemaVersion 5.");
362
+ if (round.schemaVersion !== 6)
363
+ throw new Error("ReviewRound must use schemaVersion 6.");
364
364
  validateTaskRecordReference({ taskId: round.taskId, localId: round.id }, "reviewRound");
365
365
  requireIdentity(round.reviewerRoleName, "Reviewer Role");
366
366
  requireCommit(round.reviewBaseCommit, "Review base commit");
@@ -637,7 +637,10 @@ function requireCommit(value, label) {
637
637
  return commit;
638
638
  }
639
639
  function validateReviewRequestSource(source) {
640
- if (source !== "policy" && source !== "leader") {
640
+ if (source !== "policy"
641
+ && source !== "user"
642
+ && source !== "operator"
643
+ && source !== "leader") {
641
644
  throw new Error(`Review request source is invalid: ${String(source)}.`);
642
645
  }
643
646
  return source;
@@ -0,0 +1,221 @@
1
+ import { contextContentDigest, validateContextSnapshotRef } from "../context/contextSnapshot.js";
2
+ import { computeYieldOutcomeDigest } from "./yieldReceipt.js";
3
+ export const RUN_YIELD_REJECTED_EVENT = "run.yield-rejected";
4
+ const MAX_SUMMARY_CHARACTERS = 2_000;
5
+ const MAX_REPORT_CHARACTERS = 8_000;
6
+ const MAX_CHECKS = 16;
7
+ const MAX_FINDINGS = 16;
8
+ const MAX_EVIDENCE = 16;
9
+ const MAX_LABEL_CHARACTERS = 128;
10
+ const MAX_DETAILS_CHARACTERS = 512;
11
+ const MAX_EVIDENCE_CHARACTERS = 512;
12
+ /**
13
+ * Creates the bounded diagnostic projection persisted when a Reviewer report
14
+ * cannot cross the exact terminalization fence. The full validated submission
15
+ * participates in the immutable digest, while only known, size-bounded fields
16
+ * are retained as non-authoritative evidence.
17
+ */
18
+ export function createRejectedYieldAttempt(input) {
19
+ const reviewResult = input.reviewResult;
20
+ const fullReport = reviewResult?.report ?? input.summary;
21
+ const contentDigest = computeYieldOutcomeDigest({
22
+ status: "yielded",
23
+ summary: input.summary,
24
+ ...(reviewResult === undefined ? {} : { reviewResult })
25
+ });
26
+ const summary = boundedText(input.summary, MAX_SUMMARY_CHARACTERS);
27
+ const report = boundedText(fullReport, MAX_REPORT_CHARACTERS);
28
+ const checks = (reviewResult?.checks ?? []).slice(0, MAX_CHECKS).map((check) => ({
29
+ name: boundedText(check.name, MAX_LABEL_CHARACTERS),
30
+ outcome: check.outcome,
31
+ ...(check.details === undefined
32
+ ? {}
33
+ : { details: boundedText(check.details, MAX_DETAILS_CHARACTERS) })
34
+ }));
35
+ const findings = (reviewResult?.findings ?? []).slice(0, MAX_FINDINGS).map((finding) => ({
36
+ id: boundedText(finding.id, MAX_LABEL_CHARACTERS),
37
+ severity: finding.severity,
38
+ status: finding.status,
39
+ summary: boundedText(finding.summary, MAX_DETAILS_CHARACTERS)
40
+ }));
41
+ const evidence = (reviewResult?.evidence ?? []).slice(0, MAX_EVIDENCE)
42
+ .map((entry) => boundedText(entry, MAX_EVIDENCE_CHARACTERS));
43
+ const deltaReasoning = reviewResult?.deltaReasoning === undefined
44
+ ? undefined
45
+ : boundedText(reviewResult.deltaReasoning, MAX_DETAILS_CHARACTERS);
46
+ const projectionTruncated = summary !== input.summary
47
+ || report !== fullReport
48
+ || checks.length !== (reviewResult?.checks?.length ?? 0)
49
+ || checks.some((check, index) => (check.name !== reviewResult?.checks?.[index]?.name
50
+ || check.details !== reviewResult?.checks?.[index]?.details))
51
+ || findings.length !== (reviewResult?.findings?.length ?? 0)
52
+ || findings.some((finding, index) => (finding.id !== reviewResult?.findings?.[index]?.id
53
+ || finding.summary !== reviewResult?.findings?.[index]?.summary))
54
+ || evidence.length !== (reviewResult?.evidence?.length ?? 0)
55
+ || evidence.some((entry, index) => entry !== reviewResult?.evidence?.[index])
56
+ || deltaReasoning !== reviewResult?.deltaReasoning
57
+ || reviewResult?.gitSnapshot !== undefined;
58
+ const observed = Object.freeze({
59
+ nativeSessionId: input.nativeSessionId ?? null,
60
+ launchId: input.launchId ?? null,
61
+ durableNativeSessionId: input.durableNativeSessionId ?? null,
62
+ durableLaunchId: input.durableLaunchId ?? null,
63
+ inFlightRunId: input.inFlightRunId ?? null,
64
+ inFlightReceiptId: input.inFlightReceiptId ?? null,
65
+ activeRunId: input.activeRun?.id ?? null,
66
+ activeReceiptId: input.activeRun?.receiptId ?? null,
67
+ contextSnapshot: input.contextSnapshot ?? null,
68
+ activeContextSnapshot: input.activeRun?.contextSnapshot ?? null
69
+ });
70
+ const attemptDigest = contextContentDigest({
71
+ taskId: input.taskId,
72
+ runId: input.runId,
73
+ reviewRoundId: input.reviewRoundId,
74
+ receiptId: input.receiptId,
75
+ rejectionReason: input.rejectionReason,
76
+ contentDigest,
77
+ observed
78
+ });
79
+ return Object.freeze({
80
+ schemaVersion: 1,
81
+ authority: "unaccepted",
82
+ semanticStatus: "diagnostic-only",
83
+ taskId: input.taskId,
84
+ runId: input.runId,
85
+ roleName: input.roleName,
86
+ purpose: "review",
87
+ reviewRoundId: input.reviewRoundId,
88
+ receiptId: input.receiptId,
89
+ rejectionReason: input.rejectionReason,
90
+ summary,
91
+ report,
92
+ checks: Object.freeze(checks),
93
+ findings: Object.freeze(findings),
94
+ evidence: Object.freeze(evidence),
95
+ ...(reviewResult?.evidenceCommit === undefined
96
+ ? {}
97
+ : { evidenceCommit: reviewResult.evidenceCommit }),
98
+ ...(reviewResult?.deltaDisposition === undefined
99
+ ? {}
100
+ : { deltaDisposition: reviewResult.deltaDisposition }),
101
+ ...(deltaReasoning === undefined ? {} : { deltaReasoning }),
102
+ projectionTruncated,
103
+ observed,
104
+ attemptedAt: input.attemptedAt.toISOString(),
105
+ contentDigest,
106
+ attemptDigest
107
+ });
108
+ }
109
+ export function rejectedYieldAttemptEventPayload(attempt) {
110
+ return {
111
+ runId: attempt.runId,
112
+ roleName: attempt.roleName,
113
+ reviewRoundId: attempt.reviewRoundId,
114
+ receiptId: attempt.receiptId,
115
+ rejectionReason: attempt.rejectionReason,
116
+ authority: attempt.authority,
117
+ semanticStatus: attempt.semanticStatus,
118
+ contentDigest: attempt.contentDigest,
119
+ attemptDigest: attempt.attemptDigest,
120
+ attempt: JSON.stringify(attempt)
121
+ };
122
+ }
123
+ /** Reads only canonical records emitted by rejectedYieldAttemptEventPayload. */
124
+ export function rejectedYieldAttemptFromTaskEvent(event) {
125
+ if (event.type !== RUN_YIELD_REJECTED_EVENT)
126
+ return null;
127
+ try {
128
+ const parsed = JSON.parse(event.payload.attempt ?? "");
129
+ if (!isRejectedYieldAttempt(parsed))
130
+ return null;
131
+ const expectedAttemptDigest = contextContentDigest({
132
+ taskId: parsed.taskId,
133
+ runId: parsed.runId,
134
+ reviewRoundId: parsed.reviewRoundId,
135
+ receiptId: parsed.receiptId,
136
+ rejectionReason: parsed.rejectionReason,
137
+ contentDigest: parsed.contentDigest,
138
+ observed: parsed.observed
139
+ });
140
+ if (parsed.schemaVersion !== 1
141
+ || parsed.authority !== "unaccepted"
142
+ || parsed.semanticStatus !== "diagnostic-only"
143
+ || parsed.purpose !== "review"
144
+ || parsed.taskId !== event.taskId
145
+ || parsed.runId !== event.payload.runId
146
+ || parsed.roleName !== event.payload.roleName
147
+ || parsed.reviewRoundId !== event.payload.reviewRoundId
148
+ || parsed.receiptId !== event.payload.receiptId
149
+ || parsed.rejectionReason !== event.payload.rejectionReason
150
+ || parsed.contentDigest !== event.payload.contentDigest
151
+ || parsed.attemptDigest !== event.payload.attemptDigest
152
+ || parsed.attemptDigest !== expectedAttemptDigest
153
+ || parsed.attemptedAt !== event.createdAt
154
+ || !/^[a-f0-9]{64}$/u.test(parsed.contentDigest)) {
155
+ return null;
156
+ }
157
+ return parsed;
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ function isRejectedYieldAttempt(value) {
164
+ if (typeof value !== "object" || value === null || Array.isArray(value))
165
+ return false;
166
+ const attempt = value;
167
+ const observed = attempt.observed;
168
+ if (typeof observed !== "object" || observed === null || Array.isArray(observed))
169
+ return false;
170
+ const nullableObserved = [
171
+ observed.nativeSessionId,
172
+ observed.launchId,
173
+ observed.durableNativeSessionId,
174
+ observed.durableLaunchId,
175
+ observed.inFlightRunId,
176
+ observed.inFlightReceiptId,
177
+ observed.activeRunId,
178
+ observed.activeReceiptId
179
+ ];
180
+ if (nullableObserved.some((entry) => entry !== null && typeof entry !== "string")) {
181
+ return false;
182
+ }
183
+ if (observed.contextSnapshot !== null)
184
+ validateContextSnapshotRef(observed.contextSnapshot);
185
+ if (observed.activeContextSnapshot !== null) {
186
+ validateContextSnapshotRef(observed.activeContextSnapshot);
187
+ }
188
+ return typeof attempt.taskId === "string"
189
+ && typeof attempt.runId === "string"
190
+ && typeof attempt.roleName === "string"
191
+ && typeof attempt.reviewRoundId === "string"
192
+ && typeof attempt.receiptId === "string"
193
+ && typeof attempt.rejectionReason === "string"
194
+ && typeof attempt.summary === "string"
195
+ && typeof attempt.report === "string"
196
+ && typeof attempt.projectionTruncated === "boolean"
197
+ && typeof attempt.attemptedAt === "string"
198
+ && typeof attempt.contentDigest === "string"
199
+ && typeof attempt.attemptDigest === "string"
200
+ && Array.isArray(attempt.checks)
201
+ && attempt.checks.every((check) => (typeof check === "object" && check !== null
202
+ && typeof check.name === "string"
203
+ && ["passed", "failed", "skipped"].includes(check.outcome)
204
+ && (check.details === undefined || typeof check.details === "string")))
205
+ && Array.isArray(attempt.findings)
206
+ && attempt.findings.every((finding) => (typeof finding === "object" && finding !== null
207
+ && typeof finding.id === "string"
208
+ && typeof finding.summary === "string"))
209
+ && Array.isArray(attempt.evidence)
210
+ && attempt.evidence.every((entry) => typeof entry === "string");
211
+ }
212
+ function boundedText(value, maximum) {
213
+ const safe = value
214
+ .replaceAll(/\u001B\][^\u0007]*(?:\u0007|\u001B\\|\u009C)/gu, " ")
215
+ .replaceAll(/\u001B\[[0-?]*[ -/]*[@-~]/gu, " ")
216
+ .replaceAll(/[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/gu, " ");
217
+ const characters = Array.from(safe);
218
+ if (characters.length <= maximum)
219
+ return safe;
220
+ return `${characters.slice(0, maximum - 1).join("")}…`;
221
+ }
@@ -183,9 +183,18 @@ export class TmuxSessionHost {
183
183
  : { environment: request.environment }),
184
184
  ...(request.mode === "resume" ? { nativeSessionId: request.nativeSessionId } : {})
185
185
  };
186
- const planned = request.owner.scope === "task"
187
- ? this.planner.plan({ taskId: request.owner.taskId, ...input })
188
- : this.planner.planGlobalRole(input);
186
+ let planned;
187
+ try {
188
+ planned = request.owner.scope === "task"
189
+ ? this.planner.plan({ taskId: request.owner.taskId, ...input })
190
+ : this.planner.planGlobalRole(input);
191
+ }
192
+ catch (error) {
193
+ throw toRuntimeLaunchFailure(error, "validation", {
194
+ cwd: request.workspace,
195
+ agentId: request.agentId
196
+ });
197
+ }
189
198
  if (planned.role.name !== request.owner.roleName) {
190
199
  throw new Error("Planned Role does not match the runtime owner.");
191
200
  }
@@ -304,6 +304,9 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
304
304
  run,
305
305
  session: existingSession,
306
306
  summary: error.message,
307
+ leaderRecovery: role.name === "leader" && error.diagnostic.kind === "config"
308
+ ? "blocked"
309
+ : "automatic",
307
310
  now
308
311
  });
309
312
  results.push({
@@ -375,6 +378,7 @@ export async function processActiveRoleRunDeliveries(store, delivery, now, selec
375
378
  run,
376
379
  session: existingSession,
377
380
  summary: `Role Run could not start: ${message}`,
381
+ leaderRecovery: "automatic",
378
382
  now
379
383
  });
380
384
  results.push({
@@ -57,6 +57,8 @@ const REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION = 3;
57
57
  const REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION = 4;
58
58
  const REVIEW_ROUND_TASK_ANCHOR_FROM_VERSION = 4;
59
59
  const REVIEW_ROUND_TASK_ANCHOR_TO_VERSION = 5;
60
+ const REVIEW_ROUND_ACTOR_FROM_VERSION = 5;
61
+ const REVIEW_ROUND_ACTOR_TO_VERSION = 6;
60
62
  const ACTIVE_RUN_POINTER_FROM_VERSION = 1;
61
63
  const ACTIVE_RUN_POINTER_TO_VERSION = 2;
62
64
  const ACTIVE_RUN_POINTER_NAMESPACE_FROM_VERSION = 2;
@@ -69,6 +71,10 @@ const INTEGRATION_ATTEMPT_FROM_VERSION = 2;
69
71
  const INTEGRATION_ATTEMPT_TO_VERSION = 3;
70
72
  const INTEGRATION_ATTEMPT_GATE_IDENTITY_FROM_VERSION = 3;
71
73
  const INTEGRATION_ATTEMPT_GATE_IDENTITY_TO_VERSION = 4;
74
+ const INTEGRATION_ATTEMPT_ACTOR_FROM_VERSION = 4;
75
+ const INTEGRATION_ATTEMPT_ACTOR_TO_VERSION = 5;
76
+ const MILESTONE_ACTOR_FROM_VERSION = 1;
77
+ const MILESTONE_ACTOR_TO_VERSION = 2;
72
78
  const INTEGRATION_QUEUE_FROM_VERSION = 0;
73
79
  const INTEGRATION_QUEUE_TO_VERSION = 1;
74
80
  const CONTEXT_SNAPSHOT_FROM_VERSION = 0;
@@ -174,12 +180,15 @@ export function createProductionStorageRegistry() {
174
180
  .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_FROM_VERSION, REVIEW_ROUND_TO_VERSION, "reviewRounds"))
175
181
  .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_GIT_SNAPSHOT_FROM_VERSION, REVIEW_ROUND_GIT_SNAPSHOT_TO_VERSION, "reviewRounds"))
176
182
  .registerOfflineMigration(reviewRoundTaskAnchorStep())
183
+ .registerOfflineMigration(recordFamilyStep("reviewRound", REVIEW_ROUND_ACTOR_FROM_VERSION, REVIEW_ROUND_ACTOR_TO_VERSION, "reviewRounds"))
177
184
  .registerOfflineMigration(recordFamilyStep("activeRunPointer", ACTIVE_RUN_POINTER_FROM_VERSION, ACTIVE_RUN_POINTER_TO_VERSION, "activeRuns"))
178
185
  .registerOfflineMigration(managedWorkspaceFamilyStep())
179
186
  .registerOfflineMigration(activeRunPointerNamespaceStep())
180
187
  .registerOfflineMigration(changeSetManifestStep())
181
188
  .registerOfflineMigration(integrationAttemptSupersededStep())
182
189
  .registerOfflineMigration(recordFamilyStep("integrationAttempt", INTEGRATION_ATTEMPT_GATE_IDENTITY_FROM_VERSION, INTEGRATION_ATTEMPT_GATE_IDENTITY_TO_VERSION, "integrationAttempts"))
190
+ .registerOfflineMigration(recordFamilyStep("integrationAttempt", INTEGRATION_ATTEMPT_ACTOR_FROM_VERSION, INTEGRATION_ATTEMPT_ACTOR_TO_VERSION, "integrationAttempts"))
191
+ .registerOfflineMigration(recordFamilyStep("milestone", MILESTONE_ACTOR_FROM_VERSION, MILESTONE_ACTOR_TO_VERSION, "milestones"))
183
192
  .registerOfflineMigration(integrationQueueIntroductionStep())
184
193
  .registerOfflineMigration(contextSnapshotIntroductionStep())
185
194
  .registerCompatible(storedTaskDurableJobsStep())