@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.
- package/dist/childSession.d.mts +2 -2
- package/dist/sqliteSessionStorage.cjs +46 -19
- package/dist/sqliteSessionStorage.cjs.map +1 -1
- package/dist/sqliteSessionStorage.d.mts.map +1 -1
- package/dist/sqliteSessionStorage.mjs +47 -20
- package/dist/sqliteSessionStorage.mjs.map +1 -1
- package/dist/src/server/directHarnessRuntime.cjs +48 -23
- package/dist/src/server/directHarnessRuntime.cjs.map +1 -1
- package/dist/src/server/directHarnessRuntime.mjs +48 -23
- package/dist/src/server/directHarnessRuntime.mjs.map +1 -1
- package/dist/src/services/piExtensionHost/index.cjs +13 -8
- package/dist/src/services/piExtensionHost/index.cjs.map +1 -1
- package/dist/src/services/piExtensionHost/index.mjs +13 -8
- package/dist/src/services/piExtensionHost/index.mjs.map +1 -1
- package/dist/src/systems/child/adapters/headlessChildSessionService.cjs +127 -41
- package/dist/src/systems/child/adapters/headlessChildSessionService.cjs.map +1 -1
- package/dist/src/systems/child/adapters/headlessChildSessionService.d.mts +3 -2
- package/dist/src/systems/child/adapters/headlessChildSessionService.d.mts.map +1 -1
- package/dist/src/systems/child/adapters/headlessChildSessionService.mjs +127 -42
- package/dist/src/systems/child/adapters/headlessChildSessionService.mjs.map +1 -1
- package/dist/src/systems/child/types/childSession.cjs.map +1 -1
- package/dist/src/systems/child/types/childSession.d.mts +11 -1
- package/dist/src/systems/child/types/childSession.d.mts.map +1 -1
- package/dist/src/systems/child/types/childSession.mjs.map +1 -1
- package/dist/src/systems/main/adapters/headlessHost.cjs +14 -5
- package/dist/src/systems/main/adapters/headlessHost.cjs.map +1 -1
- package/dist/src/systems/main/adapters/headlessHost.d.mts.map +1 -1
- package/dist/src/systems/main/adapters/headlessHost.mjs +14 -5
- package/dist/src/systems/main/adapters/headlessHost.mjs.map +1 -1
- package/dist/src/systems/main/adapters/headlessSessionHost.cjs +5 -0
- package/dist/src/systems/main/adapters/headlessSessionHost.cjs.map +1 -1
- package/dist/src/systems/main/adapters/headlessSessionHost.mjs +5 -0
- package/dist/src/systems/main/adapters/headlessSessionHost.mjs.map +1 -1
- package/dist/terminalPiChildSessionService.cjs +18 -5
- package/dist/terminalPiChildSessionService.cjs.map +1 -1
- package/dist/terminalPiChildSessionService.d.mts +3 -2
- package/dist/terminalPiChildSessionService.d.mts.map +1 -1
- package/dist/terminalPiChildSessionService.mjs +19 -6
- package/dist/terminalPiChildSessionService.mjs.map +1 -1
- package/package.json +1 -1
package/dist/childSession.d.mts
CHANGED
|
@@ -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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
run:
|
|
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
|
-
|
|
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
|
-
|
|
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":"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"}
|
|
@@ -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;;
|
|
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,
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
run:
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
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
|
-
|
|
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
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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);
|