@hassangameryt/freeflow 0.7.2 → 0.7.3

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 (79) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/.cursor-plugin/plugin.json +1 -1
  5. package/README.md +147 -127
  6. package/capabilities/cognitive-routing/SKILL.md +162 -93
  7. package/gemini-extension.json +1 -1
  8. package/package.json +1 -1
  9. package/pi-extension/dist/cognitive-routing-v2/config.js +35 -19
  10. package/pi-extension/dist/cognitive-routing-v2/projection.js +53 -44
  11. package/pi-extension/dist/cognitive-routing-v2/render.js +31 -10
  12. package/pi-extension/dist/cognitive-routing-v2/runtime.js +701 -105
  13. package/pi-extension/dist/cognitive-routing-v2/schemas.js +90 -24
  14. package/pi-extension/dist/cognitive-routing-v2/state.js +243 -27
  15. package/pi-extension/dist/cognitive-routing-v2/tools.js +19 -50
  16. package/pi-extension/dist/cognitive-routing-v2/types.js +5 -1
  17. package/pi-extension/dist/context-virtualization/runtime.js +29 -7
  18. package/pi-extension/dist/freeflow-context/tool.js +7 -6
  19. package/pi-extension/dist/index.js +36 -15
  20. package/pi-extension/dist/provider-support/astra/adapter.js +110 -0
  21. package/pi-extension/dist/provider-support/astra/history.js +125 -0
  22. package/pi-extension/dist/provider-support/astra/index.js +11 -0
  23. package/pi-extension/dist/provider-support/astra/session-state.js +114 -0
  24. package/pi-extension/dist/provider-support/index.js +4 -0
  25. package/pi-extension/dist/runtime/request-history.js +216 -0
  26. package/pi-extension/dist/runtime/runtime-context.js +29 -10
  27. package/pi-extension/dist/session-sources/events.js +22 -0
  28. package/pi-extension/dist/session-sources/provenance.js +32 -0
  29. package/pi-extension/dist/session-sources/sources.js +51 -7
  30. package/pi-extension/dist/settings/settings-ui.js +247 -47
  31. package/pi-extension/src/cognitive-routing-v2/config.ts +49 -19
  32. package/pi-extension/src/cognitive-routing-v2/projection.ts +53 -43
  33. package/pi-extension/src/cognitive-routing-v2/render.ts +31 -10
  34. package/pi-extension/src/cognitive-routing-v2/runtime.ts +712 -105
  35. package/pi-extension/src/cognitive-routing-v2/schemas.ts +95 -24
  36. package/pi-extension/src/cognitive-routing-v2/state.ts +244 -27
  37. package/pi-extension/src/cognitive-routing-v2/tools.ts +19 -50
  38. package/pi-extension/src/cognitive-routing-v2/types.ts +35 -3
  39. package/pi-extension/src/context-virtualization/runtime.ts +30 -8
  40. package/pi-extension/src/freeflow-context/tool.ts +7 -6
  41. package/pi-extension/src/index.ts +36 -14
  42. package/pi-extension/src/provider-support/astra/adapter.ts +108 -0
  43. package/pi-extension/src/provider-support/astra/history.ts +145 -0
  44. package/pi-extension/src/provider-support/astra/index.ts +12 -0
  45. package/pi-extension/src/provider-support/astra/session-state.ts +113 -0
  46. package/pi-extension/src/provider-support/index.ts +5 -0
  47. package/pi-extension/src/runtime/request-history.ts +243 -0
  48. package/pi-extension/src/runtime/runtime-context.ts +32 -10
  49. package/pi-extension/src/session-sources/events.ts +22 -0
  50. package/pi-extension/src/session-sources/provenance.ts +39 -0
  51. package/pi-extension/src/session-sources/sources.ts +77 -6
  52. package/pi-extension/src/settings/settings-ui.ts +276 -46
  53. package/pi-extension/tests/cache-reuse/history.test.js +117 -0
  54. package/pi-extension/tests/cache-reuse/native.test.js +91 -0
  55. package/pi-extension/tests/cache-reuse/settings.test.js +211 -0
  56. package/pi-extension/tests/cognitive-routing-v2/communication.test.js +94 -1
  57. package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
  58. package/pi-extension/tests/cognitive-routing-v2/native.test.js +34 -1
  59. package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +55 -0
  60. package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +800 -2
  61. package/pi-extension/tests/cognitive-routing-v2/render.test.js +15 -1
  62. package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +273 -3
  63. package/pi-extension/tests/cognitive-routing-v2/state.test.js +312 -1
  64. package/pi-extension/tests/context-virtualization/extension.test.js +20 -10
  65. package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
  66. package/pi-extension/tests/fixtures/routing-native.js +4 -1
  67. package/pi-extension/tests/integration/mode-free-core.test.js +4 -3
  68. package/pi-extension/tests/integration/pi-extension.test.js +508 -29
  69. package/pi-extension/tests/integration/prompt-architecture.test.js +56 -40
  70. package/pi-extension/tests/integration/unaffected-regressions.test.js +3 -3
  71. package/pi-extension/tests/provider-support/astra-boundaries.test.js +75 -0
  72. package/pi-extension/tests/provider-support/astra.test.js +306 -0
  73. package/pi-extension/tests/provider-support/cli.test.js +92 -0
  74. package/pi-extension/tests/provider-support/native.test.js +183 -0
  75. package/plugin.json +1 -1
  76. package/runtime/prompts/cognitive-routing.md +4 -4
  77. package/skills/design-for-depth/SKILL.md +1 -1
  78. package/skills/design-for-depth/references/implementation-decisions.md +3 -3
  79. package/skills/discuss/SKILL.md +2 -2
@@ -1,8 +1,12 @@
1
- import { schemaForProfile } from "./schemas.js";
1
+ import { estimateRequest } from "./budget.js";
2
+ import { annotateSources } from "../session-sources/provenance.js";
3
+ import { ROUTING_SCHEMAS } from "./schemas.js";
2
4
  import { randomUUID } from "node:crypto";
5
+ import { existsSync, realpathSync } from "node:fs";
6
+ import { relative, resolve as resolvePath } from "node:path";
3
7
  import { EventStore } from "../session-sources/events.js";
4
8
  import { Sources, bodyHash, isTaskEvidence } from "../session-sources/sources.js";
5
- import { pairFromProfile } from "./config.js";
9
+ import { pairFromProfile, resolveCognitiveRoutingState } from "./config.js";
6
10
  import { prepareView, changeSelection, representationProblems } from "./projection.js";
7
11
  import { initialState } from "./state.js";
