@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +147 -127
- package/capabilities/cognitive-routing/SKILL.md +162 -93
- package/gemini-extension.json +1 -1
- package/package.json +1 -1
- package/pi-extension/dist/cognitive-routing-v2/config.js +35 -19
- package/pi-extension/dist/cognitive-routing-v2/projection.js +53 -44
- package/pi-extension/dist/cognitive-routing-v2/render.js +31 -10
- package/pi-extension/dist/cognitive-routing-v2/runtime.js +701 -105
- package/pi-extension/dist/cognitive-routing-v2/schemas.js +90 -24
- package/pi-extension/dist/cognitive-routing-v2/state.js +243 -27
- package/pi-extension/dist/cognitive-routing-v2/tools.js +19 -50
- package/pi-extension/dist/cognitive-routing-v2/types.js +5 -1
- package/pi-extension/dist/context-virtualization/runtime.js +29 -7
- package/pi-extension/dist/freeflow-context/tool.js +7 -6
- package/pi-extension/dist/index.js +36 -15
- package/pi-extension/dist/provider-support/astra/adapter.js +110 -0
- package/pi-extension/dist/provider-support/astra/history.js +125 -0
- package/pi-extension/dist/provider-support/astra/index.js +11 -0
- package/pi-extension/dist/provider-support/astra/session-state.js +114 -0
- package/pi-extension/dist/provider-support/index.js +4 -0
- package/pi-extension/dist/runtime/request-history.js +216 -0
- package/pi-extension/dist/runtime/runtime-context.js +29 -10
- package/pi-extension/dist/session-sources/events.js +22 -0
- package/pi-extension/dist/session-sources/provenance.js +32 -0
- package/pi-extension/dist/session-sources/sources.js +51 -7
- package/pi-extension/dist/settings/settings-ui.js +247 -47
- package/pi-extension/src/cognitive-routing-v2/config.ts +49 -19
- package/pi-extension/src/cognitive-routing-v2/projection.ts +53 -43
- package/pi-extension/src/cognitive-routing-v2/render.ts +31 -10
- package/pi-extension/src/cognitive-routing-v2/runtime.ts +712 -105
- package/pi-extension/src/cognitive-routing-v2/schemas.ts +95 -24
- package/pi-extension/src/cognitive-routing-v2/state.ts +244 -27
- package/pi-extension/src/cognitive-routing-v2/tools.ts +19 -50
- package/pi-extension/src/cognitive-routing-v2/types.ts +35 -3
- package/pi-extension/src/context-virtualization/runtime.ts +30 -8
- package/pi-extension/src/freeflow-context/tool.ts +7 -6
- package/pi-extension/src/index.ts +36 -14
- package/pi-extension/src/provider-support/astra/adapter.ts +108 -0
- package/pi-extension/src/provider-support/astra/history.ts +145 -0
- package/pi-extension/src/provider-support/astra/index.ts +12 -0
- package/pi-extension/src/provider-support/astra/session-state.ts +113 -0
- package/pi-extension/src/provider-support/index.ts +5 -0
- package/pi-extension/src/runtime/request-history.ts +243 -0
- package/pi-extension/src/runtime/runtime-context.ts +32 -10
- package/pi-extension/src/session-sources/events.ts +22 -0
- package/pi-extension/src/session-sources/provenance.ts +39 -0
- package/pi-extension/src/session-sources/sources.ts +77 -6
- package/pi-extension/src/settings/settings-ui.ts +276 -46
- package/pi-extension/tests/cache-reuse/history.test.js +117 -0
- package/pi-extension/tests/cache-reuse/native.test.js +91 -0
- package/pi-extension/tests/cache-reuse/settings.test.js +211 -0
- package/pi-extension/tests/cognitive-routing-v2/communication.test.js +94 -1
- package/pi-extension/tests/cognitive-routing-v2/delegation.test.js +557 -0
- package/pi-extension/tests/cognitive-routing-v2/native.test.js +34 -1
- package/pi-extension/tests/cognitive-routing-v2/persistence.test.js +55 -0
- package/pi-extension/tests/cognitive-routing-v2/recovery.test.js +800 -2
- package/pi-extension/tests/cognitive-routing-v2/render.test.js +15 -1
- package/pi-extension/tests/cognitive-routing-v2/runtime.test.js +273 -3
- package/pi-extension/tests/cognitive-routing-v2/state.test.js +312 -1
- package/pi-extension/tests/context-virtualization/extension.test.js +20 -10
- package/pi-extension/tests/context-virtualization/runtime.test.js +17 -3
- package/pi-extension/tests/fixtures/routing-native.js +4 -1
- package/pi-extension/tests/integration/mode-free-core.test.js +4 -3
- package/pi-extension/tests/integration/pi-extension.test.js +508 -29
- package/pi-extension/tests/integration/prompt-architecture.test.js +56 -40
- package/pi-extension/tests/integration/unaffected-regressions.test.js +3 -3
- package/pi-extension/tests/provider-support/astra-boundaries.test.js +75 -0
- package/pi-extension/tests/provider-support/astra.test.js +306 -0
- package/pi-extension/tests/provider-support/cli.test.js +92 -0
- package/pi-extension/tests/provider-support/native.test.js +183 -0
- package/plugin.json +1 -1
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/skills/design-for-depth/SKILL.md +1 -1
- package/skills/design-for-depth/references/implementation-decisions.md +3 -3
- package/skills/discuss/SKILL.md +2 -2
|
@@ -1,29 +1,56 @@
|
|
|
1
1
|
import { isObject } from "./types.js";
|
|
2
|
-
const string = (maxLength = 32768) => ({
|
|
3
|
-
|
|
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) => ({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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: {
|
|
53
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
d.handoff.
|
|
218
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
518
|
+
(x) => x.assignmentId === a.id && isWorkerProfile(x.profile) && !x.assistantEntryId && !x.interrupted,
|
|
429
519
|
),
|
|
430
|
-
"
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
531
|
-
(a.state === "outstanding" ||
|
|
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
|
-
|
|
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")
|
|
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
|
|
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
|
}
|