@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
@@ -11,8 +11,7 @@ import { execSync } from "node:child_process";
11
11
  import { createHash } from "node:crypto";
12
12
  import { resolve, sep } from "node:path";
13
13
  import { acknowledgeUnknownDurableJob, createDurableJob, durableJobIdempotencyKey, isDurableJobTerminal, requestDurableJobCancel, retryDurableJobIdempotencyKey } from "../job/durableJob.js";
14
- import { formatAgentRunReceiptId } from "../task/taskRecordReference.js";
15
- import { agentRunDeliveryReceiptId } from "../run/agentRun.js";
14
+ import { activeLiveRoleAgentSession } from "../executor/agentExecutor.js";
16
15
  export function createDurableJobControl(store) {
17
16
  return {
18
17
  startJob(params, now) {
@@ -63,7 +62,7 @@ export function createDurableJobControl(store) {
63
62
  return null;
64
63
  // rr8: Bind the cancel request to the caller's managed identity. The
65
64
  // same rules as job.start apply, checked against the job's owner.
66
- assertCallerAuthorized(tx, caller, current.owner, taskId);
65
+ assertCallerAuthorized(tx, caller, taskId);
67
66
  const next = requestDurableJobCancel(current, now);
68
67
  if (next !== current)
69
68
  tx.saveDurableJob(taskId, next);
@@ -75,11 +74,7 @@ export function createDurableJobControl(store) {
75
74
  const current = tx.getDurableJob(taskId, jobId);
76
75
  if (current === null)
77
76
  return null;
78
- // Acknowledge is a Leader-only recovery decision. Reuse the shared
79
- // caller-key, active-Run, receipt, and Session checks, but explicitly
80
- // require the Leader role; start/cancel's owner binding intentionally
81
- // permits a Worker to operate its own Work Item.
82
- assertCallerAuthorized(tx, caller, current.owner, taskId, { leaderOnly: true });
77
+ assertCallerAuthorized(tx, caller, taskId);
83
78
  const next = acknowledgeUnknownDurableJob(current, now);
84
79
  if (next !== current)
85
80
  tx.saveDurableJob(taskId, next);
@@ -184,9 +179,9 @@ function validateStartParams(store, params) {
184
179
  }
185
180
  }
186
181
  // rr8: Bind the declared owner to the caller's managed identity. A
187
- // Reviewer can never start jobs; a Worker can only start jobs owned by
188
- // its own Work Item; a Leader or a plain user retains full access.
189
- assertCallerAuthorized(store, params.caller, params.owner, params.taskId);
182
+ // Role is not an authorization boundary. Scope and exact managed Session
183
+ // identity are verified independently below.
184
+ assertCallerAuthorized(store, params.caller, params.taskId);
190
185
  }
191
186
  /**
192
187
  * rr8/rr12: Bind the declared job owner to the caller's managed identity. The
@@ -194,49 +189,70 @@ function validateStartParams(store, params) {
194
189
  * against durable Run/Session state — a self-reported role or scope is never
195
190
  * authority on its own. The rules:
196
191
  *
197
- * - `user` (non-managed): full access, but only when the request carries a
198
- * `leaderAssertion` the Controller verifies against the active in-flight
199
- * Leader Run. A managed Session that sheds its identity and declares
200
- * `scope: "user"` without that proof is rejected.
192
+ * - `user` (non-managed): rejected because it has no managed Session identity.
193
+ * - `global`: full Task authority after current Role Session verification.
201
194
  * - `task` + mismatched taskId: rejected.
202
195
  * - `task` + missing `runId`: rejected (a managed caller must bind to a Run).
203
196
  * - `task` + Run not found / not active / `roleName !== role`: rejected — the
204
197
  * claimed Role must be the real Role of an active Run.
205
- * - `task` + Reviewer: always rejected.
206
- * - `task` + Leader: must carry `receiptId` and pass the same
207
- * active-Run + in-flight-receipt + Session check as `job.acknowledge`, so a
208
- * borrowed or stale Leader Run cannot authorize the request.
209
- * - `task` + any other Role (Worker etc.): the owner must be `work-item` and
210
- * the caller's Run must be for that same Work Item.
198
+ * - `task`: full authority inside the matching Task after active Run and
199
+ * per-Session caller-key verification; Role does not narrow it.
211
200
  */
212
201
  /**
213
202
  * rr13: `job.start`/`job.cancel` caller authorization at the Controller
214
203
  * boundary. Every identity claim the Controller can verify from durable state
215
- * (role, runId, receiptId, leaderAssertion) is replayable by any client in the
204
+ * (role and runId) is replayable by any client in the
216
205
  * same Home that reads state.json. The channel itself is therefore not
217
206
  * authenticated by those claims alone. A non-replayable per-Session caller key
218
207
  * closes the gap:
219
208
  *
220
209
  * - `user` scope: **rejected outright** for start/cancel. A bare shell or a
221
- * managed Session that sheds its identity has no channel binding. The human
222
- * operator acts through the Leader Session, which carries a caller key.
210
+ * managed Session that sheds its identity has no managed Session binding.
211
+ * - `global` scope: verified against the current global Role Session.
223
212
  * - `task` scope: the caller must present `callerKey` — the
224
213
  * `YUI_JOB_CALLER_KEY` injected at its native Session launch. The Controller
225
214
  * hashes it (SHA-256) and compares against the durable `jobCallerKeyHashes`
226
215
  * map for the caller's Role + Agent. No legacy fallback: an absent hash or a
227
216
  * mismatched key is UNAUTHORIZED.
228
217
  *
229
- * The existing Run/role/WorkItem binding checks (rr8/rr12) run after the key
230
- * check, so a forged identity that also lacks the key is rejected at the
231
- * channel boundary before any durable-state lookup.
218
+ * The existing Run binding checks run after the key check, so a forged
219
+ * identity that also lacks the key is rejected at the channel boundary.
232
220
  */
233
- function assertCallerAuthorized(store, caller, owner, taskId, options = {}) {
221
+ function assertCallerAuthorized(store, caller, taskId) {
234
222
  if (caller.scope === "user") {
235
223
  // rr13: A user-scope caller has no per-Session channel binding. Every
236
- // durable-state claim it could carry (leaderAssertion etc.) is replayable
224
+ // durable-state claim it could carry is replayable
237
225
  // by any client in the same Home. Reject outright (fail-closed).
238
226
  throw jobControlError("UNAUTHORIZED", "job.start/job.cancel requires a managed Session caller key; user scope is rejected.");
239
227
  }
228
+ if (caller.scope === "global") {
229
+ const roleName = caller.role;
230
+ const agentId = caller.agentId;
231
+ const role = roleName === undefined ? null : store.getGlobalRole(roleName);
232
+ const binding = role === null || agentId === undefined
233
+ ? undefined
234
+ : role.agentBindings[role.activeAgentId];
235
+ const sessions = roleName === undefined ? null : store.getGlobalRoleSessionSet(roleName);
236
+ const session = activeLiveRoleAgentSession(sessions);
237
+ if (role === null
238
+ || binding === undefined
239
+ || agentId === undefined
240
+ || caller.adapterId === undefined
241
+ || caller.launchId === undefined
242
+ || sessions === null
243
+ || sessions.activeAgentId !== role.activeAgentId
244
+ || session === null
245
+ || binding.agentId !== agentId
246
+ || binding.adapterId !== caller.adapterId
247
+ || session.agentId !== agentId
248
+ || session.adapterId !== caller.adapterId
249
+ || session.launchId !== caller.launchId
250
+ || (caller.nativeSessionId !== undefined
251
+ && session.nativeSessionId !== caller.nativeSessionId)) {
252
+ throw jobControlError("UNAUTHORIZED", "DurableJob control requires the current managed global Agent Session.");
253
+ }
254
+ return;
255
+ }
240
256
  if (caller.taskId !== taskId) {
241
257
  throw jobControlError("UNAUTHORIZED", "A managed Task Session may not start or cancel Jobs for a different Task.");
242
258
  }
@@ -262,29 +278,6 @@ function assertCallerAuthorized(store, caller, owner, taskId, options = {}) {
262
278
  if (presentedHash !== expectedHash) {
263
279
  throw jobControlError("UNAUTHORIZED", "The managed Session caller key does not match the durable hash.");
264
280
  }
265
- if (options.leaderOnly && caller.role !== "leader") {
266
- throw jobControlError("UNAUTHORIZED", "job.acknowledge requires the current Task Leader Session.");
267
- }
268
- if (caller.role === "reviewer") {
269
- throw jobControlError("UNAUTHORIZED", "A Reviewer may not start or cancel DurableJobs.");
270
- }
271
- if (caller.role === "leader") {
272
- // rr12: Verify the caller is THE current in-flight Task Leader, not a
273
- // borrowed or stale Leader Run. This mirrors job.acknowledge.
274
- if (caller.receiptId === undefined) {
275
- throw jobControlError("UNAUTHORIZED", "A Leader must carry the current in-flight Turn receipt.");
276
- }
277
- assertLeaderActionRun(store, taskId, {
278
- runId: run.id,
279
- receiptId: caller.receiptId
280
- });
281
- return;
282
- }
283
- // Worker (or any non-leader, non-reviewer managed role): the owner must be
284
- // the caller's own Work Item, verified through the caller's Run.
285
- if (owner.kind !== "work-item" || run.workItemId !== owner.workItemId) {
286
- throw jobControlError("UNAUTHORIZED", "A managed Worker may only start or cancel Jobs owned by its own Work Item.");
287
- }
288
281
  }
289
282
  /**
290
283
  * Resolve the managed workspace for the job's owner. Returns null if no
@@ -331,30 +324,6 @@ function readGitHead(path) {
331
324
  function isTerminalWorkItemStatus(status) {
332
325
  return status === "completed" || status === "failed" || status === "retired";
333
326
  }
334
- /**
335
- * rr5/f5: Verify that the caller is the current in-flight Task Leader. The
336
- * runId must identify the active Leader Run and the receiptId must match the
337
- * Run's current in-flight Turn receipt. This binds the assertion to the
338
- * Leader identity/session the Controller already tracks — a bare flag or a
339
- * stale Run/receipt pair is rejected.
340
- */
341
- function assertLeaderActionRun(store, taskId, assertion) {
342
- const run = store.getActiveAgentRun(taskId, "leader");
343
- if (run === null || run.status !== "active" || run.id !== assertion.runId) {
344
- throw jobControlError("UNAUTHORIZED", "job.acknowledge requires the current active Task Leader Run.");
345
- }
346
- const expectedReceipt = formatAgentRunReceiptId(taskId, run.id);
347
- if (assertion.receiptId !== expectedReceipt) {
348
- throw jobControlError("UNAUTHORIZED", "job.acknowledge Leader receipt does not match the current Run.");
349
- }
350
- const sessions = store.getTaskRoleSessionSet(taskId, "leader");
351
- if (sessions === null
352
- || sessions.inFlight === null
353
- || sessions.inFlight.runId !== run.id
354
- || sessions.inFlight.receiptId !== agentRunDeliveryReceiptId(run)) {
355
- throw jobControlError("UNAUTHORIZED", "job.acknowledge Leader Run is not in flight.");
356
- }
357
- }
358
327
  /**
359
328
  * Strict `job.start` params parsing. Throws CoreApplicationError-shaped errors
360
329
  * so the socket layer reports INVALID_PARAMS without leaking internals.
@@ -430,8 +399,7 @@ export function parseDurableJobCancelParams(value) {
430
399
  }
431
400
  /**
432
401
  * rr26: `job.acknowledge` carries the same managed task caller as
433
- * job.start/job.cancel. A replayable leaderAssertion without the ephemeral
434
- * Session caller key is rejected by assertCallerAuthorized.
402
+ * job.start/job.cancel.
435
403
  */
436
404
  export function parseDurableJobAcknowledgeParams(value) {
437
405
  if (typeof value !== "object" || value === null || Array.isArray(value)
@@ -508,9 +476,8 @@ function parseSteps(value) {
508
476
  * boundary (fail-closed). A non-managed caller must explicitly resolve to
509
477
  * `{scope: "user"}`; the Controller never defaults a missing identity to
510
478
  * user scope. A present caller must carry a valid `scope` and may carry
511
- * optional `taskId`, `role`, `runId`, `receiptId`, and `leaderAssertion`
512
- * fields. The identity is verified against durable Run state by
513
- * `assertCallerAuthorized`; parsing only validates the JSON shape.
479
+ * optional managed Session identity fields. The identity is verified against
480
+ * durable state by `assertCallerAuthorized`; parsing only validates the JSON shape.
514
481
  */
515
482
  function parseCaller(value) {
516
483
  if (value === undefined) {
@@ -521,14 +488,15 @@ function parseCaller(value) {
521
488
  }
522
489
  const record = value;
523
490
  const allowed = new Set([
524
- "scope", "taskId", "role", "runId", "receiptId", "leaderAssertion", "callerKey"
491
+ "scope", "taskId", "role", "agentId", "adapterId", "launchId", "nativeSessionId",
492
+ "runId", "callerKey"
525
493
  ]);
526
494
  for (const key of Object.keys(record)) {
527
495
  if (!allowed.has(key)) {
528
496
  throw jobControlError("INVALID_PARAMS", "job caller is invalid.");
529
497
  }
530
498
  }
531
- if (record.scope !== "user" && record.scope !== "task") {
499
+ if (record.scope !== "user" && record.scope !== "global" && record.scope !== "task") {
532
500
  throw jobControlError("INVALID_PARAMS", "job caller scope is invalid.");
533
501
  }
534
502
  const optionalId = (key) => {
@@ -539,9 +507,11 @@ function parseCaller(value) {
539
507
  };
540
508
  const taskId = optionalId("taskId");
541
509
  const role = optionalId("role");
510
+ const agentId = optionalId("agentId");
511
+ const adapterId = optionalId("adapterId");
512
+ const launchId = optionalId("launchId");
513
+ const nativeSessionId = optionalId("nativeSessionId");
542
514
  const runId = optionalId("runId");
543
- const receiptId = optionalId("receiptId");
544
- const leaderAssertion = parseLeaderAssertion(record.leaderAssertion);
545
515
  const callerKey = record.callerKey === undefined
546
516
  ? undefined
547
517
  : requiredId(record.callerKey, "job caller callerKey");
@@ -549,30 +519,14 @@ function parseCaller(value) {
549
519
  scope: record.scope,
550
520
  ...(taskId === undefined ? {} : { taskId }),
551
521
  ...(role === undefined ? {} : { role }),
522
+ ...(agentId === undefined ? {} : { agentId }),
523
+ ...(adapterId === undefined ? {} : { adapterId }),
524
+ ...(launchId === undefined ? {} : { launchId }),
525
+ ...(nativeSessionId === undefined ? {} : { nativeSessionId }),
552
526
  ...(runId === undefined ? {} : { runId }),
553
- ...(receiptId === undefined ? {} : { receiptId }),
554
- ...(leaderAssertion === undefined ? {} : { leaderAssertion }),
555
527
  ...(callerKey === undefined ? {} : { callerKey })
556
528
  };
557
529
  }
558
- /**
559
- * rr12: Parse a `leaderAssertion` sub-object (`{runId, receiptId}`) carried by
560
- * a user-scope caller. Returns undefined when absent; throws on a malformed
561
- * shape.
562
- */
563
- function parseLeaderAssertion(value) {
564
- if (value === undefined)
565
- return undefined;
566
- if (typeof value !== "object" || value === null || Array.isArray(value)
567
- || Object.keys(value).length !== 2) {
568
- throw jobControlError("INVALID_PARAMS", "job caller leaderAssertion must have runId and receiptId.");
569
- }
570
- const record = value;
571
- return {
572
- runId: requiredId(record.runId, "job caller leaderAssertion runId"),
573
- receiptId: requiredId(record.receiptId, "job caller leaderAssertion receiptId")
574
- };
575
- }
576
530
  function parseStringMap(value, label) {
577
531
  if (value === undefined)
578
532
  return {};
@@ -1,5 +1,4 @@
1
- import { isDeepStrictEqual } from "node:util";
2
- import { isTaskOwnedWorkspace } from "../worktree/managedWorkspace.js";
1
+ import { isTaskOwnedWorkspace, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
3
2
  function ownerLabel(owner) {
4
3
  switch (owner.type) {
5
4
  case "task":
@@ -43,11 +42,13 @@ export function classifyWorkspacePreflight(store, task, roleName, activeRun, ins
43
42
  };
44
43
  }
45
44
  // 2. Run snapshot validation: when the active Run carries a workspace
46
- // snapshot it must match the durable ManagedWorkspace exactly. A
47
- // mismatch means the Run was created against a stale baseline.
45
+ // snapshot its launch-stable identity must match the durable
46
+ // ManagedWorkspace. Audit timestamps may change without changing the
47
+ // owner, root, or Project entries authorized for the Run.
48
48
  if (activeRun?.workspace !== undefined) {
49
49
  const durableRunWorkspace = store.getManagedWorkspace(activeRun.workspace.owner);
50
- if (durableRunWorkspace === null || !isDeepStrictEqual(durableRunWorkspace, activeRun.workspace)) {
50
+ if (durableRunWorkspace === null
51
+ || !sameManagedWorkspaceIdentity(durableRunWorkspace, activeRun.workspace)) {
51
52
  const diff = [];
52
53
  if (durableRunWorkspace === null) {
53
54
  diff.push(`durable ManagedWorkspace for owner ${JSON.stringify(activeRun.workspace.owner)} is missing`);
@@ -16,7 +16,7 @@ import { acquireProjectMaintenanceLocks } from "../repository/projectMaintenance
16
16
  import { taskWorkspaceRefSegment } from "../repository/taskWorkspaceIdentity.js";
17
17
  import { FileTaskRuntimeIsolation } from "../runtime/taskRuntimeIsolation.js";
18
18
  import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
19
- import { recordIntegrationCheckJob, requireLeaderDecision, updateIntegrationAttempt } from "./integrationAttempt.js";
19
+ import { recordIntegrationCheckJob, requireResolutionDecision, updateIntegrationAttempt } from "./integrationAttempt.js";
20
20
  import { createManagedWorkspace } from "../worktree/managedWorkspace.js";
21
21
  import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
22
22
  import { readRuntimeIdentity } from "../release/runtimeRelease.js";
@@ -228,7 +228,7 @@ export class GitIntegrationService {
228
228
  }
229
229
  catch (error) {
230
230
  if (error instanceof RemoteBaselineConflictError) {
231
- const pending = requireLeaderDecision(current, {
231
+ const pending = requireResolutionDecision(current, {
232
232
  affectedPaths: error.affectedPaths,
233
233
  summary: error.message
234
234
  }, this.now());
@@ -592,7 +592,7 @@ export class GitIntegrationService {
592
592
  await git(["-C", candidatePath, "cherry-pick", "--skip"]);
593
593
  continue;
594
594
  }
595
- const pending = requireLeaderDecision(attempt, {
595
+ const pending = requireResolutionDecision(attempt, {
596
596
  affectedPaths,
597
597
  summary: `ChangeSet ${changeSetId} conflicts with ${attempt.targetRef}.`
598
598
  }, this.now());
@@ -4,7 +4,7 @@ import { normalizeCheckResult } from "./checkResult.js";
4
4
  export function createIntegrationAttempt(input, now) {
5
5
  const timestamp = now.toISOString();
6
6
  return validateIntegrationAttempt({
7
- schemaVersion: 4,
7
+ schemaVersion: 5,
8
8
  id: input.id,
9
9
  taskId: input.taskId,
10
10
  projectId: input.projectId,
@@ -37,7 +37,7 @@ export function recordIntegrationCheckJob(attempt, jobId, now) {
37
37
  updatedAt: now.toISOString()
38
38
  });
39
39
  }
40
- export function requireLeaderDecision(attempt, report, now) {
40
+ export function requireResolutionDecision(attempt, report, now) {
41
41
  validateIntegrationAttempt(attempt);
42
42
  const continuingAfterResolution = attempt.status === "blocked"
43
43
  && attempt.resolution?.action === "manual-resolution";
@@ -52,7 +52,7 @@ export function requireLeaderDecision(attempt, report, now) {
52
52
  updatedAt: now.toISOString()
53
53
  });
54
54
  }
55
- export function recordResolutionDecision(attempt, decision, now) {
55
+ export function recordResolutionDecision(attempt, decision, decidedBy, now) {
56
56
  validateIntegrationAttempt(attempt);
57
57
  if (attempt.status !== "blocked" || attempt.conflict === undefined) {
58
58
  throw new Error("Integration has no pending semantic decision.");
@@ -67,13 +67,19 @@ export function recordResolutionDecision(attempt, decision, now) {
67
67
  resolution: {
68
68
  action: decision.action,
69
69
  rationale: requireText(decision.rationale, "Resolution rationale"),
70
- decidedBy: "leader",
70
+ decidedBy: requireTaskControlActor(decidedBy),
71
71
  decidedAt: timestamp
72
72
  },
73
73
  updatedAt: timestamp,
74
74
  ...(decision.action === "reject" ? { endedAt: timestamp } : {})
75
75
  });
76
76
  }
77
+ function requireTaskControlActor(value) {
78
+ if (value !== "user" && value !== "operator" && value !== "leader") {
79
+ throw new Error(`Integration decision actor is invalid: ${String(value)}.`);
80
+ }
81
+ return value;
82
+ }
77
83
  const TERMINAL_STATUSES = ["committed", "superseded", "failed"];
78
84
  export function updateIntegrationAttempt(attempt, patch, now) {
79
85
  validateIntegrationAttempt(attempt);
@@ -117,8 +123,8 @@ export function supersedeIntegration(attempt, reason, now) {
117
123
  });
118
124
  }
119
125
  export function validateIntegrationAttempt(attempt) {
120
- if (attempt.schemaVersion !== 4) {
121
- throw new Error("IntegrationAttempt must use schemaVersion 4.");
126
+ if (attempt.schemaVersion !== 5) {
127
+ throw new Error("IntegrationAttempt must use schemaVersion 5.");
122
128
  }
123
129
  validateTaskRecordReference({
124
130
  taskId: attempt.taskId,
@@ -162,9 +168,7 @@ export function validateIntegrationAttempt(attempt) {
162
168
  throw new Error(`Resolution action is invalid: ${String(attempt.resolution.action)}.`);
163
169
  }
164
170
  requireText(attempt.resolution.rationale, "Resolution rationale");
165
- if (attempt.resolution.decidedBy !== "leader") {
166
- throw new Error("Only the Leader may record a ResolutionDecision.");
167
- }
171
+ requireTaskControlActor(attempt.resolution.decidedBy);
168
172
  requireTimestamp(attempt.resolution.decidedAt, "Resolution decidedAt");
169
173
  }
170
174
  attempt.checks?.forEach(normalizeCheckResult);
@@ -701,11 +701,11 @@ export async function reconcileIntegrationQueueEntry(store, taskId, entryId, git
701
701
  }
702
702
  /**
703
703
  * A conflicted entry may carry a blocked IntegrationAttempt waiting for a
704
- * leader decision. Requeue or supersede IS that decision — the blocked
704
+ * Task Agent decision. Requeue or supersede IS that decision — the blocked
705
705
  * attempt is abandoned — so resolve it as rejected (terminal `failed`)
706
706
  * rather than leaving it to block Task retirement.
707
707
  */
708
- function resolveBlockedAttempt(store, taskId, entry, decision, now) {
708
+ function resolveBlockedAttempt(store, taskId, entry, decision, decidedBy, now) {
709
709
  if (entry.integrationAttemptId === undefined)
710
710
  return;
711
711
  const attempt = store.getIntegrationAttempt(taskId, entry.integrationAttemptId);
@@ -714,7 +714,7 @@ function resolveBlockedAttempt(store, taskId, entry, decision, now) {
714
714
  const rejected = recordResolutionDecision(attempt, {
715
715
  action: "reject",
716
716
  rationale: `Integration Attempt rejected by queue ${decision}.`
717
- }, now());
717
+ }, decidedBy, now());
718
718
  store.saveIntegrationAttempt(taskId, rejected);
719
719
  }
720
720
  /**
@@ -737,7 +737,7 @@ function assertRecoverableAttempt(store, taskId, entry, action) {
737
737
  }
738
738
  }
739
739
  /** Retry a conflicted item (for example after a gate failure was fixed). */
740
- export function requeueIntegrationQueueEntry(store, taskId, entryId, now = () => new Date()) {
740
+ export function requeueIntegrationQueueEntry(store, taskId, entryId, decidedBy, now = () => new Date()) {
741
741
  // The committed-Attempt guard, the blocked-Attempt finalization, and the
742
742
  // queue write must be atomic: a concurrent manual-resolution continue can
743
743
  // commit the Attempt between the guard and the write, leaving the queue
@@ -749,20 +749,20 @@ export function requeueIntegrationQueueEntry(store, taskId, entryId, now = () =>
749
749
  throw new Error(`Integration queue entry not found: ${taskId}/${entryId}.`);
750
750
  }
751
751
  assertRecoverableAttempt(tx, taskId, entry, "requeue");
752
- resolveBlockedAttempt(tx, taskId, entry, "requeue", now);
752
+ resolveBlockedAttempt(tx, taskId, entry, "requeue", decidedBy, now);
753
753
  const waiting = markIntegrationQueueRequeued(entry, now());
754
754
  tx.saveIntegrationQueueEntry(taskId, waiting);
755
755
  return waiting;
756
756
  });
757
757
  }
758
- export function supersedeIntegrationQueueEntry(store, taskId, entryId, reason, now = () => new Date()) {
758
+ export function supersedeIntegrationQueueEntry(store, taskId, entryId, reason, decidedBy, now = () => new Date()) {
759
759
  return store.transaction((tx) => {
760
760
  const entry = tx.getIntegrationQueueEntry(taskId, entryId);
761
761
  if (entry === null) {
762
762
  throw new Error(`Integration queue entry not found: ${taskId}/${entryId}.`);
763
763
  }
764
764
  assertRecoverableAttempt(tx, taskId, entry, "supersede");
765
- resolveBlockedAttempt(tx, taskId, entry, "supersede", now);
765
+ resolveBlockedAttempt(tx, taskId, entry, "supersede", decidedBy, now);
766
766
  const superseded = markIntegrationQueueSuperseded(entry, reason, now());
767
767
  tx.saveIntegrationQueueEntry(taskId, superseded);
768
768
  return superseded;