@agimon-ai/doompi-core 0.0.1-alpha.107 → 0.0.1-alpha.108

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 (40) hide show
  1. package/dist/childSession.d.mts +2 -2
  2. package/dist/sqliteSessionStorage.cjs +46 -19
  3. package/dist/sqliteSessionStorage.cjs.map +1 -1
  4. package/dist/sqliteSessionStorage.d.mts.map +1 -1
  5. package/dist/sqliteSessionStorage.mjs +47 -20
  6. package/dist/sqliteSessionStorage.mjs.map +1 -1
  7. package/dist/src/server/directHarnessRuntime.cjs +48 -23
  8. package/dist/src/server/directHarnessRuntime.cjs.map +1 -1
  9. package/dist/src/server/directHarnessRuntime.mjs +48 -23
  10. package/dist/src/server/directHarnessRuntime.mjs.map +1 -1
  11. package/dist/src/services/piExtensionHost/index.cjs +13 -8
  12. package/dist/src/services/piExtensionHost/index.cjs.map +1 -1
  13. package/dist/src/services/piExtensionHost/index.mjs +13 -8
  14. package/dist/src/services/piExtensionHost/index.mjs.map +1 -1
  15. package/dist/src/systems/child/adapters/headlessChildSessionService.cjs +127 -41
  16. package/dist/src/systems/child/adapters/headlessChildSessionService.cjs.map +1 -1
  17. package/dist/src/systems/child/adapters/headlessChildSessionService.d.mts +3 -2
  18. package/dist/src/systems/child/adapters/headlessChildSessionService.d.mts.map +1 -1
  19. package/dist/src/systems/child/adapters/headlessChildSessionService.mjs +127 -42
  20. package/dist/src/systems/child/adapters/headlessChildSessionService.mjs.map +1 -1
  21. package/dist/src/systems/child/types/childSession.cjs.map +1 -1
  22. package/dist/src/systems/child/types/childSession.d.mts +11 -1
  23. package/dist/src/systems/child/types/childSession.d.mts.map +1 -1
  24. package/dist/src/systems/child/types/childSession.mjs.map +1 -1
  25. package/dist/src/systems/main/adapters/headlessHost.cjs +14 -5
  26. package/dist/src/systems/main/adapters/headlessHost.cjs.map +1 -1
  27. package/dist/src/systems/main/adapters/headlessHost.d.mts.map +1 -1
  28. package/dist/src/systems/main/adapters/headlessHost.mjs +14 -5
  29. package/dist/src/systems/main/adapters/headlessHost.mjs.map +1 -1
  30. package/dist/src/systems/main/adapters/headlessSessionHost.cjs +5 -0
  31. package/dist/src/systems/main/adapters/headlessSessionHost.cjs.map +1 -1
  32. package/dist/src/systems/main/adapters/headlessSessionHost.mjs +5 -0
  33. package/dist/src/systems/main/adapters/headlessSessionHost.mjs.map +1 -1
  34. package/dist/terminalPiChildSessionService.cjs +18 -5
  35. package/dist/terminalPiChildSessionService.cjs.map +1 -1
  36. package/dist/terminalPiChildSessionService.d.mts +3 -2
  37. package/dist/terminalPiChildSessionService.d.mts.map +1 -1
  38. package/dist/terminalPiChildSessionService.mjs +19 -6
  39. package/dist/terminalPiChildSessionService.mjs.map +1 -1
  40. package/package.json +1 -1
@@ -1,3 +1,3 @@
1
- import { DOOM_CHILD_SESSION_MCP_TOOL_SERVICE, DOOM_CHILD_SESSION_SERVICE, DoomChildSessionCapabilityCeiling, DoomChildSessionEvent, DoomChildSessionFreshSource, DoomChildSessionHandle, DoomChildSessionIntercom, DoomChildSessionRequest, DoomChildSessionRuntime, DoomChildSessionRuntimeFactory, DoomChildSessionScope, DoomChildSessionService, DoomChildSessionServiceProvider, DoomChildSessionSource, DoomChildSessionState, DoomChildSessionTerminalPiForkSource, DoomChildSessionTool, DoomChildSessionToolResult, DoomChildSessionUsage, DoomChildSessionV4ForkSource, DoomChildSessionV4RestoreSource, readDoomChildSessionService, requireDoomChildSessionService } from "./src/systems/child/types/childSession.mjs";
1
+ import { DOOM_CHILD_SESSION_MCP_TOOL_SERVICE, DOOM_CHILD_SESSION_SERVICE, DoomChildSessionCapabilityCeiling, DoomChildSessionEvent, DoomChildSessionFreshSource, DoomChildSessionHandle, DoomChildSessionIntercom, DoomChildSessionMcpCatalog, DoomChildSessionMcpTool, DoomChildSessionRequest, DoomChildSessionRuntime, DoomChildSessionRuntimeFactory, DoomChildSessionScope, DoomChildSessionService, DoomChildSessionServiceProvider, DoomChildSessionSource, DoomChildSessionState, DoomChildSessionTerminalPiForkSource, DoomChildSessionTool, DoomChildSessionToolResult, DoomChildSessionUsage, DoomChildSessionV4ForkSource, DoomChildSessionV4RestoreSource, readDoomChildSessionService, requireDoomChildSessionService } from "./src/systems/child/types/childSession.mjs";
2
2
  import { DoomChildSessionServiceDependencies, createDoomChildSessionService } from "./src/systems/child/services/childSession/index.mjs";
