@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, type SessionReader } from "../session-sources/events.js";
4
8
  import { Sources, bodyHash, textRef, isTaskEvidence, type Source } from "../session-sources/sources.js";
5
- import { pairFromProfile, type CognitiveRoutingCapabilityState } from "./config.js";
9
+ import { pairFromProfile, resolveCognitiveRoutingState, type CognitiveRoutingCapabilityState } from "./config.js";
6
10
  import { prepareView, changeSelection, representationProblems, type PreparedView } from "./projection.js";
7
11
  import { initialState } from "./state.js";
8
12
  import {
@@ -12,13 +16,18 @@ import {
12
16
  RoutingError,
13
17
  canonical,
14
18
  emptySelection,
19
+ idFor,
20
+ isWorkerProfile,
15
21
  requireCondition as check,
16
22
  samePair,
23
+ workersForDelegation,
17
24
  type Profile,
25
+ type WorkerProfile,
18
26
  type View,
19
27
  type Pair,
20
28
  type State,
21
29
  type Handoff,
30
+ type Recovery,
22
31
  type NativeEntry,
23
32
  type EventData,
24
33
  type Execution,
@@ -31,6 +40,7 @@ interface Turn {
31
40
  id: string;
32
41
  profile: View;
33
42
  assignmentId?: string;
43
+ recoveryId?: string;
34
44
  basisUserEntryId: string | null;
35
45
  before: Set<string>;
36
46
  message?: any;
@@ -77,7 +87,10 @@ export class RoutingRuntime {
77
87
  private manualHold?: Profile;
78
88
  private automaticControl = false;
79
89
  private sourceCache?: { entries: NativeEntry[]; authors: number; source: Sources };
80
- constructor(private readonly pi: any) {}
90
+ constructor(
91
+ private readonly pi: any,
92
+ private readonly packageRoots: readonly string[] = [],
93
+ ) {}
81
94
  private subject(): Subject {
82
95
  const branch = this.store?.reader.getBranch() ?? [];
83
96
  return {
@@ -108,6 +121,29 @@ export class RoutingRuntime {
108
121
  ? state.resumeBasis.get(assignmentId)
109
122
  : state.assignments.get(assignmentId)?.basisUserEntryId;
110
123
  }
124
+ private enabledWorkers(): readonly WorkerProfile[] {
125
+ return workersForDelegation(this.capability?.delegation ?? "executor");
126
+ }
127
+ private assignedWorker(state: State, assignmentId = state.assignmentId): WorkerProfile {
128
+ const assignment = assignmentId ? state.assignments.get(assignmentId) : undefined;
129
+ const handoff = assignment ? state.handoffs.get(assignment.delegateHandoffId) : undefined;
130
+ check(
131
+ handoff?.kind === "delegate" && handoff.assignmentId === assignment?.id && isWorkerProfile(handoff.to),
132
+ "assignment_worker_missing",
133
+ "The current assignment has no valid recorded worker.",
134
+ );
135
+ return handoff.to;
136
+ }
137
+ private requiredProfiles(state = this.stateData()): Profile[] {
138
+ const profiles = new Set<Profile>(["coordinator", ...this.enabledWorkers()]);
139
+ if (state.profile) profiles.add(state.profile);
140
+ if (this.manualHold) profiles.add(this.manualHold);
141
+ const assignment = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
142
+ if (assignment?.state === "outstanding" || state.recoveryId) profiles.add(this.assignedWorker(state));
143
+ const pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
144
+ if (pending && isWorkerProfile(pending.to)) profiles.add(pending.to);
145
+ return [...profiles];
146
+ }
111
147
  private retireUnbound(reason: string): void {
112
148
  for (const execution of this.stateData().executions.values()) {
113
149
  if (!execution.assistantEntryId && !execution.interrupted)
@@ -124,6 +160,7 @@ export class RoutingRuntime {
124
160
  id: t.id,
125
161
  profile: t.profile,
126
162
  assignmentId: t.assignmentId,
163
+ recoveryId: t.recoveryId,
127
164
  basisUserEntryId: t.basisUserEntryId,
128
165
  pair: t.pair,
129
166
  resultEntryIds: [],
@@ -139,7 +176,7 @@ export class RoutingRuntime {
139
176
  async beforeRun(ctx: any): Promise<void> {
140
177
  this.ctx = ctx;
141
178
  if (!this.store || !this.supported() || this.store.blocked || this.error) return;
142
- if (this.stateData().control === "automatic" && this.stateData().profile === "executor") {
179
+ if (this.stateData().control === "automatic" && isWorkerProfile(this.stateData().profile)) {
143
180
  const result = await this.control("coordinator", false);
144
181
  check(result.status !== "blocked", "reconciliation_required", result.reason);
145
182
  }
@@ -185,6 +222,7 @@ export class RoutingRuntime {
185
222
  return {
186
223
  effective: this.supported() && state.control !== "inactive" && !blocked,
187
224
  activeProfile: state.profile,
225
+ delegation: this.capability?.delegation ?? "executor",
188
226
  controlMode: state.control === "manual" ? `manual-${state.profile}` : state.control,
189
227
  runtimeStatus: blocked
190
228
  ? ("blocked" as const)
@@ -231,11 +269,36 @@ export class RoutingRuntime {
231
269
  if (profile) check(state.profile === profile && this.turn?.profile === profile, "wrong_profile");
232
270
  if (state.profile) check(samePair(this.observed(), this.profilePair(state.profile)), "configuration_mismatch");
233
271
  }
234
- private profilePair(profile: Profile): Pair {
272
+ private configuredProfilePair(profile: Profile): Pair {
235
273
  const configured = this.capability?.profiles[profile];
236
274
  check(configured, "profile_missing");
237
275
  return pairFromProfile(configured);
238
276
  }
277
+ private profilePair(profile: Profile): Pair {
278
+ return this.stateData().profileOverrides.get(profile) ?? this.configuredProfilePair(profile);
279
+ }
280
+ private profilePairs(profiles = this.requiredProfiles()): Partial<Record<Profile, Pair>> {
281
+ return Object.fromEntries(profiles.map((profile) => [profile, this.profilePair(profile)]));
282
+ }
283
+ private async validateProfilePairs(pairs: Partial<Record<Profile, Pair>>): Promise<void> {
284
+ check(pairs.coordinator, "profile_missing", "Configure the coordinator profile.");
285
+ const helper = pairs.helper !== undefined;
286
+ const executor = pairs.executor !== undefined;
287
+ check(helper || executor, "profile_missing", "Configure an enabled worker profile.");
288
+ const delegation = helper && executor ? "both" : helper ? "helper" : "executor";
289
+ const profiles = Object.fromEntries(
290
+ Object.entries(pairs).map(([profile, pair]) => [
291
+ profile,
292
+ { provider: pair!.provider, model: pair!.modelId, thinking: pair!.thinking },
293
+ ]),
294
+ );
295
+ const result = await resolveCognitiveRoutingState(
296
+ { cognitiveRouting: { enabled: true, delegation, profiles } },
297
+ {},
298
+ this.ctx,
299
+ );
300
+ check(result.effective, result.blockingReason.code, result.blockingReason.message);
301
+ }
239
302
  private model(profile: Profile) {
240
303
  const pair = this.profilePair(profile);
241
304
  const model = this.ctx?.modelRegistry?.find(pair.provider, pair.modelId);
@@ -247,6 +310,8 @@ export class RoutingRuntime {
247
310
  this.ctx = ctx;
248
311
  this.capability = capability;
249
312
  this.turn = undefined;
313
+ this.manualHold = undefined;
314
+ this.automaticControl = false;
250
315
  this.messages = [];
251
316
  this.sourceCache = undefined;
252
317
  this.receipts.clear();
@@ -256,7 +321,7 @@ export class RoutingRuntime {
256
321
  this.error = undefined;
257
322
  this.projectionError = undefined;
258
323
  this.operations = Promise.resolve();
259
- this.targetSignature = canonical(capability.profiles);
324
+ this.targetSignature = canonical({ delegation: capability.delegation, profiles: capability.profiles });
260
325
  this.suppressed = startup && process.argv.some((a) => /^(--model|--thinking)(=|$)/.test(a));
261
326
  this.store = new EventStore(this.pi, ctx.sessionManager as SessionReader);
262
327
  const subject = this.subject();
@@ -267,6 +332,7 @@ export class RoutingRuntime {
267
332
  const state = this.stateData();
268
333
  this.manualHold = state.control === "manual" ? state.profile : undefined;
269
334
  this.automaticControl = !state.events.size || state.control === "automatic";
335
+ await this.validateProfilePairs(this.profilePairs(this.requiredProfiles(state)));
270
336
  this.retireUnbound("Session rebind found an unfinished execution; no task effects replayed.");
271
337
  if (!state.events.size) {
272
338
  this.append({
@@ -307,7 +373,7 @@ export class RoutingRuntime {
307
373
  async refresh(ctx: any, capability: CognitiveRoutingCapabilityState): Promise<void> {
308
374
  this.ctx = ctx;
309
375
  const was = this.capability?.effective === true;
310
- const signature = canonical(capability.profiles);
376
+ const signature = canonical({ delegation: capability.delegation, profiles: capability.profiles });
311
377
  if (this.capability && (was !== capability.effective || signature !== this.targetSignature)) this.revision++;
312
378
  this.capability = capability;
313
379
  if (!capability.effective) {
@@ -339,7 +405,8 @@ export class RoutingRuntime {
339
405
  } else if (this.targetSignature && signature !== this.targetSignature) {
340
406
  this.error = "configuration_changed: reconcile profile configuration before automatic execution";
341
407
  }
342
- if (canonical(this.capability?.profiles) === signature) this.targetSignature = signature;
408
+ if (canonical({ delegation: this.capability?.delegation, profiles: this.capability?.profiles }) === signature)
409
+ this.targetSignature = signature;
343
410
  }
344
411
  async ancestryChanged(ctx: any, navigation = true): Promise<void> {
345
412
  this.revision++;
@@ -355,8 +422,9 @@ export class RoutingRuntime {
355
422
  await this.store.reconcile();
356
423
  this.guard(subject);
357
424
  this.error = undefined;
358
- this.retireUnbound("Selected historical ancestry ends before execution binding; effects are not replayed.");
359
425
  const state = this.stateData();
426
+ await this.validateProfilePairs(this.profilePairs(this.requiredProfiles(state)));
427
+ this.retireUnbound("Selected historical ancestry ends before execution binding; effects are not replayed.");
360
428
  if (this.manualHold) {
361
429
  this.append({
362
430
  type: "control",
@@ -364,6 +432,7 @@ export class RoutingRuntime {
364
432
  profile: this.manualHold,
365
433
  reason: "Current explicit manual hold survives navigation",
366
434
  });
435
+ await this.applyPair(this.profilePair(this.manualHold));
367
436
  } else if (
368
437
  (this.automaticControl || !state.events.size) &&
369
438
  (navigation || !samePair(this.observed(), this.profilePair(state.profile ?? "coordinator")))
@@ -445,6 +514,12 @@ export class RoutingRuntime {
445
514
  return this.enqueue(async () => {
446
515
  try {
447
516
  check(this.capability?.effective && this.store, "routing_unavailable");
517
+ if (manual && isWorkerProfile(profile))
518
+ check(
519
+ this.enabledWorkers().includes(profile),
520
+ "worker_disabled",
521
+ `${profile} is not enabled by delegation mode.`,
522
+ );
448
523
  await this.store.reconcile();
449
524
  this.guard(subject);
450
525
  this.error = undefined;
@@ -482,7 +557,8 @@ export class RoutingRuntime {
482
557
  this.automaticControl = !manual;
483
558
  return { status: manual ? "active" : "automatic" };
484
559
  } catch (error) {
485
- if (this.current(subject)) this.mark(error);
560
+ if (this.current(subject) && !(error instanceof RoutingError && error.code === "worker_disabled"))
561
+ this.mark(error);
486
562
  return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
487
563
  }
488
564
  });
@@ -490,6 +566,124 @@ export class RoutingRuntime {
490
566
  setManualProfile(profile: Profile, _mechanism?: string) {
491
567
  return this.control(profile, true);
492
568
  }
569
+ sessionProfileOverrides(): Partial<Record<Profile, Pair>> {
570
+ return Object.fromEntries(this.stateData().profileOverrides) as Partial<Record<Profile, Pair>>;
571
+ }
572
+ async setSessionProfileOverride(
573
+ profile: Profile,
574
+ override: Pair | null,
575
+ mechanism = "Session profile override",
576
+ ): Promise<{ status: string; reason?: string }> {
577
+ this.revision++;
578
+ const subject = this.subject();
579
+ return this.enqueue(async () => {
580
+ try {
581
+ check(this.capability?.effective && this.store, "routing_unavailable");
582
+ check(
583
+ this.ctx?.isIdle?.() !== false,
584
+ "host_busy",
585
+ "Wait for Pi to become idle before changing session presets.",
586
+ );
587
+ await this.store.reconcile();
588
+ this.guard(subject);
589
+ const state = this.stateData();
590
+ const previous = state.profileOverrides.get(profile);
591
+ if ((override === null && !previous) || (override !== null && samePair(previous, override)))
592
+ return { status: "unchanged" };
593
+ const target = override ?? this.configuredProfilePair(profile);
594
+ const profiles = new Set(this.requiredProfiles(state));
595
+ profiles.add(profile);
596
+ const pairs = this.profilePairs([...profiles]);
597
+ pairs[profile] = target;
598
+ await this.validateProfilePairs(pairs);
599
+ this.append({
600
+ type: "profile-overrides",
601
+ overrides: { [profile]: override },
602
+ reason: mechanism,
603
+ });
604
+ if (state.control !== "inactive" && state.profile === profile) {
605
+ try {
606
+ await this.applyPair(target);
607
+ this.guard(subject);
608
+ } catch (error) {
609
+ if (this.current(subject) && !this.externalChange && !this.error && !this.store.blocked) {
610
+ this.append({
611
+ type: "profile-overrides",
612
+ overrides: { [profile]: previous ?? null },
613
+ reason: "Session profile application failed; prior override retained",
614
+ });
615
+ }
616
+ throw error;
617
+ }
618
+ this.error = undefined;
619
+ return { status: "active" };
620
+ }
621
+ this.error = undefined;
622
+ return { status: "stored" };
623
+ } catch (error) {
624
+ if (this.current(subject)) this.mark(error);
625
+ return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
626
+ }
627
+ });
628
+ }
629
+ async resetSessionProfileOverrides(
630
+ mechanism = "Reset session profile overrides",
631
+ ): Promise<{ status: string; reason?: string }> {
632
+ this.revision++;
633
+ const subject = this.subject();
634
+ return this.enqueue(async () => {
635
+ try {
636
+ check(this.capability?.effective && this.store, "routing_unavailable");
637
+ check(
638
+ this.ctx?.isIdle?.() !== false,
639
+ "host_busy",
640
+ "Wait for Pi to become idle before resetting session presets.",
641
+ );
642
+ await this.store.reconcile();
643
+ this.guard(subject);
644
+ const state = this.stateData();
645
+ if (state.profileOverrides.size === 0) return { status: "unchanged" };
646
+ const previous = Object.fromEntries(state.profileOverrides) as Partial<Record<Profile, Pair>>;
647
+ const required = this.requiredProfiles(state);
648
+ const targetPairs = Object.fromEntries(
649
+ required.map((profile) => [profile, this.configuredProfilePair(profile)]),
650
+ ) as Partial<Record<Profile, Pair>>;
651
+ await this.validateProfilePairs(targetPairs);
652
+ this.append({
653
+ type: "profile-overrides",
654
+ overrides: { coordinator: null, helper: null, executor: null },
655
+ reason: mechanism,
656
+ });
657
+ const active = state.control !== "inactive" ? state.profile : undefined;
658
+ if (active && previous[active]) {
659
+ try {
660
+ await this.applyPair(targetPairs[active]!);
661
+ this.guard(subject);
662
+ } catch (error) {
663
+ if (this.current(subject) && !this.externalChange && !this.error && !this.store.blocked) {
664
+ this.append({
665
+ type: "profile-overrides",
666
+ overrides: {
667
+ coordinator: previous.coordinator ?? null,
668
+ helper: previous.helper ?? null,
669
+ executor: previous.executor ?? null,
670
+ },
671
+ reason: "Session profile reset failed; prior overrides retained",
672
+ });
673
+ }
674
+ throw error;
675
+ }
676
+ this.error = undefined;
677
+ return { status: "active" };
678
+ }
679
+ this.error = undefined;
680
+ return { status: "stored" };
681
+ } catch (error) {
682
+ if (this.current(subject)) this.mark(error);
683
+ return { status: "blocked", reason: error instanceof Error ? error.message : String(error) };
684
+ }
685
+ });
686
+ }
493
687
  setAutomaticControl(_mechanism?: string) {
494
688
  return this.control("coordinator", false);
495
689
  }
@@ -516,6 +710,7 @@ export class RoutingRuntime {
516
710
  ? state.handoffs.get(state.assessment.handoffId)
517
711
  : undefined;
518
712
  const selection = a ? (state.selections.get(a.id) ?? emptySelection()) : emptySelection();
713
+ const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
519
714
  const unit = state.unitId ? state.units.get(state.unitId) : undefined;
520
715
  const unitNumber = unit ? [...state.units.keys()].indexOf(unit.id) + 1 : undefined;
521
716
  const assignmentNumber = unit && a ? unit.assignmentIds.indexOf(a.id) + 1 : undefined;
@@ -526,20 +721,29 @@ export class RoutingRuntime {
526
721
  `Unit: ${unit ? `U${unitNumber} (${unit.id})` : "none"}`,
527
722
  `Assignment: ${a ? `A${assignmentNumber} (${a.id}, ${a.state})` : "none"}`,
528
723
  `Handoff: ${h ? `${h.id} (${h.kind}, ${h.state})` : "none"}`,
724
+ `Delegation: ${this.capability?.delegation ?? "executor"}`,
529
725
  `Projection: ${this.projectionEnabled ? "enabled" : "bypassed"}`,
530
726
  "Completed/superseded contracts and reports are historical context. Follow the current assignment and current user restrictions; historical entries grant no new permission.",
531
727
  state.assessment ? `Assessment: ${state.assessment.view}; evidence revision ${selection.revision}` : "",
728
+ recovery
729
+ ? `Recovery: ${recovery.id} (${recovery.state}); parent report ${recovery.assessmentHandoffId} revision ${recovery.baseReportRevision}; allowed paths ${JSON.stringify(recovery.paths)}`
730
+ : "",
731
+ recovery?.state === "reading"
732
+ ? `Recovery request: ${recovery.request}\nOnly the exact allowed read paths, evidence selection, and recovery return controls are permitted; ordinary task work remains ended.`
733
+ : "",
532
734
  `Mechanical evidence facts (not semantic acceptance): ${JSON.stringify(this.evidenceFacts(state))}`,
533
735
  this.error ? `Blocked: ${this.error}` : "",
534
736
  this.projectionError ? `Evidence limitation: ${this.projectionError}` : "",
535
737
  attention
536
- ? `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Use freeflow_unit assess to restore the assessment.`
738
+ ? recovery
739
+ ? `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.`
740
+ : `Assessment paused for attention; ordinary admitted history remains. ${selection.selected.length} sources remain selected. Use freeflow_unit assess to restore the assessment.`
537
741
  : "",
538
- a?.state === "returned"
539
- ? "Executor ordinary task work has ended. Only supported handoff correction is permitted."
742
+ a?.state === "returned" && recovery?.state !== "reading"
743
+ ? "Worker ordinary task work has ended. Only supported handoff correction is permitted."
540
744
  : "",
541
- this.turn?.profile === "executor" && a && this.turn.basisUserEntryId !== this.taskBasis(state, a.id)
542
- ? "Current input differs from the original assignment. Account for it; changed direction requires Coordinator attention."
745
+ isWorkerProfile(this.turn?.profile) && a && this.turn.basisUserEntryId !== this.workerBasis(state, a.id)
746
+ ? `Current input differs from the active ${this.turn.profile} responsibility. Account for it; changed direction requires Coordinator attention.`
543
747
  : "",
544
748
  ...(state.assessment?.problems ?? []).map((p) => `${p.code}: ${p.detail}`),
545
749
  ]
@@ -558,16 +762,19 @@ export class RoutingRuntime {
558
762
  const selection = state.assignmentId
559
763
  ? (state.selections.get(state.assignmentId) ?? emptySelection())
560
764
  : emptySelection();
765
+ const sources = this.sources(state);
561
766
  return {
562
767
  revision: selection.revision,
563
768
  selected: selection.selected.map((ref) => {
564
769
  const entry = this.ctx?.sessionManager.getEntry?.(ref.slice(4).replace(/#text$/, ""));
770
+ const locator = sources.locator(ref);
565
771
  return {
566
772
  ref,
567
773
  kind: ref.endsWith("#text") ? "assistant-text" : (entry?.message?.role ?? "unknown"),
568
774
  toolName: entry?.message?.toolName,
569
775
  producer: state.authors.get(entry?.id)?.profile ?? "common",
570
776
  assignment: state.authors.get(entry?.id)?.assignmentId,
777
+ ...(locator ? { locator } : {}),
571
778
  };
572
779
  }),
573
780
  unresolved: selection.unresolved,
@@ -589,18 +796,9 @@ export class RoutingRuntime {
589
796
  pair: this.profilePair(profile),
590
797
  systemPrompt: this.ctx.getSystemPrompt?.() ?? "",
591
798
  tools: (this.pi.getAllTools?.() ?? [])
592
- .filter((tool: any) =>
593
- ROUTING_TOOLS.includes(tool.name)
594
- ? tool.name === "freeflow_unit" ||
595
- (profile === "coordinator"
596
- ? tool.name === "freeflow_delegate"
597
- : tool.name === "freeflow_return" || (tool.name === "freeflow_project" && this.projectionEnabled))
598
- : this.pi.getActiveTools?.().includes(tool.name),
599
- )
799
+ .filter((tool: any) => this.pi.getActiveTools?.().includes(tool.name))
600
800
  .map((tool: any) =>
601
- ROUTING_TOOLS.includes(tool.name)
602
- ? { ...tool, parameters: schemaForProfile(tool.name, profile === "coordinator") }
603
- : tool,
801
+ ROUTING_TOOLS.includes(tool.name) ? { ...tool, parameters: ROUTING_SCHEMAS[tool.name] } : tool,
604
802
  ),
605
803
  runtimeMessage: this.runtimeMessage(
606
804
  state,
@@ -611,6 +809,35 @@ export class RoutingRuntime {
611
809
  restoring,
612
810
  });
613
811
  }
812
+ budgetNotice(messages: any[], ctx: any): any | undefined {
813
+ if (!this.supported()) return;
814
+ const tools = (this.pi.getAllTools?.() ?? []).filter((tool: any) => this.pi.getActiveTools?.().includes(tool.name));
815
+ const estimate = estimateRequest(ctx.getSystemPrompt?.() ?? "", tools, messages, ctx.model);
816
+ if (!estimate.warnings.length) return;
817
+ return {
818
+ role: "custom",
819
+ customType: "freeflow-routing-budget",
820
+ display: false,
821
+ content: estimate.warnings.map((warning) => warning.detail).join("\n"),
822
+ timestamp: 0,
823
+ };
824
+ }
825
+ private ordinaryContext(input: any[]): any[] {
826
+ try {
827
+ const sources = this.sources(),
828
+ associated = sources.associate(input);
829
+ const mapped = new Map(associated.filter((item) => item.source).map((item) => [item.message, item.source!]));
830
+ return annotateSources(
831
+ input,
832
+ mapped,
833
+ new Set([...mapped.values()].map((source) => source.ref)),
834
+ sources,
835
+ this.token,
836
+ );
837
+ } catch {
838
+ return input;
839
+ }
840
+ }
614
841
  async context(ctx: any, incoming: any[]): Promise<any[]> {
615
842
  this.ctx = ctx;
616
843
  const input = incoming.filter(
@@ -621,10 +848,10 @@ export class RoutingRuntime {
621
848
  ),
622
849
  );
623
850
  this.messages = input;
624
- if (!this.supported() || !this.store) return input;
851
+ if (!this.supported() || !this.store) return this.ordinaryContext(input);
625
852
  try {
626
853
  const state = this.stateData();
627
- if (state.control !== "automatic") return input;
854
+ if (state.control !== "automatic") return this.ordinaryContext(input);
628
855
  check(!this.error && !this.store.blocked && state.profile, "routing_blocked", this.error ?? this.store.blocked);
629
856
  check(samePair(this.observed(), this.profilePair(state.profile!)), "prepared_pair_mismatch");
630
857
  const sources = this.sources(state),
@@ -640,16 +867,19 @@ export class RoutingRuntime {
640
867
  },
641
868
  JSON.stringify(["attention", state.assessment.handoffId, user]),
642
869
  );
643
- // New input reaching an already prepared Executor request cannot be rerouted by changing only the live model.
870
+ // New input reaching an already prepared worker request cannot be rerouted by changing only the live model.
644
871
  const outstanding = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
872
+ const worker = outstanding ? this.assignedWorker(state, outstanding.id) : undefined;
645
873
  const interrupted =
646
- state.profile === "executor" && user !== (outstanding ? this.taskBasis(state, outstanding.id) : undefined);
874
+ isWorkerProfile(state.profile) &&
875
+ (state.profile !== worker || user !== (outstanding ? this.workerBasis(state, outstanding.id) : undefined));
647
876
  if (!this.turn || this.turn.bound) {
648
877
  this.receipts.clear();
649
878
  const execution: Execution = {
650
879
  id: randomUUID(),
651
880
  profile: state.profile!,
652
881
  assignmentId: state.assignmentId,
882
+ recoveryId: state.recoveryId,
653
883
  basisUserEntryId: user,
654
884
  pair: this.observed()!,
655
885
  resultEntryIds: [],
@@ -659,6 +889,7 @@ export class RoutingRuntime {
659
889
  id: execution.id,
660
890
  profile: execution.profile,
661
891
  assignmentId: execution.assignmentId,
892
+ recoveryId: execution.recoveryId,
662
893
  basisUserEntryId: user,
663
894
  before,
664
895
  pair: execution.pair,
@@ -681,12 +912,12 @@ export class RoutingRuntime {
681
912
  check(prepared.ready, "context_unavailable", prepared.problems.map((p) => p.detail).join("; "));
682
913
  const known = this.stateData().exposure;
683
914
  const added = prepared.fullSources.filter((s) => known.get(s.ref) !== s.hash);
684
- if (this.turn.profile === "executor")
915
+ if (isWorkerProfile(this.turn.profile))
685
916
  for (let i = 0; i < added.length; i += 128)
686
917
  this.append({
687
918
  type: "sources-exposed",
688
919
  executionId: this.turn.id,
689
- view: "executor",
920
+ view: this.turn.profile,
690
921
  sources: added.slice(i, i + 128).map((s) => ({ ref: s.ref, bodyHash: s.hash })),
691
922
  });
692
923
  if (interrupted)
@@ -703,7 +934,7 @@ export class RoutingRuntime {
703
934
  } catch (error) {
704
935
  this.mark(error);
705
936
  ctx.abort?.();
706
- // Do not send an accidental full Executor history on a projection failure.
937
+ // Do not send an accidental full worker history on a projection failure.
707
938
  return [
708
939
  {
709
940
  role: "custom",
@@ -711,7 +942,7 @@ export class RoutingRuntime {
711
942
  content: `Automatic request blocked: ${this.error}`,
712
943
  display: false,
713
944
  timestamp: 0,
714
- details: { routingInstance: this.token },
945
+ details: { routingInstance: this.token, routingRequestBlocked: true },
715
946
  },
716
947
  ];
717
948
  }
@@ -722,6 +953,57 @@ export class RoutingRuntime {
722
953
  private contextOperation(name: string, input: any): boolean {
723
954
  return name === "freeflow_context" && ["archive", "restore", "search", "retrieve"].includes(input?.operation);
724
955
  }
956
+ private handoffOperation(name: string, input: any): boolean {
957
+ if (name === "freeflow_delegate") return true;
958
+ if (name === "freeflow_return") return ["submit", "supplement", "retry"].includes(input?.operation);
959
+ return name === "freeflow_unit" && input?.operation === "recover";
960
+ }
961
+ private stableRecoveryPath(path: string): boolean {
962
+ return (
963
+ !path.startsWith("@") &&
964
+ path !== "~" &&
965
+ !path.startsWith("~/") &&
966
+ !path.startsWith("file://") &&
967
+ !/[\u00a0\u2000-\u200a\u202f\u205f\u3000]/u.test(path) &&
968
+ !(process.platform === "win32" && /^\/(?:mnt\/|cygdrive\/)?[a-z](?:\/|$)/i.test(path))
969
+ );
970
+ }
971
+ private canonicalRecoveryPath(path: string): string {
972
+ check(this.stableRecoveryPath(path), "recovery_path_unsupported", `Recovery path spelling is unsupported: ${path}`);
973
+ const requested = resolvePath(this.ctx.cwd, path);
974
+ check(existsSync(requested), "recovery_path_unavailable", `Recovery path is unavailable: ${path}`);
975
+ try {
976
+ return realpathSync(requested);
977
+ } catch {
978
+ throw new RoutingError("recovery_path_unavailable", `Recovery path is unavailable: ${path}`);
979
+ }
980
+ }
981
+ private packagedInstruction(path: string): boolean {
982
+ return this.packageRoots.some((root) => {
983
+ const inside = relative(root, path);
984
+ if (!inside || inside.startsWith("..") || resolvePath(root, inside) !== path) return false;
985
+ return (
986
+ /^(?:skills|capabilities)\/[^/]+\/SKILL\.md$/.test(inside) ||
987
+ /^(?:skills|capabilities)\/[^/]+\/references\/[^/]+\.md$/.test(inside)
988
+ );
989
+ });
990
+ }
991
+ private recoveryReadAllowed(state: State, name: string, input: any): boolean {
992
+ if (name !== "read" || typeof input?.path !== "string" || !state.recoveryId) return false;
993
+ const recovery = state.recoveries.get(state.recoveryId);
994
+ if (!recovery || recovery.state !== "reading" || !this.stableRecoveryPath(input.path)) return false;
995
+ try {
996
+ const path = this.canonicalRecoveryPath(input.path);
997
+ return recovery.paths.includes(path) || this.packagedInstruction(path);
998
+ } catch {
999
+ return false;
1000
+ }
1001
+ }
1002
+ private workerBasis(state: State, assignmentId: string): string | null {
1003
+ const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
1004
+ if (recovery?.state === "reading") return state.handoffs.get(recovery.requestHandoffId)?.basisUserEntryId ?? null;
1005
+ return this.taskBasis(state, assignmentId);
1006
+ }
725
1007
  private batch(callId: string, name: string): void {
726
1008
  check(this.turn?.message, "batch_unavailable");
727
1009
  const matches = (this.ctx.sessionManager.getBranch() as NativeEntry[]).filter(
@@ -739,14 +1021,16 @@ export class RoutingRuntime {
739
1021
  this.turn.sourceFingerprint = fingerprint;
740
1022
  this.turn.message = structuredClone(matches[0].message);
741
1023
  const calls = (this.turn.message.content ?? []).filter((b: any) => b.type === "toolCall");
742
- const handoffs = calls.filter((b: any) => HANDOFF_TOOLS.has(b.name));
1024
+ const handoffs = calls.filter((b: any) => this.handoffOperation(b.name, b.arguments));
743
1025
  check(
744
1026
  !handoffs.length ||
745
1027
  (handoffs.length === 1 &&
746
- HANDOFF_TOOLS.has(calls.at(-1)?.name) &&
1028
+ this.handoffOperation(calls.at(-1)?.name, calls.at(-1)?.arguments) &&
747
1029
  calls.every(
748
1030
  (b: any) =>
749
- HANDOFF_TOOLS.has(b.name) || b.name === "freeflow_project" || this.contextOperation(b.name, b.arguments),
1031
+ this.handoffOperation(b.name, b.arguments) ||
1032
+ b.name === "freeflow_project" ||
1033
+ this.contextOperation(b.name, b.arguments),
750
1034
  )),
751
1035
  "invalid_handoff_batch",
752
1036
  "A handoff must be last and cannot accompany ordinary task tools.",
@@ -770,14 +1054,21 @@ export class RoutingRuntime {
770
1054
  this.batch(event.toolCallId, name);
771
1055
  check(!this.error && !this.store?.blocked, "routing_blocked");
772
1056
  this.openTurn();
773
- if (this.turn?.profile === "executor" && !isRouting && !this.contextOperation(name, event.input)) {
1057
+ if (isWorkerProfile(this.turn?.profile) && !isRouting && !this.contextOperation(name, event.input)) {
774
1058
  const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
775
- check(
1059
+ const worker = a ? this.assignedWorker(state, a.id) : undefined;
1060
+ const ordinary =
776
1061
  a?.state === "outstanding" &&
777
- this.turn.basisUserEntryId === this.taskBasis(state, a.id) &&
778
- state.profile === "executor",
779
- "executor_task_phase_ended",
780
- );
1062
+ this.turn.basisUserEntryId === this.taskBasis(state, a.id) &&
1063
+ state.profile === worker &&
1064
+ this.turn.profile === worker;
1065
+ const recovery =
1066
+ a?.state === "returned" &&
1067
+ this.turn.basisUserEntryId === this.workerBasis(state, a.id) &&
1068
+ state.profile === worker &&
1069
+ this.turn.profile === worker &&
1070
+ this.recoveryReadAllowed(state, name, event.input);
1071
+ check(ordinary || recovery, "worker_task_phase_ended");
781
1072
  }
782
1073
  return undefined;
783
1074
  } catch (error) {
@@ -797,8 +1088,14 @@ export class RoutingRuntime {
797
1088
  this.ctx = ctx;
798
1089
  try {
799
1090
  check(!signal?.aborted, "cancelled");
800
- if (name === "freeflow_unit" && input.operation === "inspect") return this.result(this.inspectUnit(input));
801
- this.assertAvailable(name === "freeflow_delegate" || name === "freeflow_unit" ? "coordinator" : "executor");
1091
+ if (name === "freeflow_unit" && input.operation === "inspect") {
1092
+ check(this.supported(), "routing_unavailable");
1093
+ return this.result(this.inspectUnit(input));
1094
+ }
1095
+ const state = this.stateData();
1096
+ const expected =
1097
+ name === "freeflow_delegate" || name === "freeflow_unit" ? "coordinator" : this.assignedWorker(state);
1098
+ this.assertAvailable(expected);
802
1099
  const op = this.callOperation(callId, name);
803
1100
  const cached = this.receipts.get(op);
804
1101
  if (cached) {
@@ -813,7 +1110,7 @@ export class RoutingRuntime {
813
1110
  : name === "freeflow_project"
814
1111
  ? this.project(input, op)
815
1112
  : name === "freeflow_unit"
816
- ? this.unit(input, op)
1113
+ ? this.unit(input, callId, op)
817
1114
  : undefined;
818
1115
  if (value !== undefined) {
819
1116
  this.receipts.set(op, { input: canonical(input), value: structuredClone(value) });
@@ -832,7 +1129,13 @@ export class RoutingRuntime {
832
1129
  ? undefined
833
1130
  : name === "freeflow_delegate" || name === "freeflow_unit"
834
1131
  ? "coordinator"
835
- : "executor",
1132
+ : (() => {
1133
+ try {
1134
+ return this.assignedWorker(this.stateData());
1135
+ } catch {
1136
+ return undefined;
1137
+ }
1138
+ })(),
836
1139
  recoveryAction:
837
1140
  error instanceof RoutingError &&
838
1141
  [
@@ -853,26 +1156,48 @@ export class RoutingRuntime {
853
1156
  return [...this.stateData().events.values()].find(
854
1157
  (e) =>
855
1158
  e.operationId === op &&
856
- ["delegate-accepted", "return-accepted", "handoff-retry-requested"].includes(e.data.type),
1159
+ [
1160
+ "delegate-accepted",
1161
+ "return-accepted",
1162
+ "recovery-request-accepted",
1163
+ "recovery-supplement-accepted",
1164
+ "recovery-cancelled",
1165
+ "handoff-retry-requested",
1166
+ ].includes(e.data.type),
857
1167
  );
858
1168
  }
859
1169
  private delegate(input: any, callId: string, op: string) {
860
1170
  const prior = this.duplicate(op);
861
1171
  if (prior) {
862
1172
  check(
863
- prior.data.type === "delegate-accepted" && prior.data.assignment.contract === input.contract,
1173
+ prior.data.type === "delegate-accepted" &&
1174
+ prior.data.assignment.contract === input.contract &&
1175
+ (input.worker === undefined || prior.data.handoff.to === input.worker),
864
1176
  "operation_conflict",
865
1177
  );
866
1178
  return { status: "accepted", handoff: prior.data.handoff.id, unchanged: true };
867
1179
  }
868
1180
  const state = this.stateData();
1181
+ const enabled = this.enabledWorkers();
1182
+ let worker: WorkerProfile;
1183
+ if (input.worker === undefined) {
1184
+ check(enabled.length === 1, "worker_required", "Delegation mode 'both' requires choosing helper or executor.");
1185
+ worker = enabled[0]!;
1186
+ } else {
1187
+ check(isWorkerProfile(input.worker), "invalid_worker");
1188
+ worker = input.worker;
1189
+ }
1190
+ check(enabled.includes(worker), "worker_disabled", `${worker} is not enabled by delegation mode.`);
869
1191
  const unit = state.unitId
870
1192
  ? state.units.get(state.unitId)!
871
1193
  : { id: randomUUID(), objective: input.contract, state: "open" as const, assignmentIds: [] };
872
1194
  const old = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
873
1195
  if (input.operation === "replace") check(old?.state === "outstanding" && !this.applying, "no_quiescent_assignment");
874
1196
  else
875
- check(input.operation === "assign" && old?.state !== "outstanding" && !state.pendingId, "assignment_outstanding");
1197
+ check(
1198
+ input.operation === "assign" && old?.state !== "outstanding" && !state.pendingId && !state.recoveryId,
1199
+ "assignment_outstanding",
1200
+ );
876
1201
  const assignmentId = randomUUID(),
877
1202
  id = randomUUID();
878
1203
  const assignment = {
@@ -889,7 +1214,7 @@ export class RoutingRuntime {
889
1214
  assignmentId,
890
1215
  text: input.contract,
891
1216
  from: "coordinator",
892
- to: "executor",
1217
+ to: worker,
893
1218
  executionId: this.turn!.id,
894
1219
  toolCallId: callId,
895
1220
  basisUserEntryId: this.turn!.basisUserEntryId,
@@ -919,6 +1244,7 @@ export class RoutingRuntime {
919
1244
  status: "accepted",
920
1245
  unit: unit.id,
921
1246
  assignment: assignmentId,
1247
+ worker,
922
1248
  handoff: id,
923
1249
  transition: "pending",
924
1250
  contract: input.contract,
@@ -937,25 +1263,28 @@ export class RoutingRuntime {
937
1263
  const state = this.stateData(),
938
1264
  a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
939
1265
  check(a, "assignment_missing");
940
- const saved = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
1266
+ const worker = this.assignedWorker(state, a.id);
1267
+ const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
1268
+ const original = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
1269
+ const supplement = recovery?.supplementHandoffId ? state.handoffs.get(recovery.supplementHandoffId) : undefined;
1270
+ const retryingSupplement = input.operation === "retry" && recovery?.state === "returning";
1271
+ const saved = retryingSupplement || input.operation === "supplement" ? supplement : original;
941
1272
  const selection = state.selections.get(a.id) ?? emptySelection();
942
1273
  const duplicate = this.duplicate(op);
943
1274
  if (duplicate) {
944
1275
  if (input.operation === "retry") check(duplicate.data.type === "handoff-retry-requested", "operation_conflict");
945
- else
1276
+ else {
1277
+ const expected = input.operation === "supplement" ? "recovery-supplement-accepted" : "return-accepted";
946
1278
  check(
947
- duplicate.data.type === "return-accepted" &&
1279
+ duplicate.data.type === expected &&
948
1280
  duplicate.data.handoff.text === input.report &&
949
1281
  duplicate.data.handoff.outcome === input.outcome &&
950
1282
  canonical(duplicate.data.handoff.limitations) === canonical(input.limitations ?? []),
951
1283
  "operation_conflict",
952
1284
  );
953
- return {
954
- status: "accepted",
955
- reportSaved: true,
956
- unchanged: true,
957
- handoff: duplicate.data.handoffId ?? duplicate.data.handoff.id,
958
- };
1285
+ }
1286
+ const handoff = duplicate.data.handoffId ?? duplicate.data.handoff.id;
1287
+ return { ...this.returnReadiness(handoff), unchanged: true };
959
1288
  }
960
1289
  if (input.operation === "retry") {
961
1290
  check(saved && ["blocked", "pending"].includes(saved.state), "saved_return_missing");
@@ -971,25 +1300,55 @@ export class RoutingRuntime {
971
1300
  },
972
1301
  op,
973
1302
  );
974
- return { ...this.returnReadiness(saved.id), reportUnchanged: true };
1303
+ return {
1304
+ ...this.returnReadiness(saved.id),
1305
+ reportUnchanged: true,
1306
+ supplementUnchanged: saved.kind === "recovery-return",
1307
+ };
1308
+ }
1309
+ if (input.operation === "supplement") {
1310
+ check(
1311
+ recovery &&
1312
+ ["reading", "returning"].includes(recovery.state) &&
1313
+ (!supplement || ["pending", "blocked"].includes(supplement.state)),
1314
+ "recovery_not_returnable",
1315
+ );
1316
+ const h: Handoff = {
1317
+ id: supplement?.id ?? randomUUID(),
1318
+ kind: "recovery-return",
1319
+ assignmentId: a.id,
1320
+ text: input.report,
1321
+ from: worker,
1322
+ to: "coordinator",
1323
+ executionId: this.turn!.id,
1324
+ toolCallId: callId,
1325
+ basisUserEntryId: this.turn!.basisUserEntryId,
1326
+ state: "pending",
1327
+ reportRevision: (supplement?.reportRevision ?? 0) + 1,
1328
+ outcome: input.outcome,
1329
+ limitations: input.limitations ?? [],
1330
+ };
1331
+ this.append({ type: "recovery-supplement-accepted", recoveryId: recovery.id, handoff: h }, op);
1332
+ return this.returnReadiness(h.id);
975
1333
  }
976
1334
  check(input.operation === "submit", "invalid_return_operation");
977
1335
  check(
978
- a.state === "outstanding" || (a.state === "returned" && saved && ["pending", "blocked"].includes(saved.state)),
1336
+ a.state === "outstanding" ||
1337
+ (a.state === "returned" && original && ["pending", "blocked"].includes(original.state)),
979
1338
  "assignment_task_ended",
980
1339
  );
981
1340
  const h: Handoff = {
982
- id: saved?.id ?? randomUUID(),
1341
+ id: original?.id ?? randomUUID(),
983
1342
  kind: "return",
984
1343
  assignmentId: a.id,
985
1344
  text: input.report,
986
- from: "executor",
1345
+ from: worker,
987
1346
  to: "coordinator",
988
1347
  executionId: this.turn!.id,
989
1348
  toolCallId: callId,
990
1349
  basisUserEntryId: this.turn!.basisUserEntryId,
991
1350
  state: "pending",
992
- reportRevision: (saved?.reportRevision ?? 0) + 1,
1351
+ reportRevision: (original?.reportRevision ?? 0) + 1,
993
1352
  outcome: input.outcome,
994
1353
  limitations: input.limitations ?? [],
995
1354
  };
@@ -997,16 +1356,25 @@ export class RoutingRuntime {
997
1356
  return this.returnReadiness(h.id);
998
1357
  }
999
1358
  private returnReadiness(id: string) {
1000
- const h = this.stateData().handoffs.get(id)!;
1359
+ const state = this.stateData();
1360
+ const h = state.handoffs.get(id)!;
1001
1361
  const prepared = this.prepared("coordinator", this.messages, id);
1362
+ const isSupplement = h.kind === "recovery-return";
1363
+ const recovery = isSupplement
1364
+ ? [...state.recoveries.values()].find((candidate) => candidate.supplementHandoffId === h.id)
1365
+ : undefined;
1002
1366
  return {
1003
1367
  status: "accepted",
1004
- reportSaved: true,
1368
+ reportSaved: !isSupplement,
1369
+ supplementSaved: isSupplement,
1005
1370
  handoff: id,
1006
- reportRevision: h.reportRevision,
1371
+ ...(recovery ? { recovery: recovery.id } : {}),
1372
+ ...(isSupplement ? { supplementRevision: h.reportRevision } : { reportRevision: h.reportRevision }),
1007
1373
  assignmentRef: `assignment:${h.assignmentId}`,
1008
- reportRef: `report:${h.id}:${h.reportRevision}`,
1009
- producer: "executor",
1374
+ ...(isSupplement
1375
+ ? { supplementRef: `supplement:${recovery?.id ?? "unknown"}:${h.reportRevision}` }
1376
+ : { reportRef: `report:${h.id}:${h.reportRevision}` }),
1377
+ producer: h.from,
1010
1378
  outcome: h.outcome,
1011
1379
  limitations: h.limitations,
1012
1380
  ready: prepared.ready,
@@ -1017,8 +1385,8 @@ export class RoutingRuntime {
1017
1385
  finalExchangePending: true,
1018
1386
  provisional:
1019
1387
  "The finalized handoff exchange, target configuration and budget are revalidated at turn_end; this is not delivery evidence.",
1020
- report: h.text,
1021
- evidence: this.evidenceFacts(this.stateData()),
1388
+ ...(isSupplement ? { supplement: h.text } : { report: h.text }),
1389
+ evidence: this.evidenceFacts(state),
1022
1390
  };
1023
1391
  }
1024
1392
  private project(input: any, op: string) {
@@ -1053,7 +1421,7 @@ export class RoutingRuntime {
1053
1421
  [...sources.byRef.values()]
1054
1422
  .filter(
1055
1423
  (s) =>
1056
- s.producer === "executor" &&
1424
+ isWorkerProfile(s.producer) &&
1057
1425
  (isTaskEvidence(s) || (scope === "selected" && next.selected.includes(s.ref))) &&
1058
1426
  (scope === "selected"
1059
1427
  ? next.selected.includes(s.ref)
@@ -1067,12 +1435,14 @@ export class RoutingRuntime {
1067
1435
  s.message.content?.some((b: any) => b.type === "text" && b.text?.trim())),
1068
1436
  )
1069
1437
  .map((s) => {
1438
+ const locator = sources.locator(s.ref);
1070
1439
  return {
1071
1440
  ref: s.ref,
1072
1441
  kind: s.original ? "assistant-text" : s.message.role,
1073
1442
  producer: s.producer,
1074
1443
  assignment: s.assignmentId,
1075
1444
  toolName: s.message.toolName,
1445
+ ...(locator ? { locator } : {}),
1076
1446
  active: s.active,
1077
1447
  selected: next.selected.includes(s.ref),
1078
1448
  retainedSelection: next.selected.includes(s.ref) && !isTaskEvidence(s),
@@ -1097,7 +1467,7 @@ export class RoutingRuntime {
1097
1467
  },
1098
1468
  otherAssignments: [...sources.byRef.values()].filter(
1099
1469
  (s) =>
1100
- s.producer === "executor" && s.assignmentId !== a.id && isTaskEvidence(s) && checksFor(s).targetReady,
1470
+ isWorkerProfile(s.producer) && s.assignmentId !== a.id && isTaskEvidence(s) && checksFor(s).targetReady,
1101
1471
  ).length,
1102
1472
  }),
1103
1473
  );
@@ -1149,10 +1519,40 @@ export class RoutingRuntime {
1149
1519
  estimate: { tokens: prepared.estimatedTokens, method: prepared.estimateMethod },
1150
1520
  };
1151
1521
  }
1152
- private unit(input: any, op: string) {
1522
+ private unit(input: any, callId: string, op: string) {
1153
1523
  const state = this.stateData();
1154
1524
  const prior = [...state.events.values()].find((e) => e.operationId === op);
1155
1525
  if (prior) {
1526
+ if (input.operation === "recover") {
1527
+ check(
1528
+ prior.data.type === "recovery-request-accepted" &&
1529
+ prior.data.recovery.request === input.request &&
1530
+ canonical(prior.data.recovery.requestedPaths) === canonical([...new Set<string>(input.paths ?? [])]),
1531
+ "operation_conflict",
1532
+ );
1533
+ const { recovery, handoff } = prior.data;
1534
+ return {
1535
+ status: "accepted",
1536
+ unchanged: true,
1537
+ recovery: recovery.id,
1538
+ handoff: handoff.id,
1539
+ assignmentRef: `assignment:${recovery.assignmentId}`,
1540
+ reportRef: `report:${recovery.assessmentHandoffId}:${recovery.baseReportRevision}`,
1541
+ request: recovery.request,
1542
+ paths: recovery.paths,
1543
+ transition: handoff.state,
1544
+ };
1545
+ }
1546
+ if (input.operation === "cancel-recovery") {
1547
+ check(prior.data.type === "recovery-cancelled" && prior.data.reason === input.reason, "operation_conflict");
1548
+ return {
1549
+ status: "cancelled",
1550
+ unchanged: true,
1551
+ recovery: prior.data.recoveryId,
1552
+ reason: prior.data.reason,
1553
+ assessment: state.assessment?.handoffId,
1554
+ };
1555
+ }
1156
1556
  check(
1157
1557
  (input.operation === "assess" && prior.data.type === "assessment-resumed") ||
1158
1558
  (input.operation === "close" &&
@@ -1163,18 +1563,95 @@ export class RoutingRuntime {
1163
1563
  );
1164
1564
  return { status: "unchanged", operation: input.operation };
1165
1565
  }
1566
+ if (input.operation === "recover") {
1567
+ const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
1568
+ const base = a?.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
1569
+ check(
1570
+ state.unitId &&
1571
+ a?.state === "returned" &&
1572
+ base?.kind === "return" &&
1573
+ base.state === "configured" &&
1574
+ state.assessment?.handoffId === base.id &&
1575
+ !state.pendingId &&
1576
+ !state.recoveryId &&
1577
+ !this.applying,
1578
+ "recovery_not_available",
1579
+ );
1580
+ const worker = this.assignedWorker(state, a.id);
1581
+ const id = randomUUID(),
1582
+ handoffId = randomUUID();
1583
+ const paths = [...new Set<string>((input.paths ?? []).map((path: string) => this.canonicalRecoveryPath(path)))];
1584
+ const recovery: Recovery = {
1585
+ id,
1586
+ assignmentId: a.id,
1587
+ assessmentHandoffId: base.id,
1588
+ baseReportRevision: base.reportRevision,
1589
+ request: input.request,
1590
+ requestedPaths: [...new Set<string>(input.paths ?? [])],
1591
+ paths,
1592
+ state: "requested",
1593
+ requestHandoffId: handoffId,
1594
+ supplementRevision: 0,
1595
+ };
1596
+ const handoff: Handoff = {
1597
+ id: handoffId,
1598
+ kind: "recovery-request",
1599
+ assignmentId: a.id,
1600
+ text: input.request,
1601
+ from: "coordinator",
1602
+ to: worker,
1603
+ executionId: this.turn!.id,
1604
+ toolCallId: callId,
1605
+ basisUserEntryId: this.turn!.basisUserEntryId,
1606
+ state: "pending",
1607
+ reportRevision: 0,
1608
+ limitations: [],
1609
+ };
1610
+ this.append({ type: "recovery-request-accepted", recovery, handoff }, op);
1611
+ return {
1612
+ status: "accepted",
1613
+ recovery: id,
1614
+ handoff: handoffId,
1615
+ assignmentRef: `assignment:${a.id}`,
1616
+ reportRef: `report:${base.id}:${base.reportRevision}`,
1617
+ request: input.request,
1618
+ paths,
1619
+ transition: "pending",
1620
+ };
1621
+ }
1622
+ if (input.operation === "cancel-recovery") {
1623
+ check(state.recoveryId && !this.applying, "recovery_not_cancellable");
1624
+ check(
1625
+ ![...state.executions.values()].some(
1626
+ (execution) =>
1627
+ execution.recoveryId === state.recoveryId &&
1628
+ isWorkerProfile(execution.profile) &&
1629
+ !execution.assistantEntryId &&
1630
+ !execution.interrupted,
1631
+ ),
1632
+ "recovery_not_cancellable",
1633
+ );
1634
+ this.append({ type: "recovery-cancelled", recoveryId: state.recoveryId, reason: input.reason }, op);
1635
+ return {
1636
+ status: "cancelled",
1637
+ recovery: state.recoveryId,
1638
+ reason: input.reason,
1639
+ assessment: state.assessment?.handoffId,
1640
+ };
1641
+ }
1166
1642
  if (input.operation === "assess") {
1167
1643
  check(state.assessment, "assessment_missing");
1644
+ check(!state.recoveryId, "recovery_outstanding");
1168
1645
  if (state.assessment.view === "active") return { status: "unchanged", ready: true };
1169
1646
  const prepared = this.prepared("coordinator", this.messages, state.assessment.handoffId, true);
1170
1647
  if (!prepared.ready) return { status: "suspended", ready: false, problems: prepared.problems };
1171
- check(prepared.reservation, "reservation_missing");
1648
+ if (this.projectionEnabled) check(prepared.reservation, "reservation_missing");
1172
1649
  this.append(
1173
1650
  {
1174
1651
  type: "assessment-resumed",
1175
1652
  handoffId: state.assessment.handoffId,
1176
1653
  basisUserEntryId: this.turn!.basisUserEntryId,
1177
- reservation: prepared.reservation,
1654
+ ...(prepared.reservation ? { reservation: prepared.reservation } : {}),
1178
1655
  },
1179
1656
  op,
1180
1657
  );
@@ -1189,6 +1666,19 @@ export class RoutingRuntime {
1189
1666
  check(input.operation === "close" && state.unitId, "unit_missing");
1190
1667
  const a = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
1191
1668
  check(!this.applying, "not_quiescent");
1669
+ if (state.recoveryId) {
1670
+ check(input.outcome !== "accepted", "unfinished_work");
1671
+ check(
1672
+ ![...state.executions.values()].some(
1673
+ (execution) =>
1674
+ execution.recoveryId === state.recoveryId &&
1675
+ isWorkerProfile(execution.profile) &&
1676
+ !execution.assistantEntryId &&
1677
+ !execution.interrupted,
1678
+ ),
1679
+ "not_quiescent",
1680
+ );
1681
+ }
1192
1682
  this.append(
1193
1683
  {
1194
1684
  type: "unit-closed",
@@ -1279,6 +1769,7 @@ export class RoutingRuntime {
1279
1769
  [...state.assignments.values()].reverse().map((a) => {
1280
1770
  const u = state.units.get(a.unitId),
1281
1771
  h = a.returnHandoffId ? state.handoffs.get(a.returnHandoffId) : undefined;
1772
+ const delegate = state.handoffs.get(a.delegateHandoffId);
1282
1773
  return {
1283
1774
  ref: `assignment:${a.id}`,
1284
1775
  unitRef: `unit:${a.unitId}`,
@@ -1290,8 +1781,8 @@ export class RoutingRuntime {
1290
1781
  summary: a.contract.slice(0, 160),
1291
1782
  reportAvailable: !!h,
1292
1783
  outcome: h?.outcome,
1293
- from: "coordinator",
1294
- to: "executor",
1784
+ from: delegate?.from,
1785
+ to: delegate?.to,
1295
1786
  };
1296
1787
  });
1297
1788
  const page = this.page("work-history", rows, input.cursor, input.limit ?? 20);
@@ -1310,9 +1801,9 @@ export class RoutingRuntime {
1310
1801
  "Use a ref returned by history inspection.",
1311
1802
  );
1312
1803
  check(
1313
- /^(?:(?:unit|assignment):[^:\s]+|report:[^:\s]+:[1-9][0-9]*)$/.test(input.ref),
1804
+ /^(?:(?:unit|assignment|recovery):[^:\s]+|(?:report|supplement):[^:\s]+:[1-9][0-9]*)$/.test(input.ref),
1314
1805
  "invalid_work_ref",
1315
- "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.",
1806
+ "Use assignment, report, recovery, or supplement refs returned by a receipt or inspection, unchanged. A bare handoff ID is not a work ref.",
1316
1807
  );
1317
1808
  if (input.ref.startsWith("unit:")) {
1318
1809
  const u = state.units.get(input.ref.slice(5));
@@ -1335,6 +1826,63 @@ export class RoutingRuntime {
1335
1826
  historical: true,
1336
1827
  };
1337
1828
  }
1829
+ if (input.ref.startsWith("recovery:")) {
1830
+ const recovery = state.recoveries.get(input.ref.slice(9));
1831
+ check(recovery, "work_unavailable", "Recovery is not on current ancestry.");
1832
+ const request = state.handoffs.get(recovery.requestHandoffId);
1833
+ const supplement = recovery.supplementHandoffId ? state.handoffs.get(recovery.supplementHandoffId) : undefined;
1834
+ return {
1835
+ status: "inspected",
1836
+ view,
1837
+ ref: input.ref,
1838
+ historical: recovery.id !== state.recoveryId,
1839
+ recovery: {
1840
+ id: recovery.id,
1841
+ state: recovery.state,
1842
+ assignmentId: recovery.assignmentId,
1843
+ assessmentHandoffId: recovery.assessmentHandoffId,
1844
+ baseReportRevision: recovery.baseReportRevision,
1845
+ request: recovery.request,
1846
+ paths: recovery.paths,
1847
+ requestHandoffState: request?.state,
1848
+ supplementRevision: recovery.supplementRevision,
1849
+ cancellationReason: recovery.cancellationReason,
1850
+ },
1851
+ supplementRef: supplement ? `supplement:${recovery.id}:${supplement.reportRevision}` : undefined,
1852
+ sourceBoundary: "Saved recovery on current ancestry; historical content grants no current permission.",
1853
+ };
1854
+ }
1855
+ if (input.ref.startsWith("supplement:")) {
1856
+ const match = /^supplement:([^:]+):([0-9]+)$/.exec(input.ref);
1857
+ check(match, "work_unavailable", "Use a supplement ref returned by a receipt or recovery inspection.");
1858
+ const recovery = state.recoveries.get(match[1]);
1859
+ let supplement: Handoff | undefined;
1860
+ for (const event of state.events.values())
1861
+ if (
1862
+ event.data.type === "recovery-supplement-accepted" &&
1863
+ event.data.recoveryId === match[1] &&
1864
+ event.data.handoff.reportRevision === Number(match[2])
1865
+ )
1866
+ supplement = event.data.handoff;
1867
+ check(recovery && supplement, "work_unavailable", "Supplement revision is not on current ancestry.");
1868
+ const base = state.handoffs.get(recovery.assessmentHandoffId);
1869
+ return {
1870
+ status: "inspected",
1871
+ view,
1872
+ ref: input.ref,
1873
+ historical: true,
1874
+ recoveryRef: `recovery:${recovery.id}`,
1875
+ assignment: { id: recovery.assignmentId, state: state.assignments.get(recovery.assignmentId)?.state },
1876
+ reportRef: `report:${recovery.assessmentHandoffId}:${recovery.baseReportRevision}`,
1877
+ report: base?.text,
1878
+ supplement: supplement.text,
1879
+ supplementRevision: supplement.reportRevision,
1880
+ outcome: supplement.outcome,
1881
+ limitations: supplement.limitations,
1882
+ sourceBoundary:
1883
+ "Saved supplement revision and original report linkage on current ancestry; historical content grants no current permission.",
1884
+ };
1885
+ }
1338
1886
  let savedRevision: Handoff | undefined;
1339
1887
  if (input.ref.startsWith("report:")) {
1340
1888
  const match = /^report:([^:]+):([0-9]+)$/.exec(input.ref);
@@ -1383,8 +1931,23 @@ export class RoutingRuntime {
1383
1931
  return {
1384
1932
  ...this.state(),
1385
1933
  unit: unit ? { id: unit.id, state: unit.state, assignments: unit.assignmentIds.length } : null,
1386
- assignment: a ? { id: a.id, state: a.state } : null,
1934
+ assignment: a ? { id: a.id, state: a.state, worker: this.assignedWorker(state, a.id) } : null,
1387
1935
  pendingHandoff: state.pendingId ?? null,
1936
+ recovery: state.recoveryId
1937
+ ? (() => {
1938
+ const recovery = state.recoveries.get(state.recoveryId!);
1939
+ return recovery
1940
+ ? {
1941
+ id: recovery.id,
1942
+ state: recovery.state,
1943
+ assignmentId: recovery.assignmentId,
1944
+ assessmentHandoffId: recovery.assessmentHandoffId,
1945
+ paths: recovery.paths,
1946
+ supplementRevision: recovery.supplementRevision,
1947
+ }
1948
+ : null;
1949
+ })()
1950
+ : null,
1388
1951
  assessment: state.assessment
1389
1952
  ? { handoffId: state.assessment.handoffId, view: state.assessment.view, problems: state.assessment.problems }
1390
1953
  : null,
@@ -1471,23 +2034,29 @@ export class RoutingRuntime {
1471
2034
  "source_exchange_incomplete",
1472
2035
  "The saved handoff has an incomplete native exchange; reconcile or explicitly dispose of it.",
1473
2036
  );
1474
- if (h.kind === "return" && this.projectionEnabled) {
2037
+ let attentionProblems: Problem[] = [];
2038
+ if (["return", "recovery-return"].includes(h.kind) && this.projectionEnabled) {
1475
2039
  const prepared = this.prepared("coordinator", inputs, h.id);
1476
2040
  if (!prepared.ready) {
2041
+ const attentionFallback = h.kind === "recovery-return" && h.outcome !== "completed";
1477
2042
  this.projectionError = prepared.problems.map((p) => p.code).join(", ");
1478
- this.append({
1479
- type: "handoff-state",
1480
- handoffId: h.id,
1481
- state: "blocked",
1482
- reason: prepared.problems
1483
- .map((p) => p.detail)
1484
- .join("; ")
1485
- .slice(0, 4096),
1486
- });
1487
- return;
2043
+ if (attentionFallback) attentionProblems = prepared.problems;
2044
+ else {
2045
+ this.append({
2046
+ type: "handoff-state",
2047
+ handoffId: h.id,
2048
+ state: "blocked",
2049
+ reason: prepared.problems
2050
+ .map((p) => p.detail)
2051
+ .join("; ")
2052
+ .slice(0, 4096),
2053
+ });
2054
+ return;
2055
+ }
2056
+ } else {
2057
+ check(prepared.reservation, "reservation_missing");
2058
+ this.append({ type: "handoff-prepared", handoffId: h.id, reservation: prepared.reservation });
1488
2059
  }
1489
- check(prepared.reservation, "reservation_missing");
1490
- this.append({ type: "handoff-prepared", handoffId: h.id, reservation: prepared.reservation });
1491
2060
  }
1492
2061
  try {
1493
2062
  const current = this.stateData();
@@ -1497,6 +2066,28 @@ export class RoutingRuntime {
1497
2066
  this.append({ type: "handoff-state", handoffId: h.id, state: "configured", observedPair: this.observed() });
1498
2067
  const completed = this.stateData();
1499
2068
  const reservation = completed.reservations.get(h.id);
2069
+ const recovery =
2070
+ h.kind === "recovery-return"
2071
+ ? [...completed.recoveries.values()].find((candidate) => candidate.supplementHandoffId === h.id)
2072
+ : undefined;
2073
+ if (h.kind === "recovery-return" && recovery && completed.assessment?.view === "suspended") {
2074
+ if (attentionProblems.length) {
2075
+ this.append({
2076
+ type: "assessment-suspended",
2077
+ handoffId: recovery.assessmentHandoffId,
2078
+ reason: "delivery-gap",
2079
+ basisUserEntryId: this.lastDeliveredUser(this.sources(completed), inputs),
2080
+ problems: attentionProblems,
2081
+ });
2082
+ } else if (completed.assessment.suspensionReason === "recovery") {
2083
+ this.append({
2084
+ type: "assessment-resumed",
2085
+ handoffId: recovery.assessmentHandoffId,
2086
+ basisUserEntryId: this.lastDeliveredUser(this.sources(completed), inputs),
2087
+ ...(reservation ? { reservation } : {}),
2088
+ });
2089
+ }
2090
+ }
1500
2091
  if (h.kind === "return" && completed.assessment?.view === "suspended" && reservation) {
1501
2092
  this.append({
1502
2093
  type: "assessment-resumed",
@@ -1505,7 +2096,7 @@ export class RoutingRuntime {
1505
2096
  reservation,
1506
2097
  });
1507
2098
  }
1508
- this.projectionError = undefined;
2099
+ if (!attentionProblems.length) this.projectionError = undefined;
1509
2100
  } catch (error) {
1510
2101
  if (this.current(subject) && !this.store?.blocked && !this.error && this.stateData().pendingId === h.id) {
1511
2102
  this.append({
@@ -1515,7 +2106,7 @@ export class RoutingRuntime {
1515
2106
  reason: error instanceof Error ? error.message.slice(0, 4096) : "configuration_failed",
1516
2107
  });
1517
2108
  this.ctx.ui?.notify?.(
1518
- `Couldn’t switch to ${h.to === "coordinator" ? "Coordinator" : "Executor"} — ${h.kind === "return" ? "report" : "assignment"} saved.`,
2109
+ `Couldn’t switch to ${h.to.charAt(0).toUpperCase()}${h.to.slice(1)} — ${["return", "recovery-return"].includes(h.kind) ? "report" : "assignment"} saved.`,
1519
2110
  "warning",
1520
2111
  );
1521
2112
  } else throw error;
@@ -1541,7 +2132,7 @@ export class RoutingRuntime {
1541
2132
  check(
1542
2133
  !ctx.hasPendingMessages?.(),
1543
2134
  "pending_input",
1544
- "Queued input must be delivered and reconciled before resuming Executor.",
2135
+ "Queued input must be delivered and reconciled before resuming a worker.",
1545
2136
  );
1546
2137
  const pending = state.pendingId ? state.handoffs.get(state.pendingId) : undefined;
1547
2138
  if (pending) {
@@ -1551,17 +2142,18 @@ export class RoutingRuntime {
1551
2142
  this.guard(subject);
1552
2143
  if (this.stateData().pendingId) {
1553
2144
  check(
1554
- pending.kind === "return" && this.projectionError,
2145
+ ["return", "recovery-return"].includes(pending.kind) && this.projectionError,
1555
2146
  "handoff_still_blocked",
1556
2147
  "Saved transfer is still blocked; reconcile its configuration before retrying.",
1557
2148
  );
2149
+ const worker = this.assignedWorker(this.stateData(), pending.assignmentId);
1558
2150
  this.append({
1559
2151
  type: "control",
1560
2152
  control: "automatic",
1561
- profile: "executor",
2153
+ profile: worker,
1562
2154
  reason: "Explicit resume of saved-return evidence correction only; ordinary assignment work remains ended",
1563
2155
  });
1564
- await this.applyPair(this.profilePair("executor"));
2156
+ await this.applyPair(this.profilePair(worker));
1565
2157
  }
1566
2158
  } else {
1567
2159
  const assignment = state.assignmentId ? state.assignments.get(state.assignmentId) : undefined;
@@ -1571,7 +2163,22 @@ export class RoutingRuntime {
1571
2163
  (x) =>
1572
2164
  x.profile === "coordinator" && x.basisUserEntryId === user && x.assistantEntryId && x.outcome === "completed",
1573
2165
  );
1574
- if (assignment?.state === "outstanding") {
2166
+ const recovery = state.recoveryId ? state.recoveries.get(state.recoveryId) : undefined;
2167
+ const worker = assignment ? this.assignedWorker(state, assignment.id) : undefined;
2168
+ if (assignment?.state === "returned" && recovery?.state === "reading") {
2169
+ check(
2170
+ user === this.workerBasis(state, assignment.id) || coordinatorSawInput,
2171
+ "input_unreconciled",
2172
+ "New input needs Coordinator attention before evidence recovery can resume.",
2173
+ );
2174
+ this.append({
2175
+ type: "control",
2176
+ control: "automatic",
2177
+ profile: worker!,
2178
+ reason: "Explicit user resume of unchanged evidence recovery; ordinary assignment work remains ended",
2179
+ });
2180
+ await this.applyPair(this.profilePair(worker!));
2181
+ } else if (assignment?.state === "outstanding") {
1575
2182
  check(
1576
2183
  user === this.taskBasis(state, assignment.id) || coordinatorSawInput,
1577
2184
  "input_unreconciled",
@@ -1581,10 +2188,10 @@ export class RoutingRuntime {
1581
2188
  this.append({
1582
2189
  type: "control",
1583
2190
  control: "automatic",
1584
- profile: "executor",
2191
+ profile: worker!,
1585
2192
  reason: "Explicit user resume of the unchanged assignment; current restrictions remain applicable",
1586
2193
  });
1587
- await this.applyPair(this.profilePair("executor"));
2194
+ await this.applyPair(this.profilePair(worker!));
1588
2195
  } else await this.applyPair(this.profilePair("coordinator"));
1589
2196
  }
1590
2197
  this.guard(subject);
@@ -1621,7 +2228,7 @@ export class RoutingRuntime {
1621
2228
  check(
1622
2229
  words.length === 2 && [...PROFILES, "auto"].includes(words[1]),
1623
2230
  "invalid_profile_command",
1624
- "Use /freeflow profile coordinator|executor|auto|history, or /freeflow resume.",
2231
+ "Use /freeflow profile coordinator|helper|executor|auto|history, or /freeflow resume.",
1625
2232
  );
1626
2233
  const result =
1627
2234
  words[1] === "auto" ? await this.setAutomaticControl() : await this.setManualProfile(words[1] as Profile);