@abelxiaoxing/cadence 1.3.0 → 1.4.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 (45) hide show
  1. package/README.md +115 -10
  2. package/package.json +5 -3
  3. package/prompts/abel-design.md +71 -17
  4. package/prompts/abel-diagnose.md +10 -1
  5. package/prompts/abel-implement.md +118 -20
  6. package/prompts/abel-init.md +9 -2
  7. package/src/apply-transaction.ts +16 -14
  8. package/src/artifact-store.ts +17 -0
  9. package/src/candidate-patch.ts +2 -1
  10. package/src/canonical.ts +16 -0
  11. package/src/change-contract.ts +268 -0
  12. package/src/child-session.ts +4 -3
  13. package/src/contracts.ts +141 -33
  14. package/src/control-contracts.ts +51 -1
  15. package/src/delivery-compiler.ts +148 -31
  16. package/src/design-control.ts +69 -11
  17. package/src/design-journal.ts +216 -72
  18. package/src/durable-workflow.ts +4385 -0
  19. package/src/implement-graph.ts +11 -16
  20. package/src/index.ts +380 -981
  21. package/src/isolation-backend.ts +84 -23
  22. package/src/package-delivery.ts +486 -0
  23. package/src/package-verification.ts +509 -0
  24. package/src/route-policy.ts +1 -11
  25. package/src/run-state.ts +4 -16
  26. package/src/run-store.ts +5 -144
  27. package/src/scoped-grep-worker.mjs +23 -0
  28. package/src/scoped-tools.ts +312 -116
  29. package/src/sqlite-schema.ts +171 -0
  30. package/src/storage-schema.ts +352 -0
  31. package/src/subagent-activity.ts +14 -1
  32. package/src/submit-tool.ts +20 -2
  33. package/src/task-ledger.ts +14 -55
  34. package/src/verification-capability.ts +298 -102
  35. package/src/verification-environment.ts +213 -0
  36. package/src/verification-identity.ts +77 -0
  37. package/src/worker-broker.ts +97 -34
  38. package/src/workflow-engine.ts +28 -8285
  39. package/src/workflow-policy.ts +1427 -0
  40. package/src/workflow-state-machine.ts +4478 -0
  41. package/src/workspace-io-entry.mjs +58 -0
  42. package/src/workspace-io-worker.mjs +984 -0
  43. package/src/workspace-io.ts +84 -0
  44. package/src/workspace-store.ts +125 -19
  45. package/skills/abel-workflow/SKILL.md +0 -205
