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.
@@ -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") return errorView(validString(state.message) ? state.message : undefined);
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({ kind: "reconnecting", role: "status", label: "Reconnecting to Delivery inventory", rows });
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) return failure;
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?.list !== "function" || typeof bindings?.bySession !== "function") {
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.list();
88
- const sessions = new Map(rows.map((binding) => [binding.correlation, binding.sessionKey]));
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 sessionCorrelation = sessions.get(delivery.navigation.sessionCorrelation);
94
- if (sessionCorrelation === undefined) throw Object.assign(new Error("DELIVERY_PROJECTION_STALE_BINDING"), { code: "DELIVERY_PROJECTION_STALE_BINDING" });
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 binding = await bindings.bySession(sessionCorrelation);
103
- if (binding === undefined) return Object.freeze({ kind: "UNBOUND", sessionCorrelation });
104
- const view = await readModel.session(binding.correlation);
105
- if (view.kind === "UNBOUND") return Object.freeze({ kind: "UNBOUND", sessionCorrelation });
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({ ...view.delivery, navigation: Object.freeze({ sessionCorrelation }) }),
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 exact(value) {
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 legacy(value) {
34
+ function exactHistorical(value) {
35
35
  if (value === null || typeof value !== "object" || Array.isArray(value)
36
- || Object.keys(value).sort().join(",") !== "correlation,deliveryId,sessionKey,state,worktree"
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
- || !["BOUND", "DETACHED"].includes(value.state)) {
42
+ ) {
42
43
  throw new IntakeBindingError("INTAKE_BINDING_INVARIANT_VIOLATION");
43
44
  }
44
- return Object.freeze({ ...value, worktree: resolve(value.worktree) });
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 validate(bindings) {
48
- const accepted = bindings.map(exact);
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
- #bindings = [];
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(",") !== "bindings,schemaVersion" || !Array.isArray(document.bindings)
73
- || !["execution.intake-bindings@1.0.0", "execution.intake-bindings@2.0.0"].includes(document.schemaVersion)) {
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
- if (document.schemaVersion === "execution.intake-bindings@2.0.0") {
77
- this.#bindings = validate(document.bindings);
78
- } else {
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.#bindings = [];
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 = exact({ ...input, state: "BOUND" });
104
- const session = this.#bindings.find((entry) => entry.sessionKey === candidate.sessionKey);
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.#bindings.splice(this.#bindings.indexOf(session), 1, candidate);
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
- const delivery = this.#bindings.find((entry) => entry.deliveryId === candidate.deliveryId);
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.#bindings.splice(this.#bindings.indexOf(delivery), 1, candidate);
143
+ this.#active.splice(this.#active.indexOf(delivery), 1, candidate);
123
144
  }
124
- else this.#bindings.push(candidate);
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.#bindings.find((entry) => entry.sessionKey === sessionKey);
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.#bindings.find((entry) => entry.deliveryId === deliveryId);
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.#bindings]);
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.#bindings.findIndex((entry) => entry.deliveryId === deliveryId);
147
- if (index === -1 || this.#bindings[index].state === "DETACHED") return;
148
- this.#bindings.splice(index, 1, Object.freeze({ ...this.#bindings[index], state: "DETACHED" }));
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.#bindings.findIndex((entry) => entry.deliveryId === deliveryId);
227
+ const index = this.#active.findIndex((entry) => entry.deliveryId === deliveryId);
155
228
  if (index === -1) return;
156
- this.#bindings.splice(index, 1);
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 = { schemaVersion: "execution.intake-bindings@2.0.0", bindings: this.#bindings };
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
  }
@@ -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
+ }