@osolmaz/pi-workflows 0.13.1 → 0.13.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.
Files changed (50) hide show
  1. package/README.md +4 -4
  2. package/dist/builtins/autoimplement.workflow.d.ts +4 -0
  3. package/dist/builtins/autoplan.workflow.d.ts +6 -0
  4. package/dist/builtins/autoplan.workflow.js +68 -32
  5. package/dist/builtins/autoplan.workflow.js.map +1 -1
  6. package/dist/builtins/catalog.js +3 -3
  7. package/dist/builtins/plain-summary.workflow.js +35 -24
  8. package/dist/builtins/plain-summary.workflow.js.map +1 -1
  9. package/dist/builtins/plan-change.workflow.d.ts +2 -0
  10. package/dist/builtins/sanity-check.workflow.js +19 -22
  11. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  12. package/dist/controllers/sqlite.js +16 -51
  13. package/dist/controllers/sqlite.js.map +1 -1
  14. package/dist/extension/decision-channels.js +45 -7
  15. package/dist/extension/decision-channels.js.map +1 -1
  16. package/dist/extension/index.js +2 -1
  17. package/dist/extension/index.js.map +1 -1
  18. package/dist/extension/recorder.d.ts +1 -1
  19. package/dist/extension/recorder.js +8 -8
  20. package/dist/extension/recorder.js.map +1 -1
  21. package/dist/state/schema.js +22 -3
  22. package/dist/state/schema.js.map +1 -1
  23. package/dist/viewer/session-reducer.d.ts +0 -1
  24. package/dist/viewer/session-reducer.js +2 -24
  25. package/dist/viewer/session-reducer.js.map +1 -1
  26. package/dist/workflows/engine.js +4 -3
  27. package/dist/workflows/engine.js.map +1 -1
  28. package/dist/workflows/store.d.ts +6 -1
  29. package/dist/workflows/store.js +286 -33
  30. package/dist/workflows/store.js.map +1 -1
  31. package/dist/workflows/types.d.ts +1 -1
  32. package/docs/SQLITE_STATE.md +20 -14
  33. package/docs/plans/2026-08-25-autoplan-user-intent-capture-plan.md +107 -0
  34. package/docs/session-event-journal.md +2 -3
  35. package/docs/workflows.md +36 -9
  36. package/herdr-plugin.toml +1 -1
  37. package/package.json +1 -1
  38. package/src/builtins/autoplan.workflow.ts +80 -32
  39. package/src/builtins/catalog.ts +3 -3
  40. package/src/builtins/plain-summary.workflow.ts +38 -40
  41. package/src/builtins/sanity-check.workflow.ts +19 -22
  42. package/src/controllers/sqlite.ts +19 -53
  43. package/src/extension/decision-channels.ts +47 -7
  44. package/src/extension/index.ts +2 -1
  45. package/src/extension/recorder.ts +10 -8
  46. package/src/state/schema.ts +22 -3
  47. package/src/viewer/session-reducer.ts +2 -29
  48. package/src/workflows/engine.ts +4 -3
  49. package/src/workflows/store.ts +397 -43
  50. package/src/workflows/types.ts +0 -1
@@ -28,6 +28,7 @@ const TELEGRAM_TEXT_LIMIT = 4_096;
28
28
  const PI_PRESENTATION_WINDOW_LINES = 18;
29
29
  const DEFAULT_API_BASE = "https://api.telegram.org";
30
30
  const LEASE_TTL_MS = 60_000;
31
+ const LEASE_RENEW_WINDOW_MS = 20_000;
31
32
  const LEASE_RETRY_MS = 5_000;
32
33
  const POLL_BACKOFF_MS = 1_000;
33
34
  const MAX_SETTLEMENT_ATTEMPTS = 3;
@@ -428,6 +429,21 @@ class TelegramProjection {
428
429
  }
429
430
 
