@hyperdrive.bot/fleet-server 0.3.164 → 0.3.165
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/server/extensions/daemon-backend.js +25 -0
- package/dist/server/server/agent/agent-storage.d.ts +67 -12
- package/dist/server/server/agent/agent-storage.js +208 -132
- package/dist/server/server/agent/providers/claude/transport/pty-query.js +10 -0
- package/dist/server/server/bootstrap.d.ts +5 -0
- package/dist/server/server/bootstrap.js +26 -3
- package/dist/server/server/chat/chat-service.d.ts +22 -0
- package/dist/server/server/chat/chat-service.js +46 -3
- package/dist/server/server/daemon-worker.js +1 -0
- package/dist/server/server/fleet/decision-service.d.ts +5 -0
- package/dist/server/server/fleet/decision-service.js +30 -0
- package/dist/server/server/loop-service.d.ts +95 -0
- package/dist/server/server/loop-service.js +64 -3
- package/dist/server/server/migrations/backfill-workspace-id.migration.js +3 -2
- package/dist/server/server/push/token-store.d.ts +5 -1
- package/dist/server/server/push/token-store.js +26 -5
- package/dist/server/server/schedule/service.js +4 -1
- package/dist/server/server/schedule/store.d.ts +21 -1
- package/dist/server/server/schedule/store.js +283 -2
- package/dist/server/server/search/indexer.d.ts +10 -0
- package/dist/server/server/search/indexer.js +51 -2
- package/dist/server/server/session.js +12 -13
- package/dist/server/server/state/agent-state.d.ts +39 -0
- package/dist/server/server/state/agent-state.js +94 -0
- package/dist/server/server/state/daemon-state.d.ts +30 -0
- package/dist/server/server/state/daemon-state.js +99 -0
- package/dist/server/server/state/legacy-import.d.ts +124 -0
- package/dist/server/server/state/legacy-import.js +682 -0
- package/dist/server/server/state/legacy-manifest.d.ts +73 -0
- package/dist/server/server/state/legacy-manifest.js +112 -0
- package/dist/server/server/state/legacy-mirror.d.ts +57 -0
- package/dist/server/server/state/legacy-mirror.js +114 -0
- package/dist/server/server/state/legacy-sources.d.ts +9 -0
- package/dist/server/server/state/legacy-sources.js +163 -0
- package/dist/server/server/state/state-db.d.ts +263 -0
- package/dist/server/server/state/state-db.js +772 -0
- package/dist/server/server/state/state-schema.d.ts +39 -0
- package/dist/server/server/state/state-schema.js +125 -0
- package/dist/server/server/state/state-worker.d.ts +48 -0
- package/dist/server/server/state/state-worker.js +165 -0
- package/dist/server/server/websocket-server.js +2 -1
- package/dist/server/server/workspace/session-openable.js +7 -0
- package/dist/server/server/workspace-reconciliation-service.js +14 -3
- package/dist/server/server/workspace-registry.d.ts +45 -2
- package/dist/server/server/workspace-registry.js +75 -4
- package/dist/server/web-ui/_expo/static/js/web/{index-2ac1a7249c20c322a1f9a54563cfee62.js → index-38580ed8926a5ddc569d744b8299d339.js} +4 -4
- package/dist/server/web-ui/_expo/static/js/web/index-38580ed8926a5ddc569d744b8299d339.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-2ac1a7249c20c322a1f9a54563cfee62.js.gz → index-38580ed8926a5ddc569d744b8299d339.js.gz} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-2ac1a7249c20c322a1f9a54563cfee62.js.map.br → index-38580ed8926a5ddc569d744b8299d339.js.map.br} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-2ac1a7249c20c322a1f9a54563cfee62.js.map.gz → index-38580ed8926a5ddc569d744b8299d339.js.map.gz} +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-2ac1a7249c20c322a1f9a54563cfee62.js.br +0 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type Stats } from "node:fs";
|
|
2
|
+
import type { StateDb } from "./state-db.js";
|
|
3
|
+
import type { WorkerStatement } from "./state-worker.js";
|
|
4
|
+
/**
|
|
5
|
+
* What this daemon last left in each legacy JSON file.
|
|
6
|
+
*
|
|
7
|
+
* Every file the mirror writes, and every file the import reads, gets a row:
|
|
8
|
+
* content hash, size, mtime, and the daemon-wide `rev` the file reflects. That
|
|
9
|
+
* row is how the daemon tells its own files from everyone else's, file by file,
|
|
10
|
+
* with no global clock:
|
|
11
|
+
*
|
|
12
|
+
* - the file matches its row (same mtime and size, or same content hash):
|
|
13
|
+
* ours, never re-imported, whatever its mtime says;
|
|
14
|
+
* - the file differs from its row, or has no row: changed by someone else (a
|
|
15
|
+
* rolled-back older daemon, the daemon itself under PASEO_STATE_SQLITE=0, a
|
|
16
|
+
* hand edit), reconciled into the database by content;
|
|
17
|
+
* - the row's file is gone: deleted by someone else.
|
|
18
|
+
*
|
|
19
|
+
* `rev` settles conflicts: a database row whose rev is greater than the
|
|
20
|
+
* manifest rev of its file changed after that file was written, so on a
|
|
21
|
+
* conflict the database wins (and the next mirror write brings the file up).
|
|
22
|
+
*
|
|
23
|
+
* mtime + size is only a fast path to skip hashing a file that was not
|
|
24
|
+
* touched; a differing mtime alone never makes a file foreign.
|
|
25
|
+
*/
|
|
26
|
+
export interface ManifestEntry {
|
|
27
|
+
/** Absolute path. */
|
|
28
|
+
path: string;
|
|
29
|
+
store: string;
|
|
30
|
+
hash: string;
|
|
31
|
+
size: number;
|
|
32
|
+
mtimeMs: number;
|
|
33
|
+
rev: number;
|
|
34
|
+
}
|
|
35
|
+
export type LegacyFileState = {
|
|
36
|
+
status: "ours";
|
|
37
|
+
entry: ManifestEntry;
|
|
38
|
+
} | {
|
|
39
|
+
status: "changed" | "created";
|
|
40
|
+
entry: ManifestEntry | null;
|
|
41
|
+
content: string;
|
|
42
|
+
stat: Stats;
|
|
43
|
+
} | {
|
|
44
|
+
status: "deleted";
|
|
45
|
+
entry: ManifestEntry;
|
|
46
|
+
} | {
|
|
47
|
+
status: "absent";
|
|
48
|
+
};
|
|
49
|
+
export declare function hashContent(content: string | Buffer): string;
|
|
50
|
+
export declare class LegacyManifest {
|
|
51
|
+
private readonly state;
|
|
52
|
+
private readonly home;
|
|
53
|
+
constructor(state: StateDb, home: string);
|
|
54
|
+
private key;
|
|
55
|
+
private toEntry;
|
|
56
|
+
get(filePath: string): ManifestEntry | null;
|
|
57
|
+
/** Every entry of a store, keyed by absolute path. */
|
|
58
|
+
entries(store: string): Map<string, ManifestEntry>;
|
|
59
|
+
/** The upsert for a file whose `content` was just read or written, as a batch statement. */
|
|
60
|
+
statement(store: string, filePath: string, content: string | Buffer, stat: Pick<Stats, "size" | "mtimeMs">, rev: number): WorkerStatement;
|
|
61
|
+
forgetStatement(filePath: string): WorkerStatement;
|
|
62
|
+
/** Run a statement inside the caller's open transaction. */
|
|
63
|
+
runNow(statement: WorkerStatement): void;
|
|
64
|
+
/**
|
|
65
|
+
* Record the file as this daemon just left it, reflecting `rev`. Reads the
|
|
66
|
+
* file back, so the hash is exactly what is on disk. Resolves once committed.
|
|
67
|
+
*/
|
|
68
|
+
record(store: string, filePath: string, rev: number): Promise<void>;
|
|
69
|
+
forget(filePath: string): Promise<void>;
|
|
70
|
+
/** Compare a file on disk with its entry (looked up when not given). */
|
|
71
|
+
classify(filePath: string, entry?: ManifestEntry | null): Promise<LegacyFileState>;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=legacy-manifest.d.ts.map
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
export function hashContent(content) {
|
|
5
|
+
return createHash("sha1").update(content).digest("hex");
|
|
6
|
+
}
|
|
7
|
+
const UPSERT_SQL = `INSERT INTO legacy_manifest (path, store, hash, size, mtime_ms, rev)
|
|
8
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
9
|
+
ON CONFLICT(path) DO UPDATE SET store = excluded.store, hash = excluded.hash,
|
|
10
|
+
size = excluded.size, mtime_ms = excluded.mtime_ms, rev = excluded.rev`;
|
|
11
|
+
export class LegacyManifest {
|
|
12
|
+
constructor(state, home) {
|
|
13
|
+
this.state = state;
|
|
14
|
+
this.home = home;
|
|
15
|
+
}
|
|
16
|
+
key(filePath) {
|
|
17
|
+
return path.relative(this.home, path.resolve(filePath));
|
|
18
|
+
}
|
|
19
|
+
toEntry(row) {
|
|
20
|
+
return {
|
|
21
|
+
path: path.resolve(this.home, String(row.path)),
|
|
22
|
+
store: String(row.store),
|
|
23
|
+
hash: String(row.hash),
|
|
24
|
+
size: Number(row.size),
|
|
25
|
+
mtimeMs: Number(row.mtime_ms),
|
|
26
|
+
rev: Number(row.rev),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
get(filePath) {
|
|
30
|
+
this.state.flushSync();
|
|
31
|
+
const row = this.state
|
|
32
|
+
.prepare("SELECT * FROM legacy_manifest WHERE path = ?")
|
|
33
|
+
.get(this.key(filePath));
|
|
34
|
+
return row ? this.toEntry(row) : null;
|
|
35
|
+
}
|
|
36
|
+
/** Every entry of a store, keyed by absolute path. */
|
|
37
|
+
entries(store) {
|
|
38
|
+
this.state.flushSync();
|
|
39
|
+
const rows = this.state.prepare("SELECT * FROM legacy_manifest WHERE store = ?").all(store);
|
|
40
|
+
return new Map(rows.map((row) => [path.resolve(this.home, String(row.path)), this.toEntry(row)]));
|
|
41
|
+
}
|
|
42
|
+
/** The upsert for a file whose `content` was just read or written, as a batch statement. */
|
|
43
|
+
statement(store, filePath, content, stat, rev) {
|
|
44
|
+
return {
|
|
45
|
+
sql: UPSERT_SQL,
|
|
46
|
+
params: [this.key(filePath), store, hashContent(content), stat.size, stat.mtimeMs, rev],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
forgetStatement(filePath) {
|
|
50
|
+
return { sql: "DELETE FROM legacy_manifest WHERE path = ?", params: [this.key(filePath)] };
|
|
51
|
+
}
|
|
52
|
+
/** Run a statement inside the caller's open transaction. */
|
|
53
|
+
runNow(statement) {
|
|
54
|
+
this.state.prepare(statement.sql).run(...statement.params);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Record the file as this daemon just left it, reflecting `rev`. Reads the
|
|
58
|
+
* file back, so the hash is exactly what is on disk. Resolves once committed.
|
|
59
|
+
*/
|
|
60
|
+
async record(store, filePath, rev) {
|
|
61
|
+
let content;
|
|
62
|
+
let stat;
|
|
63
|
+
try {
|
|
64
|
+
content = await fs.readFile(filePath);
|
|
65
|
+
stat = await fs.stat(filePath);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
await this.forget(filePath);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const statement = this.statement(store, filePath, content, stat, rev);
|
|
72
|
+
await this.state.enqueue(() => this.runNow(statement));
|
|
73
|
+
}
|
|
74
|
+
async forget(filePath) {
|
|
75
|
+
const statement = this.forgetStatement(filePath);
|
|
76
|
+
await this.state.enqueue(() => this.runNow(statement));
|
|
77
|
+
}
|
|
78
|
+
/** Compare a file on disk with its entry (looked up when not given). */
|
|
79
|
+
async classify(filePath, entry) {
|
|
80
|
+
const known = entry === undefined ? this.get(filePath) : entry;
|
|
81
|
+
let stat;
|
|
82
|
+
try {
|
|
83
|
+
stat = await fs.stat(filePath);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return known ? { status: "deleted", entry: known } : { status: "absent" };
|
|
87
|
+
}
|
|
88
|
+
if (known && known.size === stat.size && known.mtimeMs === stat.mtimeMs) {
|
|
89
|
+
return { status: "ours", entry: known };
|
|
90
|
+
}
|
|
91
|
+
let content;
|
|
92
|
+
try {
|
|
93
|
+
content = await fs.readFile(filePath, "utf8");
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return known ? { status: "deleted", entry: known } : { status: "absent" };
|
|
97
|
+
}
|
|
98
|
+
if (known && hashContent(content) === known.hash) {
|
|
99
|
+
// Same bytes, new mtime (a copy, a touch): still ours. Remember the mtime
|
|
100
|
+
// so the next look is a stat again.
|
|
101
|
+
const refreshed = this.statement(known.store, filePath, content, stat, known.rev);
|
|
102
|
+
void this.state
|
|
103
|
+
.enqueue(() => this.runNow(refreshed))
|
|
104
|
+
.catch((error) => {
|
|
105
|
+
this.state.logger.warn({ err: error, filePath }, "Failed to refresh a legacy manifest entry");
|
|
106
|
+
});
|
|
107
|
+
return { status: "ours", entry: known };
|
|
108
|
+
}
|
|
109
|
+
return { status: known ? "changed" : "created", entry: known, content, stat };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=legacy-manifest.js.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Logger } from "pino";
|
|
2
|
+
/**
|
|
3
|
+
* Keeps the legacy JSON files written after `state.sqlite` took over, for one
|
|
4
|
+
* release, so that:
|
|
5
|
+
* - the updater's automatic rollback (cli daemon/update.ts, 180s readiness
|
|
6
|
+
* budget) lands on an older daemon that reads CURRENT files, not a frozen
|
|
7
|
+
* snapshot from the upgrade moment;
|
|
8
|
+
* - scripts that read the files directly keep working (week-report
|
|
9
|
+
* extract.py reads agents/, loops run-agent.sh reads
|
|
10
|
+
* projects/workspaces.json, paseo-new-host provision.sh greps projects.json).
|
|
11
|
+
*
|
|
12
|
+
* Every write is off the request path: debounced per key (with a max wait, so
|
|
13
|
+
* a hot key still flushes), latest task wins, serialized per key, failures
|
|
14
|
+
* logged and never thrown. On SIGTERM/SIGINT the daemon switches the mirror to
|
|
15
|
+
* immediate mode and flushes before anything else shuts down; only a SIGKILL
|
|
16
|
+
* (or a power cut) inside the last debounce window, at most about a second,
|
|
17
|
+
* can leave a file behind the database. The next boot of this daemon catches
|
|
18
|
+
* those files up. Remove in the release after next (see
|
|
19
|
+
* docs/architecture/data-layer-rework.md section 5).
|
|
20
|
+
*/
|
|
21
|
+
export declare class LegacyMirror {
|
|
22
|
+
readonly enabled: boolean;
|
|
23
|
+
private readonly logger;
|
|
24
|
+
private readonly debounceMs;
|
|
25
|
+
private readonly maxWaitMs;
|
|
26
|
+
private readonly queued;
|
|
27
|
+
private readonly running;
|
|
28
|
+
private immediate;
|
|
29
|
+
constructor(options: {
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
logger: Logger;
|
|
32
|
+
debounceMs?: number;
|
|
33
|
+
maxWaitMs?: number;
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* Debounce `task` under `key`; a later call for the same key replaces it.
|
|
37
|
+
* The first call of a burst fixes a deadline `maxWaitMs` out, which later
|
|
38
|
+
* calls never push back.
|
|
39
|
+
*/
|
|
40
|
+
write(key: string, task: () => Promise<void>): void;
|
|
41
|
+
/** True while a write for `key` is debounced or still running. */
|
|
42
|
+
isPending(key: string): boolean;
|
|
43
|
+
/** True while a write for `key` is running (not merely debounced). */
|
|
44
|
+
isRunning(key: string): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Shutdown: run everything queued now, and every later write without a
|
|
47
|
+
* debounce, so nothing written on the way down waits on a timer.
|
|
48
|
+
*/
|
|
49
|
+
beginShutdown(): Promise<void>;
|
|
50
|
+
/** Drop queued writes without running them. Tests use it to simulate a crash. */
|
|
51
|
+
discard(): void;
|
|
52
|
+
get pendingCount(): number;
|
|
53
|
+
/** Run every debounced task now and wait for all of them. */
|
|
54
|
+
flush(): Promise<void>;
|
|
55
|
+
private run;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=legacy-mirror.d.ts.map
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const DEFAULT_DEBOUNCE_MS = 250;
|
|
2
|
+
/**
|
|
3
|
+
* A key written more often than the debounce still reaches its file at least
|
|
4
|
+
* this often (or every four debounces, when that is sooner), so a streaming agent's legacy file is never more than about a
|
|
5
|
+
* second behind.
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_MAX_WAIT_MS = 1000;
|
|
8
|
+
/**
|
|
9
|
+
* Keeps the legacy JSON files written after `state.sqlite` took over, for one
|
|
10
|
+
* release, so that:
|
|
11
|
+
* - the updater's automatic rollback (cli daemon/update.ts, 180s readiness
|
|
12
|
+
* budget) lands on an older daemon that reads CURRENT files, not a frozen
|
|
13
|
+
* snapshot from the upgrade moment;
|
|
14
|
+
* - scripts that read the files directly keep working (week-report
|
|
15
|
+
* extract.py reads agents/, loops run-agent.sh reads
|
|
16
|
+
* projects/workspaces.json, paseo-new-host provision.sh greps projects.json).
|
|
17
|
+
*
|
|
18
|
+
* Every write is off the request path: debounced per key (with a max wait, so
|
|
19
|
+
* a hot key still flushes), latest task wins, serialized per key, failures
|
|
20
|
+
* logged and never thrown. On SIGTERM/SIGINT the daemon switches the mirror to
|
|
21
|
+
* immediate mode and flushes before anything else shuts down; only a SIGKILL
|
|
22
|
+
* (or a power cut) inside the last debounce window, at most about a second,
|
|
23
|
+
* can leave a file behind the database. The next boot of this daemon catches
|
|
24
|
+
* those files up. Remove in the release after next (see
|
|
25
|
+
* docs/architecture/data-layer-rework.md section 5).
|
|
26
|
+
*/
|
|
27
|
+
export class LegacyMirror {
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.queued = new Map();
|
|
30
|
+
this.running = new Map();
|
|
31
|
+
this.immediate = false;
|
|
32
|
+
this.enabled = options.enabled;
|
|
33
|
+
this.logger = options.logger;
|
|
34
|
+
this.debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
35
|
+
this.maxWaitMs = Math.max(this.debounceMs, options.maxWaitMs ?? Math.min(DEFAULT_MAX_WAIT_MS, 4 * this.debounceMs));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Debounce `task` under `key`; a later call for the same key replaces it.
|
|
39
|
+
* The first call of a burst fixes a deadline `maxWaitMs` out, which later
|
|
40
|
+
* calls never push back.
|
|
41
|
+
*/
|
|
42
|
+
write(key, task) {
|
|
43
|
+
if (!this.enabled) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const existing = this.queued.get(key);
|
|
48
|
+
if (existing) {
|
|
49
|
+
clearTimeout(existing.timer);
|
|
50
|
+
}
|
|
51
|
+
const firstQueuedAt = existing?.firstQueuedAt ?? now;
|
|
52
|
+
const delay = this.immediate
|
|
53
|
+
? 0
|
|
54
|
+
: Math.max(0, Math.min(this.debounceMs, firstQueuedAt + this.maxWaitMs - now));
|
|
55
|
+
const timer = setTimeout(() => this.run(key), delay);
|
|
56
|
+
timer.unref?.();
|
|
57
|
+
this.queued.set(key, { task, timer, firstQueuedAt });
|
|
58
|
+
}
|
|
59
|
+
/** True while a write for `key` is debounced or still running. */
|
|
60
|
+
isPending(key) {
|
|
61
|
+
return this.queued.has(key) || this.running.has(key);
|
|
62
|
+
}
|
|
63
|
+
/** True while a write for `key` is running (not merely debounced). */
|
|
64
|
+
isRunning(key) {
|
|
65
|
+
return this.running.has(key);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Shutdown: run everything queued now, and every later write without a
|
|
69
|
+
* debounce, so nothing written on the way down waits on a timer.
|
|
70
|
+
*/
|
|
71
|
+
beginShutdown() {
|
|
72
|
+
this.immediate = true;
|
|
73
|
+
return this.flush();
|
|
74
|
+
}
|
|
75
|
+
/** Drop queued writes without running them. Tests use it to simulate a crash. */
|
|
76
|
+
discard() {
|
|
77
|
+
for (const entry of this.queued.values())
|
|
78
|
+
clearTimeout(entry.timer);
|
|
79
|
+
this.queued.clear();
|
|
80
|
+
}
|
|
81
|
+
get pendingCount() {
|
|
82
|
+
return this.queued.size + this.running.size;
|
|
83
|
+
}
|
|
84
|
+
/** Run every debounced task now and wait for all of them. */
|
|
85
|
+
async flush() {
|
|
86
|
+
for (const [key, entry] of Array.from(this.queued)) {
|
|
87
|
+
clearTimeout(entry.timer);
|
|
88
|
+
this.run(key);
|
|
89
|
+
}
|
|
90
|
+
while (this.running.size > 0) {
|
|
91
|
+
await Promise.allSettled(this.running.values());
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
run(key) {
|
|
95
|
+
const entry = this.queued.get(key);
|
|
96
|
+
if (!entry) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.queued.delete(key);
|
|
100
|
+
const previous = this.running.get(key) ?? Promise.resolve();
|
|
101
|
+
const next = previous
|
|
102
|
+
.then(() => entry.task())
|
|
103
|
+
.catch((error) => {
|
|
104
|
+
this.logger.warn({ err: error, key }, "Legacy file mirror write failed");
|
|
105
|
+
})
|
|
106
|
+
.finally(() => {
|
|
107
|
+
if (this.running.get(key) === next) {
|
|
108
|
+
this.running.delete(key);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
this.running.set(key, next);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=legacy-mirror.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { LegacySource } from "./legacy-import.js";
|
|
2
|
+
/**
|
|
3
|
+
* Every legacy store `state.sqlite` takes over, except agents (imported by
|
|
4
|
+
* `importLegacyAgents` from the agent storage directory). Each parser is the
|
|
5
|
+
* store's own zod schema, so an import accepts exactly what the file store
|
|
6
|
+
* would have loaded.
|
|
7
|
+
*/
|
|
8
|
+
export declare function legacySources(paseoHome: string): LegacySource[];
|
|
9
|
+
//# sourceMappingURL=legacy-sources.d.ts.map
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { LoopDecisionSchema } from "@hyperdrive.bot/fleet-protocol/fleet/decisions";
|
|
3
|
+
import { StoredScheduleSchema } from "@hyperdrive.bot/fleet-protocol/schedule/types";
|
|
4
|
+
import { ChatStorePayloadSchema } from "../chat/chat-service.js";
|
|
5
|
+
import { LOOP_LOGS_RETAINED, StoredLoopsSchema } from "../loop-service.js";
|
|
6
|
+
import { capScheduleRuns } from "../schedule/store.js";
|
|
7
|
+
import { PersistedProjectRecordListSchema, PersistedWorkspaceRecordListSchema, } from "../workspace-registry.js";
|
|
8
|
+
/**
|
|
9
|
+
* Every legacy store `state.sqlite` takes over, except agents (imported by
|
|
10
|
+
* `importLegacyAgents` from the agent storage directory). Each parser is the
|
|
11
|
+
* store's own zod schema, so an import accepts exactly what the file store
|
|
12
|
+
* would have loaded.
|
|
13
|
+
*/
|
|
14
|
+
export function legacySources(paseoHome) {
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
kind: "directory",
|
|
18
|
+
store: "schedules",
|
|
19
|
+
dir: path.join(paseoHome, "schedules"),
|
|
20
|
+
deleteWins: true,
|
|
21
|
+
owns: (fileName) => {
|
|
22
|
+
const id = fileName.slice(0, -".json".length);
|
|
23
|
+
return [
|
|
24
|
+
{ table: "schedules", scope: "", id },
|
|
25
|
+
{ table: "schedule_runs", scope: id },
|
|
26
|
+
];
|
|
27
|
+
},
|
|
28
|
+
toRows: (json) => {
|
|
29
|
+
const { runs, ...schedule } = capScheduleRuns(StoredScheduleSchema.parse(json));
|
|
30
|
+
return [
|
|
31
|
+
{ table: "schedules", scope: "", id: schedule.id, value: schedule },
|
|
32
|
+
...runs.map((run, index) => ({
|
|
33
|
+
table: "schedule_runs",
|
|
34
|
+
scope: schedule.id,
|
|
35
|
+
id: run.id,
|
|
36
|
+
value: run,
|
|
37
|
+
ord: index,
|
|
38
|
+
})),
|
|
39
|
+
];
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
kind: "whole-file",
|
|
44
|
+
store: "loops",
|
|
45
|
+
filePath: path.join(paseoHome, "loops", "loops.json"),
|
|
46
|
+
tables: ["loops", "loop_logs"],
|
|
47
|
+
toRows: (json) => {
|
|
48
|
+
const rows = [];
|
|
49
|
+
for (const { logs, ...loop } of StoredLoopsSchema.parse(json)) {
|
|
50
|
+
rows.push({ table: "loops", scope: "", id: loop.id, value: loop });
|
|
51
|
+
logs.slice(-LOOP_LOGS_RETAINED).forEach((entry, index) => {
|
|
52
|
+
rows.push({
|
|
53
|
+
table: "loop_logs",
|
|
54
|
+
scope: loop.id,
|
|
55
|
+
id: String(entry.seq).padStart(10, "0"),
|
|
56
|
+
value: entry,
|
|
57
|
+
ord: index,
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return rows;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
kind: "whole-file",
|
|
66
|
+
store: "chat",
|
|
67
|
+
filePath: path.join(paseoHome, "chat", "rooms.json"),
|
|
68
|
+
tables: ["chat_rooms", "chat_messages"],
|
|
69
|
+
toRows: (json) => {
|
|
70
|
+
const payload = ChatStorePayloadSchema.parse(json);
|
|
71
|
+
const order = new Map();
|
|
72
|
+
return [
|
|
73
|
+
...payload.rooms.map((room) => ({
|
|
74
|
+
table: "chat_rooms",
|
|
75
|
+
scope: "",
|
|
76
|
+
id: room.id,
|
|
77
|
+
value: room,
|
|
78
|
+
})),
|
|
79
|
+
...payload.messages.map((message) => {
|
|
80
|
+
const ord = order.get(message.roomId) ?? 0;
|
|
81
|
+
order.set(message.roomId, ord + 1);
|
|
82
|
+
return {
|
|
83
|
+
table: "chat_messages",
|
|
84
|
+
scope: message.roomId,
|
|
85
|
+
id: message.id,
|
|
86
|
+
value: message,
|
|
87
|
+
ord,
|
|
88
|
+
};
|
|
89
|
+
}),
|
|
90
|
+
];
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
kind: "whole-file",
|
|
95
|
+
store: "projects",
|
|
96
|
+
filePath: path.join(paseoHome, "projects", "projects.json"),
|
|
97
|
+
tables: ["projects"],
|
|
98
|
+
toRows: (json) => PersistedProjectRecordListSchema.parse(json).map((record) => ({
|
|
99
|
+
table: "projects",
|
|
100
|
+
scope: "",
|
|
101
|
+
id: record.projectId,
|
|
102
|
+
value: record,
|
|
103
|
+
})),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
kind: "whole-file",
|
|
107
|
+
store: "workspaces",
|
|
108
|
+
filePath: path.join(paseoHome, "projects", "workspaces.json"),
|
|
109
|
+
tables: ["workspaces"],
|
|
110
|
+
toRows: (json) => PersistedWorkspaceRecordListSchema.parse(json).map((record) => ({
|
|
111
|
+
table: "workspaces",
|
|
112
|
+
scope: "",
|
|
113
|
+
id: record.workspaceId,
|
|
114
|
+
value: record,
|
|
115
|
+
})),
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
kind: "whole-file",
|
|
119
|
+
store: "push_tokens",
|
|
120
|
+
filePath: path.join(paseoHome, "push-tokens.json"),
|
|
121
|
+
tables: ["push_tokens"],
|
|
122
|
+
toRows: (json) => {
|
|
123
|
+
const tokens = json?.tokens;
|
|
124
|
+
return (Array.isArray(tokens) ? tokens : [])
|
|
125
|
+
.filter((token) => typeof token === "string" && token.trim() !== "")
|
|
126
|
+
.map((token) => ({
|
|
127
|
+
table: "push_tokens",
|
|
128
|
+
scope: "",
|
|
129
|
+
id: token.trim(),
|
|
130
|
+
value: { token: token.trim() },
|
|
131
|
+
}));
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
kind: "directory",
|
|
136
|
+
store: "fleet_decisions",
|
|
137
|
+
dir: path.join(paseoHome, "fleet", "decisions"),
|
|
138
|
+
owns: (fileName) => [
|
|
139
|
+
{ table: "fleet_decisions", scope: "", id: fileName.slice(0, -".json".length) },
|
|
140
|
+
],
|
|
141
|
+
toRows: (json) => {
|
|
142
|
+
const decision = LoopDecisionSchema.parse(json);
|
|
143
|
+
return [{ table: "fleet_decisions", scope: "", id: decision.id, value: decision }];
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
kind: "directory",
|
|
148
|
+
store: "extension_kv",
|
|
149
|
+
dir: path.join(paseoHome, "extension-storage"),
|
|
150
|
+
owns: (fileName) => [{ table: "extension_kv", scope: fileName.slice(0, -".json".length) }],
|
|
151
|
+
toRows: (json, fileName) => {
|
|
152
|
+
const extensionId = fileName.slice(0, -".json".length);
|
|
153
|
+
return Object.entries((json ?? {})).map(([key, value]) => ({
|
|
154
|
+
table: "extension_kv",
|
|
155
|
+
scope: extensionId,
|
|
156
|
+
id: key,
|
|
157
|
+
value: value ?? null,
|
|
158
|
+
}));
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=legacy-sources.js.map
|