@osolmaz/pi-workflows 0.12.1 → 0.13.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 (209) hide show
  1. package/README.md +52 -28
  2. package/dist/builtins/autodoc.workflow.d.ts +4 -4
  3. package/dist/builtins/autoimplement.workflow.d.ts +369 -73
  4. package/dist/builtins/autoplan.workflow.d.ts +126 -13
  5. package/dist/builtins/autoplan.workflow.js +200 -29
  6. package/dist/builtins/autoplan.workflow.js.map +1 -1
  7. package/dist/builtins/catalog.js +6 -4
  8. package/dist/builtins/catalog.js.map +1 -1
  9. package/dist/builtins/index.d.ts +2 -1
  10. package/dist/builtins/index.js +1 -0
  11. package/dist/builtins/index.js.map +1 -1
  12. package/dist/builtins/monitor.workflow.d.ts +56 -21
  13. package/dist/builtins/monitor.workflow.js +497 -216
  14. package/dist/builtins/monitor.workflow.js.map +1 -1
  15. package/dist/builtins/plain-summary.workflow.d.ts +32 -0
  16. package/dist/builtins/plain-summary.workflow.js +134 -0
  17. package/dist/builtins/plain-summary.workflow.js.map +1 -0
  18. package/dist/builtins/plan-change.workflow.d.ts +170 -22
  19. package/dist/builtins/sanity-check.workflow.d.ts +45 -3
  20. package/dist/builtins/sanity-check.workflow.js +45 -7
  21. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  22. package/dist/controllers/effects.d.ts +3 -2
  23. package/dist/controllers/effects.js +8 -1
  24. package/dist/controllers/effects.js.map +1 -1
  25. package/dist/controllers/index.d.ts +1 -1
  26. package/dist/controllers/index.js +1 -1
  27. package/dist/controllers/index.js.map +1 -1
  28. package/dist/controllers/manager.d.ts +1 -0
  29. package/dist/controllers/manager.js +41 -17
  30. package/dist/controllers/manager.js.map +1 -1
  31. package/dist/controllers/sqlite.d.ts +107 -85
  32. package/dist/controllers/sqlite.js +1611 -1486
  33. package/dist/controllers/sqlite.js.map +1 -1
  34. package/dist/controllers/store.d.ts +9 -8
  35. package/dist/controllers/store.js +1 -31
  36. package/dist/controllers/store.js.map +1 -1
  37. package/dist/controllers/types.d.ts +3 -0
  38. package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
  39. package/dist/controllers/workflow-engine-scheduler.js +3 -5
  40. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  41. package/dist/controllers/workflows.d.ts +2 -2
  42. package/dist/controllers/workflows.js +11 -10
  43. package/dist/controllers/workflows.js.map +1 -1
  44. package/dist/extension/controller-host.js +2 -2
  45. package/dist/extension/controller-host.js.map +1 -1
  46. package/dist/extension/decision-channels.d.ts +2 -0
  47. package/dist/extension/decision-channels.js +311 -118
  48. package/dist/extension/decision-channels.js.map +1 -1
  49. package/dist/extension/executor.d.ts +9 -2
  50. package/dist/extension/executor.js +89 -2
  51. package/dist/extension/executor.js.map +1 -1
  52. package/dist/extension/herdr-viewer.d.ts +0 -1
  53. package/dist/extension/herdr-viewer.js +0 -4
  54. package/dist/extension/herdr-viewer.js.map +1 -1
  55. package/dist/extension/index.js +156 -103
  56. package/dist/extension/index.js.map +1 -1
  57. package/dist/extension/recorder.d.ts +3 -10
  58. package/dist/extension/recorder.js +13 -22
  59. package/dist/extension/recorder.js.map +1 -1
  60. package/dist/extension/step-message.d.ts +7 -2
  61. package/dist/extension/step-message.js +94 -2
  62. package/dist/extension/step-message.js.map +1 -1
  63. package/dist/extension/widget.js +6 -0
  64. package/dist/extension/widget.js.map +1 -1
  65. package/dist/host/rpc-executor.d.ts +3 -0
  66. package/dist/host/rpc-executor.js +2 -0
  67. package/dist/host/rpc-executor.js.map +1 -1
  68. package/dist/host/runner.d.ts +3 -6
  69. package/dist/host/runner.js +83 -32
  70. package/dist/host/runner.js.map +1 -1
  71. package/dist/render/graph-render.js +14 -8
  72. package/dist/render/graph-render.js.map +1 -1
  73. package/dist/state/database.d.ts +35 -0
  74. package/dist/state/database.js +287 -0
  75. package/dist/state/database.js.map +1 -0
  76. package/dist/state/index.d.ts +4 -0
  77. package/dist/state/index.js +5 -0
  78. package/dist/state/index.js.map +1 -0
  79. package/dist/state/json.d.ts +6 -0
  80. package/dist/state/json.js +38 -0
  81. package/dist/state/json.js.map +1 -0
  82. package/dist/state/mutation.d.ts +57 -0
  83. package/dist/state/mutation.js +255 -0
  84. package/dist/state/mutation.js.map +1 -0
  85. package/dist/state/schema.d.ts +6 -0
  86. package/dist/state/schema.js +473 -0
  87. package/dist/state/schema.js.map +1 -0
  88. package/dist/viewer/cli.d.ts +2 -2
  89. package/dist/viewer/cli.js +142 -122
  90. package/dist/viewer/cli.js.map +1 -1
  91. package/dist/viewer/render.d.ts +4 -4
  92. package/dist/viewer/render.js +2 -17
  93. package/dist/viewer/render.js.map +1 -1
  94. package/dist/viewer/session-reducer.d.ts +1 -1
  95. package/dist/viewer/session-reducer.js +1 -1
  96. package/dist/viewer/tui.d.ts +3 -3
  97. package/dist/viewer/tui.js +15 -13
  98. package/dist/viewer/tui.js.map +1 -1
  99. package/dist/viewer/watch.d.ts +2 -5
  100. package/dist/viewer/watch.js +13 -13
  101. package/dist/viewer/watch.js.map +1 -1
  102. package/dist/workflows/composition.js +12 -2
  103. package/dist/workflows/composition.js.map +1 -1
  104. package/dist/workflows/definition.d.ts +6 -2
  105. package/dist/workflows/definition.js +17 -0
  106. package/dist/workflows/definition.js.map +1 -1
  107. package/dist/workflows/engine.d.ts +6 -6
  108. package/dist/workflows/engine.js +183 -103
  109. package/dist/workflows/engine.js.map +1 -1
  110. package/dist/workflows/errors.d.ts +3 -3
  111. package/dist/workflows/errors.js +3 -3
  112. package/dist/workflows/human-decision.d.ts +29 -4
  113. package/dist/workflows/human-decision.js +547 -283
  114. package/dist/workflows/human-decision.js.map +1 -1
  115. package/dist/workflows/index.d.ts +4 -5
  116. package/dist/workflows/index.js +3 -4
  117. package/dist/workflows/index.js.map +1 -1
  118. package/dist/workflows/schema.js +17 -1
  119. package/dist/workflows/schema.js.map +1 -1
  120. package/dist/workflows/store.d.ts +83 -103
  121. package/dist/workflows/store.js +902 -951
  122. package/dist/workflows/store.js.map +1 -1
  123. package/dist/workflows/types.d.ts +55 -81
  124. package/docs/CONTROLLERS.md +10 -8
  125. package/docs/DEFERRED_TURNS.md +2 -2
  126. package/docs/DESIGN_PHILOSOPHY.md +4 -2
  127. package/docs/HUMAN_DECISIONS.md +13 -14
  128. package/docs/MONITOR.md +179 -231
  129. package/docs/SQLITE_STATE.md +219 -0
  130. package/docs/WORKFLOW_COMPOSITION.md +19 -19
  131. package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
  132. package/docs/WORKFLOW_UPDATES.md +10 -9
  133. package/docs/development.md +9 -9
  134. package/docs/live-replay-protocol.md +28 -37
  135. package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
  136. package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
  137. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
  138. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +1 -1
  139. package/docs/plans/2026-08-21-sanity-check-plan.md +30 -16
  140. package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
  141. package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
  142. package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
  143. package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
  144. package/docs/plans/session-event-replay-implementation-plan.md +1 -1
  145. package/docs/plans/tui-viewer-implementation-plan.md +1 -1
  146. package/docs/session-event-journal.md +63 -434
  147. package/docs/tui-viewer.md +13 -15
  148. package/docs/workflows.md +104 -50
  149. package/examples/workflows/plain-summary.workflow.ts +1 -0
  150. package/herdr-plugin.toml +1 -1
  151. package/package.json +1 -1
  152. package/plugins/herdr/viewer.mjs +1 -11
  153. package/skills/autoplan/SKILL.md +5 -4
  154. package/skills/monitor/SKILL.md +91 -163
  155. package/skills/pi-workflows/SKILL.md +7 -7
  156. package/skills/sanity-check/SKILL.md +1 -1
  157. package/src/builtins/autoplan.workflow.ts +290 -40
  158. package/src/builtins/catalog.ts +6 -4
  159. package/src/builtins/index.ts +7 -1
  160. package/src/builtins/monitor.workflow.ts +663 -249
  161. package/src/builtins/plain-summary.workflow.ts +185 -0
  162. package/src/builtins/sanity-check.workflow.ts +62 -7
  163. package/src/controllers/effects.ts +7 -0
  164. package/src/controllers/index.ts +0 -6
  165. package/src/controllers/manager.ts +72 -32
  166. package/src/controllers/sqlite.ts +2420 -1948
  167. package/src/controllers/store.ts +17 -38
  168. package/src/controllers/types.ts +3 -0
  169. package/src/controllers/workflow-engine-scheduler.ts +4 -8
  170. package/src/controllers/workflows.ts +45 -21
  171. package/src/extension/controller-host.ts +1 -2
  172. package/src/extension/decision-channels.ts +427 -144
  173. package/src/extension/executor.ts +105 -2
  174. package/src/extension/herdr-viewer.ts +0 -5
  175. package/src/extension/index.ts +180 -118
  176. package/src/extension/recorder.ts +13 -22
  177. package/src/extension/step-message.ts +106 -4
  178. package/src/extension/widget.ts +8 -0
  179. package/src/host/rpc-executor.ts +4 -0
  180. package/src/host/runner.ts +93 -41
  181. package/src/render/graph-render.ts +11 -4
  182. package/src/state/database.ts +375 -0
  183. package/src/state/index.ts +29 -0
  184. package/src/state/json.ts +42 -0
  185. package/src/state/mutation.ts +435 -0
  186. package/src/state/schema.ts +475 -0
  187. package/src/viewer/cli.ts +151 -144
  188. package/src/viewer/render.ts +12 -24
  189. package/src/viewer/session-reducer.ts +1 -1
  190. package/src/viewer/tui.ts +18 -17
  191. package/src/viewer/watch.ts +13 -17
  192. package/src/workflows/composition.ts +12 -3
  193. package/src/workflows/definition.ts +36 -3
  194. package/src/workflows/engine.ts +212 -116
  195. package/src/workflows/errors.ts +3 -3
  196. package/src/workflows/human-decision.ts +795 -340
  197. package/src/workflows/index.ts +15 -19
  198. package/src/workflows/schema.ts +19 -1
  199. package/src/workflows/store.ts +1295 -1163
  200. package/src/workflows/types.ts +61 -81
  201. package/dist/workflows/artifacts.d.ts +0 -40
  202. package/dist/workflows/artifacts.js +0 -155
  203. package/dist/workflows/artifacts.js.map +0 -1
  204. package/dist/workflows/migrate-sources.d.ts +0 -42
  205. package/dist/workflows/migrate-sources.js +0 -133
  206. package/dist/workflows/migrate-sources.js.map +0 -1
  207. package/docs/run-bundles.md +0 -481
  208. package/src/workflows/artifacts.ts +0 -188
  209. package/src/workflows/migrate-sources.ts +0 -178
