@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,29 +1,56 @@
1
1
  import { isObject } from "./types.js";
2
- const string = (maxLength = 32768) => ({ type: "string", minLength: 1, maxLength });
3
- const object = (properties, required = Object.keys(properties)) => ({
2
+ const string = (maxLength = 32768, description) => ({
3
+ type: "string",
4
+ minLength: 1,
5
+ maxLength,
6
+ ...(description ? { description } : {}),
7
+ });
8
+ const object = (properties, required = Object.keys(properties), description, title) => ({
4
9
  type: "object",
5
10
  additionalProperties: false,
6
11
  properties,
7
12
  required,
13
+ ...(description ? { description } : {}),
14
+ ...(title ? { title } : {}),
8
15
  });
9
- const operation = (value) => ({ type: "string", enum: [value] });
10
- const submit = object(
11
- {
12
- operation: operation("submit"),
13
- report: string(),
14
- outcome: { type: "string", enum: ["completed", "partial", "blocked"] },
15
- limitations: { type: "array", maxItems: 32, items: string(2048) },
16
- },
17
- ["operation", "report", "outcome"],
18
- );
16
+ const operation = (value, description) => ({
17
+ type: "string",
18
+ enum: [value],
19
+ ...(description ? { description } : {}),
20
+ });
21
+ const evidenceRefs = (description) => ({
22
+ type: "array",
23
+ maxItems: 64,
24
+ items: string(512),
25
+ description,
26
+ });
27
+ const worker = {
28
+ type: "string",
29
+ enum: ["helper", "executor"],
30
+ description: "Worker for this assignment. Required when delegation mode enables both workers.",
31
+ };
32
+ const report = (name) =>
33
+ object(
34
+ {
35
+ operation: operation(name),
36
+ report: string(),
37
+ outcome: { type: "string", enum: ["completed", "partial", "blocked"] },
38
+ limitations: { type: "array", maxItems: 32, items: string(2048) },
39
+ },
40
+ ["operation", "report", "outcome"],
41
+ );
19
42
  export const ROUTING_SCHEMAS = {
20
43
  freeflow_delegate: {
21
44
  oneOf: [
22
- object({ operation: operation("assign"), contract: string() }),
23
- object({ operation: operation("replace"), contract: string(), reason: string(2048) }),
45
+ object({ operation: operation("assign"), contract: string(), worker }, ["operation", "contract"]),
46
+ object({ operation: operation("replace"), contract: string(), reason: string(2048), worker }, [
47
+ "operation",
48
+ "contract",
49
+ "reason",
50
+ ]),
24
51
  ],
25
52
  },
26
- freeflow_return: { oneOf: [submit, object({ operation: operation("retry") })] },
53
+ freeflow_return: { oneOf: [report("submit"), report("supplement"), object({ operation: operation("retry") })] },
27
54
  freeflow_unit: {
28
55
  oneOf: [
29
56
  object(
@@ -37,6 +64,15 @@ export const ROUTING_SCHEMAS = {
37
64
  ["operation"],
38
65
  ),
39
66
  object({ operation: operation("assess") }),
67
+ object(
68
+ {
69
+ operation: operation("recover"),
70
+ request: string(),
71
+ paths: { type: "array", maxItems: 32, items: string(4096) },
72
+ },
73
+ ["operation", "request"],
74
+ ),
75
+ object({ operation: operation("cancel-recovery"), reason: string(2048) }),
40
76
  object({
41
77
  operation: operation("close"),
42
78
  outcome: { type: "string", enum: ["accepted", "cancelled", "deferred"] },
@@ -48,18 +84,41 @@ export const ROUTING_SCHEMAS = {
48
84
  oneOf: [
49
85
  object(
50
86
  {
51
- operation: operation("inspect"),
52
- scope: { type: "string", enum: ["selected", "assignment", "active", "history"] },
53
- cursor: string(128),
87
+ operation: operation("inspect", "Read current selection and offered evidence candidates."),
88
+ scope: {
89
+ type: "string",
90
+ enum: ["selected", "assignment", "active", "history"],
91
+ description: "Optional candidate scope; omit to inspect the current assignment.",
92
+ },
93
+ cursor: string(128, "Use the nextCursor returned by a previous inspection."),
54
94
  },
55
95
  ["operation"],
96
+ "Inspect selection and candidates without changing evidence state.",
97
+ "Inspect evidence selection",
98
+ ),
99
+ object(
100
+ {
101
+ operation: operation("add", "Select eligible worker task-evidence refs; do not include reason."),
102
+ refs: evidenceRefs(
103
+ "Exact eligible visible refs for worker task evidence. Add known eligible refs directly; inspect when identity, eligibility, representation, or selection state is unclear. Never add a ref marked not offered for new evidence selection.",
104
+ ),
105
+ },
106
+ ["operation", "refs"],
107
+ "Add evidence refs. This shape has no reason field.",
108
+ "Add evidence",
109
+ ),
110
+ object(
111
+ {
112
+ operation: operation("remove", "Withdraw selected or unresolved refs; reason is required."),
113
+ refs: evidenceRefs(
114
+ "Currently selected or unresolved refs to withdraw. A non-selectable ref may be removed with a reason to clear its unresolved request.",
115
+ ),
116
+ reason: string(2048, "Required only for remove; explain why the selection is being withdrawn."),
117
+ },
118
+ ["operation", "refs", "reason"],
119
+ "Remove evidence refs. This is the only operation that accepts reason.",
120
+ "Remove evidence",
56
121
  ),
57
- object({ operation: operation("add"), refs: { type: "array", maxItems: 64, items: string(512) } }),
58
- object({
59
- operation: operation("remove"),
60
- refs: { type: "array", maxItems: 64, items: string(512) },
61
- reason: string(2048),
62
- }),
63
122
  ],
64
123
  },
65
124
  };
@@ -73,7 +132,14 @@ for (const schema of Object.values(ROUTING_SCHEMAS)) {
73
132
  schema.properties.operation = {
74
133
  type: "string",
75
134
  enum: schema.oneOf.flatMap((branch) => branch.properties.operation.enum),
135
+ description: "Choose exactly one operation; each operation has a strict shape and rejects unknown fields.",
76
136
  };
137
+ if (schema.properties.refs)
138
+ schema.properties.refs = {
139
+ ...schema.properties.refs,
140
+ description:
141
+ "For add, use exact eligible visible refs directly when their identity and eligibility are clear; inspect when identity, eligibility, representation, or selection state is unclear. Never add refs marked not offered for new evidence selection. For remove, use currently selected or unresolved refs; a non-selectable ref may be named to clear its unresolved request and requires reason.",
142
+ };
77
143
  }
78
144
  export function matches(value, schema) {
79
145
  if (schema.oneOf) return schema.oneOf.filter((branch) => matches(value, branch)).length === 1;
@@ -6,6 +6,7 @@ import {
6
6
  eventKey,
7
7
  eventValue,
8
8
  isObject,
9
+ isWorkerProfile,
9
10
  requireCondition as check,
10
11
  } from "./types.js";
11
12
  const text = (x, max = 32768) => typeof x === "string" && x.trim().length > 0 && x.length <= max;
@@ -18,6 +19,11 @@ const pair = (x) =>
18
19
  identity(x.provider) &&
19
20
  identity(x.modelId) &&
20
21
  EFFORTS.includes(x.thinking);
22
+ const profileOverrides = (x) =>
23
+ isObject(x) &&
24
+ Object.keys(x).length > 0 &&
25
+ Object.keys(x).every((key) => PROFILES.includes(key)) &&
26
+ Object.values(x).every((value) => value === null || pair(value));
21
27
  const problemList = (xs) =>
22
28
  Array.isArray(xs) &&
23
29
  xs.every(
@@ -50,6 +56,41 @@ const reservation = (r) =>
50
56
  Number.isFinite(r.outputReserve) &&
51
57
  r.outputReserve >= 0 &&
52
58
  text(r.estimateMethod);
59
+ const recovery = (r) =>
60
+ shape(r, [
61
+ "id",
62
+ "assignmentId",
63
+ "assessmentHandoffId",
64
+ "baseReportRevision",
65
+ "request",
66
+ "requestedPaths",
67
+ "paths",
68
+ "state",
69
+ "requestHandoffId",
70
+ "supplementHandoffId",
71
+ "supplementRevision",
72
+ "cancellationReason",
73
+ ]) &&
74
+ identity(r.id) &&
75
+ identity(r.assignmentId) &&
76
+ identity(r.assessmentHandoffId) &&
77
+ Number.isSafeInteger(r.baseReportRevision) &&
78
+ r.baseReportRevision > 0 &&
79
+ text(r.request) &&
80
+ Array.isArray(r.requestedPaths) &&
81
+ r.requestedPaths.length <= 32 &&
82
+ r.requestedPaths.every((path) => text(path, 4096)) &&
83
+ new Set(r.requestedPaths).size === r.requestedPaths.length &&
84
+ Array.isArray(r.paths) &&
85
+ r.paths.length <= 32 &&
86
+ r.paths.every((path) => text(path, 4096)) &&
87
+ new Set(r.paths).size === r.paths.length &&
88
+ ["requested", "reading", "returning", "completed", "cancelled"].includes(r.state) &&
89
+ identity(r.requestHandoffId) &&
90
+ (r.supplementHandoffId === undefined || identity(r.supplementHandoffId)) &&
91
+ Number.isSafeInteger(r.supplementRevision) &&
92
+ r.supplementRevision >= 0 &&
93
+ (r.cancellationReason === undefined || text(r.cancellationReason, 2048));
53
94
  const handoff = (h) =>
54
95
  shape(h, [
55
96
  "id",
@@ -73,7 +114,7 @@ const handoff = (h) =>
73
114
  identity(h.executionId) &&
74
115
  identity(h.toolCallId) &&
75
116
  nullableId(h.basisUserEntryId) &&
76
- ["delegate", "return"].includes(h.kind) &&
117
+ ["delegate", "return", "recovery-request", "recovery-return"].includes(h.kind) &&
77
118
  PROFILES.includes(h.from) &&
78
119
  PROFILES.includes(h.to) &&
79
120
  h.from !== h.to &&
@@ -85,15 +126,21 @@ const handoff = (h) =>
85
126
  h.limitations.every((x) => text(x, 2048)) &&
86
127
  (h.reason === undefined || text(h.reason, 4096)) &&
87
128
  (h.outcome === undefined || ["completed", "partial", "blocked"].includes(h.outcome)) &&
88
- (h.kind !== "delegate" || (h.outcome === undefined && h.reportRevision === 0));
129
+ (["delegate", "recovery-request"].includes(h.kind)
130
+ ? h.outcome === undefined && h.reportRevision === 0
131
+ : ["completed", "partial", "blocked"].includes(h.outcome) && h.reportRevision > 0);
89
132
  const fields = {
90
133
  "execution-interrupted": ["executionId", "reason"],
91
134
  "assignment-resumed": ["assignmentId", "basisUserEntryId"],
92
135
  control: ["control", "profile", "reason"],
136
+ "profile-overrides": ["overrides", "reason"],
93
137
  "execution-opened": ["execution"],
94
138
  "execution-bound": ["executionId", "assistantEntryId", "resultEntryIds", "outcome"],
95
139
  "delegate-accepted": ["unit", "assignment", "handoff", "replacement"],
96
140
  "return-accepted": ["handoff"],
141
+ "recovery-request-accepted": ["recovery", "handoff"],
142
+ "recovery-supplement-accepted": ["recoveryId", "handoff"],
143
+ "recovery-cancelled": ["recoveryId", "reason"],
97
144
  "handoff-retry-requested": [
98
145
  "handoffId",
99
146
  "attemptId",
@@ -153,17 +200,21 @@ export function parseRoutingEvent(raw) {
153
200
  "invalid_control",
154
201
  );
155
202
  break;
203
+ case "profile-overrides":
204
+ check(profileOverrides(d.overrides) && text(d.reason, 4096), "invalid_profile_overrides");
205
+ break;
156
206
  case "execution-opened": {
157
207
  const e = d.execution;
158
208
  check(
159
- shape(e, ["id", "profile", "assignmentId", "basisUserEntryId", "pair", "resultEntryIds"]) &&
209
+ shape(e, ["id", "profile", "assignmentId", "recoveryId", "basisUserEntryId", "pair", "resultEntryIds"]) &&
160
210
  identity(e.id) &&
161
211
  ["solo", ...PROFILES].includes(e.profile) &&
162
212
  nullableId(e.basisUserEntryId) &&
163
213
  pair(e.pair) &&
164
214
  uniqueStrings(e.resultEntryIds) &&
165
215
  e.resultEntryIds.length === 0 &&
166
- (e.assignmentId === undefined || identity(e.assignmentId)),
216
+ (e.assignmentId === undefined || identity(e.assignmentId)) &&
217
+ (e.recoveryId === undefined || identity(e.recoveryId)),
167
218
  "invalid_execution",
168
219
  );
169
220
  break;
@@ -211,13 +262,28 @@ export function parseRoutingEvent(raw) {
211
262
  break;
212
263
  }
213
264
  case "return-accepted":
265
+ check(handoff(d.handoff) && d.handoff.kind === "return", "invalid_return");
266
+ break;
267
+ case "recovery-request-accepted":
214
268
  check(
215
- handoff(d.handoff) &&
216
- ["completed", "partial", "blocked"].includes(d.handoff.outcome) &&
217
- d.handoff.reportRevision > 0,
218
- "invalid_return",
269
+ recovery(d.recovery) &&
270
+ handoff(d.handoff) &&
271
+ d.handoff.kind === "recovery-request" &&
272
+ d.recovery.requestHandoffId === d.handoff.id &&
273
+ d.recovery.assignmentId === d.handoff.assignmentId &&
274
+ d.recovery.request === d.handoff.text,
275
+ "invalid_recovery_request",
219
276
  );
220
277
  break;
278
+ case "recovery-supplement-accepted":
279
+ check(
280
+ identity(d.recoveryId) && handoff(d.handoff) && d.handoff.kind === "recovery-return",
281
+ "invalid_recovery_supplement",
282
+ );
283
+ break;
284
+ case "recovery-cancelled":
285
+ check(identity(d.recoveryId) && text(d.reason, 2048), "invalid_recovery_cancellation");
286
+ break;
221
287
  case "handoff-retry-requested":
222
288
  check(
223
289
  identity(d.attemptId) &&
@@ -265,7 +331,10 @@ export function parseRoutingEvent(raw) {
265
331
  );
266
332
  break;
267
333
  case "assessment-resumed":
268
- check(nullableId(d.basisUserEntryId) && reservation(d.reservation), "invalid_resumption");
334
+ check(
335
+ nullableId(d.basisUserEntryId) && (d.reservation === undefined || reservation(d.reservation)),
336
+ "invalid_resumption",
337
+ );
269
338
  break;
270
339
  case "unit-closed":
271
340
  check(
@@ -305,6 +374,7 @@ export function initialState() {
305
374
  units: new Map(),
306
375
  assignments: new Map(),
307
376
  handoffs: new Map(),
377
+ recoveries: new Map(),
308
378
  executions: new Map(),
309
379
  selections: new Map(),
310
380
  reservations: new Map(),
@@ -312,6 +382,7 @@ export function initialState() {
312
382
  exposure: new Map(),
313
383
  authors: new Map(),
314
384
  resumeBasis: new Map(),
385
+ profileOverrides: new Map(),
315
386
  events: new Map(),
316
387
  eventIds: new Map(),
317
388
  };
@@ -334,6 +405,7 @@ function applyEvent(state, event, owned = false) {
334
405
  units: new Map([...state.units].map(([k, v]) => [k, { ...v }])),
335
406
  assignments: new Map([...state.assignments].map(([k, v]) => [k, { ...v }])),
336
407
  handoffs: new Map([...state.handoffs].map(([k, v]) => [k, { ...v }])),
408
+ recoveries: new Map([...state.recoveries].map(([k, v]) => [k, { ...v, paths: [...v.paths] }])),
337
409
  executions: new Map([...state.executions].map(([k, v]) => [k, { ...v }])),
338
410
  selections: new Map(state.selections),
339
411
  reservations: new Map(state.reservations),
@@ -341,17 +413,27 @@ function applyEvent(state, event, owned = false) {
341
413
  exposure: new Map(state.exposure),
342
414
  authors: new Map(state.authors),
343
415
  resumeBasis: new Map(state.resumeBasis),
416
+ profileOverrides: new Map(state.profileOverrides),
344
417
  events: new Map(state.events),
345
418
  eventIds: new Map(state.eventIds),
346
419
  assessment: state.assessment ? { ...state.assessment } : undefined,
347
420
  },
348
421
  d = e.data;
349
422
  const assignment = () => (s.assignmentId ? s.assignments.get(s.assignmentId) : undefined);
423
+ const assignmentWorker = () => {
424
+ const a = assignment();
425
+ const h = a ? s.handoffs.get(a.delegateHandoffId) : undefined;
426
+ check(h?.kind === "delegate" && h.assignmentId === a?.id && isWorkerProfile(h.to), "assignment_worker_missing");
427
+ return h.to;
428
+ };
350
429
  const pending = () => (s.pendingId ? s.handoffs.get(s.pendingId) : undefined);
351
430
  const isPending = () => pending() && ["pending", "blocked"].includes(pending().state);
352
431
  const currentReturn = (id) => {
353
432
  const h = s.handoffs.get(id);
354
- check(h && h.kind === "return" && h.assignmentId === s.assignmentId, "not_current_return");
433
+ check(
434
+ h && ["return", "recovery-return"].includes(h.kind) && h.assignmentId === s.assignmentId,
435
+ "not_current_return",
436
+ );
355
437
  return h;
356
438
  };
357
439
  switch (d.type) {
@@ -374,6 +456,14 @@ function applyEvent(state, event, owned = false) {
374
456
  s.control = d.control;
375
457
  s.profile = d.profile;
376
458
  break;
459
+ case "profile-overrides":
460
+ for (const profile of PROFILES) {
461
+ if (!Object.hasOwn(d.overrides, profile)) continue;
462
+ const override = d.overrides[profile];
463
+ if (override === null) s.profileOverrides.delete(profile);
464
+ else s.profileOverrides.set(profile, override);
465
+ }
466
+ break;
377
467
  case "execution-opened":
378
468
  check(!s.executions.has(d.execution.id), "duplicate_execution");
379
469
  s.executions.set(d.execution.id, d.execution);
@@ -407,7 +497,7 @@ function applyEvent(state, event, owned = false) {
407
497
  check(
408
498
  h.kind === "delegate" &&
409
499
  h.from === "coordinator" &&
410
- h.to === "executor" &&
500
+ isWorkerProfile(h.to) &&
411
501
  h.state === "pending" &&
412
502
  h.text === d.assignment.contract &&
413
503
  h.assignmentId === d.assignment.id &&
@@ -425,9 +515,9 @@ function applyEvent(state, event, owned = false) {
425
515
  check(a?.state === "outstanding" && a.id === d.replacement.assignmentId && u, "no_replaceable_assignment");
426
516
  check(
427
517
  ![...s.executions.values()].some(
428
- (x) => x.assignmentId === a.id && x.profile === "executor" && !x.assistantEntryId && !x.interrupted,
518
+ (x) => x.assignmentId === a.id && isWorkerProfile(x.profile) && !x.assistantEntryId && !x.interrupted,
429
519
  ),
430
- "unresolved_executor_execution",
520
+ "unresolved_worker_execution",
431
521
  );
432
522
  check(
433
523
  !isPending() || (pending().kind === "delegate" && d.replacement.supersededHandoffId === pending().id),
@@ -446,14 +536,14 @@ function applyEvent(state, event, owned = false) {
446
536
  break;
447
537
  }
448
538
  case "return-accepted": {
449
- check(s.control === "automatic" && s.profile === "executor", "wrong_control");
539
+ check(s.control === "automatic" && s.profile === assignmentWorker(), "wrong_control");
450
540
  const a = assignment(),
451
541
  h = d.handoff;
452
542
  check(
453
543
  a &&
454
544
  h.assignmentId === a.id &&
455
545
  h.kind === "return" &&
456
- h.from === "executor" &&
546
+ h.from === assignmentWorker() &&
457
547
  h.to === "coordinator" &&
458
548
  h.state === "pending",
459
549
  "return_identity_mismatch",
@@ -483,11 +573,101 @@ function applyEvent(state, event, owned = false) {
483
573
  };
484
574
  break;
485
575
  }
576
+ case "recovery-request-accepted": {
577
+ const a = assignment(),
578
+ base = s.handoffs.get(d.recovery.assessmentHandoffId),
579
+ h = d.handoff;
580
+ check(
581
+ s.control === "automatic" &&
582
+ s.profile === "coordinator" &&
583
+ a?.state === "returned" &&
584
+ d.recovery.assignmentId === a.id &&
585
+ d.recovery.state === "requested" &&
586
+ d.recovery.supplementRevision === 0 &&
587
+ d.recovery.supplementHandoffId === undefined &&
588
+ d.recovery.cancellationReason === undefined &&
589
+ s.assessment?.handoffId === base?.id &&
590
+ base?.kind === "return" &&
591
+ a.returnHandoffId === base.id &&
592
+ base.state === "configured" &&
593
+ base.reportRevision === d.recovery.baseReportRevision &&
594
+ !s.recoveryId &&
595
+ !isPending() &&
596
+ h.kind === "recovery-request" &&
597
+ h.assignmentId === a.id &&
598
+ h.from === "coordinator" &&
599
+ h.to === assignmentWorker() &&
600
+ h.state === "pending" &&
601
+ !s.handoffs.has(h.id) &&
602
+ !s.recoveries.has(d.recovery.id),
603
+ "recovery_not_available",
604
+ );
605
+ s.recoveries.set(d.recovery.id, d.recovery);
606
+ s.recoveryId = d.recovery.id;
607
+ s.handoffs.set(h.id, h);
608
+ s.pendingId = h.id;
609
+ s.assessment.view = "suspended";
610
+ s.assessment.suspensionReason = "recovery";
611
+ s.assessment.basisUserEntryId = h.basisUserEntryId;
612
+ s.assessment.problems = [];
613
+ break;
614
+ }
615
+ case "recovery-supplement-accepted": {
616
+ const a = assignment(),
617
+ r = s.recoveries.get(d.recoveryId),
618
+ h = d.handoff,
619
+ previous = r?.supplementHandoffId ? s.handoffs.get(r.supplementHandoffId) : undefined;
620
+ check(
621
+ s.control === "automatic" &&
622
+ s.profile === assignmentWorker() &&
623
+ a?.state === "returned" &&
624
+ r?.id === s.recoveryId &&
625
+ r.assignmentId === a.id &&
626
+ s.assessment?.handoffId === r.assessmentHandoffId &&
627
+ s.handoffs.get(r.assessmentHandoffId)?.reportRevision === r.baseReportRevision &&
628
+ ["reading", "returning"].includes(r.state) &&
629
+ h.kind === "recovery-return" &&
630
+ h.assignmentId === a.id &&
631
+ h.from === assignmentWorker() &&
632
+ h.to === "coordinator" &&
633
+ h.state === "pending" &&
634
+ (!previous ? !s.handoffs.has(h.id) : ["pending", "blocked"].includes(previous.state) && previous.id === h.id),
635
+ "recovery_not_returnable",
636
+ );
637
+ const expectedRevision = (previous?.reportRevision ?? 0) + 1;
638
+ check(h.reportRevision === expectedRevision, "invalid_report_revision");
639
+ r.state = "returning";
640
+ r.supplementHandoffId = h.id;
641
+ r.supplementRevision = h.reportRevision;
642
+ s.handoffs.set(h.id, h);
643
+ s.pendingId = h.id;
644
+ break;
645
+ }
646
+ case "recovery-cancelled": {
647
+ const r = s.recoveries.get(d.recoveryId);
648
+ check(
649
+ s.control === "automatic" &&
650
+ s.profile === "coordinator" &&
651
+ r?.id === s.recoveryId &&
652
+ ["requested", "reading", "returning"].includes(r.state),
653
+ "recovery_not_cancellable",
654
+ );
655
+ const p = pending();
656
+ if (p) {
657
+ check([r.requestHandoffId, r.supplementHandoffId].includes(p.id), "handoff_pending");
658
+ p.state = "superseded";
659
+ s.pendingId = undefined;
660
+ }
661
+ r.state = "cancelled";
662
+ r.cancellationReason = d.reason;
663
+ s.recoveryId = undefined;
664
+ break;
665
+ }
486
666
  case "handoff-retry-requested": {
487
667
  const h = currentReturn(d.handoffId);
488
668
  check(
489
669
  s.control === "automatic" &&
490
- s.profile === "executor" &&
670
+ s.profile === assignmentWorker() &&
491
671
  ["blocked", "pending"].includes(h.state) &&
492
672
  h.reportRevision === d.reportRevision &&
493
673
  (s.selections.get(h.assignmentId)?.revision ?? 0) === d.selectionRevision,
@@ -507,6 +687,11 @@ function applyEvent(state, event, owned = false) {
507
687
  check(d.reservation.selectionRevision === (s.selections.get(h.assignmentId)?.revision ?? 0), "stale_selection");
508
688
  s.reservations.set(h.id, d.reservation);
509
689
  if (s.assessment?.handoffId === h.id) s.assessment.reservation = d.reservation;
690
+ if (h.kind === "recovery-return") {
691
+ const recovery = [...s.recoveries.values()].find((candidate) => candidate.supplementHandoffId === h.id);
692
+ if (recovery && s.assessment?.handoffId === recovery.assessmentHandoffId)
693
+ s.assessment.reservation = d.reservation;
694
+ }
510
695
  break;
511
696
  }
512
697
  case "handoff-state": {
@@ -518,6 +703,12 @@ function applyEvent(state, event, owned = false) {
518
703
  check(d.observedPair, "configuration_unobserved");
519
704
  s.profile = h.to;
520
705
  s.pendingId = undefined;
706
+ const r = s.recoveryId ? s.recoveries.get(s.recoveryId) : undefined;
707
+ if (r && h.id === r.requestHandoffId) r.state = "reading";
708
+ if (r && h.id === r.supplementHandoffId) {
709
+ r.state = "completed";
710
+ s.recoveryId = undefined;
711
+ }
521
712
  }
522
713
  if (d.state === "superseded") s.pendingId = undefined;
523
714
  break;
@@ -527,8 +718,10 @@ function applyEvent(state, event, owned = false) {
527
718
  check(
528
719
  a?.id === d.assignmentId &&
529
720
  s.control === "automatic" &&
530
- s.profile === "executor" &&
531
- (a.state === "outstanding" || (a.state === "returned" && isPending())),
721
+ s.profile === assignmentWorker() &&
722
+ (a.state === "outstanding" ||
723
+ (a.state === "returned" &&
724
+ (isPending() || (s.recoveryId !== undefined && s.recoveries.get(s.recoveryId)?.state === "reading")))),
532
725
  "selection_wrong_phase",
533
726
  );
534
727
  check(d.selection.revision === (s.selections.get(a.id)?.revision ?? 0) + 1, "selection_revision");
@@ -538,6 +731,7 @@ function applyEvent(state, event, owned = false) {
538
731
  case "assessment-suspended": {
539
732
  check(s.assessment?.handoffId === d.handoffId, "assessment_missing");
540
733
  s.assessment.view = "suspended";
734
+ s.assessment.suspensionReason = d.reason;
541
735
  s.assessment.basisUserEntryId = d.basisUserEntryId;
542
736
  s.assessment.problems = d.problems;
543
737
  break;
@@ -546,19 +740,24 @@ function applyEvent(state, event, owned = false) {
546
740
  check(
547
741
  s.control === "automatic" &&
548
742
  s.profile === "coordinator" &&
743
+ !s.recoveryId &&
549
744
  s.assessment?.handoffId === d.handoffId &&
550
745
  s.assessment.view === "suspended",
551
- "assessment_not_suspended",
552
- );
553
- check(
554
- d.reservation.selectionRevision === (s.selections.get(s.assessment.assignmentId)?.revision ?? 0),
555
- "stale_selection",
746
+ s.recoveryId ? "recovery_outstanding" : "assessment_not_suspended",
556
747
  );
748
+ if (d.reservation)
749
+ check(
750
+ d.reservation.selectionRevision === (s.selections.get(s.assessment.assignmentId)?.revision ?? 0),
751
+ "stale_selection",
752
+ );
557
753
  s.assessment.view = "active";
754
+ s.assessment.suspensionReason = undefined;
558
755
  s.assessment.basisUserEntryId = d.basisUserEntryId;
559
- s.assessment.reservation = d.reservation;
756
+ if (d.reservation) {
757
+ s.assessment.reservation = d.reservation;
758
+ s.reservations.set(d.handoffId, d.reservation);
759
+ }
560
760
  s.assessment.problems = [];
561
- s.reservations.set(d.handoffId, d.reservation);
562
761
  break;
563
762
  }
564
763
  case "unit-closed": {
@@ -568,7 +767,24 @@ function applyEvent(state, event, owned = false) {
568
767
  s.control === "automatic" && s.profile === "coordinator" && u?.id === s.unitId && u.state === "open",
569
768
  "unit_not_open",
570
769
  );
571
- if (d.outcome === "accepted") check(a?.state !== "outstanding" && !isPending(), "unfinished_work");
770
+ if (d.outcome === "accepted")
771
+ check(a?.state !== "outstanding" && !isPending() && !s.recoveryId, "unfinished_work");
772
+ const recovery = s.recoveryId ? s.recoveries.get(s.recoveryId) : undefined;
773
+ if (recovery) {
774
+ check(
775
+ d.outcome !== "accepted" && ["requested", "reading", "returning"].includes(recovery.state),
776
+ "unfinished_work",
777
+ );
778
+ const transfer = pending();
779
+ if (transfer) {
780
+ check([recovery.requestHandoffId, recovery.supplementHandoffId].includes(transfer.id), "handoff_pending");
781
+ transfer.state = "superseded";
782
+ s.pendingId = undefined;
783
+ }
784
+ recovery.state = "cancelled";
785
+ recovery.cancellationReason = d.assessment;
786
+ s.recoveryId = undefined;
787
+ }
572
788
  if (a?.state === "outstanding") {
573
789
  check(d.supersededAssignmentId === a.id && d.outcome !== "accepted", "supersession_required");
574
790
  a.state = "superseded";
@@ -592,7 +808,7 @@ function applyEvent(state, event, owned = false) {
592
808
  case "sources-exposed": {
593
809
  const x = s.executions.get(d.executionId);
594
810
  check(x && x.profile === d.view, "exposure_execution_missing");
595
- if (d.view === "executor") for (const source of d.sources) s.exposure.set(source.ref, source.bodyHash);
811
+ if (isWorkerProfile(d.view)) for (const source of d.sources) s.exposure.set(source.ref, source.bodyHash);
596
812
  break;
597
813
  }
598
814
  }