oorja-harness-host 0.1.0 → 0.2.1

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/README.md CHANGED
@@ -10,3 +10,14 @@ machine identity, and command-line presentation.
10
10
  Turn output remains buffered in memory until the client confirms that it has
11
11
  saved it. On graceful shutdown, the host saves the opaque AI SDK resume state
12
12
  provided by each harness session so the next process can resume it.
13
+
14
+ Harness state is stored in one SQLite database. The host keeps exclusive access
15
+ to that database while it is running; `stop()` saves the sessions and closes
16
+ the database.
17
+
18
+ ```ts
19
+ const host = await HarnessHost.start({
20
+ // ...
21
+ stateFile: '/path/to/harness-host.sqlite',
22
+ })
23
+ ```
@@ -0,0 +1,6 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ export interface Migration {
3
+ version: number;
4
+ apply: (database: DatabaseSync) => void;
5
+ }
6
+ export declare const openDatabase: (file: string, migrations: Migration[]) => DatabaseSync;
@@ -0,0 +1,29 @@
1
+ import { mkdirSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { DatabaseSync } from 'node:sqlite';
4
+ export const openDatabase = (file, migrations) => {
5
+ mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
6
+ const database = new DatabaseSync(file, { timeout: 0 });
7
+ try {
8
+ database.exec('PRAGMA locking_mode = EXCLUSIVE');
9
+ database.exec('PRAGMA journal_mode = DELETE');
10
+ database.exec('BEGIN EXCLUSIVE');
11
+ const { user_version: currentVersion } = database
12
+ .prepare('PRAGMA user_version')
13
+ .get();
14
+ for (const migration of migrations) {
15
+ if (migration.version <= currentVersion)
16
+ continue;
17
+ migration.apply(database);
18
+ database.exec(`PRAGMA user_version = ${migration.version}`);
19
+ }
20
+ database.exec('COMMIT');
21
+ return database;
22
+ }
23
+ catch (error) {
24
+ if (database.isTransaction)
25
+ database.exec('ROLLBACK');
26
+ database.close();
27
+ throw error;
28
+ }
29
+ };
@@ -10,12 +10,13 @@ export const runHarnessDoctor = async (cwd, harnessName) => {
10
10
  home ? canAccess(home, constants.R_OK | constants.W_OK) : false,
11
11
  checkLoopbackPort().then(() => true, () => false),
12
12
  ]);
