cloving 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. package/README.md +100 -15
  2. package/dist/src/broker/actor/authority-health.d.ts +9 -0
  3. package/dist/src/broker/actor/authority-health.js +24 -0
  4. package/dist/src/broker/actor/authority-runtime.d.ts +4 -2
  5. package/dist/src/broker/actor/authority-runtime.js +29 -20
  6. package/dist/src/broker/actor/authority-session.d.ts +6 -19
  7. package/dist/src/broker/actor/authority-session.js +66 -88
  8. package/dist/src/broker/actor/authority-settings.d.ts +2 -1
  9. package/dist/src/broker/actor/authority-settings.js +4 -1
  10. package/dist/src/broker/actor/authority-upgrades.d.ts +5 -1
  11. package/dist/src/broker/actor/authority-upgrades.js +2 -1
  12. package/dist/src/broker/actor/authority-watchdog.d.ts +6 -2
  13. package/dist/src/broker/actor/authority-watchdog.js +31 -9
  14. package/dist/src/broker/actor/authority.js +3 -1
  15. package/dist/src/broker/actor/projection-memory.d.ts +43 -0
  16. package/dist/src/broker/actor/projection-memory.js +207 -0
  17. package/dist/src/broker/actor/runtime-parts.d.ts +5 -0
  18. package/dist/src/broker/actor/runtime-parts.js +8 -3
  19. package/dist/src/broker/authority-restore.js +1 -1
  20. package/dist/src/broker/effect-upgrade.d.ts +1 -0
  21. package/dist/src/broker/effect-upgrade.js +4 -1
  22. package/dist/src/cli/arguments.js +1 -0
  23. package/dist/src/cli/client.d.ts +3 -1
  24. package/dist/src/cli/client.js +4 -4
  25. package/dist/src/cli/command-requests.d.ts +30 -1
  26. package/dist/src/cli/command-requests.js +43 -2
  27. package/dist/src/cli/commands.js +57 -48
  28. package/dist/src/cli/config-notice.js +5 -2
  29. package/dist/src/cli/doctor-retention.d.ts +2 -0
  30. package/dist/src/cli/doctor-retention.js +93 -0
  31. package/dist/src/cli/doctor.js +5 -0
  32. package/dist/src/cli/init-prompt.js +1 -1
  33. package/dist/src/cli/mcp-server.js +4 -4
  34. package/dist/src/cli/mcp-tools.d.ts +14 -14
  35. package/dist/src/cli/mcp-tools.js +212 -50
  36. package/dist/src/cli/queue-arguments.d.ts +15 -0
  37. package/dist/src/cli/queue-arguments.js +31 -0
  38. package/dist/src/cli/queue-render.d.ts +3 -0
  39. package/dist/src/cli/queue-render.js +26 -0
  40. package/dist/src/cli/reload-render.d.ts +2 -0
  41. package/dist/src/cli/reload-render.js +12 -0
  42. package/dist/src/cli/render.js +37 -8
  43. package/dist/src/cli/report-github-reader.js +1 -1
  44. package/dist/src/cli/runtime-arguments.d.ts +17 -3
  45. package/dist/src/cli/runtime-arguments.js +23 -19
  46. package/dist/src/cli/toolchain.d.ts +1 -0
  47. package/dist/src/cli/toolchain.js +1 -0
  48. package/dist/src/cli/usage.d.ts +2 -2
  49. package/dist/src/cli/usage.js +40 -5
  50. package/dist/src/cli/watch-api-document.d.ts +430 -0
  51. package/dist/src/cli/watch-api-document.js +291 -0
  52. package/dist/src/cli/watch-api-run.d.ts +56 -0
  53. package/dist/src/cli/watch-api-run.js +112 -0
  54. package/dist/src/cli/watch-api.d.ts +17 -0
  55. package/dist/src/cli/watch-api.js +230 -0
  56. package/dist/src/cli/watch-header-view.d.ts +1 -0
  57. package/dist/src/cli/watch-header-view.js +9 -1
  58. package/dist/src/cli/watch-model.d.ts +1 -0
  59. package/dist/src/cli/watch-model.js +3 -0
  60. package/dist/src/cli/watch-page.js +3 -1
  61. package/dist/src/cli/watch-reads.d.ts +1 -0
  62. package/dist/src/cli/watch-reads.js +6 -5
  63. package/dist/src/cli/watch-render.d.ts +1 -0
  64. package/dist/src/cli/watch-server.js +18 -4
  65. package/dist/src/cli.js +50 -43
  66. package/dist/src/configuration-display.js +2 -0
  67. package/dist/src/configuration-schema.d.ts +5 -0
  68. package/dist/src/daemon/claude-recovery.js +13 -9
  69. package/dist/src/daemon/claude-worker.d.ts +3 -3
  70. package/dist/src/daemon/claude-worker.js +10 -11
  71. package/dist/src/daemon/daemon-adapters.d.ts +7 -0
  72. package/dist/src/daemon/daemon-adapters.js +7 -3
  73. package/dist/src/daemon/daemon-loops.d.ts +2 -0
  74. package/dist/src/daemon/daemon-loops.js +17 -11
  75. package/dist/src/daemon/daemon.js +71 -60
  76. package/dist/src/daemon/github-attach.js +77 -4
  77. package/dist/src/daemon/github-calls.js +1 -1
  78. package/dist/src/daemon/github-methods.js +6 -2
  79. package/dist/src/daemon/issue-queue.d.ts +3 -0
  80. package/dist/src/daemon/issue-queue.js +66 -73
  81. package/dist/src/daemon/merge-turns.d.ts +10 -0
  82. package/dist/src/daemon/merge-turns.js +37 -0
  83. package/dist/src/daemon/queue-control.d.ts +13 -0
  84. package/dist/src/daemon/queue-control.js +86 -0
  85. package/dist/src/daemon/queue-journal.d.ts +20 -0
  86. package/dist/src/daemon/queue-journal.js +89 -0
  87. package/dist/src/daemon/record-archive.d.ts +19 -0
  88. package/dist/src/daemon/record-archive.js +82 -0
  89. package/dist/src/daemon/record-sweep.d.ts +24 -0
  90. package/dist/src/daemon/record-sweep.js +147 -0
  91. package/dist/src/daemon/retrying-turn.js +4 -4
  92. package/dist/src/daemon/settings-reload.d.ts +26 -0
  93. package/dist/src/daemon/settings-reload.js +82 -0
  94. package/dist/src/daemon/turn-details.d.ts +4 -0
  95. package/dist/src/daemon/turn-details.js +33 -0
  96. package/dist/src/daemon/worker-profile.d.ts +1 -0
  97. package/dist/src/daemon/worker-profile.js +4 -0
  98. package/dist/src/daemon/workflow-generations.d.ts +7 -4
  99. package/dist/src/daemon/workflow-generations.js +22 -14
  100. package/dist/src/daemon/workflow-reload.d.ts +41 -0
  101. package/dist/src/daemon/workflow-reload.js +244 -0
  102. package/dist/src/daemon/workflow-replay.d.ts +2 -1
  103. package/dist/src/daemon/workflow-replay.js +7 -3
  104. package/dist/src/doctor-report.d.ts +2 -2
  105. package/dist/src/doctor-report.js +2 -0
  106. package/dist/src/private-config.schema.json +31 -0
  107. package/dist/src/project-settings.d.ts +15 -1
  108. package/dist/src/project-settings.js +33 -1
  109. package/dist/src/queue-state.d.ts +26 -0
  110. package/dist/src/queue-state.js +34 -0
  111. package/dist/src/record-retention.d.ts +46 -0
  112. package/dist/src/record-retention.js +81 -0
  113. package/dist/src/service/active-tickets.js +5 -0
  114. package/dist/src/service/dispatcher.d.ts +4 -0
  115. package/dist/src/service/dispatcher.js +33 -0
  116. package/dist/src/service/queue-handlers.d.ts +4 -0
  117. package/dist/src/service/queue-handlers.js +32 -0
  118. package/dist/src/service/registry.d.ts +28 -2
  119. package/dist/src/service/registry.js +48 -0
  120. package/dist/src/service/replies.d.ts +16 -9
  121. package/dist/src/service/replies.js +5 -2
  122. package/dist/src/service/run-handlers.js +15 -10
  123. package/dist/src/service/service.d.ts +7 -3
  124. package/dist/src/service/service.js +16 -31
  125. package/dist/src/service/worker-totals.d.ts +4 -0
  126. package/dist/src/service/worker-totals.js +7 -0
  127. package/dist/src/service/workflow-reload.d.ts +35 -0
  128. package/dist/src/service/workflow-reload.js +52 -0
  129. package/dist/src/storage/decision-snapshots.d.ts +1 -2
  130. package/dist/src/storage/decision-snapshots.js +1 -7
  131. package/dist/src/storage/domain-changes.d.ts +1 -1
  132. package/dist/src/storage/domain-changes.js +1 -0
  133. package/dist/src/storage/domain-schema.d.ts +4 -1
  134. package/dist/src/storage/domain-schema.js +6 -2
  135. package/dist/src/storage/effect-schema.d.ts +1 -0
  136. package/dist/src/storage/effect-schema.js +4 -0
  137. package/dist/src/storage/projection-changes.d.ts +12 -0
  138. package/dist/src/storage/projection-changes.js +194 -0
  139. package/dist/src/storage/projection-lookups.d.ts +7 -0
  140. package/dist/src/storage/projection-lookups.js +30 -0
  141. package/dist/src/workflow-declaration.d.ts +1 -0
  142. package/dist/src/workflow-declaration.js +3 -0
  143. package/dist/tools/compile-workflows.js +3 -0
  144. package/dist/tools/project-configuration.d.ts +7 -2
  145. package/dist/tools/project-configuration.js +30 -1
  146. package/dist/tools/self-host-processes.js +37 -13
  147. package/dist/tools/self-host-project.d.ts +2 -0
  148. package/dist/tools/self-host-project.js +21 -3
  149. package/dist/tools/self-host.js +34 -24
  150. package/dist/tools/workflow-candidate-program.d.ts +4 -2
  151. package/dist/tools/workflow-candidate-program.js +30 -11
  152. package/dist/tools/workflow-candidate-source.d.ts +1 -0
  153. package/dist/tools/workflow-candidate-source.js +11 -3
  154. package/package.json +2 -2
  155. package/templates/develop/README.md +81 -4
  156. package/templates/develop/develop/flow-tests.ts +1 -1
  157. package/templates/develop/develop/interruption-tests.ts +157 -0
  158. package/templates/develop/develop/local.ts +1 -0
  159. package/templates/develop/develop/option-tests.ts +138 -9
  160. package/templates/develop/develop/red-green.ts +55 -0
  161. package/templates/develop/develop/report.ts +31 -11
  162. package/templates/develop/develop/round.ts +1 -0
  163. package/templates/develop/develop/scenarios.ts +27 -0
  164. package/templates/develop/develop/turn.ts +26 -6
  165. package/templates/develop/develop.declaration.json +1 -1
  166. package/templates/develop/develop.test.ts +4 -0
  167. package/templates/develop/template.json +29 -2