@@ -1,4 +1,4 @@
1
- import { ControllerManager, SqliteControllerStore, projectControllerStorePath, loadDiscoveredControllers, } from "../controllers/index.js";
1
+ import { ControllerManager, SqliteControllerStore, loadDiscoveredControllers, } from "../controllers/index.js";
2
2
  export function parseControllerArgs(args) {
3
3
  const trimmed = args.trim();
4
4
  if (trimmed.length === 0 || trimmed === "list") {
@@ -48,7 +48,7 @@ export class PiControllerHost {
48
48
  if (definitions.length === 0) {
49
49
  return undefined;
50
50
  }
51
- const store = new SqliteControllerStore(projectControllerStorePath(options.cwd));
51
+ const store = new SqliteControllerStore(undefined, { projectPath: options.cwd });
52
52
  const manager = new ControllerManager({
53
53
  store,
54
54
  controllers: definitions,
@@ -1 +1 @@
1
- {"version":3,"file":"controller-host.js","sourceRoot":"","sources":["../../src/extension/controller-host.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"controller-host.js","sourceRoot":"","sources":["../../src/extension/controller-host.ts"],"names":[],"mappings":"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"}
@@ -68,6 +68,7 @@ export declare class PiDecisionChannel implements HumanDecisionChannel {
68
68
  start(): Promise<void>;
69
69
  stop(): Promise<void>;
70
70
  deliver(request: HumanDecisionChannelRequest): Promise<HumanDecisionDeliveryResult>;
71
+ private alreadyDecided;
71
72
  settle(decision: SettledHumanDecision): Promise<void>;
72
73
  private settleOnce;
73
74
  }
@@ -106,6 +107,7 @@ export declare class TelegramDecisionChannel implements HumanDecisionChannel {
106
107
  private allowed;
107
108
  private isStale;
108
109
  private poll;
110
+ private deliverPendingRequests;
109
111
  private call;
110
112
  }
111
113
  export declare function createTelegramChannels(options: {
@@ -1,11 +1,10 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import fs from "node:fs";
1
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
3
2
  import fsp from "node:fs/promises";
4
3
  import os from "node:os";
5
4
  import path from "node:path";
6
5
  import { Key, matchesKey, wrapTextWithAnsi } from "@earendil-works/pi-tui";
7
- import Database from "better-sqlite3";
8
- import { MAX_PRESENTATION_TRANSPORT_PARTS, decisionDocumentSegments, decisionPresentationFingerprint, digestCanonical, } from "../workflows/decision-presentation.js";
6
+ import { resourceIdFor, tokenHash } from "../state/mutation.js";
7
+ import { MAX_PRESENTATION_TRANSPORT_PARTS, decisionDocumentSegments, decisionPresentationFingerprint, digestCanonical, humanDecisionChannelRequest, } from "../workflows/decision-presentation.js";
9
8
  import { HumanDecisionStore, createHumanDecisionAttemptId } from "../workflows/human-decision.js";
10
9
  const TELEGRAM_TEXT_LIMIT = 4_096;
11
10
  const PI_PRESENTATION_WINDOW_LINES = 18;
@@ -68,6 +67,9 @@ export class PiDecisionChannel {
68
67
  this.promptAbort?.abort();
69
68
  }
70
69
  async deliver(request) {
70
+ if (await this.alreadyDecided(request.decisionId)) {
71
+ return { status: "confirmed", channel: this.id, attemptId: "adopted" };
72
+ }
71
73
  const attemptId = createHumanDecisionAttemptId();
72
74
  const createdAt = new Date().toISOString();
73
75
  const controller = new AbortController();
@@ -159,6 +161,9 @@ export class PiDecisionChannel {
159
161
  }
160
162
  response = { choice: selectedChoice, input: { [definition.input.name]: text } };
161
163
  }
164
+ if (await this.alreadyDecided(request.decisionId)) {
165
+ return { status: "confirmed", channel: this.id, attemptId: "adopted" };
166
+ }
162
167
  const eventId = createHumanDecisionAttemptId();
163
168
  await this.options.onAnswer({
164
169
  request,
@@ -174,6 +179,10 @@ export class PiDecisionChannel {
174
179
  this.promptAbort = null;
175
180
  }
176
181
  }
182
+ async alreadyDecided(decisionId) {
183
+ return ((await this.options.store.readResolved(decisionId)) !== null ||
184
+ (await this.options.store.readCancellation(decisionId)) !== null);
185
+ }
177
186
  async settle(decision) {
178
187
  this.promptAbort?.abort();
179
188
  if (this.settlementTask !== null)
@@ -207,156 +216,301 @@ export class PiDecisionChannel {
207
216
  }
208
217
  }
209
218
  class TelegramProjection {
219
+ state;
210
220
  profile;
211
221
  owner;
212
- db;
213
- constructor(filePath, profile, owner) {
222
+ channelId;
223
+ resourceId;
224
+ token = randomBytes(32).toString("base64url");
225
+ generation = 0;
226
+ constructor(state, profile, owner) {
227
+ this.state = state;
214
228
  this.profile = profile;
215
229
  this.owner = owner;
216
- fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
217
- this.db = new Database(filePath);
218
- fs.chmodSync(filePath, 0o600);
219
- this.db.pragma("journal_mode = WAL");
220
- this.db.pragma("busy_timeout = 5000");
221
- this.db.exec(`
222
- CREATE TABLE IF NOT EXISTS leases (
223
- profile TEXT PRIMARY KEY,
224
- owner TEXT NOT NULL,
225
- expires_at INTEGER NOT NULL
226
- );
227
- CREATE TABLE IF NOT EXISTS offsets (
228
- profile TEXT PRIMARY KEY,
229
- next_offset INTEGER NOT NULL
230
- );
231
- CREATE TABLE IF NOT EXISTS callbacks (
232
- profile TEXT NOT NULL,
233
- token TEXT NOT NULL,
234
- request_json TEXT NOT NULL,
235
- choice_id TEXT NOT NULL,
236
- PRIMARY KEY (profile, token)
237
- );
238
- CREATE TABLE IF NOT EXISTS replies (
239
- profile TEXT NOT NULL,
240
- chat_id TEXT NOT NULL,
241
- message_id TEXT NOT NULL,
242
- request_json TEXT NOT NULL,
243
- choice_id TEXT NOT NULL,
244
- PRIMARY KEY (profile, chat_id, message_id)
245
- );
246
- CREATE TABLE IF NOT EXISTS messages (
247
- profile TEXT NOT NULL,
248
- decision_id TEXT NOT NULL,
249
- chat_id TEXT NOT NULL,
250
- message_id TEXT NOT NULL,
251
- PRIMARY KEY (profile, decision_id, chat_id, message_id)
252
- );
253
- CREATE TABLE IF NOT EXISTS message_parts (
254
- profile TEXT NOT NULL,
255
- decision_id TEXT NOT NULL,
256
- recipient_index INTEGER NOT NULL,
257
- part_index INTEGER NOT NULL,
258
- content_digest TEXT NOT NULL,
259
- chat_id TEXT NOT NULL,
260
- message_id TEXT NOT NULL,
261
- PRIMARY KEY (profile, decision_id, recipient_index, part_index)
262
- );
263
- `);
230
+ this.channelId = `telegram-${createHash("sha256").update(profile).digest("hex").slice(0, 40)}`;
231
+ this.resourceId = resourceIdFor("channel", this.channelId);
232
+ this.state.transaction(() => {
233
+ const now = Date.now();
234
+ this.state.connection
235
+ .prepare(`INSERT INTO resources(
236
+ resource_id, resource_type, aggregate_key, revision, created_at, updated_at
237
+ ) VALUES (?, 'channel', ?, 1, ?, ?)
238
+ ON CONFLICT(resource_type, aggregate_key) DO NOTHING`)
239
+ .run(this.resourceId, this.channelId, now, now);
240
+ this.state.connection
241
+ .prepare("INSERT INTO leases(resource_id, generation) VALUES (?, 0) ON CONFLICT(resource_id) DO NOTHING")
242
+ .run(this.resourceId);
243
+ this.state.connection
244
+ .prepare(`INSERT INTO channels(channel_id, resource_id, adapter_type, profile_key, created_at)
245
+ VALUES (?, ?, 'telegram', ?, ?) ON CONFLICT(channel_id) DO NOTHING`)
246
+ .run(this.channelId, this.resourceId, this.profile, now);
247
+ });
264
248
  }
265
249
  close() {
266
250
  this.release();
267
- this.db.close();
251
+ }
252
+ ownsLease() {
253
+ try {
254
+ this.assertOwner();
255
+ return true;
256
+ }
257
+ catch {
258
+ return false;
259
+ }
268
260
  }
269
261
  acquire(now = Date.now()) {
270
- const transaction = this.db.transaction(() => {
271
- const row = this.db
272
- .prepare("SELECT owner, expires_at FROM leases WHERE profile = ?")
273
- .get(this.profile);
274
- if (row !== undefined && row.owner !== this.owner && row.expires_at > now)
262
+ return this.state.transaction(() => {
263
+ const lease = this.lease();
264
+ if (lease.ownerId !== null &&
265
+ lease.ownerId !== this.owner &&
266
+ lease.expiresAt !== null &&
267
+ lease.expiresAt > now) {
275
268
  return false;
276
- this.db
277
- .prepare("INSERT INTO leases(profile, owner, expires_at) VALUES (?, ?, ?) ON CONFLICT(profile) DO UPDATE SET owner = excluded.owner, expires_at = excluded.expires_at")
278
- .run(this.profile, this.owner, now + LEASE_TTL_MS);
269
+ }
270
+ const generation = lease.ownerId === this.owner ? lease.generation : lease.generation + 1;
271
+ const result = this.state.connection
272
+ .prepare(`UPDATE leases
273
+ SET generation = ?, owner_type = 'channel', owner_id = ?, token_hash = ?,
274
+ acquired_at = COALESCE(acquired_at, ?), heartbeat_at = ?, expires_at = ?
275
+ WHERE resource_id = ? AND generation = ?`)
276
+ .run(generation, this.owner, tokenHash(this.token), now, now, now + LEASE_TTL_MS, this.resourceId, lease.generation);
277
+ /* istanbul ignore if -- impossible after exact schema and transaction checks */
278
+ if (result.changes !== 1)
279
+ return false;
280
+ this.generation = generation;
281
+ this.recordMutation("channel.lease_acquired", { expiresAt: now + LEASE_TTL_MS }, now);
279
282
  return true;
280
283
  });
281
- return transaction();
282
284
  }
283
285
  renew(now = Date.now()) {
284
- return (this.db
285
- .prepare("UPDATE leases SET expires_at = ? WHERE profile = ? AND owner = ?")
286
- .run(now + LEASE_TTL_MS, this.profile, this.owner).changes === 1);
286
+ return (this.state.connection
287
+ .prepare(`UPDATE leases SET heartbeat_at = ?, expires_at = ?
288
+ WHERE resource_id = ? AND owner_id = ? AND token_hash = ? AND generation = ?`)
289
+ .run(now, now + LEASE_TTL_MS, this.resourceId, this.owner, tokenHash(this.token), this.generation).changes === 1);
287
290
  }
288
291
  release() {
289
- this.db
290
- .prepare("DELETE FROM leases WHERE profile = ? AND owner = ?")
291
- .run(this.profile, this.owner);
292
+ this.state.transaction(() => {
293
+ const result = this.state.connection
294
+ .prepare(`UPDATE leases
295
+ SET owner_type = NULL, owner_id = NULL, token_hash = NULL,
296
+ acquired_at = NULL, heartbeat_at = NULL, expires_at = NULL
297
+ WHERE resource_id = ? AND owner_id = ? AND token_hash = ? AND generation = ?`)
298
+ .run(this.resourceId, this.owner, tokenHash(this.token), this.generation);
299
+ if (result.changes === 1) {
300
+ this.recordMutation("channel.lease_released", {}, Date.now());
301
+ }
302
+ });
292
303
  }
293
304
  offset() {
294
- return (this.db.prepare("SELECT next_offset FROM offsets WHERE profile = ?").get(this.profile)?.next_offset ?? 0);
305
+ const row = this.state.connection
306
+ .prepare("SELECT cursor_value AS cursorValue FROM channel_cursors WHERE channel_id = ? AND cursor_key = 'telegram_update'")
307
+ .get(this.channelId);
308
+ return isCursorRow(row) ? Number.parseInt(row.cursorValue, 10) || 0 : 0;
295
309
  }
296
310
  setOffset(offset) {
297
- this.db
298
- .prepare("INSERT INTO offsets(profile, next_offset) VALUES (?, ?) ON CONFLICT(profile) DO UPDATE SET next_offset = MAX(next_offset, excluded.next_offset)")
299
- .run(this.profile, offset);
311
+ this.mutate("channel.cursor_updated", { offset }, () => {
312
+ this.state.connection
313
+ .prepare(`INSERT INTO channel_cursors(channel_id, cursor_key, cursor_value, updated_at)
314
+ VALUES (?, 'telegram_update', ?, ?)
315
+ ON CONFLICT(channel_id, cursor_key) DO UPDATE SET
316
+ cursor_value = CAST(MAX(CAST(channel_cursors.cursor_value AS INTEGER), CAST(excluded.cursor_value AS INTEGER)) AS TEXT),
317
+ updated_at = excluded.updated_at`)
318
+ .run(this.channelId, String(offset), Date.now());
319
+ });
300
320
  }
301
321
  putCallback(token, binding) {
302
- this.db
303
- .prepare("INSERT INTO callbacks(profile, token, request_json, choice_id) VALUES (?, ?, ?, ?) ON CONFLICT(profile, token) DO UPDATE SET request_json = excluded.request_json, choice_id = excluded.choice_id")
304
- .run(this.profile, token, JSON.stringify(binding.request), binding.choice);
322
+ this.putInbox(`callback:${token}`, "callback", binding);
305
323
  }
306
324
  callback(token) {
307
- const row = this.db
308
- .prepare("SELECT request_json, choice_id FROM callbacks WHERE profile = ? AND token = ?")
309
- .get(this.profile, token);
310
- return row === undefined
311
- ? undefined
312
- : {
313
- request: JSON.parse(row.request_json),
314
- choice: row.choice_id,
315
- };
325
+ return this.inbox(`callback:${token}`);
316
326
  }
317
327
  putReply(binding) {
318
- this.db
319
- .prepare("INSERT INTO replies(profile, chat_id, message_id, request_json, choice_id) VALUES (?, ?, ?, ?, ?) ON CONFLICT(profile, chat_id, message_id) DO UPDATE SET request_json = excluded.request_json, choice_id = excluded.choice_id")
320
- .run(this.profile, binding.chatId, binding.promptMessageId, JSON.stringify(binding.request), binding.choice);
328
+ this.putInbox(`reply:${binding.chatId}:${binding.promptMessageId}`, "reply", binding);
321
329
  }
322
330
  reply(chatId, messageId) {
323
- const row = this.db
324
- .prepare("SELECT request_json, choice_id, chat_id, message_id FROM replies WHERE profile = ? AND chat_id = ? AND message_id = ?")
325
- .get(this.profile, chatId, messageId);
326
- return row === undefined
327
- ? undefined
328
- : {
329
- request: JSON.parse(row.request_json),
330
- choice: row.choice_id,
331
- chatId: row.chat_id,
332
- promptMessageId: row.message_id,
333
- };
331
+ return this.inbox(`reply:${chatId}:${messageId}`);
334
332
  }
335
333
  deleteReply(chatId, messageId) {
336
- this.db
337
- .prepare("DELETE FROM replies WHERE profile = ? AND chat_id = ? AND message_id = ?")
338
- .run(this.profile, chatId, messageId);
334
+ this.mutate("channel.reply_consumed", { chatId, messageId }, () => {
335
+ this.state.connection
336
+ .prepare("DELETE FROM channel_inbox WHERE channel_id = ? AND external_event_id = ?")
337
+ .run(this.channelId, `reply:${chatId}:${messageId}`);
338
+ });
339
339
  }
340
340
  putMessage(decisionId, chatId, messageId) {
341
- this.db
342
- .prepare("INSERT OR IGNORE INTO messages(profile, decision_id, chat_id, message_id) VALUES (?, ?, ?, ?)")
343
- .run(this.profile, decisionId, chatId, messageId);
341
+ const localId = transportMessageId(this.channelId, decisionId, chatId, messageId);
342
+ this.mutate("channel.message_confirmed", { decisionId, chatId, messageId }, () => {
343
+ const contentHash = this.state.putJson({ chatId, messageId });
344
+ this.state.connection
345
+ .prepare(`INSERT INTO channel_messages(
346
+ message_id, channel_id, decision_id, purpose, content_hash,
347
+ external_conversation_ref, external_message_ref, status, created_at, updated_at
348
+ ) VALUES (?, ?, ?, 'delivery', ?, ?, ?, 'confirmed', ?, ?)
349
+ ON CONFLICT(message_id) DO NOTHING`)
350
+ .run(localId, this.channelId, decisionId, contentHash, chatId, messageId, Date.now(), Date.now());
351
+ });
344
352
  }
345
353
  messages(decisionId) {
346
- return this.db
347
- .prepare("SELECT chat_id, message_id FROM messages WHERE profile = ? AND decision_id = ? ORDER BY chat_id, message_id")
348
- .all(this.profile, decisionId);
354
+ const rows = this.state.connection
355
+ .prepare(`SELECT external_conversation_ref AS chatId, external_message_ref AS messageId
356
+ FROM channel_messages
357
+ WHERE channel_id = ? AND decision_id = ? AND purpose = 'delivery'
358
+ AND external_conversation_ref IS NOT NULL AND external_message_ref IS NOT NULL
359
+ ORDER BY external_conversation_ref, external_message_ref`)
360
+ .all(this.channelId, decisionId);
361
+ return rows.filter(isTransportMessageRow).map((row) => ({
362
+ chat_id: row.chatId,
363
+ message_id: row.messageId,
364
+ }));
349
365
  }
350
366
  putMessagePart(decisionId, recipientIndex, partIndex, contentDigest, chatId, messageId) {
351
- this.db
352
- .prepare("INSERT INTO message_parts(profile, decision_id, recipient_index, part_index, content_digest, chat_id, message_id) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(profile, decision_id, recipient_index, part_index) DO UPDATE SET content_digest = excluded.content_digest, chat_id = excluded.chat_id, message_id = excluded.message_id")
353
- .run(this.profile, decisionId, recipientIndex, partIndex, contentDigest, chatId, messageId);
367
+ const localId = `telegram-parts-${createHash("sha256")
368
+ .update(`${this.channelId}\0${decisionId}`)
369
+ .digest("hex")
370
+ .slice(0, 40)}`;
371
+ this.mutate("channel.message_part_confirmed", { decisionId, recipientIndex, partIndex }, () => {
372
+ const parentHash = this.state.putJson({ decisionId });
373
+ this.state.connection
374
+ .prepare(`INSERT INTO channel_messages(
375
+ message_id, channel_id, decision_id, purpose, content_hash,
376
+ status, created_at, updated_at
377
+ ) VALUES (?, ?, ?, 'delivery', ?, 'confirmed', ?, ?)
378
+ ON CONFLICT(message_id) DO NOTHING`)
379
+ .run(localId, this.channelId, decisionId, parentHash, Date.now(), Date.now());
380
+ const contentHash = this.state.putText(contentDigest);
381
+ this.state.connection
382
+ .prepare(`INSERT INTO channel_message_parts(
383
+ message_id, recipient_index, part_index, content_hash,
384
+ external_conversation_ref, external_message_ref
385
+ ) VALUES (?, ?, ?, ?, ?, ?)
386
+ ON CONFLICT(message_id, recipient_index, part_index) DO UPDATE SET
387
+ content_hash = excluded.content_hash,
388
+ external_conversation_ref = excluded.external_conversation_ref,
389
+ external_message_ref = excluded.external_message_ref`)
390
+ .run(localId, recipientIndex, partIndex, contentHash, chatId, messageId);
391
+ });
354
392
  }
355
393
  messagePart(decisionId, recipientIndex, partIndex) {
356
- return this.db
357
- .prepare("SELECT recipient_index, part_index, content_digest, chat_id, message_id FROM message_parts WHERE profile = ? AND decision_id = ? AND recipient_index = ? AND part_index = ?")
358
- .get(this.profile, decisionId, recipientIndex, partIndex);
394
+ const localId = `telegram-parts-${createHash("sha256")
395
+ .update(`${this.channelId}\0${decisionId}`)
396
+ .digest("hex")
397
+ .slice(0, 40)}`;
398
+ const row = this.state.connection
399
+ .prepare(`SELECT recipient_index AS recipientIndex, part_index AS partIndex,
400
+ content_hash AS contentHash,
401
+ external_conversation_ref AS chatId,
402
+ external_message_ref AS messageId
403
+ FROM channel_message_parts
404
+ WHERE message_id = ? AND recipient_index = ? AND part_index = ?`)
405
+ .get(localId, recipientIndex, partIndex);
406
+ if (!isMessagePartProjectionRow(row))
407
+ return undefined;
408
+ const content = this.state.readBlob(row.contentHash);
409
+ /* istanbul ignore if -- foreign key guarantees the content blob */
410
+ if (content === undefined)
411
+ throw new Error("Telegram part content is missing");
412
+ return {
413
+ recipient_index: row.recipientIndex,
414
+ part_index: row.partIndex,
415
+ content_digest: content.content.toString("utf8"),
416
+ chat_id: row.chatId,
417
+ message_id: row.messageId,
418
+ };
419
+ }
420
+ putInbox(eventId, type, binding) {
421
+ this.mutate(`channel.${type}_stored`, { eventId }, () => {
422
+ const payloadHash = this.state.putJson(binding);
423
+ this.state.connection
424
+ .prepare(`INSERT INTO channel_inbox(
425
+ channel_id, external_event_id, event_type, payload_hash, received_at
426
+ ) VALUES (?, ?, ?, ?, ?)
427
+ ON CONFLICT(channel_id, external_event_id) DO UPDATE SET payload_hash = excluded.payload_hash`)
428
+ .run(this.channelId, eventId, type, payloadHash, Date.now());
429
+ });
430
+ }
431
+ inbox(eventId) {
432
+ const row = this.state.connection
433
+ .prepare("SELECT payload_hash AS payloadHash FROM channel_inbox WHERE channel_id = ? AND external_event_id = ?")
434
+ .get(this.channelId, eventId);
435
+ return isPayloadHashRow(row) ? this.state.readJson(row.payloadHash) : undefined;
436
+ }
437
+ mutate(type, payload, operation) {
438
+ this.state.transaction(() => {
439
+ this.assertOwner();
440
+ operation();
441
+ this.recordMutation(type, payload, Date.now());
442
+ });
443
+ }
444
+ recordMutation(type, payload, now) {
445
+ const revisionRow = this.state.connection
446
+ .prepare("SELECT revision FROM resources WHERE resource_id = ?")
447
+ .get(this.resourceId);
448
+ /* istanbul ignore if -- exact schema and internal query shape */
449
+ /* istanbul ignore if -- impossible after exact schema and transaction checks */
450
+ if (!isRevisionProjectionRow(revisionRow))
451
+ throw new Error("Telegram channel resource is missing");
452
+ const revision = revisionRow.revision + 1;
453
+ this.state.connection
454
+ .prepare("UPDATE resources SET revision = ?, updated_at = ? WHERE resource_id = ? AND revision = ?")
455
+ .run(revision, now, this.resourceId, revision - 1);
456
+ const payloadHash = this.state.putJson(payload, now);
457
+ this.state.connection
458
+ .prepare(`INSERT INTO events(
459
+ event_id, resource_id, resource_revision, event_type, actor_type,
460
+ actor_id, lease_generation, payload_hash, recorded_at
461
+ ) VALUES (?, ?, ?, ?, 'channel', ?, ?, ?, ?)`)
462
+ .run(`event-${randomUUID()}`, this.resourceId, revision, type, this.owner, this.generation === 0 ? null : this.generation, payloadHash, now);
359
463
  }
464
+ assertOwner() {
465
+ const lease = this.lease();
466
+ if (lease.ownerId !== this.owner ||
467
+ lease.generation !== this.generation ||
468
+ lease.tokenHash === null ||
469
+ !lease.tokenHash.equals(tokenHash(this.token)) ||
470
+ lease.expiresAt === null ||
471
+ lease.expiresAt <= Date.now()) {
472
+ throw new Error(`Telegram profile ${this.profile} is not owned by this channel process`);
473
+ }
474
+ }
475
+ lease() {
476
+ const row = this.state.connection
477
+ .prepare(`SELECT generation, owner_id AS ownerId, token_hash AS tokenHash,
478
+ expires_at AS expiresAt
479
+ FROM leases WHERE resource_id = ?`)
480
+ .get(this.resourceId);
481
+ /* istanbul ignore if -- exact schema and internal query shape */
482
+ /* istanbul ignore if -- impossible after exact schema and transaction checks */
483
+ if (!isTelegramLeaseRow(row))
484
+ throw new Error("Telegram channel lease is missing");
485
+ return row;
486
+ }
487
+ }
488
+ function transportMessageId(channelId, decisionId, chatId, messageId) {
489
+ return `telegram-message-${createHash("sha256")
490
+ .update(`${channelId}\0${decisionId}\0${chatId}\0${messageId}`)
491
+ .digest("hex")
492
+ .slice(0, 40)}`;
493
+ }
494
+ function isTelegramLeaseRow(value) {
495
+ return isProjectionRecord(value);
496
+ }
497
+ function isCursorRow(value) {
498
+ return isProjectionRecord(value);
499
+ }
500
+ function isPayloadHashRow(value) {
501
+ return isProjectionRecord(value);
502
+ }
503
+ function isTransportMessageRow(value) {
504
+ return isProjectionRecord(value);
505
+ }
506
+ function isMessagePartProjectionRow(value) {
507
+ return isProjectionRecord(value);
508
+ }
509
+ function isRevisionProjectionRow(value) {
510
+ return isProjectionRecord(value);
511
+ }
512
+ function isProjectionRecord(value) {
513
+ return typeof value === "object" && value !== null && !Array.isArray(value);
360
514
  }
361
515
  class TelegramCallError extends Error {
362
516
  ambiguous;
@@ -396,7 +550,7 @@ export class TelegramDecisionChannel {
396
550
  this.onAnswer = options.onAnswer;
397
551
  this.fetchFn = options.fetchFn ?? fetch;
398
552
  this.apiBase = options.apiBase ?? DEFAULT_API_BASE;
399
- this.projection = new TelegramProjection(path.join(options.configDir ?? decisionConfigDir(), "telegram-state.sqlite"), options.profileName, options.ownerId ?? randomUUID());
553
+ this.projection = new TelegramProjection(options.store.state, options.profileName, options.ownerId ?? randomUUID());
400
554
  }
401
555
  async start() {
402
556
  if (this.running)
@@ -416,6 +570,21 @@ export class TelegramDecisionChannel {
416
570
  }
417
571
  async deliver(request) {
418
572
  const channelPath = this.channelPath();
573
+ if (!this.projection.ownsLease()) {
574
+ const attemptId = `queued-${createHash("sha256")
575
+ .update(`${this.id}\0${request.decisionId}\0${request.requestDigest}`)
576
+ .digest("hex")
577
+ .slice(0, 40)}`;
578
+ await this.store.recordDelivery(request, channelPath, deliveryRecord(request, this.id, attemptId, "complete", "intent", new Date().toISOString(), {
579
+ errorCode: "queued_for_channel_owner",
580
+ }));
581
+ return {
582
+ status: "unknown",
583
+ channel: this.id,
584
+ attemptId,
585
+ errorCode: "queued_for_channel_owner",
586
+ };
587
+ }
419
588
  const parts = renderTelegramParts(request);
420
589
  const partDigests = parts.map((part) => digestCanonical(part));
421
590
  const prior = await this.store.listDeliveries(request.decisionId, channelPath);
@@ -643,6 +812,7 @@ export class TelegramDecisionChannel {
643
812
  continue;
644
813
  }
645
814
  try {
815
+ await this.deliverPendingRequests();
646
816
  const result = await this.call("getUpdates", {
647
817
  offset: this.projection.offset(),
648
818
  timeout: 20,
@@ -665,6 +835,27 @@ export class TelegramDecisionChannel {
665
835
  }
666
836
  }
667
837
  }
838
+ async deliverPendingRequests() {
839
+ const channelPath = this.channelPath();
840
+ for (const request of await this.store.listRequests()) {
841
+ if (request.expiresAt !== undefined && Date.parse(request.expiresAt) <= Date.now())
842
+ continue;
843
+ if ((await this.store.readResolved(request.decisionId)) !== null)
844
+ continue;
845
+ if ((await this.store.readCancellation(request.decisionId)) !== null)
846
+ continue;
847
+ const deliveries = await this.store.listDeliveries(request.decisionId, channelPath);
848
+ if (!deliveries.some((record) => record.state === "intent" && record.errorCode === "queued_for_channel_owner")) {
849
+ continue;
850
+ }
851
+ try {
852
+ await this.deliver(humanDecisionChannelRequest(request));
853
+ }
854
+ catch {
855
+ // Durable delivery evidence controls retries for each request.
856
+ }
857
+ }
858
+ }
668
859
  async call(method, payload, signal) {
669
860
  let response;
670
861
  try {
@@ -715,7 +906,9 @@ export function createTelegramChannels(options) {
715
906
  }
716
907
  return channels;
717
908
  }
718
- export async function verifyTelegramTokenFile(tokenFile, fetchFn = fetch, apiBase = DEFAULT_API_BASE) {
909
+ export async function verifyTelegramTokenFile(tokenFile, fetchFn, apiBase = DEFAULT_API_BASE) {
910
+ /* istanbul ignore next -- global fetch is used only by operator setup */
911
+ const request = fetchFn ?? fetch;
719
912
  if (!path.isAbsolute(tokenFile))
720
913
  throw new Error("Telegram token file path must be absolute");
721
914
  await requirePrivateFile(tokenFile, "Telegram token file");
@@ -724,7 +917,7 @@ export async function verifyTelegramTokenFile(tokenFile, fetchFn = fetch, apiBas
724
917
  throw new Error("Telegram token file is empty");
725
918
  let response;
726
919
  try {
727
- response = await fetchFn(`${apiBase}/bot${encodeURIComponent(token)}/getMe`, {
920
+ response = await request(`${apiBase}/bot${encodeURIComponent(token)}/getMe`, {
728
921
  method: "POST",
729
922
  headers: { "content-type": "application/json" },
730
923
  body: "{}",