@agimon-ai/doompi-core 0.0.1-alpha.97 → 0.0.1-alpha.98

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.
@@ -34,7 +34,7 @@ const MANAGED_WORKTREE_ID_LENGTH = 12;
34
34
  function validRecord(value) {
35
35
  if (typeof value !== "object" || value === null) return false;
36
36
  const record = value;
37
- return typeof record.id === "string" && ID_PATTERN.test(record.id) && typeof record.clientId === "string" && ID_PATTERN.test(record.clientId) && typeof record.parentSessionId === "string" && ID_PATTERN.test(record.parentSessionId) && typeof record.parentWorkspaceId === "string" && record.parentWorkspaceId.length > 0 && typeof record.conversationDigest === "string" && DIGEST_PATTERN.test(record.conversationDigest) && typeof record.createdAt === "string" && Number.isFinite(Date.parse(record.createdAt)) && (record.state === "pending" || record.state === "bound" || record.state === "closed") && (record.setupKind === void 0 || record.setupKind === "managed-worktree" || record.setupKind === "existing-directory") && (record.failureCode === void 0 || record.failureCode === "SESSION_WORKTREE_PROVISION_FAILED" || record.failureCode === "SESSION_UNAVAILABLE") && (record.failureCode === void 0 || record.state === "pending") && (record.cwd === void 0 || typeof record.cwd === "string" && node_path.default.isAbsolute(record.cwd) && !record.cwd.includes("\0")) && (record.workspaceId === void 0 || typeof record.workspaceId === "string" && record.workspaceId.length > 0) && (record.state !== "bound" || record.cwd !== void 0 && record.workspaceId !== void 0);
37
+ return typeof record.id === "string" && ID_PATTERN.test(record.id) && typeof record.clientId === "string" && ID_PATTERN.test(record.clientId) && typeof record.parentSessionId === "string" && ID_PATTERN.test(record.parentSessionId) && typeof record.parentWorkspaceId === "string" && record.parentWorkspaceId.length > 0 && typeof record.conversationDigest === "string" && DIGEST_PATTERN.test(record.conversationDigest) && typeof record.createdAt === "string" && Number.isFinite(Date.parse(record.createdAt)) && (record.state === "pending" || record.state === "bound" || record.state === "closed") && (record.setupKind === void 0 || record.setupKind === "managed-worktree" || record.setupKind === "existing-directory") && (record.failureCode === void 0 || record.failureCode === "SESSION_WORKTREE_PROVISION_FAILED" || record.failureCode === "SESSION_UNAVAILABLE") && (record.cwd === void 0 || typeof record.cwd === "string" && node_path.default.isAbsolute(record.cwd) && !record.cwd.includes("\0")) && (record.workspaceId === void 0 || typeof record.workspaceId === "string" && record.workspaceId.length > 0) && (record.state !== "bound" || record.cwd !== void 0 && record.workspaceId !== void 0);
38
38
  }