@@ -0,0 +1,213 @@
1
+ import {
2
+ lstatSync,
3
+ mkdirSync,
4
+ mkdtempSync,
5
+ readdirSync,
6
+ readlinkSync,
7
+ realpathSync,
8
+ rmSync,
9
+ symlinkSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import path from "node:path";
13
+ import {
14
+ type StructuredVerificationContract,
15
+ VERIFICATION_CONFIGURATION_PATHS,
16
+ } from "./contracts.ts";
17
+ import type { IsolationMount } from "./isolation-backend.ts";
18
+ import {
19
+ resolveVerificationRunner,
20
+ type VerificationRunnerBinding,
21
+ validateVerificationAdapterCapability,
22
+ verificationRunnerFiles,
23
+ } from "./verification-capability.ts";
24
+ import { runWorkspaceIo } from "./workspace-io.ts";
25
+
26
+ export async function captureVerificationEnvironmentIdentity(
27
+ dependencyOwner: string,
28
+ verifications: readonly StructuredVerificationContract[],
29
+ signal?: AbortSignal,
30
+ ): Promise<string> {
31
+ const roots = [path.join(dependencyOwner, "node_modules")];
32
+ const node = resolveVerificationRunner("node");
33
+ if (node) roots.push(realpathSync(node.executablePath));
34
+ for (const verification of verifications) {
35
+ // Identity describes installed tools; admission/execution separately owns config authorization.
36
+ const capability = validateVerificationAdapterCapability(
37
+ dependencyOwner,
38
+ verification,
39
+ { executionWritePaths: VERIFICATION_CONFIGURATION_PATHS },
40
+ );
41
+ if (!capability.ok) throw new Error("verification-environment-unavailable");
42
+ roots.push(
43
+ ...verificationRunnerFiles(
44
+ dependencyOwner,
45
+ verification,
46
+ capability.runnerBindings,
47
+ ),
48
+ );
49
+ for (const binding of capability.runnerBindings)
50
+ if (binding.mountSource) roots.push(binding.mountSource);
51
+ }
52
+ return runWorkspaceIo<string>({
53
+ root: dependencyOwner,
54
+ artifactRoot: dependencyOwner,
55
+ operation: "verificationIdentity",
56
+ args: [roots],
57
+ signal,
58
+ });
59
+ }
60
+
61
+ const CACHES = [".vite", ".vite-temp"];
62
+ function within(root: string, candidate: string): boolean {
63
+ const relative = path.relative(root, candidate);
64
+ return (
65
+ relative !== ".." &&
66
+ !relative.startsWith(`..${path.sep}`) &&
67
+ !path.isAbsolute(relative)
68
+ );
69
+ }
70
+
71
+ /** A disposable dependency view: immutable packages and independently writable caches. */
72
+ export function prepareVerificationEnvironment(
73
+ root: string,
74
+ dependencyOwner: string,
75
+ runnerBindings: readonly VerificationRunnerBinding[],
76
+ ) {
77
+ const privateRoot = mkdtempSync(
78
+ path.join(path.dirname(root), ".cadence-verification-"),
79
+ );
80
+ const cleanup = () => rmSync(privateRoot, { recursive: true, force: true });
81
+ try {
82
+ const mounts: IsolationMount[] = [
83
+ { source: privateRoot, target: "/cadence", writable: true },
84
+ ];
85
+ for (const directory of [
86
+ "home",
87
+ "cache",
88
+ "tmp",
89
+ "reports",
90
+ "dependencies",
91
+ ]) {
92
+ mkdirSync(path.join(privateRoot, directory), { mode: 0o700 });
93
+ }
94
+ writeFileSync(path.join(privateRoot, "home/user.npmrc"), "");
95
+ writeFileSync(path.join(privateRoot, "home/global.npmrc"), "");
96
+ const dependencies = path.join(dependencyOwner, "node_modules");
97
+ const stat = lstatSync(dependencies, { throwIfNoEntry: false });
98
+ if (stat) {
99
+ const target = path.join(root, "node_modules");
100
+ const targetStat = lstatSync(target, { throwIfNoEntry: false });
101
+ if (
102
+ !stat.isDirectory() ||
103
+ stat.isSymbolicLink() ||
104
+ (targetStat &&
105
+ (!targetStat.isDirectory() || targetStat.isSymbolicLink()))
106
+ ) {
107
+ throw new Error("dependency-path-unsafe");
108
+ }
109
+ if (!targetStat) mkdirSync(target, { mode: 0o700 });
110
+ const view = path.join(privateRoot, "dependencies");
111
+ mounts.push({ source: view, target: "/workspace/node_modules" });
112
+ const entries = readdirSync(dependencies);
113
+ if (entries.length > 20_000) throw new Error("dependency-view-too-large");
114
+ const canonical = realpathSync(dependencies);
115
+ for (const name of entries) {
116
+ if (CACHES.includes(name)) continue;
117
+ const source = path.join(dependencies, name);
118
+ const placeholder = path.join(view, name);
119
+ const entry = lstatSync(source);
120
+ if (entry.isSymbolicLink()) {
121
+ const link = readlinkSync(source);
122
+ if (
123
+ path.isAbsolute(link) ||
124
+ !within(canonical, realpathSync(source))
125
+ ) {
126
+ throw new Error("dependency-path-unsafe");
127
+ }
128
+ symlinkSync(link, placeholder);
129
+ } else {
130
+ if (entry.isDirectory()) mkdirSync(placeholder);
131
+ else if (entry.isFile()) writeFileSync(placeholder, "");
132
+ else throw new Error("dependency-path-unsafe");
133
+ mounts.push({ source, target: `/workspace/node_modules/${name}` });
134
+ }
135
+ }
136
+ for (const cache of CACHES) {
137
+ mkdirSync(path.join(view, cache));
138
+ const source = path.join(privateRoot, "cache", cache);
139
+ mkdirSync(source);
140
+ mounts.push({
141
+ source,
142
+ target: `/workspace/node_modules/${cache}`,
143
+ writable: true,
144
+ });
145
+ }
146
+ }
147
+ const sources = new Map<string, string>();
148
+ const bindings = runnerBindings.map((binding) => {
149
+ if (!binding.mountSource) return { ...binding };
150
+ const source = path.resolve(binding.mountSource);
151
+ const stat = lstatSync(source);
152
+ const executable = realpathSync(binding.executablePath);
153
+ if (
154
+ !stat.isDirectory() ||
155
+ stat.isSymbolicLink() ||
156
+ !within(source, executable)
157
+ ) {
158
+ throw new Error("runner-mount-unavailable");
159
+ }
160
+ let target = sources.get(source);
161
+ if (!target) {
162
+ target = `/cadence-runners/${sources.size}`;
163
+ sources.set(source, target);
164
+ mounts.push({ source, target });
165
+ }
166
+ const translate = (file: string) =>
167
+ path.posix.join(target, ...path.relative(source, file).split(path.sep));
168
+ return {
169
+ ...binding,
170
+ executablePath: translate(executable),
171
+ ...(binding.fixedArgs
172
+ ? {
173
+ fixedArgs: binding.fixedArgs.map((arg) =>
174
+ path.isAbsolute(arg) && within(source, arg)
175
+ ? translate(arg)
176
+ : arg,
177
+ ),
178
+ }
179
+ : {}),
180
+ };
181
+ });
182
+ return {
183
+ privateRoot,
184
+ mounts,
185
+ bindings,
186
+ cleanup,
187
+ environment: {
188
+ CI: "1",
189
+ HOME: "/cadence/home",
190
+ XDG_CACHE_HOME: "/cadence/cache",
191
+ TMPDIR: "/cadence/tmp",
192
+ npm_config_cache: "/cadence/cache/npm",
193
+ npm_config_userconfig: "/cadence/home/user.npmrc",
194
+ npm_config_globalconfig: "/cadence/home/global.npmrc",
195
+ PATH: [
196
+ ...new Set(
197
+ // Honor explicitly resolved private runtimes before system fallbacks.
198
+ [
199
+ ...bindings.filter((binding) => binding.mountSource),
200
+ ...bindings.filter((binding) => !binding.mountSource),
201
+ ].map((binding) => path.posix.dirname(binding.executablePath)),
202
+ ),
203
+ "/usr/local/bin",
204
+ "/usr/bin",
205
+ "/bin",
206
+ ].join(":"),
207
+ },
208
+ };
209
+ } catch (error) {
210
+ cleanup();
211
+ throw error;
212
+ }
213
+ }
@@ -0,0 +1,77 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ constants,
5
+ fstatSync,
6
+ lstatSync,
7
+ openSync,
8
+ readdirSync,
9
+ readlinkSync,
10
+ readSync,
11
+ } from "node:fs";
12
+ import path from "node:path";
13
+
14
+ /** Content identity, including executable modes and links. Never follows dependency links. */
15
+ export function verificationEnvironmentDigest(
16
+ roots: readonly string[],
17
+ checkCancelled: () => void = () => {},
18
+ ) {
19
+ const digest = createHash("sha256");
20
+ let entries = 0;
21
+ let bytes = 0;
22
+ const started = Date.now();
23
+ const buffer = Buffer.alloc(256 * 1024);
24
+ const visit = (file: string, relative: string, dependencyRoot: boolean) => {
25
+ checkCancelled();
26
+ if (++entries > 500_000 || Date.now() - started > 60_000)
27
+ throw new Error("verification-environment-limit");
28
+ const stat = lstatSync(file, { throwIfNoEntry: false });
29
+ digest.update(JSON.stringify([relative, stat ? stat.mode & 0o777 : null]));
30
+ if (!stat) {
31
+ digest.update("absent");
32
+ return;
33
+ }
34
+ if (stat.isSymbolicLink()) {
35
+ digest.update(JSON.stringify(["link", readlinkSync(file)]));
36
+ return;
37
+ }
38
+ if (stat.isDirectory()) {
39
+ digest.update("directory");
40
+ for (const name of readdirSync(file).sort()) {
41
+ if (dependencyRoot && [".vite", ".vite-temp"].includes(name)) continue;
42
+ visit(path.join(file, name), `${relative}/${name}`, false);
43
+ }
44
+ return;
45
+ }
46
+ if (!stat.isFile()) throw new Error("verification-environment-unsafe");
47
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
48
+ try {
49
+ const before = fstatSync(fd);
50
+ if (before.ino !== stat.ino || before.dev !== stat.dev)
51
+ throw new Error("verification-environment-changed");
52
+ const content = createHash("sha256");
53
+ for (;;) {
54
+ const count = readSync(fd, buffer, 0, buffer.length, null);
55
+ if (count === 0) break;
56
+ checkCancelled();
57
+ bytes += count;
58
+ if (bytes > 16 * 1024 ** 3 || Date.now() - started > 60_000)
59
+ throw new Error("verification-environment-limit");
60
+ content.update(buffer.subarray(0, count));
61
+ }
62
+ const after = fstatSync(fd);
63
+ if (
64
+ before.size !== after.size ||
65
+ before.mtimeMs !== after.mtimeMs ||
66
+ before.ctimeMs !== after.ctimeMs
67
+ )
68
+ throw new Error("verification-environment-changed");
69
+ digest.update(JSON.stringify(["file", content.digest("hex")]));
70
+ } finally {
71
+ closeSync(fd);
72
+ }
73
+ };
74
+ for (const root of [...new Set(roots)].sort())
75
+ visit(root, root, path.basename(root) === "node_modules");
76
+ return digest.digest("hex");
77
+ }
@@ -21,6 +21,8 @@ export interface RouteRequirements {
21
21
  dialects?: RouteDialect[];
22
22
  minContextWindow?: number;
23
23
  minOutputTokens?: number;
24
+ preferredContextWindow?: number;
25
+ preferredOutputTokens?: number;
24
26
  }
