@beeos-ai/device-agent 0.5.1 → 0.6.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.
@@ -0,0 +1,402 @@
1
+ /**
2
+ * Append-only JSONL persistence layer for `MessageStore`.
3
+ *
4
+ * Layout (one file per session, plus one metadata file):
5
+ *
6
+ * ${BEEOS_HOME ?? ~/.beeos}/device-agent/sessions/
7
+ * ├── <sid>.jsonl # append-only record log
8
+ * └── <sid>.meta.json # { sessionId, title, createdAt, updatedAt }
9
+ *
10
+ * Each JSONL line is a serialised `PersistedRecord` (see message-store.ts):
11
+ *
12
+ * {"v":1,"type":"user_message","id":"...","ts":...,"content":"...","turnId":"...","messageId":"..."}
13
+ * {"v":1,"type":"assistant_turn","id":"...","ts":...,"messageId":"...","turnId":"...",
14
+ * "startedAt":...,"completedAt":...,"text":"...","toolCalls":[...],"stopReason":"...","error":"..."}
15
+ *
16
+ * IO strategy:
17
+ *
18
+ * - Appends use `fs.writeSync` against a long-lived FD opened in
19
+ * append mode (`O_APPEND`). Sync IO sidesteps the buffer-flush
20
+ * race that bites WriteStream-based rotation (`clearSession`
21
+ * followed immediately by a new append). The blocking cost is
22
+ * negligible at our message rate (1-2 records per ~5-10s task).
23
+ * - `O_APPEND` guarantees every kernel write seeks to EOF
24
+ * atomically, so concurrent appenders on the same file (rare —
25
+ * would only happen if two device-agent siblings shared a
26
+ * sessions dir) never interleave a single line.
27
+ *
28
+ * Crash semantics:
29
+ *
30
+ * - Each write hits disk before the call returns; finalised turns
31
+ * survive an unclean shutdown. The user-selected `memory_only`
32
+ * in-flight policy means a turn that hadn't yet reached `task:end`
33
+ * is lost — that's expected.
34
+ * - Reads tolerate partial / corrupt tail lines (the writer might
35
+ * have died mid-line on a previous run) by skipping any line that
36
+ * fails to parse. Same convention as beeos-claw's
37
+ * `LocalSessionHistory.readEntries`.
38
+ */
39
+ import * as fs from "node:fs";
40
+ import * as fsp from "node:fs/promises";
41
+ import * as path from "node:path";
42
+ import * as readline from "node:readline";
43
+ import { MessageStore, } from "./message-store.js";
44
+ /**
45
+ * Debounce window for `<sid>.meta.json` writes. Title / updatedAt change
46
+ * frequently (every appended turn touches updatedAt) — we coalesce
47
+ * within 500ms so a bursty stream of appends doesn't fsync metadata
48
+ * dozens of times per second.
49
+ */
50
+ const META_DEBOUNCE_MS = 500;
51
+ /**
52
+ * Persistent variant of `MessageStore` — every append is mirrored to a
53
+ * per-session JSONL log, every metadata change debounced into
54
+ * `<sid>.meta.json`.
55
+ *
56
+ * Construct via `JsonlMessageStore.open(...)` so the on-disk state can
57
+ * be loaded into memory before any reads happen.
58
+ */
59
+ export class JsonlMessageStore extends MessageStore {
60
+ sessionsDir;
61
+ logger;
62
+ /** Lazily-opened append-mode FDs keyed by sessionId. */
63
+ fds = new Map();
64
+ metaTimers = new Map();
65
+ closed = false;
66
+ constructor(opts) {
67
+ super({
68
+ ...(opts.maxRecordsPerSession !== undefined ? { maxRecordsPerSession: opts.maxRecordsPerSession } : {}),
69
+ ...(opts.maxSessions !== undefined ? { maxSessions: opts.maxSessions } : {}),
70
+ });
71
+ this.sessionsDir = opts.sessionsDir;
72
+ if (opts.logger)
73
+ this.logger = opts.logger;
74
+ }
75
+ /** Construct and load on-disk state into memory. */
76
+ static async open(opts) {
77
+ const store = new JsonlMessageStore(opts);
78
+ await fsp.mkdir(opts.sessionsDir, { recursive: true });
79
+ const sids = opts.preloadSids
80
+ ? [...opts.preloadSids]
81
+ : await store.discoverSids();
82
+ for (const sid of sids) {
83
+ await store.preloadSession(sid);
84
+ }
85
+ return store;
86
+ }
87
+ /** Path of the JSONL transcript for a given sid. */
88
+ sessionFile(sid) {
89
+ return path.join(this.sessionsDir, `${safeFilename(sid)}.jsonl`);
90
+ }
91
+ /** Path of the metadata file for a given sid. */
92
+ metaFile(sid) {
93
+ return path.join(this.sessionsDir, `${safeFilename(sid)}.meta.json`);
94
+ }
95
+ /** Discover every sid present on disk by scanning `*.jsonl` files. */
96
+ async discoverSids() {
97
+ let entries;
98
+ try {
99
+ entries = await fsp.readdir(this.sessionsDir);
100
+ }
101
+ catch (err) {
102
+ if (err.code === "ENOENT")
103
+ return [];
104
+ throw err;
105
+ }
106
+ return entries
107
+ .filter((name) => name.endsWith(".jsonl"))
108
+ .map((name) => name.slice(0, -".jsonl".length));
109
+ }
110
+ /**
111
+ * Read the JSONL for a sid into memory. Skips rows that fail to
112
+ * parse (truncated tail line on a previous crash, schema mismatch
113
+ * from a future version, etc.) so a single bad row doesn't sink the
114
+ * whole session.
115
+ */
116
+ async preloadSession(sid) {
117
+ const meta = await this.readMeta(sid);
118
+ if (meta) {
119
+ // Inject directly into the parent map so we don't bump updatedAt
120
+ // back to `now` on cold start.
121
+ this.sessions.set(sid, {
122
+ sessionId: meta.sessionId,
123
+ title: meta.title,
124
+ createdAt: meta.createdAt,
125
+ updatedAt: meta.updatedAt,
126
+ });
127
+ }
128
+ else {
129
+ // Will be populated by the first record we read, then synced to
130
+ // disk on the next mutation (or close).
131
+ this.sessions.set(sid, {
132
+ sessionId: sid,
133
+ title: "New chat",
134
+ createdAt: Date.now(),
135
+ updatedAt: 0,
136
+ });
137
+ }
138
+ const file = this.sessionFile(sid);
139
+ if (!fs.existsSync(file))
140
+ return;
141
+ const rl = readline.createInterface({
142
+ input: fs.createReadStream(file, { encoding: "utf8" }),
143
+ crlfDelay: Infinity,
144
+ });
145
+ let parsed = 0;
146
+ let skipped = 0;
147
+ for await (const rawLine of rl) {
148
+ const line = rawLine.trim();
149
+ if (!line)
150
+ continue;
151
+ let rec;
152
+ try {
153
+ rec = JSON.parse(line);
154
+ }
155
+ catch {
156
+ skipped++;
157
+ continue;
158
+ }
159
+ if (!isPersistedRecord(rec)) {
160
+ skipped++;
161
+ continue;
162
+ }
163
+ this.loadRecord(sid, rec);
164
+ parsed++;
165
+ }
166
+ if (skipped > 0) {
167
+ this.logger?.warn({ sid, parsed, skipped, file }, "jsonl-store: skipped malformed lines during preload");
168
+ }
169
+ // If preload populated records but we had no meta on disk,
170
+ // backfill createdAt from the earliest record so the index
171
+ // matches the transcript.
172
+ if (!meta) {
173
+ const records = this.records.get(sid);
174
+ if (records && records.length > 0) {
175
+ const first = records[0];
176
+ const last = records[records.length - 1];
177
+ const session = this.sessions.get(sid);
178
+ session.createdAt = first.ts;
179
+ session.updatedAt = Math.max(session.updatedAt, last.ts);
180
+ }
181
+ }
182
+ }
183
+ async readMeta(sid) {
184
+ try {
185
+ const raw = await fsp.readFile(this.metaFile(sid), "utf8");
186
+ const parsed = JSON.parse(raw);
187
+ if (typeof parsed.sessionId === "string" &&
188
+ typeof parsed.title === "string" &&
189
+ typeof parsed.createdAt === "number" &&
190
+ typeof parsed.updatedAt === "number") {
191
+ return parsed;
192
+ }
193
+ return null;
194
+ }
195
+ catch (err) {
196
+ if (err.code === "ENOENT")
197
+ return null;
198
+ this.logger?.warn({ sid, err: err.message }, "jsonl-store: failed to read meta.json — falling back to defaults");
199
+ return null;
200
+ }
201
+ }
202
+ getOrCreateFd(sid) {
203
+ let fd = this.fds.get(sid);
204
+ if (fd === undefined) {
205
+ // O_APPEND ("a") makes every kernel write seek to EOF atomically,
206
+ // safe against concurrent appenders on the same file.
207
+ fd = fs.openSync(this.sessionFile(sid), "a");
208
+ this.fds.set(sid, fd);
209
+ }
210
+ return fd;
211
+ }
212
+ // ── overrides — every mutation also persists ─────────────────────────────
213
+ appendUserMessage(sessionId, content, opts = {}) {
214
+ if (this.closed)
215
+ throw new Error("JsonlMessageStore: appendUserMessage on closed store");
216
+ const record = super.appendUserMessage(sessionId, content, opts);
217
+ this.writeJsonlLine(sessionId, record);
218
+ this.scheduleMetaWrite(sessionId);
219
+ return record;
220
+ }
221
+ appendAssistantTurn(sessionId, turn, opts = {}) {
222
+ if (this.closed)
223
+ throw new Error("JsonlMessageStore: appendAssistantTurn on closed store");
224
+ const record = super.appendAssistantTurn(sessionId, turn, opts);
225
+ this.writeJsonlLine(sessionId, record);
226
+ this.scheduleMetaWrite(sessionId);
227
+ return record;
228
+ }
229
+ upsertSession(sessionId, title) {
230
+ super.upsertSession(sessionId, title);
231
+ this.scheduleMetaWrite(sessionId);
232
+ }
233
+ renameSession(sessionId, title) {
234
+ super.renameSession(sessionId, title);
235
+ this.scheduleMetaWrite(sessionId);
236
+ }
237
+ clearSession(sessionId) {
238
+ super.clearSession(sessionId);
239
+ // Truncate the JSONL on disk — we just lost the in-memory log so
240
+ // the file should match. Stream needs to be rotated so subsequent
241
+ // appends start from offset 0.
242
+ this.rotateStream(sessionId);
243
+ this.scheduleMetaWrite(sessionId);
244
+ }
245
+ deleteSession(sessionId) {
246
+ super.deleteSession(sessionId);
247
+ // Close any open FD first to avoid Windows EBUSY on unlink.
248
+ this.closeFd(sessionId);
249
+ // Cancel any pending meta write — the file is about to be gone.
250
+ const debounced = this.metaTimers.get(sessionId);
251
+ if (debounced) {
252
+ clearTimeout(debounced.timer);
253
+ this.metaTimers.delete(sessionId);
254
+ }
255
+ // Sync unlinks — the public API is sync so callers (including
256
+ // tests) expect the files to be gone by the time we return.
257
+ unlinkSyncIfExists(this.sessionFile(sessionId), this.logger);
258
+ unlinkSyncIfExists(this.metaFile(sessionId), this.logger);
259
+ }
260
+ // ── shutdown ─────────────────────────────────────────────────────────────
261
+ /** Flush every pending meta write and close every open FD. */
262
+ async close() {
263
+ if (this.closed)
264
+ return;
265
+ this.closed = true;
266
+ // Flush any pending meta writes.
267
+ const metaWrites = [];
268
+ for (const [sid, debounced] of this.metaTimers) {
269
+ clearTimeout(debounced.timer);
270
+ metaWrites.push(this.writeMetaNow(sid, debounced.pending));
271
+ }
272
+ this.metaTimers.clear();
273
+ // JSONL writes are sync, so the FDs hold no pending data — just
274
+ // close them.
275
+ for (const sid of Array.from(this.fds.keys())) {
276
+ this.closeFd(sid);
277
+ }
278
+ await Promise.all(metaWrites);
279
+ }
280
+ // ── helpers ──────────────────────────────────────────────────────────────
281
+ writeJsonlLine(sid, record) {
282
+ const fd = this.getOrCreateFd(sid);
283
+ const line = JSON.stringify(record) + "\n";
284
+ fs.writeSync(fd, line);
285
+ }
286
+ closeFd(sid) {
287
+ const fd = this.fds.get(sid);
288
+ if (fd === undefined)
289
+ return;
290
+ this.fds.delete(sid);
291
+ try {
292
+ fs.closeSync(fd);
293
+ }
294
+ catch (err) {
295
+ this.logger?.warn({ sid, err: err.message }, "jsonl-store: failed to close JSONL fd");
296
+ }
297
+ }
298
+ rotateStream(sid) {
299
+ // Close the FD first so the truncate sees no in-flight writers.
300
+ // Sync IO means there are no buffered writes to discard — every
301
+ // appendUserMessage / appendAssistantTurn already hit disk before
302
+ // returning.
303
+ this.closeFd(sid);
304
+ try {
305
+ fs.writeFileSync(this.sessionFile(sid), "");
306
+ }
307
+ catch (err) {
308
+ this.logger?.warn({ sid, err: err.message }, "jsonl-store: failed to truncate JSONL during clearSession");
309
+ }
310
+ }
311
+ scheduleMetaWrite(sid) {
312
+ const meta = this.snapshotMeta(sid);
313
+ if (!meta)
314
+ return;
315
+ const existing = this.metaTimers.get(sid);
316
+ if (existing) {
317
+ existing.pending = meta;
318
+ return;
319
+ }
320
+ const timer = setTimeout(() => {
321
+ const cur = this.metaTimers.get(sid);
322
+ this.metaTimers.delete(sid);
323
+ if (!cur)
324
+ return;
325
+ void this.writeMetaNow(sid, cur.pending);
326
+ }, META_DEBOUNCE_MS);
327
+ // Don't keep the event loop alive solely for the meta debounce —
328
+ // a paused / idle agent should still be able to exit cleanly.
329
+ if (typeof timer.unref === "function")
330
+ timer.unref();
331
+ this.metaTimers.set(sid, { timer, pending: meta });
332
+ }
333
+ snapshotMeta(sid) {
334
+ const s = this.sessions.get(sid);
335
+ if (!s)
336
+ return null;
337
+ return {
338
+ sessionId: s.sessionId,
339
+ title: s.title,
340
+ createdAt: s.createdAt,
341
+ updatedAt: s.updatedAt,
342
+ };
343
+ }
344
+ async writeMetaNow(sid, meta) {
345
+ try {
346
+ // Atomic-enough: write to tmp + rename. Avoids a torn meta file
347
+ // if the process dies mid-write (rare, but cheap to defend).
348
+ const file = this.metaFile(sid);
349
+ const tmp = `${file}.tmp`;
350
+ await fsp.writeFile(tmp, JSON.stringify(meta) + "\n", "utf8");
351
+ await fsp.rename(tmp, file);
352
+ }
353
+ catch (err) {
354
+ this.logger?.warn({ sid, err: err.message }, "jsonl-store: failed to write meta.json");
355
+ }
356
+ }
357
+ }
358
+ /** Reject sids that would escape the sessions directory. */
359
+ function safeFilename(sid) {
360
+ // Forbid path separators and the magic dotted entries; allow `:` for
361
+ // our `device:<deviceId>` convention by transliterating to `_`.
362
+ const cleaned = sid.replace(/[\\/]/g, "_").replace(/^\.+$/, "_");
363
+ return cleaned.replace(/:/g, "_");
364
+ }
365
+ function unlinkSyncIfExists(file, logger) {
366
+ try {
367
+ fs.unlinkSync(file);
368
+ }
369
+ catch (err) {
370
+ const code = err.code;
371
+ if (code === "ENOENT")
372
+ return;
373
+ logger?.warn({ file, err: err.message }, "jsonl-store: failed to unlink file");
374
+ }
375
+ }
376
+ /**
377
+ * Best-effort schema check. Anything that doesn't look like a v1
378
+ * `user_message` or `assistant_turn` record is dropped.
379
+ */
380
+ function isPersistedRecord(value) {
381
+ if (!value || typeof value !== "object")
382
+ return false;
383
+ const r = value;
384
+ if (r.v !== 1)
385
+ return false;
386
+ if (typeof r.id !== "string" || typeof r.ts !== "number")
387
+ return false;
388
+ if (r.type === "user_message") {
389
+ return typeof r.content === "string";
390
+ }
391
+ if (r.type === "assistant_turn") {
392
+ return (typeof r.text === "string" &&
393
+ typeof r.messageId === "string" &&
394
+ typeof r.turnId === "string" &&
395
+ typeof r.startedAt === "number" &&
396
+ typeof r.completedAt === "number" &&
397
+ Array.isArray(r.toolCalls) &&
398
+ typeof r.stopReason === "string");
399
+ }
400
+ return false;
401
+ }
402
+ //# sourceMappingURL=jsonl-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonl-store.js","sourceRoot":"","sources":["../../src/app/jsonl-store.ts"],"names":[],"mappings":"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"}
@@ -1,55 +1,106 @@
1
1
  /**
2
- * In-memory MessageStore.
2
+ * In-memory MessageStore — typed turn-grained transcript log.
3
3
  *
4
- * Mirrors the surface of the Python `MessageStore` (`add_message`,
5
- * `get_messages_as_dicts`, `list_sessions`, etc.) so the ACP handlers stay
6
- * byte-for-byte compatible. Persistence is left out of the MVP; `~/.beeos`
7
- * SQLite can be re-added later behind the same interface (pluggable adapter).
4
+ * Each session holds an ordered list of `PersistedRecord`s, one per
5
+ * logical turn:
6
+ *
7
+ * - `user_message` — emitted on `session/prompt` entry.
8
+ * - `assistant_turn` — emitted exactly once per `task:end` (replaces
9
+ * the legacy "one row per chunk" model that produced fragmented
10
+ * transcripts on reload). Carries the full `text`, every tool call
11
+ * in invocation order, the stop reason, and an optional terminal
12
+ * error string.
13
+ *
14
+ * This base class is pure in-memory; persistence is added by
15
+ * `JsonlMessageStore` which extends this and forwards every mutation
16
+ * to disk before returning.
17
+ *
18
+ * The shape is deliberately aligned with OpenClaw's per-agent JSONL
19
+ * (`~/.openclaw/agents/<id>/sessions/<sid>.jsonl`) so operational
20
+ * tooling that already knows that layout works here too.
8
21
  */