@@ -1,23 +1,22 @@
1
- import { eligibleIntents, } from "../../authority/reducer.js";
2
- import { loadOperationSnapshot, loadProjections, } from "../../storage/decision-snapshots.js";
3
1
  import { loadEvents } from "../../storage/domain-rows.js";
4
2
  import { pinnedResumption } from "../authority-admission.js";
5
- import { currentState, intentKind, storedDefinitions, storedEvents, } from "../authority-restore.js";
3
+ import { storedDefinitions, storedEvents, } from "../authority-restore.js";
6
4
  import { checkedRequest } from "./authority-actor.js";
7
5
  import { subscriptions, } from "./authority-changes.js";
8
6
  import { authorityQueries, guardedRead, } from "./authority-queries.js";
9
7
  import { effectQueries } from "./effect-queries.js";
10
8
  import { backupInto } from "../../storage/domain-schema.js";
11
9
  import { observeSettings, } from "./authority-connection.js";
10
+ import { authorityHealth } from "./authority-health.js";
12
11
  import { failed, serializable, storeReceipt, invalidCommand, oversized, tooLarge, unserializable, } from "./authority-receipts.js";
12
+ export { operationRecords } from "./projection-memory.js";
13
13
  export function authoritySession(parts) {
14
14
  const session = {
15
15
  parts,
16
16
  stopping: new AbortController(),
17
17
  delivering: false,
18
18
  failure: null,
19
- condition: null,
20
- describing: null,
19
+ health: authorityHealth(parts),
21
20
  closed: false,
22
21
  running: Promise.resolve(),
23
22
  invocations: new Set(),
@@ -33,18 +32,18 @@ export function authoritySession(parts) {
33
32
  });
34
33
  return {
35
34
  execute: (command, context) => executeCommand(session, { command, context }),
36
- state: async () => currentState(await projected(session)),
35
+ state: async () => (await projected(session)).state(),
37
36
  events: async () => {
38
37
  await refreshed(session);
39
38
  available(session);
40
39
  return storedEvents(await parts.reader.connection(loadEvents));
41
40
  },
42
- eligibleIntents: async () => eligible(await projected(session)),
43
- operations: async () => operationRecords(await read(loadOperationSnapshot)),
41
+ eligibleIntents: async () => (await projected(session)).eligible(),
42
+ operations: async () => (await projected(session)).operations(),
44
43
  resumePinned: async (runId, manifest) => {
45
- const snapshot = await projected(session);
46
- return pinnedResumption(currentState(snapshot), {
47
- definitions: storedDefinitions(snapshot),
44
+ const current = await projected(session);
45
+ return pinnedResumption(current.state(), {
46
+ definitions: storedDefinitions(current.snapshot),
48
47
  runId,
49
48
  available: manifest,
50
49
  });
@@ -56,7 +55,11 @@ export function authoritySession(parts) {
56
55
  },
57
56
  epoch: parts.epoch,
58
57
  recoveryLimit: parts.recoveryLimit,
59
- queries: authorityQueries(read),
58
+ queries: {
59
+ ...authorityQueries(read),
60
+ sequences: async () => (await projected(session)).sequences(),
61
+ workflowIds: async (runIds) => (await projected(session)).workflowIds(runIds),
62
+ },
60
63
  effects: effectQueries(read, { stopLimit: parts.stopLimit }),
61
64
  deliver: (signal) => deliver(session, signal),
62
65
  changes: { subscribe: session.changes.add },
@@ -82,29 +85,16 @@ async function stopped(session) {
82
85
  }
83
86
  }
84
87
  async function refreshed(session) {
85
- if (session.parts.unhealthy() === undefined)
88
+ if (session.failure !== null || session.closed)
86
89
  return;
87
- await failClosed(session, "authority failure");
90
+ await session.health.refresh();
88
91
  }
89
- async function failClosed(session, failure) {
92
+ function failClosed(session, failure) {
90
93
  session.failure ??= failure;
91
- await conditionRead(session);
92
- close(session).catch(() => undefined);
93
- }
94
- async function conditionRead(session) {
95
- if (session.condition !== null && session.describing === null)
96
- return;
97
- session.describing ??= described(session).finally(() => {
98
- session.describing = null;
99
- });
100
- await session.describing;
94
+ close(session).catch(ignored);
101
95
  }
102
- async function described(session) {
103
- const detected = session.parts.unhealthy();
104
- if (detected === undefined)
105
- return;
106
- session.condition = detected;
107
- session.condition = (await session.parts.condition()) ?? detected;
96
+ function ignored() {
97
+ return undefined;
108
98
  }
109
99
  async function backup(session, target) {
110
100
  await refreshed(session);
@@ -116,36 +106,43 @@ async function backup(session, target) {
116
106
  throw new Error(`backup-failed: ${String(Reflect.get(Object(error), "message"))}`, { cause: error });
117
107
  }
118
108
  }
119
- function available(session) {
120
- const failure = session.condition ?? session.failure;
121
- if (failure !== null)
122
- throw new Error(`authority-unavailable: ${failure}`);
109
+ function opened(session) {
110
+ if (session.failure !== null)
111
+ throw new Error(`authority-unavailable: ${session.failure}`);
123
112
  if (session.closed)
124
113
  throw new Error("authority-unavailable: closed");
125
114
  }
115
+ function available(session) {
116
+ opened(session);
117
+ const condition = session.health.condition();
118
+ if (condition !== null)
119
+ throw new Error(`authority-unavailable: ${condition}`);
120
+ }
126
121
  function unavailableReceipt(session) {
127
- const failure = session.condition ?? session.failure;
128
- if (failure !== null)
122
+ if (session.failure !== null)
129
123
  return {
130
124
  kind: "failed",
131
125
  reason: "storage-unavailable",
132
- detail: `closed after ${failure}`,
126
+ detail: `closed after ${session.failure}`,
133
127
  };
134
128
  if (session.closed)
135
129
  return { kind: "failed", reason: "storage-unavailable", detail: "closed" };
136
- return undefined;
130
+ const condition = session.health.condition();
131
+ return condition === null
132
+ ? undefined
133
+ : { kind: "failed", reason: "authority-unavailable", detail: condition };
137
134
  }
138
135
  async function projected(session) {
139
136
  await refreshed(session);
140
137
  available(session);
141
- return session.parts.reader.connection(loadProjections);
138
+ return session.parts.memory.read(session.parts.database);
142
139
  }
143
140
  async function deliver(session, signal) {
144
141
  if (!session.parts.deliveryConfigured)
145
142
  throw new Error("delivery-unconfigured");
146
143
  if (session.delivering)
147
144
  throw new Error("delivery-already-running");
148
- available(session);
145
+ opened(session);
149
146
  session.delivering = true;
150
147
  try {
151
148
  await delivered(session, signal);
@@ -154,21 +151,19 @@ async function deliver(session, signal) {
154
151
  session.delivering = false;
155
152
  }
156
153
  await refreshed(session);
157
- const failure = session.condition ?? session.failure;
154
+ const failure = session.failure ?? session.health.condition();
158
155
  if (failure !== null)
159
156
  throw new Error(`authority-unavailable: ${failure}`);
160
157
  }
161
158
  async function delivered(session, signal) {
162
159
  if (!(await session.parts.handlersSettled()))
163
160
  throw new Error("delivery-handler-in-flight");
164
- available(session);
165
- const watch = new AbortController();
161
+ opened(session);
166
162
  const timer = setInterval(() => {
167
- if (session.parts.unhealthy() !== undefined)
168
- watch.abort();
163
+ refreshed(session).catch(ignored);
169
164
  }, session.parts.watchMilliseconds);
170
165
  try {
171
- session.running = session.parts.run(AbortSignal.any([signal, session.stopping.signal, watch.signal]));
166
+ session.running = session.parts.run(AbortSignal.any([signal, session.stopping.signal]));
172
167
  await session.running;
173
168
  }
174
169
  finally {
@@ -177,8 +172,7 @@ async function delivered(session, signal) {
177
172
  }
178
173
  async function executeCommand(session, input) {
179
174
  const { command, context } = input;
180
- await refreshed(session);
181
- const unavailable = unavailableReceipt(session);
175
+ const unavailable = await refusal(session);
182
176
  if (unavailable !== undefined)
183
177
  return unavailable;
184
178
  const given = serializable({
@@ -196,15 +190,32 @@ async function executeCommand(session, input) {
196
190
  if (oversized(request))
197
191
  return tooLarge;
198
192
  try {
199
- return await healthyReceipt(session, await invoked(session, request));
193
+ return storeReceipt(await invoked(session, request));
200
194
  }
201
195
  catch (error) {
202
- const receipt = failed(error);
203
- await failClosed(session, receipt.reason === "storage-failure"
204
- ? "storage failure"
205
- : "authority failure");
206
- return receipt;
196
+ return failedReceipt(session, error);
197
+ }
198
+ }
199
+ async function refusal(session) {
200
+ try {
201
+ await refreshed(session);
207
202
  }
203
+ catch (error) {
204
+ return failed(error);
205
+ }
206
+ return unavailableReceipt(session);
207
+ }
208
+ function failedReceipt(session, error) {
209
+ const receipt = failed(error);
210
+ if (timedOut(error))
211
+ return receipt;
212
+ failClosed(session, receipt.reason === "storage-failure"
213
+ ? "storage failure"
214
+ : "authority failure");
215
+ return receipt;
216
+ }
217
+ function timedOut(error) {
218
+ return error instanceof Error && error.name === "SyncTimeout";
208
219
  }
209
220
  async function invoked(session, request) {
210
221
  const invocation = session.parts.execute(request);
@@ -216,36 +227,3 @@ async function invoked(session, request) {
216
227
  session.invocations.delete(invocation);
217
228
  }
218
229
  }
219
- async function healthyReceipt(session, value) {
220
- const receipt = storeReceipt(value);
221
- await refreshed(session);
222
- return receipt;
223
- }
224
- export function operationRecords(snapshot) {
225
- const deliveries = Map.groupBy(snapshot.deliveries, (row) => row.operationId);
226
- return snapshot.outbox.map((row) => ({
227
- operationId: row.operationId,
228
- kind: intentKind(row.kind),
229
- runId: row.runId,
230
- createdSequence: row.createdSequence,
231
- releasedSequence: row.releasedSequence,
232
- deliveries: (deliveries.get(row.operationId) ?? []).map(({ effectId, outcome, receipt, purpose, epoch }) => ({
233
- effectId,
234
- outcome,
235
- receipt,
236
- purpose,
237
- epoch,
238
- })),
239
- }));
240
- }
241
- function eligible(snapshot) {
242
- return eligibleIntents({
243
- ...currentState(snapshot),
244
- effects: {
245
- operations: Object.fromEntries(snapshot.pending.map((operationId) => [
246
- operationId,
247
- { status: "dispatch-pending" },
248
- ])),
249
- },
250
- });
251
- }
@@ -1,4 +1,4 @@
1
- import type { SolidObjectsConfiguration } from "solid-objects";
1
+ import type { Logger, SolidObjectsConfiguration } from "solid-objects";
2
2
  export interface AuthorityTiming {
3
3
  readonly leaseMilliseconds: number;
4
4
  readonly renewalMilliseconds: number;
@@ -30,4 +30,5 @@ export declare function effectConfiguration(options: {
30
30
  readonly stop?: unknown;
31
31
  };
32
32
  }): PlanSettings;
33
+ export declare function authorityLogger(log: ((line: string) => void) | undefined): Logger;
33
34
  export declare function librarySettings(timing: AuthorityTiming, log: ((line: string) => void) | undefined): Partial<SolidObjectsConfiguration>;
@@ -47,6 +47,9 @@ export function effectConfiguration(options) {
47
47
  function standardError(line) {
48
48
  process.stderr.write(line);
49
49
  }
50
+ export function authorityLogger(log) {
51
+ return lineLogger(log ?? standardError);
52
+ }
50
53
  export function librarySettings(timing, log) {
51
54
  return {
52
55
  tableNamePrefix: "solid_",
@@ -54,7 +57,7 @@ export function librarySettings(timing, log) {
54
57
  maxActivationDurationMilliseconds: 50,
55
58
  maxPayloadBytes: 900_000 + commandLimits.stdoutBytes * 12,
56
59
  maxResultBytes: 900_000 + commandLimits.stdoutBytes * 12,
57
- logger: lineLogger(log ?? standardError),
60
+ logger: authorityLogger(log),
58
61
  leaseDurationMilliseconds: timing.leaseMilliseconds,
59
62
  leaseRenewalIntervalMilliseconds: timing.renewalMilliseconds,
60
63
  processHeartbeatIntervalMilliseconds: timing.heartbeatMilliseconds,
@@ -2,7 +2,7 @@ import { upgradeCommandWaits, upgradeCommandWaitChecks } from "../command-wait-r
2
2
  import { loadDecided, loadEffected, loadWaiting, loadCurrent } from "../../storage/decision-snapshots.ts";
3
3
  import { loadSnapshot } from "../../storage/domain-rows.ts";
4
4
  import { upgradeDecisions } from "../decision-upgrade.ts";
5
- import { upgradeEffects, upgradeEventIndexes, upgradeSpendingIndexes } from "../effect-upgrade.ts";
5
+ import { upgradeEffects, upgradeEventIndexes, upgradeRunIndexes, upgradeSpendingIndexes } from "../effect-upgrade.ts";
6
6
  declare const upgrades: readonly [{
7
7
  readonly from: "adopted";
8
8
  readonly load: typeof loadSnapshot;
@@ -27,6 +27,10 @@ declare const upgrades: readonly [{
27
27
  readonly from: "indexed";
28
28
  readonly load: typeof loadCurrent;
29
29
  readonly apply: typeof upgradeEventIndexes;
30
+ }, {
31
+ readonly from: "event-indexed";
32
+ readonly load: typeof loadCurrent;
33
+ readonly apply: typeof upgradeRunIndexes;
30
34
  }];
31
35
  export type UpgradeStep = (typeof upgrades)[number];
32
36
  export declare function upgradeSteps(kind: string): readonly UpgradeStep[];
@@ -2,7 +2,7 @@ import { upgradeCommandWaits, upgradeCommandWaitChecks, } from "../command-wait-
2
2
  import { loadDecided, loadEffected, loadWaiting, loadCurrent, } from "../../storage/decision-snapshots.js";
3
3
  import { loadSnapshot } from "../../storage/domain-rows.js";
4
4
  import { upgradeDecisions } from "../decision-upgrade.js";
5
- import { upgradeEffects, upgradeEventIndexes, upgradeSpendingIndexes, } from "../effect-upgrade.js";
5
+ import { upgradeEffects, upgradeEventIndexes, upgradeRunIndexes, upgradeSpendingIndexes, } from "../effect-upgrade.js";
6
6
  const upgrades = [
7
7
  { from: "adopted", load: loadSnapshot, apply: upgradeDecisions },
8
8
  { from: "decided", load: loadDecided, apply: upgradeEffects },
@@ -10,6 +10,7 @@ const upgrades = [
10
10
  { from: "waiting", load: loadWaiting, apply: upgradeCommandWaitChecks },
11
11
  { from: "checked", load: loadCurrent, apply: upgradeSpendingIndexes },
12
12
  { from: "indexed", load: loadCurrent, apply: upgradeEventIndexes },
13
+ { from: "event-indexed", load: loadCurrent, apply: upgradeRunIndexes },
13
14
  ];
14
15
  export function upgradeSteps(kind) {
15
16
  const first = upgrades.findIndex((step) => step.from === kind);
@@ -3,10 +3,14 @@ export interface WatchedEvent {
3
3
  readonly messageId: string | null;
4
4
  readonly attributes: Readonly<Record<string, unknown>>;
5
5
  }
6
+ export interface WatchReport {
7
+ readonly kind: "stranded" | "dead-lettered";
8
+ readonly messageId: string;
9
+ }
10
+ export type MessageRuns = (messageIds: readonly string[]) => Promise<ReadonlyMap<string, string>>;
6
11
  export interface StrandingWatchdog {
7
12
  readonly observe: (event: WatchedEvent) => void;
8
- readonly stranded: () => string | undefined;
9
- readonly deadLettered: () => string | undefined;
13
+ readonly report: (runs: MessageRuns) => Promise<WatchReport | undefined>;
10
14
  }
11
15
  export declare function strandingWatchdog(bound: {
12
16
  readonly boundMilliseconds: number;
@@ -1,3 +1,4 @@
1
+ import { terminal } from "../../authority/transitions.js";
1
2
  const callbacks = ["deliveryCompleted", "deliveryFailed"];
2
3
  const finished = [
3
4
  "solid_objects.message.completed",
@@ -7,33 +8,54 @@ const finished = [
7
8
  export function strandingWatchdog(bound) {
8
9
  const clock = bound.clock ?? monotonic;
9
10
  const lost = new Map();
10
- const dead = [];
11
- let reported;
11
+ const dead = new Set();
12
12
  return {
13
13
  observe: (event) => {
14
14
  const { messageId } = event;
15
15
  if (messageId === null)
16
16
  return;
17
17
  if (deadCallback(event))
18
- dead.push(messageId);
18
+ dead.add(messageId);
19
19
  if (event.name === "solid_objects.activation.lost" &&
20
20
  !lost.has(messageId))
21
21
  lost.set(messageId, clock());
22
22
  if (finished.includes(event.name))
23
23
  lost.delete(messageId);
24
24
  },
25
- stranded: () => {
26
- reported ??= overdue(lost, clock() - bound.boundMilliseconds);
27
- return reported;
25
+ report: async (runs) => {
26
+ const overdue = overdueMessages(lost, clock() - bound.boundMilliseconds);
27
+ const suspects = [...dead, ...overdue];
28
+ if (suspects.length === 0)
29
+ return undefined;
30
+ const statuses = await runs(suspects);
31
+ const released = suspects.filter((messageId) => settled(statuses.get(messageId)));
32
+ for (const messageId of released) {
33
+ lost.delete(messageId);
34
+ dead.delete(messageId);
35
+ }
36
+ return firstReport({ dead: [...dead], stranded: overdue, lost });
28
37
  },
29
- deadLettered: () => dead[0],
30
38
  };
31
39
  }
32
40
  function monotonic() {
33
41
  return performance.now();
34
42
  }
35
- function overdue(lost, limit) {
36
- return [...lost].find(([, at]) => at < limit)?.[0];
43
+ function overdueMessages(lost, limit) {
44
+ return [...lost]
45
+ .filter(([, at]) => at < limit)
46
+ .map(([messageId]) => messageId);
47
+ }
48
+ function settled(status) {
49
+ return terminal.some((candidate) => candidate === status);
50
+ }
51
+ function firstReport(found) {
52
+ const deadLetter = found.dead[0];
53
+ if (deadLetter !== undefined)
54
+ return { kind: "dead-lettered", messageId: deadLetter };
55
+ const stranded = found.stranded.find((messageId) => found.lost.has(messageId));
56
+ return stranded === undefined
57
+ ? undefined
58
+ : { kind: "stranded", messageId: stranded };
37
59
  }
38
60
  function deadCallback(event) {
39
61
  return (event.name === "solid_objects.dead_letter.created" &&
@@ -76,6 +76,7 @@ async function begun(opening) {
76
76
  reason: "unresolved-delivery",
77
77
  detail: condition,
78
78
  };
79
+ await parts.memory.load(owned.database);
79
80
  return integer(opened, "epoch");
80
81
  }
81
82
  async function start(opening) {
@@ -99,6 +100,7 @@ async function start(opening) {
99
100
  authority: authoritySession({
100
101
  database,
101
102
  reader: parts.reader,
103
+ memory: parts.memory,
102
104
  execute: (request) => authority.execute(json(request)),
103
105
  run: (signal) => parts.runtime.run(signal),
104
106
  stop: parts.stop,
@@ -106,8 +108,8 @@ async function start(opening) {
106
108
  deliveryConfigured: options.delivery !== undefined,
107
109
  epoch,
108
110
  ...parts.limits,
109
- unhealthy: parts.unhealthy,
110
111
  condition: parts.condition,
112
+ logger: parts.logger,
111
113
  watchMilliseconds: parts.watchMilliseconds,
112
114
  subscribe: (listener) => parts.subscribe(actorId, listener),
113
115
  }),
@@ -0,0 +1,43 @@
1
+ import type { Database, Logger } from "solid-objects";
2
+ import { type EffectIntent } from "../../authority/reducer.ts";
3
+ import { type ProjectionSnapshot } from "../../storage/decision-snapshots.ts";
4
+ import type { DomainChange } from "../../storage/domain-changes.ts";
5
+ import { type StoredState } from "../authority-restore.ts";
6
+ import type { Sequences } from "./authority-queries.ts";
7
+ export interface OperationRecord {
8
+ readonly operationId: string;
9
+ readonly kind: EffectIntent["kind"];
10
+ readonly runId: string;
11
+ readonly createdSequence: number;
12
+ readonly releasedSequence: number | null;
13
+ readonly deliveries: readonly {
14
+ readonly effectId: string;
15
+ readonly outcome: string | null;
16
+ readonly receipt: string | null;
17
+ readonly purpose: string;
18
+ readonly epoch: number | null;
19
+ }[];
20
+ }
21
+ export interface Projection {
22
+ readonly snapshot: ProjectionSnapshot;
23
+ readonly state: () => StoredState;
24
+ readonly eligible: () => readonly EffectIntent[];
25
+ readonly operations: () => readonly OperationRecord[];
26
+ readonly sequences: () => Sequences;
27
+ readonly workflowIds: (runIds?: readonly string[]) => Readonly<Record<string, string>>;
28
+ }
29
+ type Source = Pick<Database, "connection">;
30
+ export interface ProjectionMemory {
31
+ readonly record: (change: RecordedChange) => void;
32
+ readonly load: (database: Source) => Promise<void>;
33
+ readonly read: (database: Source) => Promise<Projection>;
34
+ }
35
+ export interface RecordedChange {
36
+ readonly messageId: string;
37
+ readonly change: DomainChange;
38
+ }
39
+ export declare function projectionMemory(options: {
40
+ readonly logger: Logger;
41
+ }): ProjectionMemory;
42
+ export declare function operationRecords(snapshot: Pick<ProjectionSnapshot, "outbox" | "deliveries">): readonly OperationRecord[];
43
+ export {};