39
39
  /**
40
40
  * Records written before setups carried a provider have none, and recovery refuses a setup it
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["createHash","path","fs","randomUUID"],"sources":["../../../../src/services/sessionMcpConversations/index.ts"],"sourcesContent":["import { createHash, randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nconst STORE_FILE = 'session-mcp-conversations.json';\nconst STORE_VERSION = 1;\nconst MAX_BINDINGS = 1000;\nconst MAX_CONVERSATION_BYTES = 1024;\nconst CONVERSATION_META_KEY = 'openai/session';\nconst ID_PATTERN = /^[A-Za-z0-9_-]+$/u;\nconst DIGEST_PATTERN = /^[a-f0-9]{64}$/u;\n\nexport type SessionMcpSetupKind = 'managed-worktree' | 'existing-directory';\nexport type SessionMcpSetupFailureCode = 'SESSION_WORKTREE_PROVISION_FAILED' | 'SESSION_UNAVAILABLE';\n\n/** Client metadata is a routing hint within an authenticated registration, never authority. */\nexport function sessionMcpConversationDigest(meta: unknown): string | undefined {\n if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) return undefined;\n const value = (meta as Record<string, unknown>)[CONVERSATION_META_KEY];\n if (\n typeof value !== 'string' ||\n value.trim() !== value ||\n value.length === 0 ||\n Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) ||\n Buffer.byteLength(value, 'utf8') > MAX_CONVERSATION_BYTES\n )\n return undefined;\n return createHash('sha256')\n .update(JSON.stringify(['chatgpt', value]))\n .digest('hex');\n}\n\nexport interface SessionMcpConversation {\n /** Also the server-reserved target session identity. Never supplied by the model. */\n readonly id: string;\n readonly clientId: string;\n readonly parentSessionId: string;\n readonly parentWorkspaceId: string;\n readonly conversationDigest: string;\n readonly createdAt: string;\n readonly state: 'pending' | 'bound' | 'closed';\n /** Host-selected execution provider. Old records without this field are not assumed to be worktrees. */\n readonly setupKind?: SessionMcpSetupKind;\n /** Public, bounded failure code. Never persist Git stderr or exception text here. */\n readonly failureCode?: SessionMcpSetupFailureCode;\n /** Persisted before checkout or session creation starts. */\n readonly cwd?: string;\n readonly workspaceId?: string;\n}\n\nexport class SessionMcpConversationError extends Error {\n constructor(\n readonly code: string,\n message: string,\n readonly bindingId?: string,\n ) {\n super(message);\n this.name = 'SessionMcpConversationError';\n }\n}\n\nexport interface SessionMcpConversationStore {\n list(): readonly SessionMcpConversation[];\n find(clientId: string, parentSessionId: string, digest: string): SessionMcpConversation | undefined;\n reserve(clientId: string, parentSessionId: string, parentWorkspaceId: string, digest: string): SessionMcpConversation;\n get(id: string, parentSessionId: string): SessionMcpConversation;\n selectSetup(id: string, parentSessionId: string, kind: SessionMcpSetupKind): SessionMcpConversation;\n prepare(id: string, parentSessionId: string, cwd: string): SessionMcpConversation;\n fail(id: string, parentSessionId: string, code: SessionMcpSetupFailureCode): SessionMcpConversation;\n bind(id: string, parentSessionId: string, workspaceId: string): SessionMcpConversation;\n recover(id: string, parentSessionId: string): SessionMcpConversation;\n closeSession(sessionId: string): void;\n closeClient(clientId: string): void;\n}\n\n/** How many id characters name a conversation's managed worktree branch and directory. */\nconst MANAGED_WORKTREE_ID_LENGTH = 12;\n\nfunction validRecord(value: unknown): value is SessionMcpConversation {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Partial<SessionMcpConversation>;\n return (\n typeof record.id === 'string' &&\n ID_PATTERN.test(record.id) &&\n typeof record.clientId === 'string' &&\n ID_PATTERN.test(record.clientId) &&\n typeof record.parentSessionId === 'string' &&\n ID_PATTERN.test(record.parentSessionId) &&\n typeof record.parentWorkspaceId === 'string' &&\n record.parentWorkspaceId.length > 0 &&\n typeof record.conversationDigest === 'string' &&\n DIGEST_PATTERN.test(record.conversationDigest) &&\n typeof record.createdAt === 'string' &&\n Number.isFinite(Date.parse(record.createdAt)) &&\n (record.state === 'pending' || record.state === 'bound' || record.state === 'closed') &&\n (record.setupKind === undefined ||\n record.setupKind === 'managed-worktree' ||\n record.setupKind === 'existing-directory') &&\n (record.failureCode === undefined ||\n record.failureCode === 'SESSION_WORKTREE_PROVISION_FAILED' ||\n record.failureCode === 'SESSION_UNAVAILABLE') &&\n (record.failureCode === undefined || record.state === 'pending') &&\n (record.cwd === undefined ||\n (typeof record.cwd === 'string' && path.isAbsolute(record.cwd) && !record.cwd.includes('\\0'))) &&\n (record.workspaceId === undefined || (typeof record.workspaceId === 'string' && record.workspaceId.length > 0)) &&\n (record.state !== 'bound' || (record.cwd !== undefined && record.workspaceId !== undefined))\n );\n}\n\n/**\n * Records written before setups carried a provider have none, and recovery refuses a setup it\n * cannot attribute, which left every such conversation stuck. A managed conversation worktree is\n * the only setup named `doompi-conversation-<first 12 id characters>--`, so a record reserving\n * that directory can be attributed to it. Anything else stays unattributed and refused.\n */\nfunction withLegacySetupKind(record: SessionMcpConversation): SessionMcpConversation {\n if (record.setupKind !== undefined || record.cwd === undefined) return record;\n const managedName = `doompi-conversation-${record.id.slice(0, MANAGED_WORKTREE_ID_LENGTH)}--`;\n return path.basename(record.cwd).startsWith(managedName) ? { ...record, setupKind: 'managed-worktree' } : record;\n}\n\nfunction routingKey(\n record: Pick<SessionMcpConversation, 'clientId' | 'parentSessionId' | 'conversationDigest'>,\n): string {\n return JSON.stringify([record.clientId, record.parentSessionId, record.conversationDigest]);\n}\n\n/** One owning headless host persists these bindings. Corruption never becomes an empty, reusable store. */\nexport function createSessionMcpConversationStore(stateDir: string): SessionMcpConversationStore {\n const file = path.join(stateDir, STORE_FILE);\n let failure: Error | undefined;\n let current: readonly SessionMcpConversation[] = load();\n\n function load(): readonly SessionMcpConversation[] {\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(file, 'utf8'));\n if (typeof parsed !== 'object' || parsed === null) throw new Error('Invalid store');\n const stored = parsed as { version?: unknown; bindings?: unknown };\n if (stored.version !== STORE_VERSION || !Array.isArray(stored.bindings) || !stored.bindings.every(validRecord))\n throw new Error('Invalid bindings');\n const records: SessionMcpConversation[] = stored.bindings;\n if (\n records.length > MAX_BINDINGS ||\n new Set(records.map((record) => record.id)).size !== records.length ||\n new Set(records.map(routingKey)).size !== records.length\n )\n throw new Error('Duplicate or excessive bindings');\n return Object.freeze(records.map((record) => Object.freeze(withLegacySetupKind(record))));\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];\n failure = new SessionMcpConversationError(\n 'CONVERSATION_STORE_UNAVAILABLE',\n 'Conversation bindings could not be loaded. Restore the binding store before using conversation routing.',\n );\n return [];\n }\n }\n\n function list(): readonly SessionMcpConversation[] {\n if (failure) throw failure;\n return current;\n }\n\n function persist(next: readonly SessionMcpConversation[]): void {\n list();\n const temporary = `${file}.${randomUUID()}.tmp`;\n try {\n fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });\n const descriptor = fs.openSync(temporary, 'wx', 0o600);\n try {\n fs.writeFileSync(descriptor, `${JSON.stringify({ version: STORE_VERSION, bindings: next }, null, 2)}\\n`);\n fs.fsyncSync(descriptor);\n } finally {\n fs.closeSync(descriptor);\n }\n fs.renameSync(temporary, file);\n current = Object.freeze(next.map((record) => Object.freeze(record)));\n } catch {\n throw new SessionMcpConversationError(\n 'CONVERSATION_STORE_UNAVAILABLE',\n 'Conversation bindings could not be saved. No new execution target was admitted.',\n );\n } finally {\n // This temporary file never grants access and can safely be left for local recovery.\n try {\n fs.rmSync(temporary, { force: true });\n } catch {\n /* An inaccessible temporary file is not loaded. */\n }\n }\n }\n\n function get(id: string, parentSessionId: string): SessionMcpConversation {\n const record = list().find((entry) => entry.id === id && entry.parentSessionId === parentSessionId);\n if (!record || record.state === 'closed')\n throw new SessionMcpConversationError(\n 'SESSION_UNAVAILABLE',\n 'This conversation binding is unavailable. It will not be recreated automatically.',\n );\n return record;\n }\n\n function replace(record: SessionMcpConversation): SessionMcpConversation {\n persist(list().map((entry) => (entry.id === record.id ? record : entry)));\n return get(record.id, record.parentSessionId);\n }\n\n return {\n list,\n get,\n find: (clientId, parentSessionId, digest) =>\n list().find(\n (record) =>\n record.clientId === clientId &&\n record.parentSessionId === parentSessionId &&\n record.conversationDigest === digest,\n ),\n reserve(clientId, parentSessionId, parentWorkspaceId, digest) {\n const existing = list().find(\n (record) =>\n record.clientId === clientId &&\n record.parentSessionId === parentSessionId &&\n record.conversationDigest === digest,\n );\n if (existing) return existing;\n if (list().length >= MAX_BINDINGS)\n throw new SessionMcpConversationError(\n 'CONVERSATION_LIMIT_REACHED',\n 'The conversation binding limit has been reached. Use a dedicated session connection.',\n );\n const record: SessionMcpConversation = {\n id: randomUUID(),\n clientId,\n parentSessionId,\n parentWorkspaceId,\n conversationDigest: digest,\n createdAt: new Date().toISOString(),\n state: 'pending',\n };\n if (!validRecord(record))\n throw new SessionMcpConversationError('INVALID_CONVERSATION', 'Invalid conversation identity.');\n persist([...list(), record]);\n return get(record.id, parentSessionId);\n },\n selectSetup(id, parentSessionId, kind) {\n const record = get(id, parentSessionId);\n if (record.setupKind === kind)\n return record.failureCode === undefined ? record : replace({ ...record, failureCode: undefined });\n if (record.setupKind !== undefined && (record.cwd !== undefined || record.state === 'bound'))\n throw new SessionMcpConversationError(\n 'SESSION_TARGET_FIXED',\n 'An initialized setup cannot change its provider.',\n id,\n );\n if (record.setupKind === undefined && record.cwd !== undefined)\n throw new SessionMcpConversationError(\n 'SESSION_TARGET_FIXED',\n 'This existing setup has no verified provider. Recover it without changing its ownership.',\n id,\n );\n return replace({ ...record, setupKind: kind, failureCode: undefined });\n },\n prepare(id, parentSessionId, cwd) {\n const record = get(id, parentSessionId);\n if (!path.isAbsolute(cwd) || cwd.includes('\\0'))\n throw new Error('A canonical absolute execution directory is required.');\n if (record.cwd !== undefined && record.cwd !== cwd)\n throw new SessionMcpConversationError(\n 'SESSION_TARGET_FIXED',\n 'This conversation already reserved a different execution directory. Recover that setup instead.',\n id,\n );\n if (record.cwd === cwd) return record;\n return replace({ ...record, cwd });\n },\n fail(id, parentSessionId, code) {\n const record = get(id, parentSessionId);\n if (record.state !== 'pending' || record.failureCode === code) return record;\n return replace({ ...record, failureCode: code });\n },\n bind(id, parentSessionId, workspaceId) {\n const record = get(id, parentSessionId);\n if (!record.cwd || !workspaceId) throw new Error('A prepared directory and admitted workspace are required.');\n if (record.state === 'bound') {\n if (record.workspaceId !== workspaceId) throw new Error('The bound workspace cannot change.');\n return record;\n }\n return replace({ ...record, state: 'bound', workspaceId, failureCode: undefined });\n },\n recover(id, parentSessionId) {\n const record = list().find((entry) => entry.id === id && entry.parentSessionId === parentSessionId);\n if (!record)\n throw new SessionMcpConversationError('SESSION_UNAVAILABLE', 'This conversation binding is unavailable.');\n return replace({ ...record, state: 'pending' });\n },\n closeSession(sessionId) {\n const next = list().map((record): SessionMcpConversation =>\n record.id === sessionId || record.parentSessionId === sessionId ? { ...record, state: 'closed' } : record,\n );\n if (next.some((record, index) => record !== current[index])) persist(next);\n },\n closeClient(clientId) {\n const next = list().map((record): SessionMcpConversation =>\n record.clientId === clientId ? { ...record, state: 'closed' } : record,\n );\n if (next.some((record, index) => record !== current[index])) persist(next);\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["createHash","path","fs","randomUUID"],"sources":["../../../../src/services/sessionMcpConversations/index.ts"],"sourcesContent":["import { createHash, randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nconst STORE_FILE = 'session-mcp-conversations.json';\nconst STORE_VERSION = 1;\nconst MAX_BINDINGS = 1000;\nconst MAX_CONVERSATION_BYTES = 1024;\nconst CONVERSATION_META_KEY = 'openai/session';\nconst ID_PATTERN = /^[A-Za-z0-9_-]+$/u;\nconst DIGEST_PATTERN = /^[a-f0-9]{64}$/u;\n\nexport type SessionMcpSetupKind = 'managed-worktree' | 'existing-directory';\nexport type SessionMcpSetupFailureCode = 'SESSION_WORKTREE_PROVISION_FAILED' | 'SESSION_UNAVAILABLE';\n\n/** Client metadata is a routing hint within an authenticated registration, never authority. */\nexport function sessionMcpConversationDigest(meta: unknown): string | undefined {\n if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) return undefined;\n const value = (meta as Record<string, unknown>)[CONVERSATION_META_KEY];\n if (\n typeof value !== 'string' ||\n value.trim() !== value ||\n value.length === 0 ||\n Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) ||\n Buffer.byteLength(value, 'utf8') > MAX_CONVERSATION_BYTES\n )\n return undefined;\n return createHash('sha256')\n .update(JSON.stringify(['chatgpt', value]))\n .digest('hex');\n}\n\nexport interface SessionMcpConversation {\n /** Also the server-reserved target session identity. Never supplied by the model. */\n readonly id: string;\n readonly clientId: string;\n readonly parentSessionId: string;\n readonly parentWorkspaceId: string;\n readonly conversationDigest: string;\n readonly createdAt: string;\n readonly state: 'pending' | 'bound' | 'closed';\n /** Host-selected execution provider. Old records without this field are not assumed to be worktrees. */\n readonly setupKind?: SessionMcpSetupKind;\n /** Public, bounded failure code. Never persist Git stderr or exception text here. */\n readonly failureCode?: SessionMcpSetupFailureCode;\n /** Persisted before checkout or session creation starts. */\n readonly cwd?: string;\n readonly workspaceId?: string;\n}\n\nexport class SessionMcpConversationError extends Error {\n constructor(\n readonly code: string,\n message: string,\n readonly bindingId?: string,\n ) {\n super(message);\n this.name = 'SessionMcpConversationError';\n }\n}\n\nexport interface SessionMcpConversationStore {\n list(): readonly SessionMcpConversation[];\n find(clientId: string, parentSessionId: string, digest: string): SessionMcpConversation | undefined;\n reserve(clientId: string, parentSessionId: string, parentWorkspaceId: string, digest: string): SessionMcpConversation;\n get(id: string, parentSessionId: string): SessionMcpConversation;\n selectSetup(id: string, parentSessionId: string, kind: SessionMcpSetupKind): SessionMcpConversation;\n prepare(id: string, parentSessionId: string, cwd: string): SessionMcpConversation;\n fail(id: string, parentSessionId: string, code: SessionMcpSetupFailureCode): SessionMcpConversation;\n bind(id: string, parentSessionId: string, workspaceId: string): SessionMcpConversation;\n recover(id: string, parentSessionId: string): SessionMcpConversation;\n closeSession(sessionId: string): void;\n closeClient(clientId: string): void;\n}\n\n/** How many id characters name a conversation's managed worktree branch and directory. */\nconst MANAGED_WORKTREE_ID_LENGTH = 12;\n\nfunction validRecord(value: unknown): value is SessionMcpConversation {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Partial<SessionMcpConversation>;\n return (\n typeof record.id === 'string' &&\n ID_PATTERN.test(record.id) &&\n typeof record.clientId === 'string' &&\n ID_PATTERN.test(record.clientId) &&\n typeof record.parentSessionId === 'string' &&\n ID_PATTERN.test(record.parentSessionId) &&\n typeof record.parentWorkspaceId === 'string' &&\n record.parentWorkspaceId.length > 0 &&\n typeof record.conversationDigest === 'string' &&\n DIGEST_PATTERN.test(record.conversationDigest) &&\n typeof record.createdAt === 'string' &&\n Number.isFinite(Date.parse(record.createdAt)) &&\n (record.state === 'pending' || record.state === 'bound' || record.state === 'closed') &&\n (record.setupKind === undefined ||\n record.setupKind === 'managed-worktree' ||\n record.setupKind === 'existing-directory') &&\n (record.failureCode === undefined ||\n record.failureCode === 'SESSION_WORKTREE_PROVISION_FAILED' ||\n record.failureCode === 'SESSION_UNAVAILABLE') &&\n (record.cwd === undefined ||\n (typeof record.cwd === 'string' && path.isAbsolute(record.cwd) && !record.cwd.includes('\\0'))) &&\n (record.workspaceId === undefined || (typeof record.workspaceId === 'string' && record.workspaceId.length > 0)) &&\n (record.state !== 'bound' || (record.cwd !== undefined && record.workspaceId !== undefined))\n );\n}\n\n/**\n * Records written before setups carried a provider have none, and recovery refuses a setup it\n * cannot attribute, which left every such conversation stuck. A managed conversation worktree is\n * the only setup named `doompi-conversation-<first 12 id characters>--`, so a record reserving\n * that directory can be attributed to it. Anything else stays unattributed and refused.\n */\nfunction withLegacySetupKind(record: SessionMcpConversation): SessionMcpConversation {\n if (record.setupKind !== undefined || record.cwd === undefined) return record;\n const managedName = `doompi-conversation-${record.id.slice(0, MANAGED_WORKTREE_ID_LENGTH)}--`;\n return path.basename(record.cwd).startsWith(managedName) ? { ...record, setupKind: 'managed-worktree' } : record;\n}\n\nfunction routingKey(\n record: Pick<SessionMcpConversation, 'clientId' | 'parentSessionId' | 'conversationDigest'>,\n): string {\n return JSON.stringify([record.clientId, record.parentSessionId, record.conversationDigest]);\n}\n\n/** One owning headless host persists these bindings. Corruption never becomes an empty, reusable store. */\nexport function createSessionMcpConversationStore(stateDir: string): SessionMcpConversationStore {\n const file = path.join(stateDir, STORE_FILE);\n let failure: Error | undefined;\n let current: readonly SessionMcpConversation[] = load();\n\n function load(): readonly SessionMcpConversation[] {\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(file, 'utf8'));\n if (typeof parsed !== 'object' || parsed === null) throw new Error('Invalid store');\n const stored = parsed as { version?: unknown; bindings?: unknown };\n if (stored.version !== STORE_VERSION || !Array.isArray(stored.bindings) || !stored.bindings.every(validRecord))\n throw new Error('Invalid bindings');\n const records: SessionMcpConversation[] = stored.bindings;\n if (\n records.length > MAX_BINDINGS ||\n new Set(records.map((record) => record.id)).size !== records.length ||\n new Set(records.map(routingKey)).size !== records.length\n )\n throw new Error('Duplicate or excessive bindings');\n return Object.freeze(records.map((record) => Object.freeze(withLegacySetupKind(record))));\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];\n failure = new SessionMcpConversationError(\n 'CONVERSATION_STORE_UNAVAILABLE',\n 'Conversation bindings could not be loaded. Restore the binding store before using conversation routing.',\n );\n return [];\n }\n }\n\n function list(): readonly SessionMcpConversation[] {\n if (failure) throw failure;\n return current;\n }\n\n function persist(next: readonly SessionMcpConversation[]): void {\n list();\n const temporary = `${file}.${randomUUID()}.tmp`;\n try {\n fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });\n const descriptor = fs.openSync(temporary, 'wx', 0o600);\n try {\n fs.writeFileSync(descriptor, `${JSON.stringify({ version: STORE_VERSION, bindings: next }, null, 2)}\\n`);\n fs.fsyncSync(descriptor);\n } finally {\n fs.closeSync(descriptor);\n }\n fs.renameSync(temporary, file);\n current = Object.freeze(next.map((record) => Object.freeze(record)));\n } catch {\n throw new SessionMcpConversationError(\n 'CONVERSATION_STORE_UNAVAILABLE',\n 'Conversation bindings could not be saved. No new execution target was admitted.',\n );\n } finally {\n // This temporary file never grants access and can safely be left for local recovery.\n try {\n fs.rmSync(temporary, { force: true });\n } catch {\n /* An inaccessible temporary file is not loaded. */\n }\n }\n }\n\n function get(id: string, parentSessionId: string): SessionMcpConversation {\n const record = list().find((entry) => entry.id === id && entry.parentSessionId === parentSessionId);\n if (!record || record.state === 'closed')\n throw new SessionMcpConversationError(\n 'SESSION_UNAVAILABLE',\n 'This conversation binding is unavailable. It will not be recreated automatically.',\n );\n return record;\n }\n\n function replace(record: SessionMcpConversation): SessionMcpConversation {\n persist(list().map((entry) => (entry.id === record.id ? record : entry)));\n return get(record.id, record.parentSessionId);\n }\n\n return {\n list,\n get,\n find: (clientId, parentSessionId, digest) =>\n list().find(\n (record) =>\n record.clientId === clientId &&\n record.parentSessionId === parentSessionId &&\n record.conversationDigest === digest,\n ),\n reserve(clientId, parentSessionId, parentWorkspaceId, digest) {\n const existing = list().find(\n (record) =>\n record.clientId === clientId &&\n record.parentSessionId === parentSessionId &&\n record.conversationDigest === digest,\n );\n if (existing) return existing;\n if (list().length >= MAX_BINDINGS)\n throw new SessionMcpConversationError(\n 'CONVERSATION_LIMIT_REACHED',\n 'The conversation binding limit has been reached. Use a dedicated session connection.',\n );\n const record: SessionMcpConversation = {\n id: randomUUID(),\n clientId,\n parentSessionId,\n parentWorkspaceId,\n conversationDigest: digest,\n createdAt: new Date().toISOString(),\n state: 'pending',\n };\n if (!validRecord(record))\n throw new SessionMcpConversationError('INVALID_CONVERSATION', 'Invalid conversation identity.');\n persist([...list(), record]);\n return get(record.id, parentSessionId);\n },\n selectSetup(id, parentSessionId, kind) {\n const record = get(id, parentSessionId);\n if (record.setupKind === kind)\n return record.failureCode === undefined ? record : replace({ ...record, failureCode: undefined });\n if (record.setupKind !== undefined && (record.cwd !== undefined || record.state === 'bound'))\n throw new SessionMcpConversationError(\n 'SESSION_TARGET_FIXED',\n 'An initialized setup cannot change its provider.',\n id,\n );\n if (record.setupKind === undefined && record.cwd !== undefined)\n throw new SessionMcpConversationError(\n 'SESSION_TARGET_FIXED',\n 'This existing setup has no verified provider. Recover it without changing its ownership.',\n id,\n );\n return replace({ ...record, setupKind: kind, failureCode: undefined });\n },\n prepare(id, parentSessionId, cwd) {\n const record = get(id, parentSessionId);\n if (!path.isAbsolute(cwd) || cwd.includes('\\0'))\n throw new Error('A canonical absolute execution directory is required.');\n if (record.cwd !== undefined && record.cwd !== cwd)\n throw new SessionMcpConversationError(\n 'SESSION_TARGET_FIXED',\n 'This conversation already reserved a different execution directory. Recover that setup instead.',\n id,\n );\n if (record.cwd === cwd) return record;\n return replace({ ...record, cwd });\n },\n fail(id, parentSessionId, code) {\n const record = get(id, parentSessionId);\n if (record.state !== 'pending' || record.failureCode === code) return record;\n return replace({ ...record, failureCode: code });\n },\n bind(id, parentSessionId, workspaceId) {\n const record = get(id, parentSessionId);\n if (!record.cwd || !workspaceId) throw new Error('A prepared directory and admitted workspace are required.');\n if (record.state === 'bound') {\n if (record.workspaceId !== workspaceId) throw new Error('The bound workspace cannot change.');\n return record;\n }\n return replace({ ...record, state: 'bound', workspaceId, failureCode: undefined });\n },\n recover(id, parentSessionId) {\n const record = list().find((entry) => entry.id === id && entry.parentSessionId === parentSessionId);\n if (!record)\n throw new SessionMcpConversationError('SESSION_UNAVAILABLE', 'This conversation binding is unavailable.');\n return replace({ ...record, state: 'pending' });\n },\n closeSession(sessionId) {\n const next = list().map((record): SessionMcpConversation =>\n record.id === sessionId || record.parentSessionId === sessionId ? { ...record, state: 'closed' } : record,\n );\n if (next.some((record, index) => record !== current[index])) persist(next);\n },\n closeClient(clientId) {\n const next = list().map((record): SessionMcpConversation =>\n record.clientId === clientId ? { ...record, state: 'closed' } : record,\n );\n if (next.some((record, index) => record !== current[index])) persist(next);\n },\n };\n}\n"],"mappings":";;;;;;;AAIA,MAAM,aAAa;AACnB,MAAM,gBAAgB;AACtB,MAAM,eAAe;AACrB,MAAM,yBAAyB;AAC/B,MAAM,wBAAwB;AAC9B,MAAM,aAAa;AACnB,MAAM,iBAAiB;;AAMvB,SAAgB,6BAA6B,MAAmC;CAC9E,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,IAAI,GAAG,OAAO,KAAA;CAC7E,MAAM,QAAS,KAAiC;CAChD,IACE,OAAO,UAAU,YACjB,MAAM,KAAK,MAAM,SACjB,MAAM,WAAW,KACjB,MAAM,KAAK,KAAK,CAAC,CAAC,MAAM,cAAc,UAAU,WAAW,CAAC,IAAI,MAAM,UAAU,WAAW,CAAC,MAAM,GAAG,KACrG,OAAO,WAAW,OAAO,MAAM,IAAI,wBAEnC,OAAO,KAAA;CACT,QAAA,GAAOA,YAAAA,WAAAA,CAAW,QAAQ,CAAC,CACxB,OAAO,KAAK,UAAU,CAAC,WAAW,KAAK,CAAC,CAAC,CAAC,CAC1C,OAAO,KAAK;AACjB;AAoBA,IAAa,8BAAb,cAAiD,MAAM;CAE1C;CAEA;CAHX,YACE,MACA,SACA,WACA;EACA,MAAM,OAAO;EAJJ,KAAA,OAAA;EAEA,KAAA,YAAA;EAGT,KAAK,OAAO;CACd;AACF;;AAiBA,MAAM,6BAA6B;AAEnC,SAAS,YAAY,OAAiD;CACpE,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM,OAAO;CACxD,MAAM,SAAS;CACf,OACE,OAAO,OAAO,OAAO,YACrB,WAAW,KAAK,OAAO,EAAE,KACzB,OAAO,OAAO,aAAa,YAC3B,WAAW,KAAK,OAAO,QAAQ,KAC/B,OAAO,OAAO,oBAAoB,YAClC,WAAW,KAAK,OAAO,eAAe,KACtC,OAAO,OAAO,sBAAsB,YACpC,OAAO,kBAAkB,SAAS,KAClC,OAAO,OAAO,uBAAuB,YACrC,eAAe,KAAK,OAAO,kBAAkB,KAC7C,OAAO,OAAO,cAAc,YAC5B,OAAO,SAAS,KAAK,MAAM,OAAO,SAAS,CAAC,MAC3C,OAAO,UAAU,aAAa,OAAO,UAAU,WAAW,OAAO,UAAU,cAC3E,OAAO,cAAc,KAAA,KACpB,OAAO,cAAc,sBACrB,OAAO,cAAc,0BACtB,OAAO,gBAAgB,KAAA,KACtB,OAAO,gBAAgB,uCACvB,OAAO,gBAAgB,2BACxB,OAAO,QAAQ,KAAA,KACb,OAAO,OAAO,QAAQ,YAAYC,UAAAA,QAAK,WAAW,OAAO,GAAG,KAAK,CAAC,OAAO,IAAI,SAAS,IAAI,OAC5F,OAAO,gBAAgB,KAAA,KAAc,OAAO,OAAO,gBAAgB,YAAY,OAAO,YAAY,SAAS,OAC3G,OAAO,UAAU,WAAY,OAAO,QAAQ,KAAA,KAAa,OAAO,gBAAgB,KAAA;AAErF;;;;;;;AAQA,SAAS,oBAAoB,QAAwD;CACnF,IAAI,OAAO,cAAc,KAAA,KAAa,OAAO,QAAQ,KAAA,GAAW,OAAO;CACvE,MAAM,cAAc,uBAAuB,OAAO,GAAG,MAAM,GAAG,0BAA0B,EAAE;CAC1F,OAAOA,UAAAA,QAAK,SAAS,OAAO,GAAG,CAAC,CAAC,WAAW,WAAW,IAAI;EAAE,GAAG;EAAQ,WAAW;CAAmB,IAAI;AAC5G;AAEA,SAAS,WACP,QACQ;CACR,OAAO,KAAK,UAAU;EAAC,OAAO;EAAU,OAAO;EAAiB,OAAO;CAAkB,CAAC;AAC5F;;AAGA,SAAgB,kCAAkC,UAA+C;CAC/F,MAAM,OAAOA,UAAAA,QAAK,KAAK,UAAU,UAAU;CAC3C,IAAI;CACJ,IAAI,UAA6C,KAAK;CAEtD,SAAS,OAA0C;EACjD,IAAI;GACF,MAAM,SAAkB,KAAK,MAAMC,QAAAA,QAAG,aAAa,MAAM,MAAM,CAAC;GAChE,IAAI,OAAO,WAAW,YAAY,WAAW,MAAM,MAAM,IAAI,MAAM,eAAe;GAClF,MAAM,SAAS;GACf,IAAI,OAAO,YAAY,iBAAiB,CAAC,MAAM,QAAQ,OAAO,QAAQ,KAAK,CAAC,OAAO,SAAS,MAAM,WAAW,GAC3G,MAAM,IAAI,MAAM,kBAAkB;GACpC,MAAM,UAAoC,OAAO;GACjD,IACE,QAAQ,SAAS,gBACjB,IAAI,IAAI,QAAQ,KAAK,WAAW,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,QAAQ,UAC7D,IAAI,IAAI,QAAQ,IAAI,UAAU,CAAC,CAAC,CAAC,SAAS,QAAQ,QAElD,MAAM,IAAI,MAAM,iCAAiC;GACnD,OAAO,OAAO,OAAO,QAAQ,KAAK,WAAW,OAAO,OAAO,oBAAoB,MAAM,CAAC,CAAC,CAAC;EAC1F,SAAS,OAAO;GACd,IAAK,MAAgC,SAAS,UAAU,OAAO,CAAC;GAChE,UAAU,IAAI,4BACZ,kCACA,yGACF;GACA,OAAO,CAAC;EACV;CACF;CAEA,SAAS,OAA0C;EACjD,IAAI,SAAS,MAAM;EACnB,OAAO;CACT;CAEA,SAAS,QAAQ,MAA+C;EAC9D,KAAK;EACL,MAAM,YAAY,GAAG,KAAK,IAAA,GAAGC,YAAAA,WAAAA,CAAW,EAAE;EAC1C,IAAI;GACF,QAAA,QAAG,UAAU,UAAU;IAAE,WAAW;IAAM,MAAM;GAAM,CAAC;GACvD,MAAM,aAAaD,QAAAA,QAAG,SAAS,WAAW,MAAM,GAAK;GACrD,IAAI;IACF,QAAA,QAAG,cAAc,YAAY,GAAG,KAAK,UAAU;KAAE,SAAS;KAAe,UAAU;IAAK,GAAG,MAAM,CAAC,EAAE,GAAG;IACvG,QAAA,QAAG,UAAU,UAAU;GACzB,UAAU;IACR,QAAA,QAAG,UAAU,UAAU;GACzB;GACA,QAAA,QAAG,WAAW,WAAW,IAAI;GAC7B,UAAU,OAAO,OAAO,KAAK,KAAK,WAAW,OAAO,OAAO,MAAM,CAAC,CAAC;EACrE,QAAQ;GACN,MAAM,IAAI,4BACR,kCACA,iFACF;EACF,UAAU;GAER,IAAI;IACF,QAAA,QAAG,OAAO,WAAW,EAAE,OAAO,KAAK,CAAC;GACtC,QAAQ,CAER;EACF;CACF;CAEA,SAAS,IAAI,IAAY,iBAAiD;EACxE,MAAM,SAAS,KAAK,CAAC,CAAC,MAAM,UAAU,MAAM,OAAO,MAAM,MAAM,oBAAoB,eAAe;EAClG,IAAI,CAAC,UAAU,OAAO,UAAU,UAC9B,MAAM,IAAI,4BACR,uBACA,mFACF;EACF,OAAO;CACT;CAEA,SAAS,QAAQ,QAAwD;EACvE,QAAQ,KAAK,CAAC,CAAC,KAAK,UAAW,MAAM,OAAO,OAAO,KAAK,SAAS,KAAM,CAAC;EACxE,OAAO,IAAI,OAAO,IAAI,OAAO,eAAe;CAC9C;CAEA,OAAO;EACL;EACA;EACA,OAAO,UAAU,iBAAiB,WAChC,KAAK,CAAC,CAAC,MACJ,WACC,OAAO,aAAa,YACpB,OAAO,oBAAoB,mBAC3B,OAAO,uBAAuB,MAClC;EACF,QAAQ,UAAU,iBAAiB,mBAAmB,QAAQ;GAC5D,MAAM,WAAW,KAAK,CAAC,CAAC,MACrB,WACC,OAAO,aAAa,YACpB,OAAO,oBAAoB,mBAC3B,OAAO,uBAAuB,MAClC;GACA,IAAI,UAAU,OAAO;GACrB,IAAI,KAAK,CAAC,CAAC,UAAU,cACnB,MAAM,IAAI,4BACR,8BACA,sFACF;GACF,MAAM,SAAiC;IACrC,KAAA,GAAIC,YAAAA,WAAAA,CAAW;IACf;IACA;IACA;IACA,oBAAoB;IACpB,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;IAClC,OAAO;GACT;GACA,IAAI,CAAC,YAAY,MAAM,GACrB,MAAM,IAAI,4BAA4B,wBAAwB,gCAAgC;GAChG,QAAQ,CAAC,GAAG,KAAK,GAAG,MAAM,CAAC;GAC3B,OAAO,IAAI,OAAO,IAAI,eAAe;EACvC;EACA,YAAY,IAAI,iBAAiB,MAAM;GACrC,MAAM,SAAS,IAAI,IAAI,eAAe;GACtC,IAAI,OAAO,cAAc,MACvB,OAAO,OAAO,gBAAgB,KAAA,IAAY,SAAS,QAAQ;IAAE,GAAG;IAAQ,aAAa,KAAA;GAAU,CAAC;GAClG,IAAI,OAAO,cAAc,KAAA,MAAc,OAAO,QAAQ,KAAA,KAAa,OAAO,UAAU,UAClF,MAAM,IAAI,4BACR,wBACA,oDACA,EACF;GACF,IAAI,OAAO,cAAc,KAAA,KAAa,OAAO,QAAQ,KAAA,GACnD,MAAM,IAAI,4BACR,wBACA,4FACA,EACF;GACF,OAAO,QAAQ;IAAE,GAAG;IAAQ,WAAW;IAAM,aAAa,KAAA;GAAU,CAAC;EACvE;EACA,QAAQ,IAAI,iBAAiB,KAAK;GAChC,MAAM,SAAS,IAAI,IAAI,eAAe;GACtC,IAAI,CAACF,UAAAA,QAAK,WAAW,GAAG,KAAK,IAAI,SAAS,IAAI,GAC5C,MAAM,IAAI,MAAM,uDAAuD;GACzE,IAAI,OAAO,QAAQ,KAAA,KAAa,OAAO,QAAQ,KAC7C,MAAM,IAAI,4BACR,wBACA,mGACA,EACF;GACF,IAAI,OAAO,QAAQ,KAAK,OAAO;GAC/B,OAAO,QAAQ;IAAE,GAAG;IAAQ;GAAI,CAAC;EACnC;EACA,KAAK,IAAI,iBAAiB,MAAM;GAC9B,MAAM,SAAS,IAAI,IAAI,eAAe;GACtC,IAAI,OAAO,UAAU,aAAa,OAAO,gBAAgB,MAAM,OAAO;GACtE,OAAO,QAAQ;IAAE,GAAG;IAAQ,aAAa;GAAK,CAAC;EACjD;EACA,KAAK,IAAI,iBAAiB,aAAa;GACrC,MAAM,SAAS,IAAI,IAAI,eAAe;GACtC,IAAI,CAAC,OAAO,OAAO,CAAC,aAAa,MAAM,IAAI,MAAM,2DAA2D;GAC5G,IAAI,OAAO,UAAU,SAAS;IAC5B,IAAI,OAAO,gBAAgB,aAAa,MAAM,IAAI,MAAM,oCAAoC;IAC5F,OAAO;GACT;GACA,OAAO,QAAQ;IAAE,GAAG;IAAQ,OAAO;IAAS;IAAa,aAAa,KAAA;GAAU,CAAC;EACnF;EACA,QAAQ,IAAI,iBAAiB;GAC3B,MAAM,SAAS,KAAK,CAAC,CAAC,MAAM,UAAU,MAAM,OAAO,MAAM,MAAM,oBAAoB,eAAe;GAClG,IAAI,CAAC,QACH,MAAM,IAAI,4BAA4B,uBAAuB,2CAA2C;GAC1G,OAAO,QAAQ;IAAE,GAAG;IAAQ,OAAO;GAAU,CAAC;EAChD;EACA,aAAa,WAAW;GACtB,MAAM,OAAO,KAAK,CAAC,CAAC,KAAK,WACvB,OAAO,OAAO,aAAa,OAAO,oBAAoB,YAAY;IAAE,GAAG;IAAQ,OAAO;GAAS,IAAI,MACrG;GACA,IAAI,KAAK,MAAM,QAAQ,UAAU,WAAW,QAAQ,MAAM,GAAG,QAAQ,IAAI;EAC3E;EACA,YAAY,UAAU;GACpB,MAAM,OAAO,KAAK,CAAC,CAAC,KAAK,WACvB,OAAO,aAAa,WAAW;IAAE,GAAG;IAAQ,OAAO;GAAS,IAAI,MAClE;GACA,IAAI,KAAK,MAAM,QAAQ,UAAU,WAAW,QAAQ,MAAM,GAAG,QAAQ,IAAI;EAC3E;CACF;AACF"}
@@ -31,7 +31,7 @@ const MANAGED_WORKTREE_ID_LENGTH = 12;
31
31
  function validRecord(value) {
32
32
  if (typeof value !== "object" || value === null) return false;
33
33
  const record = value;
34
- return typeof record.id === "string" && ID_PATTERN.test(record.id) && typeof record.clientId === "string" && ID_PATTERN.test(record.clientId) && typeof record.parentSessionId === "string" && ID_PATTERN.test(record.parentSessionId) && typeof record.parentWorkspaceId === "string" && record.parentWorkspaceId.length > 0 && typeof record.conversationDigest === "string" && DIGEST_PATTERN.test(record.conversationDigest) && typeof record.createdAt === "string" && Number.isFinite(Date.parse(record.createdAt)) && (record.state === "pending" || record.state === "bound" || record.state === "closed") && (record.setupKind === void 0 || record.setupKind === "managed-worktree" || record.setupKind === "existing-directory") && (record.failureCode === void 0 || record.failureCode === "SESSION_WORKTREE_PROVISION_FAILED" || record.failureCode === "SESSION_UNAVAILABLE") && (record.failureCode === void 0 || record.state === "pending") && (record.cwd === void 0 || typeof record.cwd === "string" && path.isAbsolute(record.cwd) && !record.cwd.includes("\0")) && (record.workspaceId === void 0 || typeof record.workspaceId === "string" && record.workspaceId.length > 0) && (record.state !== "bound" || record.cwd !== void 0 && record.workspaceId !== void 0);
34
+ return typeof record.id === "string" && ID_PATTERN.test(record.id) && typeof record.clientId === "string" && ID_PATTERN.test(record.clientId) && typeof record.parentSessionId === "string" && ID_PATTERN.test(record.parentSessionId) && typeof record.parentWorkspaceId === "string" && record.parentWorkspaceId.length > 0 && typeof record.conversationDigest === "string" && DIGEST_PATTERN.test(record.conversationDigest) && typeof record.createdAt === "string" && Number.isFinite(Date.parse(record.createdAt)) && (record.state === "pending" || record.state === "bound" || record.state === "closed") && (record.setupKind === void 0 || record.setupKind === "managed-worktree" || record.setupKind === "existing-directory") && (record.failureCode === void 0 || record.failureCode === "SESSION_WORKTREE_PROVISION_FAILED" || record.failureCode === "SESSION_UNAVAILABLE") && (record.cwd === void 0 || typeof record.cwd === "string" && path.isAbsolute(record.cwd) && !record.cwd.includes("\0")) && (record.workspaceId === void 0 || typeof record.workspaceId === "string" && record.workspaceId.length > 0) && (record.state !== "bound" || record.cwd !== void 0 && record.workspaceId !== void 0);
35
35
  }
36
36
  /**
37
37
  * Records written before setups carried a provider have none, and recovery refuses a setup it
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/sessionMcpConversations/index.ts"],"sourcesContent":["import { createHash, randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nconst STORE_FILE = 'session-mcp-conversations.json';\nconst STORE_VERSION = 1;\nconst MAX_BINDINGS = 1000;\nconst MAX_CONVERSATION_BYTES = 1024;\nconst CONVERSATION_META_KEY = 'openai/session';\nconst ID_PATTERN = /^[A-Za-z0-9_-]+$/u;\nconst DIGEST_PATTERN = /^[a-f0-9]{64}$/u;\n\nexport type SessionMcpSetupKind = 'managed-worktree' | 'existing-directory';\nexport type SessionMcpSetupFailureCode = 'SESSION_WORKTREE_PROVISION_FAILED' | 'SESSION_UNAVAILABLE';\n\n/** Client metadata is a routing hint within an authenticated registration, never authority. */\nexport function sessionMcpConversationDigest(meta: unknown): string | undefined {\n if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) return undefined;\n const value = (meta as Record<string, unknown>)[CONVERSATION_META_KEY];\n if (\n typeof value !== 'string' ||\n value.trim() !== value ||\n value.length === 0 ||\n Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) ||\n Buffer.byteLength(value, 'utf8') > MAX_CONVERSATION_BYTES\n )\n return undefined;\n return createHash('sha256')\n .update(JSON.stringify(['chatgpt', value]))\n .digest('hex');\n}\n\nexport interface SessionMcpConversation {\n /** Also the server-reserved target session identity. Never supplied by the model. */\n readonly id: string;\n readonly clientId: string;\n readonly parentSessionId: string;\n readonly parentWorkspaceId: string;\n readonly conversationDigest: string;\n readonly createdAt: string;\n readonly state: 'pending' | 'bound' | 'closed';\n /** Host-selected execution provider. Old records without this field are not assumed to be worktrees. */\n readonly setupKind?: SessionMcpSetupKind;\n /** Public, bounded failure code. Never persist Git stderr or exception text here. */\n readonly failureCode?: SessionMcpSetupFailureCode;\n /** Persisted before checkout or session creation starts. */\n readonly cwd?: string;\n readonly workspaceId?: string;\n}\n\nexport class SessionMcpConversationError extends Error {\n constructor(\n readonly code: string,\n message: string,\n readonly bindingId?: string,\n ) {\n super(message);\n this.name = 'SessionMcpConversationError';\n }\n}\n\nexport interface SessionMcpConversationStore {\n list(): readonly SessionMcpConversation[];\n find(clientId: string, parentSessionId: string, digest: string): SessionMcpConversation | undefined;\n reserve(clientId: string, parentSessionId: string, parentWorkspaceId: string, digest: string): SessionMcpConversation;\n get(id: string, parentSessionId: string): SessionMcpConversation;\n selectSetup(id: string, parentSessionId: string, kind: SessionMcpSetupKind): SessionMcpConversation;\n prepare(id: string, parentSessionId: string, cwd: string): SessionMcpConversation;\n fail(id: string, parentSessionId: string, code: SessionMcpSetupFailureCode): SessionMcpConversation;\n bind(id: string, parentSessionId: string, workspaceId: string): SessionMcpConversation;\n recover(id: string, parentSessionId: string): SessionMcpConversation;\n closeSession(sessionId: string): void;\n closeClient(clientId: string): void;\n}\n\n/** How many id characters name a conversation's managed worktree branch and directory. */\nconst MANAGED_WORKTREE_ID_LENGTH = 12;\n\nfunction validRecord(value: unknown): value is SessionMcpConversation {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Partial<SessionMcpConversation>;\n return (\n typeof record.id === 'string' &&\n ID_PATTERN.test(record.id) &&\n typeof record.clientId === 'string' &&\n ID_PATTERN.test(record.clientId) &&\n typeof record.parentSessionId === 'string' &&\n ID_PATTERN.test(record.parentSessionId) &&\n typeof record.parentWorkspaceId === 'string' &&\n record.parentWorkspaceId.length > 0 &&\n typeof record.conversationDigest === 'string' &&\n DIGEST_PATTERN.test(record.conversationDigest) &&\n typeof record.createdAt === 'string' &&\n Number.isFinite(Date.parse(record.createdAt)) &&\n (record.state === 'pending' || record.state === 'bound' || record.state === 'closed') &&\n (record.setupKind === undefined ||\n record.setupKind === 'managed-worktree' ||\n record.setupKind === 'existing-directory') &&\n (record.failureCode === undefined ||\n record.failureCode === 'SESSION_WORKTREE_PROVISION_FAILED' ||\n record.failureCode === 'SESSION_UNAVAILABLE') &&\n (record.failureCode === undefined || record.state === 'pending') &&\n (record.cwd === undefined ||\n (typeof record.cwd === 'string' && path.isAbsolute(record.cwd) && !record.cwd.includes('\\0'))) &&\n (record.workspaceId === undefined || (typeof record.workspaceId === 'string' && record.workspaceId.length > 0)) &&\n (record.state !== 'bound' || (record.cwd !== undefined && record.workspaceId !== undefined))\n );\n}\n\n/**\n * Records written before setups carried a provider have none, and recovery refuses a setup it\n * cannot attribute, which left every such conversation stuck. A managed conversation worktree is\n * the only setup named `doompi-conversation-<first 12 id characters>--`, so a record reserving\n * that directory can be attributed to it. Anything else stays unattributed and refused.\n */\nfunction withLegacySetupKind(record: SessionMcpConversation): SessionMcpConversation {\n if (record.setupKind !== undefined || record.cwd === undefined) return record;\n const managedName = `doompi-conversation-${record.id.slice(0, MANAGED_WORKTREE_ID_LENGTH)}--`;\n return path.basename(record.cwd).startsWith(managedName) ? { ...record, setupKind: 'managed-worktree' } : record;\n}\n\nfunction routingKey(\n record: Pick<SessionMcpConversation, 'clientId' | 'parentSessionId' | 'conversationDigest'>,\n): string {\n return JSON.stringify([record.clientId, record.parentSessionId, record.conversationDigest]);\n}\n\n/** One owning headless host persists these bindings. Corruption never becomes an empty, reusable store. */\nexport function createSessionMcpConversationStore(stateDir: string): SessionMcpConversationStore {\n const file = path.join(stateDir, STORE_FILE);\n let failure: Error | undefined;\n let current: readonly SessionMcpConversation[] = load();\n\n function load(): readonly SessionMcpConversation[] {\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(file, 'utf8'));\n if (typeof parsed !== 'object' || parsed === null) throw new Error('Invalid store');\n const stored = parsed as { version?: unknown; bindings?: unknown };\n if (stored.version !== STORE_VERSION || !Array.isArray(stored.bindings) || !stored.bindings.every(validRecord))\n throw new Error('Invalid bindings');\n const records: SessionMcpConversation[] = stored.bindings;\n if (\n records.length > MAX_BINDINGS ||\n new Set(records.map((record) => record.id)).size !== records.length ||\n new Set(records.map(routingKey)).size !== records.length\n )\n throw new Error('Duplicate or excessive bindings');\n return Object.freeze(records.map((record) => Object.freeze(withLegacySetupKind(record))));\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];\n failure = new SessionMcpConversationError(\n 'CONVERSATION_STORE_UNAVAILABLE',\n 'Conversation bindings could not be loaded. Restore the binding store before using conversation routing.',\n );\n return [];\n }\n }\n\n function list(): readonly SessionMcpConversation[] {\n if (failure) throw failure;\n return current;\n }\n\n function persist(next: readonly SessionMcpConversation[]): void {\n list();\n const temporary = `${file}.${randomUUID()}.tmp`;\n try {\n fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });\n const descriptor = fs.openSync(temporary, 'wx', 0o600);\n try {\n fs.writeFileSync(descriptor, `${JSON.stringify({ version: STORE_VERSION, bindings: next }, null, 2)}\\n`);\n fs.fsyncSync(descriptor);\n } finally {\n fs.closeSync(descriptor);\n }\n fs.renameSync(temporary, file);\n current = Object.freeze(next.map((record) => Object.freeze(record)));\n } catch {\n throw new SessionMcpConversationError(\n 'CONVERSATION_STORE_UNAVAILABLE',\n 'Conversation bindings could not be saved. No new execution target was admitted.',\n );\n } finally {\n // This temporary file never grants access and can safely be left for local recovery.\n try {\n fs.rmSync(temporary, { force: true });\n } catch {\n /* An inaccessible temporary file is not loaded. */\n }\n }\n }\n\n function get(id: string, parentSessionId: string): SessionMcpConversation {\n const record = list().find((entry) => entry.id === id && entry.parentSessionId === parentSessionId);\n if (!record || record.state === 'closed')\n throw new SessionMcpConversationError(\n 'SESSION_UNAVAILABLE',\n 'This conversation binding is unavailable. It will not be recreated automatically.',\n );\n return record;\n }\n\n function replace(record: SessionMcpConversation): SessionMcpConversation {\n persist(list().map((entry) => (entry.id === record.id ? record : entry)));\n return get(record.id, record.parentSessionId);\n }\n\n return {\n list,\n get,\n find: (clientId, parentSessionId, digest) =>\n list().find(\n (record) =>\n record.clientId === clientId &&\n record.parentSessionId === parentSessionId &&\n record.conversationDigest === digest,\n ),\n reserve(clientId, parentSessionId, parentWorkspaceId, digest) {\n const existing = list().find(\n (record) =>\n record.clientId === clientId &&\n record.parentSessionId === parentSessionId &&\n record.conversationDigest === digest,\n );\n if (existing) return existing;\n if (list().length >= MAX_BINDINGS)\n throw new SessionMcpConversationError(\n 'CONVERSATION_LIMIT_REACHED',\n 'The conversation binding limit has been reached. Use a dedicated session connection.',\n );\n const record: SessionMcpConversation = {\n id: randomUUID(),\n clientId,\n parentSessionId,\n parentWorkspaceId,\n conversationDigest: digest,\n createdAt: new Date().toISOString(),\n state: 'pending',\n };\n if (!validRecord(record))\n throw new SessionMcpConversationError('INVALID_CONVERSATION', 'Invalid conversation identity.');\n persist([...list(), record]);\n return get(record.id, parentSessionId);\n },\n selectSetup(id, parentSessionId, kind) {\n const record = get(id, parentSessionId);\n if (record.setupKind === kind)\n return record.failureCode === undefined ? record : replace({ ...record, failureCode: undefined });\n if (record.setupKind !== undefined && (record.cwd !== undefined || record.state === 'bound'))\n throw new SessionMcpConversationError(\n 'SESSION_TARGET_FIXED',\n 'An initialized setup cannot change its provider.',\n id,\n );\n if (record.setupKind === undefined && record.cwd !== undefined)\n throw new SessionMcpConversationError(\n 'SESSION_TARGET_FIXED',\n 'This existing setup has no verified provider. Recover it without changing its ownership.',\n id,\n );\n return replace({ ...record, setupKind: kind, failureCode: undefined });\n },\n prepare(id, parentSessionId, cwd) {\n const record = get(id, parentSessionId);\n if (!path.isAbsolute(cwd) || cwd.includes('\\0'))\n throw new Error('A canonical absolute execution directory is required.');\n if (record.cwd !== undefined && record.cwd !== cwd)\n throw new SessionMcpConversationError(\n 'SESSION_TARGET_FIXED',\n 'This conversation already reserved a different execution directory. Recover that setup instead.',\n id,\n );\n if (record.cwd === cwd) return record;\n return replace({ ...record, cwd });\n },\n fail(id, parentSessionId, code) {\n const record = get(id, parentSessionId);\n if (record.state !== 'pending' || record.failureCode === code) return record;\n return replace({ ...record, failureCode: code });\n },\n bind(id, parentSessionId, workspaceId) {\n const record = get(id, parentSessionId);\n if (!record.cwd || !workspaceId) throw new Error('A prepared directory and admitted workspace are required.');\n if (record.state === 'bound') {\n if (record.workspaceId !== workspaceId) throw new Error('The bound workspace cannot change.');\n return record;\n }\n return replace({ ...record, state: 'bound', workspaceId, failureCode: undefined });\n },\n recover(id, parentSessionId) {\n const record = list().find((entry) => entry.id === id && entry.parentSessionId === parentSessionId);\n if (!record)\n throw new SessionMcpConversationError('SESSION_UNAVAILABLE', 'This conversation binding is unavailable.');\n return replace({ ...record, state: 'pending' });\n },\n closeSession(sessionId) {\n const next = list().map((record): SessionMcpConversation =>\n record.id === sessionId || record.parentSessionId === sessionId ? { ...record, state: 'closed' } : record,\n );\n if (next.some((record, index) => record !== current[index])) persist(next);\n },\n closeClient(clientId) {\n const next = list().map((record): SessionMcpConversation =>\n record.clientId === clientId ? { ...record, state: 'closed' } : record,\n );\n if (next.some((record, index) => record !== current[index])) persist(next);\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../../../src/services/sessionMcpConversations/index.ts"],"sourcesContent":["import { createHash, randomUUID } from 'node:crypto';\nimport fs from 'node:fs';\nimport path from 'node:path';\n\nconst STORE_FILE = 'session-mcp-conversations.json';\nconst STORE_VERSION = 1;\nconst MAX_BINDINGS = 1000;\nconst MAX_CONVERSATION_BYTES = 1024;\nconst CONVERSATION_META_KEY = 'openai/session';\nconst ID_PATTERN = /^[A-Za-z0-9_-]+$/u;\nconst DIGEST_PATTERN = /^[a-f0-9]{64}$/u;\n\nexport type SessionMcpSetupKind = 'managed-worktree' | 'existing-directory';\nexport type SessionMcpSetupFailureCode = 'SESSION_WORKTREE_PROVISION_FAILED' | 'SESSION_UNAVAILABLE';\n\n/** Client metadata is a routing hint within an authenticated registration, never authority. */\nexport function sessionMcpConversationDigest(meta: unknown): string | undefined {\n if (typeof meta !== 'object' || meta === null || Array.isArray(meta)) return undefined;\n const value = (meta as Record<string, unknown>)[CONVERSATION_META_KEY];\n if (\n typeof value !== 'string' ||\n value.trim() !== value ||\n value.length === 0 ||\n Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) ||\n Buffer.byteLength(value, 'utf8') > MAX_CONVERSATION_BYTES\n )\n return undefined;\n return createHash('sha256')\n .update(JSON.stringify(['chatgpt', value]))\n .digest('hex');\n}\n\nexport interface SessionMcpConversation {\n /** Also the server-reserved target session identity. Never supplied by the model. */\n readonly id: string;\n readonly clientId: string;\n readonly parentSessionId: string;\n readonly parentWorkspaceId: string;\n readonly conversationDigest: string;\n readonly createdAt: string;\n readonly state: 'pending' | 'bound' | 'closed';\n /** Host-selected execution provider. Old records without this field are not assumed to be worktrees. */\n readonly setupKind?: SessionMcpSetupKind;\n /** Public, bounded failure code. Never persist Git stderr or exception text here. */\n readonly failureCode?: SessionMcpSetupFailureCode;\n /** Persisted before checkout or session creation starts. */\n readonly cwd?: string;\n readonly workspaceId?: string;\n}\n\nexport class SessionMcpConversationError extends Error {\n constructor(\n readonly code: string,\n message: string,\n readonly bindingId?: string,\n ) {\n super(message);\n this.name = 'SessionMcpConversationError';\n }\n}\n\nexport interface SessionMcpConversationStore {\n list(): readonly SessionMcpConversation[];\n find(clientId: string, parentSessionId: string, digest: string): SessionMcpConversation | undefined;\n reserve(clientId: string, parentSessionId: string, parentWorkspaceId: string, digest: string): SessionMcpConversation;\n get(id: string, parentSessionId: string): SessionMcpConversation;\n selectSetup(id: string, parentSessionId: string, kind: SessionMcpSetupKind): SessionMcpConversation;\n prepare(id: string, parentSessionId: string, cwd: string): SessionMcpConversation;\n fail(id: string, parentSessionId: string, code: SessionMcpSetupFailureCode): SessionMcpConversation;\n bind(id: string, parentSessionId: string, workspaceId: string): SessionMcpConversation;\n recover(id: string, parentSessionId: string): SessionMcpConversation;\n closeSession(sessionId: string): void;\n closeClient(clientId: string): void;\n}\n\n/** How many id characters name a conversation's managed worktree branch and directory. */\nconst MANAGED_WORKTREE_ID_LENGTH = 12;\n\nfunction validRecord(value: unknown): value is SessionMcpConversation {\n if (typeof value !== 'object' || value === null) return false;\n const record = value as Partial<SessionMcpConversation>;\n return (\n typeof record.id === 'string' &&\n ID_PATTERN.test(record.id) &&\n typeof record.clientId === 'string' &&\n ID_PATTERN.test(record.clientId) &&\n typeof record.parentSessionId === 'string' &&\n ID_PATTERN.test(record.parentSessionId) &&\n typeof record.parentWorkspaceId === 'string' &&\n record.parentWorkspaceId.length > 0 &&\n typeof record.conversationDigest === 'string' &&\n DIGEST_PATTERN.test(record.conversationDigest) &&\n typeof record.createdAt === 'string' &&\n Number.isFinite(Date.parse(record.createdAt)) &&\n (record.state === 'pending' || record.state === 'bound' || record.state === 'closed') &&\n (record.setupKind === undefined ||\n record.setupKind === 'managed-worktree' ||\n record.setupKind === 'existing-directory') &&\n (record.failureCode === undefined ||\n record.failureCode === 'SESSION_WORKTREE_PROVISION_FAILED' ||\n record.failureCode === 'SESSION_UNAVAILABLE') &&\n (record.cwd === undefined ||\n (typeof record.cwd === 'string' && path.isAbsolute(record.cwd) && !record.cwd.includes('\\0'))) &&\n (record.workspaceId === undefined || (typeof record.workspaceId === 'string' && record.workspaceId.length > 0)) &&\n (record.state !== 'bound' || (record.cwd !== undefined && record.workspaceId !== undefined))\n );\n}\n\n/**\n * Records written before setups carried a provider have none, and recovery refuses a setup it\n * cannot attribute, which left every such conversation stuck. A managed conversation worktree is\n * the only setup named `doompi-conversation-<first 12 id characters>--`, so a record reserving\n * that directory can be attributed to it. Anything else stays unattributed and refused.\n */\nfunction withLegacySetupKind(record: SessionMcpConversation): SessionMcpConversation {\n if (record.setupKind !== undefined || record.cwd === undefined) return record;\n const managedName = `doompi-conversation-${record.id.slice(0, MANAGED_WORKTREE_ID_LENGTH)}--`;\n return path.basename(record.cwd).startsWith(managedName) ? { ...record, setupKind: 'managed-worktree' } : record;\n}\n\nfunction routingKey(\n record: Pick<SessionMcpConversation, 'clientId' | 'parentSessionId' | 'conversationDigest'>,\n): string {\n return JSON.stringify([record.clientId, record.parentSessionId, record.conversationDigest]);\n}\n\n/** One owning headless host persists these bindings. Corruption never becomes an empty, reusable store. */\nexport function createSessionMcpConversationStore(stateDir: string): SessionMcpConversationStore {\n const file = path.join(stateDir, STORE_FILE);\n let failure: Error | undefined;\n let current: readonly SessionMcpConversation[] = load();\n\n function load(): readonly SessionMcpConversation[] {\n try {\n const parsed: unknown = JSON.parse(fs.readFileSync(file, 'utf8'));\n if (typeof parsed !== 'object' || parsed === null) throw new Error('Invalid store');\n const stored = parsed as { version?: unknown; bindings?: unknown };\n if (stored.version !== STORE_VERSION || !Array.isArray(stored.bindings) || !stored.bindings.every(validRecord))\n throw new Error('Invalid bindings');\n const records: SessionMcpConversation[] = stored.bindings;\n if (\n records.length > MAX_BINDINGS ||\n new Set(records.map((record) => record.id)).size !== records.length ||\n new Set(records.map(routingKey)).size !== records.length\n )\n throw new Error('Duplicate or excessive bindings');\n return Object.freeze(records.map((record) => Object.freeze(withLegacySetupKind(record))));\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];\n failure = new SessionMcpConversationError(\n 'CONVERSATION_STORE_UNAVAILABLE',\n 'Conversation bindings could not be loaded. Restore the binding store before using conversation routing.',\n );\n return [];\n }\n }\n\n function list(): readonly SessionMcpConversation[] {\n if (failure) throw failure;\n return current;\n }\n\n function persist(next: readonly SessionMcpConversation[]): void {\n list();\n const temporary = `${file}.${randomUUID()}.tmp`;\n try {\n fs.mkdirSync(stateDir, { recursive: true, mode: 0o700 });\n const descriptor = fs.openSync(temporary, 'wx', 0o600);\n try {\n fs.writeFileSync(descriptor, `${JSON.stringify({ version: STORE_VERSION, bindings: next }, null, 2)}\\n`);\n fs.fsyncSync(descriptor);\n } finally {\n fs.closeSync(descriptor);\n }\n fs.renameSync(temporary, file);\n current = Object.freeze(next.map((record) => Object.freeze(record)));\n } catch {\n throw new SessionMcpConversationError(\n 'CONVERSATION_STORE_UNAVAILABLE',\n 'Conversation bindings could not be saved. No new execution target was admitted.',\n );\n } finally {\n // This temporary file never grants access and can safely be left for local recovery.\n try {\n fs.rmSync(temporary, { force: true });\n } catch {\n /* An inaccessible temporary file is not loaded. */\n }\n }\n }\n\n function get(id: string, parentSessionId: string): SessionMcpConversation {\n const record = list().find((entry) => entry.id === id && entry.parentSessionId === parentSessionId);\n if (!record || record.state === 'closed')\n throw new SessionMcpConversationError(\n 'SESSION_UNAVAILABLE',\n 'This conversation binding is unavailable. It will not be recreated automatically.',\n );\n return record;\n }\n\n function replace(record: SessionMcpConversation): SessionMcpConversation {\n persist(list().map((entry) => (entry.id === record.id ? record : entry)));\n return get(record.id, record.parentSessionId);\n }\n\n return {\n list,\n get,\n find: (clientId, parentSessionId, digest) =>\n list().find(\n (record) =>\n record.clientId === clientId &&\n record.parentSessionId === parentSessionId &&\n record.conversationDigest === digest,\n ),\n reserve(clientId, parentSessionId, parentWorkspaceId, digest) {\n const existing = list().find(\n (record) =>\n record.clientId === clientId &&\n record.parentSessionId === parentSessionId &&\n record.conversationDigest === digest,\n );\n if (existing) return existing;\n if (list().length >= MAX_BINDINGS)\n throw new SessionMcpConversationError(\n 'CONVERSATION_LIMIT_REACHED',\n 'The conversation binding limit has been reached. Use a dedicated session connection.',\n );\n const record: SessionMcpConversation = {\n id: randomUUID(),\n clientId,\n parentSessionId,\n parentWorkspaceId,\n conversationDigest: digest,\n createdAt: new Date().toISOString(),\n state: 'pending',\n };\n if (!validRecord(record))\n throw new SessionMcpConversationError('INVALID_CONVERSATION', 'Invalid conversation identity.');\n persist([...list(), record]);\n return get(record.id, parentSessionId);\n },\n selectSetup(id, parentSessionId, kind) {\n const record = get(id, parentSessionId);\n if (record.setupKind === kind)\n return record.failureCode === undefined ? record : replace({ ...record, failureCode: undefined });\n if (record.setupKind !== undefined && (record.cwd !== undefined || record.state === 'bound'))\n throw new SessionMcpConversationError(\n 'SESSION_TARGET_FIXED',\n 'An initialized setup cannot change its provider.',\n id,\n );\n if (record.setupKind === undefined && record.cwd !== undefined)\n throw new SessionMcpConversationError(\n 'SESSION_TARGET_FIXED',\n 'This existing setup has no verified provider. Recover it without changing its ownership.',\n id,\n );\n return replace({ ...record, setupKind: kind, failureCode: undefined });\n },\n prepare(id, parentSessionId, cwd) {\n const record = get(id, parentSessionId);\n if (!path.isAbsolute(cwd) || cwd.includes('\\0'))\n throw new Error('A canonical absolute execution directory is required.');\n if (record.cwd !== undefined && record.cwd !== cwd)\n throw new SessionMcpConversationError(\n 'SESSION_TARGET_FIXED',\n 'This conversation already reserved a different execution directory. Recover that setup instead.',\n id,\n );\n if (record.cwd === cwd) return record;\n return replace({ ...record, cwd });\n },\n fail(id, parentSessionId, code) {\n const record = get(id, parentSessionId);\n if (record.state !== 'pending' || record.failureCode === code) return record;\n return replace({ ...record, failureCode: code });\n },\n bind(id, parentSessionId, workspaceId) {\n const record = get(id, parentSessionId);\n if (!record.cwd || !workspaceId) throw new Error('A prepared directory and admitted workspace are required.');\n if (record.state === 'bound') {\n if (record.workspaceId !== workspaceId) throw new Error('The bound workspace cannot change.');\n return record;\n }\n return replace({ ...record, state: 'bound', workspaceId, failureCode: undefined });\n },\n recover(id, parentSessionId) {\n const record = list().find((entry) => entry.id === id && entry.parentSessionId === parentSessionId);\n if (!record)\n throw new SessionMcpConversationError('SESSION_UNAVAILABLE', 'This conversation binding is unavailable.');\n return replace({ ...record, state: 'pending' });\n },\n closeSession(sessionId) {\n const next = list().map((record): SessionMcpConversation =>\n record.id === sessionId || record.parentSessionId === sessionId ? { ...record, state: 'closed' } : record,\n );\n if (next.some((record, index) => record !== current[index])) persist(next);\n },\n closeClient(clientId) {\n const next = list().map((record): SessionMcpConversation =>\n record.clientId === clientId ? { ...record, state: 'closed' } : record,\n );\n if (next.some((record, index) => record !== current[index])) persist(next);\n },\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-core",
3
- "version": "0.0.1-alpha.97",
3
+ "version": "0.0.1-alpha.98",
4
4
  "description": "Doompi runtime systems and reusable core services.",
5
5
  "keywords": [
6
6
  "ai",