talon-agent 5.2.1 → 5.3.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 +2 -2
- package/src/app.ts +94 -1
- 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/events.ts +14 -0
- package/src/cli/index.ts +64 -45
- 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 +52 -0
- package/src/core/daemon/handoff.ts +192 -0
- package/src/core/daemon/respawn.ts +127 -52
- 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 +47 -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/index.ts +13 -5
- package/src/plugins/playwright/index.ts +39 -3
- package/src/plugins/playwright/provision.ts +21 -0
- package/src/plugins/playwright/version-coupling.ts +195 -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 +129 -81
- package/src/util/paths.ts +5 -0
- /package/src/core/tools/{ops/mcp-env.ts → mcp-env.ts} +0 -0
|
@@ -366,7 +366,60 @@ CREATE TABLE IF NOT EXISTS whatsapp_messages (
|
|
|
366
366
|
);
|
|
367
367
|
CREATE INDEX IF NOT EXISTS idx_whatsapp_messages_msg ON whatsapp_messages(msg_id);
|
|
368
368
|
CREATE INDEX IF NOT EXISTS idx_whatsapp_messages_wa_id ON whatsapp_messages(wa_id);
|
|
369
|
-
CREATE INDEX IF NOT EXISTS idx_whatsapp_messages_time ON whatsapp_messages(timestamp)
|
|
369
|
+
CREATE INDEX IF NOT EXISTS idx_whatsapp_messages_time ON whatsapp_messages(timestamp);
|
|
370
|
+
|
|
371
|
+
-- Snapshot index: the listing/status view over ~/.talon/backups/. The
|
|
372
|
+
-- manifest.json next to the parts on disk stays the source of truth for
|
|
373
|
+
-- a restore (a database that needs restoring cannot also be the record
|
|
374
|
+
-- of how), so these rows are a cache — dropped rows are re-derived from
|
|
375
|
+
-- the directories on the next boot, and a row whose directory is gone is
|
|
376
|
+
-- kept because the snapshot may still exist on a remote target.
|
|
377
|
+
CREATE TABLE IF NOT EXISTS backups (
|
|
378
|
+
id TEXT PRIMARY KEY,
|
|
379
|
+
kind TEXT NOT NULL,
|
|
380
|
+
label TEXT,
|
|
381
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
382
|
+
created_at INTEGER NOT NULL,
|
|
383
|
+
size_bytes INTEGER NOT NULL DEFAULT 0,
|
|
384
|
+
manifest_json TEXT NOT NULL
|
|
385
|
+
);
|
|
386
|
+
CREATE INDEX IF NOT EXISTS idx_backups_created ON backups(created_at DESC);
|
|
387
|
+
|
|
388
|
+
-- Per-target upload state for one snapshot. Separate from the manifest
|
|
389
|
+
-- copy so retention can ask "what is on Drive?" without opening a file
|
|
390
|
+
-- per snapshot, and so a failed upload's error survives a restart.
|
|
391
|
+
CREATE TABLE IF NOT EXISTS backup_remotes (
|
|
392
|
+
backup_id TEXT NOT NULL,
|
|
393
|
+
target_id TEXT NOT NULL,
|
|
394
|
+
status TEXT NOT NULL,
|
|
395
|
+
remote_id TEXT,
|
|
396
|
+
uploaded_at INTEGER,
|
|
397
|
+
error TEXT,
|
|
398
|
+
PRIMARY KEY (backup_id, target_id)
|
|
399
|
+
);`;
|
|
400
|
+
|
|
401
|
+
export const backupsSql = {
|
|
402
|
+
upsert: `INSERT OR REPLACE INTO backups
|
|
403
|
+
(id, kind, label, pinned, created_at, size_bytes, manifest_json)
|
|
404
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
405
|
+
get: `SELECT id, kind, label, pinned, created_at, size_bytes, manifest_json
|
|
406
|
+
FROM backups WHERE id = ?`,
|
|
407
|
+
all: `SELECT id, kind, label, pinned, created_at, size_bytes, manifest_json
|
|
408
|
+
FROM backups ORDER BY created_at DESC`,
|
|
409
|
+
ids: `SELECT id FROM backups`,
|
|
410
|
+
setPinned: `UPDATE backups SET pinned = ? WHERE id = ?`,
|
|
411
|
+
setManifest: `UPDATE backups SET manifest_json = ?, pinned = ?, size_bytes = ? WHERE id = ?`,
|
|
412
|
+
remove: `DELETE FROM backups WHERE id = ?`,
|
|
413
|
+
upsertRemote: `INSERT OR REPLACE INTO backup_remotes
|
|
414
|
+
(backup_id, target_id, status, remote_id, uploaded_at, error)
|
|
415
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
416
|
+
remotesAll: `SELECT backup_id, target_id, status, remote_id, uploaded_at, error
|
|
417
|
+
FROM backup_remotes`,
|
|
418
|
+
remotesFor: `SELECT backup_id, target_id, status, remote_id, uploaded_at, error
|
|
419
|
+
FROM backup_remotes WHERE backup_id = ?`,
|
|
420
|
+
removeRemotes: `DELETE FROM backup_remotes WHERE backup_id = ?`,
|
|
421
|
+
removeRemote: `DELETE FROM backup_remotes WHERE backup_id = ? AND target_id = ?`,
|
|
422
|
+
} as const;
|
|
370
423
|
|
|
371
424
|
export const chatSettingsSql = {
|
|
372
425
|
upsert: `INSERT OR REPLACE INTO chat_settings (chat_id, settings) VALUES (?, ?)`,
|
|
@@ -421,6 +474,12 @@ ALTER TABLE history_messages ADD COLUMN attachments TEXT`,
|
|
|
421
474
|
addSessionsLastTurnEndedAtColumn: `-- Column reconciliation for databases that shipped before the cache-age
|
|
422
475
|
-- signal existed. Fresh databases get the column via schema.sql.
|
|
423
476
|
ALTER TABLE sessions ADD COLUMN last_turn_ended_at INTEGER`,
|
|
477
|
+
vacuumInto: `-- Transactionally consistent copy of the whole database into a new file,
|
|
478
|
+
-- produced by SQLite itself (storage/db.ts snapshotDatabase). A backup
|
|
479
|
+
-- must never copy a live .db byte-wise: the WAL holds committed pages the
|
|
480
|
+
-- main file does not, so the copy would be a corrupt database or an old
|
|
481
|
+
-- one. Verified to accept a bound path on node:sqlite and bun:sqlite.
|
|
482
|
+
VACUUM INTO ?`,
|
|
424
483
|
} as const;
|
|
425
484
|
|
|
426
485
|
export const goalsSql = {
|
package/src/util/log.ts
CHANGED
|
@@ -16,9 +16,10 @@ import {
|
|
|
16
16
|
statSync,
|
|
17
17
|
renameSync,
|
|
18
18
|
unlinkSync,
|
|
19
|
-
|
|
19
|
+
openSync,
|
|
20
|
+
writeSync,
|
|
21
|
+
closeSync,
|
|
20
22
|
} from "node:fs";
|
|
21
|
-
import { Writable } from "node:stream";
|
|
22
23
|
import { dirs, files } from "./paths.js";
|
|
23
24
|
|
|
24
25
|
export type LogComponent =
|
|
@@ -66,6 +67,7 @@ export type LogComponent =
|
|
|
66
67
|
| "playwright"
|
|
67
68
|
| "memory"
|
|
68
69
|
| "stickers"
|
|
70
|
+
| "backup"
|
|
69
71
|
| "backend-controller";
|
|
70
72
|
|
|
71
73
|
const LOG_FILE = files.log;
|
|
@@ -81,20 +83,29 @@ if (!existsSync(dirs.root)) {
|
|
|
81
83
|
|
|
82
84
|
// Rotate log file on startup if it exceeds 10MB
|
|
83
85
|
const MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Move `path` aside to `path.old` when it has outgrown the cap. Used at
|
|
89
|
+
* import time for talon.log and at handoff time for respawn.log, so both
|
|
90
|
+
* files follow the same one-generation rule. Never throws.
|
|
91
|
+
*/
|
|
92
|
+
function rotateIfLarge(path: string): void {
|
|
93
|
+
try {
|
|
94
|
+
if (!existsSync(path) || statSync(path).size <= MAX_LOG_SIZE) return;
|
|
95
|
+
const rotated = `${path}.old`;
|
|
87
96
|
try {
|
|
88
97
|
unlinkSync(rotated);
|
|
89
98
|
} catch {
|
|
90
|
-
/*
|
|
99
|
+
/* no previous generation */
|
|
91
100
|
}
|
|
92
|
-
renameSync(
|
|
101
|
+
renameSync(path, rotated);
|
|
102
|
+
} catch {
|
|
103
|
+
/* a log file we cannot rotate is still a log file we can append to */
|
|
93
104
|
}
|
|
94
|
-
} catch {
|
|
95
|
-
/* ignore */
|
|
96
105
|
}
|
|
97
106
|
|
|
107
|
+
rotateIfLarge(LOG_FILE);
|
|
108
|
+
|
|
98
109
|
// Suppress console output for terminal frontend (stdout belongs to the REPL)
|
|
99
110
|
let quiet = process.env.TALON_QUIET === "1";
|
|
100
111
|
if (!quiet) {
|
|
@@ -149,9 +160,21 @@ const SINK_RETRY_MS = 30_000;
|
|
|
149
160
|
/** Ceiling for the doubling backoff between reopen attempts. */
|
|
150
161
|
const SINK_MAX_RETRY_MS = 5 * 60_000;
|
|
151
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Where a {@link ResilientFileSink} puts its lines. Writing is
|
|
165
|
+
* synchronous by contract: a line that `write()` returned from is on the
|
|
166
|
+
* fd, not in a userspace queue. See the class doc for why that matters.
|
|
167
|
+
*/
|
|
168
|
+
export type SyncLogTarget = {
|
|
169
|
+
/** Append one already-serialized line. Throws on failure. */
|
|
170
|
+
write(line: string): void;
|
|
171
|
+
/** Release the underlying handle. Never throws. */
|
|
172
|
+
close(): void;
|
|
173
|
+
};
|
|
174
|
+
|
|
152
175
|
export type ResilientFileSinkOptions = {
|
|
153
|
-
/** Opens the underlying file
|
|
154
|
-
open?: (path: string) =>
|
|
176
|
+
/** Opens the underlying file. Injection seam for tests. */
|
|
177
|
+
open?: (path: string) => SyncLogTarget;
|
|
155
178
|
/** Where the pause/resume notices go. Defaults to the console sink. */
|
|
156
179
|
notify?: (level: "warn" | "info", message: string) => void;
|
|
157
180
|
/** First backoff step (default 30s). */
|
|
@@ -161,20 +184,34 @@ export type ResilientFileSinkOptions = {
|
|
|
161
184
|
};
|
|
162
185
|
|
|
163
186
|
/**
|
|
164
|
-
* A log file destination that cannot take the process down
|
|
187
|
+
* A log file destination that cannot take the process down, and cannot
|
|
188
|
+
* lose the last thing the process said.
|
|
165
189
|
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
190
|
+
* Two failures shaped this class, both on 2026-09-18.
|
|
191
|
+
*
|
|
192
|
+
* First, a bare `createWriteStream` handed to `pino.multistream` is a
|
|
193
|
+
* loaded gun: when the disk fills (ENOSPC), or the file is unlinked
|
|
194
|
+
* under a rotation (EBADF), or the fd goes bad (EIO), the stream emits
|
|
195
|
+
* `error`. With no listener that is an uncaught exception — raised from
|
|
170
196
|
* inside a log call, so the crash handler's own `logError` runs on a
|
|
171
|
-
* logger that is already broken.
|
|
172
|
-
*
|
|
173
|
-
*
|
|
197
|
+
* logger that is already broken. A full disk killed the daemon that
|
|
198
|
+
* way: no shutdown, no pidfile cleanup, no successor.
|
|
199
|
+
*
|
|
200
|
+
* Second — and this is why the writes below are synchronous — a
|
|
201
|
+
* `createWriteStream` buffers in userspace and drains on later ticks,
|
|
202
|
+
* while every terminal log line Talon writes is immediately followed by
|
|
203
|
+
* `process.exit()`: "State saved", "Respawn child started", "Timeout
|
|
204
|
+
* exceeded, forcing exit", "Fatal startup error". `process.exit()`
|
|
205
|
+
* discards whatever is still queued, so precisely the lines that explain
|
|
206
|
+
* a failed handoff were the ones that never reached the file. Measured
|
|
207
|
+
* under Bun 1.3.9: three lines logged and then `process.exit(0)` produced
|
|
208
|
+
* an empty (not even created) log file. A sink that writes with
|
|
209
|
+
* `writeSync` has nothing to flush and nothing to lose — no flush step to
|
|
210
|
+
* remember at any exit site, which is the only version of this that stays
|
|
211
|
+
* fixed.
|
|
174
212
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* - write failures pause file logging and destroy the broken stream,
|
|
213
|
+
* pino only ever sees this object, which never throws and never blocks:
|
|
214
|
+
* - write failures pause file logging and close the broken handle,
|
|
178
215
|
* - lines written while paused are DROPPED and counted (never
|
|
179
216
|
* buffered — the failure mode here is "no space", so growing a
|
|
180
217
|
* buffer is the last thing to do),
|
|
@@ -184,26 +221,21 @@ export type ResilientFileSinkOptions = {
|
|
|
184
221
|
* The console sink keeps working throughout, and carries the two
|
|
185
222
|
* notices.
|
|
186
223
|
*/
|
|
187
|
-
export class ResilientFileSink
|
|
224
|
+
export class ResilientFileSink {
|
|
188
225
|
private readonly path: string;
|
|
189
|
-
private readonly
|
|
226
|
+
private readonly openTarget: (path: string) => SyncLogTarget;
|
|
190
227
|
private readonly notify: (level: "warn" | "info", message: string) => void;
|
|
191
228
|
private readonly baseRetryMs: number;
|
|
192
229
|
private readonly maxRetryMs: number;
|
|
193
|
-
private
|
|
230
|
+
private target: SyncLogTarget | null = null;
|
|
194
231
|
private retryMs: number;
|
|
195
232
|
private retryTimer: ReturnType<typeof setTimeout> | null = null;
|
|
196
233
|
private droppedWhileDown = 0;
|
|
197
234
|
private down = false;
|
|
198
235
|
|
|
199
236
|
constructor(path: string, opts: ResilientFileSinkOptions = {}) {
|
|
200
|
-
// decodeStrings:false keeps pino's serialized lines as strings;
|
|
201
|
-
// autoDestroy:false means a downstream failure can never tear this
|
|
202
|
-
// object down — it is the only thing standing between a broken file
|
|
203
|
-
// and the process.
|
|
204
|
-
super({ decodeStrings: false, autoDestroy: false });
|
|
205
237
|
this.path = path;
|
|
206
|
-
this.
|
|
238
|
+
this.openTarget = opts.open ?? openSyncLogFile;
|
|
207
239
|
this.notify = opts.notify ?? notifyViaConsoleSink;
|
|
208
240
|
this.baseRetryMs = opts.retryMs ?? SINK_RETRY_MS;
|
|
209
241
|
this.maxRetryMs = opts.maxRetryMs ?? SINK_MAX_RETRY_MS;
|
|
@@ -221,63 +253,41 @@ export class ResilientFileSink extends Writable {
|
|
|
221
253
|
return this.down;
|
|
222
254
|
}
|
|
223
255
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
): void {
|
|
229
|
-
const inner = this.inner;
|
|
230
|
-
if (inner === null) {
|
|
256
|
+
/** pino.multistream's entire contract: one serialized line in. */
|
|
257
|
+
write(line: string): void {
|
|
258
|
+
const target = this.target;
|
|
259
|
+
if (target === null) {
|
|
231
260
|
this.droppedWhileDown++;
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
inner.write(chunk as string, (err) => {
|
|
235
|
-
if (!err) this.markHealthy();
|
|
236
|
-
});
|
|
237
|
-
} catch (err) {
|
|
238
|
-
// Synchronous throw (write-after-destroy on a stream we have not
|
|
239
|
-
// been told about yet) — same handling as an `error` event.
|
|
240
|
-
this.fail(err);
|
|
241
|
-
}
|
|
261
|
+
return;
|
|
242
262
|
}
|
|
243
|
-
// Always report success: pino must never see this sink fail, and
|
|
244
|
-
// backpressure here would stall whoever called log().
|
|
245
|
-
callback();
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
override _final(callback: (error?: Error | null) => void): void {
|
|
249
|
-
this.clearRetry();
|
|
250
263
|
try {
|
|
251
|
-
|
|
252
|
-
} catch {
|
|
253
|
-
|
|
264
|
+
target.write(line);
|
|
265
|
+
} catch (err) {
|
|
266
|
+
this.droppedWhileDown++;
|
|
267
|
+
this.fail(err);
|
|
268
|
+
return;
|
|
254
269
|
}
|
|
255
|
-
|
|
270
|
+
this.markHealthy();
|
|
256
271
|
}
|
|
257
272
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
273
|
+
/** No-op: every write already reached the fd. Part of pino's shape. */
|
|
274
|
+
flushSync(): void {}
|
|
275
|
+
|
|
276
|
+
/** Close the file and stop retrying. Part of pino's shape. */
|
|
277
|
+
end(): void {
|
|
262
278
|
this.clearRetry();
|
|
263
279
|
this.detachInner();
|
|
264
|
-
callback(null);
|
|
265
280
|
}
|
|
266
281
|
|
|
267
282
|
private openInner(): void {
|
|
268
283
|
try {
|
|
269
|
-
|
|
270
|
-
inner.on("error", (err: Error) => {
|
|
271
|
-
// Ignore errors from a stream we have already given up on.
|
|
272
|
-
if (this.inner === inner) this.fail(err);
|
|
273
|
-
});
|
|
274
|
-
this.inner = inner;
|
|
284
|
+
this.target = this.openTarget(this.path);
|
|
275
285
|
} catch (err) {
|
|
276
286
|
this.fail(err);
|
|
277
287
|
}
|
|
278
288
|
}
|
|
279
289
|
|
|
280
|
-
/** Give up on the current
|
|
290
|
+
/** Give up on the current handle and arm a reopen. Never throws. */
|
|
281
291
|
private fail(err: unknown): void {
|
|
282
292
|
const firstFailure = !this.down;
|
|
283
293
|
this.detachInner();
|
|
@@ -311,15 +321,11 @@ export class ResilientFileSink extends Writable {
|
|
|
311
321
|
}
|
|
312
322
|
|
|
313
323
|
private detachInner(): void {
|
|
314
|
-
const
|
|
315
|
-
this.
|
|
316
|
-
if (
|
|
324
|
+
const target = this.target;
|
|
325
|
+
this.target = null;
|
|
326
|
+
if (target === null) return;
|
|
317
327
|
try {
|
|
318
|
-
|
|
319
|
-
// destroy() can surface one last error — swallow it here rather
|
|
320
|
-
// than let it reach process-level uncaughtException.
|
|
321
|
-
inner.on("error", () => {});
|
|
322
|
-
inner.destroy();
|
|
328
|
+
target.close();
|
|
323
329
|
} catch {
|
|
324
330
|
/* best effort */
|
|
325
331
|
}
|
|
@@ -358,9 +364,51 @@ export class ResilientFileSink extends Writable {
|
|
|
358
364
|
}
|
|
359
365
|
}
|
|
360
366
|
|
|
361
|
-
|
|
367
|
+
/** Append `line` to `fd`, looping over a short write. Throws on failure. */
|
|
368
|
+
function appendLine(fd: number, line: string): void {
|
|
369
|
+
const buf = Buffer.from(line, "utf-8");
|
|
370
|
+
let offset = 0;
|
|
371
|
+
while (offset < buf.length) {
|
|
372
|
+
offset += writeSync(fd, buf, offset, buf.length - offset);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** The production {@link SyncLogTarget}: an appended fd, written with writeSync. */
|
|
377
|
+
export function openSyncLogFile(path: string): SyncLogTarget {
|
|
362
378
|
// 0600: turns and tool output land here — same sensitivity as history.
|
|
363
|
-
|
|
379
|
+
const fd = openSync(path, "a", 0o600);
|
|
380
|
+
return {
|
|
381
|
+
write: (line) => appendLine(fd, line),
|
|
382
|
+
close: () => {
|
|
383
|
+
try {
|
|
384
|
+
closeSync(fd);
|
|
385
|
+
} catch {
|
|
386
|
+
/* already gone */
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Open ~/.talon/respawn.log for a successor's stdout+stderr.
|
|
394
|
+
*
|
|
395
|
+
* A `/restart` or `/update` handoff spawns the next daemon detached; with
|
|
396
|
+
* `stdio: "ignore"` a successor that dies before its logger exists — a
|
|
397
|
+
* broken import after a dependency install, a fatal bind, a runtime that
|
|
398
|
+
* aborts — leaves no trace anywhere, which is exactly how the 2026-09-18
|
|
399
|
+
* handoff vanished. Handing the child this fd makes that visible. Same
|
|
400
|
+
* one-generation rotation as talon.log.
|
|
401
|
+
*
|
|
402
|
+
* Returns null when the file cannot be opened; the caller falls back to
|
|
403
|
+
* nothing, never to a failed handoff.
|
|
404
|
+
*/
|
|
405
|
+
export function openRespawnLog(path: string = files.respawnLog): number | null {
|
|
406
|
+
try {
|
|
407
|
+
rotateIfLarge(path);
|
|
408
|
+
return openSync(path, "a", 0o600);
|
|
409
|
+
} catch {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
364
412
|
}
|
|
365
413
|
|
|
366
414
|
function notifyViaConsoleSink(level: "warn" | "info", message: string): void {
|
package/src/util/paths.ts
CHANGED
|
@@ -102,6 +102,11 @@ export const files = {
|
|
|
102
102
|
config: resolve(TALON_ROOT, "config.json"),
|
|
103
103
|
/** Structured log: ~/.talon/talon.log */
|
|
104
104
|
log: resolve(TALON_ROOT, "talon.log"),
|
|
105
|
+
/**
|
|
106
|
+
* Successor stdout+stderr during a `/restart` or `/update` handoff:
|
|
107
|
+
* ~/.talon/respawn.log. See core/daemon/respawn.ts.
|
|
108
|
+
*/
|
|
109
|
+
respawnLog: resolve(TALON_ROOT, "respawn.log"),
|
|
105
110
|
/** Legacy JSON session store (imported into talon.db on first boot) */
|
|
106
111
|
sessions: resolve(TALON_ROOT, "data", "sessions.json"),
|
|
107
112
|
/** SQLite database (history, sessions, chat settings, media index): ~/.talon/data/talon.db */
|
|
File without changes
|