430
431
  acquire(now = Date.now()): boolean {
432
+ const observed = this.lease();
433
+ const observedToken = tokenHash(this.token);
434
+ const ownedByThis =
435
+ observed.ownerId === this.owner &&
436
+ observed.tokenHash !== null &&
437
+ observed.tokenHash.equals(observedToken) &&
438
+ observed.expiresAt !== null &&
439
+ observed.expiresAt > now;
440
+ if (ownedByThis) {
441
+ this.generation = observed.generation;
442
+ return true;
443
+ }
444
+ if (observed.ownerId !== null && observed.expiresAt !== null && observed.expiresAt > now) {
445
+ return false;
446
+ }
431
447
  return this.state.transaction(() => {
432
448
  const lease = this.lease();
433
449
  if (
@@ -438,7 +454,11 @@ class TelegramProjection {
438
454
  ) {
439
455
  return false;
440
456
  }
441
- const generation = lease.ownerId === this.owner ? lease.generation : lease.generation + 1;
457
+ const sameToken =
458
+ lease.ownerId === this.owner &&
459
+ lease.tokenHash !== null &&
460
+ lease.tokenHash.equals(observedToken);
461
+ const generation = sameToken ? lease.generation : lease.generation + 1;
442
462
  const result = this.state.connection
443
463
  .prepare(
444
464
  `UPDATE leases
@@ -449,7 +469,7 @@ class TelegramProjection {
449
469
  .run(
450
470
  generation,
451
471
  this.owner,
452
- tokenHash(this.token),
472
+ observedToken,
453
473
  now,
454
474
  now,
455
475
  now + LEASE_TTL_MS,
@@ -465,6 +485,18 @@ class TelegramProjection {
465
485
  }
466
486
 
467
487
  renew(now = Date.now()): boolean {
488
+ const lease = this.lease();
489
+ if (
490
+ lease.ownerId !== this.owner ||
491
+ lease.generation !== this.generation ||
492
+ lease.tokenHash === null ||
493
+ !lease.tokenHash.equals(tokenHash(this.token)) ||
494
+ lease.expiresAt === null ||
495
+ lease.expiresAt <= now
496
+ ) {
497
+ return false;
498
+ }
499
+ if (lease.expiresAt - now > LEASE_RENEW_WINDOW_MS) return true;
468
500
  return (
469
501
  this.state.connection
470
502
  .prepare(
@@ -1222,11 +1254,11 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
1222
1254
 
1223
1255
  private async poll(signal: AbortSignal): Promise<void> {
1224
1256
  while (this.running && !signal.aborted) {
1225
- if (!this.projection.acquire()) {
1226
- await wait(LEASE_RETRY_MS, signal);
1227
- continue;
1228
- }
1229
1257
  try {
1258
+ if (!this.projection.acquire()) {
1259
+ await wait(leaseRetryDelay(), signal);
1260
+ continue;
1261
+ }
1230
1262
  await this.deliverPendingRequests();
1231
1263
  const result = await this.call(
1232
1264
  "getUpdates",
@@ -1246,7 +1278,11 @@ export class TelegramDecisionChannel implements HumanDecisionChannel {
1246
1278
  if (!this.projection.renew()) this.projection.release();
1247
1279
  } catch {
1248
1280
  if (signal.aborted) return;
1249
- await wait(POLL_BACKOFF_MS, signal);
1281
+ try {
1282
+ await wait(POLL_BACKOFF_MS, signal);
1283
+ } catch {
1284
+ if (signal.aborted) return;
1285
+ }
1250
1286
  }
1251
1287
  }
1252
1288
  }
@@ -1773,6 +1809,10 @@ async function writePrivateJson(filePath: string, value: unknown): Promise<void>
1773
1809
  await fsp.chmod(filePath, 0o600);
1774
1810
  }
1775
1811
 
1812
+ function leaseRetryDelay(): number {
1813
+ return LEASE_RETRY_MS + Math.floor(Math.random() * 1_000);
1814
+ }
1815
+
1776
1816
  async function wait(milliseconds: number, signal: AbortSignal): Promise<void> {
1777
1817
  await new Promise<void>((resolve) => {
1778
1818
  const timer = setTimeout(resolve, milliseconds);
@@ -9,6 +9,7 @@ import type {
9
9
  } from "../controllers/sqlite.js";
10
10
  import type { JsonObject } from "../controllers/types.js";
11
11
  import type { WorkflowSchedulerResult } from "../controllers/workflows.js";
12
+ import { canonicalJson } from "../state/json.js";
12
13
  import { compositionMetadata } from "../workflows/composition.js";
13
14
  import { humanDecisionChannelRequest } from "../workflows/decision-presentation.js";
14
15
  import { WorkflowEngine } from "../workflows/engine.js";
@@ -219,7 +220,7 @@ type StartRunOptions = {
219
220
  };
220
221
 
221
222
  function definitionDigest(snapshot: WorkflowDefinitionSnapshot): string {
222
- return `sha256:${createHash("sha256").update(JSON.stringify(snapshot)).digest("hex")}`;
223
+ return `sha256:${createHash("sha256").update(canonicalJson(snapshot)).digest("hex")}`;
223
224
  }
224
225
 
225
226
  function launchSourceIdentity(workflow: WorkflowDefinition, root: unknown): unknown {
@@ -246,6 +246,13 @@ export class SessionRecorder {
246
246
  return;
247
247
  }
248
248
  const normalized = normalizeAssistantEvent(event.assistantMessageEvent);
249
+ if (
250
+ normalized.type === "text_delta" ||
251
+ normalized.type === "thinking_delta" ||
252
+ normalized.type === "toolcall_delta"
253
+ ) {
254
+ return;
255
+ }
249
256
  const toolCallId = toolCallIdFromAssistantEvent(normalized);
250
257
  if (toolCallId) {
251
258
  this.toolOwners.set(toolCallId, owner);
@@ -295,14 +302,9 @@ export class SessionRecorder {
295
302
  });
296
303
  }
297
304
 
298
- handleToolUpdate(event: ToolExecutionUpdateEventLike): void {
299
- const owner = this.toolOwners.get(event.toolCallId);
300
- if (!owner) {
301
- return;
302
- }
303
- this.enqueue(owner, "tool_execution_updated", {}, undefined, {
304
- toolCallId: event.toolCallId,
305
- });
305
+ handleToolUpdate(_event: ToolExecutionUpdateEventLike): void {
306
+ // Incremental tool progress is transient. The recorder stores the settled
307
+ // tool result and the surrounding lifecycle facts.
306
308
  }
307
309
 
308
310
  handleToolEnd(event: ToolExecutionEndEventLike): void {
@@ -106,8 +106,15 @@ CREATE TABLE runs (
106
106
  paused INTEGER NOT NULL DEFAULT 0 CHECK (paused IN (0, 1)),
107
107
  status_detail TEXT,
108
108
  input_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
109
- output_hash BLOB REFERENCES blobs(blob_hash),
109
+ workflow_sources_hash BLOB REFERENCES blobs(blob_hash),
110
+ human_decision_hash BLOB REFERENCES blobs(blob_hash),
111
+ final_output_hash BLOB REFERENCES blobs(blob_hash),
110
112
  error_hash BLOB REFERENCES blobs(blob_hash),
113
+ carried_step_count INTEGER NOT NULL DEFAULT 0 CHECK (carried_step_count >= 0),
114
+ current_node TEXT,
115
+ current_attempt_id TEXT,
116
+ current_node_started_at INTEGER,
117
+ waiting_on TEXT,
111
118
  created_at INTEGER NOT NULL,
112
119
  updated_at INTEGER NOT NULL,
113
120
  finished_at INTEGER,
@@ -159,9 +166,9 @@ CREATE TABLE node_attempts (
159
166
  )),
160
167
  input_hash BLOB REFERENCES blobs(blob_hash),
161
168
  contract_hash BLOB REFERENCES blobs(blob_hash),
162
- presentation_hash BLOB REFERENCES blobs(blob_hash),
169
+ prompt_hash BLOB REFERENCES blobs(blob_hash),
163
170
  output_hash BLOB REFERENCES blobs(blob_hash),
164
- result_hash BLOB REFERENCES blobs(blob_hash),
171
+ step_metadata_hash BLOB REFERENCES blobs(blob_hash),
165
172
  error_hash BLOB REFERENCES blobs(blob_hash),
166
173
  started_at INTEGER,
167
174
  deadline_at INTEGER,
@@ -175,10 +182,22 @@ CREATE UNIQUE INDEX node_attempts_active_idx ON node_attempts(run_id)
175
182
  WHERE status IN ('pending', 'running', 'waiting');
176
183
  CREATE INDEX node_attempts_run_idx ON node_attempts(run_id, created_at);
177
184
 
185
+ CREATE TABLE run_steps (
186
+ run_id TEXT NOT NULL REFERENCES runs(run_id) ON DELETE CASCADE,
187
+ step_index INTEGER NOT NULL CHECK (step_index >= 0),
188
+ attempt_id TEXT NOT NULL REFERENCES node_attempts(attempt_id),
189
+ output_override_hash BLOB REFERENCES blobs(blob_hash),
190
+ PRIMARY KEY (run_id, step_index),
191
+ UNIQUE (run_id, attempt_id)
192
+ ) STRICT;
193
+
194
+ CREATE INDEX run_steps_attempt_idx ON run_steps(attempt_id);
195
+
178
196
  CREATE TABLE workflow_updates (
179
197
  update_id TEXT PRIMARY KEY,
180
198
  attempt_id TEXT NOT NULL REFERENCES node_attempts(attempt_id) ON DELETE CASCADE,
181
199
  update_seq INTEGER NOT NULL CHECK (update_seq > 0),
200
+ run_revision INTEGER NOT NULL CHECK (run_revision > 0),
182
201
  update_type TEXT NOT NULL,
183
202
  update_key TEXT NOT NULL,
184
203
  data_hash BLOB NOT NULL REFERENCES blobs(blob_hash),
@@ -20,7 +20,6 @@ export type TemporalTool = {
20
20
  messageId: string;
21
21
  toolName: string;
22
22
  status: "running" | "finished" | "failed";
23
- updates: number;
24
23
  args?: unknown;
25
24
  result?: unknown;
26
25
  };
@@ -155,22 +154,6 @@ function foldSessionEvents(
155
154
  ? "thinking"
156
155
  : "toolCall",
157
156
  );
158
- } else if (
159
- contentIndex !== undefined &&
160
- (assistantType === "text_delta" ||
161
- assistantType === "thinking_delta" ||
162
- assistantType === "toolcall_delta")
163
- ) {
164
- const block = ensureBlock(
165
- message,
166
- contentIndex,
167
- assistantType === "text_delta"
168
- ? "text"
169
- : assistantType === "thinking_delta"
170
- ? "thinking"
171
- : "toolCall",
172
- );
173
- block.text += payloadString(event.payload, "delta") ?? "";
174
157
  } else if (
175
158
  contentIndex !== undefined &&
176
159
  (assistantType === "text_end" || assistantType === "thinking_end")
@@ -181,10 +164,10 @@ function foldSessionEvents(
181
164
  assistantType === "text_end" ? "text" : "thinking",
182
165
  );
183
166
  const content = payloadString(event.payload, "content") ?? "";
184
- if (block.text !== content) {
167
+ if (block.text.length > 0 && block.text !== content) {
185
168
  diagnostics.push(`${assistantType} mismatch for ${event.messageId}:${contentIndex}`);
186
- block.text = content;
187
169
  }
170
+ block.text = content;
188
171
  } else if (contentIndex !== undefined && assistantType === "toolcall_end") {
189
172
  const block = ensureBlock(message, contentIndex, "toolCall");
190
173
  block.value = event.payload.toolCall;
@@ -229,22 +212,12 @@ function foldSessionEvents(
229
212
  messageId: event.messageId,
230
213
  toolName: payloadString(event.payload, "toolName") ?? "tool",
231
214
  status: "running",
232
- updates: 0,
233
215
  ...(event.payload.args === undefined ? {} : { args: event.payload.args }),
234
216
  };
235
217
  tools.set(event.toolCallId, tool);
236
218
  toolOrder.push(event.toolCallId);
237
219
  break;
238
220
  }
239
- case "tool_execution_updated": {
240
- const tool = event.toolCallId ? tools.get(event.toolCallId) : undefined;
241
- if (tool) {
242
- tool.updates += 1;
243
- } else {
244
- diagnostics.push(`tool_execution_updated ${event.seq} precedes start`);
245
- }
246
- break;
247
- }
248
221
  case "tool_execution_finished": {
249
222
  const tool = event.toolCallId ? tools.get(event.toolCallId) : undefined;
250
223
  if (!tool) {
@@ -1,5 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { isDeepStrictEqual } from "node:util";
3
+ import { canonicalJson } from "../state/json.js";
3
4
  import {
4
5
  compileWorkflowDefinition,
5
6
  compositionMetadata,
@@ -723,8 +724,8 @@ export class WorkflowEngine {
723
724
  throw new RunParkedError();
724
725
  }
725
726
  this.recordAttempt(workflow, state, attempt);
726
- // The terminal node event carries the output, receipt, and conversation
727
- // linkage so the trace alone is sufficient to reconstruct the run.
727
+ // The durable step row owns the output. The trace keeps the terminal fact
728
+ // and compact execution metadata without copying the output value.
728
729
  await this.persist(runId, state, {
729
730
  scope: "node",
730
731
  type: attempt.result.outcome === "ok" ? "node_finished" : "node_failed",
@@ -1561,7 +1562,7 @@ function workflowIdentityMismatch(
1561
1562
 
1562
1563
  function definitionDigest(workflow: WorkflowDefinition): string {
1563
1564
  return `sha256:${createHash("sha256")
1564
- .update(JSON.stringify(createDefinitionSnapshot(workflow)))
1565
+ .update(canonicalJson(createDefinitionSnapshot(workflow)))
1565
1566
  .digest("hex")}`;
1566
1567
  }
1567
1568