@osolmaz/pi-workflows 0.12.1 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -33
- package/dist/builtins/autodoc.workflow.d.ts +194 -7
- package/dist/builtins/autodoc.workflow.js +156 -30
- package/dist/builtins/autodoc.workflow.js.map +1 -1
- package/dist/builtins/autoimplement-command-batches.d.ts +1 -0
- package/dist/builtins/autoimplement-command-batches.js +29 -31
- package/dist/builtins/autoimplement-command-batches.js.map +1 -1
- package/dist/builtins/autoimplement.workflow.d.ts +1619 -97
- package/dist/builtins/autoimplement.workflow.js +416 -153
- package/dist/builtins/autoimplement.workflow.js.map +1 -1
- package/dist/builtins/autoplan.workflow.d.ts +126 -13
- package/dist/builtins/autoplan.workflow.js +200 -29
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +7 -5
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/change-verification.workflow.d.ts +110 -0
- package/dist/builtins/change-verification.workflow.js +860 -0
- package/dist/builtins/change-verification.workflow.js.map +1 -0
- package/dist/builtins/index.d.ts +2 -1
- package/dist/builtins/index.js +1 -0
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +56 -21
- package/dist/builtins/monitor.workflow.js +499 -217
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/plain-summary.workflow.d.ts +32 -0
- package/dist/builtins/plain-summary.workflow.js +134 -0
- package/dist/builtins/plain-summary.workflow.js.map +1 -0
- package/dist/builtins/plan-change.workflow.d.ts +527 -26
- package/dist/builtins/plan-change.workflow.js +26 -0
- package/dist/builtins/plan-change.workflow.js.map +1 -1
- package/dist/builtins/sanity-check.workflow.d.ts +45 -3
- package/dist/builtins/sanity-check.workflow.js +45 -7
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/builtins/workspace-preparation.workflow.d.ts +75 -0
- package/dist/builtins/workspace-preparation.workflow.js +498 -0
- package/dist/builtins/workspace-preparation.workflow.js.map +1 -0
- package/dist/controllers/effects.d.ts +3 -2
- package/dist/controllers/effects.js +8 -1
- package/dist/controllers/effects.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/manager.d.ts +1 -0
- package/dist/controllers/manager.js +41 -17
- package/dist/controllers/manager.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +107 -85
- package/dist/controllers/sqlite.js +1611 -1486
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/store.d.ts +9 -8
- package/dist/controllers/store.js +1 -31
- package/dist/controllers/store.js.map +1 -1
- package/dist/controllers/types.d.ts +3 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
- package/dist/controllers/workflow-engine-scheduler.js +3 -5
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/controllers/workflows.d.ts +2 -2
- package/dist/controllers/workflows.js +11 -10
- package/dist/controllers/workflows.js.map +1 -1
- package/dist/extension/controller-host.js +2 -2
- package/dist/extension/controller-host.js.map +1 -1
- package/dist/extension/decision-channels.d.ts +2 -0
- package/dist/extension/decision-channels.js +311 -118
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/executor.d.ts +9 -2
- package/dist/extension/executor.js +90 -6
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/herdr-viewer.d.ts +0 -1
- package/dist/extension/herdr-viewer.js +0 -4
- package/dist/extension/herdr-viewer.js.map +1 -1
- package/dist/extension/index.js +156 -117
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +3 -10
- package/dist/extension/recorder.js +13 -22
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/step-message.d.ts +7 -2
- package/dist/extension/step-message.js +94 -2
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +6 -0
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/rpc-executor.d.ts +3 -0
- package/dist/host/rpc-executor.js +2 -0
- package/dist/host/rpc-executor.js.map +1 -1
- package/dist/host/runner.d.ts +3 -6
- package/dist/host/runner.js +83 -32
- package/dist/host/runner.js.map +1 -1
- package/dist/render/graph-render.js +14 -8
- package/dist/render/graph-render.js.map +1 -1
- package/dist/state/database.d.ts +35 -0
- package/dist/state/database.js +287 -0
- package/dist/state/database.js.map +1 -0
- package/dist/state/index.d.ts +4 -0
- package/dist/state/index.js +5 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/json.d.ts +6 -0
- package/dist/state/json.js +38 -0
- package/dist/state/json.js.map +1 -0
- package/dist/state/mutation.d.ts +57 -0
- package/dist/state/mutation.js +255 -0
- package/dist/state/mutation.js.map +1 -0
- package/dist/state/schema.d.ts +6 -0
- package/dist/state/schema.js +473 -0
- package/dist/state/schema.js.map +1 -0
- package/dist/viewer/cli.d.ts +2 -2
- package/dist/viewer/cli.js +142 -122
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.d.ts +4 -4
- package/dist/viewer/render.js +2 -17
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +1 -1
- package/dist/viewer/session-reducer.js +1 -1
- package/dist/viewer/tui.d.ts +3 -3
- package/dist/viewer/tui.js +15 -13
- package/dist/viewer/tui.js.map +1 -1
- package/dist/viewer/watch.d.ts +2 -5
- package/dist/viewer/watch.js +13 -13
- package/dist/viewer/watch.js.map +1 -1
- package/dist/workflows/composition.js +12 -2
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +6 -2
- package/dist/workflows/definition.js +17 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -6
- package/dist/workflows/engine.js +183 -103
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +3 -3
- package/dist/workflows/errors.js +3 -3
- package/dist/workflows/human-decision.d.ts +29 -4
- package/dist/workflows/human-decision.js +547 -283
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +4 -5
- package/dist/workflows/index.js +3 -4
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +17 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +83 -103
- package/dist/workflows/store.js +902 -951
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +55 -81
- package/docs/CONTROLLERS.md +10 -8
- package/docs/DEFERRED_TURNS.md +2 -2
- package/docs/DESIGN_PHILOSOPHY.md +4 -2
- package/docs/HUMAN_DECISIONS.md +13 -14
- package/docs/MONITOR.md +179 -231
- package/docs/SQLITE_STATE.md +219 -0
- package/docs/WORKFLOW_COMPOSITION.md +27 -19
- package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
- package/docs/WORKFLOW_UPDATES.md +10 -9
- package/docs/development.md +9 -9
- package/docs/live-replay-protocol.md +28 -37
- package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
- package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +1 -1
- package/docs/plans/2026-08-21-sanity-check-plan.md +30 -16
- package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
- package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
- package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
- package/docs/plans/2026-08-24-change-scoped-verification-plan.md +419 -0
- package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
- package/docs/plans/session-event-replay-implementation-plan.md +1 -1
- package/docs/plans/tui-viewer-implementation-plan.md +1 -1
- package/docs/session-event-journal.md +63 -434
- package/docs/tui-viewer.md +13 -15
- package/docs/workflows.md +111 -62
- package/examples/workflows/plain-summary.workflow.ts +1 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/plugins/herdr/viewer.mjs +1 -11
- package/skills/autodoc/SKILL.md +7 -0
- package/skills/autoimplement/SKILL.md +4 -0
- package/skills/autoplan/SKILL.md +5 -4
- package/skills/monitor/SKILL.md +91 -163
- package/skills/pi-workflows/SKILL.md +7 -7
- package/skills/sanity-check/SKILL.md +1 -1
- package/src/builtins/autodoc.workflow.ts +184 -33
- package/src/builtins/autoimplement-command-batches.ts +39 -33
- package/src/builtins/autoimplement.workflow.ts +483 -175
- package/src/builtins/autoplan.workflow.ts +290 -40
- package/src/builtins/catalog.ts +7 -5
- package/src/builtins/change-verification.workflow.ts +1143 -0
- package/src/builtins/index.ts +7 -1
- package/src/builtins/monitor.workflow.ts +667 -250
- package/src/builtins/plain-summary.workflow.ts +185 -0
- package/src/builtins/plan-change.workflow.ts +35 -0
- package/src/builtins/sanity-check.workflow.ts +62 -7
- package/src/builtins/workspace-preparation.workflow.ts +668 -0
- package/src/controllers/effects.ts +7 -0
- package/src/controllers/index.ts +0 -6
- package/src/controllers/manager.ts +72 -32
- package/src/controllers/sqlite.ts +2420 -1948
- package/src/controllers/store.ts +17 -38
- package/src/controllers/types.ts +3 -0
- package/src/controllers/workflow-engine-scheduler.ts +4 -8
- package/src/controllers/workflows.ts +45 -21
- package/src/extension/controller-host.ts +1 -2
- package/src/extension/decision-channels.ts +427 -144
- package/src/extension/executor.ts +106 -6
- package/src/extension/herdr-viewer.ts +0 -5
- package/src/extension/index.ts +180 -132
- package/src/extension/recorder.ts +13 -22
- package/src/extension/step-message.ts +106 -4
- package/src/extension/widget.ts +8 -0
- package/src/host/rpc-executor.ts +4 -0
- package/src/host/runner.ts +93 -41
- package/src/render/graph-render.ts +11 -4
- package/src/state/database.ts +375 -0
- package/src/state/index.ts +29 -0
- package/src/state/json.ts +42 -0
- package/src/state/mutation.ts +435 -0
- package/src/state/schema.ts +475 -0
- package/src/viewer/cli.ts +151 -144
- package/src/viewer/render.ts +12 -24
- package/src/viewer/session-reducer.ts +1 -1
- package/src/viewer/tui.ts +18 -17
- package/src/viewer/watch.ts +13 -17
- package/src/workflows/composition.ts +12 -3
- package/src/workflows/definition.ts +36 -3
- package/src/workflows/engine.ts +212 -116
- package/src/workflows/errors.ts +3 -3
- package/src/workflows/human-decision.ts +795 -340
- package/src/workflows/index.ts +15 -19
- package/src/workflows/schema.ts +19 -1
- package/src/workflows/store.ts +1295 -1163
- package/src/workflows/types.ts +61 -81
- package/dist/workflows/artifacts.d.ts +0 -40
- package/dist/workflows/artifacts.js +0 -155
- package/dist/workflows/artifacts.js.map +0 -1
- package/dist/workflows/migrate-sources.d.ts +0 -42
- package/dist/workflows/migrate-sources.js +0 -133
- package/dist/workflows/migrate-sources.js.map +0 -1
- package/docs/run-bundles.md +0 -481
- package/src/workflows/artifacts.ts +0 -188
- package/src/workflows/migrate-sources.ts +0 -178
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ControllerManager, SqliteControllerStore,
|
|
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(
|
|
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":"AAAA,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,
|
|
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
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
217
|
-
this.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
.
|
|
274
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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.
|
|
285
|
-
.prepare(
|
|
286
|
-
|
|
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.
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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.
|
|
298
|
-
.
|
|
299
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
337
|
-
.
|
|
338
|
-
|
|
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.
|
|
342
|
-
|
|
343
|
-
.
|
|
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
|
-
|
|
347
|
-
.prepare(
|
|
348
|
-
|
|
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
|
-
|
|
352
|
-
.
|
|
353
|
-
.
|
|
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
|
-
|
|
357
|
-
.
|
|
358
|
-
.
|
|
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(
|
|
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
|
|
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
|
|
920
|
+
response = await request(`${apiBase}/bot${encodeURIComponent(token)}/getMe`, {
|
|
728
921
|
method: "POST",
|
|
729
922
|
headers: { "content-type": "application/json" },
|
|
730
923
|
body: "{}",
|