@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.
Files changed (55) hide show
  1. package/dist/server/extensions/daemon-backend.js +25 -0
  2. package/dist/server/server/agent/agent-storage.d.ts +67 -12
  3. package/dist/server/server/agent/agent-storage.js +208 -132
  4. package/dist/server/server/agent/providers/claude/transport/pty-query.js +10 -0
  5. package/dist/server/server/bootstrap.d.ts +5 -0
  6. package/dist/server/server/bootstrap.js +26 -3
  7. package/dist/server/server/chat/chat-service.d.ts +22 -0
  8. package/dist/server/server/chat/chat-service.js +46 -3
  9. package/dist/server/server/daemon-worker.js +1 -0
  10. package/dist/server/server/fleet/decision-service.d.ts +5 -0
  11. package/dist/server/server/fleet/decision-service.js +30 -0
  12. package/dist/server/server/loop-service.d.ts +95 -0
  13. package/dist/server/server/loop-service.js +64 -3
  14. package/dist/server/server/migrations/backfill-workspace-id.migration.js +3 -2
  15. package/dist/server/server/push/token-store.d.ts +5 -1
  16. package/dist/server/server/push/token-store.js +26 -5
  17. package/dist/server/server/schedule/service.js +4 -1
  18. package/dist/server/server/schedule/store.d.ts +21 -1
  19. package/dist/server/server/schedule/store.js +283 -2
  20. package/dist/server/server/search/indexer.d.ts +10 -0
  21. package/dist/server/server/search/indexer.js +51 -2
  22. package/dist/server/server/session.js +12 -13
  23. package/dist/server/server/state/agent-state.d.ts +39 -0
  24. package/dist/server/server/state/agent-state.js +94 -0
  25. package/dist/server/server/state/daemon-state.d.ts +30 -0
  26. package/dist/server/server/state/daemon-state.js +99 -0
  27. package/dist/server/server/state/legacy-import.d.ts +124 -0
  28. package/dist/server/server/state/legacy-import.js +682 -0
  29. package/dist/server/server/state/legacy-manifest.d.ts +73 -0
  30. package/dist/server/server/state/legacy-manifest.js +112 -0
  31. package/dist/server/server/state/legacy-mirror.d.ts +57 -0
  32. package/dist/server/server/state/legacy-mirror.js +114 -0
  33. package/dist/server/server/state/legacy-sources.d.ts +9 -0
  34. package/dist/server/server/state/legacy-sources.js +163 -0
  35. package/dist/server/server/state/state-db.d.ts +263 -0
  36. package/dist/server/server/state/state-db.js +772 -0
  37. package/dist/server/server/state/state-schema.d.ts +39 -0
  38. package/dist/server/server/state/state-schema.js +125 -0
  39. package/dist/server/server/state/state-worker.d.ts +48 -0
  40. package/dist/server/server/state/state-worker.js +165 -0
  41. package/dist/server/server/websocket-server.js +2 -1
  42. package/dist/server/server/workspace/session-openable.js +7 -0
  43. package/dist/server/server/workspace-reconciliation-service.js +14 -3
  44. package/dist/server/server/workspace-registry.d.ts +45 -2
  45. package/dist/server/server/workspace-registry.js +75 -4
  46. package/dist/server/web-ui/_expo/static/js/web/{index-2ac1a7249c20c322a1f9a54563cfee62.js → index-38580ed8926a5ddc569d744b8299d339.js} +4 -4
  47. package/dist/server/web-ui/_expo/static/js/web/index-38580ed8926a5ddc569d744b8299d339.js.br +0 -0
  48. package/dist/server/web-ui/_expo/static/js/web/{index-2ac1a7249c20c322a1f9a54563cfee62.js.gz → index-38580ed8926a5ddc569d744b8299d339.js.gz} +0 -0
  49. package/dist/server/web-ui/_expo/static/js/web/{index-2ac1a7249c20c322a1f9a54563cfee62.js.map.br → index-38580ed8926a5ddc569d744b8299d339.js.map.br} +0 -0
  50. package/dist/server/web-ui/_expo/static/js/web/{index-2ac1a7249c20c322a1f9a54563cfee62.js.map.gz → index-38580ed8926a5ddc569d744b8299d339.js.map.gz} +0 -0
  51. package/dist/server/web-ui/index.html +1 -1
  52. package/dist/server/web-ui/index.html.br +0 -0
  53. package/dist/server/web-ui/index.html.gz +0 -0
  54. package/package.json +6 -6
  55. 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