8
12
  import {
@@ -11,13 +15,16 @@ import {
11
15
  RoutingError,
12
16
  canonical,
13
17
  emptySelection,
18
+ isWorkerProfile,
14
19
  requireCondition as check,
15
20
  samePair,
21
+ workersForDelegation,
16
22
  } from "./types.js";
17
23
  export const ROUTING_TOOLS = ["freeflow_delegate", "freeflow_return", "freeflow_unit", "freeflow_project"];
18
24
  const HANDOFF_TOOLS = new Set(["freeflow_delegate", "freeflow_return"]);
19
25
  export class RoutingRuntime {
20
26
  pi;
27
+ packageRoots;
21
28
  store;
22
29
  ctx;
23
30
  capability;
@@ -37,8 +44,9 @@ export class RoutingRuntime {
37
44
  manualHold;
38
45
  automaticControl = false;
39
46
  sourceCache;
40
- constructor(pi) {
47
+ constructor(pi, packageRoots = []) {
41
48
  this.pi = pi;
49
+ this.packageRoots = packageRoots;
42
50
  }
43
51
  subject() {
44
52
  const branch = this.store?.reader.getBranch() ?? [];
@@ -70,6 +78,29 @@ export class RoutingRuntime {
70
78
  ? state.resumeBasis.get(assignmentId)
71
79
  : state.assignments.get(assignmentId)?.basisUserEntryId;
72
80
  }
81
+ enabledWorkers() {
82
+ return workersForDelegation(this.capability?.delegation ?? "executor");
83
+ }
84
+ assignedWorker(state, assignmentId = state.assignmentId) {
85
+ const assignment = assignmentId ? state.assignments.get(assignmentId) : undefined;
86
+ const handoff = assignment ? state.handoffs.get(assignment.delegateHandoffId) : undefined;
87
+ check(
88
+ handoff?.kind === "delegate" && handoff.assignmentId === assignment?.id && isWorkerProfile(handoff.to),
89
+ "assignment_worker_missing",
90
+ "The current assignment has no valid recorded worker.",
91
+ );
92
+ return handoff.to;
93
+ }
94
+ requiredProfiles(state = this.stateData()) {
95
+ const profiles = new Set(["coordinator", ...this.enabledWorkers()]);
96
+ if (state.profile) profiles.add(state.profile);
97
+ if (this.manualHold) profiles.add(this.manualHold);
98
+ const assignment = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
99
+ if (assignment?.state === "outstanding" || state.recoveryId) profiles.add(this.assignedWorker(state));
100
+ const pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
101
+ if (pending && isWorkerProfile(pending.to)) profiles.add(pending.to);
102
+ return [...profiles];
103
+ }
73
104
  retireUnbound(reason) {
74
105
  for (const execution of this.stateData().executions.values()) {
75
106
  if (!execution.assistantEntryId && !execution.interrupted)
@@ -86,6 +117,7 @@ export class RoutingRuntime {
86
117
  id: t.id,
87
118
  profile: t.profile,
88
119
  assignmentId: t.assignmentId,
120
+ recoveryId: t.recoveryId,
89
121
  basisUserEntryId: t.basisUserEntryId,
90
122
  pair: t.pair,
91
123
  resultEntryIds: [],
@@ -101,7 +133,7 @@ export class RoutingRuntime {
101
133
  async beforeRun(ctx) {
102
134
  this.ctx = ctx;
103
135
  if (!this.store || !this.supported() || this.store.blocked || this.error) return;
104
- if (this.stateData().control === "automatic" && this.stateData().profile === "executor") {
136
+ if (this.stateData().control === "automatic" && isWorkerProfile(this.stateData().profile)) {
105
137
  const result = await this.control("coordinator", false);
106
138
  check(result.status !== "blocked", "reconciliation_required", result.reason);
107
139
  }
@@ -147,6 +179,7 @@ export class RoutingRuntime {
147
179
  return {
148
180
  effective: this.supported() && state.control !== "inactive" && !blocked,
149
181
  activeProfile: state.profile,
182
+ delegation: this.capability?.delegation ?? "executor",
150
183
  controlMode: state.control === "manual" ? `manual-${state.profile}` : state.control,
151
184
  runtimeStatus: blocked ? "blocked" : this.supported() && state.control !== "inactive" ? "active" : "inactive",
152
185
  runtimeReason: blocked ?? (this.suppressed ? "startup_selection" : this.capability?.blockingReason.message),
@@ -189,11 +222,36 @@ export class RoutingRuntime {
189
222
  if (profile) check(state.profile === profile && this.turn?.profile === profile, "wrong_profile");
190
223
  if (state.profile) check(samePair(this.observed(), this.profilePair(state.profile)), "configuration_mismatch");
191
224
  }
192
- profilePair(profile) {
225
+ configuredProfilePair(profile) {
193
226
  const configured = this.capability?.profiles[profile];
194
227
  check(configured, "profile_missing");
195
228
  return pairFromProfile(configured);
196
229
  }
230
+ profilePair(profile) {
231
+ return this.stateData().profileOverrides.get(profile) ?? this.configuredProfilePair(profile);
232
+ }
233
+ profilePairs(profiles = this.requiredProfiles()) {
234
+ return Object.fromEntries(profiles.map((profile) => [profile, this.profilePair(profile)]));
235
+ }
236
+ async validateProfilePairs(pairs) {
237
+ check(pairs.coordinator, "profile_missing", "Configure the coordinator profile.");
238
+ const helper = pairs.helper !== undefined;
239
+ const executor = pairs.executor !== undefined;
240
+ check(helper || executor, "profile_missing", "Configure an enabled worker profile.");
241
+ const delegation = helper && executor ? "both" : helper ? "helper" : "executor";
242
+ const profiles = Object.fromEntries(
243
+ Object.entries(pairs).map(([profile, pair]) => [
244
+ profile,
245
+ { provider: pair.provider, model: pair.modelId, thinking: pair.thinking },
246
+ ]),
247
+ );
248
+ const result = await resolveCognitiveRoutingState(
249
+ { cognitiveRouting: { enabled: true, delegation, profiles } },
250
+ {},
251
+ this.ctx,
252
+ );
253
+ check(result.effective, result.blockingReason.code, result.blockingReason.message);
254
+ }
197
255
  model(profile) {
198
256
  const pair = this.profilePair(profile);
199
257
  const model = this.ctx?.modelRegistry?.find(pair.provider, pair.modelId);
@@ -205,6 +263,8 @@ export class RoutingRuntime {
205
263
  this.ctx = ctx;
206
264
  this.capability = capability;
207
265
  this.turn = undefined;
266
+ this.manualHold = undefined;
267
+ this.automaticControl = false;
208
268
  this.messages = [];
209
269
  this.sourceCache = undefined;
210
270
  this.receipts.clear();
@@ -214,7 +274,7 @@ export class RoutingRuntime {
214
274
  this.error = undefined;
215
275
  this.projectionError = undefined;
216
276
  this.operations = Promise.resolve();
217
- this.targetSignature = canonical(capability.profiles);
277
+ this.targetSignature = canonical({ delegation: capability.delegation, profiles: capability.profiles });
218
278
  this.suppressed = startup && process.argv.some((a) => /^(--model|--thinking)(=|$)/.test(a));
219
279
  this.store = new EventStore(this.pi, ctx.sessionManager);
220
280
  const subject = this.subject();
@@ -225,6 +285,7 @@ export class RoutingRuntime {
225
285
  const state = this.stateData();
226
286
  this.manualHold = state.control === "manual" ? state.profile : undefined;
227
287
  this.automaticControl = !state.events.size || state.control === "automatic";
288
+ await this.validateProfilePairs(this.profilePairs(this.requiredProfiles(state)));
228
289
  this.retireUnbound("Session rebind found an unfinished execution; no task effects replayed.");
229
290
  if (!state.events.size) {
230
291
  this.append({
@@ -265,7 +326,7 @@ export class RoutingRuntime {
265
326
  async refresh(ctx, capability) {
266
327
  this.ctx = ctx;
267
328
  const was = this.capability?.effective === true;
268
- const signature = canonical(capability.profiles);
329
+ const signature = canonical({ delegation: capability.delegation, profiles: capability.profiles });
269
330
  if (this.capability && (was !== capability.effective || signature !== this.targetSignature)) this.revision++;
270
331
  this.capability = capability;
271
332
  if (!capability.effective) {
@@ -297,7 +358,8 @@ export class RoutingRuntime {
297
358
  } else if (this.targetSignature && signature !== this.targetSignature) {
298
359
  this.error = "configuration_changed: reconcile profile configuration before automatic execution";
299
360
  }
300
- if (canonical(this.capability?.profiles) === signature) this.targetSignature = signature;
361
+ if (canonical({ delegation: this.capability?.delegation, profiles: this.capability?.profiles }) === signature)
362
+ this.targetSignature = signature;
301
363
  }
302
364
  async ancestryChanged(ctx, navigation = true) {
303
365
  this.revision++;
@@ -313,8 +375,9 @@ export class RoutingRuntime {
313
375
  await this.store.reconcile();
314
376
  this.guard(subject);
315
377
  this.error = undefined;
316
- this.retireUnbound("Selected historical ancestry ends before execution binding; effects are not replayed.");
317
378
  const state = this.stateData();
379
+ await this.validateProfilePairs(this.profilePairs(this.requiredProfiles(state)));
380
+ this.retireUnbound("Selected historical ancestry ends before execution binding; effects are not replayed.");
318
381
  if (this.manualHold) {
319
382
  this.append({
320
383
  type: "control",
@@ -322,6 +385,7 @@ export class RoutingRuntime {
322
385
  profile: this.manualHold,
323
386
  reason: "Current explicit manual hold survives navigation",
324
387
  });
388
+ await this.applyPair(this.profilePair(this.manualHold));
325
389
  } else if (
326
390
  (this.automaticControl || !state.events.size) &&
327
391
  (navigation || !samePair(this.observed(), this.profilePair(state.profile ?? "coordinator")))
@@ -403,6 +467,12 @@ export class RoutingRuntime {
403
467
  return this.enqueue(async () => {
404
468
  try {
405
469
  check(this.capability?.effective && this.store, "routing_unavailable");
470
+ if (manual && isWorkerProfile(profile))
471
+ check(
472
+ this.enabledWorkers().includes(profile),
473
+ "worker_disabled",
474
+ `${profile} is not enabled by delegation mode.`,
475
+ );
406
476
  await this.store.reconcile();
407
477
  this.guard(subject);
408
478
  this.error = undefined;
@@ -440,7 +510,8 @@ export class RoutingRuntime {
440
510
  this.automaticControl = !manual;
441
511
  return { status: manual ? "active" : "automatic" };
442
512
  } catch (error) {
443
- if (this.current(subject)) this.mark(error);
513
+ if (this.current(subject) && !(error instanceof RoutingError && error.code === "worker_disabled"))
514
+ this.mark(error);
444
515
  return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
445
516
  }
446
517
  });
@@ -448,6 +519,118 @@ export class RoutingRuntime {
448
519
  setManualProfile(profile, _mechanism) {
449
520
  return this.control(profile, true);
450
521
  }
522
+ sessionProfileOverrides() {
523
+ return Object.fromEntries(this.stateData().profileOverrides);
524
+ }
525
+ async setSessionProfileOverride(profile, override, mechanism = "Session profile override") {
526
+ this.revision++;
527
+ const subject = this.subject();
528
+ return this.enqueue(async () => {
529
+ try {
530
+ check(this.capability?.effective && this.store, "routing_unavailable");
531
+ check(
532
+ this.ctx?.isIdle?.() !== false,
533
+ "host_busy",
534
+ "Wait for Pi to become idle before changing session presets.",
535
+ );
536
+ await this.store.reconcile();
537
+ this.guard(subject);
538
+ const state = this.stateData();
539
+ const previous = state.profileOverrides.get(profile);
540
+ if ((override === null && !previous) || (override !== null && samePair(previous, override)))
541
+ return { status: "unchanged" };
542
+ const target = override ?? this.configuredProfilePair(profile);
543
+ const profiles = new Set(this.requiredProfiles(state));
544
+ profiles.add(profile);
545
+ const pairs = this.profilePairs([...profiles]);
546
+ pairs[profile] = target;
547
+ await this.validateProfilePairs(pairs);
548
+ this.append({
549
+ type: "profile-overrides",
550
+ overrides: { [profile]: override },
551
+ reason: mechanism,
552
+ });
553
+ if (state.control !== "inactive" && state.profile === profile) {
554
+ try {
555
+ await this.applyPair(target);
556
+ this.guard(subject);
557
+ } catch (error) {
558
+ if (this.current(subject) && !this.externalChange && !this.error && !this.store.blocked) {
559
+ this.append({
560
+ type: "profile-overrides",
561
+ overrides: { [profile]: previous ?? null },
562
+ reason: "Session profile application failed; prior override retained",
563
+ });
564
+ }
565
+ throw error;
566
+ }
567
+ this.error = undefined;
568
+ return { status: "active" };
569
+ }
570
+ this.error = undefined;
571
+ return { status: "stored" };
572
+ } catch (error) {
573
+ if (this.current(subject)) this.mark(error);
574
+ return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
575
+ }
576
+ });
577
+ }
578
+ async resetSessionProfileOverrides(mechanism = "Reset session profile overrides") {
579
+ this.revision++;
580
+ const subject = this.subject();
581
+ return this.enqueue(async () => {
582
+ try {
583
+ check(this.capability?.effective && this.store, "routing_unavailable");
584
+ check(
585
+ this.ctx?.isIdle?.() !== false,
586
+ "host_busy",
587
+ "Wait for Pi to become idle before resetting session presets.",
588
+ );
589
+ await this.store.reconcile();
590
+ this.guard(subject);
591
+ const state = this.stateData();
592
+ if (state.profileOverrides.size === 0) return { status: "unchanged" };
593
+ const previous = Object.fromEntries(state.profileOverrides);
594
+ const required = this.requiredProfiles(state);
595
+ const targetPairs = Object.fromEntries(
596
+ required.map((profile) => [profile, this.configuredProfilePair(profile)]),
597
+ );
598
+ await this.validateProfilePairs(targetPairs);
599
+ this.append({
600
+ type: "profile-overrides",
601
+ overrides: { coordinator: null, helper: null, executor: null },
602
+ reason: mechanism,
603
+ });
604
+ const active = state.control !== "inactive" ? state.profile : undefined;
605
+ if (active && previous[active]) {
606
+ try {
607
+ await this.applyPair(targetPairs[active]);
608
+ this.guard(subject);
609
+ } catch (error) {
610
+ if (this.current(subject) && !this.externalChange && !this.error && !this.store.blocked) {
611
+ this.append({
612
+ type: "profile-overrides",
613
+ overrides: {
614
+ coordinator: previous.coordinator ?? null,
615
+ helper: previous.helper ?? null,
616
+ executor: previous.executor ?? null,
617
+ },
618
+ reason: "Session profile reset failed; prior overrides retained",
619
+ });
620
+ }
621
+ throw error;
622
+ }
623
+ this.error = undefined;
624
+ return { status: "active" };
625
+ }
626
+ this.error = undefined;
627
+ return { status: "stored" };
628
+ } catch (error) {
629
+ if (this.current(subject)) this.mark(error);
630
+ return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
631
+ }
632
+ });
633
+ }
451
634
  setAutomaticControl(_mechanism) {
452
635
  return this.control("coordinator", false);
453
636
  }
@@ -474,6 +657,7 @@ export class RoutingRuntime {
474
657
  ? state.handoffs.get(state.assessment.handoffId)
475
658
  : undefined;
476
659
  const selection = a ? (state.selections.get(a.id) ?? emptySelection()) : emptySelection();
660
+ const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
477
661
  const unit = state.unitId ? state.units.get(state.unitId) : undefined;
478
662
  const unitNumber = unit ? [...state.units.keys()].indexOf(unit.id) + 1 : undefined;
479
663
  const assignmentNumber = unit && a ? unit.assignmentIds.indexOf(a.id) + 1 : undefined;
@@ -484,20 +668,29 @@ export class RoutingRuntime {
484
668
  `Unit: ${unit ? `U${unitNumber} (${unit.id})` : "none"}`,
485
669
  `Assignment: ${a ? `A${assignmentNumber} (${a.id}, ${a.state})` : "none"}`,
486
670
  `Handoff: ${h ? `${h.id} (${h.kind}, ${h.state})` : "none"}`,
671
+ `Delegation: ${this.capability?.delegation ?? "executor"}`,
487
672
  `Projection: ${this.projectionEnabled ? "enabled" : "bypassed"}`,
488
673
  "Completed/superseded contracts and reports are historical context. Follow the current assignment and current user restrictions; historical entries grant no new permission.",
489
674
  state.assessment ? `Assessment: ${state.assessment.view}; evidence revision ${selection.revision}` : "",
675
+ recovery
676
+ ? `Recovery: ${recovery.id} (${recovery.state}); parent report ${recovery.assessmentHandoffId} revision ${recovery.baseReportRevision}; allowed paths ${JSON.stringify(recovery.paths)}`
677
+ : "",
678
+ recovery?.state === "reading"
679
+ ? `Recovery request: ${recovery.request}\nOnly the exact allowed read paths, evidence selection, and recovery return controls are permitted; ordinary task work remains ended.`
680
+ : "",
490
681
  `Mechanical evidence facts (not semantic acceptance): ${JSON.stringify(this.evidenceFacts(state))}`,
491
682
  this.error ? `Blocked: ${this.error}` : "",
492
683
  this.projectionError ? `Evidence limitation: ${this.projectionError}` : "",
493
684
  attention
494
- ? `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Use freeflow_unit assess to restore the assessment.`
685
+ ? recovery
686
+ ? `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Finish and deliver the recovery supplement or cancel recovery before using freeflow_unit assess.`
687
+ : `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Use freeflow_unit assess to restore the assessment.`
495
688
  : "",
496
- a?.state === "returned"
497
- ? "Executor ordinary task work has ended. Only supported handoff correction is permitted."
689
+ a?.state === "returned" && recovery?.state !== "reading"
690
+ ? "Worker ordinary task work has ended. Only supported handoff correction is permitted."
498
691
  : "",
499
- this.turn?.profile === "executor" && a && this.turn.basisUserEntryId !== this.taskBasis(state, a.id)
500
- ? "Current input differs from the original assignment. Account for it; changed direction requires Coordinator attention."
692
+ isWorkerProfile(this.turn?.profile) && a && this.turn.basisUserEntryId !== this.workerBasis(state, a.id)
693
+ ? `Current input differs from the active ${this.turn.profile} responsibility. Account for it; changed direction requires Coordinator attention.`
501
694
  : "",
502
695
  ...(state.assessment?.problems ?? []).map((p) => `${p.code}: ${p.detail}`),
503
696
  ]
@@ -516,16 +709,19 @@ export class RoutingRuntime {
516
709
  const selection = state.assignmentId
517
710
  ? (state.selections.get(state.assignmentId) ?? emptySelection())
518
711
  : emptySelection();
712
+ const sources = this.sources(state);
519
713
  return {
520
714
  revision: selection.revision,
521
715
  selected: selection.selected.map((ref) => {
522
716
  const entry = this.ctx?.sessionManager.getEntry?.(ref.slice(4).replace(/#text$/, ""));
717
+ const locator = sources.locator(ref);
523
718
  return {
524
719
  ref,
525
720
  kind: ref.endsWith("#text") ? "assistant-text" : (entry?.message?.role ?? "unknown"),
526
721
  toolName: entry?.message?.toolName,
527
722
  producer: state.authors.get(entry?.id)?.profile ?? "common",
528
723
  assignment: state.authors.get(entry?.id)?.assignmentId,
724
+ ...(locator ? { locator } : {}),
529
725
  };
530
726
  }),
531
727
  unresolved: selection.unresolved,
@@ -547,18 +743,9 @@ export class RoutingRuntime {
547
743
  pair: this.profilePair(profile),
548
744
  systemPrompt: this.ctx.getSystemPrompt?.() ?? "",
549
745
  tools: (this.pi.getAllTools?.() ?? [])
550
- .filter((tool) =>
551
- ROUTING_TOOLS.includes(tool.name)
552
- ? tool.name === "freeflow_unit" ||
553
- (profile === "coordinator"
554
- ? tool.name === "freeflow_delegate"
555
- : tool.name === "freeflow_return" || (tool.name === "freeflow_project" && this.projectionEnabled))
556
- : this.pi.getActiveTools?.().includes(tool.name),
557
- )
746
+ .filter((tool) => this.pi.getActiveTools?.().includes(tool.name))
558
747
  .map((tool) =>
559
- ROUTING_TOOLS.includes(tool.name)
560
- ? { ...tool, parameters: schemaForProfile(tool.name, profile === "coordinator") }
561
- : tool,
748
+ ROUTING_TOOLS.includes(tool.name) ? { ...tool, parameters: ROUTING_SCHEMAS[tool.name] } : tool,
562
749
  ),
563
750
  runtimeMessage: this.runtimeMessage(
564
751
  state,
@@ -569,6 +756,35 @@ export class RoutingRuntime {
569
756
  restoring,
570
757
  });
571
758
  }
759
+ budgetNotice(messages, ctx) {
760
+ if (!this.supported()) return;
761
+ const tools = (this.pi.getAllTools?.() ?? []).filter((tool) => this.pi.getActiveTools?.().includes(tool.name));
762
+ const estimate = estimateRequest(ctx.getSystemPrompt?.() ?? "", tools, messages, ctx.model);
763
+ if (!estimate.warnings.length) return;
764
+ return {
765
+ role: "custom",
766
+ customType: "freeflow-routing-budget",
767
+ display: false,
768
+ content: estimate.warnings.map((warning) => warning.detail).join("\n"),
769
+ timestamp: 0,
770
+ };
771
+ }
772
+ ordinaryContext(input) {
773
+ try {
774
+ const sources = this.sources(),
775
+ associated = sources.associate(input);
776
+ const mapped = new Map(associated.filter((item) => item.source).map((item) => [item.message, item.source]));
777
+ return annotateSources(
778
+ input,
779
+ mapped,
780
+ new Set([...mapped.values()].map((source) => source.ref)),
781
+ sources,
782
+ this.token,
783
+ );
784
+ } catch {
785
+ return input;
786
+ }
787
+ }
572
788
  async context(ctx, incoming) {
573
789
  this.ctx = ctx;
574
790
  const input = incoming.filter(
@@ -579,10 +795,10 @@ export class RoutingRuntime {
579
795
  ),
580
796
  );
581
797
  this.messages = input;
582
- if (!this.supported() || !this.store) return input;
798
+ if (!this.supported() || !this.store) return this.ordinaryContext(input);
583
799
  try {
584
800
  const state = this.stateData();
585
- if (state.control !== "automatic") return input;
801
+ if (state.control !== "automatic") return this.ordinaryContext(input);
586
802
  check(!this.error && !this.store.blocked && state.profile, "routing_blocked", this.error ?? this.store.blocked);
587
803
  check(samePair(this.observed(), this.profilePair(state.profile)), "prepared_pair_mismatch");
588
804
  const sources = this.sources(state),
@@ -598,16 +814,19 @@ export class RoutingRuntime {
598
814
  },
599
815
  JSON.stringify(["attention", state.assessment.handoffId, user]),
600
816
  );
601
- // New input reaching an already prepared Executor request cannot be rerouted by changing only the live model.
817
+ // New input reaching an already prepared worker request cannot be rerouted by changing only the live model.
602
818
  const outstanding = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
819
+ const worker = outstanding ? this.assignedWorker(state, outstanding.id) : undefined;
603
820
  const interrupted =
604
- state.profile === "executor" && user !== (outstanding ? this.taskBasis(state, outstanding.id) : undefined);
821
+ isWorkerProfile(state.profile) &&
822
+ (state.profile !== worker || user !== (outstanding ? this.workerBasis(state, outstanding.id) : undefined));
605
823
  if (!this.turn || this.turn.bound) {
606
824
  this.receipts.clear();
607
825
  const execution = {
608
826
  id: randomUUID(),
609
827
  profile: state.profile,
610
828
  assignmentId: state.assignmentId,
829
+ recoveryId: state.recoveryId,
611
830
  basisUserEntryId: user,
612
831
  pair: this.observed(),
613
832
  resultEntryIds: [],
@@ -617,6 +836,7 @@ export class RoutingRuntime {
617
836
  id: execution.id,
618
837
  profile: execution.profile,
619
838
  assignmentId: execution.assignmentId,
839
+ recoveryId: execution.recoveryId,
620
840
  basisUserEntryId: user,
621
841
  before,
622
842
  pair: execution.pair,
@@ -639,12 +859,12 @@ export class RoutingRuntime {
639
859
  check(prepared.ready, "context_unavailable", prepared.problems.map((p) => p.detail).join("; "));
640
860
  const known = this.stateData().exposure;
641
861
  const added = prepared.fullSources.filter((s) => known.get(s.ref) !== s.hash);
642
- if (this.turn.profile === "executor")
862
+ if (isWorkerProfile(this.turn.profile))
643
863
  for (let i = 0; i < added.length; i += 128)
644
864
  this.append({
645
865
  type: "sources-exposed",
646
866
  executionId: this.turn.id,
647
- view: "executor",
867
+ view: this.turn.profile,
648
868
  sources: added.slice(i, i + 128).map((s) => ({ ref: s.ref, bodyHash: s.hash })),
649
869
  });
650
870
  if (interrupted)
@@ -661,7 +881,7 @@ export class RoutingRuntime {
661
881
  } catch (error) {
662
882
  this.mark(error);
663
883
  ctx.abort?.();
664
- // Do not send an accidental full Executor history on a projection failure.
884
+ // Do not send an accidental full worker history on a projection failure.
665
885
  return [
666
886
  {
667
887
  role: "custom",
@@ -669,7 +889,7 @@ export class RoutingRuntime {
669
889
  content: `Automatic request blocked: ${this.error}`,
670
890
  display: false,
671
891
  timestamp: 0,
672
- details: { routingInstance: this.token },
892
+ details: { routingInstance: this.token, routingRequestBlocked: true },
673
893
  },
674
894
  ];
675
895
  }
@@ -680,6 +900,57 @@ export class RoutingRuntime {
680
900
  contextOperation(name, input) {
681
901
  return name === "freeflow_context" && ["archive", "restore", "search", "retrieve"].includes(input?.operation);
682
902
  }
903
+ handoffOperation(name, input) {
904
+ if (name === "freeflow_delegate") return true;
905
+ if (name === "freeflow_return") return ["submit", "supplement", "retry"].includes(input?.operation);
906
+ return name === "freeflow_unit" && input?.operation === "recover";
907
+ }
908
+ stableRecoveryPath(path) {
909
+ return (
910
+ !path.startsWith("@") &&
911
+ path !== "~" &&
912
+ !path.startsWith("~/") &&
913
+ !path.startsWith("file://") &&
914
+ !/[\u00a0\u2000-\u200a\u202f\u205f\u3000]/u.test(path) &&
915
+ !(process.platform === "win32" && /^\/(?:mnt\/|cygdrive\/)?[a-z](?:\/|$)/i.test(path))
916
+ );
917
+ }
918
+ canonicalRecoveryPath(path) {
919
+ check(this.stableRecoveryPath(path), "recovery_path_unsupported", `Recovery path spelling is unsupported: ${path}`);
920
+ const requested = resolvePath(this.ctx.cwd, path);
921
+ check(existsSync(requested), "recovery_path_unavailable", `Recovery path is unavailable: ${path}`);
922
+ try {
923
+ return realpathSync(requested);
924
+ } catch {
925
+ throw new RoutingError("recovery_path_unavailable", `Recovery path is unavailable: ${path}`);
926
+ }
927
+ }
928
+ packagedInstruction(path) {
929
+ return this.packageRoots.some((root) => {
930
+ const inside = relative(root, path);
931
+ if (!inside || inside.startsWith("..") || resolvePath(root, inside) !== path) return false;
932
+ return (
933
+ /^(?:skills|capabilities)\/[^/]+\/SKILL\.md$/.test(inside) ||
934
+ /^(?:skills|capabilities)\/[^/]+\/references\/[^/]+\.md$/.test(inside)
935
+ );
936
+ });
937
+ }
938
+ recoveryReadAllowed(state, name, input) {
939
+ if (name !== "read" || typeof input?.path !== "string" || !state.recoveryId) return false;
940
+ const recovery = state.recoveries.get(state.recoveryId);
941
+ if (!recovery || recovery.state !== "reading" || !this.stableRecoveryPath(input.path)) return false;
942
+ try {
943
+ const path = this.canonicalRecoveryPath(input.path);
944
+ return recovery.paths.includes(path) || this.packagedInstruction(path);
945
+ } catch {
946
+ return false;
947
+ }
948
+ }
949
+ workerBasis(state, assignmentId) {
950
+ const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
951
+ if (recovery?.state === "reading") return state.handoffs.get(recovery.requestHandoffId)?.basisUserEntryId ?? null;
952
+ return this.taskBasis(state, assignmentId);
953
+ }
683
954
  batch(callId, name) {
684
955
  check(this.turn?.message, "batch_unavailable");
685
956
  const matches = this.ctx.sessionManager
@@ -699,14 +970,16 @@ export class RoutingRuntime {
699
970
  this.turn.sourceFingerprint = fingerprint;
700
971
  this.turn.message = structuredClone(matches[0].message);
701
972
  const calls = (this.turn.message.content ?? []).filter((b) => b.type === "toolCall");
702
- const handoffs = calls.filter((b) => HANDOFF_TOOLS.has(b.name));
973
+ const handoffs = calls.filter((b) => this.handoffOperation(b.name, b.arguments));
703
974
  check(
704
975
  !handoffs.length ||
705
976
  (handoffs.length === 1 &&
706
- HANDOFF_TOOLS.has(calls.at(-1)?.name) &&
977
+ this.handoffOperation(calls.at(-1)?.name, calls.at(-1)?.arguments) &&
707
978
  calls.every(
708
979
  (b) =>
709
- HANDOFF_TOOLS.has(b.name) || b.name === "freeflow_project" || this.contextOperation(b.name, b.arguments),
980
+ this.handoffOperation(b.name, b.arguments) ||
981
+ b.name === "freeflow_project" ||
982
+ this.contextOperation(b.name, b.arguments),
710
983
  )),
711
984
  "invalid_handoff_batch",
712
985
  "A handoff must be last and cannot accompany ordinary task tools.",
@@ -730,14 +1003,21 @@ export class RoutingRuntime {
730
1003
  this.batch(event.toolCallId, name);
731
1004
  check(!this.error && !this.store?.blocked, "routing_blocked");
732
1005
  this.openTurn();
733
- if (this.turn?.profile === "executor" && !isRouting && !this.contextOperation(name, event.input)) {
1006
+ if (isWorkerProfile(this.turn?.profile) && !isRouting && !this.contextOperation(name, event.input)) {
734
1007
  const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
735
- check(
1008
+ const worker = a ? this.assignedWorker(state, a.id) : undefined;
1009
+ const ordinary =
736
1010
  a?.state === "outstanding" &&
737
- this.turn.basisUserEntryId === this.taskBasis(state, a.id) &&
738
- state.profile === "executor",
739
- "executor_task_phase_ended",
740
- );
1011
+ this.turn.basisUserEntryId === this.taskBasis(state, a.id) &&
1012
+ state.profile === worker &&
1013
+ this.turn.profile === worker;
1014
+ const recovery =
1015
+ a?.state === "returned" &&
1016
+ this.turn.basisUserEntryId === this.workerBasis(state, a.id) &&
1017
+ state.profile === worker &&
1018
+ this.turn.profile === worker &&
1019
+ this.recoveryReadAllowed(state, name, event.input);
1020
+ check(ordinary || recovery, "worker_task_phase_ended");
741
1021
  }
742
1022
  return undefined;
743
1023
  } catch (error) {
@@ -757,8 +1037,14 @@ export class RoutingRuntime {
757
1037
  this.ctx = ctx;
758
1038
  try {
759
1039
  check(!signal?.aborted, "cancelled");
760
- if (name === "freeflow_unit" && input.operation === "inspect") return this.result(this.inspectUnit(input));
761
- this.assertAvailable(name === "freeflow_delegate" || name === "freeflow_unit" ? "coordinator" : "executor");
1040
+ if (name === "freeflow_unit" && input.operation === "inspect") {
1041
+ check(this.supported(), "routing_unavailable");
1042
+ return this.result(this.inspectUnit(input));
1043
+ }
1044
+ const state = this.stateData();
1045
+ const expected =
1046
+ name === "freeflow_delegate" || name === "freeflow_unit" ? "coordinator" : this.assignedWorker(state);
1047
+ this.assertAvailable(expected);
762
1048
  const op = this.callOperation(callId, name);
763
1049
  const cached = this.receipts.get(op);
764
1050
  if (cached) {
@@ -773,7 +1059,7 @@ export class RoutingRuntime {
773
1059
  : name === "freeflow_project"
774
1060
  ? this.project(input, op)
775
1061
  : name === "freeflow_unit"
776
- ? this.unit(input, op)
1062
+ ? this.unit(input, callId, op)
777
1063
  : undefined;
778
1064
  if (value !== undefined) {
779
1065
  this.receipts.set(op, { input: canonical(input), value: structuredClone(value) });
@@ -792,7 +1078,13 @@ export class RoutingRuntime {
792
1078
  ? undefined
793
1079
  : name === "freeflow_delegate" || name === "freeflow_unit"
794
1080
  ? "coordinator"
795
- : "executor",
1081
+ : (() => {
1082
+ try {
1083
+ return this.assignedWorker(this.stateData());
1084
+ } catch {
1085
+ return undefined;
1086
+ }
1087
+ })(),
796
1088
  recoveryAction:
797
1089
  error instanceof RoutingError &&
798
1090
  [
@@ -813,26 +1105,48 @@ export class RoutingRuntime {
813
1105
  return [...this.stateData().events.values()].find(
814
1106
  (e) =>
815
1107
  e.operationId === op &&
816
- ["delegate-accepted", "return-accepted", "handoff-retry-requested"].includes(e.data.type),
1108
+ [
1109
+ "delegate-accepted",
1110
+ "return-accepted",
1111
+ "recovery-request-accepted",
1112
+ "recovery-supplement-accepted",
1113
+ "recovery-cancelled",
1114
+ "handoff-retry-requested",
1115
+ ].includes(e.data.type),
817
1116
  );
818
1117
  }
819
1118
  delegate(input, callId, op) {
820
1119
  const prior = this.duplicate(op);
821
1120
  if (prior) {
822
1121
  check(
823
- prior.data.type === "delegate-accepted" && prior.data.assignment.contract === input.contract,
1122
+ prior.data.type === "delegate-accepted" &&
1123
+ prior.data.assignment.contract === input.contract &&
1124
+ (input.worker === undefined || prior.data.handoff.to === input.worker),
824
1125
  "operation_conflict",
825
1126
  );
826
1127
  return { status: "accepted", handoff: prior.data.handoff.id, unchanged: true };
827
1128
  }
828
1129
  const state = this.stateData();
1130
+ const enabled = this.enabledWorkers();
1131
+ let worker;
1132
+ if (input.worker === undefined) {
1133
+ check(enabled.length === 1, "worker_required", "Delegation mode 'both' requires choosing helper or executor.");
1134
+ worker = enabled[0];
1135
+ } else {
1136
+ check(isWorkerProfile(input.worker), "invalid_worker");
1137
+ worker = input.worker;
1138
+ }
1139
+ check(enabled.includes(worker), "worker_disabled", `${worker} is not enabled by delegation mode.`);
829
1140
  const unit = state.unitId
830
1141
  ? state.units.get(state.unitId)
831
1142
  : { id: randomUUID(), objective: input.contract, state: "open", assignmentIds: [] };
832
1143
  const old = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
833
1144
  if (input.operation === "replace") check(old?.state === "outstanding" && !this.applying, "no_quiescent_assignment");
834
1145
  else
835
- check(input.operation === "assign" && old?.state !== "outstanding" && !state.pendingId, "assignment_outstanding");
1146
+ check(
1147
+ input.operation === "assign" && old?.state !== "outstanding" && !state.pendingId && !state.recoveryId,
1148
+ "assignment_outstanding",
1149
+ );
836
1150
  const assignmentId = randomUUID(),
837
1151
  id = randomUUID();
838
1152
  const assignment = {
@@ -849,7 +1163,7 @@ export class RoutingRuntime {
849
1163
  assignmentId,
850
1164
  text: input.contract,
851
1165
  from: "coordinator",
852
- to: "executor",
1166
+ to: worker,
853
1167
  executionId: this.turn.id,
854
1168
  toolCallId: callId,
855
1169
  basisUserEntryId: this.turn.basisUserEntryId,
@@ -879,6 +1193,7 @@ export class RoutingRuntime {
879
1193
  status: "accepted",
880
1194
  unit: unit.id,
881
1195
  assignment: assignmentId,
1196
+ worker,
882
1197
  handoff: id,
883
1198
  transition: "pending",
884
1199
  contract: input.contract,
@@ -897,25 +1212,28 @@ export class RoutingRuntime {
897
1212
  const state = this.stateData(),
898
1213
  a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
899
1214
  check(a, "assignment_missing");
900
- const saved = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
1215
+ const worker = this.assignedWorker(state, a.id);
1216
+ const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
1217
+ const original = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
1218
+ const supplement = recovery?.supplementHandoffId ? state.handoffs.get(recovery.supplementHandoffId) : undefined;
1219
+ const retryingSupplement = input.operation === "retry" && recovery?.state === "returning";
1220
+ const saved = retryingSupplement || input.operation === "supplement" ? supplement : original;
901
1221
  const selection = state.selections.get(a.id) ?? emptySelection();
902
1222
  const duplicate = this.duplicate(op);
903
1223
  if (duplicate) {
904
1224
  if (input.operation === "retry") check(duplicate.data.type === "handoff-retry-requested", "operation_conflict");
905
- else
1225
+ else {
1226
+ const expected = input.operation === "supplement" ? "recovery-supplement-accepted" : "return-accepted";
906
1227
  check(
907
- duplicate.data.type === "return-accepted" &&
1228
+ duplicate.data.type === expected &&
908
1229
  duplicate.data.handoff.text === input.report &&
909
1230
  duplicate.data.handoff.outcome === input.outcome &&
910
1231
  canonical(duplicate.data.handoff.limitations) === canonical(input.limitations ?? []),
911
1232
  "operation_conflict",
912
1233
  );
913
- return {
914
- status: "accepted",
915
- reportSaved: true,
916
- unchanged: true,
917
- handoff: duplicate.data.handoffId ?? duplicate.data.handoff.id,
918
- };
1234
+ }
1235
+ const handoff = duplicate.data.handoffId ?? duplicate.data.handoff.id;
1236
+ return { ...this.returnReadiness(handoff), unchanged: true };
919
1237
  }
920
1238
  if (input.operation === "retry") {
921
1239
  check(saved && ["blocked", "pending"].includes(saved.state), "saved_return_missing");
@@ -931,25 +1249,55 @@ export class RoutingRuntime {
931
1249
  },
932
1250
  op,
933
1251
  );
934
- return { ...this.returnReadiness(saved.id), reportUnchanged: true };
1252
+ return {
1253
+ ...this.returnReadiness(saved.id),
1254
+ reportUnchanged: true,
1255
+ supplementUnchanged: saved.kind === "recovery-return",
1256
+ };
1257
+ }
1258
+ if (input.operation === "supplement") {
1259
+ check(
1260
+ recovery &&
1261
+ ["reading", "returning"].includes(recovery.state) &&
1262
+ (!supplement || ["pending", "blocked"].includes(supplement.state)),
1263
+ "recovery_not_returnable",
1264
+ );
1265
+ const h = {
1266
+ id: supplement?.id ?? randomUUID(),
1267
+ kind: "recovery-return",
1268
+ assignmentId: a.id,
1269
+ text: input.report,
1270
+ from: worker,
1271
+ to: "coordinator",
1272
+ executionId: this.turn.id,
1273
+ toolCallId: callId,
1274
+ basisUserEntryId: this.turn.basisUserEntryId,
1275
+ state: "pending",
1276
+ reportRevision: (supplement?.reportRevision ?? 0) + 1,
1277
+ outcome: input.outcome,
1278
+ limitations: input.limitations ?? [],
1279
+ };
1280
+ this.append({ type: "recovery-supplement-accepted", recoveryId: recovery.id, handoff: h }, op);
1281
+ return this.returnReadiness(h.id);
935
1282
  }
936
1283
  check(input.operation === "submit", "invalid_return_operation");
937
1284
  check(
938
- a.state === "outstanding" || (a.state === "returned" && saved && ["pending", "blocked"].includes(saved.state)),
1285
+ a.state === "outstanding" ||
1286
+ (a.state === "returned" && original && ["pending", "blocked"].includes(original.state)),
939
1287
  "assignment_task_ended",
940
1288
  );
941
1289
  const h = {
942
- id: saved?.id ?? randomUUID(),
1290
+ id: original?.id ?? randomUUID(),
943
1291
  kind: "return",
944
1292
  assignmentId: a.id,
945
1293
  text: input.report,
946
- from: "executor",
1294
+ from: worker,
947
1295
  to: "coordinator",
948
1296
  executionId: this.turn.id,
949
1297
  toolCallId: callId,
950
1298
  basisUserEntryId: this.turn.basisUserEntryId,
951
1299
  state: "pending",
952
- reportRevision: (saved?.reportRevision ?? 0) + 1,
1300
+ reportRevision: (original?.reportRevision ?? 0) + 1,
953
1301
  outcome: input.outcome,
954
1302
  limitations: input.limitations ?? [],
955
1303
  };
@@ -957,16 +1305,25 @@ export class RoutingRuntime {
957
1305
  return this.returnReadiness(h.id);
958
1306
  }
959
1307
  returnReadiness(id) {
960
- const h = this.stateData().handoffs.get(id);
1308
+ const state = this.stateData();
1309
+ const h = state.handoffs.get(id);
961
1310
  const prepared = this.prepared("coordinator", this.messages, id);
1311
+ const isSupplement = h.kind === "recovery-return";
1312
+ const recovery = isSupplement
1313
+ ? [...state.recoveries.values()].find((candidate) => candidate.supplementHandoffId === h.id)
1314
+ : undefined;
962
1315
  return {
963
1316
  status: "accepted",
964
- reportSaved: true,
1317
+ reportSaved: !isSupplement,
1318
+ supplementSaved: isSupplement,
965
1319
  handoff: id,
966
- reportRevision: h.reportRevision,
1320
+ ...(recovery ? { recovery: recovery.id } : {}),
1321
+ ...(isSupplement ? { supplementRevision: h.reportRevision } : { reportRevision: h.reportRevision }),
967
1322
  assignmentRef: `assignment:${h.assignmentId}`,
968
- reportRef: `report:${h.id}:${h.reportRevision}`,
969
- producer: "executor",
1323
+ ...(isSupplement
1324
+ ? { supplementRef: `supplement:${recovery?.id ?? "unknown"}:${h.reportRevision}` }
1325
+ : { reportRef: `report:${h.id}:${h.reportRevision}` }),
1326
+ producer: h.from,
970
1327
  outcome: h.outcome,
971
1328
  limitations: h.limitations,
972
1329
  ready: prepared.ready,
@@ -977,8 +1334,8 @@ export class RoutingRuntime {
977
1334
  finalExchangePending: true,
978
1335
  provisional:
979
1336
  "The finalized handoff exchange, target configuration and budget are revalidated at turn_end; this is not delivery evidence.",
980
- report: h.text,
981
- evidence: this.evidenceFacts(this.stateData()),
1337
+ ...(isSupplement ? { supplement: h.text } : { report: h.text }),
1338
+ evidence: this.evidenceFacts(state),
982
1339
  };
983
1340
  }
984
1341
  project(input, op) {
@@ -1013,7 +1370,7 @@ export class RoutingRuntime {
1013
1370
  [...sources.byRef.values()]
1014
1371
  .filter(
1015
1372
  (s) =>
1016
- s.producer === "executor" &&
1373
+ isWorkerProfile(s.producer) &&
1017
1374
  (isTaskEvidence(s) || (scope === "selected" && next.selected.includes(s.ref))) &&
1018
1375
  (scope === "selected"
1019
1376
  ? next.selected.includes(s.ref)
@@ -1027,12 +1384,14 @@ export class RoutingRuntime {
1027
1384
  s.message.content?.some((b) => b.type === "text" && b.text?.trim())),
1028
1385
  )
1029
1386
  .map((s) => {
1387
+ const locator = sources.locator(s.ref);
1030
1388
  return {
1031
1389
  ref: s.ref,
1032
1390
  kind: s.original ? "assistant-text" : s.message.role,
1033
1391
  producer: s.producer,
1034
1392
  assignment: s.assignmentId,
1035
1393
  toolName: s.message.toolName,
1394
+ ...(locator ? { locator } : {}),
1036
1395
  active: s.active,
1037
1396
  selected: next.selected.includes(s.ref),
1038
1397
  retainedSelection: next.selected.includes(s.ref) && !isTaskEvidence(s),
@@ -1057,7 +1416,7 @@ export class RoutingRuntime {
1057
1416
  },
1058
1417
  otherAssignments: [...sources.byRef.values()].filter(
1059
1418
  (s) =>
1060
- s.producer === "executor" && s.assignmentId !== a.id && isTaskEvidence(s) && checksFor(s).targetReady,
1419
+ isWorkerProfile(s.producer) && s.assignmentId !== a.id && isTaskEvidence(s) && checksFor(s).targetReady,
1061
1420
  ).length,
1062
1421
  }),
1063
1422
  );
@@ -1109,10 +1468,40 @@ export class RoutingRuntime {
1109
1468
  estimate: { tokens: prepared.estimatedTokens, method: prepared.estimateMethod },
1110
1469
  };
1111
1470
  }
1112
- unit(input, op) {
1471
+ unit(input, callId, op) {
1113
1472
  const state = this.stateData();
1114
1473
  const prior = [...state.events.values()].find((e) => e.operationId === op);
1115
1474
  if (prior) {
1475
+ if (input.operation === "recover") {
1476
+ check(
1477
+ prior.data.type === "recovery-request-accepted" &&
1478
+ prior.data.recovery.request === input.request &&
1479
+ canonical(prior.data.recovery.requestedPaths) === canonical([...new Set(input.paths ?? [])]),
1480
+ "operation_conflict",
1481
+ );
1482
+ const { recovery, handoff } = prior.data;
1483
+ return {
1484
+ status: "accepted",
1485
+ unchanged: true,
1486
+ recovery: recovery.id,
1487
+ handoff: handoff.id,
1488
+ assignmentRef: `assignment:${recovery.assignmentId}`,
1489
+ reportRef: `report:${recovery.assessmentHandoffId}:${recovery.baseReportRevision}`,
1490
+ request: recovery.request,
1491
+ paths: recovery.paths,
1492
+ transition: handoff.state,
1493
+ };
1494
+ }
1495
+ if (input.operation === "cancel-recovery") {
1496
+ check(prior.data.type === "recovery-cancelled" && prior.data.reason === input.reason, "operation_conflict");
1497
+ return {
1498
+ status: "cancelled",
1499
+ unchanged: true,
1500
+ recovery: prior.data.recoveryId,
1501
+ reason: prior.data.reason,
1502
+ assessment: state.assessment?.handoffId,
1503
+ };
1504
+ }
1116
1505
  check(
1117
1506
  (input.operation === "assess" && prior.data.type === "assessment-resumed") ||
1118
1507
  (input.operation === "close" &&
@@ -1123,18 +1512,95 @@ export class RoutingRuntime {
1123
1512
  );
1124
1513
  return { status: "unchanged", operation: input.operation };
1125
1514
  }
1515
+ if (input.operation === "recover") {
1516
+ const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
1517
+ const base = a?.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
1518
+ check(
1519
+ state.unitId &&
1520
+ a?.state === "returned" &&
1521
+ base?.kind === "return" &&
1522
+ base.state === "configured" &&
1523
+ state.assessment?.handoffId === base.id &&
1524
+ !state.pendingId &&
1525
+ !state.recoveryId &&
1526
+ !this.applying,
1527
+ "recovery_not_available",
1528
+ );
1529
+ const worker = this.assignedWorker(state, a.id);
1530
+ const id = randomUUID(),
1531
+ handoffId = randomUUID();
1532
+ const paths = [...new Set((input.paths ?? []).map((path) => this.canonicalRecoveryPath(path)))];
1533
+ const recovery = {
1534
+ id,
1535
+ assignmentId: a.id,
1536
+ assessmentHandoffId: base.id,
1537
+ baseReportRevision: base.reportRevision,
1538
+ request: input.request,
1539
+ requestedPaths: [...new Set(input.paths ?? [])],
1540
+ paths,
1541
+ state: "requested",
1542
+ requestHandoffId: handoffId,
1543
+ supplementRevision: 0,
1544
+ };
1545
+ const handoff = {
1546
+ id: handoffId,
1547
+ kind: "recovery-request",
1548
+ assignmentId: a.id,
1549
+ text: input.request,
1550
+ from: "coordinator",
1551
+ to: worker,
1552
+ executionId: this.turn.id,
1553
+ toolCallId: callId,
1554
+ basisUserEntryId: this.turn.basisUserEntryId,
1555
+ state: "pending",
1556
+ reportRevision: 0,
1557
+ limitations: [],
1558
+ };
1559
+ this.append({ type: "recovery-request-accepted", recovery, handoff }, op);
1560
+ return {
1561
+ status: "accepted",
1562
+ recovery: id,
1563
+ handoff: handoffId,
1564
+ assignmentRef: `assignment:${a.id}`,
1565
+ reportRef: `report:${base.id}:${base.reportRevision}`,
1566
+ request: input.request,
1567
+ paths,
1568
+ transition: "pending",
1569
+ };
1570
+ }
1571
+ if (input.operation === "cancel-recovery") {
1572
+ check(state.recoveryId && !this.applying, "recovery_not_cancellable");
1573
+ check(
1574
+ ![...state.executions.values()].some(
1575
+ (execution) =>
1576
+ execution.recoveryId === state.recoveryId &&
1577
+ isWorkerProfile(execution.profile) &&
1578
+ !execution.assistantEntryId &&
1579
+ !execution.interrupted,
1580
+ ),
1581
+ "recovery_not_cancellable",
1582
+ );
1583
+ this.append({ type: "recovery-cancelled", recoveryId: state.recoveryId, reason: input.reason }, op);
1584
+ return {
1585
+ status: "cancelled",
1586
+ recovery: state.recoveryId,
1587
+ reason: input.reason,
1588
+ assessment: state.assessment?.handoffId,
1589
+ };
1590
+ }
1126
1591
  if (input.operation === "assess") {
1127
1592
  check(state.assessment, "assessment_missing");
1593
+ check(!state.recoveryId, "recovery_outstanding");
1128
1594
  if (state.assessment.view === "active") return { status: "unchanged", ready: true };
1129
1595
  const prepared = this.prepared("coordinator", this.messages, state.assessment.handoffId, true);
1130
1596
  if (!prepared.ready) return { status: "suspended", ready: false, problems: prepared.problems };
1131
- check(prepared.reservation, "reservation_missing");
1597
+ if (this.projectionEnabled) check(prepared.reservation, "reservation_missing");
1132
1598
  this.append(
1133
1599
  {
1134
1600
  type: "assessment-resumed",
1135
1601
  handoffId: state.assessment.handoffId,
1136
1602
  basisUserEntryId: this.turn.basisUserEntryId,
1137
- reservation: prepared.reservation,
1603
+ ...(prepared.reservation ? { reservation: prepared.reservation } : {}),
1138
1604
  },
1139
1605
  op,
1140
1606
  );
@@ -1149,6 +1615,19 @@ export class RoutingRuntime {
1149
1615
  check(input.operation === "close" && state.unitId, "unit_missing");
1150
1616
  const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
1151
1617
  check(!this.applying, "not_quiescent");
1618
+ if (state.recoveryId) {
1619
+ check(input.outcome !== "accepted", "unfinished_work");
1620
+ check(
1621
+ ![...state.executions.values()].some(
1622
+ (execution) =>
1623
+ execution.recoveryId === state.recoveryId &&
1624
+ isWorkerProfile(execution.profile) &&
1625
+ !execution.assistantEntryId &&
1626
+ !execution.interrupted,
1627
+ ),
1628
+ "not_quiescent",
1629
+ );
1630
+ }
1152
1631
  this.append(
1153
1632
  {
1154
1633
  type: "unit-closed",
@@ -1231,6 +1710,7 @@ export class RoutingRuntime {
1231
1710
  [...state.assignments.values()].reverse().map((a) => {
1232
1711
  const u = state.units.get(a.unitId),
1233
1712
  h = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
1713
+ const delegate = state.handoffs.get(a.delegateHandoffId);
1234
1714
  return {
1235
1715
  ref: `assignment:${a.id}`,
1236
1716
  unitRef: `unit:${a.unitId}`,
@@ -1242,8 +1722,8 @@ export class RoutingRuntime {
1242
1722
  summary: a.contract.slice(0, 160),
1243
1723
  reportAvailable: !!h,
1244
1724
  outcome: h?.outcome,
1245
- from: "coordinator",
1246
- to: "executor",
1725
+ from: delegate?.from,
1726
+ to: delegate?.to,
1247
1727
  };
1248
1728
  });
1249
1729
  const page = this.page("work-history", rows, input.cursor, input.limit ?? 20);
@@ -1262,9 +1742,9 @@ export class RoutingRuntime {
1262
1742
  "Use a ref returned by history inspection.",
1263
1743
  );
1264
1744
  check(
1265
- /^(?:(?:unit|assignment):[^:\s]+|report:[^:\s]+:[1-9][0-9]*)$/.test(input.ref),
1745
+ /^(?:(?:unit|assignment|recovery):[^:\s]+|(?:report|supplement):[^:\s]+:[1-9][0-9]*)$/.test(input.ref),
1266
1746
  "invalid_work_ref",
1267
- "Use assignmentRef or reportRef from the return receipt, or a work ref returned by inspection, unchanged. A bare handoff ID is not a work ref.",
1747
+ "Use assignment, report, recovery, or supplement refs returned by a receipt or inspection, unchanged. A bare handoff ID is not a work ref.",
1268
1748
  );
1269
1749
  if (input.ref.startsWith("unit:")) {
1270
1750
  const u = state.units.get(input.ref.slice(5));
@@ -1287,6 +1767,63 @@ export class RoutingRuntime {
1287
1767
  historical: true,
1288
1768
  };
1289
1769
  }
1770
+ if (input.ref.startsWith("recovery:")) {
1771
+ const recovery = state.recoveries.get(input.ref.slice(9));
1772
+ check(recovery, "work_unavailable", "Recovery is not on current ancestry.");
1773
+ const request = state.handoffs.get(recovery.requestHandoffId);
1774
+ const supplement = recovery.supplementHandoffId ? state.handoffs.get(recovery.supplementHandoffId) : undefined;
1775
+ return {
1776
+ status: "inspected",
1777
+ view,
1778
+ ref: input.ref,
1779
+ historical: recovery.id !== state.recoveryId,
1780
+ recovery: {
1781
+ id: recovery.id,
1782
+ state: recovery.state,
1783
+ assignmentId: recovery.assignmentId,
1784
+ assessmentHandoffId: recovery.assessmentHandoffId,
1785
+ baseReportRevision: recovery.baseReportRevision,
1786
+ request: recovery.request,
1787
+ paths: recovery.paths,
1788
+ requestHandoffState: request?.state,
1789
+ supplementRevision: recovery.supplementRevision,
1790
+ cancellationReason: recovery.cancellationReason,
1791
+ },
1792
+ supplementRef: supplement ? `supplement:${recovery.id}:${supplement.reportRevision}` : undefined,
1793
+ sourceBoundary: "Saved recovery on current ancestry; historical content grants no current permission.",
1794
+ };
1795
+ }
1796
+ if (input.ref.startsWith("supplement:")) {
1797
+ const match = /^supplement:([^:]+):([0-9]+)$/.exec(input.ref);
1798
+ check(match, "work_unavailable", "Use a supplement ref returned by a receipt or recovery inspection.");
1799
+ const recovery = state.recoveries.get(match[1]);
1800
+ let supplement;
1801
+ for (const event of state.events.values())
1802
+ if (
1803
+ event.data.type === "recovery-supplement-accepted" &&
1804
+ event.data.recoveryId === match[1] &&
1805
+ event.data.handoff.reportRevision === Number(match[2])
1806
+ )
1807
+ supplement = event.data.handoff;
1808
+ check(recovery && supplement, "work_unavailable", "Supplement revision is not on current ancestry.");
1809
+ const base = state.handoffs.get(recovery.assessmentHandoffId);
1810
+ return {
1811
+ status: "inspected",
1812
+ view,
1813
+ ref: input.ref,
1814
+ historical: true,
1815
+ recoveryRef: `recovery:${recovery.id}`,
1816
+ assignment: { id: recovery.assignmentId, state: state.assignments.get(recovery.assignmentId)?.state },
1817
+ reportRef: `report:${recovery.assessmentHandoffId}:${recovery.baseReportRevision}`,
1818
+ report: base?.text,
1819
+ supplement: supplement.text,
1820
+ supplementRevision: supplement.reportRevision,
1821
+ outcome: supplement.outcome,
1822
+ limitations: supplement.limitations,
1823
+ sourceBoundary:
1824
+ "Saved supplement revision and original report linkage on current ancestry; historical content grants no current permission.",
1825
+ };
1826
+ }
1290
1827
  let savedRevision;
1291
1828
  if (input.ref.startsWith("report:")) {
1292
1829
  const match = /^report:([^:]+):([0-9]+)$/.exec(input.ref);
@@ -1335,8 +1872,23 @@ export class RoutingRuntime {
1335
1872
  return {
1336
1873
  ...this.state(),
1337
1874
  unit: unit ? { id: unit.id, state: unit.state, assignments: unit.assignmentIds.length } : null,
1338
- assignment: a ? { id: a.id, state: a.state } : null,
1875
+ assignment: a ? { id: a.id, state: a.state, worker: this.assignedWorker(state, a.id) } : null,
1339
1876
  pendingHandoff: state.pendingId ?? null,
1877
+ recovery: state.recoveryId
1878
+ ? (() => {
1879
+ const recovery = state.recoveries.get(state.recoveryId);
1880
+ return recovery
1881
+ ? {
1882
+ id: recovery.id,
1883
+ state: recovery.state,
1884
+ assignmentId: recovery.assignmentId,
1885
+ assessmentHandoffId: recovery.assessmentHandoffId,
1886
+ paths: recovery.paths,
1887
+ supplementRevision: recovery.supplementRevision,
1888
+ }
1889
+ : null;
1890
+ })()
1891
+ : null,
1340
1892
  assessment: state.assessment
1341
1893
  ? { handoffId: state.assessment.handoffId, view: state.assessment.view, problems: state.assessment.problems }
1342
1894
  : null,
@@ -1423,23 +1975,29 @@ export class RoutingRuntime {
1423
1975
  "source_exchange_incomplete",
1424
1976
  "The saved handoff has an incomplete native exchange; reconcile or explicitly dispose of it.",
1425
1977
  );
1426
- if (h.kind === "return" && this.projectionEnabled) {
1978
+ let attentionProblems = [];
1979
+ if (["return", "recovery-return"].includes(h.kind) && this.projectionEnabled) {
1427
1980
  const prepared = this.prepared("coordinator", inputs, h.id);
1428
1981
  if (!prepared.ready) {
1982
+ const attentionFallback = h.kind === "recovery-return" && h.outcome !== "completed";
1429
1983
  this.projectionError = prepared.problems.map((p) => p.code).join(", ");
1430
- this.append({
1431
- type: "handoff-state",
1432
- handoffId: h.id,
1433
- state: "blocked",
1434
- reason: prepared.problems
1435
- .map((p) => p.detail)
1436
- .join("; ")
1437
- .slice(0, 4096),
1438
- });
1439
- return;
1984
+ if (attentionFallback) attentionProblems = prepared.problems;
1985
+ else {
1986
+ this.append({
1987
+ type: "handoff-state",
1988
+ handoffId: h.id,
1989
+ state: "blocked",
1990
+ reason: prepared.problems
1991
+ .map((p) => p.detail)
1992
+ .join("; ")
1993
+ .slice(0, 4096),
1994
+ });
1995
+ return;
1996
+ }
1997
+ } else {
1998
+ check(prepared.reservation, "reservation_missing");
1999
+ this.append({ type: "handoff-prepared", handoffId: h.id, reservation: prepared.reservation });
1440
2000
  }
1441
- check(prepared.reservation, "reservation_missing");
1442
- this.append({ type: "handoff-prepared", handoffId: h.id, reservation: prepared.reservation });
1443
2001
  }
1444
2002
  try {
1445
2003
  const current = this.stateData();
@@ -1449,6 +2007,28 @@ export class RoutingRuntime {
1449
2007
  this.append({ type: "handoff-state", handoffId: h.id, state: "configured", observedPair: this.observed() });
1450
2008
  const completed = this.stateData();
1451
2009
  const reservation = completed.reservations.get(h.id);
2010
+ const recovery =
2011
+ h.kind === "recovery-return"
2012
+ ? [...completed.recoveries.values()].find((candidate) => candidate.supplementHandoffId === h.id)
2013
+ : undefined;
2014
+ if (h.kind === "recovery-return" && recovery && completed.assessment?.view === "suspended") {
2015
+ if (attentionProblems.length) {
2016
+ this.append({
2017
+ type: "assessment-suspended",
2018
+ handoffId: recovery.assessmentHandoffId,
2019
+ reason: "delivery-gap",
2020
+ basisUserEntryId: this.lastDeliveredUser(this.sources(completed), inputs),
2021
+ problems: attentionProblems,
2022
+ });
2023
+ } else if (completed.assessment.suspensionReason === "recovery") {
2024
+ this.append({
2025
+ type: "assessment-resumed",
2026
+ handoffId: recovery.assessmentHandoffId,
2027
+ basisUserEntryId: this.lastDeliveredUser(this.sources(completed), inputs),
2028
+ ...(reservation ? { reservation } : {}),
2029
+ });
2030
+ }
2031
+ }
1452
2032
  if (h.kind === "return" && completed.assessment?.view === "suspended" && reservation) {
1453
2033
  this.append({
1454
2034
  type: "assessment-resumed",
@@ -1457,7 +2037,7 @@ export class RoutingRuntime {
1457
2037
  reservation,
1458
2038
  });
1459
2039
  }
1460
- this.projectionError = undefined;
2040
+ if (!attentionProblems.length) this.projectionError = undefined;
1461
2041
  } catch (error) {
1462
2042
  if (this.current(subject) && !this.store?.blocked && !this.error && this.stateData().pendingId === h.id) {
1463
2043
  this.append({
@@ -1467,7 +2047,7 @@ export class RoutingRuntime {
1467
2047
  reason: error instanceof Error ? error.message.slice(0, 4096) : "configuration_failed",
1468
2048
  });
1469
2049
  this.ctx.ui?.notify?.(
1470
- `Couldn’t switch to ${h.to === "coordinator" ? "Coordinator" : "Executor"} — ${h.kind === "return" ? "report" : "assignment"} saved.`,
2050
+ `Couldn’t switch to ${h.to.charAt(0).toUpperCase()}${h.to.slice(1)} — ${["return", "recovery-return"].includes(h.kind) ? "report" : "assignment"} saved.`,
1471
2051
  "warning",
1472
2052
  );
1473
2053
  } else throw error;
@@ -1493,7 +2073,7 @@ export class RoutingRuntime {
1493
2073
  check(
1494
2074
  !ctx.hasPendingMessages?.(),
1495
2075
  "pending_input",
1496
- "Queued input must be delivered and reconciled before resuming Executor.",
2076
+ "Queued input must be delivered and reconciled before resuming a worker.",
1497
2077
  );
1498
2078
  const pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
1499
2079
  if (pending) {
@@ -1503,17 +2083,18 @@ export class RoutingRuntime {
1503
2083
  this.guard(subject);
1504
2084
  if (this.stateData().pendingId) {
1505
2085
  check(
1506
- pending.kind === "return" && this.projectionError,
2086
+ ["return", "recovery-return"].includes(pending.kind) && this.projectionError,
1507
2087
  "handoff_still_blocked",
1508
2088
  "Saved transfer is still blocked; reconcile its configuration before retrying.",
1509
2089
  );
2090
+ const worker = this.assignedWorker(this.stateData(), pending.assignmentId);
1510
2091
  this.append({
1511
2092
  type: "control",
1512
2093
  control: "automatic",
1513
- profile: "executor",
2094
+ profile: worker,
1514
2095
  reason: "Explicit resume of saved-return evidence correction only; ordinary assignment work remains ended",
1515
2096
  });
1516
- await this.applyPair(this.profilePair("executor"));
2097
+ await this.applyPair(this.profilePair(worker));
1517
2098
  }
1518
2099
  } else {
1519
2100
  const assignment = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
@@ -1523,7 +2104,22 @@ export class RoutingRuntime {
1523
2104
  (x) =>
1524
2105
  x.profile === "coordinator" && x.basisUserEntryId === user && x.assistantEntryId && x.outcome === "completed",
1525
2106
  );
1526
- if (assignment?.state === "outstanding") {
2107
+ const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
2108
+ const worker = assignment ? this.assignedWorker(state, assignment.id) : undefined;
2109
+ if (assignment?.state === "returned" && recovery?.state === "reading") {
2110
+ check(
2111
+ user === this.workerBasis(state, assignment.id) || coordinatorSawInput,
2112
+ "input_unreconciled",
2113
+ "New input needs Coordinator attention before evidence recovery can resume.",
2114
+ );
2115
+ this.append({
2116
+ type: "control",
2117
+ control: "automatic",
2118
+ profile: worker,
2119
+ reason: "Explicit user resume of unchanged evidence recovery; ordinary assignment work remains ended",
2120
+ });
2121
+ await this.applyPair(this.profilePair(worker));
2122
+ } else if (assignment?.state === "outstanding") {
1527
2123
  check(
1528
2124
  user === this.taskBasis(state, assignment.id) || coordinatorSawInput,
1529
2125
  "input_unreconciled",
@@ -1533,10 +2129,10 @@ export class RoutingRuntime {
1533
2129
  this.append({
1534
2130
  type: "control",
1535
2131
  control: "automatic",
1536
- profile: "executor",
2132
+ profile: worker,
1537
2133
  reason: "Explicit user resume of the unchanged assignment; current restrictions remain applicable",
1538
2134
  });
1539
- await this.applyPair(this.profilePair("executor"));
2135
+ await this.applyPair(this.profilePair(worker));
1540
2136
  } else await this.applyPair(this.profilePair("coordinator"));
1541
2137
  }
1542
2138
  this.guard(subject);
@@ -1573,7 +2169,7 @@ export class RoutingRuntime {
1573
2169
  check(
1574
2170
  words.length === 2 && [...PROFILES, "auto"].includes(words[1]),
1575
2171
  "invalid_profile_command",
1576
- "Use /freeflow profile coordinator|executor|auto|history, or /freeflow resume.",
2172
+ "Use /freeflow profile coordinator|helper|executor|auto|history, or /freeflow resume.",
1577
2173
  );
1578
2174
  const result = words[1] === "auto" ? await this.setAutomaticControl() : await this.setManualProfile(words[1]);
1579
2175
  ctx.ui.notify(JSON.stringify(result), result.status === "blocked" ? "warning" : "info");