25
27
 
26
28
  interface RouteHealth {
@@ -56,6 +58,7 @@ export interface WorkerBrokerOptions {
56
58
 
57
59
  export interface RunWorkerAttemptInput<T> {
58
60
  runId: string;
61
+ taskId?: string;
59
62
  operationId: string;
60
63
  role: WorkerRole;
61
64
  requirements?: RouteRequirements;
@@ -126,7 +129,13 @@ function validRequirements(value: unknown): value is RouteRequirements {
126
129
  if (
127
130
  Object.keys(candidate).some(
128
131
  (key) =>
129
- !["dialects", "minContextWindow", "minOutputTokens"].includes(key),
132
+ ![
133
+ "dialects",
134
+ "minContextWindow",
135
+ "minOutputTokens",
136
+ "preferredContextWindow",
137
+ "preferredOutputTokens",
138
+ ].includes(key),
130
139
  )
131
140
  ) {
132
141
  return false;
@@ -147,7 +156,12 @@ function validRequirements(value: unknown): value is RouteRequirements {
147
156
  (candidate.minContextWindow as number) > 0)) &&
148
157
  (candidate.minOutputTokens === undefined ||
149
158
  (Number.isSafeInteger(candidate.minOutputTokens) &&
150
- (candidate.minOutputTokens as number) > 0))
159
+ (candidate.minOutputTokens as number) > 0)) &&
160
+ [candidate.preferredContextWindow, candidate.preferredOutputTokens].every(
161
+ (value) =>
162
+ value === undefined ||
163
+ (Number.isSafeInteger(value) && (value as number) > 0),
164
+ )
151
165
  );
152
166
  }
