@stage-labs/metro 0.1.0-beta.276 → 0.1.0-beta.277
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/dist/claude.js +2 -1
- package/dist/native-recovery-scan.js +246 -0
- package/dist/native-recovery-state.js +154 -0
- package/dist/native-recovery.js +164 -0
- package/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/marketplace/plugin/bin/policy.mjs +4 -0
- package/runtime/node_modules/@metro-labs/core/package.json +2 -0
- package/runtime/node_modules/@metro-labs/core/src/runner-activity.ts +2 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/channel-directory.ts +159 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/channel-pages.ts +129 -0
- package/runtime/node_modules/@metro-labs/core/src/stations/types.ts +1 -0
- package/runtime/node_modules/@metro-labs/daemon/src/claude/api.ts +1 -1
- package/runtime/node_modules/@metro-labs/daemon/src/claude/runner-process.ts +6 -4
- package/runtime/node_modules/@metro-labs/daemon/src/claude/session.ts +3 -3
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/channel-tool.ts +80 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/permission-prompt.ts +12 -1
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/policy-gate.ts +6 -0
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-catalog.ts +4 -1
- package/runtime/node_modules/@metro-labs/daemon/src/mcp/tool-dispatch.ts +7 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/actions.ts +2 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/channel-source.ts +79 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/channels.ts +186 -0
- package/runtime/node_modules/@metro-labs/discord-bot/src/station.ts +1 -0
- package/runtime/node_modules/@metro-labs/gmail/src/actions.ts +2 -1
- package/runtime/node_modules/@metro-labs/gmail/src/channels.ts +66 -0
- package/runtime/node_modules/@metro-labs/gmail/src/station.ts +1 -0
- package/runtime/node_modules/@metro-labs/outlook/src/actions.ts +2 -1
- package/runtime/node_modules/@metro-labs/outlook/src/channels.ts +60 -0
- package/runtime/node_modules/@metro-labs/outlook/src/station.ts +1 -0
- package/runtime/node_modules/@metro-labs/telegram/src/actions.ts +19 -0
- package/runtime/node_modules/@metro-labs/telegram/src/channels.ts +58 -0
- package/runtime/node_modules/@metro-labs/telegram/src/station.ts +1 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/actions.ts +2 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/channel-files.ts +3 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/channels.ts +43 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/index.ts +5 -34
- package/runtime/node_modules/@metro-labs/telegram-bot/src/observed-chats.ts +172 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/station.ts +4 -0
- package/runtime/node_modules/@metro-labs/telegram-bot/src/types.ts +30 -3
- package/runtime/node_modules/@metro-labs/telegram-bot/src/updates.ts +26 -0
- package/runtime/node_modules/@metro-labs/threema/src/accounts.ts +13 -1
- package/runtime/node_modules/@metro-labs/threema/src/actions.ts +7 -1
- package/runtime/node_modules/@metro-labs/threema/src/channels.ts +37 -0
- package/runtime/node_modules/@metro-labs/threema/src/chats.ts +88 -0
- package/runtime/node_modules/@metro-labs/threema/src/inbound.ts +5 -0
- package/runtime/node_modules/@metro-labs/threema/src/outbound.ts +1 -0
- package/runtime/node_modules/@metro-labs/threema/src/station.ts +10 -2
- package/runtime/node_modules/@metro-labs/whatsapp/src/actions.ts +11 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/channel-chats.ts +137 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/channels.ts +48 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client-types.ts +2 -0
- package/runtime/node_modules/@metro-labs/whatsapp/src/client.ts +8 -6
- package/runtime/node_modules/@metro-labs/whatsapp/src/station.ts +4 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/actions.ts +3 -1
- package/runtime/node_modules/@metro-labs/xmtp/src/channels.ts +56 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/network.ts +4 -0
- package/runtime/node_modules/@metro-labs/xmtp/src/station.ts +1 -0
- package/runtime/runtime.json +1 -1
- package/runtime/sdk-runner/node_modules/@metro-labs/core/package.json +2 -0
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/runner-activity.ts +2 -0
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/channel-directory.ts +159 -0
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/channel-pages.ts +129 -0
- package/runtime/sdk-runner/node_modules/@metro-labs/core/src/stations/types.ts +1 -0
- package/runtime/sdk-runner/runner.json +1 -1
- package/runtime/sdk-runner/src/activity-tasks.ts +7 -1
- package/runtime/sdk-runner/src/activity.ts +33 -3
- package/runtime/sdk-runner/src/app.ts +5 -2
- package/runtime/sdk-runner/src/inbox.ts +10 -1
- package/runtime/sdk-runner/src/input-queue.ts +6 -4
- package/runtime/sdk-runner/src/main.ts +21 -8
- package/runtime/sdk-runner/src/rules.ts +1 -0
- package/runtime/sdk-runner/src/runner.ts +18 -4
- package/runtime/sdk-runner/src/session-store.ts +19 -4
- package/runtime/sdk-runner/src/session-watch.ts +5 -1
- package/runtime/sdk-runner/src/task-ledger.ts +229 -0
- package/runtime/sdk-runner/src/task-recovery.ts +203 -0
- package/runtime/sdk-runner/src/task-state.ts +71 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
3
|
+
import type { Inbox, Unanswered, Uuid } from './inbox.js';
|
|
4
|
+
import type { SessionStore } from './session-store.js';
|
|
5
|
+
import { completedCommand, startedCommand, uuidsOf } from './session-watch.js';
|
|
6
|
+
import { TaskLedger } from './task-ledger.js';
|
|
7
|
+
import { TASK_LIMIT, type SavedTask } from './task-state.js';
|
|
8
|
+
|
|
9
|
+
const DELAYS = [30_000, 120_000, 300_000];
|
|
10
|
+
const marker = (token: string): string => `[metro-task-handled:${token}]`;
|
|
11
|
+
interface Delivery { uuid: Uuid; tokens: string[]; consumed: boolean; completed?: boolean }
|
|
12
|
+
|
|
13
|
+
function noticeText(tasks: SavedTask[]): string {
|
|
14
|
+
const rows = tasks.flatMap((task) => task.notice === null ? [] : [
|
|
15
|
+
`Task ${task.id}, owner ${task.owner ?? 'unknown'}, attempt ${task.attempt}, state ${task.state}, reason ${task.notice.reason}. Acknowledge with ${marker(task.notice.token)}`,
|
|
16
|
+
]);
|
|
17
|
+
return `[Metro task recovery. These are pending obligations, not new user requests.\n${rows.join('\n')}\nCheck the existing worker and its transcript before deciding whether to continue it or report a blocker. Reuse its owner; do not create a duplicate worker or blindly repeat edits, sends or commands. Respect the existing tool and approval policy. After handling each obligation, put its exact acknowledgment marker on its own line in your assistant response, not in a chat send. Merely receiving this notice is not acknowledgment. Rate-limit reminders use bounded backoff; do not start a retry loop.]`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function mainLines(m: Record<string, unknown>): string[] {
|
|
21
|
+
if (m.type !== 'assistant' || m.aborted === true || !isRecord(m.message) || !Array.isArray(m.message.content)) return [];
|
|
22
|
+
return m.message.content.filter(isRecord).filter((block) => block.type === 'text' && typeof block.text === 'string').flatMap((block) => String(block.text).split('\n').map((line) => line.trim()));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const turnBoundary = (m: Record<string, unknown>): boolean => m.type === 'system'
|
|
26
|
+
&& (m.subtype === 'init' || (m.subtype === 'session_state_changed' && m.state === 'idle'));
|
|
27
|
+
|
|
28
|
+
export class TaskRecovery {
|
|
29
|
+
readonly ledger: TaskLedger;
|
|
30
|
+
private readonly deliveries = new Map<string, Delivery>();
|
|
31
|
+
private readonly currentInputs = new Set<string>();
|
|
32
|
+
private readonly acknowledged = new Map<string, string[]>();
|
|
33
|
+
private replyInputs: string[] | null = null;
|
|
34
|
+
private turnBoundaries = false;
|
|
35
|
+
private timer: ReturnType<typeof setTimeout> | null = null;
|
|
36
|
+
private deferredUntil = 0;
|
|
37
|
+
private closed = false;
|
|
38
|
+
|
|
39
|
+
constructor(private readonly store: SessionStore, private readonly inbox: Inbox, tasks: SavedTask[], interrupted: Unanswered[], private readonly fail: (err: unknown) => void, private readonly now = Date.now, private readonly handled = (): void => undefined) {
|
|
40
|
+
this.ledger = new TaskLedger(tasks, (rows, handled) => { store.saveTasks(rows, handled); });
|
|
41
|
+
this.ledger.interrupted(interrupted, now());
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
observe(m: Record<string, unknown>): void {
|
|
45
|
+
if (this.closed) return;
|
|
46
|
+
if (!this.recoveryOnly(m)) this.ledger.observe(m, this.now());
|
|
47
|
+
if (m.parent_tool_use_id !== null && m.parent_tool_use_id !== undefined) return;
|
|
48
|
+
this.replyTurn(m);
|
|
49
|
+
this.consume(m);
|
|
50
|
+
if (m.type === 'result') this.settle(m);
|
|
51
|
+
this.retireDeliveries(m);
|
|
52
|
+
this.schedule();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
start(): void { this.schedule(); }
|
|
56
|
+
|
|
57
|
+
close(cancel: boolean): void {
|
|
58
|
+
this.closed = true;
|
|
59
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
60
|
+
this.timer = null;
|
|
61
|
+
if (cancel) this.store.cancelTasks(this.ledger.cancel(), this.inbox.activeUuids());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private retireDeliveries(m: Record<string, unknown>): void {
|
|
65
|
+
const completed = completedCommand(m);
|
|
66
|
+
if (completed !== null) {
|
|
67
|
+
const delivery = this.deliveries.get(completed);
|
|
68
|
+
if (delivery !== undefined) delivery.completed = true;
|
|
69
|
+
this.currentInputs.delete(completed);
|
|
70
|
+
}
|
|
71
|
+
if (m.type !== 'system' || m.subtype !== 'session_state_changed' || m.state !== 'idle') return;
|
|
72
|
+
for (const [id, delivery] of this.deliveries) if (delivery.completed === true) this.deliveries.delete(id);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private recoveryOnly(m: Record<string, unknown>): boolean {
|
|
76
|
+
if (m.type !== 'result') return false;
|
|
77
|
+
const ids = uuidsOf(m) ?? [];
|
|
78
|
+
const offered = new Set<string | undefined>(this.ledger.snapshot().map((task) => task.notice?.offered));
|
|
79
|
+
if (ids.length > 0) return ids.every((id) => this.deliveries.has(id) || offered.has(id));
|
|
80
|
+
return this.inbox.startedUuids().length === 0 && [...this.deliveries.values()].some((delivery) => delivery.consumed);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private replyTurn(m: Record<string, unknown>): void {
|
|
84
|
+
if (turnBoundary(m)) {
|
|
85
|
+
this.turnBoundaries = true;
|
|
86
|
+
this.replyInputs = null;
|
|
87
|
+
}
|
|
88
|
+
if (m.type === 'assistant' || m.type === 'stream_event') this.replyInputs = uuidsOf(m) ?? this.replyInputs;
|
|
89
|
+
const ended = m.type === 'result' ? uuidsOf(m) ?? [] : [completedCommand(m)];
|
|
90
|
+
if (this.replyInputs?.some((id) => ended.includes(id))) this.replyInputs = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private replyIds(m: Record<string, unknown>): string[] {
|
|
94
|
+
const stamped = uuidsOf(m) ?? this.replyInputs;
|
|
95
|
+
if (stamped !== null) return stamped;
|
|
96
|
+
return !this.turnBoundaries && this.currentInputs.size === 1 ? [...this.currentInputs] : [];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private consume(m: Record<string, unknown>): void {
|
|
100
|
+
const command = startedCommand(m) ?? completedCommand(m);
|
|
101
|
+
const ids = [...(uuidsOf(m) ?? []), ...(command === null ? [] : [command])];
|
|
102
|
+
for (const id of ids) this.currentInputs.add(id);
|
|
103
|
+
for (const delivery of this.deliveries.values()) {
|
|
104
|
+
if (ids.includes(delivery.uuid)) delivery.consumed = true;
|
|
105
|
+
}
|
|
106
|
+
this.consumed(ids);
|
|
107
|
+
this.acknowledge(m);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private consumed(ids: string[]): void {
|
|
111
|
+
let changed = false;
|
|
112
|
+
for (const task of this.ledger.snapshot()) {
|
|
113
|
+
const notice = task.notice;
|
|
114
|
+
if (notice?.offered === undefined || !ids.includes(notice.offered)) continue;
|
|
115
|
+
notice.nextAt = this.now() + (DELAYS[notice.deliveries] ?? 300_000);
|
|
116
|
+
notice.deliveries += 1;
|
|
117
|
+
notice.consumed = true;
|
|
118
|
+
delete notice.offered;
|
|
119
|
+
changed = true;
|
|
120
|
+
}
|
|
121
|
+
if (changed) this.ledger.changed();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private acknowledge(m: Record<string, unknown>): void {
|
|
125
|
+
const lines = mainLines(m);
|
|
126
|
+
if (lines.length === 0) return;
|
|
127
|
+
const ids = this.replyIds(m);
|
|
128
|
+
for (const task of this.ledger.snapshot()) {
|
|
129
|
+
if (task.notice?.consumed !== true || !lines.includes(marker(task.notice.token))) continue;
|
|
130
|
+
this.acknowledged.set(task.notice.token, [...new Set([...(this.acknowledged.get(task.notice.token) ?? []), ...ids])]);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private settle(m: Record<string, unknown>): void {
|
|
135
|
+
const ids = uuidsOf(m) ?? [];
|
|
136
|
+
for (const id of ids) { this.deliveries.delete(id); this.currentInputs.delete(id); }
|
|
137
|
+
const handled = m.subtype === 'success' && m.is_error !== true ? this.handledTasks(ids) : new Set<string>();
|
|
138
|
+
for (const [token, inputs] of this.acknowledged) {
|
|
139
|
+
const pending = inputs.filter((id) => !ids.includes(id));
|
|
140
|
+
if (pending.length === 0) this.acknowledged.delete(token);
|
|
141
|
+
else this.acknowledged.set(token, pending);
|
|
142
|
+
}
|
|
143
|
+
if (handled.size === 0) return;
|
|
144
|
+
this.ledger.changed(handled);
|
|
145
|
+
this.ledger.interrupted(this.store.interrupted(), this.now());
|
|
146
|
+
if (this.ledger.snapshot().every((task) => task.notice === null)) this.handled();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private handledTasks(ids: string[]): Set<string> {
|
|
150
|
+
const handled = new Set<string>();
|
|
151
|
+
for (const task of this.ledger.snapshot()) {
|
|
152
|
+
if (task.notice === null || !this.acknowledged.get(task.notice.token)?.some((id) => ids.includes(id))) continue;
|
|
153
|
+
task.handledAttempt = task.notice.attempt;
|
|
154
|
+
task.notice = null;
|
|
155
|
+
handled.add(task.id);
|
|
156
|
+
}
|
|
157
|
+
return handled;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private eligible(task: SavedTask): boolean {
|
|
161
|
+
const notice = task.notice;
|
|
162
|
+
if (notice === null || notice.deliveries >= DELAYS.length) return false;
|
|
163
|
+
return ![...this.deliveries.values()].some((delivery) => delivery.completed !== true && delivery.tokens.includes(notice.token));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private schedule(): void {
|
|
167
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
168
|
+
this.timer = null;
|
|
169
|
+
if (this.closed) return;
|
|
170
|
+
const pending = this.ledger.snapshot().filter((task) => this.eligible(task));
|
|
171
|
+
if (pending.length === 0) return;
|
|
172
|
+
const at = Math.max(this.deferredUntil, Math.min(...pending.map((task) => task.notice?.nextAt ?? Infinity)));
|
|
173
|
+
this.timer = setTimeout(() => {
|
|
174
|
+
this.timer = null;
|
|
175
|
+
try { this.deliver(); } catch (err) { this.fail(err); }
|
|
176
|
+
}, Math.min(300_000, Math.max(0, at - this.now())));
|
|
177
|
+
this.timer.unref();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private deliver(): void {
|
|
181
|
+
const due = this.ledger.snapshot().filter((task) => this.eligible(task) && (task.notice?.nextAt ?? Infinity) <= this.now());
|
|
182
|
+
const offered = due[0]?.notice?.offered;
|
|
183
|
+
const tasks = due.filter((task) => task.notice?.offered === offered).slice(0, 20);
|
|
184
|
+
if (tasks.length === 0) { this.schedule(); return; }
|
|
185
|
+
const text = noticeText(tasks);
|
|
186
|
+
if (this.deliveries.size >= TASK_LIMIT * DELAYS.length || !this.inbox.accepts('note', text)) {
|
|
187
|
+
this.deferredUntil = this.now() + 30_000;
|
|
188
|
+
this.schedule();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const uuid = offered ?? randomUUID();
|
|
192
|
+
const tokens: string[] = [];
|
|
193
|
+
for (const task of tasks) {
|
|
194
|
+
if (task.notice === null) continue;
|
|
195
|
+
task.notice.offered = uuid;
|
|
196
|
+
tokens.push(task.notice.token);
|
|
197
|
+
}
|
|
198
|
+
this.ledger.changed();
|
|
199
|
+
this.deliveries.set(uuid, { uuid, tokens, consumed: false });
|
|
200
|
+
this.inbox.push('note', text, undefined, uuid);
|
|
201
|
+
this.schedule();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { isRecord } from '@metro-labs/core/is-record';
|
|
3
|
+
import { activityName } from './activity-tasks.js';
|
|
4
|
+
import type { Uuid } from './inbox.js';
|
|
5
|
+
|
|
6
|
+
export const TASK_LIMIT = 200;
|
|
7
|
+
const STATES = ['running', 'completed', 'failed', 'stopped', 'interrupted'] as const;
|
|
8
|
+
const REASONS = ['task_error', 'rate_limit', 'worker_restart', 'interrupted', 'provider_error'] as const;
|
|
9
|
+
export type TaskReason = (typeof REASONS)[number];
|
|
10
|
+
|
|
11
|
+
export interface TaskNotice {
|
|
12
|
+
token: string;
|
|
13
|
+
reason: TaskReason;
|
|
14
|
+
attempt: number;
|
|
15
|
+
deliveries: number;
|
|
16
|
+
nextAt: number;
|
|
17
|
+
consumed?: boolean;
|
|
18
|
+
offered?: Uuid;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SavedTask {
|
|
22
|
+
id: string;
|
|
23
|
+
toolUseId: string | null;
|
|
24
|
+
owner: string | null;
|
|
25
|
+
parentToolUseId?: string | null;
|
|
26
|
+
attempt: number;
|
|
27
|
+
handledAttempt?: number;
|
|
28
|
+
state: (typeof STATES)[number];
|
|
29
|
+
events: string[];
|
|
30
|
+
updatedAt: number;
|
|
31
|
+
notice: TaskNotice | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const integer = (raw: unknown): raw is number => typeof raw === 'number' && Number.isSafeInteger(raw) && raw >= 0;
|
|
35
|
+
const name = (raw: unknown): raw is string | null => raw === null || activityName(raw) !== null;
|
|
36
|
+
const reason = (raw: unknown): raw is TaskReason => REASONS.some((value) => value === raw);
|
|
37
|
+
const optionalInteger = (raw: unknown): boolean => raw === undefined || integer(raw);
|
|
38
|
+
const optionalUuid = (raw: unknown): boolean => raw === undefined || (typeof raw === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(raw));
|
|
39
|
+
|
|
40
|
+
function notice(raw: unknown): raw is TaskNotice | null {
|
|
41
|
+
if (raw === null) return true;
|
|
42
|
+
if (!isRecord(raw) || activityName(raw.token) === null || !reason(raw.reason)) return false;
|
|
43
|
+
if (!optionalUuid(raw.offered)) return false;
|
|
44
|
+
return integer(raw.attempt) && integer(raw.deliveries) && integer(raw.nextAt) && (raw.consumed === undefined || typeof raw.consumed === 'boolean');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const events = (raw: unknown): boolean => Array.isArray(raw) && raw.length <= 16 && raw.every((event) => activityName(event) !== null);
|
|
48
|
+
|
|
49
|
+
function task(raw: unknown): raw is SavedTask {
|
|
50
|
+
if (!isRecord(raw) || activityName(raw.id) === null) return false;
|
|
51
|
+
if (![raw.toolUseId, raw.owner, raw.parentToolUseId ?? null].every(name)) return false;
|
|
52
|
+
if (!events(raw.events) || !optionalInteger(raw.handledAttempt)) return false;
|
|
53
|
+
return integer(raw.attempt) && integer(raw.updatedAt) && STATES.some((state) => state === raw.state) && notice(raw.notice);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function savedTasks(raw: unknown): SavedTask[] {
|
|
57
|
+
if (raw === undefined) return [];
|
|
58
|
+
if (!Array.isArray(raw) || raw.length > TASK_LIMIT || !raw.every(task) || new Set(raw.map((row) => row.id)).size !== raw.length)
|
|
59
|
+
throw new Error('The saved Agent SDK state cannot be read. Restore it before starting.');
|
|
60
|
+
return raw;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function pendingNotice(reason: TaskReason, attempt: number, now: number): TaskNotice {
|
|
64
|
+
return { token: randomUUID(), reason, attempt, deliveries: 0, nextAt: now + (reason === 'rate_limit' ? 30_000 : 0) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function recoveredTasks(tasks: SavedTask[], now = Date.now()): SavedTask[] {
|
|
68
|
+
return tasks.map((task) => task.state === 'running'
|
|
69
|
+
? { ...task, state: 'interrupted', updatedAt: now, notice: task.notice?.attempt === task.attempt ? task.notice : pendingNotice('interrupted', task.attempt, now) }
|
|
70
|
+
: task);
|
|
71
|
+
}
|