talon-agent 5.26.6 → 5.28.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.
- package/package.json +1 -1
- package/src/app.ts +44 -13
- package/src/backend/claude-sdk/factory.ts +2 -0
- package/src/backend/claude-sdk/one-shot.ts +22 -1
- package/src/backend/codex/factory.ts +2 -0
- package/src/backend/codex/one-shot.ts +68 -19
- package/src/backend/runtime/one-shot-hooks.ts +24 -0
- package/src/core/agent-runtime/capabilities.ts +7 -0
- package/src/core/agents/index.ts +2 -0
- package/src/core/agents/prompt.ts +75 -0
- package/src/core/agents/registry.ts +317 -7
- package/src/core/agents/runner.ts +331 -10
- package/src/core/backup/{restore-guard.ts → restore/guard.ts} +5 -5
- package/src/core/backup/restore/notice.ts +164 -0
- package/src/core/backup/restore.ts +17 -5
- package/src/core/mesh/credentials/store.ts +9 -1
- package/src/core/mesh/persist.ts +9 -0
- package/src/core/types.ts +15 -0
- package/src/frontend/discord/actions/channels.ts +6 -3
- package/src/frontend/discord/actions/index.ts +24 -0
- package/src/frontend/discord/commands/backup.ts +1 -0
- package/src/frontend/discord/handlers/registry.ts +35 -0
- package/src/frontend/native/commands/backup.ts +1 -0
- package/src/frontend/native/turn/actions.ts +29 -1
- package/src/frontend/telegram/commands/backup.ts +1 -0
- package/src/storage/agents/repo.ts +200 -0
- package/src/storage/sql/agents.sql +43 -0
- package/src/storage/sql/schema.sql +54 -0
- package/src/storage/sql/statements.generated.ts +87 -1
|
@@ -23,7 +23,7 @@ import { resolve } from "node:path";
|
|
|
23
23
|
import { dirs } from "../../../util/paths.js";
|
|
24
24
|
import { log, logWarn } from "../../../util/log.js";
|
|
25
25
|
import { TalonError } from "../../errors.js";
|
|
26
|
-
import { readArray, writePrivateJson } from "../persist.js";
|
|
26
|
+
import { readArray, writePrivateJson, writesSettled } from "../persist.js";
|
|
27
27
|
import { faultText } from "../../engine/fault-text.js";
|
|
28
28
|
import { raiseAlert, resolveAlert } from "../../frontend-runtime/alerts.js";
|
|
29
29
|
import {
|
|
@@ -506,6 +506,14 @@ export class DeviceCredentialStore {
|
|
|
506
506
|
}
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Wait for every background write queued so far (mintNow, bind, re-pair
|
|
511
|
+
* revocation, lastUsedAt touches) to reach disk or fail.
|
|
512
|
+
*/
|
|
513
|
+
flush(): Promise<void> {
|
|
514
|
+
return writesSettled(this.file);
|
|
515
|
+
}
|
|
516
|
+
|
|
509
517
|
private persistSoon(): void {
|
|
510
518
|
void this.persist().catch((err: unknown) =>
|
|
511
519
|
logWarn("mesh", `Could not persist mesh credentials: ${err}`),
|
package/src/core/mesh/persist.ts
CHANGED
|
@@ -25,6 +25,15 @@ export async function readArray<T>(path: string): Promise<T[]> {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Resolve once every write queued for `path` so far has finished (settled,
|
|
30
|
+
* success or failure) — for callers that persisted fire-and-forget and now
|
|
31
|
+
* need the file on disk.
|
|
32
|
+
*/
|
|
33
|
+
export function writesSettled(path: string): Promise<void> {
|
|
34
|
+
return writeQueues.get(path) ?? Promise.resolve();
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
/** Persist JSON atomically with 0600 perms, serialized per path. */
|
|
29
38
|
export async function writePrivateJson(
|
|
30
39
|
path: string,
|
package/src/core/types.ts
CHANGED
|
@@ -172,6 +172,21 @@ export type OneShotAgentParams = {
|
|
|
172
172
|
* and logs a throwing callback so a consumer bug can never fail the run.
|
|
173
173
|
*/
|
|
174
174
|
onAssistantText?: (text: string) => void;
|
|
175
|
+
/**
|
|
176
|
+
* Resume this backend conversation instead of starting a fresh one — the
|
|
177
|
+
* handle an earlier run reported through `onSessionId`. Honoured only by
|
|
178
|
+
* backends whose `BackgroundRunner.supportsResume` is true (Claude SDK:
|
|
179
|
+
* session id; Codex: thread id); the others ignore it and start fresh,
|
|
180
|
+
* which is why the sub-agent runner checks that flag before relying on it.
|
|
181
|
+
*/
|
|
182
|
+
resumeSessionId?: string;
|
|
183
|
+
/**
|
|
184
|
+
* Called once the backend knows the run's conversation handle (Claude SDK
|
|
185
|
+
* session id, Codex thread id) — a sub-agent persists it so a daemon
|
|
186
|
+
* restart can resume the conversation. Best-effort and synchronous, like
|
|
187
|
+
* `onAssistantText`: a throwing callback must never fail the run.
|
|
188
|
+
*/
|
|
189
|
+
onSessionId?: (sessionId: string) => void;
|
|
175
190
|
};
|
|
176
191
|
|
|
177
192
|
/** How much cache telemetry a backend can surface in /status. */
|
|
@@ -14,9 +14,12 @@ export async function resolveChannel(
|
|
|
14
14
|
const info = lookupDiscordChat(numericChatId);
|
|
15
15
|
if (!info) return null;
|
|
16
16
|
try {
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
|
|
17
|
+
// A DM rebuilt from its chat key has no channel id — open it by user.
|
|
18
|
+
if (info.channelId) {
|
|
19
|
+
const ch = await client.channels.fetch(info.channelId);
|
|
20
|
+
if (ch && "send" in ch && (ch as TextBasedChannel).isSendable?.()) {
|
|
21
|
+
return ch as TextBasedChannel;
|
|
22
|
+
}
|
|
20
23
|
}
|
|
21
24
|
if (info.userId) {
|
|
22
25
|
const user = await client.users.fetch(info.userId);
|
|
@@ -24,6 +24,11 @@ import type { Client } from "discord.js";
|
|
|
24
24
|
import type { Gateway } from "../../../core/engine/gateway.js";
|
|
25
25
|
import type { ActionResult } from "../../../core/types.js";
|
|
26
26
|
import { resolveChannel } from "./channels.js";
|
|
27
|
+
import {
|
|
28
|
+
lookupDiscordChat,
|
|
29
|
+
parseDiscordChatKey,
|
|
30
|
+
registerDiscordChat,
|
|
31
|
+
} from "../handlers/registry.js";
|
|
27
32
|
import { messagingHandlers, restoreScheduledMessages } from "./messaging.js";
|
|
28
33
|
import { mediaHandlers } from "./media.js";
|
|
29
34
|
import { chatInfoHandlers } from "./chat-info.js";
|
|
@@ -43,6 +48,24 @@ const handlers: DiscordActionHandlers = Object.assign(Object.create(null), {
|
|
|
43
48
|
...chatInfoHandlers,
|
|
44
49
|
});
|
|
45
50
|
|
|
51
|
+
/**
|
|
52
|
+
* A plain send addressed to a chat this process hasn't registered — the
|
|
53
|
+
* channel that asked for a staged restore, say, before anyone has spoken
|
|
54
|
+
* in it since the restart. The sender names the chat by key in
|
|
55
|
+
* `body.target`; when that key is the one the numeric id was derived
|
|
56
|
+
* from, register it so the channel resolves. send_message only.
|
|
57
|
+
*/
|
|
58
|
+
function adoptAddressedChat(
|
|
59
|
+
action: string,
|
|
60
|
+
body: Record<string, unknown>,
|
|
61
|
+
chatId: number,
|
|
62
|
+
): void {
|
|
63
|
+
if (action !== "send_message" || lookupDiscordChat(chatId)) return;
|
|
64
|
+
const key = typeof body.target === "string" ? body.target : "";
|
|
65
|
+
const info = key ? parseDiscordChatKey(key) : undefined;
|
|
66
|
+
if (info && info.numericChatId === chatId) registerDiscordChat(info);
|
|
67
|
+
}
|
|
68
|
+
|
|
46
69
|
export function createDiscordActionHandler(client: Client, gateway: Gateway) {
|
|
47
70
|
const scheduledMessages = new Map<string, ReturnType<typeof setTimeout>>();
|
|
48
71
|
|
|
@@ -58,6 +81,7 @@ export function createDiscordActionHandler(client: Client, gateway: Gateway) {
|
|
|
58
81
|
const handler = handlers[action];
|
|
59
82
|
if (!handler) return null; // not a Discord action
|
|
60
83
|
|
|
84
|
+
adoptAddressedChat(action, body, chatId);
|
|
61
85
|
const channel = await resolveChannel(client, chatId);
|
|
62
86
|
|
|
63
87
|
// For non-channel actions (e.g. cancel_scheduled) that don't need a
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Discord channel info the action handler needs to post back.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { deriveNumericChatId } from "../../../core/frontend-runtime/chat-id.js";
|
|
6
7
|
import {
|
|
7
8
|
chatRegistry,
|
|
8
9
|
chatRegistryByString,
|
|
@@ -21,3 +22,37 @@ export function lookupDiscordChat(
|
|
|
21
22
|
): DiscordChatInfo | undefined {
|
|
22
23
|
return chatRegistry.get(numericChatId);
|
|
23
24
|
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Rebuild a chat's registry entry from its string key alone
|
|
28
|
+
* (`discord_guild_<guild>_<channel>` or `discord_dm_<user>`), for a chat
|
|
29
|
+
* this process hasn't seen a message from yet — the registry is in-memory,
|
|
30
|
+
* so after a restart only the allowed users' DMs are known until someone
|
|
31
|
+
* speaks. A DM entry carries no channel id; resolveChannel opens the DM
|
|
32
|
+
* from the user id. Undefined for anything that isn't a Discord chat key.
|
|
33
|
+
*/
|
|
34
|
+
export function parseDiscordChatKey(
|
|
35
|
+
chatKey: string,
|
|
36
|
+
): DiscordChatInfo | undefined {
|
|
37
|
+
const guild = /^discord_guild_(\d+)_(\d+)$/.exec(chatKey);
|
|
38
|
+
if (guild) {
|
|
39
|
+
return {
|
|
40
|
+
channelId: guild[2]!,
|
|
41
|
+
guildId: guild[1]!,
|
|
42
|
+
userId: null,
|
|
43
|
+
numericChatId: deriveNumericChatId(chatKey),
|
|
44
|
+
chatId: chatKey,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
const dm = /^discord_dm_(\d+)$/.exec(chatKey);
|
|
48
|
+
if (dm) {
|
|
49
|
+
return {
|
|
50
|
+
channelId: "",
|
|
51
|
+
guildId: null,
|
|
52
|
+
userId: dm[1]!,
|
|
53
|
+
numericChatId: deriveNumericChatId(chatKey),
|
|
54
|
+
chatId: chatKey,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
@@ -15,6 +15,10 @@ import type {
|
|
|
15
15
|
import type { Gateway } from "../../../core/engine/gateway.js";
|
|
16
16
|
import type { NativeChats, ChatEntry } from "../chats/chats.js";
|
|
17
17
|
import type { BridgeEvent, ClientButton } from "../protocol.js";
|
|
18
|
+
import {
|
|
19
|
+
deriveNumericChatId,
|
|
20
|
+
isNativeChatId,
|
|
21
|
+
} from "../../../core/frontend-runtime/chat-id.js";
|
|
18
22
|
|
|
19
23
|
export type NativeActionDeps = {
|
|
20
24
|
chats: NativeChats;
|
|
@@ -69,6 +73,28 @@ const NATIVE_ACTIONS = new Set([
|
|
|
69
73
|
"send_chat_action",
|
|
70
74
|
]);
|
|
71
75
|
|
|
76
|
+
/**
|
|
77
|
+
* A plain send addressed to a native chat this daemon doesn't list — the
|
|
78
|
+
* chat that asked for a staged restore, say, when the restored database
|
|
79
|
+
* predates it. The sender names the chat by key in `body.target`; the
|
|
80
|
+
* chat is adopted (as a deep link would) so the message lands in its
|
|
81
|
+
* history and the app shows it. Only when the key really is the chat the
|
|
82
|
+
* numeric id was derived from, and only for send_message: edits,
|
|
83
|
+
* reactions and deletes address messages, which an unknown chat has none of.
|
|
84
|
+
*/
|
|
85
|
+
function adoptAddressedChat(
|
|
86
|
+
chats: NativeChats,
|
|
87
|
+
action: string,
|
|
88
|
+
body: Record<string, unknown>,
|
|
89
|
+
chatId: number,
|
|
90
|
+
): ChatEntry | undefined {
|
|
91
|
+
if (action !== "send_message") return undefined;
|
|
92
|
+
const key = typeof body.target === "string" ? body.target : "";
|
|
93
|
+
if (!key || !isNativeChatId(key) || deriveNumericChatId(key) !== chatId)
|
|
94
|
+
return undefined;
|
|
95
|
+
return chats.ensure(key);
|
|
96
|
+
}
|
|
97
|
+
|
|
72
98
|
export function createNativeActionHandler(
|
|
73
99
|
deps: NativeActionDeps,
|
|
74
100
|
): FrontendActionHandler {
|
|
@@ -77,7 +103,9 @@ export function createNativeActionHandler(
|
|
|
77
103
|
return async (body, chatId): Promise<ActionResult | null> => {
|
|
78
104
|
const action = typeof body.action === "string" ? body.action : "";
|
|
79
105
|
if (!NATIVE_ACTIONS.has(action)) return null;
|
|
80
|
-
const entry =
|
|
106
|
+
const entry =
|
|
107
|
+
chats.byNumeric(chatId) ??
|
|
108
|
+
adoptAddressedChat(chats, action, body, chatId);
|
|
81
109
|
if (!entry) return { ok: false, error: "No active native chat" };
|
|
82
110
|
|
|
83
111
|
switch (action) {
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agents repository — executes the statements in sql/agents.sql against
|
|
3
|
+
* the `agents` table; no SQL text lives here. The domain side (what gets
|
|
4
|
+
* written when, and the boot-time resume) is core/agents/registry.ts and
|
|
5
|
+
* core/agents/runner.ts.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getDatabase } from "../db.js";
|
|
9
|
+
import { agentsSql } from "../sql/statements.generated.js";
|
|
10
|
+
|
|
11
|
+
/** One persisted message waiting in an agent's mailbox. */
|
|
12
|
+
type PersistedAgentMessage = {
|
|
13
|
+
from: string;
|
|
14
|
+
text: string;
|
|
15
|
+
at: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** A sub-agent as stored — everything a restart needs to bring it back. */
|
|
19
|
+
export type PersistedAgent = {
|
|
20
|
+
id: string;
|
|
21
|
+
label: string;
|
|
22
|
+
brief: string;
|
|
23
|
+
parentKind: "chat" | "agent";
|
|
24
|
+
/** Chat key (chat parent) or agent id (agent parent). */
|
|
25
|
+
parentId: string;
|
|
26
|
+
/** Frontend numeric chat id — chat parents only. */
|
|
27
|
+
parentNumericChatId?: number;
|
|
28
|
+
backendId: string;
|
|
29
|
+
model?: string;
|
|
30
|
+
requestedModel?: string;
|
|
31
|
+
reasoningEffort?: string;
|
|
32
|
+
timeoutMs?: number;
|
|
33
|
+
depth: number;
|
|
34
|
+
cwd?: string;
|
|
35
|
+
state: string;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
startedAt?: number;
|
|
38
|
+
endedAt?: number;
|
|
39
|
+
updatedAt: number;
|
|
40
|
+
sessionId?: string;
|
|
41
|
+
inbox: PersistedAgentMessage[];
|
|
42
|
+
reported: boolean;
|
|
43
|
+
resultSummary?: string;
|
|
44
|
+
resultDetails?: string;
|
|
45
|
+
error?: string;
|
|
46
|
+
elapsedMs: number;
|
|
47
|
+
resumeCount: number;
|
|
48
|
+
interruptedAt?: number;
|
|
49
|
+
/** The spawn asked for the pre-flight lane instruction. */
|
|
50
|
+
preflight?: boolean;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type Row = {
|
|
54
|
+
id: string;
|
|
55
|
+
label: string;
|
|
56
|
+
brief: string;
|
|
57
|
+
parent_kind: string;
|
|
58
|
+
parent_id: string;
|
|
59
|
+
parent_numeric: number | null;
|
|
60
|
+
backend_id: string;
|
|
61
|
+
model: string | null;
|
|
62
|
+
requested_model: string | null;
|
|
63
|
+
effort: string | null;
|
|
64
|
+
timeout_ms: number | null;
|
|
65
|
+
depth: number;
|
|
66
|
+
cwd: string | null;
|
|
67
|
+
state: string;
|
|
68
|
+
created_at: number;
|
|
69
|
+
started_at: number | null;
|
|
70
|
+
ended_at: number | null;
|
|
71
|
+
updated_at: number;
|
|
72
|
+
session_id: string | null;
|
|
73
|
+
inbox_json: string;
|
|
74
|
+
reported: number;
|
|
75
|
+
result_summary: string | null;
|
|
76
|
+
result_details: string | null;
|
|
77
|
+
error: string | null;
|
|
78
|
+
elapsed_ms: number;
|
|
79
|
+
resume_count: number;
|
|
80
|
+
interrupted_at: number | null;
|
|
81
|
+
preflight: number;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
function parseInbox(raw: string): PersistedAgentMessage[] {
|
|
85
|
+
try {
|
|
86
|
+
const parsed: unknown = JSON.parse(raw);
|
|
87
|
+
if (!Array.isArray(parsed)) return [];
|
|
88
|
+
return parsed.filter(
|
|
89
|
+
(m): m is PersistedAgentMessage =>
|
|
90
|
+
typeof m === "object" &&
|
|
91
|
+
m !== null &&
|
|
92
|
+
typeof (m as { from?: unknown }).from === "string" &&
|
|
93
|
+
typeof (m as { text?: unknown }).text === "string" &&
|
|
94
|
+
typeof (m as { at?: unknown }).at === "number",
|
|
95
|
+
);
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function rowToAgent(row: Row): PersistedAgent {
|
|
102
|
+
const agent: PersistedAgent = {
|
|
103
|
+
id: row.id,
|
|
104
|
+
label: row.label,
|
|
105
|
+
brief: row.brief,
|
|
106
|
+
parentKind: row.parent_kind === "agent" ? "agent" : "chat",
|
|
107
|
+
parentId: row.parent_id,
|
|
108
|
+
backendId: row.backend_id,
|
|
109
|
+
depth: row.depth,
|
|
110
|
+
state: row.state,
|
|
111
|
+
createdAt: row.created_at,
|
|
112
|
+
updatedAt: row.updated_at,
|
|
113
|
+
inbox: parseInbox(row.inbox_json),
|
|
114
|
+
reported: row.reported === 1,
|
|
115
|
+
elapsedMs: row.elapsed_ms,
|
|
116
|
+
resumeCount: row.resume_count,
|
|
117
|
+
};
|
|
118
|
+
if (row.parent_numeric !== null)
|
|
119
|
+
agent.parentNumericChatId = row.parent_numeric;
|
|
120
|
+
if (row.model !== null) agent.model = row.model;
|
|
121
|
+
if (row.requested_model !== null) agent.requestedModel = row.requested_model;
|
|
122
|
+
if (row.effort !== null) agent.reasoningEffort = row.effort;
|
|
123
|
+
if (row.timeout_ms !== null) agent.timeoutMs = row.timeout_ms;
|
|
124
|
+
if (row.cwd !== null) agent.cwd = row.cwd;
|
|
125
|
+
if (row.started_at !== null) agent.startedAt = row.started_at;
|
|
126
|
+
if (row.ended_at !== null) agent.endedAt = row.ended_at;
|
|
127
|
+
if (row.session_id !== null) agent.sessionId = row.session_id;
|
|
128
|
+
if (row.result_summary !== null) agent.resultSummary = row.result_summary;
|
|
129
|
+
if (row.result_details !== null) agent.resultDetails = row.result_details;
|
|
130
|
+
if (row.error !== null) agent.error = row.error;
|
|
131
|
+
if (row.interrupted_at !== null) agent.interruptedAt = row.interrupted_at;
|
|
132
|
+
if (row.preflight === 1) agent.preflight = true;
|
|
133
|
+
return agent;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function upsert(a: PersistedAgent): void {
|
|
137
|
+
getDatabase()
|
|
138
|
+
.prepare(agentsSql.upsert)
|
|
139
|
+
.run(
|
|
140
|
+
a.id,
|
|
141
|
+
a.label,
|
|
142
|
+
a.brief,
|
|
143
|
+
a.parentKind,
|
|
144
|
+
a.parentId,
|
|
145
|
+
a.parentNumericChatId ?? null,
|
|
146
|
+
a.backendId,
|
|
147
|
+
a.model ?? null,
|
|
148
|
+
a.requestedModel ?? null,
|
|
149
|
+
a.reasoningEffort ?? null,
|
|
150
|
+
a.timeoutMs ?? null,
|
|
151
|
+
a.depth,
|
|
152
|
+
a.cwd ?? null,
|
|
153
|
+
a.state,
|
|
154
|
+
a.createdAt,
|
|
155
|
+
a.startedAt ?? null,
|
|
156
|
+
a.endedAt ?? null,
|
|
157
|
+
a.updatedAt,
|
|
158
|
+
a.sessionId ?? null,
|
|
159
|
+
JSON.stringify(a.inbox),
|
|
160
|
+
a.reported ? 1 : 0,
|
|
161
|
+
a.resultSummary ?? null,
|
|
162
|
+
a.resultDetails ?? null,
|
|
163
|
+
a.error ?? null,
|
|
164
|
+
a.elapsedMs,
|
|
165
|
+
a.resumeCount,
|
|
166
|
+
a.interruptedAt ?? null,
|
|
167
|
+
a.preflight ? 1 : 0,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function get(id: string): PersistedAgent | undefined {
|
|
172
|
+
const row = getDatabase().prepare(agentsSql.get).get(id) as Row | undefined;
|
|
173
|
+
return row ? rowToAgent(row) : undefined;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Rows still queued/running — interrupted by the last restart. */
|
|
177
|
+
export function listInterrupted(): PersistedAgent[] {
|
|
178
|
+
const rows = getDatabase().prepare(agentsSql.listInterrupted).all() as Row[];
|
|
179
|
+
return rows.map(rowToAgent);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function remove(id: string): boolean {
|
|
183
|
+
const result = getDatabase().prepare(agentsSql.remove).run(id) as {
|
|
184
|
+
changes?: number;
|
|
185
|
+
};
|
|
186
|
+
return (result.changes ?? 0) > 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Delete settled rows last touched before `cutoff`. Returns how many. */
|
|
190
|
+
export function pruneSettled(cutoff: number): number {
|
|
191
|
+
const result = getDatabase().prepare(agentsSql.pruneSettled).run(cutoff) as {
|
|
192
|
+
changes?: number;
|
|
193
|
+
};
|
|
194
|
+
return result.changes ?? 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Test-only: wipe the table between suites sharing a worker DB. */
|
|
198
|
+
export function removeAll(): void {
|
|
199
|
+
getDatabase().prepare(agentsSql.removeAll).run();
|
|
200
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
-- Statements for the agents table (see storage/agents/repo.ts for
|
|
2
|
+
-- the parameter order and row↔domain mapping).
|
|
3
|
+
|
|
4
|
+
-- name: upsert
|
|
5
|
+
INSERT OR REPLACE INTO agents
|
|
6
|
+
(id, label, brief, parent_kind, parent_id, parent_numeric, backend_id,
|
|
7
|
+
model, requested_model, effort, timeout_ms, depth, cwd, state,
|
|
8
|
+
created_at, started_at, ended_at, updated_at, session_id, inbox_json,
|
|
9
|
+
reported, result_summary, result_details, error, elapsed_ms,
|
|
10
|
+
resume_count, interrupted_at, preflight)
|
|
11
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
12
|
+
|
|
13
|
+
-- name: get
|
|
14
|
+
SELECT id, label, brief, parent_kind, parent_id, parent_numeric, backend_id,
|
|
15
|
+
model, requested_model, effort, timeout_ms, depth, cwd, state,
|
|
16
|
+
created_at, started_at, ended_at, updated_at, session_id, inbox_json,
|
|
17
|
+
reported, result_summary, result_details, error, elapsed_ms,
|
|
18
|
+
resume_count, interrupted_at, preflight
|
|
19
|
+
FROM agents WHERE id = ?
|
|
20
|
+
|
|
21
|
+
-- Rows a restart interrupted, parents before children (a child can only
|
|
22
|
+
-- be re-attached to a parent that is already back in the registry).
|
|
23
|
+
|
|
24
|
+
-- name: listInterrupted
|
|
25
|
+
SELECT id, label, brief, parent_kind, parent_id, parent_numeric, backend_id,
|
|
26
|
+
model, requested_model, effort, timeout_ms, depth, cwd, state,
|
|
27
|
+
created_at, started_at, ended_at, updated_at, session_id, inbox_json,
|
|
28
|
+
reported, result_summary, result_details, error, elapsed_ms,
|
|
29
|
+
resume_count, interrupted_at, preflight
|
|
30
|
+
FROM agents WHERE state IN ('queued', 'running')
|
|
31
|
+
ORDER BY depth, created_at
|
|
32
|
+
|
|
33
|
+
-- name: remove
|
|
34
|
+
DELETE FROM agents WHERE id = ?
|
|
35
|
+
|
|
36
|
+
-- Retention: settled rows past their window. Live rows are never pruned.
|
|
37
|
+
|
|
38
|
+
-- name: pruneSettled
|
|
39
|
+
DELETE FROM agents
|
|
40
|
+
WHERE state NOT IN ('queued', 'running') AND updated_at < ?
|
|
41
|
+
|
|
42
|
+
-- name: removeAll
|
|
43
|
+
DELETE FROM agents
|
|
@@ -406,3 +406,57 @@ CREATE TABLE IF NOT EXISTS backup_remotes (
|
|
|
406
406
|
error TEXT,
|
|
407
407
|
PRIMARY KEY (backup_id, target_id)
|
|
408
408
|
);
|
|
409
|
+
|
|
410
|
+
-- Sub-agents, persisted so a daemon restart does not kill them. One row
|
|
411
|
+
-- per spawn, written at registration and kept current on every lifecycle
|
|
412
|
+
-- change (start, SDK session id, mailbox, report, settle). On boot, every
|
|
413
|
+
-- row still 'queued' or 'running' was interrupted by the restart and is
|
|
414
|
+
-- respawned — resuming its backend session when the backend can. See
|
|
415
|
+
-- resumeAgentsAfterRestart in core/agents/runner.ts.
|
|
416
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
417
|
+
id TEXT PRIMARY KEY,
|
|
418
|
+
label TEXT NOT NULL,
|
|
419
|
+
brief TEXT NOT NULL,
|
|
420
|
+
-- 'chat' | 'agent'; parent_id is the chat key or the parent agent id.
|
|
421
|
+
parent_kind TEXT NOT NULL,
|
|
422
|
+
parent_id TEXT NOT NULL,
|
|
423
|
+
-- The frontend's numeric chat id (chat parents only) — the dispatcher
|
|
424
|
+
-- needs it to wake the chat with the report.
|
|
425
|
+
parent_numeric INTEGER,
|
|
426
|
+
backend_id TEXT NOT NULL,
|
|
427
|
+
-- The model the run resolved to, and the one the caller asked for (null
|
|
428
|
+
-- = the backend default), so a resume can re-resolve if it vanished.
|
|
429
|
+
model TEXT,
|
|
430
|
+
requested_model TEXT,
|
|
431
|
+
effort TEXT,
|
|
432
|
+
timeout_ms INTEGER,
|
|
433
|
+
depth INTEGER NOT NULL DEFAULT 0,
|
|
434
|
+
cwd TEXT,
|
|
435
|
+
state TEXT NOT NULL,
|
|
436
|
+
created_at INTEGER NOT NULL,
|
|
437
|
+
started_at INTEGER,
|
|
438
|
+
ended_at INTEGER,
|
|
439
|
+
updated_at INTEGER NOT NULL,
|
|
440
|
+
-- Backend conversation handle (Claude SDK session id, Codex thread id),
|
|
441
|
+
-- recorded as soon as the backend reports it.
|
|
442
|
+
session_id TEXT,
|
|
443
|
+
-- Pending check_inbox messages, JSON array of {from,text,at}.
|
|
444
|
+
inbox_json TEXT NOT NULL DEFAULT '[]',
|
|
445
|
+
-- 1 once report_result landed; result_* hold what it said.
|
|
446
|
+
reported INTEGER NOT NULL DEFAULT 0,
|
|
447
|
+
result_summary TEXT,
|
|
448
|
+
result_details TEXT,
|
|
449
|
+
error TEXT,
|
|
450
|
+
-- Wall-clock the agent has already spent in earlier (interrupted) runs,
|
|
451
|
+
-- charged against its timeout when it is resumed.
|
|
452
|
+
elapsed_ms INTEGER NOT NULL DEFAULT 0,
|
|
453
|
+
-- How many times a restart has resumed it (bounded — a run that keeps
|
|
454
|
+
-- dying with the daemon eventually settles as failed).
|
|
455
|
+
resume_count INTEGER NOT NULL DEFAULT 0,
|
|
456
|
+
-- When the last restart interrupted it, if one did.
|
|
457
|
+
interrupted_at INTEGER,
|
|
458
|
+
-- 1 when the spawn asked for the pre-flight lane, so a re-briefed resume
|
|
459
|
+
-- keeps the instruction.
|
|
460
|
+
preflight INTEGER NOT NULL DEFAULT 0
|
|
461
|
+
);
|
|
462
|
+
CREATE INDEX IF NOT EXISTS idx_agents_state ON agents(state, depth, created_at);
|
|
@@ -410,7 +410,93 @@ CREATE TABLE IF NOT EXISTS backup_remotes (
|
|
|
410
410
|
uploaded_at INTEGER,
|
|
411
411
|
error TEXT,
|
|
412
412
|
PRIMARY KEY (backup_id, target_id)
|
|
413
|
-
)
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
-- Sub-agents, persisted so a daemon restart does not kill them. One row
|
|
416
|
+
-- per spawn, written at registration and kept current on every lifecycle
|
|
417
|
+
-- change (start, SDK session id, mailbox, report, settle). On boot, every
|
|
418
|
+
-- row still 'queued' or 'running' was interrupted by the restart and is
|
|
419
|
+
-- respawned — resuming its backend session when the backend can. See
|
|
420
|
+
-- resumeAgentsAfterRestart in core/agents/runner.ts.
|
|
421
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
422
|
+
id TEXT PRIMARY KEY,
|
|
423
|
+
label TEXT NOT NULL,
|
|
424
|
+
brief TEXT NOT NULL,
|
|
425
|
+
-- 'chat' | 'agent'; parent_id is the chat key or the parent agent id.
|
|
426
|
+
parent_kind TEXT NOT NULL,
|
|
427
|
+
parent_id TEXT NOT NULL,
|
|
428
|
+
-- The frontend's numeric chat id (chat parents only) — the dispatcher
|
|
429
|
+
-- needs it to wake the chat with the report.
|
|
430
|
+
parent_numeric INTEGER,
|
|
431
|
+
backend_id TEXT NOT NULL,
|
|
432
|
+
-- The model the run resolved to, and the one the caller asked for (null
|
|
433
|
+
-- = the backend default), so a resume can re-resolve if it vanished.
|
|
434
|
+
model TEXT,
|
|
435
|
+
requested_model TEXT,
|
|
436
|
+
effort TEXT,
|
|
437
|
+
timeout_ms INTEGER,
|
|
438
|
+
depth INTEGER NOT NULL DEFAULT 0,
|
|
439
|
+
cwd TEXT,
|
|
440
|
+
state TEXT NOT NULL,
|
|
441
|
+
created_at INTEGER NOT NULL,
|
|
442
|
+
started_at INTEGER,
|
|
443
|
+
ended_at INTEGER,
|
|
444
|
+
updated_at INTEGER NOT NULL,
|
|
445
|
+
-- Backend conversation handle (Claude SDK session id, Codex thread id),
|
|
446
|
+
-- recorded as soon as the backend reports it.
|
|
447
|
+
session_id TEXT,
|
|
448
|
+
-- Pending check_inbox messages, JSON array of {from,text,at}.
|
|
449
|
+
inbox_json TEXT NOT NULL DEFAULT '[]',
|
|
450
|
+
-- 1 once report_result landed; result_* hold what it said.
|
|
451
|
+
reported INTEGER NOT NULL DEFAULT 0,
|
|
452
|
+
result_summary TEXT,
|
|
453
|
+
result_details TEXT,
|
|
454
|
+
error TEXT,
|
|
455
|
+
-- Wall-clock the agent has already spent in earlier (interrupted) runs,
|
|
456
|
+
-- charged against its timeout when it is resumed.
|
|
457
|
+
elapsed_ms INTEGER NOT NULL DEFAULT 0,
|
|
458
|
+
-- How many times a restart has resumed it (bounded — a run that keeps
|
|
459
|
+
-- dying with the daemon eventually settles as failed).
|
|
460
|
+
resume_count INTEGER NOT NULL DEFAULT 0,
|
|
461
|
+
-- When the last restart interrupted it, if one did.
|
|
462
|
+
interrupted_at INTEGER,
|
|
463
|
+
-- 1 when the spawn asked for the pre-flight lane, so a re-briefed resume
|
|
464
|
+
-- keeps the instruction.
|
|
465
|
+
preflight INTEGER NOT NULL DEFAULT 0
|
|
466
|
+
);
|
|
467
|
+
CREATE INDEX IF NOT EXISTS idx_agents_state ON agents(state, depth, created_at);`;
|
|
468
|
+
|
|
469
|
+
export const agentsSql = {
|
|
470
|
+
upsert: `INSERT OR REPLACE INTO agents
|
|
471
|
+
(id, label, brief, parent_kind, parent_id, parent_numeric, backend_id,
|
|
472
|
+
model, requested_model, effort, timeout_ms, depth, cwd, state,
|
|
473
|
+
created_at, started_at, ended_at, updated_at, session_id, inbox_json,
|
|
474
|
+
reported, result_summary, result_details, error, elapsed_ms,
|
|
475
|
+
resume_count, interrupted_at, preflight)
|
|
476
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
477
|
+
get: `SELECT id, label, brief, parent_kind, parent_id, parent_numeric, backend_id,
|
|
478
|
+
model, requested_model, effort, timeout_ms, depth, cwd, state,
|
|
479
|
+
created_at, started_at, ended_at, updated_at, session_id, inbox_json,
|
|
480
|
+
reported, result_summary, result_details, error, elapsed_ms,
|
|
481
|
+
resume_count, interrupted_at, preflight
|
|
482
|
+
FROM agents WHERE id = ?
|
|
483
|
+
|
|
484
|
+
-- Rows a restart interrupted, parents before children (a child can only
|
|
485
|
+
-- be re-attached to a parent that is already back in the registry).`,
|
|
486
|
+
listInterrupted: `SELECT id, label, brief, parent_kind, parent_id, parent_numeric, backend_id,
|
|
487
|
+
model, requested_model, effort, timeout_ms, depth, cwd, state,
|
|
488
|
+
created_at, started_at, ended_at, updated_at, session_id, inbox_json,
|
|
489
|
+
reported, result_summary, result_details, error, elapsed_ms,
|
|
490
|
+
resume_count, interrupted_at, preflight
|
|
491
|
+
FROM agents WHERE state IN ('queued', 'running')
|
|
492
|
+
ORDER BY depth, created_at`,
|
|
493
|
+
remove: `DELETE FROM agents WHERE id = ?
|
|
494
|
+
|
|
495
|
+
-- Retention: settled rows past their window. Live rows are never pruned.`,
|
|
496
|
+
pruneSettled: `DELETE FROM agents
|
|
497
|
+
WHERE state NOT IN ('queued', 'running') AND updated_at < ?`,
|
|
498
|
+
removeAll: `DELETE FROM agents`,
|
|
499
|
+
} as const;
|
|
414
500
|
|
|
415
501
|
export const backupsSql = {
|
|
416
502
|
upsert: `INSERT OR REPLACE INTO backups
|