@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,61 @@
|
|
|
1
1
|
import { isObject } from "./types.js";
|
|
2
|
-
const string = (maxLength = 32768) => ({
|
|
3
|
-
|
|
2
|
+
const string = (maxLength = 32768, description?: string) => ({
|
|
3
|
+
type: "string",
|
|
4
|
+
minLength: 1,
|
|
5
|
+
maxLength,
|
|
6
|
+
...(description ? { description } : {}),
|
|
7
|
+
});
|
|
8
|
+
const object = (
|
|
9
|
+
properties: Record<string, any>,
|
|
10
|
+
required = Object.keys(properties),
|
|
11
|
+
description?: string,
|
|
12
|
+
title?: string,
|
|
13
|
+
) => ({
|
|
4
14
|
type: "object",
|
|
5
15
|
additionalProperties: false,
|
|
6
16
|
properties,
|
|
7
17
|
required,
|
|
18
|
+
...(description ? { description } : {}),
|
|
19
|
+
...(title ? { title } : {}),
|
|
8
20
|
});
|
|
9
|
-
const operation = (value: string) => ({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
const operation = (value: string, description?: string) => ({
|
|
22
|
+
type: "string",
|
|
23
|
+
enum: [value],
|
|
24
|
+
...(description ? { description } : {}),
|
|
25
|
+
});
|
|
26
|
+
const evidenceRefs = (description: string) => ({
|
|
27
|
+
type: "array",
|
|
28
|
+
maxItems: 64,
|
|
29
|
+
items: string(512),
|
|
30
|
+
description,
|
|
31
|
+
});
|
|
32
|
+
const worker = {
|
|
33
|
+
type: "string",
|
|
34
|
+
enum: ["helper", "executor"],
|
|
35
|
+
description: "Worker for this assignment. Required when delegation mode enables both workers.",
|
|
36
|
+
};
|
|
37
|
+
const report = (name: "submit" | "supplement") =>
|
|
38
|
+
object(
|
|
39
|
+
{
|
|
40
|
+
operation: operation(name),
|
|
41
|
+
report: string(),
|
|
42
|
+
outcome: { type: "string", enum: ["completed", "partial", "blocked"] },
|
|
43
|
+
limitations: { type: "array", maxItems: 32, items: string(2048) },
|
|
44
|
+
},
|
|
45
|
+
["operation", "report", "outcome"],
|
|
46
|
+
);
|
|
19
47
|
export const ROUTING_SCHEMAS: Record<string, any> = {
|
|
20
48
|
freeflow_delegate: {
|
|
21
49
|
oneOf: [
|
|
22
|
-
object({ operation: operation("assign"), contract: string() }),
|
|
23
|
-
object({ operation: operation("replace"), contract: string(), reason: string(2048) }
|
|
50
|
+
object({ operation: operation("assign"), contract: string(), worker }, ["operation", "contract"]),
|
|
51
|
+
object({ operation: operation("replace"), contract: string(), reason: string(2048), worker }, [
|
|
52
|
+
"operation",
|
|
53
|
+
"contract",
|
|
54
|
+
"reason",
|
|
55
|
+
]),
|
|
24
56
|
],
|
|
25
57
|
},
|
|
26
|
-
freeflow_return: { oneOf: [submit, object({ operation: operation("retry") })] },
|
|
58
|
+
freeflow_return: { oneOf: [report("submit"), report("supplement"), object({ operation: operation("retry") })] },
|
|
27
59
|
freeflow_unit: {
|
|
28
60
|
oneOf: [
|
|
29
61
|
object(
|
|
@@ -37,6 +69,15 @@ export const ROUTING_SCHEMAS: Record<string, any> = {
|
|
|
37
69
|
["operation"],
|
|
38
70
|
),
|
|
39
71
|
object({ operation: operation("assess") }),
|
|
72
|
+
object(
|
|
73
|
+
{
|
|
74
|
+
operation: operation("recover"),
|
|
75
|
+
request: string(),
|
|
76
|
+
paths: { type: "array", maxItems: 32, items: string(4096) },
|
|
77
|
+
},
|
|
78
|
+
["operation", "request"],
|
|
79
|
+
),
|
|
80
|
+
object({ operation: operation("cancel-recovery"), reason: string(2048) }),
|
|
40
81
|
object({
|
|
41
82
|
operation: operation("close"),
|
|
42
83
|
outcome: { type: "string", enum: ["accepted", "cancelled", "deferred"] },
|
|
@@ -48,18 +89,41 @@ export const ROUTING_SCHEMAS: Record<string, any> = {
|
|
|
48
89
|
oneOf: [
|
|
49
90
|
object(
|
|
50
91
|
{
|
|
51
|
-
operation: operation("inspect"),
|
|
52
|
-
scope: {
|
|
53
|
-
|
|
92
|
+
operation: operation("inspect", "Read current selection and offered evidence candidates."),
|
|
93
|
+
scope: {
|
|
94
|
+
type: "string",
|
|
95
|
+
enum: ["selected", "assignment", "active", "history"],
|
|
96
|
+
description: "Optional candidate scope; omit to inspect the current assignment.",
|
|
97
|
+
},
|
|
98
|
+
cursor: string(128, "Use the nextCursor returned by a previous inspection."),
|
|
54
99
|
},
|
|
55
100
|
["operation"],
|
|
101
|
+
"Inspect selection and candidates without changing evidence state.",
|
|
102
|
+
"Inspect evidence selection",
|
|
103
|
+
),
|
|
104
|
+
object(
|
|
105
|
+
{
|
|
106
|
+
operation: operation("add", "Select eligible worker task-evidence refs; do not include reason."),
|
|
107
|
+
refs: evidenceRefs(
|
|
108
|
+
"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.",
|
|
109
|
+
),
|
|
110
|
+
},
|
|
111
|
+
["operation", "refs"],
|
|
112
|
+
"Add evidence refs. This shape has no reason field.",
|
|
113
|
+
"Add evidence",
|
|
114
|
+
),
|
|
115
|
+
object(
|
|
116
|
+
{
|
|
117
|
+
operation: operation("remove", "Withdraw selected or unresolved refs; reason is required."),
|
|
118
|
+
refs: evidenceRefs(
|
|
119
|
+
"Currently selected or unresolved refs to withdraw. A non-selectable ref may be removed with a reason to clear its unresolved request.",
|
|
120
|
+
),
|
|
121
|
+
reason: string(2048, "Required only for remove; explain why the selection is being withdrawn."),
|
|
122
|
+
},
|
|
123
|
+
["operation", "refs", "reason"],
|
|
124
|
+
"Remove evidence refs. This is the only operation that accepts reason.",
|
|
125
|
+
"Remove evidence",
|
|
56
126
|
),
|
|
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
127
|
],
|
|
64
128
|
},
|
|
65
129
|
};
|
|
@@ -73,7 +137,14 @@ for (const schema of Object.values(ROUTING_SCHEMAS)) {
|
|
|
73
137
|
schema.properties.operation = {
|
|
74
138
|
type: "string",
|
|
75
139
|
enum: schema.oneOf.flatMap((branch: any) => branch.properties.operation.enum),
|
|
140
|
+
description: "Choose exactly one operation; each operation has a strict shape and rejects unknown fields.",
|
|
76
141
|
};
|
|
142
|
+
if (schema.properties.refs)
|
|
143
|
+
schema.properties.refs = {
|
|
144
|
+
...schema.properties.refs,
|
|
145
|
+
description:
|
|
146
|
+
"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.",
|
|
147
|
+
};
|
|
77
148
|
}
|
|
78
149
|
|
|
79
150
|
export function matches(value: any, schema: any): boolean {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
eventKey,
|
|
8
8
|
eventValue,
|
|
9
9
|
isObject,
|
|
10
|
+
isWorkerProfile,
|
|
10
11
|
requireCondition as check,
|
|
11
12
|
refFor,
|
|
12
13
|
type RoutingEvent,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
type NativeEntry,
|
|
15
16
|
type Pair,
|
|
16
17
|
type Handoff,
|
|
18
|
+
type Recovery,
|
|
17
19
|
type Reservation,
|
|
18
20
|
} from "./types.js";
|
|
19
21
|
|
|
@@ -28,6 +30,11 @@ const pair = (x: any): x is Pair =>
|
|
|
28
30
|
identity(x.provider) &&
|
|
29
31
|
identity(x.modelId) &&
|
|
30
32
|
EFFORTS.includes(x.thinking);
|
|
33
|
+
const profileOverrides = (x: any): boolean =>
|
|
34
|
+
isObject(x) &&
|
|
35
|
+
Object.keys(x).length > 0 &&
|
|
36
|
+
Object.keys(x).every((key) => PROFILES.includes(key as any)) &&
|
|
37
|
+
Object.values(x).every((value) => value === null || pair(value));
|
|
31
38
|
const problemList = (xs: any): boolean =>
|
|
32
39
|
Array.isArray(xs) &&
|
|
33
40
|
xs.every(
|
|
@@ -60,6 +67,41 @@ const reservation = (r: any): r is Reservation =>
|
|
|
60
67
|
Number.isFinite(r.outputReserve) &&
|
|
61
68
|
r.outputReserve >= 0 &&
|
|
62
69
|
text(r.estimateMethod);
|
|
70
|
+
const recovery = (r: any): r is Recovery =>
|
|
71
|
+
shape(r, [
|
|
72
|
+
"id",
|
|
73
|
+
"assignmentId",
|
|
74
|
+
"assessmentHandoffId",
|
|
75
|
+
"baseReportRevision",
|
|
76
|
+
"request",
|
|
77
|
+
"requestedPaths",
|
|
78
|
+
"paths",
|
|
79
|
+
"state",
|
|
80
|
+
"requestHandoffId",
|
|
81
|
+
"supplementHandoffId",
|
|
82
|
+
"supplementRevision",
|
|
83
|
+
"cancellationReason",
|
|
84
|
+
]) &&
|
|
85
|
+
identity(r.id) &&
|
|
86
|
+
identity(r.assignmentId) &&
|
|
87
|
+
identity(r.assessmentHandoffId) &&
|
|
88
|
+
Number.isSafeInteger(r.baseReportRevision) &&
|
|
89
|
+
r.baseReportRevision > 0 &&
|
|
90
|
+
text(r.request) &&
|
|
91
|
+
Array.isArray(r.requestedPaths) &&
|
|
92
|
+
r.requestedPaths.length <= 32 &&
|
|
93
|
+
r.requestedPaths.every((path: unknown) => text(path, 4096)) &&
|
|
94
|
+
new Set(r.requestedPaths).size === r.requestedPaths.length &&
|
|
95
|
+
Array.isArray(r.paths) &&
|
|
96
|
+
r.paths.length <= 32 &&
|
|
97
|
+
r.paths.every((path: unknown) => text(path, 4096)) &&
|
|
98
|
+
new Set(r.paths).size === r.paths.length &&
|
|
99
|
+
["requested", "reading", "returning", "completed", "cancelled"].includes(r.state) &&
|
|
100
|
+
identity(r.requestHandoffId) &&
|
|
101
|
+
(r.supplementHandoffId === undefined || identity(r.supplementHandoffId)) &&
|
|
102
|
+
Number.isSafeInteger(r.supplementRevision) &&
|
|
103
|
+
r.supplementRevision >= 0 &&
|
|
104
|
+
(r.cancellationReason === undefined || text(r.cancellationReason, 2048));
|
|
63
105
|
const handoff = (h: any): h is Handoff =>
|
|
64
106
|
shape(h, [
|
|
65
107
|
"id",
|
|
@@ -83,7 +125,7 @@ const handoff = (h: any): h is Handoff =>
|
|
|
83
125
|
identity(h.executionId) &&
|
|
84
126
|
identity(h.toolCallId) &&
|
|
85
127
|
nullableId(h.basisUserEntryId) &&
|
|
86
|
-
["delegate", "return"].includes(h.kind) &&
|
|
128
|
+
["delegate", "return", "recovery-request", "recovery-return"].includes(h.kind) &&
|
|
87
129
|
PROFILES.includes(h.from) &&
|
|
88
130
|
PROFILES.includes(h.to) &&
|
|
89
131
|
h.from !== h.to &&
|
|
@@ -95,15 +137,21 @@ const handoff = (h: any): h is Handoff =>
|
|
|
95
137
|
h.limitations.every((x: unknown) => text(x, 2048)) &&
|
|
96
138
|
(h.reason === undefined || text(h.reason, 4096)) &&
|
|
97
139
|
(h.outcome === undefined || ["completed", "partial", "blocked"].includes(h.outcome)) &&
|
|
98
|
-
(
|
|
140
|
+
(["delegate", "recovery-request"].includes(h.kind)
|
|
141
|
+
? h.outcome === undefined && h.reportRevision === 0
|
|
142
|
+
: ["completed", "partial", "blocked"].includes(h.outcome) && h.reportRevision > 0);
|
|
99
143
|
const fields: Record<string, string[]> = {
|
|
100
144
|
"execution-interrupted": ["executionId", "reason"],
|
|
101
145
|
"assignment-resumed": ["assignmentId", "basisUserEntryId"],
|
|
102
146
|
control: ["control", "profile", "reason"],
|
|
147
|
+
"profile-overrides": ["overrides", "reason"],
|
|
103
148
|
"execution-opened": ["execution"],
|
|
104
149
|
"execution-bound": ["executionId", "assistantEntryId", "resultEntryIds", "outcome"],
|
|
105
150
|
"delegate-accepted": ["unit", "assignment", "handoff", "replacement"],
|
|
106
151
|
"return-accepted": ["handoff"],
|
|
152
|
+
"recovery-request-accepted": ["recovery", "handoff"],
|
|
153
|
+
"recovery-supplement-accepted": ["recoveryId", "handoff"],
|
|
154
|
+
"recovery-cancelled": ["recoveryId", "reason"],
|
|
107
155
|
"handoff-retry-requested": [
|
|
108
156
|
"handoffId",
|
|
109
157
|
"attemptId",
|
|
@@ -163,17 +211,21 @@ export function parseRoutingEvent(raw: unknown): RoutingEvent {
|
|
|
163
211
|
"invalid_control",
|
|
164
212
|
);
|
|
165
213
|
break;
|
|
214
|
+
case "profile-overrides":
|
|
215
|
+
check(profileOverrides(d.overrides) && text(d.reason, 4096), "invalid_profile_overrides");
|
|
216
|
+
break;
|
|
166
217
|
case "execution-opened": {
|
|
167
218
|
const e = d.execution;
|
|
168
219
|
check(
|
|
169
|
-
shape(e, ["id", "profile", "assignmentId", "basisUserEntryId", "pair", "resultEntryIds"]) &&
|
|
220
|
+
shape(e, ["id", "profile", "assignmentId", "recoveryId", "basisUserEntryId", "pair", "resultEntryIds"]) &&
|
|
170
221
|
identity(e.id) &&
|
|
171
222
|
["solo", ...PROFILES].includes(e.profile) &&
|
|
172
223
|
nullableId(e.basisUserEntryId) &&
|
|
173
224
|
pair(e.pair) &&
|
|
174
225
|
uniqueStrings(e.resultEntryIds) &&
|
|
175
226
|
e.resultEntryIds.length === 0 &&
|
|
176
|
-
(e.assignmentId === undefined || identity(e.assignmentId))
|
|
227
|
+
(e.assignmentId === undefined || identity(e.assignmentId)) &&
|
|
228
|
+
(e.recoveryId === undefined || identity(e.recoveryId)),
|
|
177
229
|
"invalid_execution",
|
|
178
230
|
);
|
|
179
231
|
break;
|
|
@@ -221,13 +273,28 @@ export function parseRoutingEvent(raw: unknown): RoutingEvent {
|
|
|
221
273
|
break;
|
|
222
274
|
}
|
|
223
275
|
case "return-accepted":
|
|
276
|
+
check(handoff(d.handoff) && d.handoff.kind === "return", "invalid_return");
|
|
277
|
+
break;
|
|
278
|
+
case "recovery-request-accepted":
|
|
224
279
|
check(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
d.handoff.
|
|
228
|
-
|
|
280
|
+
recovery(d.recovery) &&
|
|
281
|
+
handoff(d.handoff) &&
|
|
282
|
+
d.handoff.kind === "recovery-request" &&
|
|
283
|
+
d.recovery.requestHandoffId === d.handoff.id &&
|
|
284
|
+
d.recovery.assignmentId === d.handoff.assignmentId &&
|
|
285
|
+
d.recovery.request === d.handoff.text,
|
|
286
|
+
"invalid_recovery_request",
|
|
229
287
|
);
|
|
230
288
|
break;
|
|
289
|
+
case "recovery-supplement-accepted":
|
|
290
|
+
check(
|
|
291
|
+
identity(d.recoveryId) && handoff(d.handoff) && d.handoff.kind === "recovery-return",
|
|
292
|
+
"invalid_recovery_supplement",
|
|
293
|
+
);
|
|
294
|
+
break;
|
|
295
|
+
case "recovery-cancelled":
|
|
296
|
+
check(identity(d.recoveryId) && text(d.reason, 2048), "invalid_recovery_cancellation");
|
|
297
|
+
break;
|
|
231
298
|
case "handoff-retry-requested":
|
|
232
299
|
check(
|
|
233
300
|
identity(d.attemptId) &&
|
|
@@ -275,7 +342,10 @@ export function parseRoutingEvent(raw: unknown): RoutingEvent {
|
|
|
275
342
|
);
|
|
276
343
|
break;
|
|
277
344
|
case "assessment-resumed":
|
|
278
|
-
check(
|
|
345
|
+
check(
|
|
346
|
+
nullableId(d.basisUserEntryId) && (d.reservation === undefined || reservation(d.reservation)),
|
|
347
|
+
"invalid_resumption",
|
|
348
|
+
);
|
|
279
349
|
break;
|
|
280
350
|
case "unit-closed":
|
|
281
351
|
check(
|
|
@@ -315,6 +385,7 @@ export function initialState(): State {
|
|
|
315
385
|
units: new Map(),
|
|
316
386
|
assignments: new Map(),
|
|
317
387
|
handoffs: new Map(),
|
|
388
|
+
recoveries: new Map(),
|
|
318
389
|
executions: new Map(),
|
|
319
390
|
selections: new Map(),
|
|
320
391
|
reservations: new Map(),
|
|
@@ -322,6 +393,7 @@ export function initialState(): State {
|
|
|
322
393
|
exposure: new Map(),
|
|
323
394
|
authors: new Map(),
|
|
324
395
|
resumeBasis: new Map(),
|
|
396
|
+
profileOverrides: new Map(),
|
|
325
397
|
events: new Map(),
|
|
326
398
|
eventIds: new Map(),
|
|
327
399
|
};
|
|
@@ -344,6 +416,7 @@ function applyEvent(state: State, event: RoutingEvent, owned = false): State {
|
|
|
344
416
|
units: new Map([...state.units].map(([k, v]) => [k, { ...v }])),
|
|
345
417
|
assignments: new Map([...state.assignments].map(([k, v]) => [k, { ...v }])),
|
|
346
418
|
handoffs: new Map([...state.handoffs].map(([k, v]) => [k, { ...v }])),
|
|
419
|
+
recoveries: new Map([...state.recoveries].map(([k, v]) => [k, { ...v, paths: [...v.paths] }])),
|
|
347
420
|
executions: new Map([...state.executions].map(([k, v]) => [k, { ...v }])),
|
|
348
421
|
selections: new Map(state.selections),
|
|
349
422
|
reservations: new Map(state.reservations),
|
|
@@ -351,17 +424,27 @@ function applyEvent(state: State, event: RoutingEvent, owned = false): State {
|
|
|
351
424
|
exposure: new Map(state.exposure),
|
|
352
425
|
authors: new Map(state.authors),
|
|
353
426
|
resumeBasis: new Map(state.resumeBasis),
|
|
427
|
+
profileOverrides: new Map(state.profileOverrides),
|
|
354
428
|
events: new Map(state.events),
|
|
355
429
|
eventIds: new Map(state.eventIds),
|
|
356
430
|
assessment: state.assessment ? { ...state.assessment } : undefined,
|
|
357
431
|
},
|
|
358
432
|
d = e.data;
|
|
359
433
|
const assignment = () => (s.assignmentId ? s.assignments.get(s.assignmentId) : undefined);
|
|
434
|
+
const assignmentWorker = () => {
|
|
435
|
+
const a = assignment();
|
|
436
|
+
const h = a ? s.handoffs.get(a.delegateHandoffId) : undefined;
|
|
437
|
+
check(h?.kind === "delegate" && h.assignmentId === a?.id && isWorkerProfile(h.to), "assignment_worker_missing");
|
|
438
|
+
return h.to;
|
|
439
|
+
};
|
|
360
440
|
const pending = () => (s.pendingId ? s.handoffs.get(s.pendingId) : undefined);
|
|
361
441
|
const isPending = () => pending() && ["pending", "blocked"].includes(pending()!.state);
|
|
362
442
|
const currentReturn = (id: string) => {
|
|
363
443
|
const h = s.handoffs.get(id);
|
|
364
|
-
check(
|
|
444
|
+
check(
|
|
445
|
+
h && ["return", "recovery-return"].includes(h.kind) && h.assignmentId === s.assignmentId,
|
|
446
|
+
"not_current_return",
|
|
447
|
+
);
|
|
365
448
|
return h;
|
|
366
449
|
};
|
|
367
450
|
switch (d.type) {
|
|
@@ -384,6 +467,14 @@ function applyEvent(state: State, event: RoutingEvent, owned = false): State {
|
|
|
384
467
|
s.control = d.control;
|
|
385
468
|
s.profile = d.profile;
|
|
386
469
|
break;
|
|
470
|
+
case "profile-overrides":
|
|
471
|
+
for (const profile of PROFILES) {
|
|
472
|
+
if (!Object.hasOwn(d.overrides, profile)) continue;
|
|
473
|
+
const override = d.overrides[profile];
|
|
474
|
+
if (override === null) s.profileOverrides.delete(profile);
|
|
475
|
+
else s.profileOverrides.set(profile, override);
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
387
478
|
case "execution-opened":
|
|
388
479
|
check(!s.executions.has(d.execution.id), "duplicate_execution");
|
|
389
480
|
s.executions.set(d.execution.id, d.execution);
|
|
@@ -417,7 +508,7 @@ function applyEvent(state: State, event: RoutingEvent, owned = false): State {
|
|
|
417
508
|
check(
|
|
418
509
|
h.kind === "delegate" &&
|
|
419
510
|
h.from === "coordinator" &&
|
|
420
|
-
h.to
|
|
511
|
+
isWorkerProfile(h.to) &&
|
|
421
512
|
h.state === "pending" &&
|
|
422
513
|
h.text === d.assignment.contract &&
|
|
423
514
|
h.assignmentId === d.assignment.id &&
|
|
@@ -435,9 +526,9 @@ function applyEvent(state: State, event: RoutingEvent, owned = false): State {
|
|
|
435
526
|
check(a?.state === "outstanding" && a.id === d.replacement.assignmentId && u, "no_replaceable_assignment");
|
|
436
527
|
check(
|
|
437
528
|
![...s.executions.values()].some(
|
|
438
|
-
(x) => x.assignmentId === a.id && x.profile
|
|
529
|
+
(x) => x.assignmentId === a.id && isWorkerProfile(x.profile) && !x.assistantEntryId && !x.interrupted,
|
|
439
530
|
),
|
|
440
|
-
"
|
|
531
|
+
"unresolved_worker_execution",
|
|
441
532
|
);
|
|
442
533
|
check(
|
|
443
534
|
!isPending() || (pending()!.kind === "delegate" && d.replacement.supersededHandoffId === pending()!.id),
|
|
@@ -456,14 +547,14 @@ function applyEvent(state: State, event: RoutingEvent, owned = false): State {
|
|
|
456
547
|
break;
|
|
457
548
|
}
|
|
458
549
|
case "return-accepted": {
|
|
459
|
-
check(s.control === "automatic" && s.profile ===
|
|
550
|
+
check(s.control === "automatic" && s.profile === assignmentWorker(), "wrong_control");
|
|
460
551
|
const a = assignment(),
|
|
461
552
|
h = d.handoff;
|
|
462
553
|
check(
|
|
463
554
|
a &&
|
|
464
555
|
h.assignmentId === a.id &&
|
|
465
556
|
h.kind === "return" &&
|
|
466
|
-
h.from ===
|
|
557
|
+
h.from === assignmentWorker() &&
|
|
467
558
|
h.to === "coordinator" &&
|
|
468
559
|
h.state === "pending",
|
|
469
560
|
"return_identity_mismatch",
|
|
@@ -493,11 +584,101 @@ function applyEvent(state: State, event: RoutingEvent, owned = false): State {
|
|
|
493
584
|
};
|
|
494
585
|
break;
|
|
495
586
|
}
|
|
587
|
+
case "recovery-request-accepted": {
|
|
588
|
+
const a = assignment(),
|
|
589
|
+
base = s.handoffs.get(d.recovery.assessmentHandoffId),
|
|
590
|
+
h = d.handoff;
|
|
591
|
+
check(
|
|
592
|
+
s.control === "automatic" &&
|
|
593
|
+
s.profile === "coordinator" &&
|
|
594
|
+
a?.state === "returned" &&
|
|
595
|
+
d.recovery.assignmentId === a.id &&
|
|
596
|
+
d.recovery.state === "requested" &&
|
|
597
|
+
d.recovery.supplementRevision === 0 &&
|
|
598
|
+
d.recovery.supplementHandoffId === undefined &&
|
|
599
|
+
d.recovery.cancellationReason === undefined &&
|
|
600
|
+
s.assessment?.handoffId === base?.id &&
|
|
601
|
+
base?.kind === "return" &&
|
|
602
|
+
a.returnHandoffId === base.id &&
|
|
603
|
+
base.state === "configured" &&
|
|
604
|
+
base.reportRevision === d.recovery.baseReportRevision &&
|
|
605
|
+
!s.recoveryId &&
|
|
606
|
+
!isPending() &&
|
|
607
|
+
h.kind === "recovery-request" &&
|
|
608
|
+
h.assignmentId === a.id &&
|
|
609
|
+
h.from === "coordinator" &&
|
|
610
|
+
h.to === assignmentWorker() &&
|
|
611
|
+
h.state === "pending" &&
|
|
612
|
+
!s.handoffs.has(h.id) &&
|
|
613
|
+
!s.recoveries.has(d.recovery.id),
|
|
614
|
+
"recovery_not_available",
|
|
615
|
+
);
|
|
616
|
+
s.recoveries.set(d.recovery.id, d.recovery);
|
|
617
|
+
s.recoveryId = d.recovery.id;
|
|
618
|
+
s.handoffs.set(h.id, h);
|
|
619
|
+
s.pendingId = h.id;
|
|
620
|
+
s.assessment.view = "suspended";
|
|
621
|
+
s.assessment.suspensionReason = "recovery";
|
|
622
|
+
s.assessment.basisUserEntryId = h.basisUserEntryId;
|
|
623
|
+
s.assessment.problems = [];
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
case "recovery-supplement-accepted": {
|
|
627
|
+
const a = assignment(),
|
|
628
|
+
r = s.recoveries.get(d.recoveryId),
|
|
629
|
+
h = d.handoff,
|
|
630
|
+
previous = r?.supplementHandoffId ? s.handoffs.get(r.supplementHandoffId) : undefined;
|
|
631
|
+
check(
|
|
632
|
+
s.control === "automatic" &&
|
|
633
|
+
s.profile === assignmentWorker() &&
|
|
634
|
+
a?.state === "returned" &&
|
|
635
|
+
r?.id === s.recoveryId &&
|
|
636
|
+
r.assignmentId === a.id &&
|
|
637
|
+
s.assessment?.handoffId === r.assessmentHandoffId &&
|
|
638
|
+
s.handoffs.get(r.assessmentHandoffId)?.reportRevision === r.baseReportRevision &&
|
|
639
|
+
["reading", "returning"].includes(r.state) &&
|
|
640
|
+
h.kind === "recovery-return" &&
|
|
641
|
+
h.assignmentId === a.id &&
|
|
642
|
+
h.from === assignmentWorker() &&
|
|
643
|
+
h.to === "coordinator" &&
|
|
644
|
+
h.state === "pending" &&
|
|
645
|
+
(!previous ? !s.handoffs.has(h.id) : ["pending", "blocked"].includes(previous.state) && previous.id === h.id),
|
|
646
|
+
"recovery_not_returnable",
|
|
647
|
+
);
|
|
648
|
+
const expectedRevision = (previous?.reportRevision ?? 0) + 1;
|
|
649
|
+
check(h.reportRevision === expectedRevision, "invalid_report_revision");
|
|
650
|
+
r.state = "returning";
|
|
651
|
+
r.supplementHandoffId = h.id;
|
|
652
|
+
r.supplementRevision = h.reportRevision;
|
|
653
|
+
s.handoffs.set(h.id, h);
|
|
654
|
+
s.pendingId = h.id;
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
case "recovery-cancelled": {
|
|
658
|
+
const r = s.recoveries.get(d.recoveryId);
|
|
659
|
+
check(
|
|
660
|
+
s.control === "automatic" &&
|
|
661
|
+
s.profile === "coordinator" &&
|
|
662
|
+
r?.id === s.recoveryId &&
|
|
663
|
+
["requested", "reading", "returning"].includes(r.state),
|
|
664
|
+
"recovery_not_cancellable",
|
|
665
|
+
);
|
|
666
|
+
const p = pending();
|
|
667
|
+
if (p) {
|
|
668
|
+
check([r.requestHandoffId, r.supplementHandoffId].includes(p.id), "handoff_pending");
|
|
669
|
+
p.state = "superseded";
|
|
670
|
+
s.pendingId = undefined;
|
|
671
|
+
}
|
|
672
|
+
r.state = "cancelled";
|
|
673
|
+
r.cancellationReason = d.reason;
|
|
674
|
+
s.recoveryId = undefined;
|
|
675
|
+
break;
|
|
676
|
+
}
|
|
496
677
|
case "handoff-retry-requested": {
|
|
497
678
|
const h = currentReturn(d.handoffId);
|
|
498
679
|
check(
|
|
499
680
|
s.control === "automatic" &&
|
|
500
|
-
s.profile ===
|
|
681
|
+
s.profile === assignmentWorker() &&
|
|
501
682
|
["blocked", "pending"].includes(h.state) &&
|
|
502
683
|
h.reportRevision === d.reportRevision &&
|
|
503
684
|
(s.selections.get(h.assignmentId)?.revision ?? 0) === d.selectionRevision,
|
|
@@ -517,6 +698,11 @@ function applyEvent(state: State, event: RoutingEvent, owned = false): State {
|
|
|
517
698
|
check(d.reservation.selectionRevision === (s.selections.get(h.assignmentId)?.revision ?? 0), "stale_selection");
|
|
518
699
|
s.reservations.set(h.id, d.reservation);
|
|
519
700
|
if (s.assessment?.handoffId === h.id) s.assessment.reservation = d.reservation;
|
|
701
|
+
if (h.kind === "recovery-return") {
|
|
702
|
+
const recovery = [...s.recoveries.values()].find((candidate) => candidate.supplementHandoffId === h.id);
|
|
703
|
+
if (recovery && s.assessment?.handoffId === recovery.assessmentHandoffId)
|
|
704
|
+
s.assessment.reservation = d.reservation;
|
|
705
|
+
}
|
|
520
706
|
break;
|
|
521
707
|
}
|
|
522
708
|
case "handoff-state": {
|
|
@@ -528,6 +714,12 @@ function applyEvent(state: State, event: RoutingEvent, owned = false): State {
|
|
|
528
714
|
check(d.observedPair, "configuration_unobserved");
|
|
529
715
|
s.profile = h.to;
|
|
530
716
|
s.pendingId = undefined;
|
|
717
|
+
const r = s.recoveryId ? s.recoveries.get(s.recoveryId) : undefined;
|
|
718
|
+
if (r && h.id === r.requestHandoffId) r.state = "reading";
|
|
719
|
+
if (r && h.id === r.supplementHandoffId) {
|
|
720
|
+
r.state = "completed";
|
|
721
|
+
s.recoveryId = undefined;
|
|
722
|
+
}
|
|
531
723
|
}
|
|
532
724
|
if (d.state === "superseded") s.pendingId = undefined;
|
|
533
725
|
break;
|
|
@@ -537,8 +729,10 @@ function applyEvent(state: State, event: RoutingEvent, owned = false): State {
|
|
|
537
729
|
check(
|
|
538
730
|
a?.id === d.assignmentId &&
|
|
539
731
|
s.control === "automatic" &&
|
|
540
|
-
s.profile ===
|
|
541
|
-
(a.state === "outstanding" ||
|
|
732
|
+
s.profile === assignmentWorker() &&
|
|
733
|
+
(a.state === "outstanding" ||
|
|
734
|
+
(a.state === "returned" &&
|
|
735
|
+
(isPending() || (s.recoveryId !== undefined && s.recoveries.get(s.recoveryId)?.state === "reading")))),
|
|
542
736
|
"selection_wrong_phase",
|
|
543
737
|
);
|
|
544
738
|
check(d.selection.revision === (s.selections.get(a.id)?.revision ?? 0) + 1, "selection_revision");
|
|
@@ -548,6 +742,7 @@ function applyEvent(state: State, event: RoutingEvent, owned = false): State {
|
|
|
548
742
|
case "assessment-suspended": {
|
|
549
743
|
check(s.assessment?.handoffId === d.handoffId, "assessment_missing");
|
|
550
744
|
s.assessment.view = "suspended";
|
|
745
|
+
s.assessment.suspensionReason = d.reason;
|
|
551
746
|
s.assessment.basisUserEntryId = d.basisUserEntryId;
|
|
552
747
|
s.assessment.problems = d.problems;
|
|
553
748
|
break;
|
|
@@ -556,19 +751,24 @@ function applyEvent(state: State, event: RoutingEvent, owned = false): State {
|
|
|
556
751
|
check(
|
|
557
752
|
s.control === "automatic" &&
|
|
558
753
|
s.profile === "coordinator" &&
|
|
754
|
+
!s.recoveryId &&
|
|
559
755
|
s.assessment?.handoffId === d.handoffId &&
|
|
560
756
|
s.assessment.view === "suspended",
|
|
561
|
-
"assessment_not_suspended",
|
|
562
|
-
);
|
|
563
|
-
check(
|
|
564
|
-
d.reservation.selectionRevision === (s.selections.get(s.assessment.assignmentId)?.revision ?? 0),
|
|
565
|
-
"stale_selection",
|
|
757
|
+
s.recoveryId ? "recovery_outstanding" : "assessment_not_suspended",
|
|
566
758
|
);
|
|
759
|
+
if (d.reservation)
|
|
760
|
+
check(
|
|
761
|
+
d.reservation.selectionRevision === (s.selections.get(s.assessment.assignmentId)?.revision ?? 0),
|
|
762
|
+
"stale_selection",
|
|
763
|
+
);
|
|
567
764
|
s.assessment.view = "active";
|
|
765
|
+
s.assessment.suspensionReason = undefined;
|
|
568
766
|
s.assessment.basisUserEntryId = d.basisUserEntryId;
|
|
569
|
-
|
|
767
|
+
if (d.reservation) {
|
|
768
|
+
s.assessment.reservation = d.reservation;
|
|
769
|
+
s.reservations.set(d.handoffId, d.reservation);
|
|
770
|
+
}
|
|
570
771
|
s.assessment.problems = [];
|
|
571
|
-
s.reservations.set(d.handoffId, d.reservation);
|
|
572
772
|
break;
|
|
573
773
|
}
|
|
574
774
|
case "unit-closed": {
|
|
@@ -578,7 +778,24 @@ function applyEvent(state: State, event: RoutingEvent, owned = false): State {
|
|
|
578
778
|
s.control === "automatic" && s.profile === "coordinator" && u?.id === s.unitId && u.state === "open",
|
|
579
779
|
"unit_not_open",
|
|
580
780
|
);
|
|
581
|
-
if (d.outcome === "accepted")
|
|
781
|
+
if (d.outcome === "accepted")
|
|
782
|
+
check(a?.state !== "outstanding" && !isPending() && !s.recoveryId, "unfinished_work");
|
|
783
|
+
const recovery = s.recoveryId ? s.recoveries.get(s.recoveryId) : undefined;
|
|
784
|
+
if (recovery) {
|
|
785
|
+
check(
|
|
786
|
+
d.outcome !== "accepted" && ["requested", "reading", "returning"].includes(recovery.state),
|
|
787
|
+
"unfinished_work",
|
|
788
|
+
);
|
|
789
|
+
const transfer = pending();
|
|
790
|
+
if (transfer) {
|
|
791
|
+
check([recovery.requestHandoffId, recovery.supplementHandoffId].includes(transfer.id), "handoff_pending");
|
|
792
|
+
transfer.state = "superseded";
|
|
793
|
+
s.pendingId = undefined;
|
|
794
|
+
}
|
|
795
|
+
recovery.state = "cancelled";
|
|
796
|
+
recovery.cancellationReason = d.assessment;
|
|
797
|
+
s.recoveryId = undefined;
|
|
798
|
+
}
|
|
582
799
|
if (a?.state === "outstanding") {
|
|
583
800
|
check(d.supersededAssignmentId === a.id && d.outcome !== "accepted", "supersession_required");
|
|
584
801
|
a.state = "superseded";
|
|
@@ -602,7 +819,7 @@ function applyEvent(state: State, event: RoutingEvent, owned = false): State {
|
|
|
602
819
|
case "sources-exposed": {
|
|
603
820
|
const x = s.executions.get(d.executionId);
|
|
604
821
|
check(x && x.profile === d.view, "exposure_execution_missing");
|
|
605
|
-
if (d.view
|
|
822
|
+
if (isWorkerProfile(d.view)) for (const source of d.sources) s.exposure.set(source.ref, source.bodyHash);
|
|
606
823
|
break;
|
|
607
824
|
}
|
|
608
825
|
}
|