@steerable/agent-shell 0.6.55 → 0.6.57

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.
@@ -1,11 +1,14 @@
1
+ import { existsSync } from 'node:fs';
1
2
  import path from 'node:path';
2
3
  import Database from 'better-sqlite3';
3
4
  import { getProductConfig } from '../product-config.js';
4
5
  import { getUserDataDir } from '../runtime.js';
5
6
  import { LOCAL_SCOPE } from './driver.js';
6
7
  import { SqliteScopedStore } from './index.js';
7
- import { acquireWriteLease, lockPathForDb, } from './write-lease.js';
8
+ import { HOST_SCHEMA_VERSION, OPEN_LOCK_WAIT_MS, StorageUpgradeBlockedError, openLockPath, } from './process-locks.js';
9
+ import { acquireSharedLease, acquireWriteLease, lockPathForDb, StoreAlreadyOwnedError, } from './write-lease.js';
8
10
  import { acquireWriteLeaseOrExit } from './write-lease-error.js';
11
+ const MAIN_DB_BUSY_TIMEOUT_MS = 5_000;
9
12
  function invokeStatement(statement, operation, params) {
10
13
  if (Array.isArray(params))
11
14
  return statement[operation](...params);
@@ -71,32 +74,92 @@ class SqlitePackDbAccess {
71
74
  }
72
75
  }
73
76
  }
77
+ function readUserVersion(dbPath) {
78
+ if (!existsSync(dbPath))
79
+ return 0;
80
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true, timeout: 5_000 });
81
+ try {
82
+ return Number(db.pragma('user_version', { simple: true }));
83
+ }
84
+ finally {
85
+ db.close();
86
+ }
87
+ }
88
+ function blockUpgrade(message) {
89
+ console.error(`[bs] storage upgrade blocked: ${message}`);
90
+ queueMicrotask(() => process.exit(75));
91
+ throw new StorageUpgradeBlockedError(message);
92
+ }
74
93
  /** Default local SQLite storage driver. */
