talon-agent 5.2.2 → 5.4.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/README.md +10 -6
- package/package.json +1 -1
- package/src/app.ts +78 -0
- package/src/backend/agy/auth.ts +128 -0
- package/src/backend/agy/constants.ts +88 -0
- package/src/backend/agy/doctor.ts +161 -0
- package/src/backend/agy/effort.ts +76 -0
- package/src/backend/agy/events.ts +402 -0
- package/src/backend/agy/factory.ts +137 -0
- package/src/backend/agy/handler/index.ts +9 -0
- package/src/backend/agy/handler/message.ts +444 -0
- package/src/backend/agy/init.ts +64 -0
- package/src/backend/agy/mcp/config.ts +336 -0
- package/src/backend/agy/mcp/register.ts +134 -0
- package/src/backend/agy/models.ts +335 -0
- package/src/backend/agy/one-shot.ts +301 -0
- package/src/backend/agy/process/child.ts +378 -0
- package/src/backend/agy/process/orphans.ts +106 -0
- package/src/backend/agy/sessions.ts +90 -0
- package/src/backend/agy/state.ts +77 -0
- package/src/backend/builtins.ts +1 -0
- package/src/backend/codex/mcp-config.ts +1 -1
- package/src/backend/openai-agents/mcp-pool.ts +1 -1
- package/src/backend/runtime/index.ts +1 -1
- package/src/cli/commands/backup.ts +396 -0
- package/src/cli/config-view.ts +5 -0
- package/src/cli/config.ts +4 -2
- package/src/cli/events.ts +14 -0
- package/src/cli/index.ts +64 -45
- package/src/cli/setup.ts +49 -0
- package/src/core/agent-runtime/model-ref.ts +1 -0
- package/src/core/backup/archive/digest.ts +77 -0
- package/src/core/backup/archive/tar.ts +567 -0
- package/src/core/backup/archive/zstd.ts +31 -0
- package/src/core/backup/index.ts +54 -0
- package/src/core/backup/plan.ts +273 -0
- package/src/core/backup/restore.ts +410 -0
- package/src/core/backup/scheduler.ts +357 -0
- package/src/core/backup/snapshot.ts +408 -0
- package/src/core/backup/status.ts +194 -0
- package/src/core/backup/store.ts +312 -0
- package/src/core/backup/targets.ts +281 -0
- package/src/core/backup/types.ts +96 -0
- package/src/core/backup/upload.ts +172 -0
- package/src/core/bus/events.ts +45 -1
- package/src/core/config/index.ts +53 -0
- package/src/core/engine/gateway-actions/backup/index.ts +129 -0
- package/src/core/engine/gateway-actions/index.ts +4 -0
- package/src/core/mcp-hub/talon-server.ts +1 -1
- package/src/core/plugin/actions.ts +34 -0
- package/src/core/plugin/index.ts +5 -1
- package/src/core/tools/{ops/bridge.ts → bridge.ts} +7 -2
- package/src/core/tools/index.ts +2 -0
- package/src/core/tools/ops/backup.ts +67 -0
- package/src/core/tools/types.ts +2 -1
- package/src/core/update/self-update.ts +3 -0
- package/src/frontend/discord/callbacks/components/index.ts +3 -0
- package/src/frontend/discord/commands/backup.ts +203 -0
- package/src/frontend/discord/commands/definitions.ts +35 -0
- package/src/frontend/discord/commands/router.ts +3 -0
- package/src/frontend/telegram/callbacks/backup.ts +55 -0
- package/src/frontend/telegram/callbacks/index.ts +8 -0
- package/src/frontend/telegram/commands/backup.ts +209 -0
- package/src/frontend/telegram/commands/definitions.ts +4 -0
- package/src/frontend/telegram/commands/index.ts +2 -0
- package/src/storage/backup/index.ts +82 -0
- package/src/storage/backup/repo.ts +164 -0
- package/src/storage/db.ts +20 -0
- package/src/storage/sql/backups.sql +46 -0
- package/src/storage/sql/db.sql +8 -0
- package/src/storage/sql/schema.sql +30 -0
- package/src/storage/sql/statements.generated.ts +60 -1
- package/src/util/log.ts +1 -0
- /package/src/core/tools/{ops/mcp-env.ts → mcp-env.ts} +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backups and checkpoints — the subsystem's public surface.
|
|
3
|
+
*
|
|
4
|
+
* A snapshot is everything that makes this deployment itself: config,
|
|
5
|
+
* prompts, keys, sessions, the database (via `VACUUM INTO`, never a
|
|
6
|
+
* byte-wise copy), the agent's memory and skills, and the memory palace
|
|
7
|
+
* as its own content-addressed part. Scheduled snapshots are pruned by
|
|
8
|
+
* retention; checkpoints are labelled, optionally pinned, and taken
|
|
9
|
+
* before anything risky (a self-update, a restore).
|
|
10
|
+
*
|
|
11
|
+
* This barrel is what the surfaces above use — the CLI, the `/backup`
|
|
12
|
+
* commands, the gateway actions, bootstrap and app. Inside the
|
|
13
|
+
* subsystem the modules import each other directly.
|
|
14
|
+
*
|
|
15
|
+
* Read them in this order: `plan` (what goes in), `archive/` (how it is
|
|
16
|
+
* written), `snapshot` (the build), `store` (the local store and its
|
|
17
|
+
* index), `targets` + `upload` (getting it off the machine), `scheduler`
|
|
18
|
+
* (when), `restore` (getting it back), `status` (what every surface
|
|
19
|
+
* renders). docs/backups.md has the operator's view and the plugin
|
|
20
|
+
* protocol.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export {
|
|
24
|
+
initBackup,
|
|
25
|
+
runBackup,
|
|
26
|
+
stopBackupScheduler,
|
|
27
|
+
checkpointBeforeUpdate,
|
|
28
|
+
} from "./scheduler.js";
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
isSnapshotId,
|
|
32
|
+
listSnapshots,
|
|
33
|
+
readManifest,
|
|
34
|
+
setSnapshotPinned,
|
|
35
|
+
} from "./store.js";
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
applyPendingRestore,
|
|
39
|
+
readRestorePending,
|
|
40
|
+
restoreSnapshot,
|
|
41
|
+
writeRestorePending,
|
|
42
|
+
} from "./restore.js";
|
|
43
|
+
|
|
44
|
+
export { discoverTargets, type BackupTarget } from "./targets.js";
|
|
45
|
+
|
|
46
|
+
export {
|
|
47
|
+
collectBackupStatus,
|
|
48
|
+
formatBackupStatus,
|
|
49
|
+
formatBytes,
|
|
50
|
+
formatRelative,
|
|
51
|
+
formatSnapshotList,
|
|
52
|
+
} from "./status.js";
|
|
53
|
+
|
|
54
|
+
export type { SnapshotSummary } from "./types.js";
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What goes into a snapshot — the include/exclude rules and the walker
|
|
3
|
+
* that turns them into a list of archive members.
|
|
4
|
+
*
|
|
5
|
+
* The rules answer one question: if this machine died, what would we need
|
|
6
|
+
* to rebuild the same agent? Identity (config, prompts, keys, sessions),
|
|
7
|
+
* everything the agent wrote about itself (memory, skills, scripts), and
|
|
8
|
+
* nothing that can be re-fetched or re-derived (node_modules, venvs,
|
|
9
|
+
* browser downloads, logs, traces). The database is deliberately absent
|
|
10
|
+
* here: a live SQLite file copied byte-wise is a corrupt SQLite file, so
|
|
11
|
+
* the builder adds it via `VACUUM INTO` instead (see snapshot.ts).
|
|
12
|
+
*
|
|
13
|
+
* `~/.talon/ns` is excluded by name and never stat()ed. It is a FUSE
|
|
14
|
+
* mount that can be dead ("Transport endpoint is not connected"), and on
|
|
15
|
+
* a dead mount a stat blocks or throws — a backup must not be the thing
|
|
16
|
+
* that hangs on it.
|
|
17
|
+
*
|
|
18
|
+
* Pure except for the walker: the rules are plain string predicates so
|
|
19
|
+
* they can be tested without a filesystem.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { lstat, readdir, readlink } from "node:fs/promises";
|
|
23
|
+
import { isAbsolute, join, resolve, sep } from "node:path";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
import type { BackupSettings } from "./types.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The workspace is mostly machine-generated bulk (uploads, media, build
|
|
29
|
+
* output, project checkouts). These are the parts that are the agent:
|
|
30
|
+
* what it knows, what it learned to do, and who it decided to be.
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_WORKSPACE_INCLUDE: readonly string[] = [
|
|
33
|
+
"identity.md",
|
|
34
|
+
"memory.md",
|
|
35
|
+
"state.md",
|
|
36
|
+
"heartbeat-instructions.md",
|
|
37
|
+
"memory/**",
|
|
38
|
+
"skills/**",
|
|
39
|
+
"scripts/**",
|
|
40
|
+
"secrets/**",
|
|
41
|
+
"stickers/**",
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The policy defaults — the single source of truth the zod schema in
|
|
46
|
+
* core/config defers to, so `config.backup` and an absent `config.backup`
|
|
47
|
+
* mean exactly the same thing.
|
|
48
|
+
*/
|
|
49
|
+
export const DEFAULT_BACKUP_SETTINGS = {
|
|
50
|
+
enabled: true,
|
|
51
|
+
intervalHours: 6,
|
|
52
|
+
keepLocal: 12,
|
|
53
|
+
keepRemote: 30,
|
|
54
|
+
includePalace: true,
|
|
55
|
+
workspaceInclude: DEFAULT_WORKSPACE_INCLUDE,
|
|
56
|
+
extraPaths: [] as readonly string[],
|
|
57
|
+
checkpointBeforeUpdate: true,
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
/** Fill in whatever `config.backup` left out (or was entirely absent). */
|
|
61
|
+
export function resolveBackupSettings(
|
|
62
|
+
partial?: Partial<BackupSettings>,
|
|
63
|
+
): BackupSettings {
|
|
64
|
+
return { ...DEFAULT_BACKUP_SETTINGS, ...partial };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Roots under ~/.talon that a snapshot always carries, in archive order. */
|
|
68
|
+
export const HOME_INCLUDES: readonly string[] = [
|
|
69
|
+
"config.json",
|
|
70
|
+
"prompts",
|
|
71
|
+
"data",
|
|
72
|
+
"keys",
|
|
73
|
+
"whatsapp-auth",
|
|
74
|
+
".user-session",
|
|
75
|
+
"mesh-devices.json",
|
|
76
|
+
"mesh-history.json",
|
|
77
|
+
"teleport-state.json",
|
|
78
|
+
"agent-workspace",
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
/** The exclusion rules, in the words the manifest records them by. */
|
|
82
|
+
export const EXCLUDE_RULES: readonly string[] = [
|
|
83
|
+
"talon.log*",
|
|
84
|
+
"errors.log",
|
|
85
|
+
"node-bin/",
|
|
86
|
+
"*venv*/",
|
|
87
|
+
"ns/ (FUSE mount — never stat()ed)",
|
|
88
|
+
"backups/",
|
|
89
|
+
"data/traces/**",
|
|
90
|
+
"data/talon.db* (the database is added via VACUUM INTO)",
|
|
91
|
+
"*.tmp-*",
|
|
92
|
+
"workspace/palace/** (its own part)",
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* True when an archive path must not be captured. `path` is the path the
|
|
97
|
+
* member would have INSIDE the archive, which for the ~/.talon roots is
|
|
98
|
+
* also its path relative to the Talon home.
|
|
99
|
+
*/
|
|
100
|
+
export function isExcluded(path: string): boolean {
|
|
101
|
+
const segments = path.split("/").filter(Boolean);
|
|
102
|
+
if (segments.length === 0) return true;
|
|
103
|
+
const first = segments[0];
|
|
104
|
+
const last = segments[segments.length - 1];
|
|
105
|
+
// Anchored rules — only at the root of the archive.
|
|
106
|
+
if (first === "ns" || first === "backups") return true;
|
|
107
|
+
if (
|
|
108
|
+
first === "data" &&
|
|
109
|
+
(segments[1] === "traces" || segments[1]?.startsWith("talon.db"))
|
|
110
|
+
) {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
segments.length === 1 &&
|
|
115
|
+
(first.startsWith("talon.log") || first === "errors.log")
|
|
116
|
+
) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
if (path === "workspace/palace" || path.startsWith("workspace/palace/"))
|
|
120
|
+
return true;
|
|
121
|
+
// Rules that hold at any depth: build output, virtualenvs, half-written
|
|
122
|
+
// files from an atomic write that never landed.
|
|
123
|
+
if (
|
|
124
|
+
segments.some(
|
|
125
|
+
(s) => s === "node-bin" || s === "node_modules" || s.includes("venv"),
|
|
126
|
+
)
|
|
127
|
+
) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
if (last.includes(".tmp-")) return true;
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Match one workspace-relative path against the `workspaceInclude` list.
|
|
136
|
+
* The pattern language is deliberately two rules wide — an exact relative
|
|
137
|
+
* path, or a `dir/**` prefix — because that is all the config needs and a
|
|
138
|
+
* glob engine is a dependency plus a surprise.
|
|
139
|
+
*/
|
|
140
|
+
export function matchesWorkspaceInclude(
|
|
141
|
+
relative: string,
|
|
142
|
+
patterns: readonly string[],
|
|
143
|
+
): boolean {
|
|
144
|
+
for (const pattern of patterns) {
|
|
145
|
+
if (pattern.endsWith("/**")) {
|
|
146
|
+
const prefix = pattern.slice(0, -3);
|
|
147
|
+
if (relative === prefix || relative.startsWith(`${prefix}/`)) return true;
|
|
148
|
+
} else if (pattern === relative) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The workspace subtrees to walk, derived from the include patterns. */
|
|
156
|
+
export function workspaceRoots(patterns: readonly string[]): string[] {
|
|
157
|
+
return [
|
|
158
|
+
...new Set(patterns.map((p) => (p.endsWith("/**") ? p.slice(0, -3) : p))),
|
|
159
|
+
];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Expand a leading `~` and resolve against the home directory. */
|
|
163
|
+
export function expandUserPath(path: string): string {
|
|
164
|
+
const trimmed = path.trim();
|
|
165
|
+
if (trimmed === "~") return homedir();
|
|
166
|
+
if (trimmed.startsWith("~/")) return resolve(homedir(), trimmed.slice(2));
|
|
167
|
+
return isAbsolute(trimmed) ? resolve(trimmed) : resolve(homedir(), trimmed);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** One member the builder will hand to the tar writer. */
|
|
171
|
+
export type SourceEntry = {
|
|
172
|
+
/** Path inside the archive (POSIX separators). */
|
|
173
|
+
archivePath: string;
|
|
174
|
+
/** Absolute path on disk. */
|
|
175
|
+
source: string;
|
|
176
|
+
type: "file" | "dir" | "symlink";
|
|
177
|
+
mode: number;
|
|
178
|
+
/** Epoch seconds. */
|
|
179
|
+
mtime: number;
|
|
180
|
+
size: number;
|
|
181
|
+
linkTarget?: string;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Walk one root into archive members. Missing roots are skipped silently —
|
|
186
|
+
* a fresh install has no whatsapp-auth/, and that is not an error. An
|
|
187
|
+
* unreadable entry is skipped too: a backup that aborts because one file
|
|
188
|
+
* lost its permissions is a backup that never runs.
|
|
189
|
+
*/
|
|
190
|
+
export async function collectTree(
|
|
191
|
+
absRoot: string,
|
|
192
|
+
archiveRoot: string,
|
|
193
|
+
opts: {
|
|
194
|
+
/** Defaults to {@link isExcluded}; the palace part overrides it. */
|
|
195
|
+
exclude?: (archivePath: string) => boolean;
|
|
196
|
+
onSkip?: (path: string, err: unknown) => void;
|
|
197
|
+
} = {},
|
|
198
|
+
): Promise<SourceEntry[]> {
|
|
199
|
+
const { exclude = isExcluded, onSkip } = opts;
|
|
200
|
+
const entries: SourceEntry[] = [];
|
|
201
|
+
const visit = async (abs: string, archivePath: string): Promise<void> => {
|
|
202
|
+
if (exclude(archivePath)) return;
|
|
203
|
+
let stats;
|
|
204
|
+
try {
|
|
205
|
+
stats = await lstat(abs);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
// A root that does not exist is the normal case on a fresh install
|
|
208
|
+
// (no whatsapp-auth/, no agent-workspace/) — only real failures
|
|
209
|
+
// (permissions, I/O) are worth a line in the log.
|
|
210
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") onSkip?.(abs, err);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const mtime = Math.floor(stats.mtimeMs / 1000);
|
|
214
|
+
const mode = stats.mode & 0o7777;
|
|
215
|
+
if (stats.isSymbolicLink()) {
|
|
216
|
+
try {
|
|
217
|
+
entries.push({
|
|
218
|
+
archivePath,
|
|
219
|
+
source: abs,
|
|
220
|
+
type: "symlink",
|
|
221
|
+
mode,
|
|
222
|
+
mtime,
|
|
223
|
+
size: 0,
|
|
224
|
+
linkTarget: await readlink(abs),
|
|
225
|
+
});
|
|
226
|
+
} catch (err) {
|
|
227
|
+
onSkip?.(abs, err);
|
|
228
|
+
}
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (stats.isDirectory()) {
|
|
232
|
+
entries.push({
|
|
233
|
+
archivePath,
|
|
234
|
+
source: abs,
|
|
235
|
+
type: "dir",
|
|
236
|
+
mode,
|
|
237
|
+
mtime,
|
|
238
|
+
size: 0,
|
|
239
|
+
});
|
|
240
|
+
let children: string[] = [];
|
|
241
|
+
try {
|
|
242
|
+
children = (await readdir(abs)).sort();
|
|
243
|
+
} catch (err) {
|
|
244
|
+
onSkip?.(abs, err);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
for (const child of children) {
|
|
248
|
+
await visit(join(abs, child), `${archivePath}/${child}`);
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (!stats.isFile()) return; // sockets, fifos, devices: not agent state
|
|
253
|
+
entries.push({
|
|
254
|
+
archivePath,
|
|
255
|
+
source: abs,
|
|
256
|
+
type: "file",
|
|
257
|
+
mode,
|
|
258
|
+
mtime,
|
|
259
|
+
size: stats.size,
|
|
260
|
+
});
|
|
261
|
+
};
|
|
262
|
+
await visit(absRoot, archiveRoot);
|
|
263
|
+
return entries;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** True when `path` is inside `root` (or is `root`). */
|
|
267
|
+
export function isInside(root: string, path: string): boolean {
|
|
268
|
+
const normalizedRoot = resolve(root);
|
|
269
|
+
const normalized = resolve(path);
|
|
270
|
+
return (
|
|
271
|
+
normalized === normalizedRoot || normalized.startsWith(normalizedRoot + sep)
|
|
272
|
+
);
|
|
273
|
+
}
|