dsh-wsr-execution 0.2.0 → 0.2.2
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/README.md +1 -1
- package/lib/client.js +379 -105
- package/package.json +4 -4
- package/src/action-presentation/model.js +13 -7
- package/src/action-presentation/view.js +145 -18
- package/src/client/browser-entry.js +23 -4
- package/src/client/delivery/control-plane-port.js +11 -2
- package/src/client/delivery/session-delivery-view.js +148 -37
- package/src/client/delivery-inventory/model.js +17 -3
- package/src/host/delivery-control-plane.js +39 -12
- package/src/intake/binding-repository.js +118 -41
- package/src/intake/command.js +10 -0
- package/src/intake/plugin.js +131 -37
|
@@ -5,11 +5,21 @@ const LIFECYCLES = new Set([
|
|
|
5
5
|
]);
|
|
6
6
|
const WORKSPACE_KEY = "wsr.sidebar.workspace.expanded.v1";
|
|
7
7
|
const DELIVERY_KEY = "wsr.sidebar.delivery.expanded.v1";
|
|
8
|
+
const ERROR_CODES = new Set([
|
|
9
|
+
"DELIVERY_PROJECTION_CORRUPT",
|
|
10
|
+
"DELIVERY_PROJECTION_STALE_BINDING",
|
|
11
|
+
"DELIVERY_PROJECTION_RECOVERY_MISMATCH",
|
|
12
|
+
"DELIVERY_PROJECTION_UNAVAILABLE",
|
|
13
|
+
]);
|
|
8
14
|
|
|
9
15
|
function errorView(label = "Delivery inventory unavailable") {
|
|
10
16
|
return Object.freeze({ kind: "error", role: "alert", label, rows: Object.freeze([]) });
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
function diagnostic(state, label) {
|
|
20
|
+
return typeof state?.code === "string" && ERROR_CODES.has(state.code) ? `${state.code}: ${label}` : label;
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
function validString(value) {
|
|
14
24
|
return typeof value === "string" && value.length > 0 && value.length <= 512;
|
|
15
25
|
}
|
|
@@ -49,7 +59,10 @@ function rowsFrom(deliveries, selectedSessionId) {
|
|
|
49
59
|
/** Project only the formal Execution DeliveryControlPlaneSnapshot. */
|
|
50
60
|
export function projectDeliveryInventory(state, { selectedSessionId } = {}) {
|
|
51
61
|
if (state?.kind === "loading") return Object.freeze({ kind: "loading", role: "status", label: "Loading Deliveries", rows: Object.freeze([]) });
|
|
52
|
-
if (state?.kind === "error")
|
|
62
|
+
if (state?.kind === "error") {
|
|
63
|
+
const label = validString(state.message) ? state.message : "Delivery inventory unavailable";
|
|
64
|
+
return errorView(diagnostic(state, label));
|
|
65
|
+
}
|
|
53
66
|
if (!new Set(["ready", "reconnecting"]).has(state?.kind)) return errorView();
|
|
54
67
|
const snapshot = state.snapshot;
|
|
55
68
|
if (snapshot === null || typeof snapshot !== "object" || Array.isArray(snapshot)
|
|
@@ -57,7 +70,9 @@ export function projectDeliveryInventory(state, { selectedSessionId } = {}) {
|
|
|
57
70
|
|| !Number.isSafeInteger(snapshot.generation) || snapshot.generation < 1) return errorView();
|
|
58
71
|
const rows = rowsFrom(snapshot.deliveries, selectedSessionId);
|
|
59
72
|
if (rows === undefined) return errorView();
|
|
60
|
-
if (state.kind === "reconnecting") return Object.freeze({
|
|
73
|
+
if (state.kind === "reconnecting") return Object.freeze({
|
|
74
|
+
kind: "reconnecting", role: "status", label: diagnostic(state, "Reconnecting to Delivery inventory"), rows,
|
|
75
|
+
});
|
|
61
76
|
if (rows.length === 0) return Object.freeze({ kind: "empty", role: "status", label: "No Deliveries", rows });
|
|
62
77
|
return Object.freeze({ kind: "ready", role: "list", label: "Deliveries", rows });
|
|
63
78
|
}
|
|
@@ -113,4 +128,3 @@ export function createMemoryCollapseStore(storage = {}) {
|
|
|
113
128
|
setDeliveryExpanded(value) { update("deliveryExpanded", DELIVERY_KEY, value); },
|
|
114
129
|
});
|
|
115
130
|
}
|
|
116
|
-
|
|
@@ -55,7 +55,10 @@ export async function createDeliveryControlPlaneGateway(readModel) {
|
|
|
55
55
|
if (payload === null || typeof payload !== "object" || Array.isArray(payload) || Object.keys(payload).length !== 0) {
|
|
56
56
|
throw new TypeError("CONTROL_PLANE_RPC_INVALID");
|
|
57
57
|
}
|
|
58
|
-
if (failure !== undefined)
|
|
58
|
+
if (failure !== undefined) {
|
|
59
|
+
try { current = await readModel.snapshot(); failure = undefined; }
|
|
60
|
+
catch (error) { failure = errorResult(error); return failure; }
|
|
61
|
+
}
|
|
59
62
|
if (current === undefined) {
|
|
60
63
|
try { current = await readModel.snapshot(); }
|
|
61
64
|
catch (error) { return errorResult(error); }
|
|
@@ -80,33 +83,57 @@ export async function createDeliveryControlPlaneGateway(readModel) {
|
|
|
80
83
|
/** Map the owner correlation coordinate to the DSH Session id at the Host edge. */
|
|
81
84
|
export function createDshSessionControlPlaneReadModel(readModel, bindings) {
|
|
82
85
|
assertReadModel(readModel);
|
|
83
|
-
if (typeof bindings?.
|
|
86
|
+
if (typeof bindings?.listProjection !== "function" || typeof bindings?.bySession !== "function") {
|
|
84
87
|
throw new TypeError("DSH_SESSION_BINDINGS_REQUIRED");
|
|
85
88
|
}
|
|
89
|
+
const stale = () => Object.assign(new Error("DELIVERY_PROJECTION_STALE_BINDING"), { code: "DELIVERY_PROJECTION_STALE_BINDING" });
|
|
90
|
+
const exact = (delivery, binding) => delivery.deliveryId === binding.deliveryId
|
|
91
|
+
&& delivery.deliveryBindingIdentity === binding.deliveryBindingIdentity
|
|
92
|
+
&& delivery.worktree === binding.worktree
|
|
93
|
+
&& delivery.navigation?.sessionCorrelation === binding.correlation;
|
|
86
94
|
const mapSnapshot = async (snapshot) => {
|
|
87
|
-
const rows = await bindings.
|
|
88
|
-
const sessions = new Map(rows.map((binding) => [binding.correlation, binding
|
|
95
|
+
const rows = await bindings.listProjection();
|
|
96
|
+
const sessions = new Map(rows.map((binding) => [binding.correlation, binding]));
|
|
97
|
+
if (sessions.size !== rows.length) throw stale();
|
|
89
98
|
return Object.freeze({
|
|
90
99
|
...snapshot,
|
|
91
100
|
deliveries: Object.freeze(snapshot.deliveries.map((delivery) => {
|
|
92
101
|
if (delivery.navigation === null) return delivery;
|
|
93
|
-
const
|
|
94
|
-
if (
|
|
95
|
-
return Object.freeze({ ...delivery, navigation: Object.freeze({ sessionCorrelation }) });
|
|
102
|
+
const binding = sessions.get(delivery.navigation.sessionCorrelation);
|
|
103
|
+
if (binding === undefined || !exact(delivery, binding)) throw stale();
|
|
104
|
+
return Object.freeze({ ...delivery, navigation: Object.freeze({ sessionCorrelation: binding.sessionKey }) });
|
|
96
105
|
})),
|
|
97
106
|
});
|
|
98
107
|
};
|
|
99
108
|
return Object.freeze({
|
|
100
109
|
async snapshot() { return mapSnapshot(await readModel.snapshot()); },
|
|
101
110
|
async session(sessionCorrelation) {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
const [active, associations, snapshot] = await Promise.all([
|
|
112
|
+
bindings.bySession(sessionCorrelation), bindings.listProjection(), readModel.snapshot(),
|
|
113
|
+
]);
|
|
114
|
+
const scoped = associations.filter((binding) => binding.sessionKey === sessionCorrelation);
|
|
115
|
+
if (scoped.length === 0) return Object.freeze({ kind: "UNBOUND", sessionCorrelation });
|
|
116
|
+
const matched = scoped.map((binding) => {
|
|
117
|
+
const candidates = snapshot.deliveries.filter((delivery) => exact(delivery, binding));
|
|
118
|
+
if (candidates.length !== 1) throw stale();
|
|
119
|
+
return Object.freeze({ binding, delivery: candidates[0] });
|
|
120
|
+
});
|
|
121
|
+
let selected;
|
|
122
|
+
if (active !== undefined) {
|
|
123
|
+
selected = matched.find(({ binding }) => binding.state === "BOUND" && binding.deliveryId === active.deliveryId);
|
|
124
|
+
if (selected === undefined) throw stale();
|
|
125
|
+
} else {
|
|
126
|
+
const historical = matched.filter(({ binding, delivery }) => binding.state === "HISTORICAL"
|
|
127
|
+
&& delivery.lifecycle === "TERMINAL" && delivery.recoverable === false);
|
|
128
|
+
if (historical.length !== matched.length) throw stale();
|
|
129
|
+
historical.sort((left, right) => right.delivery.timing.updatedAt - left.delivery.timing.updatedAt
|
|
130
|
+
|| right.delivery.deliveryId.localeCompare(left.delivery.deliveryId));
|
|
131
|
+
selected = historical[0];
|
|
132
|
+
}
|
|
106
133
|
return Object.freeze({
|
|
107
134
|
kind: "BOUND",
|
|
108
135
|
sessionCorrelation,
|
|
109
|
-
delivery: Object.freeze({ ...
|
|
136
|
+
delivery: Object.freeze({ ...selected.delivery, navigation: Object.freeze({ sessionCorrelation }) }),
|
|
110
137
|
});
|
|
111
138
|
},
|
|
112
139
|
async subscribe(listener, onError) {
|
|
@@ -10,7 +10,7 @@ export class IntakeBindingError extends Error {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
function
|
|
13
|
+
function exactActive(value) {
|
|
14
14
|
if (value === null || typeof value !== "object" || Array.isArray(value)
|
|
15
15
|
|| Object.keys(value).sort().join(",") !== "correlation,deliveryBindingIdentity,deliveryId,sessionKey,state,worktree"
|
|
16
16
|
|| typeof value.sessionKey !== "string" || value.sessionKey.length === 0
|
|
@@ -31,21 +31,28 @@ function exact(value) {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function
|
|
34
|
+
function exactHistorical(value) {
|
|
35
35
|
if (value === null || typeof value !== "object" || Array.isArray(value)
|
|
36
|
-
|| Object.keys(value).sort().join(",") !== "correlation,deliveryId,sessionKey,
|
|
36
|
+
|| Object.keys(value).sort().join(",") !== "correlation,deliveryBindingIdentity,deliveryId,sessionKey,worktree"
|
|
37
37
|
|| typeof value.sessionKey !== "string" || value.sessionKey.length === 0
|
|
38
38
|
|| typeof value.correlation !== "string" || value.correlation.length === 0
|
|
39
39
|
|| typeof value.deliveryId !== "string" || value.deliveryId.length === 0
|
|
40
|
+
|| typeof value.deliveryBindingIdentity !== "string" || !/^sha256:[0-9a-f]{64}$/u.test(value.deliveryBindingIdentity)
|
|
40
41
|
|| typeof value.worktree !== "string" || !isAbsolute(value.worktree)
|
|
41
|
-
|
|
42
|
+
) {
|
|
42
43
|
throw new IntakeBindingError("INTAKE_BINDING_INVARIANT_VIOLATION");
|
|
43
44
|
}
|
|
44
|
-
return Object.freeze({
|
|
45
|
+
return Object.freeze({
|
|
46
|
+
sessionKey: value.sessionKey,
|
|
47
|
+
correlation: value.correlation,
|
|
48
|
+
deliveryId: value.deliveryId,
|
|
49
|
+
deliveryBindingIdentity: value.deliveryBindingIdentity,
|
|
50
|
+
worktree: resolve(value.worktree),
|
|
51
|
+
});
|
|
45
52
|
}
|
|
46
53
|
|
|
47
|
-
function
|
|
48
|
-
const accepted = bindings.map(
|
|
54
|
+
function validateActive(bindings) {
|
|
55
|
+
const accepted = bindings.map(exactActive);
|
|
49
56
|
if (new Set(accepted.map((entry) => entry.sessionKey)).size !== accepted.length
|
|
50
57
|
|| new Set(accepted.map((entry) => entry.correlation)).size !== accepted.length
|
|
51
58
|
|| new Set(accepted.map((entry) => entry.deliveryId)).size !== accepted.length
|
|
@@ -56,8 +63,28 @@ function validate(bindings) {
|
|
|
56
63
|
return accepted;
|
|
57
64
|
}
|
|
58
65
|
|
|
66
|
+
function validateHistorical(associations) {
|
|
67
|
+
const accepted = associations.map(exactHistorical);
|
|
68
|
+
if (new Set(accepted.map((entry) => entry.correlation)).size !== accepted.length
|
|
69
|
+
|| new Set(accepted.map((entry) => entry.deliveryId)).size !== accepted.length
|
|
70
|
+
|| new Set(accepted.map((entry) => entry.deliveryBindingIdentity)).size !== accepted.length) {
|
|
71
|
+
throw new IntakeBindingError("INTAKE_BINDING_INVARIANT_VIOLATION");
|
|
72
|
+
}
|
|
73
|
+
return accepted;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function validateTogether(active, historical) {
|
|
77
|
+
const all = [...active, ...historical];
|
|
78
|
+
if (new Set(all.map((entry) => entry.correlation)).size !== all.length
|
|
79
|
+
|| new Set(all.map((entry) => entry.deliveryId)).size !== all.length
|
|
80
|
+
|| new Set(all.map((entry) => entry.deliveryBindingIdentity)).size !== all.length) {
|
|
81
|
+
throw new IntakeBindingError("INTAKE_BINDING_INVARIANT_VIOLATION");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
59
85
|
export class IntakeSessionBindingRepository {
|
|
60
|
-
#
|
|
86
|
+
#active = [];
|
|
87
|
+
#historical = [];
|
|
61
88
|
#started = false;
|
|
62
89
|
constructor(file) {
|
|
63
90
|
if (typeof file !== "string" || !isAbsolute(file)) throw new IntakeBindingError("INTAKE_BINDING_INVARIANT_VIOLATION");
|
|
@@ -69,91 +96,137 @@ export class IntakeSessionBindingRepository {
|
|
|
69
96
|
try {
|
|
70
97
|
const document = JSON.parse(await readFile(this.file, "utf8"));
|
|
71
98
|
if (document === null || typeof document !== "object" || Array.isArray(document)
|
|
72
|
-
|| Object.keys(document).sort().join(",") !== "
|
|
73
|
-
||
|
|
99
|
+
|| Object.keys(document).sort().join(",") !== "activeBindings,historicalAssociations,schemaVersion"
|
|
100
|
+
|| document.schemaVersion !== "execution.intake-bindings@3.0.0"
|
|
101
|
+
|| !Array.isArray(document.activeBindings) || !Array.isArray(document.historicalAssociations)) {
|
|
74
102
|
throw new IntakeBindingError("INTAKE_BINDING_INVARIANT_VIOLATION");
|
|
75
103
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
if (!Array.isArray(inventory)) throw new IntakeBindingError("INTAKE_BINDING_INVARIANT_VIOLATION");
|
|
80
|
-
this.#bindings = validate(document.bindings.map((candidate) => {
|
|
81
|
-
const entry = legacy(candidate);
|
|
82
|
-
const matches = inventory.filter((item) => item !== null && typeof item === "object"
|
|
83
|
-
&& item.deliveryId === entry.deliveryId && typeof item.worktree === "string" && isAbsolute(item.worktree)
|
|
84
|
-
&& resolve(item.worktree) === entry.worktree
|
|
85
|
-
&& typeof item.deliveryBindingIdentity === "string" && /^sha256:[0-9a-f]{64}$/u.test(item.deliveryBindingIdentity));
|
|
86
|
-
if (matches.length !== 1) throw new IntakeBindingError("INTAKE_BINDING_INVARIANT_VIOLATION");
|
|
87
|
-
return { ...entry, deliveryBindingIdentity: matches[0].deliveryBindingIdentity };
|
|
88
|
-
}));
|
|
89
|
-
await this.#persist();
|
|
90
|
-
}
|
|
104
|
+
this.#active = validateActive(document.activeBindings);
|
|
105
|
+
this.#historical = validateHistorical(document.historicalAssociations);
|
|
106
|
+
validateTogether(this.#active, this.#historical);
|
|
91
107
|
} catch (cause) {
|
|
92
108
|
if (cause?.code !== "ENOENT") {
|
|
93
109
|
if (cause instanceof IntakeBindingError) throw cause;
|
|
94
110
|
throw new IntakeBindingError("INTAKE_BINDING_INVARIANT_VIOLATION");
|
|
95
111
|
}
|
|
96
|
-
this.#
|
|
112
|
+
this.#active = [];
|
|
113
|
+
this.#historical = [];
|
|
97
114
|
}
|
|
98
115
|
this.#started = true;
|
|
99
116
|
}
|
|
100
117
|
|
|
101
118
|
async claim(input) {
|
|
102
119
|
this.#ready();
|
|
103
|
-
const candidate =
|
|
104
|
-
const session = this.#
|
|
120
|
+
const candidate = exactActive({ ...input, state: "BOUND" });
|
|
121
|
+
const session = this.#active.find((entry) => entry.sessionKey === candidate.sessionKey);
|
|
105
122
|
if (session !== undefined) {
|
|
106
123
|
if (session.deliveryId === candidate.deliveryId && session.correlation === candidate.correlation
|
|
107
124
|
&& session.worktree === candidate.worktree && session.deliveryBindingIdentity === candidate.deliveryBindingIdentity) return session;
|
|
108
125
|
if (session.state === "DETACHED" && session.deliveryId === candidate.deliveryId
|
|
109
126
|
&& session.worktree === candidate.worktree && session.deliveryBindingIdentity === candidate.deliveryBindingIdentity) {
|
|
110
|
-
this.#
|
|
127
|
+
this.#active.splice(this.#active.indexOf(session), 1, candidate);
|
|
111
128
|
await this.#persist();
|
|
112
129
|
return candidate;
|
|
113
130
|
}
|
|
114
131
|
throw new IntakeBindingError("SESSION_INTAKE_BOUND");
|
|
115
132
|
}
|
|
116
|
-
|
|
133
|
+
if (this.#historical.some((entry) => entry.deliveryId === candidate.deliveryId
|
|
134
|
+
|| entry.correlation === candidate.correlation || entry.deliveryBindingIdentity === candidate.deliveryBindingIdentity)) {
|
|
135
|
+
throw new IntakeBindingError("INTAKE_BINDING_INVARIANT_VIOLATION");
|
|
136
|
+
}
|
|
137
|
+
const delivery = this.#active.find((entry) => entry.deliveryId === candidate.deliveryId);
|
|
117
138
|
if (delivery?.state === "BOUND") throw new IntakeBindingError("DELIVERY_INTAKE_BOUND");
|
|
118
139
|
if (delivery?.state === "DETACHED") {
|
|
119
140
|
if (delivery.worktree !== candidate.worktree || delivery.deliveryBindingIdentity !== candidate.deliveryBindingIdentity) {
|
|
120
141
|
throw new IntakeBindingError("INTAKE_BINDING_INVARIANT_VIOLATION");
|
|
121
142
|
}
|
|
122
|
-
this.#
|
|
143
|
+
this.#active.splice(this.#active.indexOf(delivery), 1, candidate);
|
|
123
144
|
}
|
|
124
|
-
else this.#
|
|
145
|
+
else this.#active.push(candidate);
|
|
146
|
+
validateTogether(this.#active, this.#historical);
|
|
125
147
|
await this.#persist();
|
|
126
148
|
return candidate;
|
|
127
149
|
}
|
|
128
150
|
|
|
129
151
|
async bySession(sessionKey) {
|
|
130
152
|
this.#ready();
|
|
131
|
-
return this.#
|
|
153
|
+
return this.#active.find((entry) => entry.sessionKey === sessionKey);
|
|
132
154
|
}
|
|
133
155
|
|
|
134
156
|
async byDelivery(deliveryId) {
|
|
135
157
|
this.#ready();
|
|
136
|
-
return this.#
|
|
158
|
+
return this.#active.find((entry) => entry.deliveryId === deliveryId);
|
|
137
159
|
}
|
|
138
160
|
|
|
139
161
|
async list() {
|
|
140
162
|
this.#ready();
|
|
141
|
-
return Object.freeze([...this.#
|
|
163
|
+
return Object.freeze([...this.#active]);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async listProjection() {
|
|
167
|
+
this.#ready();
|
|
168
|
+
return Object.freeze([
|
|
169
|
+
...this.#active.filter((entry) => entry.state === "BOUND"),
|
|
170
|
+
...this.#historical.map((entry) => Object.freeze({ ...entry, state: "HISTORICAL" })),
|
|
171
|
+
]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async archiveTerminal(sessionKey, delivery) {
|
|
175
|
+
this.#ready();
|
|
176
|
+
if (typeof sessionKey !== "string" || sessionKey.length === 0
|
|
177
|
+
|| delivery === null || typeof delivery !== "object" || Array.isArray(delivery)
|
|
178
|
+
|| delivery.lifecycle !== "TERMINAL" || delivery.recoverable !== false || delivery.current !== null
|
|
179
|
+
|| typeof delivery.navigation?.sessionCorrelation !== "string"
|
|
180
|
+
|| !Number.isSafeInteger(delivery.timing?.updatedAt)
|
|
181
|
+
|| delivery.terminal === null || typeof delivery.terminal !== "object"
|
|
182
|
+
|| !["SUCCEEDED", "FAILED", "CANCELLED"].includes(delivery.terminal.outcome)
|
|
183
|
+
|| delivery.terminal.finishedAt !== delivery.timing.updatedAt) {
|
|
184
|
+
throw new IntakeBindingError("INTAKE_BINDING_INVARIANT_VIOLATION");
|
|
185
|
+
}
|
|
186
|
+
const candidate = exactHistorical({
|
|
187
|
+
sessionKey,
|
|
188
|
+
correlation: delivery.navigation.sessionCorrelation,
|
|
189
|
+
deliveryId: delivery.deliveryId,
|
|
190
|
+
deliveryBindingIdentity: delivery.deliveryBindingIdentity,
|
|
191
|
+
worktree: delivery.worktree,
|
|
192
|
+
});
|
|
193
|
+
const sameCoordinate = (entry) => entry.deliveryId === candidate.deliveryId
|
|
194
|
+
|| entry.correlation === candidate.correlation || entry.deliveryBindingIdentity === candidate.deliveryBindingIdentity;
|
|
195
|
+
const historical = this.#historical.find(sameCoordinate);
|
|
196
|
+
if (historical !== undefined) {
|
|
197
|
+
if (historical.sessionKey === candidate.sessionKey && historical.deliveryId === candidate.deliveryId
|
|
198
|
+
&& historical.correlation === candidate.correlation && historical.worktree === candidate.worktree
|
|
199
|
+
&& historical.deliveryBindingIdentity === candidate.deliveryBindingIdentity) return historical;
|
|
200
|
+
throw new IntakeBindingError("INTAKE_BINDING_INVARIANT_VIOLATION");
|
|
201
|
+
}
|
|
202
|
+
const active = this.#active.find(sameCoordinate);
|
|
203
|
+
if (active !== undefined) {
|
|
204
|
+
if (active.sessionKey !== candidate.sessionKey || active.deliveryId !== candidate.deliveryId
|
|
205
|
+
|| active.correlation !== candidate.correlation || active.worktree !== candidate.worktree
|
|
206
|
+
|| active.deliveryBindingIdentity !== candidate.deliveryBindingIdentity) {
|
|
207
|
+
throw new IntakeBindingError("INTAKE_BINDING_INVARIANT_VIOLATION");
|
|
208
|
+
}
|
|
209
|
+
this.#active.splice(this.#active.indexOf(active), 1);
|
|
210
|
+
}
|
|
211
|
+
this.#historical.push(candidate);
|
|
212
|
+
validateTogether(this.#active, this.#historical);
|
|
213
|
+
await this.#persist();
|
|
214
|
+
return candidate;
|
|
142
215
|
}
|
|
143
216
|
|
|
144
217
|
async markDetached(deliveryId) {
|
|
145
218
|
this.#ready();
|
|
146
|
-
const index = this.#
|
|
147
|
-
if (index === -1 || this.#
|
|
148
|
-
this.#
|
|
219
|
+
const index = this.#active.findIndex((entry) => entry.deliveryId === deliveryId);
|
|
220
|
+
if (index === -1 || this.#active[index].state === "DETACHED") return;
|
|
221
|
+
this.#active.splice(index, 1, Object.freeze({ ...this.#active[index], state: "DETACHED" }));
|
|
149
222
|
await this.#persist();
|
|
150
223
|
}
|
|
151
224
|
|
|
152
225
|
async detach(deliveryId) {
|
|
153
226
|
this.#ready();
|
|
154
|
-
const index = this.#
|
|
227
|
+
const index = this.#active.findIndex((entry) => entry.deliveryId === deliveryId);
|
|
155
228
|
if (index === -1) return;
|
|
156
|
-
this.#
|
|
229
|
+
this.#active.splice(index, 1);
|
|
157
230
|
await this.#persist();
|
|
158
231
|
}
|
|
159
232
|
|
|
@@ -164,7 +237,11 @@ export class IntakeSessionBindingRepository {
|
|
|
164
237
|
async #persist() {
|
|
165
238
|
await mkdir(dirname(this.file), { recursive: true, mode: 0o700 });
|
|
166
239
|
const temporary = `${this.file}.${randomUUID()}.tmp`;
|
|
167
|
-
const document = {
|
|
240
|
+
const document = {
|
|
241
|
+
schemaVersion: "execution.intake-bindings@3.0.0",
|
|
242
|
+
activeBindings: this.#active,
|
|
243
|
+
historicalAssociations: this.#historical,
|
|
244
|
+
};
|
|
168
245
|
await writeFile(temporary, `${JSON.stringify(document)}\n`, { flag: "wx", mode: 0o600 });
|
|
169
246
|
await rename(temporary, this.file);
|
|
170
247
|
}
|
package/src/intake/command.js
CHANGED
|
@@ -42,3 +42,13 @@ export function parseWsrCommand(value) {
|
|
|
42
42
|
}
|
|
43
43
|
return invalid();
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
export function promptDiagnostic(operation, attachments = []) {
|
|
47
|
+
if (operation?.operation !== "create") return undefined;
|
|
48
|
+
const hasPrompt = typeof operation.remainder === "string" && operation.remainder.trim().length > 0;
|
|
49
|
+
if (hasPrompt || (Array.isArray(attachments) && attachments.length > 0)) return undefined;
|
|
50
|
+
return Object.freeze({
|
|
51
|
+
code: "TASK_PROMPT_REQUIRED",
|
|
52
|
+
message: "Add a Task instruction after the Workflow selector or attach a file.",
|
|
53
|
+
});
|
|
54
|
+
}
|