13
- const nodeMajor = Number(process.versions.node.split('.')[0]);
13
+ const [nodeMajor, nodeMinor] = process.versions.node.split('.').map(Number);
14
+ const nodeIsSupported = nodeMajor > 24 || (nodeMajor === 24 && nodeMinor >= 15);
14
15
  return [
15
16
  {
16
17
  name: 'node',
17
- ok: nodeMajor >= 22,
18
- detail: `Node ${process.versions.node}; requires Node 22 or newer`,
18
+ ok: nodeIsSupported,
19
+ detail: `Node ${process.versions.node}; requires Node 24.15 or newer`,
19
20
  },
20
21
  {
21
22
  name: 'pnpm',
@@ -1,7 +1,7 @@
1
1
  import { createServer } from 'node:net';
2
2
  import { HarnessAgent } from '@ai-sdk/harness/agent';
3
3
  import { jsonSchema } from 'ai';
4
- import { HARNESS_MODELS } from '../models.js';
4
+ import { HARNESS_MODELS } from '../host/models.js';
5
5
  import { LocalSandboxSession } from '../sandbox/local.js';
6
6
  export const createHarnessRuntime = ({ harnessId, cwd, createHarness, }) => ({
7
7
  harnessId,
@@ -1,5 +1,5 @@
1
1
  export declare class Future<T> {
2
- promise: Promise<T>;
2
+ readonly promise: Promise<T>;
3
3
  resolve?: (arg: T) => void;
4
4
  reject?: (error: unknown) => void;
5
5
  onFinally?: () => void;
@@ -1,21 +1,23 @@
1
- import { type HarnessCommand, type HarnessEvent, type HostState } from './messages.js';
2
- import type { ResumeStateStore } from './resumeState.js';
3
- import type { HarnessRuntime } from './runtime.js';
1
+ import { type HarnessCommand, type HarnessEvent, type HostState } from '../messages.js';
2
+ import type { HarnessRuntime } from '../runtime.js';
4
3
  export type HarnessHostOptions = {
5
4
  hostId: string;
6
5
  hostname: string;
6
+ stateFile: string;
7
7
  runtimes: HarnessRuntime[];
8
- resumeStates: ResumeStateStore;
9
8
  emit: (event: HarnessEvent) => void;
10
9
  onError: (error: unknown) => void;
11
10
  };
12
11
  export declare class HarnessHost {
13
12
  private readonly options;
13
+ private readonly database;
14
+ private readonly repo;
14
15
  private readonly runtimes;
15
16
  private readonly sessions;
16
17
  private readonly openingSessions;
17
18
  private stopping;
18
- constructor(options: HarnessHostOptions);
19
+ private constructor();
20
+ static start: (options: HarnessHostOptions) => Promise<HarnessHost>;
19
21
  getState: () => HostState;
20
22
  handleCommand: (command: HarnessCommand) => Promise<void>;
21
23
  stop: () => Promise<void>;
@@ -1,19 +1,42 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { smoothStream } from 'ai';
3
- import { HarnessTurnPartType, MessageType, SessionStatus, TurnOutcome, } from './messages.js';
3
+ import { HarnessTurnPartType, MessageType, SessionStatus, TurnOutcome, } from '../messages.js';
4
4
  import { createTurnActivity, updateTurnActivity, } from './activity.js';
5
5
  import { HARNESS_MODELS } from './models.js';
6
- import { Future } from './utils.js';
6
+ import { HarnessRepo, harnessMigrations } from '../repo/index.js';
7
+ import { openDatabase } from '../database/index.js';
8
+ import { Future } from './Future.js';
7
9
  export class HarnessHost {
8
10
  options;
11
+ database;
12
+ repo;
9
13
  runtimes;
10
14
  sessions = new Map();
11
15
  openingSessions = new Map();
12
16
  stopping = false;
13
- constructor(options) {
17
+ constructor(options, database, repo) {
14
18
  this.options = options;
19
+ this.database = database;
20
+ this.repo = repo;
15
21
  this.runtimes = new Map(options.runtimes.map((runtime) => [runtime.harnessId, runtime]));
16
22
  }
23
+ static start = async (options) => {
24
+ const database = openDatabase(options.stateFile, harnessMigrations);
25
+ try {
26
+ const repo = new HarnessRepo(database);
27
+ const storedHostId = repo.getHostId();
28
+ if (storedHostId && storedHostId !== options.hostId) {
29
+ throw new Error(`Harness state belongs to host ${storedHostId}, not ${options.hostId}`);
30
+ }
31
+ if (!storedHostId)
32
+ repo.saveHostId(options.hostId);
33
+ return new HarnessHost(options, database, repo);
34
+ }
35
+ catch (error) {
36
+ database.close();
37
+ throw error;
38
+ }
39
+ };
17
40
  getState = () => ({
18
41
  type: MessageType.HOST_STATE,
19
42
  hostId: this.options.hostId,
@@ -53,12 +76,17 @@ export class HarnessHost {
53
76
  };
54
77
  stop = async () => {
55
78
  this.stopping = true;
56
- await Promise.allSettled(this.openingSessions.values());
57
- const sessions = [...this.sessions.values()];
58
- sessions.forEach(({ activeTurn }) => activeTurn?.abortController.abort());
59
- await Promise.allSettled(sessions.map(({ activeTurn }) => activeTurn?.completion.promise));
60
- await Promise.allSettled(sessions.map(this.saveSession));
61
- this.sessions.clear();
79
+ try {
80
+ await Promise.allSettled(this.openingSessions.values());
81
+ const sessions = [...this.sessions.values()];
82
+ sessions.forEach(({ activeTurn }) => activeTurn?.abortController.abort());
83
+ await Promise.allSettled(sessions.map(({ activeTurn }) => activeTurn?.completion.promise));
84
+ await Promise.allSettled(sessions.map(this.saveSession));
85
+ this.sessions.clear();
86
+ }
87
+ finally {
88
+ this.database.close();
89
+ }
62
90
  };
63
91
  submitMessage = async (command) => {
64
92
  const session = await this.getSession(command);
@@ -226,7 +254,7 @@ export class HarnessHost {
226
254
  await session.runtime.destroy();
227
255
  this.sessions.delete(command.sessionId);
228
256
  }
229
- await this.options.resumeStates.remove(command.harnessId, command.sessionId);
257
+ this.repo.deleteSession(command.harnessId, command.sessionId);
230
258
  this.emitState();
231
259
  };
232
260
  setSessionModel = (command) => {
@@ -262,7 +290,11 @@ export class HarnessHost {
262
290
  }
263
291
  };
264
292
  openSession = async (runtime, sessionId) => {
265
- const resumeFrom = await this.options.resumeStates.load(runtime.harnessId, sessionId);
293
+ const failure = this.repo.getResumeFailure(runtime.harnessId, sessionId);
294
+ if (failure) {
295
+ throw new Error(`Cannot resume ${runtime.harnessId} session ${sessionId}: checkpoint failed at ${failure.timestamp}: ${failure.message}`);
296
+ }
297
+ const resumeFrom = this.repo.getResumeState(runtime.harnessId, sessionId);
266
298
  const session = {
267
299
  harnessId: runtime.harnessId,
268
300
  sessionId,
@@ -297,11 +329,11 @@ export class HarnessHost {
297
329
  saveSession = async (session) => {
298
330
  try {
299
331
  const state = await session.runtime.stop();
300
- await this.options.resumeStates.save(session.harnessId, session.sessionId, state);
332
+ this.repo.saveResumeState(session.harnessId, session.sessionId, state);
301
333
  }
302
334
  catch (error) {
303
335
  this.options.onError(error);
304
- await this.options.resumeStates.markFailed(session.harnessId, session.sessionId, errorMessage(error));
336
+ this.repo.saveResumeFailure(session.harnessId, session.sessionId, errorMessage(error));
305
337
  throw error;
306
338
  }
307
339
  };
@@ -1,2 +1,2 @@
1
- import { HarnessId, type HarnessModels } from './messages.js';
1
+ import { HarnessId, type HarnessModels } from '../messages.js';
2
2
  export declare const HARNESS_MODELS: Record<HarnessId, HarnessModels>;
@@ -1,4 +1,4 @@
1
- import { HarnessId } from './messages.js';
1
+ import { HarnessId } from '../messages.js';
2
2
  // Curated choices for now. Hosts can load this catalog from a service later.
3
3
  export const HARNESS_MODELS = {
4
4
  [HarnessId.CLAUDE_CODE]: {
package/dist/index.d.ts CHANGED
@@ -1,10 +1,8 @@
1
- export { HarnessHost, type HarnessHostOptions } from './HarnessHost.js';
1
+ export { HarnessHost, type HarnessHostOptions } from './host/HarnessHost.js';
2
2
  export { ALL_HARNESSES, createHarnessRuntimes, doctorHarnessHost, prepareHarnessHost, type HarnessSelection, } from './harnesses/index.js';
3
3
  export { createClaudeCodeRuntime, type ClaudeCodeConfig, } from './harnesses/claudeCode/index.js';
4
4
  export { createCodexRuntime, type CodexConfig } from './harnesses/codex/index.js';
5
5
  export { createOpenCodeRuntime, type OpenCodeConfig } from './harnesses/openCode/index.js';
6
6
  export * from './messages.js';
7
- export { createFileResumeStateStore } from './resumeState/file.js';
8
- export type { ResumeStateStore } from './resumeState.js';
9
7
  export type { HarnessRuntime, RuntimeSession } from './runtime.js';
10
- export type { ChangedFile, TurnActivity } from './activity.js';
8
+ export type { ChangedFile, TurnActivity } from './host/activity.js';
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
- export { HarnessHost } from './HarnessHost.js';
1
+ export { HarnessHost } from './host/HarnessHost.js';
2
2
  export { ALL_HARNESSES, createHarnessRuntimes, doctorHarnessHost, prepareHarnessHost, } from './harnesses/index.js';
3
3
  export { createClaudeCodeRuntime, } from './harnesses/claudeCode/index.js';
4
4
  export { createCodexRuntime } from './harnesses/codex/index.js';
5
5
  export { createOpenCodeRuntime } from './harnesses/openCode/index.js';
6
6
  export * from './messages.js';
7
- export { createFileResumeStateStore } from './resumeState/file.js';
@@ -1,5 +1,5 @@
1
1
  import type { TextStreamPart, ToolSet } from 'ai';
2
- import type { TurnActivity } from './activity.js';
2
+ import type { TurnActivity } from './host/activity.js';
3
3
  export declare enum HarnessId {
4
4
  CLAUDE_CODE = "claude-code",
5
5
  CODEX = "codex",
@@ -0,0 +1,21 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ import type { HarnessAgentResumeSessionState } from '@ai-sdk/harness/agent';
3
+ import type { HarnessId } from '../messages.js';
4
+ export declare class HarnessRepo {
5
+ private readonly database;
6
+ constructor(database: DatabaseSync);
7
+ getHostId: () => string | undefined;
8
+ saveHostId: (hostId: string) => void;
9
+ getResumeState: (harnessId: HarnessId, sessionId: string) => HarnessAgentResumeSessionState | undefined;
10
+ getResumeFailure: (harnessId: HarnessId, sessionId: string) => ResumeFailure | undefined;
11
+ saveResumeState: (harnessId: HarnessId, sessionId: string, state: HarnessAgentResumeSessionState) => void;
12
+ saveResumeFailure: (harnessId: HarnessId, sessionId: string, message: string) => void;
13
+ deleteSession: (harnessId: HarnessId, sessionId: string) => void;
14
+ private deleteResumeFailure;
15
+ private transaction;
16
+ }
17
+ type ResumeFailure = {
18
+ message: string;
19
+ timestamp: string;
20
+ };
21
+ export {};
@@ -0,0 +1,76 @@
1
+ export class HarnessRepo {
2
+ database;
3
+ constructor(database) {
4
+ this.database = database;
5
+ }
6
+ getHostId = () => {
7
+ const row = this.database
8
+ .prepare('SELECT id FROM host WHERE singleton = 1')
9
+ .get();
10
+ return row?.id;
11
+ };
12
+ saveHostId = (hostId) => {
13
+ this.database
14
+ .prepare('INSERT INTO host (singleton, id) VALUES (1, ?)')
15
+ .run(hostId);
16
+ };
17
+ getResumeState = (harnessId, sessionId) => {
18
+ const row = this.database
19
+ .prepare('SELECT state FROM resume_state WHERE harness_id = ? AND session_id = ?')
20
+ .get(harnessId, sessionId);
21
+ return row
22
+ ? JSON.parse(row.state)
23
+ : undefined;
24
+ };
25
+ getResumeFailure = (harnessId, sessionId) => this.database
26
+ .prepare('SELECT message, timestamp FROM resume_failure WHERE harness_id = ? AND session_id = ?')
27
+ .get(harnessId, sessionId);
28
+ saveResumeState = (harnessId, sessionId, state) => {
29
+ this.transaction(() => {
30
+ this.database
31
+ .prepare(`
32
+ INSERT INTO resume_state (harness_id, session_id, state)
33
+ VALUES (?, ?, ?)
34
+ ON CONFLICT (harness_id, session_id)
35
+ DO UPDATE SET state = excluded.state
36
+ `)
37
+ .run(harnessId, sessionId, JSON.stringify(state));
38
+ this.deleteResumeFailure(harnessId, sessionId);
39
+ });
40
+ };
41
+ saveResumeFailure = (harnessId, sessionId, message) => {
42
+ this.database
43
+ .prepare(`
44
+ INSERT INTO resume_failure (harness_id, session_id, message, timestamp)
45
+ VALUES (?, ?, ?, ?)
46
+ ON CONFLICT (harness_id, session_id)
47
+ DO UPDATE SET message = excluded.message, timestamp = excluded.timestamp
48
+ `)
49
+ .run(harnessId, sessionId, message, new Date().toISOString());
50
+ };
51
+ deleteSession = (harnessId, sessionId) => {
52
+ this.transaction(() => {
53
+ this.database
54
+ .prepare('DELETE FROM resume_state WHERE harness_id = ? AND session_id = ?')
55
+ .run(harnessId, sessionId);
56
+ this.deleteResumeFailure(harnessId, sessionId);
57
+ });
58
+ };
59
+ deleteResumeFailure = (harnessId, sessionId) => {
60
+ this.database
61
+ .prepare('DELETE FROM resume_failure WHERE harness_id = ? AND session_id = ?')
62
+ .run(harnessId, sessionId);
63
+ };
64
+ transaction = (operation) => {
65
+ this.database.exec('BEGIN');
66
+ try {
67
+ const result = operation();
68
+ this.database.exec('COMMIT');
69
+ return result;
70
+ }
71
+ catch (error) {
72
+ this.database.exec('ROLLBACK');
73
+ throw error;
74
+ }
75
+ };
76
+ }
@@ -0,0 +1,2 @@
1
+ export { HarnessRepo } from './HarnessRepo.js';
2
+ export { harnessMigrations } from './migrations/index.js';
@@ -0,0 +1,2 @@
1
+ export { HarnessRepo } from './HarnessRepo.js';
2
+ export { harnessMigrations } from './migrations/index.js';
@@ -0,0 +1,2 @@
1
+ import type { Migration } from '../../database/index.js';
2
+ export declare const initialMigration: Migration;
@@ -0,0 +1,26 @@
1
+ export const initialMigration = {
2
+ version: 1,
3
+ apply: (database) => {
4
+ database.exec(`
5
+ CREATE TABLE host (
6
+ singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
7
+ id TEXT NOT NULL
8
+ ) STRICT;
9
+
10
+ CREATE TABLE resume_state (
11
+ harness_id TEXT NOT NULL,
12
+ session_id TEXT NOT NULL,
13
+ state TEXT NOT NULL,
14
+ PRIMARY KEY (harness_id, session_id)
15
+ ) STRICT;
16
+
17
+ CREATE TABLE resume_failure (
18
+ harness_id TEXT NOT NULL,
19
+ session_id TEXT NOT NULL,
20
+ message TEXT NOT NULL,
21
+ timestamp TEXT NOT NULL,
22
+ PRIMARY KEY (harness_id, session_id)
23
+ ) STRICT;
24
+ `);
25
+ },
26
+ };
@@ -0,0 +1,2 @@
1
+ import type { Migration } from '../../database/index.js';
2
+ export declare const harnessMigrations: Migration[];
@@ -0,0 +1,2 @@
1
+ import { initialMigration } from './001-initial.js';
2
+ export const harnessMigrations = [initialMigration];
@@ -1,4 +1,5 @@
1
- import type { Experimental_SandboxSession } from 'ai';
1
+ import type { Experimental_SandboxProcess, Experimental_SandboxSession } from 'ai';
2
+ import type { Experimental_SandboxSpawnOptions as SandboxSpawnOptions, Experimental_SandboxSpawnOptionsWithStdin as SandboxSpawnOptionsWithStdin, Experimental_SandboxSpawnOptionsWithoutStdin as SandboxSpawnOptionsWithoutStdin, Experimental_WritableSandboxProcess as WritableSandboxProcess } from '@ai-sdk/provider-utils';
2
3
  export declare class LocalSandboxSession implements Experimental_SandboxSession {
3
4
  private readonly cwd;
4
5
  readonly description: string;
@@ -10,6 +11,8 @@ export declare class LocalSandboxSession implements Experimental_SandboxSession
10
11
  writeBinaryFile: Experimental_SandboxSession['writeBinaryFile'];
11
12
  writeFile: Experimental_SandboxSession['writeFile'];
12
13
  writeTextFile: Experimental_SandboxSession['writeTextFile'];
13
- spawn: Experimental_SandboxSession['spawn'];
14
+ spawn(options: SandboxSpawnOptionsWithStdin): Promise<WritableSandboxProcess>;
15
+ spawn(options: SandboxSpawnOptionsWithoutStdin): Promise<Experimental_SandboxProcess>;
16
+ spawn(options: SandboxSpawnOptions): Promise<Experimental_SandboxProcess | WritableSandboxProcess>;
14
17
  run: Experimental_SandboxSession['run'];
15
18
  }
@@ -1,7 +1,7 @@
1
1
  import { spawn as spawnChild } from 'node:child_process';
2
2
  import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- import { Readable } from 'node:stream';
4
+ import { Readable, Writable } from 'node:stream';
5
5
  export class LocalSandboxSession {
6
6
  cwd;
7
7
  description;
@@ -63,7 +63,7 @@ export class LocalSandboxSession {
63
63
  abortSignal,
64
64
  });
65
65
  };
66
- spawn = async ({ command, workingDirectory, env, abortSignal }) => {
66
+ async spawn({ command, workingDirectory, env, abortSignal, stdin = false, }) {
67
67
  if (abortSignal?.aborted)
68
68
  throw abortSignal.reason;
69
69
  const child = spawnChild(command, {
@@ -72,7 +72,7 @@ export class LocalSandboxSession {
72
72
  shell: true,
73
73
  // Terminal Ctrl-C must reach the host first so it can save the harness session.
74
74
  detached: process.platform !== 'win32',
75
- stdio: ['ignore', 'pipe', 'pipe'],
75
+ stdio: [stdin ? 'pipe' : 'ignore', 'pipe', 'pipe'],
76
76
  });
77
77
  const stdout = Readable.toWeb(child.stdout);
78
78
  const stderr = Readable.toWeb(child.stderr);
@@ -111,8 +111,14 @@ export class LocalSandboxSession {
111
111
  };
112
112
  abortSignal?.addEventListener('abort', handleAbort, { once: true });
113
113
  void wait.finally(() => abortSignal?.removeEventListener('abort', handleAbort)).catch(() => { });
114
- return { pid: child.pid, stdout, stderr, wait: () => wait, kill };
115
- };
114
+ const sandboxProcess = { pid: child.pid, stdout, stderr, wait: () => wait, kill };
115
+ if (!stdin)
116
+ return sandboxProcess;
117
+ return {
118
+ ...sandboxProcess,
119
+ stdin: Writable.toWeb(child.stdin),
120
+ };
121
+ }
116
122
  run = async (options) => {
117
123
  const process = await this.spawn(options);
118
124
  const [stdout, stderr, result] = await Promise.all([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oorja-harness-host",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Host AI coding harness sessions on a Supakit-connected computer.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,16 +14,17 @@
14
14
  "dist"
15
15
  ],
16
16
  "scripts": {
17
- "build": "tsc -b && tsc-alias -p tsconfig.json --resolve-full-paths --resolve-full-extension .js",
17
+ "build": "pnpm run clean && tsc -b --force && tsc-alias -p tsconfig.json --resolve-full-paths --resolve-full-extension .js",
18
18
  "clean": "shx rm -rf dist",
19
19
  "prepack": "pnpm run build"
20
20
  },
21
21
  "dependencies": {
22
- "@ai-sdk/harness": "1.0.164",
23
- "@ai-sdk/harness-claude-code": "1.0.168",
24
- "@ai-sdk/harness-codex": "1.0.166",
25
- "@ai-sdk/harness-opencode": "1.0.166",
26
- "ai": "7.0.153"
22
+ "@ai-sdk/harness": "1.0.167",
23
+ "@ai-sdk/harness-claude-code": "1.0.171",
24
+ "@ai-sdk/harness-codex": "1.0.169",
25
+ "@ai-sdk/harness-opencode": "1.0.169",
26
+ "@ai-sdk/provider-utils": "5.0.70",
27
+ "ai": "7.0.156"
27
28
  },
28
29
  "devDependencies": {
29
30
  "@types/node": "^22.10.2",
@@ -32,7 +33,7 @@
32
33
  "typescript": "^5.9.2"
33
34
  },
34
35
  "engines": {
35
- "node": ">=22.10.0"
36
+ "node": ">=24.15.0"
36
37
  },
37
38
  "packageManager": "pnpm@11.5.2"
38
39
  }
@@ -1,2 +0,0 @@
1
- import type { ResumeStateStore } from '../resumeState.js';
2
- export declare const createFileResumeStateStore: (directory: string) => ResumeStateStore;
@@ -1,60 +0,0 @@
1
- import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
2
- import path from 'node:path';
3
- export const createFileResumeStateStore = (directory) => ({
4
- load: async (harnessId, sessionId) => {
5
- try {
6
- const failure = JSON.parse(await readFile(failurePath(directory, harnessId, sessionId), 'utf8'));
7
- throw new Error(`Cannot resume ${harnessId} session ${sessionId}: checkpoint failed at ${failure.timestamp}: ${failure.message}`);
8
- }
9
- catch (error) {
10
- if (!isMissingFile(error))
11
- throw error;
12
- }
13
- try {
14
- const stored = JSON.parse(await readFile(statePath(directory, harnessId, sessionId), 'utf8'));
15
- return stored.state;
16
- }
17
- catch (error) {
18
- if (isMissingFile(error))
19
- return;
20
- throw error;
21
- }
22
- },
23
- save: async (harnessId, sessionId, state) => {
24
- const destination = statePath(directory, harnessId, sessionId);
25
- await saveRecord(destination, { harnessId, sessionId, state });
26
- await removeFile(failurePath(directory, harnessId, sessionId));
27
- },
28
- markFailed: async (harnessId, sessionId, message) => {
29
- const failure = {
30
- harnessId,
31
- sessionId,
32
- timestamp: new Date().toISOString(),
33
- message,
34
- };
35
- await saveRecord(failurePath(directory, harnessId, sessionId), failure);
36
- },
37
- remove: async (harnessId, sessionId) => {
38
- await removeFile(statePath(directory, harnessId, sessionId));
39
- await removeFile(failurePath(directory, harnessId, sessionId));
40
- },
41
- });
42
- const statePath = (directory, harnessId, sessionId) => path.join(directory, `${encode(harnessId)}-${encode(sessionId)}.json`);
43
- const failurePath = (directory, harnessId, sessionId) => path.join(directory, `${encode(harnessId)}-${encode(sessionId)}.failure.json`);
44
- const saveRecord = async (destination, record) => {
45
- await mkdir(path.dirname(destination), { recursive: true, mode: 0o700 });
46
- const temporary = `${destination}.${process.pid}.tmp`;
47
- await writeFile(temporary, JSON.stringify(record), { encoding: 'utf8', mode: 0o600 });
48
- await rename(temporary, destination);
49
- };
50
- const removeFile = async (file) => {
51
- try {
52
- await unlink(file);
53
- }
54
- catch (error) {
55
- if (!isMissingFile(error))
56
- throw error;
57
- }
58
- };
59
- const encode = (value) => Buffer.from(value).toString('base64url');
60
- const isMissingFile = (error) => error instanceof Error && 'code' in error && error.code === 'ENOENT';
@@ -1,8 +0,0 @@
1
- import type { HarnessAgentResumeSessionState } from '@ai-sdk/harness/agent';
2
- import type { HarnessId } from './messages.js';
3
- export type ResumeStateStore = {
4
- load: (harnessId: HarnessId, sessionId: string) => Promise<HarnessAgentResumeSessionState | undefined>;
5
- save: (harnessId: HarnessId, sessionId: string, state: HarnessAgentResumeSessionState) => Promise<void>;
6
- markFailed: (harnessId: HarnessId, sessionId: string, message: string) => Promise<void>;
7
- remove: (harnessId: HarnessId, sessionId: string) => Promise<void>;
8
- };
@@ -1 +0,0 @@
1
- export {};
File without changes
File without changes
File without changes