9
- export interface StoredMessage {
22
+ import type { AssistantTurnRecord } from "./in-flight-turn.js";
23
+ /** Schema version. Bumped only on incompatible disk format changes. */
24
+ export declare const RECORD_SCHEMA_VERSION: 1;
25
+ /** A user-authored message — written on `session/prompt` entry. */
26
+ export interface UserMessageRecord {
27
+ v: typeof RECORD_SCHEMA_VERSION;
28
+ type: "user_message";
29
+ /** Stable row id. Unique within a sid. */
10
30
  id: string;
11
- sessionId: string;
12
- role: "user" | "assistant" | "system" | "tool";
31
+ /** Wall-clock timestamp at write time (epoch ms). */
32
+ ts: number;
33
+ /** Plain text of the prompt (resource-link normalisation already applied). */
13
34
  content: string;
14
- messageType: "text" | "tool_call" | "tool_result" | "status";
15
- metadata: Record<string, unknown>;
16
- createdAt: number;
35
+ /** turnId of the corresponding assistant turn — populated when known. */
36
+ turnId?: string;
37
+ /** messageId of the corresponding assistant turn — populated when known. */
38
+ messageId?: string;
17
39
  }
40
+ /** Persisted shape of an assistant turn — wraps `AssistantTurnRecord`. */
41
+ export interface PersistedAssistantTurn extends AssistantTurnRecord {
42
+ v: typeof RECORD_SCHEMA_VERSION;
43
+ type: "assistant_turn";
44
+ /** Stable row id. Unique within a sid. */
45
+ id: string;
46
+ /** Wall-clock timestamp at write time (epoch ms) — equals `completedAt`. */
47
+ ts: number;
48
+ }
49
+ export type PersistedRecord = UserMessageRecord | PersistedAssistantTurn;
18
50
  export interface SessionRecord {
19
51
  sessionId: string;
20
52
  title: string;
21
53
  createdAt: number;
22
54
  updatedAt: number;
23
55
  }
24
- export interface MessageHistoryEntry {
25
- role: string;
26
- content: string;
27
- type: string;
28
- createdAt: number;
29
- metadata?: Record<string, unknown>;
56
+ export interface MessageStoreOptions {
57
+ /** Soft cap on records kept per session. LRU evicts the oldest. */
58
+ maxRecordsPerSession?: number;
59
+ /** Soft cap on session count — LRU evicts least-recently-updated. */
60
+ maxSessions?: number;
30
61
  }
62
+ /** Mutable in-memory message store. */
31
63
  export declare class MessageStore {
32
- private readonly maxPerSession;
33
- private readonly maxSessions;
34
- private readonly sessions;
35
- private readonly messages;
36
- constructor(opts?: {
37
- maxMessagesPerSession?: number;
38
- maxSessions?: number;
39
- });
64
+ protected readonly maxRecordsPerSession: number;
65
+ protected readonly maxSessions: number;
66
+ protected readonly sessions: Map<string, SessionRecord>;
67
+ protected readonly records: Map<string, PersistedRecord[]>;
68
+ constructor(opts?: MessageStoreOptions);
40
69
  upsertSession(sessionId: string, title?: string): void;
41
70
  sessionExists(sessionId: string): boolean;
42
71
  renameSession(sessionId: string, title: string): void;
43
72
  deleteSession(sessionId: string): void;
73
+ /** Drop every record in a session but keep the session record itself. */
74
+ clearSession(sessionId: string): void;
44
75
  listSessions(limit?: number): Array<{
45
76
  sessionId: string;
46
77
  title: string;
47
78
  updatedAt: number;
48
79
  }>;
49
- clearSession(sessionId: string): void;
50
- addMessage(sessionId: string, role: StoredMessage["role"], content: string, messageType?: StoredMessage["messageType"], metadata?: Record<string, unknown>): string;
51
- getMessages(sessionId: string, limit?: number): StoredMessage[];
52
- getMessagesAsDicts(sessionId: string, limit?: number): MessageHistoryEntry[];
53
- /** Drop the least-recently-updated sessions when we exceed maxSessions. */
54
- private evictSessionLru;
80
+ /** Append a user prompt row. Returns the persisted record. */
81
+ appendUserMessage(sessionId: string, content: string, opts?: {
82
+ turnId?: string;
83
+ messageId?: string;
84
+ now?: number;
85
+ }): UserMessageRecord;
86
+ /** Append a finalized assistant turn. Returns the persisted record. */
87
+ appendAssistantTurn(sessionId: string, turn: AssistantTurnRecord, opts?: {
88
+ now?: number;
89
+ }): PersistedAssistantTurn;
90
+ /** Pre-loaded record (used by `JsonlMessageStore.open` to seed memory). */
91
+ loadRecord(sessionId: string, record: PersistedRecord): void;
92
+ /** All records for a session in chronological insertion order. */
93
+ getTurns(sessionId: string): PersistedRecord[];
94
+ /** True iff at least one record exists for this session. */
95
+ hasRecords(sessionId: string): boolean;
96
+ /**
97
+ * Append a record to memory and bump session.updatedAt.
98
+ * Subclasses (JsonlMessageStore) override to additionally persist.
99
+ *
100
+ * `touch=false` is used by `loadRecord` so a cold-start replay of a
101
+ * year-old transcript doesn't artificially bump updatedAt to "now".
102
+ */
103
+ protected persistRecord(sessionId: string, record: PersistedRecord, ts: number, touch?: boolean): void;
104
+ /** Drop the least-recently-updated sessions when exceeding maxSessions. */
105
+ protected evictSessionLru(): void;
55
106
  }