153
167
 
@@ -176,7 +190,6 @@ export class WorkerBroker {
176
190
  readonly #policy: RoutePolicy;
177
191
  readonly #now: () => number;
178
192
  readonly #healthStore: RouteHealthStore;
179
- readonly #health = new Map<string, RouteHealth>();
180
193
 
181
194
  constructor(policy: RoutePolicy, options: WorkerBrokerOptions = {}) {
182
195
  this.#policy = structuredClone(policy);
@@ -187,7 +200,6 @@ export class WorkerBroker {
187
200
  #routeHealth(routeId: string): RouteHealth {
188
201
  const route = this.#policy.routes[routeId];
189
202
  return (
190
- this.#health.get(routeId) ??
191
203
  (route ? this.#healthStore.get(route.fingerprint) : undefined) ?? {
192
204
  state: "healthy",
193
205
  }
@@ -197,7 +209,6 @@ export class WorkerBroker {
197
209
  #setRouteHealth(routeId: string, health: RouteHealth): void {
198
210
  const route = this.#policy.routes[routeId];
199
211
  if (!route) throw new Error("route-not-declared");
200
- this.#health.set(routeId, structuredClone(health));
201
212
  this.#healthStore.set(route.fingerprint, health);
202
213
  }
203
214
 
@@ -229,14 +240,13 @@ export class WorkerBroker {
229
240
  });
230
241
  }
231
242
 
232
- select(input: {
233
- role: WorkerRole;
234
- dialects?: RouteDialect[];
235
- minContextWindow?: number;
236
- minOutputTokens?: number;
237
- exclude?: readonly string[];
238
- routeId?: string;
239
- }):
243
+ select(
244
+ input: RouteRequirements & {
245
+ role: WorkerRole;
246
+ exclude?: readonly string[];
247
+ routeId?: string;
248
+ },
249
+ ):
240
250
  | { ok: true; route: WorkerRoutePolicy; health: RouteHealthState }
241
251
  | { ok: false; code: "endpoint-unavailable" | "route-not-declared" } {
242
252
  const requirements: RouteRequirements = {
@@ -247,6 +257,12 @@ export class WorkerBroker {
247
257
  ...(input.minOutputTokens === undefined
248
258
  ? {}
249
259
  : { minOutputTokens: input.minOutputTokens }),
260
+ ...(input.preferredContextWindow === undefined
261
+ ? {}
262
+ : { preferredContextWindow: input.preferredContextWindow }),
263
+ ...(input.preferredOutputTokens === undefined
264
+ ? {}
265
+ : { preferredOutputTokens: input.preferredOutputTokens }),
250
266
  };
251
267
  if (!isRole(input.role) || !validRequirements(requirements)) {
252
268
  return { ok: false, code: "endpoint-unavailable" };
@@ -261,7 +277,22 @@ export class WorkerBroker {
261
277
  return { ok: false, code: "route-not-declared" };
262
278
  }
263
279
  const excluded = new Set(input.exclude ?? []);
264
- for (const routeId of candidates) {
280
+ // Stable preference tiers preserve declared order within each tier. Health,
281
+ // hard minima and explicit pins still determine eligibility.
282
+ const preferred = (routeId: string) => {
283
+ const route = this.#policy.routes[routeId];
284
+ return route &&
285
+ route.capabilities.contextWindow >=
286
+ (requirements.preferredContextWindow ?? 0) &&
287
+ route.capabilities.maxTokens >=
288
+ (requirements.preferredOutputTokens ?? 0)
289
+ ? 1
290
+ : 0;
291
+ };
292
+ const ordered = [...candidates].sort(
293
+ (left, right) => preferred(right) - preferred(left),
294
+ );
295
+ for (const routeId of ordered) {
265
296
  if (excluded.has(routeId)) continue;
266
297
  const route = this.#policy.routes[routeId];
267
298
  if (!route || !capable(route, input.role, requirements)) continue;
@@ -606,7 +637,8 @@ export class WorkerBroker {
606
637
  }
607
638
 
608
639
  /**
609
- * Adds stable run/role route bindings to the stateless route-attempt broker.
640
+ * Adds run/role defaults and independent task bindings to route attempts.
641
+ * All bound brokers consult the same health store.
610
642
  * Attempt credentials and provider objects remain disposable and are never
611
643
  * retained by this facade.
612
644
  */
@@ -622,16 +654,21 @@ export class RunWorkerBroker {
622
654
 
623
655
  constructor(policy: RoutePolicy, options: WorkerBrokerOptions = {}) {
624
656
  this.#policy = structuredClone(policy);
625
- this.#options = options;
626
- this.#defaultBroker = new WorkerBroker(this.#policy, options);
657
+ this.#options = {
658
+ ...options,
659
+ healthStore: options.healthStore ?? new MemoryRouteHealthStore(),
660
+ };
661
+ this.#defaultBroker = new WorkerBroker(this.#policy, this.#options);
627
662
  }
628
663
 
629
- #bindingKey(runId: string, role: WorkerRole): string {
630
- return `${runId}\0${role}`;
664
+ #bindingKey(runId: string, role: WorkerRole, taskId?: string): string {
665
+ return JSON.stringify([runId, role, taskId ?? null]);
631
666
  }
632
667
 
633
- #brokerFor(runId: string, role: WorkerRole): WorkerBroker {
634
- const binding = this.#bindings.get(this.#bindingKey(runId, role));
668
+ #brokerFor(runId: string, role: WorkerRole, taskId?: string): WorkerBroker {
669
+ const binding =
670
+ this.#bindings.get(this.#bindingKey(runId, role, taskId)) ??
671
+ this.#bindings.get(this.#bindingKey(runId, role));
635
672
  if (!binding) return this.#defaultBroker;
636
673
  const routeId = binding.routeId;
637
674
  const key = `${role}\0${routeId}`;
@@ -657,6 +694,8 @@ export class RunWorkerBroker {
657
694
  if (
658
695
  typeof input.runId !== "string" ||
659
696
  input.runId.length === 0 ||
697
+ (input.taskId !== undefined &&
698
+ (typeof input.taskId !== "string" || input.taskId.length === 0)) ||
660
699
  typeof input.operationId !== "string" ||
661
700
  input.operationId.length === 0
662
701
  ) {
@@ -667,7 +706,11 @@ export class RunWorkerBroker {
667
706
  attempts: [],
668
707
  };
669
708
  }
670
- const result = await this.#brokerFor(input.runId, input.role).run({
709
+ const result = await this.#brokerFor(
710
+ input.runId,
711
+ input.role,
712
+ input.taskId,
713
+ ).run({
671
714
  operationId: input.operationId,
672
715
  role: input.role,
673
716
  ...(input.requirements ? { requirements: input.requirements } : {}),
@@ -679,39 +722,54 @@ export class RunWorkerBroker {
679
722
  if (result.ok) {
680
723
  const route = this.#policy.routes[result.routeId];
681
724
  if (!route) throw new Error("broker-route-result-invalid");
682
- this.#bindings.set(this.#bindingKey(input.runId, input.role), {
683
- routeId: route.id,
684
- fingerprint: route.fingerprint,
685
- });
725
+ this.#bindings.set(
726
+ this.#bindingKey(input.runId, input.role, input.taskId),
727
+ {
728
+ routeId: route.id,
729
+ fingerprint: route.fingerprint,
730
+ },
731
+ );
686
732
  }
687
733
  return result;
688
734
  }
689
735
 
690
736
  rebind(input: {
691
737
  runId: string;
738
+ taskId?: string;
692
739
  role: WorkerRole;
693
740
  routeId: string;
694
741
  requirements?: RouteRequirements;
695
742
  }) {
696
- const result = this.#defaultBroker.rebind(input);
743
+ const { taskId, ...routeInput } = input;
744
+ if (
745
+ taskId !== undefined &&
746
+ (typeof taskId !== "string" || taskId.length === 0)
747
+ ) {
748
+ return { ok: false as const, code: "approval-boundary" as const };
749
+ }
750
+ const result = this.#defaultBroker.rebind(routeInput);
697
751
  if (result.ok) {
698
- this.#bindings.set(this.#bindingKey(input.runId, input.role), {
699
- routeId: result.route.id,
700
- fingerprint: result.route.fingerprint,
701
- });
752
+ this.#bindings.set(
753
+ this.#bindingKey(input.runId, input.role, input.taskId),
754
+ {
755
+ routeId: result.route.id,
756
+ fingerprint: result.route.fingerprint,
757
+ },
758
+ );
702
759
  }
703
760
  return result;
704
761
  }
705
762
 
706
763
  resumeBinding(input: {
707
764
  runId: string;
765
+ taskId?: string;
708
766
  role: WorkerRole;
709
767
  routeId: string;
710
768
  expectedFingerprint?: string;
711
769
  requirements?: RouteRequirements;
712
770
  }) {
713
771
  const binding = this.#bindings.get(
714
- this.#bindingKey(input.runId, input.role),
772
+ this.#bindingKey(input.runId, input.role, input.taskId),
715
773
  );
716
774
  const route = this.#policy.routes[input.routeId];
717
775
  if (!binding) {
@@ -724,6 +782,7 @@ export class RunWorkerBroker {
724
782
  }
725
783
  return this.rebind({
726
784
  runId: input.runId,
785
+ ...(input.taskId === undefined ? {} : { taskId: input.taskId }),
727
786
  role: input.role,
728
787
  routeId: input.routeId,
729
788
  ...(input.requirements ? { requirements: input.requirements } : {}),
@@ -751,8 +810,12 @@ export class RunWorkerBroker {
751
810
  this.#boundBrokers.clear();
752
811
  }
753
812
 
754
- binding(runId: string, role: WorkerRole): string | undefined {
755
- return this.#bindings.get(this.#bindingKey(runId, role))?.routeId;
813
+ binding(
814
+ runId: string,
815
+ role: WorkerRole,
816
+ taskId?: string,
817
+ ): string | undefined {
818
+ return this.#bindings.get(this.#bindingKey(runId, role, taskId))?.routeId;
756
819
  }
757
820
 
758
821
  status(): Record<string, unknown> {