75
94
  export class SqliteStorageDriver {
76
95
  db = null;
77
- writeLease = null;
96
+ openLease = null;
97
+ changeTimer = null;
78
98
  localSqliteStore = null;
79
99
  stores = new Map();
80
100
  async initialize() {
81
101
  if (this.db)
82
102
  return;
83
103
  const dbPath = path.join(getUserDataDir(), getProductConfig().dbFileName ?? 'agent-shell.db');
84
- this.writeLease = acquireWriteLeaseOrExit(() => acquireWriteLease(lockPathForDb(dbPath)));
104
+ // A previous build held this lock for the whole process. If it is still
105
+ // held, that process cannot share the database, so refuse to migrate
106
+ // or to open alongside it.
107
+ acquireWriteLeaseOrExit(() => {
108
+ const legacy = acquireWriteLease(lockPathForDb(dbPath));
109
+ legacy.release();
110
+ });
111
+ const gate = openLockPath(dbPath);
112
+ let version = readUserVersion(dbPath);
113
+ if (version > HOST_SCHEMA_VERSION) {
114
+ blockUpgrade(`本地数据库版本 ${version} 高于本程序支持的版本 ${HOST_SCHEMA_VERSION}。请升级后再打开。`);
115
+ }
116
+ let exclusive = null;
117
+ if (version === HOST_SCHEMA_VERSION) {
118
+ // Make sure the open-lock table exists, then register as a shared holder.
119
+ // A live shared holder rejects this exclusive probe immediately.
120
+ try {
121
+ acquireWriteLease(gate).release();
122
+ }
123
+ catch (error) {
124
+ if (!(error instanceof StoreAlreadyOwnedError))
125
+ throw error;
126
+ }
127
+ }
128
+ else {
129
+ try {
130
+ exclusive = acquireWriteLease(gate, OPEN_LOCK_WAIT_MS);
131
+ }
132
+ catch (error) {
133
+ if (!(error instanceof StoreAlreadyOwnedError))
134
+ throw error;
135
+ version = readUserVersion(dbPath);
136
+ if (version !== HOST_SCHEMA_VERSION) {
137
+ blockUpgrade('本地数据库需要升级,但仍被另一个进程打开。请先关闭正在运行的应用及其命令行任务后再升级。');
138
+ }
139
+ }
140
+ }
85
141
  let db = null;
86
142
  try {
87
- db = new Database(dbPath);
143
+ db = new Database(dbPath, { timeout: MAIN_DB_BUSY_TIMEOUT_MS });
144
+ db.pragma(`busy_timeout = ${MAIN_DB_BUSY_TIMEOUT_MS}`);
88
145
  db.pragma('journal_mode = WAL');
89
146
  db.pragma('foreign_keys = ON');
90
147
  this.db = db;
91
148
  const local = new SqliteScopedStore(db, LOCAL_SCOPE);
92
149
  await local.initialize();
150
+ db.pragma(`user_version = ${HOST_SCHEMA_VERSION}`);
93
151
  this.localSqliteStore = local;
94
152
  this.stores.set(this.scopeKey(LOCAL_SCOPE), local);
153
+ exclusive?.release();
154
+ exclusive = null;
155
+ this.openLease = acquireSharedLease(gate);
95
156
  }
96
157
  catch (error) {
158
+ exclusive?.release();
97
159
  db?.close();
98
- this.writeLease.release();
99
- this.writeLease = null;
160
+ this.db = null;
161
+ this.openLease?.release();
162
+ this.openLease = null;
100
163
  throw error;
101
164
  }
102
165
  }
@@ -116,13 +179,31 @@ export class SqliteStorageDriver {
116
179
  async applyPackMigrations() {
117
180
  this.localSqliteStore?.applyPackMigrations();
118
181
  }
182
+ watchChanges(onChange) {
183
+ const db = this.requireDb();
184
+ if (this.changeTimer)
185
+ return;
186
+ let version = Number(db.pragma('data_version', { simple: true }));
187
+ this.changeTimer = setInterval(() => {
188
+ const next = Number(db.pragma('data_version', { simple: true }));
189
+ if (next === version)
190
+ return;
191
+ version = next;
192
+ onChange();
193
+ }, 1_000);
194
+ this.changeTimer.unref?.();
195
+ }
119
196
  async close() {
197
+ if (this.changeTimer) {
198
+ clearInterval(this.changeTimer);
199
+ this.changeTimer = null;
200
+ }
120
201
  this.stores.clear();
121
202
  this.db?.close();
122
203
  this.db = null;
123
204
  this.localSqliteStore = null;
124
- this.writeLease?.release();
125
- this.writeLease = null;
205
+ this.openLease?.release();
206
+ this.openLease = null;
126
207
  }
127
208
  requireDb() {
128
209
  if (!this.db)
@@ -23,4 +23,10 @@ export interface HeldWriteLease {
23
23
  release(): void;
24
24
  }
25
25
  export declare function lockPathForDb(dbPath: string): string;
26
- export declare function acquireWriteLease(lockPath: string): HeldWriteLease;
26
+ export declare function acquireWriteLease(lockPath: string, timeoutMs?: number): HeldWriteLease;
27
+ /**
28
+ * Shared open registration. Many processes may hold this at once.
29
+ * An exclusive lease on the same file waits until every shared holder closes.
30
+ * The lock file must already contain the lease table (the exclusive opener creates it).
31
+ */
32
+ export declare function acquireSharedLease(lockPath: string): HeldWriteLease;
@@ -36,17 +36,18 @@ export function lockPathForDb(dbPath) {
36
36
  const parsed = path.parse(path.resolve(dbPath));
37
37
  return path.join(parsed.dir, `${parsed.name}.lock`);
38
38
  }
39
- export function acquireWriteLease(lockPath) {
39
+ export function acquireWriteLease(lockPath, timeoutMs = 0) {
40
+ const waitMs = Number.isFinite(timeoutMs) ? Math.max(0, Math.trunc(timeoutMs)) : 0;
40
41
  fs.mkdirSync(path.dirname(lockPath), { recursive: true });
41
42
  // Loaded lazily so tests that only exercise lockPathForDb do not need
42
43
  // better-sqlite3's Electron ABI.
43
44
  const Database = require('better-sqlite3');
44
- const db = new Database(lockPath, { timeout: 0 });
45
+ const db = new Database(lockPath, { timeout: waitMs });
45
46
  try {
46
47
  // Every statement here can hit the incumbent's lock, including the
47
48
  // journal_mode pragma — it rewrites the header and so needs the same
48
- // write lock. busy_timeout 0 makes that immediate instead of a stall.
49
- db.pragma('busy_timeout = 0');
49
+ // write lock. timeoutMs 0 makes that immediate instead of a stall.
50
+ db.pragma(`busy_timeout = ${waitMs}`);
50
51
  db.pragma('journal_mode = DELETE');
51
52
  db.exec('CREATE TABLE IF NOT EXISTS lease (id INTEGER PRIMARY KEY)');
52
53
  db.exec('BEGIN EXCLUSIVE');
@@ -72,3 +73,33 @@ export function acquireWriteLease(lockPath) {
72
73
  },
73
74
  };
74
75
  }
76
+ /**
77
+ * Shared open registration. Many processes may hold this at once.
78
+ * An exclusive lease on the same file waits until every shared holder closes.
79
+ * The lock file must already contain the lease table (the exclusive opener creates it).
80
+ */
81
+ export function acquireSharedLease(lockPath) {
82
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
83
+ const Database = require('better-sqlite3');
84
+ const db = new Database(lockPath, { timeout: 5_000 });
85
+ try {
86
+ db.pragma('busy_timeout = 5000');
87
+ db.exec('BEGIN');
88
+ db.prepare('SELECT id FROM lease LIMIT 1').get();
89
+ }
90
+ catch (err) {
91
+ db.close();
92
+ if (isSqliteBusy(err))
93
+ throw new StoreAlreadyOwnedError(lockPath);
94
+ throw err;
95
+ }
96
+ let released = false;
97
+ return {
98
+ release() {
99
+ if (released)
100
+ return;
101
+ released = true;
102
+ db.close();
103
+ },
104
+ };
105
+ }
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import os from 'os';
3
3
  import { randomUUID } from 'crypto';
4
- import log from 'electron-log';
4
+ import { log } from './log.js';
5
5
  import * as pty from 'node-pty';
6
6
  import { adaptCommandForPowerShell } from './shell-adapt.js';
7
7
  const DEFAULT_COLS = 100;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@steerable/agent-shell",
3
- "version": "0.6.55",
3
+ "version": "0.6.57",
4
4
  "description": "Steerable framework — product-neutral Node HostRuntime and headless HTTP server shared by Tauri desktop and browser deployments; scenario packs extend it through @steerable/pack-sdk.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://steerableframework.com/",
@@ -49,12 +49,11 @@
49
49
  "better-sqlite3": "^11.9.1",
50
50
  "conf": "10.2.0",
51
51
  "dotenv": "^17.4.2",
52
- "electron-log": "^5.4.3",
53
52
  "he": "^1.2.0",
54
53
  "node-pty": "^1.1.0",
55
- "@steerable/agent-harness": "0.6.55",
56
- "@steerable/pack-sdk": "0.6.55",
57
- "@steerable/agent-protocol": "0.6.55"
54
+ "@steerable/agent-harness": "0.6.57",
55
+ "@steerable/pack-sdk": "0.6.57",
56
+ "@steerable/agent-protocol": "0.6.57"
58
57
  },
59
58
  "devDependencies": {
60
59
  "@types/better-sqlite3": "^7.6.13",