3
- export { DOOM_CHILD_SESSION_MCP_TOOL_SERVICE, DOOM_CHILD_SESSION_SERVICE, DoomChildSessionCapabilityCeiling, DoomChildSessionEvent, DoomChildSessionFreshSource, DoomChildSessionHandle, DoomChildSessionIntercom, DoomChildSessionRequest, DoomChildSessionRuntime, DoomChildSessionRuntimeFactory, DoomChildSessionScope, DoomChildSessionService, DoomChildSessionServiceDependencies, DoomChildSessionServiceProvider, DoomChildSessionSource, DoomChildSessionState, DoomChildSessionTerminalPiForkSource, DoomChildSessionTool, DoomChildSessionToolResult, DoomChildSessionUsage, DoomChildSessionV4ForkSource, DoomChildSessionV4RestoreSource, createDoomChildSessionService, readDoomChildSessionService, requireDoomChildSessionService };
3
+ export { DOOM_CHILD_SESSION_MCP_TOOL_SERVICE, DOOM_CHILD_SESSION_SERVICE, DoomChildSessionCapabilityCeiling, DoomChildSessionEvent, DoomChildSessionFreshSource, DoomChildSessionHandle, DoomChildSessionIntercom, DoomChildSessionMcpCatalog, DoomChildSessionMcpTool, DoomChildSessionRequest, DoomChildSessionRuntime, DoomChildSessionRuntimeFactory, DoomChildSessionScope, DoomChildSessionService, DoomChildSessionServiceDependencies, DoomChildSessionServiceProvider, DoomChildSessionSource, DoomChildSessionState, DoomChildSessionTerminalPiForkSource, DoomChildSessionTool, DoomChildSessionToolResult, DoomChildSessionUsage, DoomChildSessionV4ForkSource, DoomChildSessionV4RestoreSource, createDoomChildSessionService, readDoomChildSessionService, requireDoomChildSessionService };
@@ -60,24 +60,40 @@ function validateDurableSessionFile(file) {
60
60
  /** Completed histories use a read-only connection. Initialization cannot write or migrate. */
61
61
  async function openReadOnlyDurableStorage(file) {
62
62
  validateDurableSessionFile(file);
63
- const database = new _earendil_works_pi_durable_storage_sqlite_node.NodeSqliteDatabase(new node_sqlite.DatabaseSync(file, { readOnly: true }));
64
- return await _earendil_works_pi_durable_storage_sqlite.SqliteStorage.open({
65
- exec: (sql) => database.exec(sql),
66
- run: (sql, ...params) => database.run(sql, ...params),
67
- get: (sql, ...params) => database.get(sql, ...params),
68
- all: (sql, ...params) => database.all(sql, ...params),
69
- close: () => database.close(),
70
- transaction: (callback) => callback({
71
- exec: async (sql) => {
72
- if (!/^CREATE TABLE IF NOT EXISTS durable_schema\s/u.test(sql)) throw new Error("Read-only durable storage");
73
- },
74
- run: async (sql) => {
75
- if (sql !== "INSERT OR IGNORE INTO durable_schema (singleton, version) VALUES (1, 0)") throw new Error("Read-only durable storage");
76
- },
63
+ const native = new node_sqlite.DatabaseSync(file, { readOnly: true });
64
+ const database = new _earendil_works_pi_durable_storage_sqlite_node.NodeSqliteDatabase(native);
65
+ let closed = false;
66
+ const close = async () => {
67
+ if (closed) return;
68
+ native.close();
69
+ closed = true;
70
+ };
71
+ try {
72
+ return await _earendil_works_pi_durable_storage_sqlite.SqliteStorage.open({
73
+ exec: (sql) => database.exec(sql),
74
+ run: (sql, ...params) => database.run(sql, ...params),
77
75
  get: (sql, ...params) => database.get(sql, ...params),
78
- all: (sql, ...params) => database.all(sql, ...params)
79
- })
80
- });
76
+ all: (sql, ...params) => database.all(sql, ...params),
77
+ close,
78
+ transaction: (callback) => callback({
79
+ exec: async (sql) => {
80
+ if (!/^CREATE TABLE IF NOT EXISTS durable_schema\s/u.test(sql)) throw new Error("Read-only durable storage");
81
+ },
82
+ run: async (sql) => {
83
+ if (sql !== "INSERT OR IGNORE INTO durable_schema (singleton, version) VALUES (1, 0)") throw new Error("Read-only durable storage");
84
+ },
85
+ get: (sql, ...params) => database.get(sql, ...params),
86
+ all: (sql, ...params) => database.all(sql, ...params)
87
+ })
88
+ });
89
+ } catch (error) {
90
+ try {
91
+ await close();
92
+ } catch (cleanupError) {
93
+ throw new AggregateError([error, cleanupError], "Read-only storage initialization cleanup failed");
94
+ }
95
+ throw error;
96
+ }
81
97
  }
82
98
  /** Opens the one fresh-format SQLite container owned by a server session. */
83
99
  async function openSqliteSessionStorage(options, context) {
@@ -97,10 +113,14 @@ async function openSqliteSessionStorage(options, context) {
97
113
  });
98
114
  const historyLease = await options.historyOwnership.acquire(sessionFile);
99
115
  let storage;
116
+ let database;
117
+ let writerOpenAttempted = false;
100
118
  try {
101
119
  await historyLease.assertQuiescent();
102
120
  if (node_fs.default.existsSync(sessionFile)) validateDurableSessionFile(sessionFile);
103
- storage = await (0, _earendil_works_pi_durable_storage_sqlite_node.openNodeSqliteStorage)(sessionFile);
121
+ writerOpenAttempted = true;
122
+ database = await (0, _earendil_works_pi_durable_storage_sqlite_node.openNodeSqliteDatabase)(sessionFile);
123
+ storage = await _earendil_works_pi_durable_storage_sqlite.SqliteStorage.open(database);
104
124
  const session = (0, _earendil_works_pi_durable.createSession)(storage);
105
125
  const identity = await session.snapshot(SessionIdentityDoc, context);
106
126
  if (existing && (!identity || identity.id !== id)) throw new Error("SQLite session id does not match the requested session");
@@ -121,10 +141,17 @@ async function openSqliteSessionStorage(options, context) {
121
141
  historyLease
122
142
  };
123
143
  } catch (error) {
144
+ if (writerOpenAttempted && !database) throw error;
124
145
  try {
125
146
  if (storage) await storage.close(context);
126
- } finally {
147
+ else if (database) await database.close();
148
+ } catch (cleanupError) {
149
+ throw new AggregateError([error, cleanupError], "SQLite storage initialization cleanup failed");
150
+ }
151
+ try {
127
152
  await historyLease.release();
153
+ } catch (cleanupError) {
154
+ throw new AggregateError([error, cleanupError], "SQLite storage ownership cleanup failed");
128
155
  }
129
156
  throw error;
130
157
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sqliteSessionStorage.cjs","names":["BACKGROUND_CONTEXT","defineDoc","DatabaseSync","NodeSqliteDatabase","SqliteStorage","path","randomUUID","fs","openNodeSqliteStorage","createSession"],"sources":["../src/services/sqliteSessionStorage/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\n\nimport type { Context } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { createSession, defineDoc, type Storage } from '@earendil-works/pi-durable';\nimport { SqliteStorage } from '@earendil-works/pi-durable/storage/sqlite';\nimport { NodeSqliteDatabase, openNodeSqliteStorage } from '@earendil-works/pi-durable/storage/sqlite/node';\n\nimport type { DirectHarnessRuntimeOptions } from '../../types/server/directHarnessRuntime';\n\nexport const DURABLE_BACKGROUND_CONTEXT = BACKGROUND_CONTEXT;\n\nexport const DURABLE_DIRECTORY = 'durable-v1';\n/** Public string identity belongs to Doom, not the durable numeric ID namespace. */\nexport const SessionIdentityDoc = defineDoc({\n kind: 'doompi.session.identity',\n version: 1,\n scope: 'session',\n initial: () => ({ id: '', createdAt: 0, parentSessionId: '' }),\n});\nexport const SessionMetadataDoc = defineDoc({\n kind: 'doompi.session.metadata',\n version: 1,\n scope: 'session',\n initial: () => ({ name: '', workspaceRoot: '', execution: '', laneName: 'main' }),\n});\n\n/** Validate existing files without admitting a writer or applying schema upgrades. */\nexport function validateDurableSessionFile(file: string): void {\n const database = new DatabaseSync(file, { readOnly: true });\n try {\n const schema = database.prepare('SELECT version FROM durable_schema WHERE singleton = 1').get();\n if (schema?.version !== 1) throw new Error('Unsupported durable SQLite schema');\n const tables = database.prepare(\"SELECT name FROM sqlite_master WHERE type = 'table'\").all();\n for (const name of [\n 'durable_metadata',\n 'record_ids',\n 'conversations',\n 'entries',\n 'tasks',\n 'submissions',\n 'documents',\n 'document_revisions',\n ]) {\n if (!tables.some((table) => table.name === name)) throw new Error('Invalid durable SQLite schema');\n }\n } catch (cause) {\n throw new Error('Unsupported or legacy SQLite session format', { cause });\n } finally {\n database.close();\n }\n}\n\n/** Completed histories use a read-only connection. Initialization cannot write or migrate. */\nexport async function openReadOnlyDurableStorage(file: string): Promise<Storage> {\n validateDurableSessionFile(file);\n const database = new NodeSqliteDatabase(new DatabaseSync(file, { readOnly: true }));\n const storage = await SqliteStorage.open({\n exec: (sql) => database.exec(sql),\n run: (sql, ...params) => database.run(sql, ...params),\n get: (sql, ...params) => database.get(sql, ...params),\n all: (sql, ...params) => database.all(sql, ...params),\n close: () => database.close(),\n transaction: (callback) =>\n callback({\n exec: async (sql) => {\n if (!/^CREATE TABLE IF NOT EXISTS durable_schema\\s/u.test(sql)) throw new Error('Read-only durable storage');\n },\n run: async (sql) => {\n if (sql !== 'INSERT OR IGNORE INTO durable_schema (singleton, version) VALUES (1, 0)')\n throw new Error('Read-only durable storage');\n },\n get: (sql, ...params) => database.get(sql, ...params),\n all: (sql, ...params) => database.all(sql, ...params),\n }),\n });\n return storage;\n}\n\n/** Opens the one fresh-format SQLite container owned by a server session. */\nexport async function openSqliteSessionStorage(\n options: Pick<\n DirectHarnessRuntimeOptions,\n 'historyOwnership' | 'sessionsRoot' | 'sessionPath' | 'sessionId' | 'parentSessionId'\n >,\n context: Context,\n) {\n if (!options.historyOwnership) throw new Error('SQLite sessions require explicit HistoryOwnership');\n if (!options.sessionsRoot && !options.sessionPath) throw new Error('SQLite sessions require a server data directory');\n const id = options.sessionId ?? (options.sessionPath ? path.basename(options.sessionPath, '.sqlite') : randomUUID());\n if (!/^[a-zA-Z0-9_-]+$/u.test(id)) throw new Error('Invalid SQLite session id');\n const sessionFile = path.resolve(\n options.sessionPath ?? path.join(options.sessionsRoot!, DURABLE_DIRECTORY, `${id}.sqlite`),\n );\n if (path.basename(path.dirname(sessionFile)) !== DURABLE_DIRECTORY || path.extname(sessionFile) !== '.sqlite')\n throw new Error('Unsupported or legacy SQLite session path');\n const existing = fs.existsSync(sessionFile);\n if (!existing && options.sessionsRoot && fs.existsSync(path.join(options.sessionsRoot, `${id}.sqlite`)))\n throw new Error('Legacy SQLite sessions are unsupported by fresh durable storage');\n if (options.sessionPath && !existing) throw new Error(`SQLite session not found: ${sessionFile}`);\n if (existing) validateDurableSessionFile(sessionFile);\n fs.mkdirSync(path.dirname(sessionFile), { recursive: true, mode: 0o700 });\n const historyLease = await options.historyOwnership.acquire(sessionFile);\n let storage: Storage | undefined;\n try {\n await historyLease.assertQuiescent();\n if (fs.existsSync(sessionFile)) validateDurableSessionFile(sessionFile);\n storage = await openNodeSqliteStorage(sessionFile);\n const session = createSession(storage);\n const identity = await session.snapshot(SessionIdentityDoc, context);\n if (existing && (!identity || identity.id !== id))\n throw new Error('SQLite session id does not match the requested session');\n if (!existing) {\n await session.commit(async (tx) => {\n const doc = await tx.doc(SessionIdentityDoc);\n doc.id = id;\n doc.createdAt = Date.now();\n doc.parentSessionId = options.parentSessionId ?? '';\n }, context);\n fs.chmodSync(sessionFile, 0o600);\n }\n const repository = { close: (closeContext: Context = context) => session.close(closeContext) };\n return { storage, session, sessionFile, repository, historyLease };\n } catch (error) {\n try {\n if (storage) await storage.close(context);\n } finally {\n await historyLease.release();\n }\n throw error;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sqliteSessionStorage.cjs","names":["BACKGROUND_CONTEXT","defineDoc","DatabaseSync","NodeSqliteDatabase","SqliteStorage","path","randomUUID","fs","openNodeSqliteDatabase","createSession"],"sources":["../src/services/sqliteSessionStorage/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\n\nimport type { Context } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { createSession, defineDoc, type Storage } from '@earendil-works/pi-durable';\nimport { SqliteStorage } from '@earendil-works/pi-durable/storage/sqlite';\nimport { NodeSqliteDatabase, openNodeSqliteDatabase } from '@earendil-works/pi-durable/storage/sqlite/node';\n\nimport type { DirectHarnessRuntimeOptions } from '../../types/server/directHarnessRuntime';\n\nexport const DURABLE_BACKGROUND_CONTEXT = BACKGROUND_CONTEXT;\n\nexport const DURABLE_DIRECTORY = 'durable-v1';\n/** Public string identity belongs to Doom, not the durable numeric ID namespace. */\nexport const SessionIdentityDoc = defineDoc({\n kind: 'doompi.session.identity',\n version: 1,\n scope: 'session',\n initial: () => ({ id: '', createdAt: 0, parentSessionId: '' }),\n});\nexport const SessionMetadataDoc = defineDoc({\n kind: 'doompi.session.metadata',\n version: 1,\n scope: 'session',\n initial: () => ({ name: '', workspaceRoot: '', execution: '', laneName: 'main' }),\n});\n\n/** Validate existing files without admitting a writer or applying schema upgrades. */\nexport function validateDurableSessionFile(file: string): void {\n const database = new DatabaseSync(file, { readOnly: true });\n try {\n const schema = database.prepare('SELECT version FROM durable_schema WHERE singleton = 1').get();\n if (schema?.version !== 1) throw new Error('Unsupported durable SQLite schema');\n const tables = database.prepare(\"SELECT name FROM sqlite_master WHERE type = 'table'\").all();\n for (const name of [\n 'durable_metadata',\n 'record_ids',\n 'conversations',\n 'entries',\n 'tasks',\n 'submissions',\n 'documents',\n 'document_revisions',\n ]) {\n if (!tables.some((table) => table.name === name)) throw new Error('Invalid durable SQLite schema');\n }\n } catch (cause) {\n throw new Error('Unsupported or legacy SQLite session format', { cause });\n } finally {\n database.close();\n }\n}\n\n/** Completed histories use a read-only connection. Initialization cannot write or migrate. */\nexport async function openReadOnlyDurableStorage(file: string): Promise<Storage> {\n validateDurableSessionFile(file);\n const native = new DatabaseSync(file, { readOnly: true });\n const database = new NodeSqliteDatabase(native);\n let closed = false;\n const close = async () => {\n if (closed) return;\n // The writer adapter checkpoints WAL on close, which read-only connections cannot do.\n native.close();\n closed = true;\n };\n try {\n const storage = await SqliteStorage.open({\n exec: (sql) => database.exec(sql),\n run: (sql, ...params) => database.run(sql, ...params),\n get: (sql, ...params) => database.get(sql, ...params),\n all: (sql, ...params) => database.all(sql, ...params),\n close,\n transaction: (callback) =>\n callback({\n exec: async (sql) => {\n if (!/^CREATE TABLE IF NOT EXISTS durable_schema\\s/u.test(sql))\n throw new Error('Read-only durable storage');\n },\n run: async (sql) => {\n if (sql !== 'INSERT OR IGNORE INTO durable_schema (singleton, version) VALUES (1, 0)')\n throw new Error('Read-only durable storage');\n },\n get: (sql, ...params) => database.get(sql, ...params),\n all: (sql, ...params) => database.all(sql, ...params),\n }),\n });\n return storage;\n } catch (error) {\n try {\n await close();\n } catch (cleanupError) {\n throw new AggregateError([error, cleanupError], 'Read-only storage initialization cleanup failed');\n }\n throw error;\n }\n}\n\n/** Opens the one fresh-format SQLite container owned by a server session. */\nexport async function openSqliteSessionStorage(\n options: Pick<\n DirectHarnessRuntimeOptions,\n 'historyOwnership' | 'sessionsRoot' | 'sessionPath' | 'sessionId' | 'parentSessionId'\n >,\n context: Context,\n) {\n if (!options.historyOwnership) throw new Error('SQLite sessions require explicit HistoryOwnership');\n if (!options.sessionsRoot && !options.sessionPath) throw new Error('SQLite sessions require a server data directory');\n const id = options.sessionId ?? (options.sessionPath ? path.basename(options.sessionPath, '.sqlite') : randomUUID());\n if (!/^[a-zA-Z0-9_-]+$/u.test(id)) throw new Error('Invalid SQLite session id');\n const sessionFile = path.resolve(\n options.sessionPath ?? path.join(options.sessionsRoot!, DURABLE_DIRECTORY, `${id}.sqlite`),\n );\n if (path.basename(path.dirname(sessionFile)) !== DURABLE_DIRECTORY || path.extname(sessionFile) !== '.sqlite')\n throw new Error('Unsupported or legacy SQLite session path');\n const existing = fs.existsSync(sessionFile);\n if (!existing && options.sessionsRoot && fs.existsSync(path.join(options.sessionsRoot, `${id}.sqlite`)))\n throw new Error('Legacy SQLite sessions are unsupported by fresh durable storage');\n if (options.sessionPath && !existing) throw new Error(`SQLite session not found: ${sessionFile}`);\n if (existing) validateDurableSessionFile(sessionFile);\n fs.mkdirSync(path.dirname(sessionFile), { recursive: true, mode: 0o700 });\n const historyLease = await options.historyOwnership.acquire(sessionFile);\n let storage: Storage | undefined;\n let database: NodeSqliteDatabase | undefined;\n let writerOpenAttempted = false;\n try {\n await historyLease.assertQuiescent();\n if (fs.existsSync(sessionFile)) validateDurableSessionFile(sessionFile);\n writerOpenAttempted = true;\n database = await openNodeSqliteDatabase(sessionFile);\n storage = await SqliteStorage.open(database);\n const session = createSession(storage);\n const identity = await session.snapshot(SessionIdentityDoc, context);\n if (existing && (!identity || identity.id !== id))\n throw new Error('SQLite session id does not match the requested session');\n if (!existing) {\n await session.commit(async (tx) => {\n const doc = await tx.doc(SessionIdentityDoc);\n doc.id = id;\n doc.createdAt = Date.now();\n doc.parentSessionId = options.parentSessionId ?? '';\n }, context);\n fs.chmodSync(sessionFile, 0o600);\n }\n const repository = { close: (closeContext: Context = context) => session.close(closeContext) };\n return { storage, session, sessionFile, repository, historyLease };\n } catch (error) {\n // ponytail: An upstream open rejection has no closure receipt, so retain ownership until shutdown is confirmed.\n if (writerOpenAttempted && !database) throw error;\n try {\n if (storage) await storage.close(context);\n else if (database) await database.close();\n } catch (cleanupError) {\n throw new AggregateError([error, cleanupError], 'SQLite storage initialization cleanup failed');\n }\n try {\n await historyLease.release();\n } catch (cleanupError) {\n throw new AggregateError([error, cleanupError], 'SQLite storage ownership cleanup failed');\n }\n throw error;\n }\n}\n"],"mappings":";;;;;;;;;;;;;AAaA,MAAa,6BAA6BA,8BAAAA;AAE1C,MAAa,oBAAoB;;AAEjC,MAAa,sBAAA,GAAqBC,2BAAAA,UAAAA,CAAU;CAC1C,MAAM;CACN,SAAS;CACT,OAAO;CACP,gBAAgB;EAAE,IAAI;EAAI,WAAW;EAAG,iBAAiB;CAAG;AAC9D,CAAC;AACD,MAAa,sBAAA,GAAqBA,2BAAAA,UAAAA,CAAU;CAC1C,MAAM;CACN,SAAS;CACT,OAAO;CACP,gBAAgB;EAAE,MAAM;EAAI,eAAe;EAAI,WAAW;EAAI,UAAU;CAAO;AACjF,CAAC;;AAGD,SAAgB,2BAA2B,MAAoB;CAC7D,MAAM,WAAW,IAAIC,YAAAA,aAAa,MAAM,EAAE,UAAU,KAAK,CAAC;CAC1D,IAAI;EAEF,IADe,SAAS,QAAQ,wDAAwD,CAAC,CAAC,IACjF,CAAC,EAAE,YAAY,GAAG,MAAM,IAAI,MAAM,mCAAmC;EAC9E,MAAM,SAAS,SAAS,QAAQ,qDAAqD,CAAC,CAAC,IAAI;EAC3F,KAAK,MAAM,QAAQ;GACjB;GACA;GACA;GACA;GACA;GACA;GACA;GACA;EACF,GACE,IAAI,CAAC,OAAO,MAAM,UAAU,MAAM,SAAS,IAAI,GAAG,MAAM,IAAI,MAAM,+BAA+B;CAErG,SAAS,OAAO;EACd,MAAM,IAAI,MAAM,+CAA+C,EAAE,MAAM,CAAC;CAC1E,UAAU;EACR,SAAS,MAAM;CACjB;AACF;;AAGA,eAAsB,2BAA2B,MAAgC;CAC/E,2BAA2B,IAAI;CAC/B,MAAM,SAAS,IAAIA,YAAAA,aAAa,MAAM,EAAE,UAAU,KAAK,CAAC;CACxD,MAAM,WAAW,IAAIC,+CAAAA,mBAAmB,MAAM;CAC9C,IAAI,SAAS;CACb,MAAM,QAAQ,YAAY;EACxB,IAAI,QAAQ;EAEZ,OAAO,MAAM;EACb,SAAS;CACX;CACA,IAAI;EAqBF,OAAO,MApBeC,0CAAAA,cAAc,KAAK;GACvC,OAAO,QAAQ,SAAS,KAAK,GAAG;GAChC,MAAM,KAAK,GAAG,WAAW,SAAS,IAAI,KAAK,GAAG,MAAM;GACpD,MAAM,KAAK,GAAG,WAAW,SAAS,IAAI,KAAK,GAAG,MAAM;GACpD,MAAM,KAAK,GAAG,WAAW,SAAS,IAAI,KAAK,GAAG,MAAM;GACpD;GACA,cAAc,aACZ,SAAS;IACP,MAAM,OAAO,QAAQ;KACnB,IAAI,CAAC,gDAAgD,KAAK,GAAG,GAC3D,MAAM,IAAI,MAAM,2BAA2B;IAC/C;IACA,KAAK,OAAO,QAAQ;KAClB,IAAI,QAAQ,2EACV,MAAM,IAAI,MAAM,2BAA2B;IAC/C;IACA,MAAM,KAAK,GAAG,WAAW,SAAS,IAAI,KAAK,GAAG,MAAM;IACpD,MAAM,KAAK,GAAG,WAAW,SAAS,IAAI,KAAK,GAAG,MAAM;GACtD,CAAC;EACL,CAAC;CAEH,SAAS,OAAO;EACd,IAAI;GACF,MAAM,MAAM;EACd,SAAS,cAAc;GACrB,MAAM,IAAI,eAAe,CAAC,OAAO,YAAY,GAAG,iDAAiD;EACnG;EACA,MAAM;CACR;AACF;;AAGA,eAAsB,yBACpB,SAIA,SACA;CACA,IAAI,CAAC,QAAQ,kBAAkB,MAAM,IAAI,MAAM,mDAAmD;CAClG,IAAI,CAAC,QAAQ,gBAAgB,CAAC,QAAQ,aAAa,MAAM,IAAI,MAAM,iDAAiD;CACpH,MAAM,KAAK,QAAQ,cAAc,QAAQ,cAAcC,UAAAA,QAAK,SAAS,QAAQ,aAAa,SAAS,KAAA,GAAIC,YAAAA,WAAAA,CAAW;CAClH,IAAI,CAAC,oBAAoB,KAAK,EAAE,GAAG,MAAM,IAAI,MAAM,2BAA2B;CAC9E,MAAM,cAAcD,UAAAA,QAAK,QACvB,QAAQ,eAAeA,UAAAA,QAAK,KAAK,QAAQ,cAAA,cAAkC,GAAG,GAAG,QAAQ,CAC3F;CACA,IAAIA,UAAAA,QAAK,SAASA,UAAAA,QAAK,QAAQ,WAAW,CAAC,MAAA,gBAA2BA,UAAAA,QAAK,QAAQ,WAAW,MAAM,WAClG,MAAM,IAAI,MAAM,2CAA2C;CAC7D,MAAM,WAAWE,QAAAA,QAAG,WAAW,WAAW;CAC1C,IAAI,CAAC,YAAY,QAAQ,gBAAgBA,QAAAA,QAAG,WAAWF,UAAAA,QAAK,KAAK,QAAQ,cAAc,GAAG,GAAG,QAAQ,CAAC,GACpG,MAAM,IAAI,MAAM,iEAAiE;CACnF,IAAI,QAAQ,eAAe,CAAC,UAAU,MAAM,IAAI,MAAM,6BAA6B,aAAa;CAChG,IAAI,UAAU,2BAA2B,WAAW;CACpD,QAAA,QAAG,UAAUA,UAAAA,QAAK,QAAQ,WAAW,GAAG;EAAE,WAAW;EAAM,MAAM;CAAM,CAAC;CACxE,MAAM,eAAe,MAAM,QAAQ,iBAAiB,QAAQ,WAAW;CACvE,IAAI;CACJ,IAAI;CACJ,IAAI,sBAAsB;CAC1B,IAAI;EACF,MAAM,aAAa,gBAAgB;EACnC,IAAIE,QAAAA,QAAG,WAAW,WAAW,GAAG,2BAA2B,WAAW;EACtE,sBAAsB;EACtB,WAAW,OAAA,GAAMC,+CAAAA,uBAAAA,CAAuB,WAAW;EACnD,UAAU,MAAMJ,0CAAAA,cAAc,KAAK,QAAQ;EAC3C,MAAM,WAAA,GAAUK,2BAAAA,cAAAA,CAAc,OAAO;EACrC,MAAM,WAAW,MAAM,QAAQ,SAAS,oBAAoB,OAAO;EACnE,IAAI,aAAa,CAAC,YAAY,SAAS,OAAO,KAC5C,MAAM,IAAI,MAAM,wDAAwD;EAC1E,IAAI,CAAC,UAAU;GACb,MAAM,QAAQ,OAAO,OAAO,OAAO;IACjC,MAAM,MAAM,MAAM,GAAG,IAAI,kBAAkB;IAC3C,IAAI,KAAK;IACT,IAAI,YAAY,KAAK,IAAI;IACzB,IAAI,kBAAkB,QAAQ,mBAAmB;GACnD,GAAG,OAAO;GACV,QAAA,QAAG,UAAU,aAAa,GAAK;EACjC;EAEA,OAAO;GAAE;GAAS;GAAS;GAAa,YAAA,EADnB,QAAQ,eAAwB,YAAY,QAAQ,MAAM,YAAY,EAC1C;GAAG;EAAa;CACnE,SAAS,OAAO;EAEd,IAAI,uBAAuB,CAAC,UAAU,MAAM;EAC5C,IAAI;GACF,IAAI,SAAS,MAAM,QAAQ,MAAM,OAAO;QACnC,IAAI,UAAU,MAAM,SAAS,MAAM;EAC1C,SAAS,cAAc;GACrB,MAAM,IAAI,eAAe,CAAC,OAAO,YAAY,GAAG,8CAA8C;EAChG;EACA,IAAI;GACF,MAAM,aAAa,QAAQ;EAC7B,SAAS,cAAc;GACrB,MAAM,IAAI,eAAe,CAAC,OAAO,YAAY,GAAG,yCAAyC;EAC3F;EACA,MAAM;CACR;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"sqliteSessionStorage.d.mts","names":[],"sources":["../src/services/sqliteSessionStorage/index.ts"],"mappings":";;;;;qBAaa,4BAA0B;qBAE1B;;qBAEA,yDAAkB;;;;;qBAMlB,yDAAkB;;;;;;;wBAQf,2BAA2B;;wBA0BrB,2BAA2B,eAAe,QAAQ;;wBA0BlD,yBACpB,SAAS,KACP,qHAGF,SAAS,UAAO;;;;;IAoC6B,QAAA,eAAA,YAAO;;gBAAA"}
1
+ {"version":3,"file":"sqliteSessionStorage.d.mts","names":[],"sources":["../src/services/sqliteSessionStorage/index.ts"],"mappings":";;;;;qBAaa,4BAA0B;qBAE1B;;qBAEA,yDAAkB;;;;;qBAMlB,yDAAkB;;;;;;;wBAQf,2BAA2B;;wBA0BrB,2BAA2B,eAAe,QAAQ;;wBA4ClD,yBACpB,SAAS,KACP,qHAGF,SAAS,UAAO;;;;;IAwC6B,QAAA,eAAA,YAAO;;gBAAA"}
@@ -5,7 +5,7 @@ import { DatabaseSync } from "node:sqlite";
5
5
  import { createSession, defineDoc } from "@earendil-works/pi-durable";
6
6
  import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
7
7
  import { SqliteStorage } from "@earendil-works/pi-durable/storage/sqlite";
8
- import { NodeSqliteDatabase, openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
8
+ import { NodeSqliteDatabase, openNodeSqliteDatabase } from "@earendil-works/pi-durable/storage/sqlite/node";
9
9
  //#region src/services/sqliteSessionStorage/index.ts
10
10
  const DURABLE_BACKGROUND_CONTEXT = BACKGROUND_CONTEXT;
11
11
  const DURABLE_DIRECTORY = "durable-v1";
@@ -56,24 +56,40 @@ function validateDurableSessionFile(file) {
56
56
  /** Completed histories use a read-only connection. Initialization cannot write or migrate. */
57
57
  async function openReadOnlyDurableStorage(file) {
58
58
  validateDurableSessionFile(file);
59
- const database = new NodeSqliteDatabase(new DatabaseSync(file, { readOnly: true }));
60
- return await SqliteStorage.open({
61
- exec: (sql) => database.exec(sql),
62
- run: (sql, ...params) => database.run(sql, ...params),
63
- get: (sql, ...params) => database.get(sql, ...params),
64
- all: (sql, ...params) => database.all(sql, ...params),
65
- close: () => database.close(),
66
- transaction: (callback) => callback({
67
- exec: async (sql) => {
68
- if (!/^CREATE TABLE IF NOT EXISTS durable_schema\s/u.test(sql)) throw new Error("Read-only durable storage");
69
- },
70
- run: async (sql) => {
71
- if (sql !== "INSERT OR IGNORE INTO durable_schema (singleton, version) VALUES (1, 0)") throw new Error("Read-only durable storage");
72
- },
59
+ const native = new DatabaseSync(file, { readOnly: true });
60
+ const database = new NodeSqliteDatabase(native);
61
+ let closed = false;
62
+ const close = async () => {
63
+ if (closed) return;
64
+ native.close();
65
+ closed = true;
66
+ };
67
+ try {
68
+ return await SqliteStorage.open({
69
+ exec: (sql) => database.exec(sql),
70
+ run: (sql, ...params) => database.run(sql, ...params),
73
71
  get: (sql, ...params) => database.get(sql, ...params),
74
- all: (sql, ...params) => database.all(sql, ...params)
75
- })
76
- });
72
+ all: (sql, ...params) => database.all(sql, ...params),
73
+ close,
74
+ transaction: (callback) => callback({
75
+ exec: async (sql) => {
76
+ if (!/^CREATE TABLE IF NOT EXISTS durable_schema\s/u.test(sql)) throw new Error("Read-only durable storage");
77
+ },
78
+ run: async (sql) => {
79
+ if (sql !== "INSERT OR IGNORE INTO durable_schema (singleton, version) VALUES (1, 0)") throw new Error("Read-only durable storage");
80
+ },
81
+ get: (sql, ...params) => database.get(sql, ...params),
82
+ all: (sql, ...params) => database.all(sql, ...params)
83
+ })
84
+ });
85
+ } catch (error) {
86
+ try {
87
+ await close();
88
+ } catch (cleanupError) {
89
+ throw new AggregateError([error, cleanupError], "Read-only storage initialization cleanup failed");
90
+ }
91
+ throw error;
92
+ }
77
93
  }
78
94
  /** Opens the one fresh-format SQLite container owned by a server session. */
79
95
  async function openSqliteSessionStorage(options, context) {
@@ -93,10 +109,14 @@ async function openSqliteSessionStorage(options, context) {
93
109
  });
94
110
  const historyLease = await options.historyOwnership.acquire(sessionFile);
95
111
  let storage;
112
+ let database;
113
+ let writerOpenAttempted = false;
96
114
  try {
97
115
  await historyLease.assertQuiescent();
98
116
  if (fs.existsSync(sessionFile)) validateDurableSessionFile(sessionFile);
99
- storage = await openNodeSqliteStorage(sessionFile);
117
+ writerOpenAttempted = true;
118
+ database = await openNodeSqliteDatabase(sessionFile);
119
+ storage = await SqliteStorage.open(database);
100
120
  const session = createSession(storage);
101
121
  const identity = await session.snapshot(SessionIdentityDoc, context);
102
122
  if (existing && (!identity || identity.id !== id)) throw new Error("SQLite session id does not match the requested session");
@@ -117,10 +137,17 @@ async function openSqliteSessionStorage(options, context) {
117
137
  historyLease
118
138
  };
119
139
  } catch (error) {
140
+ if (writerOpenAttempted && !database) throw error;
120
141
  try {
121
142
  if (storage) await storage.close(context);
122
- } finally {
143
+ else if (database) await database.close();
144
+ } catch (cleanupError) {
145
+ throw new AggregateError([error, cleanupError], "SQLite storage initialization cleanup failed");
146
+ }
147
+ try {
123
148
  await historyLease.release();
149
+ } catch (cleanupError) {
150
+ throw new AggregateError([error, cleanupError], "SQLite storage ownership cleanup failed");
124
151
  }
125
152
  throw error;
126
153
  }
@@ -1 +1 @@
1
- {"version":3,"file":"sqliteSessionStorage.mjs","names":[],"sources":["../src/services/sqliteSessionStorage/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\n\nimport type { Context } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { createSession, defineDoc, type Storage } from '@earendil-works/pi-durable';\nimport { SqliteStorage } from '@earendil-works/pi-durable/storage/sqlite';\nimport { NodeSqliteDatabase, openNodeSqliteStorage } from '@earendil-works/pi-durable/storage/sqlite/node';\n\nimport type { DirectHarnessRuntimeOptions } from '../../types/server/directHarnessRuntime';\n\nexport const DURABLE_BACKGROUND_CONTEXT = BACKGROUND_CONTEXT;\n\nexport const DURABLE_DIRECTORY = 'durable-v1';\n/** Public string identity belongs to Doom, not the durable numeric ID namespace. */\nexport const SessionIdentityDoc = defineDoc({\n kind: 'doompi.session.identity',\n version: 1,\n scope: 'session',\n initial: () => ({ id: '', createdAt: 0, parentSessionId: '' }),\n});\nexport const SessionMetadataDoc = defineDoc({\n kind: 'doompi.session.metadata',\n version: 1,\n scope: 'session',\n initial: () => ({ name: '', workspaceRoot: '', execution: '', laneName: 'main' }),\n});\n\n/** Validate existing files without admitting a writer or applying schema upgrades. */\nexport function validateDurableSessionFile(file: string): void {\n const database = new DatabaseSync(file, { readOnly: true });\n try {\n const schema = database.prepare('SELECT version FROM durable_schema WHERE singleton = 1').get();\n if (schema?.version !== 1) throw new Error('Unsupported durable SQLite schema');\n const tables = database.prepare(\"SELECT name FROM sqlite_master WHERE type = 'table'\").all();\n for (const name of [\n 'durable_metadata',\n 'record_ids',\n 'conversations',\n 'entries',\n 'tasks',\n 'submissions',\n 'documents',\n 'document_revisions',\n ]) {\n if (!tables.some((table) => table.name === name)) throw new Error('Invalid durable SQLite schema');\n }\n } catch (cause) {\n throw new Error('Unsupported or legacy SQLite session format', { cause });\n } finally {\n database.close();\n }\n}\n\n/** Completed histories use a read-only connection. Initialization cannot write or migrate. */\nexport async function openReadOnlyDurableStorage(file: string): Promise<Storage> {\n validateDurableSessionFile(file);\n const database = new NodeSqliteDatabase(new DatabaseSync(file, { readOnly: true }));\n const storage = await SqliteStorage.open({\n exec: (sql) => database.exec(sql),\n run: (sql, ...params) => database.run(sql, ...params),\n get: (sql, ...params) => database.get(sql, ...params),\n all: (sql, ...params) => database.all(sql, ...params),\n close: () => database.close(),\n transaction: (callback) =>\n callback({\n exec: async (sql) => {\n if (!/^CREATE TABLE IF NOT EXISTS durable_schema\\s/u.test(sql)) throw new Error('Read-only durable storage');\n },\n run: async (sql) => {\n if (sql !== 'INSERT OR IGNORE INTO durable_schema (singleton, version) VALUES (1, 0)')\n throw new Error('Read-only durable storage');\n },\n get: (sql, ...params) => database.get(sql, ...params),\n all: (sql, ...params) => database.all(sql, ...params),\n }),\n });\n return storage;\n}\n\n/** Opens the one fresh-format SQLite container owned by a server session. */\nexport async function openSqliteSessionStorage(\n options: Pick<\n DirectHarnessRuntimeOptions,\n 'historyOwnership' | 'sessionsRoot' | 'sessionPath' | 'sessionId' | 'parentSessionId'\n >,\n context: Context,\n) {\n if (!options.historyOwnership) throw new Error('SQLite sessions require explicit HistoryOwnership');\n if (!options.sessionsRoot && !options.sessionPath) throw new Error('SQLite sessions require a server data directory');\n const id = options.sessionId ?? (options.sessionPath ? path.basename(options.sessionPath, '.sqlite') : randomUUID());\n if (!/^[a-zA-Z0-9_-]+$/u.test(id)) throw new Error('Invalid SQLite session id');\n const sessionFile = path.resolve(\n options.sessionPath ?? path.join(options.sessionsRoot!, DURABLE_DIRECTORY, `${id}.sqlite`),\n );\n if (path.basename(path.dirname(sessionFile)) !== DURABLE_DIRECTORY || path.extname(sessionFile) !== '.sqlite')\n throw new Error('Unsupported or legacy SQLite session path');\n const existing = fs.existsSync(sessionFile);\n if (!existing && options.sessionsRoot && fs.existsSync(path.join(options.sessionsRoot, `${id}.sqlite`)))\n throw new Error('Legacy SQLite sessions are unsupported by fresh durable storage');\n if (options.sessionPath && !existing) throw new Error(`SQLite session not found: ${sessionFile}`);\n if (existing) validateDurableSessionFile(sessionFile);\n fs.mkdirSync(path.dirname(sessionFile), { recursive: true, mode: 0o700 });\n const historyLease = await options.historyOwnership.acquire(sessionFile);\n let storage: Storage | undefined;\n try {\n await historyLease.assertQuiescent();\n if (fs.existsSync(sessionFile)) validateDurableSessionFile(sessionFile);\n storage = await openNodeSqliteStorage(sessionFile);\n const session = createSession(storage);\n const identity = await session.snapshot(SessionIdentityDoc, context);\n if (existing && (!identity || identity.id !== id))\n throw new Error('SQLite session id does not match the requested session');\n if (!existing) {\n await session.commit(async (tx) => {\n const doc = await tx.doc(SessionIdentityDoc);\n doc.id = id;\n doc.createdAt = Date.now();\n doc.parentSessionId = options.parentSessionId ?? '';\n }, context);\n fs.chmodSync(sessionFile, 0o600);\n }\n const repository = { close: (closeContext: Context = context) => session.close(closeContext) };\n return { storage, session, sessionFile, repository, historyLease };\n } catch (error) {\n try {\n if (storage) await storage.close(context);\n } finally {\n await historyLease.release();\n }\n throw error;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"sqliteSessionStorage.mjs","names":[],"sources":["../src/services/sqliteSessionStorage/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { DatabaseSync } from 'node:sqlite';\n\nimport type { Context } from '@earendil-works/chord';\nimport { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';\nimport { createSession, defineDoc, type Storage } from '@earendil-works/pi-durable';\nimport { SqliteStorage } from '@earendil-works/pi-durable/storage/sqlite';\nimport { NodeSqliteDatabase, openNodeSqliteDatabase } from '@earendil-works/pi-durable/storage/sqlite/node';\n\nimport type { DirectHarnessRuntimeOptions } from '../../types/server/directHarnessRuntime';\n\nexport const DURABLE_BACKGROUND_CONTEXT = BACKGROUND_CONTEXT;\n\nexport const DURABLE_DIRECTORY = 'durable-v1';\n/** Public string identity belongs to Doom, not the durable numeric ID namespace. */\nexport const SessionIdentityDoc = defineDoc({\n kind: 'doompi.session.identity',\n version: 1,\n scope: 'session',\n initial: () => ({ id: '', createdAt: 0, parentSessionId: '' }),\n});\nexport const SessionMetadataDoc = defineDoc({\n kind: 'doompi.session.metadata',\n version: 1,\n scope: 'session',\n initial: () => ({ name: '', workspaceRoot: '', execution: '', laneName: 'main' }),\n});\n\n/** Validate existing files without admitting a writer or applying schema upgrades. */\nexport function validateDurableSessionFile(file: string): void {\n const database = new DatabaseSync(file, { readOnly: true });\n try {\n const schema = database.prepare('SELECT version FROM durable_schema WHERE singleton = 1').get();\n if (schema?.version !== 1) throw new Error('Unsupported durable SQLite schema');\n const tables = database.prepare(\"SELECT name FROM sqlite_master WHERE type = 'table'\").all();\n for (const name of [\n 'durable_metadata',\n 'record_ids',\n 'conversations',\n 'entries',\n 'tasks',\n 'submissions',\n 'documents',\n 'document_revisions',\n ]) {\n if (!tables.some((table) => table.name === name)) throw new Error('Invalid durable SQLite schema');\n }\n } catch (cause) {\n throw new Error('Unsupported or legacy SQLite session format', { cause });\n } finally {\n database.close();\n }\n}\n\n/** Completed histories use a read-only connection. Initialization cannot write or migrate. */\nexport async function openReadOnlyDurableStorage(file: string): Promise<Storage> {\n validateDurableSessionFile(file);\n const native = new DatabaseSync(file, { readOnly: true });\n const database = new NodeSqliteDatabase(native);\n let closed = false;\n const close = async () => {\n if (closed) return;\n // The writer adapter checkpoints WAL on close, which read-only connections cannot do.\n native.close();\n closed = true;\n };\n try {\n const storage = await SqliteStorage.open({\n exec: (sql) => database.exec(sql),\n run: (sql, ...params) => database.run(sql, ...params),\n get: (sql, ...params) => database.get(sql, ...params),\n all: (sql, ...params) => database.all(sql, ...params),\n close,\n transaction: (callback) =>\n callback({\n exec: async (sql) => {\n if (!/^CREATE TABLE IF NOT EXISTS durable_schema\\s/u.test(sql))\n throw new Error('Read-only durable storage');\n },\n run: async (sql) => {\n if (sql !== 'INSERT OR IGNORE INTO durable_schema (singleton, version) VALUES (1, 0)')\n throw new Error('Read-only durable storage');\n },\n get: (sql, ...params) => database.get(sql, ...params),\n all: (sql, ...params) => database.all(sql, ...params),\n }),\n });\n return storage;\n } catch (error) {\n try {\n await close();\n } catch (cleanupError) {\n throw new AggregateError([error, cleanupError], 'Read-only storage initialization cleanup failed');\n }\n throw error;\n }\n}\n\n/** Opens the one fresh-format SQLite container owned by a server session. */\nexport async function openSqliteSessionStorage(\n options: Pick<\n DirectHarnessRuntimeOptions,\n 'historyOwnership' | 'sessionsRoot' | 'sessionPath' | 'sessionId' | 'parentSessionId'\n >,\n context: Context,\n) {\n if (!options.historyOwnership) throw new Error('SQLite sessions require explicit HistoryOwnership');\n if (!options.sessionsRoot && !options.sessionPath) throw new Error('SQLite sessions require a server data directory');\n const id = options.sessionId ?? (options.sessionPath ? path.basename(options.sessionPath, '.sqlite') : randomUUID());\n if (!/^[a-zA-Z0-9_-]+$/u.test(id)) throw new Error('Invalid SQLite session id');\n const sessionFile = path.resolve(\n options.sessionPath ?? path.join(options.sessionsRoot!, DURABLE_DIRECTORY, `${id}.sqlite`),\n );\n if (path.basename(path.dirname(sessionFile)) !== DURABLE_DIRECTORY || path.extname(sessionFile) !== '.sqlite')\n throw new Error('Unsupported or legacy SQLite session path');\n const existing = fs.existsSync(sessionFile);\n if (!existing && options.sessionsRoot && fs.existsSync(path.join(options.sessionsRoot, `${id}.sqlite`)))\n throw new Error('Legacy SQLite sessions are unsupported by fresh durable storage');\n if (options.sessionPath && !existing) throw new Error(`SQLite session not found: ${sessionFile}`);\n if (existing) validateDurableSessionFile(sessionFile);\n fs.mkdirSync(path.dirname(sessionFile), { recursive: true, mode: 0o700 });\n const historyLease = await options.historyOwnership.acquire(sessionFile);\n let storage: Storage | undefined;\n let database: NodeSqliteDatabase | undefined;\n let writerOpenAttempted = false;\n try {\n await historyLease.assertQuiescent();\n if (fs.existsSync(sessionFile)) validateDurableSessionFile(sessionFile);\n writerOpenAttempted = true;\n database = await openNodeSqliteDatabase(sessionFile);\n storage = await SqliteStorage.open(database);\n const session = createSession(storage);\n const identity = await session.snapshot(SessionIdentityDoc, context);\n if (existing && (!identity || identity.id !== id))\n throw new Error('SQLite session id does not match the requested session');\n if (!existing) {\n await session.commit(async (tx) => {\n const doc = await tx.doc(SessionIdentityDoc);\n doc.id = id;\n doc.createdAt = Date.now();\n doc.parentSessionId = options.parentSessionId ?? '';\n }, context);\n fs.chmodSync(sessionFile, 0o600);\n }\n const repository = { close: (closeContext: Context = context) => session.close(closeContext) };\n return { storage, session, sessionFile, repository, historyLease };\n } catch (error) {\n // ponytail: An upstream open rejection has no closure receipt, so retain ownership until shutdown is confirmed.\n if (writerOpenAttempted && !database) throw error;\n try {\n if (storage) await storage.close(context);\n else if (database) await database.close();\n } catch (cleanupError) {\n throw new AggregateError([error, cleanupError], 'SQLite storage initialization cleanup failed');\n }\n try {\n await historyLease.release();\n } catch (cleanupError) {\n throw new AggregateError([error, cleanupError], 'SQLite storage ownership cleanup failed');\n }\n throw error;\n }\n}\n"],"mappings":"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"}
@@ -562,9 +562,34 @@ async function createDirectHarnessRuntime(options) {
562
562
  const attach = async () => {
563
563
  if (watcher) await watcher.stop();
564
564
  watcher = await (0, _earendil_works_pi_durable.watchEvents)(harness, conversation.id, context);
565
+ let predecessor = watcher.snapshot.entries.at(-1)?.id;
566
+ lastAssistant = void 0;
567
+ partial = void 0;
568
+ toolResults = [];
565
569
  watcher.start(async (events, eventContext) => {
566
570
  for (const event of events) {
567
571
  let mapped;
572
+ if (event.type === "turn_start" || event.type === "run_start") {
573
+ lastAssistant = void 0;
574
+ partial = void 0;
575
+ toolResults = [];
576
+ }
577
+ if (event.type === "message_end" || event.type === "entry_appended") {
578
+ const entry = require_index$1.projectDurableEntries([event.entry])[0];
579
+ if (entry) {
580
+ entry.parentId = predecessor === void 0 ? null : String(predecessor);
581
+ const added = {
582
+ type: "entry_added",
583
+ entry
584
+ };
585
+ emit({
586
+ type: "entry_appended",
587
+ entry
588
+ });
589
+ eventsSettled = eventsSettled.then(() => deliver(added, eventContext));
590
+ }
591
+ predecessor = event.entry.id;
592
+ }
568
593
  if ([
569
594
  "run_start",
570
595
  "run_end",
@@ -589,10 +614,6 @@ async function createDirectHarnessRuntime(options) {
589
614
  runId: String(conversation.id)
590
615
  };
591
616
  }
592
- if (event.type === "entry_appended") mapped = {
593
- type: "entry_added",
594
- entry: require_index$1.projectDurableEntries([event.entry])[0]
595
- };
596
617
  if (event.type === "tool_execution_start") mapped = {
597
618
  ...event,
598
619
  type: "tool_start"
@@ -617,6 +638,8 @@ async function createDirectHarnessRuntime(options) {
617
638
  toolResults
618
639
  };
619
640
  toolResults = [];
641
+ lastAssistant = void 0;
642
+ partial = void 0;
620
643
  }
621
644
  if (event.type === "message_update" && partial) for (const change of event.changes) {
622
645
  if (change.type === "message") {
@@ -671,7 +694,7 @@ async function createDirectHarnessRuntime(options) {
671
694
  type: "queue_update",
672
695
  queues: event.items.map((item) => ({ kind: item.mode }))
673
696
  };
674
- if (event.type === "entry_appended") {
697
+ if (event.type === "message_end") {
675
698
  for (const message of event.entry.model ?? []) if (message.role === "assistant") eventsSettled = eventsSettled.then(() => deliver({
676
699
  type: "usage",
677
700
  row: {
@@ -1099,20 +1122,26 @@ async function createDirectHarnessRuntime(options) {
1099
1122
  };
1100
1123
  let disposePromise;
1101
1124
  const dispose = () => disposePromise ??= (async () => {
1102
- await interrupt();
1125
+ const failures = [];
1126
+ const attempt = async (cleanup) => {
1127
+ try {
1128
+ await cleanup();
1129
+ return true;
1130
+ } catch (error) {
1131
+ failures.push(error);
1132
+ return false;
1133
+ }
1134
+ };
1135
+ await attempt(interrupt);
1103
1136
  disposed = true;
1104
- await watcher.stop();
1105
- await eventsSettled;
1106
- try {
1107
- await harness.close(context);
1108
- await storage.repository?.close(context);
1109
- await environment.cleanup(context);
1110
- await storage.historyLease?.release();
1111
- resolveExited(0);
1112
- } catch (error) {
1113
- resolveExited(1);
1114
- throw error;
1115
- }
1137
+ await attempt(() => watcher.stop());
1138
+ await attempt(() => eventsSettled);
1139
+ const harnessClosed = await attempt(() => harness.close(context));
1140
+ const repositoryClosed = harnessClosed && await attempt(async () => storage.repository?.close(context));
1141
+ await attempt(() => environment.cleanup(context));
1142
+ if (harnessClosed && repositoryClosed) await attempt(async () => storage.historyLease?.release());
1143
+ resolveExited(failures.length ? 1 : 0);
1144
+ if (failures.length) throw new AggregateError(failures, "Direct harness cleanup failed");
1116
1145
  })();
1117
1146
  const recordUsage = async (usage, usageOptions) => {
1118
1147
  const entryId = await appendEntry({
@@ -1600,12 +1629,8 @@ async function createDirectHarnessRuntime(options) {
1600
1629
  } catch (closeError) {
1601
1630
  failures.push(closeError);
1602
1631
  }
1603
- try {
1632
+ if (failures.length === 1) try {
1604
1633
  await storage.repository?.close(context);
1605
- } catch (closeError) {
1606
- failures.push(closeError);
1607
- }
1608
- try {
1609
1634
  await storage.historyLease?.release();
1610
1635
  } catch (closeError) {
1611
1636
  failures.push(closeError);