@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,99 @@
|
|
|
1
|
+
import { existsSync, promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { SqliteAgentRecordBackend } from "./agent-state.js";
|
|
4
|
+
import { importLegacyState } from "./legacy-import.js";
|
|
5
|
+
import { legacySources } from "./legacy-sources.js";
|
|
6
|
+
import { attachStateDb, detachStateDb, openStateDb, STATE_DB_FILENAME, STATE_SQLITE_ENV, } from "./state-db.js";
|
|
7
|
+
/**
|
|
8
|
+
* Free space the first import needs, as a multiple of the legacy state it
|
|
9
|
+
* copies: the database itself (about 1x on a real install) plus the WAL, which
|
|
10
|
+
* grows to about 1.4x while checkpoints are paused during the import.
|
|
11
|
+
*/
|
|
12
|
+
export const IMPORT_SPACE_FACTOR = 2.5;
|
|
13
|
+
async function freeBytesAt(dir) {
|
|
14
|
+
const stats = await fs.statfs(dir);
|
|
15
|
+
return Number(stats.bavail) * Number(stats.bsize);
|
|
16
|
+
}
|
|
17
|
+
async function dirBytes(dir, depth) {
|
|
18
|
+
let total = 0;
|
|
19
|
+
let entries;
|
|
20
|
+
try {
|
|
21
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const full = path.join(dir, entry.name);
|
|
28
|
+
if (entry.isDirectory() && depth > 0)
|
|
29
|
+
total += await dirBytes(full, depth - 1);
|
|
30
|
+
else if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
31
|
+
total += await fs.stat(full).then((stat) => stat.size, () => 0);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return total;
|
|
35
|
+
}
|
|
36
|
+
/** Bytes of every legacy store the first import copies. */
|
|
37
|
+
async function legacyStateBytes(paseoHome, agentStoragePath) {
|
|
38
|
+
let total = await dirBytes(agentStoragePath, 1);
|
|
39
|
+
for (const source of legacySources(paseoHome)) {
|
|
40
|
+
total +=
|
|
41
|
+
source.kind === "directory"
|
|
42
|
+
? await dirBytes(source.dir, 0)
|
|
43
|
+
: await fs.stat(source.filePath).then((stat) => stat.size, () => 0);
|
|
44
|
+
}
|
|
45
|
+
return total;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Open `state.sqlite` for a daemon, copy the legacy stores in (first boot) or
|
|
49
|
+
* re-sync files an older daemon changed (after a rollback), and attach it for
|
|
50
|
+
* the stores constructed deeper down. See data-layer-rework.md section 5.
|
|
51
|
+
*/
|
|
52
|
+
export async function openDaemonState(options) {
|
|
53
|
+
const { paseoHome, agentStoragePath, logger } = options;
|
|
54
|
+
const none = {
|
|
55
|
+
stateDb: null,
|
|
56
|
+
agentBackend: undefined,
|
|
57
|
+
close: async () => undefined,
|
|
58
|
+
};
|
|
59
|
+
const firstImport = !existsSync(path.join(paseoHome, STATE_DB_FILENAME));
|
|
60
|
+
if (firstImport && process.env[STATE_SQLITE_ENV] !== "0") {
|
|
61
|
+
const need = Math.ceil(IMPORT_SPACE_FACTOR * (await legacyStateBytes(paseoHome, agentStoragePath)));
|
|
62
|
+
const free = await (options.freeBytes ?? (() => freeBytesAt(paseoHome)))().catch(() => Infinity);
|
|
63
|
+
if (free < need) {
|
|
64
|
+
logger.warn({ freeBytes: free, neededBytes: need }, "Not enough free disk space to move daemon state into state.sqlite; staying on the legacy file stores");
|
|
65
|
+
return none;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const stateDb = openStateDb({ paseoHome, logger });
|
|
69
|
+
if (!stateDb) {
|
|
70
|
+
return none;
|
|
71
|
+
}
|
|
72
|
+
const report = await importLegacyState(stateDb, {
|
|
73
|
+
agentStoragePath,
|
|
74
|
+
sources: legacySources(paseoHome),
|
|
75
|
+
logger,
|
|
76
|
+
});
|
|
77
|
+
attachStateDb(paseoHome, stateDb);
|
|
78
|
+
logger.info({
|
|
79
|
+
elapsed: options.elapsed(),
|
|
80
|
+
importMs: Math.round(report.totalMs),
|
|
81
|
+
maxBatchMs: Math.round(report.maxBatchMs),
|
|
82
|
+
p99BatchMs: Math.round(report.p99BatchMs),
|
|
83
|
+
stores: report.stores
|
|
84
|
+
.filter((store) => store.mode !== "skip")
|
|
85
|
+
.map((store) => `${store.store}:${store.mode}:${store.imported}/${store.skipped}`),
|
|
86
|
+
mirror: stateDb.mirror.enabled,
|
|
87
|
+
}, "State store ready");
|
|
88
|
+
return {
|
|
89
|
+
stateDb,
|
|
90
|
+
agentBackend: new SqliteAgentRecordBackend(stateDb, agentStoragePath, logger, report.agentCatchUp),
|
|
91
|
+
close: async () => {
|
|
92
|
+
detachStateDb(paseoHome);
|
|
93
|
+
await stateDb.close().catch((error) => {
|
|
94
|
+
logger.warn({ err: error }, "Failed to close state.sqlite cleanly");
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=daemon-state.js.map
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Logger } from "pino";
|
|
2
|
+
import type { StateDb } from "./state-db.js";
|
|
3
|
+
import { type DocTableName } from "./state-schema.js";
|
|
4
|
+
/**
|
|
5
|
+
* First-boot copy of the legacy JSON stores into `state.sqlite`.
|
|
6
|
+
*
|
|
7
|
+
* - COPY, never move: no legacy file is deleted or renamed, so a rollback to an
|
|
8
|
+
* older daemon still finds everything (and the mirror keeps it current).
|
|
9
|
+
* - Batched and yielding: each batch is one transaction and then a
|
|
10
|
+
* `setImmediate`, so the event loop is never held for long.
|
|
11
|
+
* - Resumable: each store keeps a marker whose cursor commits in the same
|
|
12
|
+
* transaction as the rows it covers. A crash mid-import resumes after the
|
|
13
|
+
* last committed batch; re-running a finished store is a no-op.
|
|
14
|
+
* - Re-sync by content, not by clock: every file read here or written by the
|
|
15
|
+
* mirror has a row in the legacy manifest (`legacy-manifest.ts`). At the next
|
|
16
|
+
* boot a file is foreign only when its content no longer matches that row; a
|
|
17
|
+
* foreign file is reconciled row by row (the file wins, unless the database
|
|
18
|
+
* row changed after the file was written), and a file that is gone deletes
|
|
19
|
+
* what it held. Files this daemon wrote and nobody touched are never read.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Files per import batch. Parsing and validating 50 agent records is about 5ms
|
|
23
|
+
* of main-thread work; the commit itself runs on the state worker.
|
|
24
|
+
*/
|
|
25
|
+
export declare const AGENT_IMPORT_BATCH = 50;
|
|
26
|
+
export interface StoreImportResult {
|
|
27
|
+
store: string;
|
|
28
|
+
imported: number;
|
|
29
|
+
skipped: number;
|
|
30
|
+
ms: number;
|
|
31
|
+
/** "import" on a first copy, "resync" when re-reading changed files, "skip" when current. */
|
|
32
|
+
mode: "import" | "resync" | "skip";
|
|
33
|
+
}
|
|
34
|
+
export interface LegacyImportReport {
|
|
35
|
+
totalMs: number;
|
|
36
|
+
maxBatchMs: number;
|
|
37
|
+
p99BatchMs: number;
|
|
38
|
+
batches: number;
|
|
39
|
+
stores: StoreImportResult[];
|
|
40
|
+
}
|
|
41
|
+
export interface DocImportRow {
|
|
42
|
+
table: DocTableName;
|
|
43
|
+
scope: string;
|
|
44
|
+
id: string;
|
|
45
|
+
value: unknown;
|
|
46
|
+
ord?: number;
|
|
47
|
+
}
|
|
48
|
+
/** A store whose whole state is one legacy JSON file (loops.json, rooms.json, ...). */
|
|
49
|
+
export interface WholeFileSource {
|
|
50
|
+
kind: "whole-file";
|
|
51
|
+
store: string;
|
|
52
|
+
filePath: string;
|
|
53
|
+
/** Tables this file fully describes; a re-sync removes rows the file no longer has. */
|
|
54
|
+
tables: DocTableName[];
|
|
55
|
+
toRows(json: unknown): DocImportRow[];
|
|
56
|
+
}
|
|
57
|
+
/** A store kept as one JSON file per entity in a directory. */
|
|
58
|
+
export interface DirectorySource {
|
|
59
|
+
kind: "directory";
|
|
60
|
+
store: string;
|
|
61
|
+
dir: string;
|
|
62
|
+
toRows(json: unknown, fileName: string): DocImportRow[];
|
|
63
|
+
/** The rows one file fully describes (an omitted id means the whole scope). */
|
|
64
|
+
owns(fileName: string): RowOwnership[];
|
|
65
|
+
/** A foreign deletion beats a database edit made after the last mirror (schedules). */
|
|
66
|
+
deleteWins?: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface RowOwnership {
|
|
69
|
+
table: DocTableName;
|
|
70
|
+
scope?: string;
|
|
71
|
+
id?: string;
|
|
72
|
+
}
|
|
73
|
+
export type LegacySource = WholeFileSource | DirectorySource;
|
|
74
|
+
/**
|
|
75
|
+
* Runs one batch and records how long it held the event loop: the synchronous
|
|
76
|
+
* part of `fn` (parsing, building statements, and the commit itself when there
|
|
77
|
+
* is no worker). A commit on the worker is awaited but not counted, because the
|
|
78
|
+
* loop is free while it runs.
|
|
79
|
+
*/
|
|
80
|
+
export declare class BatchClock {
|
|
81
|
+
maxBatchMs: number;
|
|
82
|
+
/** Every batch's loop-holding time, for percentiles in the boot log. */
|
|
83
|
+
readonly batchMs: number[];
|
|
84
|
+
batch<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
85
|
+
}
|
|
86
|
+
/** What the agent import wants the storage backend to bring back in line with the database. */
|
|
87
|
+
export interface AgentMirrorCatchUp {
|
|
88
|
+
/** Agent ids whose row changed after their legacy file was last written. */
|
|
89
|
+
rewrite: string[];
|
|
90
|
+
/** Legacy files of agents the database deleted after the file was written. */
|
|
91
|
+
unlink: string[];
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Agents: a directory of project directories. The first import is resumable
|
|
95
|
+
* by sorted path; when the same agent id shows up twice (a cwd move that left
|
|
96
|
+
* the old file behind), the newer `updatedAt` wins. Afterwards, see `resyncAgents`.
|
|
97
|
+
*/
|
|
98
|
+
export declare function importLegacyAgents(state: StateDb, baseDir: string, clock: BatchClock, logger: Logger, catchUp?: AgentMirrorCatchUp): Promise<StoreImportResult>;
|
|
99
|
+
/**
|
|
100
|
+
* How long a deleted row keeps its tombstone. A tombstone matters for as long
|
|
101
|
+
* as a stale legacy file could still come back to contradict it: a rollback to
|
|
102
|
+
* the file-based release and a re-upgrade, or a crash before the mirror's
|
|
103
|
+
* unlink. A week covers the updater's rollback and any realistic
|
|
104
|
+
* downgrade-and-return; past it the tombstone is only boot-time cost (every
|
|
105
|
+
* create/delete cycle of a short-lived agent leaves one). Revisit when the
|
|
106
|
+
* delta protocol exposes `rev` to clients: then keep them past the oldest
|
|
107
|
+
* client cursor too.
|
|
108
|
+
*/
|
|
109
|
+
export declare const TOMBSTONE_RETENTION_MS: number;
|
|
110
|
+
export interface ImportLegacyStateOptions {
|
|
111
|
+
agentStoragePath: string;
|
|
112
|
+
sources: LegacySource[];
|
|
113
|
+
logger: Logger;
|
|
114
|
+
}
|
|
115
|
+
export interface LegacyImportResult extends LegacyImportReport {
|
|
116
|
+
agentCatchUp: AgentMirrorCatchUp;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Import (or re-sync) every legacy store. Never throws for a single store: a
|
|
120
|
+
* store that fails is logged and left for the next boot, its marker still
|
|
121
|
+
* "running" so it resumes where it stopped.
|
|
122
|
+
*/
|
|
123
|
+
export declare function importLegacyState(state: StateDb, options: ImportLegacyStateOptions): Promise<LegacyImportResult>;
|
|
124
|
+
//# sourceMappingURL=legacy-import.d.ts.map
|