openclaw-lark-multi-agent 1.5.1 → 1.7.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.
@@ -0,0 +1,10 @@
1
+ /** An authoritative terminal result of one generation-pinned, admitted executor run.
2
+ * Admission, terminal result and platform delivery receipts are separate facts. */
3
+ export type GoExecutorResult = {
4
+ sessionKey: string;
5
+ sessionId: string;
6
+ runId: string;
7
+ disposition: "visible" | "silent" | "empty" | "error";
8
+ text: string;
9
+ error?: string;
10
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,32 @@
1
+ /** Pure adapter contract. Independent queries are observations, not a global atomic snapshot. */
2
+ export type GoRuntimeSnapshot = {
3
+ sessionKey: string;
4
+ sessionId: string;
5
+ goalId: string;
6
+ goalRevision: number;
7
+ executorRunIds: string[];
8
+ checkerRunId: string;
9
+ complete: boolean;
10
+ archived: boolean;
11
+ independent?: boolean;
12
+ queryAvailable?: boolean;
13
+ running?: boolean;
14
+ atomicSnapshot?: boolean;
15
+ queuedInputs: number;
16
+ recoverableInputs: number;
17
+ pendingDeliveries: number;
18
+ tasks: Array<{
19
+ taskId: string;
20
+ runId: string;
21
+ parentTaskId: string | null;
22
+ state: string;
23
+ deliveryState: string;
24
+ }>;
25
+ };
26
+ export declare function goSnapshotBlocked(s: GoRuntimeSnapshot, expected: {
27
+ sessionKey: string;
28
+ sessionId: string;
29
+ goalId: string;
30
+ goalRevision: number;
31
+ executorRunIds: string[];
32
+ }): string | null;
@@ -0,0 +1,56 @@
1
+ export function goSnapshotBlocked(s, expected) {
2
+ if (!s.independent && (!s.checkerRunId || !expected.executorRunIds.length || expected.executorRunIds.some(id => !id)))
3
+ return "missing run identity";
4
+ if (s.independent ? !s.queryAvailable : !s.complete)
5
+ return "incomplete snapshot";
6
+ if (s.independent && s.queuedInputs !== 0)
7
+ return "pending or unknown input/delivery";
8
+ if (s.independent && s.running !== false)
9
+ return "active or unknown session";
10
+ if (s.archived || s.sessionKey !== expected.sessionKey || s.sessionId !== expected.sessionId || (!s.independent && (s.goalId !== expected.goalId || s.goalRevision !== expected.goalRevision)))
11
+ return "identity/version changed";
12
+ if (!s.independent && [s.queuedInputs, s.recoverableInputs, s.pendingDeliveries].some(n => !Number.isSafeInteger(n) || n !== 0))
13
+ return "pending or unknown input/delivery";
14
+ if (!s.independent && (new Set(s.executorRunIds).size !== s.executorRunIds.length || s.executorRunIds.length !== expected.executorRunIds.length || expected.executorRunIds.some(id => !s.executorRunIds.includes(id))))
15
+ return "run association changed";
16
+ const byId = new Map(s.tasks.map(t => [t.taskId, t]));
17
+ if (byId.size !== s.tasks.length)
18
+ return "duplicate task identity";
19
+ for (const task of s.tasks) {
20
+ const visited = new Set();
21
+ let cursor = task;
22
+ while (cursor) {
23
+ if (visited.has(cursor.taskId))
24
+ return "cyclic task ancestry";
25
+ visited.add(cursor.taskId);
26
+ cursor = cursor.parentTaskId ? byId.get(cursor.parentTaskId) : undefined;
27
+ }
28
+ }
29
+ const related = new Set(s.tasks.filter(t => expected.executorRunIds.includes(t.runId)).map(t => t.taskId));
30
+ // Fixed point, not ordering/prefix inference. Exclude no task just because its text says checker.
31
+ let changed = true;
32
+ while (changed) {
33
+ changed = false;
34
+ for (const t of s.tasks)
35
+ if (t.parentTaskId && related.has(t.parentTaskId) && !related.has(t.taskId)) {
36
+ related.add(t.taskId);
37
+ changed = true;
38
+ }
39
+ }
40
+ for (const t of s.tasks) {
41
+ if (t.parentTaskId && !byId.has(t.parentTaskId))
42
+ return "missing ancestor";
43
+ if (!related.has(t.taskId) && !s.independent)
44
+ continue;
45
+ if (t.runId === s.checkerRunId)
46
+ return "checker/executor association conflict";
47
+ if (!["completed", "failed", "cancelled"].includes(t.state))
48
+ return "active or unknown task";
49
+ if (!["delivered", "not_required"].includes(t.deliveryState))
50
+ return "pending or unknown task return";
51
+ }
52
+ // Each root run needs explicit terminal state evidence; absence is never idle.
53
+ if (!s.independent && expected.executorRunIds.some(id => !s.tasks.some(t => t.runId === id)))
54
+ return "missing root run";
55
+ return null;
56
+ }
@@ -74,9 +74,114 @@ export interface PendingErrorNotice {
74
74
  hasObservedContinuingTask?: boolean;
75
75
  recoveryProbeUsed?: boolean;
76
76
  }
77
+ export type GoJobRow = {
78
+ id: number;
79
+ botName: string;
80
+ chatId: string;
81
+ goal: string;
82
+ acceptance: string;
83
+ state: "STARTING" | "WAITING_WORK" | "CHECKING" | "FOLLOWUP_QUEUED" | "COMPLETING" | "COMPLETED" | "PAUSED" | "BLOCKED" | "STOPPED";
84
+ executorSessionKey: string;
85
+ executorSessionId: string;
86
+ checkerSessionKey: string;
87
+ checkerModel: string;
88
+ revision: number;
89
+ continueCount: number;
90
+ waitStreak: number;
91
+ stagnantCount: number;
92
+ parseRetries: number;
93
+ evidenceWatermark: number;
94
+ lastMissingJson: string;
95
+ lastVerdict: string;
96
+ lastVerdictSummary: string;
97
+ pauseReason: string;
98
+ createdAt: number;
99
+ updatedAt: number;
100
+ };
77
101
  export declare class MessageStore {
78
102
  private db;
79
103
  constructor(dbPath?: string);
104
+ /** Go / Go-Checker supervision state (design: artifacts/lma-go-checker-design-20260917 §10). */
105
+ private initGoTables;
106
+ private ensureGoIntegrationSchema;
107
+ bindGoNativeIdentity(goId: number, identity: {
108
+ nativeGoalId?: string;
109
+ executorSessionId: string;
110
+ checkerSessionId: string;
111
+ checkerRunId?: string;
112
+ executorRunIds: string[];
113
+ goalUpdatedAt?: number;
114
+ goalRevision?: number | null;
115
+ }): void;
116
+ getGoNativeIdentity(goId: number): any;
117
+ setGoCriteria(goId: number, criteria: Array<{
118
+ id: string;
119
+ text: string;
120
+ }>): void;
121
+ getGoCriteria(goId: number): Array<{
122
+ id: string;
123
+ text: string;
124
+ }>;
125
+ getGoDeliveryOperation(goId: number, key: string): any;
126
+ recordGoDeliveryOperation(goId: number, key: string, state: "attempted" | "accepted" | "unknown" | "rejected", response?: unknown): void;
127
+ /** Durable initial-execution intent, separate from the at-most-once send ledger. */
128
+ createGoBootstrap(goId: number): void;
129
+ getGoBootstrap(goId: number): string | null;
130
+ finishGoBootstrap(goId: number, state: "done" | "superseded"): void;
131
+ isGoActionComplete(goId: number, actionKey: string): boolean;
132
+ listGoSends(goId: number): Array<{
133
+ actionKey: string;
134
+ idempotencyKey: string;
135
+ sessionKey: string;
136
+ sessionId: string;
137
+ }>;
138
+ recordGoRunResult(goId: number, actionKey: string, result: {
139
+ state: "complete" | "unknown";
140
+ text?: string;
141
+ runId: string;
142
+ executorResult?: import("./go-execution.js").GoExecutorResult;
143
+ }): void;
144
+ getGoRunResult(goId: number, actionKey: string): any;
145
+ listUnknownGoRunResults(goId: number): Array<{
146
+ actionKey: string;
147
+ runId: string;
148
+ sessionKey: string;
149
+ sessionId: string;
150
+ }>;
151
+ hasUnknownGoRunResults(goId: number): boolean;
152
+ associateGoAdmittedRun(goId: number, runId: string, executor: boolean): void;
153
+ /** Durable delivery ownership includes completed/stopped jobs, not only active ones. */
154
+ ownsGoExecutorRun(botName: string, chatId: string, sessionKey: string, sessionId: string, runId: string): boolean;
155
+ recordGoVisibleEvidence(goId: number, sourceKey: string, payload: {
156
+ sessionId: string;
157
+ runId: string;
158
+ kind: string;
159
+ text: string;
160
+ attachments: unknown[];
161
+ }): boolean;
162
+ recordGoDeliveryReceipt(goId: number, sourceKey: string, receipt: unknown): void;
163
+ listGoVisibleEvidence(goId: number): any[];
164
+ bindGoSendIdentity(goId: number, actionKey: string, identity: {
165
+ sessionKey: string;
166
+ sessionId: string;
167
+ }): void;
168
+ listUnresolvedGoSends(goId: number): Array<{
169
+ actionKey: string;
170
+ idempotencyKey: string;
171
+ sessionKey: string;
172
+ sessionId: string;
173
+ }>;
174
+ recordGoActionAttempt(goId: number, actionKey: string): {
175
+ state: string;
176
+ idempotency_key: string;
177
+ receipt_json: string;
178
+ } | undefined;
179
+ getGoActionLifecycle(goId: number, actionKey: string): {
180
+ state: string;
181
+ idempotency_key: string;
182
+ receipt_json: string;
183
+ } | undefined;
184
+ recordGoActionOutcome(goId: number, actionKey: string, state: 'accepted' | 'unknown', receipt?: unknown): void;
80
185
  observeRunError(botName: string, chatId: string, provenance: ErrorProvenance): PendingErrorNotice;
81
186
  getErrorNotice(botName: string, chatId: string, noticeId: string): PendingErrorNotice | undefined;
82
187
  saveErrorNotice(record: PendingErrorNotice): void;
@@ -210,5 +315,42 @@ export declare class MessageStore {
210
315
  */
211
316
  markBotProcessed(botName: string, messageId: string): void;
212
317
  tryMarkBotProcessed(botName: string, messageId: string): boolean;
318
+ createGoJob(j: {
319
+ botName: string;
320
+ chatId: string;
321
+ goal: string;
322
+ acceptance: string;
323
+ executorSessionKey: string;
324
+ executorSessionId: string;
325
+ checkerSessionKey: string;
326
+ checkerModel: string;
327
+ state?: "STARTING" | "WAITING_WORK";
328
+ }): number;
329
+ private rowToGoJob;
330
+ getActiveGoJob(botName: string, chatId: string): GoJobRow | null;
331
+ getGoJobById(id: number): GoJobRow | null;
332
+ listActiveGoJobs(botName?: string): GoJobRow[];
333
+ /** CAS update: only applies when the current revision matches. Returns success. */
334
+ updateGoJobCas(id: number, expectedRevision: number, patch: Partial<Record<"state" | "pauseReason" | "continueCount" | "waitStreak" | "stagnantCount" | "parseRetries" | "evidenceWatermark" | "lastMissingJson" | "lastVerdict" | "lastVerdictSummary" | "executorSessionId", unknown>>): boolean;
335
+ /** Insert evidence; returns the new event seq, or -1 when deduplicated. */
336
+ insertGoEvent(goId: number, sourceKey: string, kind: string, content: string): number;
337
+ listGoEventsSince(goId: number, afterSeq: number): Array<{
338
+ seq: number;
339
+ kind: string;
340
+ sourceKey: string;
341
+ content: string;
342
+ delivered: number;
343
+ }>;
344
+ maxGoEventSeq(goId: number): number;
345
+ insertGoCheck(goId: number, checkId: string, evidenceMaxSeq: number): boolean;
346
+ finishGoCheck(goId: number, checkId: string, state: string, action: string, summary: string, raw: string): void;
347
+ /** Idempotent action claim: returns true when newly claimed (caller performs the action), false when it already existed. */
348
+ claimGoAction(goId: number, actionKey: string, kind: string, payload: string): boolean;
349
+ hasUnresolvedGoActions(goId: number): boolean;
350
+ completeGoAction(goId: number, actionKey: string, receipt: string): void;
351
+ setGoModelPref(botName: string, chatId: string, model: string | null): void;
352
+ getGoModelPref(botName: string, chatId: string): string | null;
353
+ private closeObservers;
354
+ onClose(callback: () => void): () => boolean;
213
355
  close(): void;
214
356
  }
@@ -11,6 +11,207 @@ export class MessageStore {
11
11
  this.db = new Database(path);
12
12
  this.db.pragma("journal_mode = WAL");
13
13
  this.init();
14
+ this.initGoTables();
15
+ }
16
+ /** Go / Go-Checker supervision state (design: artifacts/lma-go-checker-design-20260917 §10). */
17
+ initGoTables() {
18
+ this.db.exec(`
19
+ CREATE TABLE IF NOT EXISTS go_jobs (
20
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
21
+ bot_name TEXT NOT NULL,
22
+ chat_id TEXT NOT NULL,
23
+ goal TEXT NOT NULL,
24
+ acceptance TEXT NOT NULL DEFAULT '',
25
+ state TEXT NOT NULL DEFAULT 'WAITING_WORK',
26
+ executor_session_key TEXT NOT NULL,
27
+ executor_session_id TEXT NOT NULL DEFAULT '',
28
+ checker_session_key TEXT NOT NULL,
29
+ checker_model TEXT NOT NULL,
30
+ revision INTEGER NOT NULL DEFAULT 1,
31
+ continue_count INTEGER NOT NULL DEFAULT 0,
32
+ wait_streak INTEGER NOT NULL DEFAULT 0,
33
+ stagnant_count INTEGER NOT NULL DEFAULT 0,
34
+ parse_retries INTEGER NOT NULL DEFAULT 0,
35
+ evidence_watermark INTEGER NOT NULL DEFAULT 0,
36
+ last_missing_json TEXT NOT NULL DEFAULT '[]',
37
+ last_verdict TEXT NOT NULL DEFAULT '',
38
+ last_verdict_summary TEXT NOT NULL DEFAULT '',
39
+ pause_reason TEXT NOT NULL DEFAULT '',
40
+ created_at INTEGER NOT NULL,
41
+ updated_at INTEGER NOT NULL
42
+ );
43
+ CREATE INDEX IF NOT EXISTS idx_go_jobs_active ON go_jobs(bot_name, chat_id, state);
44
+ CREATE TABLE IF NOT EXISTS go_events (
45
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
46
+ go_id INTEGER NOT NULL,
47
+ source_key TEXT NOT NULL,
48
+ kind TEXT NOT NULL,
49
+ content TEXT NOT NULL,
50
+ delivered INTEGER NOT NULL DEFAULT 1,
51
+ created_at INTEGER NOT NULL,
52
+ UNIQUE(go_id, source_key)
53
+ );
54
+ CREATE TABLE IF NOT EXISTS go_checks (
55
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
56
+ go_id INTEGER NOT NULL,
57
+ check_id TEXT NOT NULL,
58
+ evidence_max_seq INTEGER NOT NULL,
59
+ state TEXT NOT NULL DEFAULT 'pending',
60
+ action TEXT NOT NULL DEFAULT '',
61
+ summary TEXT NOT NULL DEFAULT '',
62
+ raw TEXT NOT NULL DEFAULT '',
63
+ created_at INTEGER NOT NULL,
64
+ UNIQUE(go_id, check_id)
65
+ );
66
+ CREATE TABLE IF NOT EXISTS go_actions (
67
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
68
+ go_id INTEGER NOT NULL,
69
+ action_key TEXT NOT NULL,
70
+ kind TEXT NOT NULL,
71
+ payload TEXT NOT NULL DEFAULT '',
72
+ receipt TEXT NOT NULL DEFAULT '',
73
+ done INTEGER NOT NULL DEFAULT 0,
74
+ created_at INTEGER NOT NULL,
75
+ UNIQUE(go_id, action_key)
76
+ );
77
+ CREATE TABLE IF NOT EXISTS go_model_prefs (
78
+ bot_name TEXT NOT NULL,
79
+ chat_id TEXT NOT NULL,
80
+ model TEXT,
81
+ updated_at INTEGER NOT NULL,
82
+ PRIMARY KEY(bot_name, chat_id)
83
+ );
84
+ `);
85
+ }
86
+ ensureGoIntegrationSchema() {
87
+ this.db.exec(`CREATE TABLE IF NOT EXISTS go_native_bindings (
88
+ go_id INTEGER PRIMARY KEY, identity_json TEXT NOT NULL);
89
+ CREATE TABLE IF NOT EXISTS go_action_lifecycle (
90
+ go_id INTEGER NOT NULL, action_key TEXT NOT NULL, state TEXT NOT NULL,
91
+ idempotency_key TEXT NOT NULL, receipt_json TEXT NOT NULL DEFAULT '',
92
+ PRIMARY KEY(go_id, action_key));
93
+ CREATE TABLE IF NOT EXISTS go_visible_evidence (
94
+ go_id INTEGER NOT NULL, source_key TEXT NOT NULL, payload_json TEXT NOT NULL,
95
+ receipt_json TEXT NOT NULL DEFAULT '', PRIMARY KEY(go_id, source_key));`);
96
+ }
97
+ bindGoNativeIdentity(goId, identity) {
98
+ this.ensureGoIntegrationSchema();
99
+ this.db.prepare('INSERT INTO go_native_bindings VALUES (?, ?)').run(goId, JSON.stringify(identity));
100
+ }
101
+ getGoNativeIdentity(goId) {
102
+ this.ensureGoIntegrationSchema();
103
+ const row = this.db.prepare('SELECT identity_json FROM go_native_bindings WHERE go_id=?').get(goId);
104
+ return row ? JSON.parse(row.identity_json) : null;
105
+ }
106
+ setGoCriteria(goId, criteria) {
107
+ this.db.exec("CREATE TABLE IF NOT EXISTS go_criteria(go_id INTEGER PRIMARY KEY,criteria_json TEXT NOT NULL)");
108
+ this.db.prepare("INSERT OR REPLACE INTO go_criteria VALUES (?,?)").run(goId, JSON.stringify(criteria));
109
+ }
110
+ getGoCriteria(goId) {
111
+ this.db.exec("CREATE TABLE IF NOT EXISTS go_criteria(go_id INTEGER PRIMARY KEY,criteria_json TEXT NOT NULL)");
112
+ const row = this.db.prepare("SELECT criteria_json FROM go_criteria WHERE go_id=?").get(goId);
113
+ return row ? JSON.parse(row.criteria_json) : [];
114
+ }
115
+ getGoDeliveryOperation(goId, key) {
116
+ this.db.exec("CREATE TABLE IF NOT EXISTS go_delivery_operations(go_id INTEGER,operation_key TEXT,state TEXT,response_json TEXT,PRIMARY KEY(go_id,operation_key))");
117
+ return this.db.prepare("SELECT * FROM go_delivery_operations WHERE go_id=? AND operation_key=?").get(goId, key);
118
+ }
119
+ recordGoDeliveryOperation(goId, key, state, response) {
120
+ this.getGoDeliveryOperation(goId, key);
121
+ this.db.prepare("INSERT INTO go_delivery_operations VALUES (?,?,?,?) ON CONFLICT(go_id,operation_key) DO UPDATE SET state=excluded.state,response_json=excluded.response_json WHERE go_delivery_operations.state!='accepted'").run(goId, key, state, response === undefined ? "" : JSON.stringify(response));
122
+ }
123
+ /** Durable initial-execution intent, separate from the at-most-once send ledger. */
124
+ createGoBootstrap(goId) {
125
+ this.db.exec("CREATE TABLE IF NOT EXISTS go_bootstrap(go_id INTEGER PRIMARY KEY,state TEXT NOT NULL)");
126
+ this.db.prepare("INSERT OR IGNORE INTO go_bootstrap VALUES (?,'pending')").run(goId);
127
+ }
128
+ getGoBootstrap(goId) {
129
+ this.db.exec("CREATE TABLE IF NOT EXISTS go_bootstrap(go_id INTEGER PRIMARY KEY,state TEXT NOT NULL)");
130
+ return this.db.prepare("SELECT state FROM go_bootstrap WHERE go_id=?").get(goId)?.state ?? null;
131
+ }
132
+ finishGoBootstrap(goId, state) {
133
+ this.getGoBootstrap(goId);
134
+ this.db.prepare("UPDATE go_bootstrap SET state=? WHERE go_id=?").run(state, goId);
135
+ }
136
+ isGoActionComplete(goId, actionKey) {
137
+ return this.db.prepare("SELECT done FROM go_actions WHERE go_id=? AND action_key=?").get(goId, actionKey)?.done === 1;
138
+ }
139
+ listGoSends(goId) {
140
+ this.listUnresolvedGoSends(goId);
141
+ return this.db.prepare("SELECT s.*,l.idempotency_key FROM go_send_identities s JOIN go_action_lifecycle l ON s.go_id=l.go_id AND s.action_key=l.action_key WHERE s.go_id=?").all(goId)
142
+ .map(r => ({ actionKey: r.action_key, idempotencyKey: r.idempotency_key, ...JSON.parse(r.identity_json) }));
143
+ }
144
+ recordGoRunResult(goId, actionKey, result) {
145
+ this.db.exec("CREATE TABLE IF NOT EXISTS go_run_results(go_id INTEGER,action_key TEXT,result_json TEXT,PRIMARY KEY(go_id,action_key))");
146
+ this.db.prepare("INSERT OR REPLACE INTO go_run_results VALUES (?,?,?)").run(goId, actionKey, JSON.stringify(result));
147
+ }
148
+ getGoRunResult(goId, actionKey) {
149
+ this.db.exec("CREATE TABLE IF NOT EXISTS go_run_results(go_id INTEGER,action_key TEXT,result_json TEXT,PRIMARY KEY(go_id,action_key))");
150
+ const row = this.db.prepare("SELECT result_json FROM go_run_results WHERE go_id=? AND action_key=?").get(goId, actionKey);
151
+ return row ? JSON.parse(row.result_json) : null;
152
+ }
153
+ listUnknownGoRunResults(goId) {
154
+ this.getGoRunResult(goId, "");
155
+ this.listUnresolvedGoSends(goId);
156
+ return this.db.prepare("SELECT r.action_key,r.result_json,s.identity_json FROM go_run_results r JOIN go_send_identities s ON r.go_id=s.go_id AND r.action_key=s.action_key WHERE r.go_id=?").all(goId).flatMap(r => {
157
+ const result = JSON.parse(r.result_json);
158
+ return result.state === "unknown" ? [{ actionKey: r.action_key, runId: result.runId, ...JSON.parse(r.identity_json) }] : [];
159
+ });
160
+ }
161
+ hasUnknownGoRunResults(goId) {
162
+ this.getGoRunResult(goId, "");
163
+ return this.db.prepare("SELECT result_json FROM go_run_results WHERE go_id=?").all(goId).some(r => JSON.parse(r.result_json).state === "unknown");
164
+ }
165
+ associateGoAdmittedRun(goId, runId, executor) {
166
+ const identity = this.getGoNativeIdentity(goId);
167
+ if (!identity)
168
+ throw new Error("Missing independent identity");
169
+ if (executor)
170
+ identity.executorRunIds = [...new Set([...identity.executorRunIds, runId])];
171
+ else
172
+ identity.checkerRunId = runId;
173
+ this.db.prepare("UPDATE go_native_bindings SET identity_json=? WHERE go_id=?").run(JSON.stringify(identity), goId);
174
+ }
175
+ /** Durable delivery ownership includes completed/stopped jobs, not only active ones. */
176
+ ownsGoExecutorRun(botName, chatId, sessionKey, sessionId, runId) {
177
+ this.ensureGoIntegrationSchema();
178
+ return this.db.prepare("SELECT j.executor_session_key,j.executor_session_id,b.identity_json FROM go_jobs j JOIN go_native_bindings b ON j.id=b.go_id WHERE j.bot_name=? AND j.chat_id=? AND j.executor_session_id=?").all(botName, chatId, sessionId)
179
+ .some(row => [row.executor_session_key, `agent:main:${row.executor_session_key}`].includes(sessionKey) && JSON.parse(row.identity_json).executorRunIds.includes(runId));
180
+ }
181
+ recordGoVisibleEvidence(goId, sourceKey, payload) {
182
+ this.ensureGoIntegrationSchema();
183
+ return this.db.prepare('INSERT OR IGNORE INTO go_visible_evidence (go_id,source_key,payload_json) VALUES (?,?,?)').run(goId, sourceKey, JSON.stringify(payload)).changes === 1;
184
+ }
185
+ recordGoDeliveryReceipt(goId, sourceKey, receipt) {
186
+ this.ensureGoIntegrationSchema();
187
+ this.db.prepare('UPDATE go_visible_evidence SET receipt_json=? WHERE go_id=? AND source_key=?').run(JSON.stringify(receipt), goId, sourceKey);
188
+ }
189
+ listGoVisibleEvidence(goId) {
190
+ this.ensureGoIntegrationSchema();
191
+ return this.db.prepare('SELECT * FROM go_visible_evidence WHERE go_id=? ORDER BY rowid').all(goId)
192
+ .map(r => ({ sourceKey: r.source_key, ...JSON.parse(r.payload_json), receipt: r.receipt_json ? JSON.parse(r.receipt_json) : null }));
193
+ }
194
+ bindGoSendIdentity(goId, actionKey, identity) {
195
+ this.db.exec("CREATE TABLE IF NOT EXISTS go_send_identities(go_id INTEGER,action_key TEXT,identity_json TEXT,PRIMARY KEY(go_id,action_key))");
196
+ this.db.prepare("INSERT OR IGNORE INTO go_send_identities VALUES (?,?,?)").run(goId, actionKey, JSON.stringify(identity));
197
+ }
198
+ listUnresolvedGoSends(goId) {
199
+ this.db.exec("CREATE TABLE IF NOT EXISTS go_send_identities(go_id INTEGER,action_key TEXT,identity_json TEXT,PRIMARY KEY(go_id,action_key))");
200
+ return this.db.prepare("SELECT s.*,l.idempotency_key FROM go_send_identities s JOIN go_action_lifecycle l ON s.go_id=l.go_id AND s.action_key=l.action_key WHERE s.go_id=? AND l.state!='accepted'").all(goId).map(r => ({ actionKey: r.action_key, idempotencyKey: r.idempotency_key, ...JSON.parse(r.identity_json) }));
201
+ }
202
+ recordGoActionAttempt(goId, actionKey) {
203
+ this.ensureGoIntegrationSchema();
204
+ const idempotencyKey = `lma-go:${goId}:${actionKey}`;
205
+ this.db.prepare("INSERT OR IGNORE INTO go_action_lifecycle (go_id,action_key,state,idempotency_key) VALUES (?,?,'attempted',?)").run(goId, actionKey, idempotencyKey);
206
+ return this.getGoActionLifecycle(goId, actionKey);
207
+ }
208
+ getGoActionLifecycle(goId, actionKey) {
209
+ this.ensureGoIntegrationSchema();
210
+ return this.db.prepare('SELECT * FROM go_action_lifecycle WHERE go_id=? AND action_key=?').get(goId, actionKey);
211
+ }
212
+ recordGoActionOutcome(goId, actionKey, state, receipt) {
213
+ this.ensureGoIntegrationSchema();
214
+ this.db.prepare('UPDATE go_action_lifecycle SET state=?,receipt_json=? WHERE go_id=? AND action_key=? AND state!=?').run(state, receipt === undefined ? '' : JSON.stringify(receipt), goId, actionKey, 'accepted');
14
215
  }
15
216
  observeRunError(botName, chatId, provenance) {
16
217
  const noticeId = errorIdentity(provenance.sessionKey, provenance.runId);
@@ -1123,7 +1324,116 @@ export class MessageStore {
1123
1324
  const result = this.db.prepare(`INSERT OR IGNORE INTO processed_events (bot_name, message_id) VALUES (?, ?)`).run(botName, messageId);
1124
1325
  return result.changes === 1;
1125
1326
  }
1327
+ // --- Go / Go-Checker supervision (DESIGN.md §10) ---
1328
+ createGoJob(j) {
1329
+ const now = Date.now();
1330
+ const r = this.db.prepare(`INSERT INTO go_jobs
1331
+ (bot_name, chat_id, goal, acceptance, state, executor_session_key, executor_session_id, checker_session_key, checker_model, revision, created_at, updated_at)
1332
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)`)
1333
+ .run(j.botName, j.chatId, j.goal, j.acceptance, j.state || "WAITING_WORK", j.executorSessionKey, j.executorSessionId, j.checkerSessionKey, j.checkerModel, now, now);
1334
+ return Number(r.lastInsertRowid);
1335
+ }
1336
+ rowToGoJob(row) {
1337
+ return {
1338
+ id: row.id, botName: row.bot_name, chatId: row.chat_id, goal: row.goal, acceptance: row.acceptance,
1339
+ state: row.state, executorSessionKey: row.executor_session_key, executorSessionId: row.executor_session_id,
1340
+ checkerSessionKey: row.checker_session_key, checkerModel: row.checker_model, revision: row.revision,
1341
+ continueCount: row.continue_count, waitStreak: row.wait_streak, stagnantCount: row.stagnant_count,
1342
+ parseRetries: row.parse_retries, evidenceWatermark: row.evidence_watermark,
1343
+ lastMissingJson: row.last_missing_json, lastVerdict: row.last_verdict, lastVerdictSummary: row.last_verdict_summary,
1344
+ pauseReason: row.pause_reason, createdAt: row.created_at, updatedAt: row.updated_at,
1345
+ };
1346
+ }
1347
+ getActiveGoJob(botName, chatId) {
1348
+ const row = this.db.prepare(`SELECT * FROM go_jobs WHERE bot_name=? AND chat_id=? AND state NOT IN ('COMPLETED','STOPPED') ORDER BY id DESC LIMIT 1`).get(botName, chatId);
1349
+ return row ? this.rowToGoJob(row) : null;
1350
+ }
1351
+ getGoJobById(id) {
1352
+ const row = this.db.prepare(`SELECT * FROM go_jobs WHERE id=?`).get(id);
1353
+ return row ? this.rowToGoJob(row) : null;
1354
+ }
1355
+ listActiveGoJobs(botName) {
1356
+ const rows = botName
1357
+ ? this.db.prepare(`SELECT * FROM go_jobs WHERE bot_name=? AND state NOT IN ('COMPLETED','STOPPED')`).all(botName)
1358
+ : this.db.prepare(`SELECT * FROM go_jobs WHERE state NOT IN ('COMPLETED','STOPPED')`).all();
1359
+ return rows.map((r) => this.rowToGoJob(r));
1360
+ }
1361
+ /** CAS update: only applies when the current revision matches. Returns success. */
1362
+ updateGoJobCas(id, expectedRevision, patch) {
1363
+ const sets = [];
1364
+ const vals = [];
1365
+ const colMap = {
1366
+ state: "state", pauseReason: "pause_reason", continueCount: "continue_count", waitStreak: "wait_streak",
1367
+ stagnantCount: "stagnant_count", parseRetries: "parse_retries", evidenceWatermark: "evidence_watermark",
1368
+ lastMissingJson: "last_missing_json", lastVerdict: "last_verdict", lastVerdictSummary: "last_verdict_summary",
1369
+ executorSessionId: "executor_session_id",
1370
+ };
1371
+ for (const [k, v] of Object.entries(patch)) {
1372
+ const col = colMap[k];
1373
+ if (!col)
1374
+ continue;
1375
+ sets.push(`${col}=?`);
1376
+ vals.push(v);
1377
+ }
1378
+ if (sets.length === 0)
1379
+ return false;
1380
+ sets.push("revision=revision+1", "updated_at=?");
1381
+ vals.push(Date.now());
1382
+ const r = this.db.prepare(`UPDATE go_jobs SET ${sets.join(", ")} WHERE id=? AND revision=?`).run(...vals, id, expectedRevision);
1383
+ return r.changes === 1;
1384
+ }
1385
+ /** Insert evidence; returns the new event seq, or -1 when deduplicated. */
1386
+ insertGoEvent(goId, sourceKey, kind, content) {
1387
+ const r = this.db.prepare(`INSERT OR IGNORE INTO go_events (go_id, source_key, kind, content, delivered, created_at) VALUES (?, ?, ?, ?, 1, ?)`)
1388
+ .run(goId, sourceKey, kind, content, Date.now());
1389
+ if (r.changes === 0)
1390
+ return -1;
1391
+ return Number(r.lastInsertRowid);
1392
+ }
1393
+ listGoEventsSince(goId, afterSeq) {
1394
+ const rows = this.db.prepare(`SELECT id, kind, source_key, content, delivered FROM go_events WHERE go_id=? AND id>? ORDER BY id`).all(goId, afterSeq);
1395
+ return rows.map((r) => ({ seq: r.id, kind: r.kind, sourceKey: r.source_key, content: r.content, delivered: r.delivered }));
1396
+ }
1397
+ maxGoEventSeq(goId) {
1398
+ const r = this.db.prepare(`SELECT MAX(id) AS m FROM go_events WHERE go_id=?`).get(goId);
1399
+ return r?.m || 0;
1400
+ }
1401
+ insertGoCheck(goId, checkId, evidenceMaxSeq) {
1402
+ return this.db.prepare(`INSERT OR IGNORE INTO go_checks (go_id, check_id, evidence_max_seq, state, created_at) VALUES (?, ?, ?, 'pending', ?)`)
1403
+ .run(goId, checkId, evidenceMaxSeq, Date.now()).changes === 1;
1404
+ }
1405
+ finishGoCheck(goId, checkId, state, action, summary, raw) {
1406
+ this.db.prepare(`UPDATE go_checks SET state=?, action=?, summary=?, raw=? WHERE go_id=? AND check_id=?`)
1407
+ .run(state, action, summary, raw.slice(0, 4000), goId, checkId);
1408
+ }
1409
+ /** Idempotent action claim: returns true when newly claimed (caller performs the action), false when it already existed. */
1410
+ claimGoAction(goId, actionKey, kind, payload) {
1411
+ this.recordGoActionAttempt(goId, actionKey);
1412
+ return this.db.prepare(`INSERT OR IGNORE INTO go_actions (go_id, action_key, kind, payload, done, created_at) VALUES (?, ?, ?, ?, 0, ?)`)
1413
+ .run(goId, actionKey, kind, payload, Date.now()).changes === 1;
1414
+ }
1415
+ hasUnresolvedGoActions(goId) {
1416
+ return !!this.db.prepare(`SELECT 1 FROM go_actions WHERE go_id=? AND done=0 LIMIT 1`).get(goId) || this.listUnresolvedGoSends(goId).length > 0 || this.hasUnknownGoRunResults(goId);
1417
+ }
1418
+ completeGoAction(goId, actionKey, receipt) {
1419
+ this.recordGoActionOutcome(goId, actionKey, "accepted", receipt);
1420
+ this.db.prepare(`UPDATE go_actions SET done=1, receipt=? WHERE go_id=? AND action_key=?`).run(receipt.slice(0, 500), goId, actionKey);
1421
+ }
1422
+ setGoModelPref(botName, chatId, model) {
1423
+ this.db.prepare(`INSERT INTO go_model_prefs (bot_name, chat_id, model, updated_at) VALUES (?, ?, ?, ?)
1424
+ ON CONFLICT(bot_name, chat_id) DO UPDATE SET model=excluded.model, updated_at=excluded.updated_at`)
1425
+ .run(botName, chatId, model, Date.now());
1426
+ }
1427
+ getGoModelPref(botName, chatId) {
1428
+ const r = this.db.prepare(`SELECT model FROM go_model_prefs WHERE bot_name=? AND chat_id=?`).get(botName, chatId);
1429
+ return r?.model ?? null;
1430
+ }
1431
+ closeObservers = new Set();
1432
+ onClose(callback) { this.closeObservers.add(callback); return () => this.closeObservers.delete(callback); }
1126
1433
  close() {
1434
+ for (const observer of this.closeObservers)
1435
+ observer();
1436
+ this.closeObservers.clear();
1127
1437
  this.db.close();
1128
1438
  }
1129
1439
  }