negotium 0.3.6 → 0.3.7
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/agent-helpers.js +2 -2
- package/dist/agent-helpers.js.map +2 -2
- package/dist/hosted-agent.js +2 -2
- package/dist/hosted-agent.js.map +2 -2
- package/dist/main.js +276 -27
- package/dist/main.js.map +10 -9
- package/dist/mcp-factories.js +2 -2
- package/dist/mcp-factories.js.map +2 -2
- package/dist/registry.js +2 -2
- package/dist/registry.js.map +2 -2
- package/dist/runtime/src/application/submit-runtime-gateway-turn.ts +47 -22
- package/dist/runtime/src/index.ts +2 -0
- package/dist/runtime/src/node-host.ts +3 -1
- package/dist/runtime/src/topics/update.ts +175 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/package.json +1 -1
package/dist/registry.js
CHANGED
|
@@ -122,7 +122,7 @@ import { createRequire } from "module";
|
|
|
122
122
|
import { dirname, join as join2 } from "path";
|
|
123
123
|
|
|
124
124
|
// ../../packages/core/src/version.ts
|
|
125
|
-
var NEGOTIUM_VERSION = "0.3.
|
|
125
|
+
var NEGOTIUM_VERSION = "0.3.7";
|
|
126
126
|
|
|
127
127
|
// ../../packages/core/src/agents/codex-native-multi-agent.ts
|
|
128
128
|
var moduleRequire = createRequire(import.meta.url);
|
|
@@ -480,4 +480,4 @@ export {
|
|
|
480
480
|
getRegistry2 as getRegistry
|
|
481
481
|
};
|
|
482
482
|
|
|
483
|
-
//# debugId=
|
|
483
|
+
//# debugId=AC7AEECE8CBADCC964756E2164756E21
|
package/dist/registry.js.map
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
"import { existsSync, unlinkSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { forkCodexSession } from \"#agents/codex-app-server\";\nimport type { AgentRegistry, AgentRegistryOperations } from \"#agents/contracts\";\nimport { hostedCodexHomePath } from \"#agents/execution-host\";\nimport { writeCodexRollout } from \"#agents/rollout/codex\";\nimport { logger } from \"#platform/logger\";\nimport { CODEX_EFFORT_VALUES, type EffortLevel } from \"#types\";\n\nconst VALID_EFFORTS = new Set<EffortLevel>(CODEX_EFFORT_VALUES);\n\n// Codex CLI's own empirical default is gpt-5.6-sol (`codex exec` 2026-07-10 →\n// \"model: gpt-5.6-sol\"), but we deliberately default to gpt-5.6-luna — the\n// cheapest/fastest GPT-5.6 tier — for a general always-on assistant where most\n// queries are light. Heavier work escalates to terra/sol via set_model. This\n// value is passed explicitly to the SDK (see event-processor resolveDefaultModel),\n// so the footer and the actual model stay in sync.\nexport const codexRegistry: AgentRegistry = {\n kind: \"codex\",\n defaultModel: \"gpt-5.6-luna\",\n // defaultEffort intentionally omitted — Codex SDK treats absence as\n // \"reasoning off\". Setting \"high\"/etc. would silently flip on reasoning.\n\n expandModelAlias(s) {\n return s;\n },\n\n validateModel(s) {\n // Codex doesn't publish a closed model list and OpenAI ships new IDs\n // (gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, o3, ...) frequently. Best-effort: accept any\n // non-empty string. Bad IDs surface at SDK call time with a clear error.\n return typeof s === \"string\" && s.length > 0;\n },\n\n validEfforts: CODEX_EFFORT_VALUES,\n validateEffort(s) {\n // GPT-5.6 accepts low/medium/high/xhigh/max; 'minimal'\n // was removed because the Codex API rejects it when default tools\n // (image_gen, web_search) are active.\n return VALID_EFFORTS.has(s);\n },\n\n footerLabel(model, effort) {\n // Codex omits effort to mean \"reasoning off\". Show `(off)` explicitly so\n // the user can distinguish from claude (which always has a default).\n return `${model} · ${effort ?? \"(off)\"}`;\n },\n};\n\nexport const codexRegistryOperations: AgentRegistryOperations = {\n writeRollout(opts) {\n // Codex SDK exposes the resume key as `threadId`; AgentRegistry unifies\n // the name to `sessionId` so callers don't branch on agent.\n // `reuseSessionId` (if any) is forwarded as `threadId` so claude→codex→claude\n // round-trips also keep one continuous codex thread instead of orphaning a\n // fresh uuidv7 on every switch.\n const { threadId, rolloutPath } = writeCodexRollout({\n cwd: opts.cwd,\n entries: opts.entries,\n model: opts.model ?? codexRegistry.defaultModel,\n ...(opts.effort ? { effort: opts.effort } : {}),\n ...(opts.reuseSessionId ? { threadId: opts.reuseSessionId } : {}),\n });\n return { sessionId: threadId, rolloutPath };\n },\n\n // The TypeScript SDK does not expose forking yet, but the bundled Codex App\n // Server does. Native thread/fork preserves the provider's stored prefix,\n // including tool structure, which gives prompt caching the best chance to\n // reuse the parent context. Callers retain unified-log synthesis as fallback.\n async forkSession({ parentSessionId }) {\n return await forkCodexSession(parentSessionId);\n },\n\n // Codex stores rollouts at `~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-<ts>-<threadId>.jsonl`.\n // The date prefix is unknown at cleanup time (it's the *original* write\n // timestamp, not \"now\"), so we glob across the whole sessions tree by\n // threadId suffix. With at most a few thousand files in active use this\n // is well under a millisecond on a warm filesystem.\n async cleanupRollouts({ sessionIds }) {\n if (sessionIds.length === 0) return;\n const sessionsDir = join(hostedCodexHomePath(), \"sessions\");\n // No sessions directory means there are no provider rollouts left to remove.\n if (!existsSync(sessionsDir)) return;\n const failures: unknown[] = [];\n // One Glob per threadId so a single corrupt entry can't poison the rest.\n // Bun.Glob's `scan` yields paths relative to its base dir.\n for (const tid of sessionIds) {\n try {\n const glob = new Bun.Glob(`**/rollout-*-${tid}.jsonl`);\n for await (const rel of glob.scan({ cwd: sessionsDir, onlyFiles: true })) {\n const path = join(sessionsDir, rel);\n try {\n unlinkSync(path);\n } catch (e) {\n if ((e as NodeJS.ErrnoException)?.code !== \"ENOENT\") {\n logger.warn({ err: e, path }, \"codex cleanupRollouts: unlink failed\");\n failures.push(e);\n }\n }\n }\n } catch (e) {\n logger.warn({ err: e, threadId: tid }, \"codex cleanupRollouts: scan failed\");\n failures.push(e);\n }\n }\n if (failures.length > 0) {\n throw new AggregateError(failures, \"codex cleanupRollouts failed\");\n }\n },\n};\n",
|
|
7
7
|
"import { type ChildProcessWithoutNullStreams, spawn } from \"node:child_process\";\nimport { codexCliScriptPath } from \"#agents/codex-native-multi-agent\";\nimport { hostedCodexHomePath } from \"#agents/execution-host\";\nimport { latestCodexRolloutPath } from \"#agents/rollout/codex\";\nimport { NEGOTIUM_VERSION } from \"#version\";\n\ninterface CodexAppServerForkResult {\n forkId: string;\n rolloutPath: string;\n}\n\ninterface CodexAppServerForkHost {\n spawnServer(): ChildProcessWithoutNullStreams;\n findRolloutPath(threadId: string): string | undefined;\n timeoutMs: number;\n}\n\ntype JsonRpcResponse = {\n id?: number;\n result?: { thread?: { id?: unknown } };\n error?: { message?: unknown };\n};\n\nexport function createCodexAppServerForker(host: CodexAppServerForkHost) {\n return async (parentThreadId: string): Promise<CodexAppServerForkResult> => {\n const child = host.spawnServer();\n\n return await new Promise<CodexAppServerForkResult>((resolve, reject) => {\n let settled = false;\n let stdoutBuffer = \"\";\n let stderr = \"\";\n const timer = setTimeout(\n () => finish(new Error(\"Codex thread fork timed out\")),\n host.timeoutMs,\n );\n\n const finish = (error?: Error, result?: CodexAppServerForkResult) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n try {\n child.stdin.end();\n child.kill();\n } catch {\n // The app server may already have exited after stdin closed.\n }\n if (error) reject(error);\n else if (result) resolve(result);\n else reject(new Error(\"Codex thread fork returned no result\"));\n };\n\n const send = (message: Record<string, unknown>) => {\n child.stdin.write(`${JSON.stringify(message)}\\n`);\n };\n\n child.stderr.on(\"data\", (chunk) => {\n if (stderr.length < 8_192) stderr += String(chunk);\n });\n child.on(\"error\", (error) => finish(error));\n child.on(\"exit\", (code, signal) => {\n if (settled) return;\n const detail = signal ? `signal ${signal}` : `code ${code ?? 1}`;\n finish(\n new Error(\n `Codex app server exited with ${detail}${stderr.trim() ? `: ${stderr.trim()}` : \"\"}`,\n ),\n );\n });\n child.stdout.on(\"data\", (chunk) => {\n stdoutBuffer += String(chunk);\n for (;;) {\n const newline = stdoutBuffer.indexOf(\"\\n\");\n if (newline < 0) break;\n const line = stdoutBuffer.slice(0, newline);\n stdoutBuffer = stdoutBuffer.slice(newline + 1);\n let message: JsonRpcResponse;\n try {\n message = JSON.parse(line) as JsonRpcResponse;\n } catch {\n continue;\n }\n\n if (message.id === 1) {\n if (message.error) {\n finish(new Error(String(message.error.message || \"Codex initialization failed\")));\n return;\n }\n send({ method: \"initialized\" });\n send({ id: 2, method: \"thread/fork\", params: { threadId: parentThreadId } });\n continue;\n }\n if (message.id !== 2) continue;\n if (message.error) {\n finish(new Error(String(message.error.message || \"Codex thread fork failed\")));\n return;\n }\n const forkId = message.result?.thread?.id;\n if (typeof forkId !== \"string\" || !forkId) {\n finish(new Error(\"Codex thread fork returned no thread id\"));\n return;\n }\n const rolloutPath = host.findRolloutPath(forkId);\n if (!rolloutPath) {\n finish(new Error(`Codex thread fork rollout was not found for ${forkId}`));\n return;\n }\n finish(undefined, { forkId, rolloutPath });\n return;\n }\n });\n\n send({\n id: 1,\n method: \"initialize\",\n params: {\n clientInfo: { name: \"negotium\", version: NEGOTIUM_VERSION },\n },\n });\n });\n };\n}\n\nconst forkCodexThread = createCodexAppServerForker({\n spawnServer() {\n return spawn(process.execPath, [codexCliScriptPath(), \"app-server\", \"--stdio\"], {\n env: { ...process.env, CODEX_HOME: hostedCodexHomePath() },\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n },\n findRolloutPath: latestCodexRolloutPath,\n timeoutMs: 15_000,\n});\n\nexport async function forkCodexSession(parentThreadId: string): Promise<CodexAppServerForkResult> {\n return await forkCodexThread(parentThreadId);\n}\n",
|
|
8
8
|
"import { spawn } from \"node:child_process\";\nimport { randomUUID } from \"node:crypto\";\nimport {\n chmodSync,\n copyFileSync,\n existsSync,\n mkdtempSync,\n readFileSync,\n renameSync,\n rmSync,\n unlinkSync,\n writeFileSync,\n} from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { tmpdir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { NEGOTIUM_VERSION } from \"#version\";\n\ntype CodexModel = Record<string, unknown>;\ntype CodexModelCache = {\n client_version?: unknown;\n models?: unknown;\n};\n\nconst moduleRequire = createRequire(import.meta.url);\nconst codexSdkPackagePath = moduleRequire.resolve(\"@openai/codex-sdk/package.json\");\nconst codexSdkRequire = createRequire(codexSdkPackagePath);\nconst bundledCodexPackagePath = codexSdkRequire.resolve(\"@openai/codex/package.json\");\n\nfunction readPackageVersion(packageJsonPath: string): string {\n const parsed = JSON.parse(readFileSync(packageJsonPath, \"utf8\")) as { version?: unknown };\n if (typeof parsed.version !== \"string\" || !parsed.version.trim()) {\n throw new Error(`Codex package has no valid version: ${packageJsonPath}`);\n }\n return parsed.version;\n}\n\nexport const BUNDLED_CODEX_VERSION = readPackageVersion(bundledCodexPackagePath);\nconst SAFE_BUNDLED_CODEX_VERSION = BUNDLED_CODEX_VERSION.replace(/[^a-zA-Z0-9._-]/g, \"_\");\nconst NEGOTIUM_MODEL_CACHE = `negotium-models-cache-${SAFE_BUNDLED_CODEX_VERSION}.json`;\nconst NEGOTIUM_MODEL_CATALOG = `negotium-model-catalog-${SAFE_BUNDLED_CODEX_VERSION}.json`;\n\nexport function codexCliScriptPath(): string {\n return join(dirname(bundledCodexPackagePath), \"bin\", \"codex.js\");\n}\n\nfunction parseCodexModelCache(contents: string, sourcePath: string): CodexModelCache {\n let parsed: CodexModelCache;\n try {\n parsed = JSON.parse(contents) as CodexModelCache;\n } catch (error) {\n throw new Error(`Codex model cache is invalid JSON: ${sourcePath}`, { cause: error });\n }\n if (!Array.isArray(parsed.models) || parsed.models.length === 0) {\n throw new Error(`Codex model cache has no models: ${sourcePath}`);\n }\n return parsed;\n}\n\nfunction readCodexModelCache(cachePath: string): {\n contents: string;\n parsed: CodexModelCache;\n} {\n const contents = readFileSync(cachePath, \"utf8\");\n return { contents, parsed: parseCodexModelCache(contents, cachePath) };\n}\n\nfunction readCompatibleCodexModelCache(cachePath: string): {\n contents: string;\n parsed: CodexModelCache;\n} {\n const cache = readCodexModelCache(cachePath);\n if (cache.parsed.client_version !== BUNDLED_CODEX_VERSION) {\n const found =\n typeof cache.parsed.client_version === \"string\"\n ? cache.parsed.client_version\n : \"missing or invalid\";\n throw new Error(\n `Codex model cache version ${found} does not match Negotium's bundled Codex ${BUNDLED_CODEX_VERSION}: ${cachePath}`,\n );\n }\n return cache;\n}\n\nfunction writePrivateFileAtomic(path: string, contents: string): void {\n if (existsSync(path) && readFileSync(path, \"utf8\") === contents) return;\n\n const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;\n try {\n writeFileSync(tempPath, contents, { encoding: \"utf8\", mode: 0o600 });\n renameSync(tempPath, path);\n chmodSync(path, 0o600);\n } finally {\n try {\n unlinkSync(tempPath);\n } catch {\n // renameSync normally consumed the temporary file.\n }\n }\n}\n\nexport function bundledCodexModelCachePath(authFilePath: string): string {\n return join(dirname(authFilePath), NEGOTIUM_MODEL_CACHE);\n}\n\nasync function bootstrapCodexModelCache(codexHome: string, cachePath: string): Promise<void> {\n const child = spawn(process.execPath, [codexCliScriptPath(), \"app-server\", \"--stdio\"], {\n env: { ...process.env, CODEX_HOME: codexHome },\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n });\n\n await new Promise<void>((resolve, reject) => {\n let settled = false;\n let stdoutBuffer = \"\";\n let stderr = \"\";\n const timer = setTimeout(\n () => finish(new Error(\"timed out while refreshing the Codex model catalog\")),\n 15_000,\n );\n\n const finish = (error?: Error) => {\n if (settled) return;\n settled = true;\n clearTimeout(timer);\n try {\n child.stdin.end();\n child.kill();\n } catch {\n // The app server may already have exited after stdin closed.\n }\n if (error) reject(error);\n else if (!existsSync(cachePath)) reject(new Error(\"Codex did not create its model cache\"));\n else resolve();\n };\n\n const send = (message: Record<string, unknown>) => {\n child.stdin.write(`${JSON.stringify(message)}\\n`);\n };\n\n child.stderr.on(\"data\", (chunk) => {\n if (stderr.length < 4_096) stderr += String(chunk);\n });\n child.on(\"error\", (error) => finish(error));\n child.on(\"exit\", (code, signal) => {\n if (!settled) {\n finish(\n new Error(\n `Codex model catalog refresh exited with ${signal ? `signal ${signal}` : `code ${code ?? 1}`}${stderr.trim() ? `: ${stderr.trim()}` : \"\"}`,\n ),\n );\n }\n });\n child.stdout.on(\"data\", (chunk) => {\n stdoutBuffer += String(chunk);\n for (;;) {\n const newline = stdoutBuffer.indexOf(\"\\n\");\n if (newline < 0) break;\n const line = stdoutBuffer.slice(0, newline);\n stdoutBuffer = stdoutBuffer.slice(newline + 1);\n let message: { id?: number; error?: { message?: string } };\n try {\n message = JSON.parse(line) as typeof message;\n } catch {\n continue;\n }\n if (message.id === 1) {\n if (message.error) {\n finish(new Error(message.error.message || \"Codex initialization failed\"));\n return;\n }\n send({ method: \"initialized\" });\n send({ id: 2, method: \"model/list\", params: { includeHidden: true } });\n } else if (message.id === 2) {\n if (message.error) {\n finish(new Error(message.error.message || \"Codex model listing failed\"));\n } else {\n finish();\n }\n return;\n }\n }\n });\n\n send({\n id: 1,\n method: \"initialize\",\n params: {\n clientInfo: { name: \"negotium\", version: NEGOTIUM_VERSION },\n capabilities: { experimentalApi: true },\n },\n });\n });\n}\n\nasync function bootstrapIsolatedCodexModelCache(\n authFilePath: string,\n bootstrap: (codexHome: string, cachePath: string) => Promise<void>,\n): Promise<string> {\n const sourceHome = dirname(authFilePath);\n const isolatedHome = mkdtempSync(join(tmpdir(), \"negotium-codex-models-\"));\n const isolatedCachePath = join(isolatedHome, \"models_cache.json\");\n\n try {\n const isolatedAuthPath = join(isolatedHome, \"auth.json\");\n copyFileSync(authFilePath, isolatedAuthPath);\n chmodSync(isolatedAuthPath, 0o600);\n\n // Preserve custom provider configuration while keeping the bundled CLI's\n // cache write completely outside the user's shared CODEX_HOME.\n const sourceConfigPath = join(sourceHome, \"config.toml\");\n if (existsSync(sourceConfigPath)) {\n const isolatedConfigPath = join(isolatedHome, \"config.toml\");\n copyFileSync(sourceConfigPath, isolatedConfigPath);\n chmodSync(isolatedConfigPath, 0o600);\n }\n\n await bootstrap(isolatedHome, isolatedCachePath);\n return readCompatibleCodexModelCache(isolatedCachePath).contents;\n } finally {\n rmSync(isolatedHome, { recursive: true, force: true });\n }\n}\n\nexport async function ensureCodexModelCache(\n authFilePath: string,\n bootstrap: (codexHome: string, cachePath: string) => Promise<void> = bootstrapCodexModelCache,\n): Promise<string> {\n const codexHome = dirname(authFilePath);\n const configuredCachePath = process.env.NEGOTIUM_CODEX_MODELS_CACHE_FILE;\n if (configuredCachePath) {\n if (!existsSync(configuredCachePath)) {\n throw new Error(`Configured Codex model cache does not exist: ${configuredCachePath}`);\n }\n readCompatibleCodexModelCache(configuredCachePath);\n return configuredCachePath;\n }\n\n // The global Codex CLI owns models_cache.json and may update it to a schema\n // newer than the SDK bundled by Negotium. Snapshot a cache generated for our\n // exact bundled version so later global CLI updates cannot break turns.\n const bundledCachePath = bundledCodexModelCachePath(authFilePath);\n const sharedCachePath = join(codexHome, \"models_cache.json\");\n if (existsSync(sharedCachePath)) {\n try {\n const shared = readCompatibleCodexModelCache(sharedCachePath);\n // Keep model metadata fresh while the global CLI remains compatible.\n writePrivateFileAtomic(bundledCachePath, shared.contents);\n return bundledCachePath;\n } catch {\n // A compatible private snapshot is safer than failing because another\n // process briefly exposed an incomplete shared-cache write.\n }\n }\n\n if (existsSync(bundledCachePath)) {\n try {\n readCompatibleCodexModelCache(bundledCachePath);\n return bundledCachePath;\n } catch {\n // Re-bootstrap below instead of passing a corrupt or version-mismatched\n // private snapshot into this SDK version.\n }\n }\n\n const refreshedContents = await bootstrapIsolatedCodexModelCache(authFilePath, bootstrap);\n writePrivateFileAtomic(bundledCachePath, refreshedContents);\n return bundledCachePath;\n}\n\n/**\n * Codex can resolve model metadata before `features.multi_agent=false`, so a\n * model-advertised v1/v2 value may still register native collaboration tools.\n * Feed Codex an authoritative copy of its own catalog with only that field\n * disabled. Runtime MCP delegation remains available independently.\n */\nexport function writeCodexCatalogWithNativeMultiAgentDisabled(\n authFilePath: string,\n sourcePath: string,\n): string {\n const codexHome = dirname(authFilePath);\n const outputPath = join(codexHome, NEGOTIUM_MODEL_CATALOG);\n\n const parsed = readCodexModelCache(sourcePath).parsed;\n\n const models = (parsed.models as unknown[]).map((model, index): CodexModel => {\n if (!model || typeof model !== \"object\" || Array.isArray(model)) {\n throw new Error(`Codex model cache entry ${index} is invalid: ${sourcePath}`);\n }\n return { ...(model as CodexModel), multi_agent_version: \"disabled\" };\n });\n const contents = `${JSON.stringify({ models }, null, 2)}\\n`;\n writePrivateFileAtomic(outputPath, contents);\n return outputPath;\n}\n",
|
|
9
|
-
"export const NEGOTIUM_VERSION = \"0.3.
|
|
9
|
+
"export const NEGOTIUM_VERSION = \"0.3.7\";\n",
|
|
10
10
|
"import { randomUUID } from \"node:crypto\";\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\nimport type { AgentRegistry, AgentRegistryOperations } from \"#agents/contracts\";\nimport { assertUuidLike, ensureCwdExists, extractChatPairs } from \"#agents/rollout/shared\";\nimport { logger } from \"#platform/logger\";\nimport { MAESTRO_EFFORT_VALUES } from \"#types\";\n\nconst ALIAS_MAP: Record<string, string> = {\n \"deepseek-pro\": \"deepseek-v4-pro\",\n // \"deepseek-flash\" was disabled in 0.1.25 because DeepSeek had retired its\n // old flash model. \"DeepSeek-V4-Flash-0731\" (released 2026-07-31) is an\n // unrelated, currently-live model reusing a similar name — verified with a\n // live API call before re-enabling this alias.\n \"deepseek-flash\": \"deepseek-v4-flash\",\n kimi: \"kimi-k3\",\n \"kimi-pro\": \"kimi-k3\",\n \"kimi-code\": \"kimi-k2.7-code\",\n};\nconst VALID_MODELS = new Set([...Object.keys(ALIAS_MAP), ...Object.values(ALIAS_MAP)]);\nconst VALID_EFFORTS = new Set(MAESTRO_EFFORT_VALUES);\n\nexport const maestroRegistry: AgentRegistry = {\n kind: \"maestro\",\n defaultModel: \"deepseek-pro\",\n defaultEffort: \"medium\",\n expandModelAlias(model) {\n return ALIAS_MAP[model] ?? model;\n },\n validateModel(model) {\n return VALID_MODELS.has(model);\n },\n validEfforts: MAESTRO_EFFORT_VALUES,\n validateEffort(effort) {\n return VALID_EFFORTS.has(effort);\n },\n footerLabel(model, effort) {\n return effort ? `${model} · ${effort}` : model;\n },\n};\n\nfunction maestroSessionsDir(): string {\n return join(\n process.env.MAESTRO_DATA_DIR\n ? resolve(process.env.MAESTRO_DATA_DIR)\n : join(homedir(), \".maestro\"),\n \"sessions\",\n );\n}\n\nfunction maestroSessionPath(sessionId: string): string {\n return join(maestroSessionsDir(), `${sessionId}.jsonl`);\n}\n\nfunction maestroActiveSessionPath(sessionId: string): string {\n return join(maestroSessionsDir(), `${sessionId}.active.jsonl`);\n}\n\nfunction existingCreatedAt(path: string): string | undefined {\n if (!existsSync(path)) return undefined;\n try {\n const firstLine = readFileSync(path, \"utf8\").split(\"\\n\", 1)[0];\n const parsed = JSON.parse(firstLine) as { _meta?: { createdAt?: unknown } };\n return typeof parsed._meta?.createdAt === \"string\" ? parsed._meta.createdAt : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Placeholder for a synthesized turn whose text is blank.\n *\n * Moonshot (Kimi) hard-rejects the *whole* request when any history message has\n * empty content — `Kimi API 400: the message at position N with role 'user'\n * must not be empty` (verified against the live API, kimi-k3). DeepSeek's\n * validator is laxer today but the same shape is not worth relying on.\n *\n * maestro-agent-sdk 0.2.1 drops empty assistant slots, but only in the\n * *content-block* form (`content: []`); the pair encoder below writes plain\n * strings, and `content: \"\"` still reaches the wire and 400s. Blank sides are\n * reachable here from real conversations — an attachment-only user submission\n * records a `user_message` with `content: \"\"`, and `renderUserPromptBatch`\n * passes it through verbatim — so the synthesized session must not contain one.\n *\n * We substitute rather than drop: dropping a pair would silently delete a turn\n * from the historical narrative the next agent reads, while a one-line marker\n * keeps the turn boundary and tells the model why the slot is thin.\n */\nconst BLANK_TURN_PLACEHOLDER = \"(no text content in this turn)\";\n\nfunction nonEmptyTurnText(text: string): string {\n return text.trim().length > 0 ? text : BLANK_TURN_PLACEHOLDER;\n}\n\nfunction writeRollout(options: Parameters<AgentRegistryOperations[\"writeRollout\"]>[0]) {\n const sessionId = options.reuseSessionId ?? randomUUID();\n assertUuidLike(\"sessionId\", sessionId);\n ensureCwdExists(options.cwd);\n const path = maestroSessionPath(sessionId);\n mkdirSync(maestroSessionsDir(), { recursive: true });\n const messages = extractChatPairs(options.entries).flatMap((pair) => [\n { role: \"user\", content: nonEmptyTurnText(pair.userText) },\n { role: \"assistant\", content: nonEmptyTurnText(pair.assistantText) },\n ]);\n const lines = [\n {\n _meta: {\n version: 1,\n cwd: options.cwd,\n createdAt: existingCreatedAt(path) ?? new Date().toISOString(),\n // Deliberately a literal, not the SDK's `MAESTRO_SDK_VERSION`: this\n // module must not statically import maestro-agent-sdk (asserted by\n // tests/core/daemon-import-boundaries.test.ts — the SDK stays off the\n // daemon startup path). The value names the session *format* version\n // this encoder targets, which is what a reader of the header needs.\n sdkVersion: \"0.2.0\",\n },\n },\n ...messages,\n ];\n writeFileSync(path, `${lines.map((line) => JSON.stringify(line)).join(\"\\n\")}\\n`, {\n mode: 0o600,\n });\n return { sessionId, rolloutPath: path };\n}\n\nexport const maestroRegistryOperations: AgentRegistryOperations = {\n writeRollout(options) {\n return writeRollout(options);\n },\n async forkSession(options) {\n // Fork the full raw history while preserving the compacted working view.\n // A compacted parent that cannot copy its active projection is not a usable\n // cache-preserving fork, so let the caller fall back to bounded synthesis.\n const { deleteMaestroSession, forkSessionAt, loadRawMaestroSession } = await import(\n \"maestro-agent-sdk\"\n );\n const parentMessages = loadRawMaestroSession(options.parentSessionId);\n if (!parentMessages) {\n throw new Error(`Maestro parent session not found: ${options.parentSessionId}`);\n }\n const parentHasActiveProjection = existsSync(maestroActiveSessionPath(options.parentSessionId));\n const fork = forkSessionAt({\n parentSessionId: options.parentSessionId,\n messageIndex: parentMessages.length,\n cwd: options.cwd,\n userId: String(options.userId),\n });\n if (parentHasActiveProjection && !fork.activeProjectionForked) {\n deleteMaestroSession(fork.sessionId);\n throw new Error(`Maestro active projection could not be forked: ${options.parentSessionId}`);\n }\n const activePath = maestroActiveSessionPath(fork.sessionId);\n return {\n forkId: fork.sessionId,\n rolloutPath: fork.rolloutPath,\n ...(existsSync(activePath) ? { cleanupPaths: [activePath] } : {}),\n };\n },\n async cleanupRollouts(options) {\n // Keep the SDK off the daemon startup path, but use its canonical cleanup\n // once a Maestro session is actually being removed. It also clears memory,\n // task, todo, and in-process file-state sidecars.\n const { deleteMaestroSession } = await import(\"maestro-agent-sdk\");\n const failures: unknown[] = [];\n for (const sessionId of options.sessionIds) {\n try {\n deleteMaestroSession(sessionId);\n const remaining = [\n maestroSessionPath(sessionId),\n maestroActiveSessionPath(sessionId),\n ].filter(existsSync);\n if (remaining.length > 0) {\n throw new Error(`Maestro session files remain after cleanup: ${remaining.join(\", \")}`);\n }\n } catch (error) {\n logger.warn({ err: error, sessionId }, \"maestro cleanupRollouts: cleanup failed\");\n failures.push(error);\n }\n }\n if (failures.length > 0) {\n throw new AggregateError(failures, \"Maestro rollout cleanup failed\");\n }\n },\n};\n",
|
|
11
11
|
"import { claudeRegistry, claudeRegistryOperations } from \"#agents/claude-registry\";\nimport { codexRegistry, codexRegistryOperations } from \"#agents/codex-registry\";\nimport type { AgentRegistry, AgentRegistryOperations } from \"#agents/contracts\";\nimport { maestroRegistry, maestroRegistryOperations } from \"#agents/maestro-registry\";\nimport type { AgentKind } from \"#types\";\n\nexport type {\n AgentRegistry,\n AgentRegistryOperations,\n CleanupRolloutsOptions,\n ForkRegistryOptions,\n ForkRegistryResult,\n WriteRolloutOptions,\n WriteRolloutResult,\n} from \"#agents/contracts\";\n\nconst REGISTRIES: Record<AgentKind, AgentRegistry> = {\n claude: claudeRegistry,\n codex: codexRegistry,\n maestro: maestroRegistry,\n};\n\nexport function getRegistry(agent: AgentKind): AgentRegistry {\n return REGISTRIES[agent];\n}\n\nconst OPERATIONS: Record<AgentKind, AgentRegistryOperations> = {\n claude: claudeRegistryOperations,\n codex: codexRegistryOperations,\n maestro: maestroRegistryOperations,\n};\n\nexport function getRegistryOperations(agent: AgentKind): AgentRegistryOperations {\n return OPERATIONS[agent];\n}\n",
|
|
12
12
|
"import {\n type AgentRegistry,\n type AgentRegistryOperations,\n type CleanupRolloutsOptions,\n type ForkRegistryOptions,\n type ForkRegistryResult,\n getRegistry as resolveCoreRegistry,\n getRegistryOperations as resolveCoreRegistryOperations,\n type WriteRolloutOptions,\n type WriteRolloutResult,\n} from \"@negotium/core/registry\";\n\nexport type {\n AgentRegistry,\n AgentRegistryOperations,\n CleanupRolloutsOptions,\n ForkRegistryOptions,\n ForkRegistryResult,\n WriteRolloutOptions,\n WriteRolloutResult,\n};\n\nexport const getRegistry: typeof resolveCoreRegistry = (agent) => resolveCoreRegistry(agent);\n\nexport const getRegistryOperations: typeof resolveCoreRegistryOperations = (agent) =>\n resolveCoreRegistryOperations(agent);\n"
|
|
13
13
|
],
|
|
14
14
|
"mappings": "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",
|
|
15
|
-
"debugId": "
|
|
15
|
+
"debugId": "AC7AEECE8CBADCC964756E2164756E21",
|
|
16
16
|
"names": []
|
|
17
17
|
}
|
|
@@ -27,6 +27,15 @@ export interface SubmitRuntimeGatewayTurnParams {
|
|
|
27
27
|
clientMessageId: string;
|
|
28
28
|
requestId?: string;
|
|
29
29
|
allowAutoContinue?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Whether this message should also run the AI. Defaults to true.
|
|
32
|
+
*
|
|
33
|
+
* A host whose room has the AI removed, or set to mention-only, still needs
|
|
34
|
+
* the message in the canonical transcript — otherwise Terminal and Telegram
|
|
35
|
+
* see a room with holes in it. `false` records the message and acknowledges
|
|
36
|
+
* it exactly as usual, and only declines to queue the turn.
|
|
37
|
+
*/
|
|
38
|
+
respond?: boolean;
|
|
30
39
|
/** Answer inside this thread instead of the room's main flow (S-13). */
|
|
31
40
|
threadRootId?: string;
|
|
32
41
|
}
|
|
@@ -97,6 +106,11 @@ function gatewayPayloadHash(
|
|
|
97
106
|
params.clientMessageId,
|
|
98
107
|
requestId,
|
|
99
108
|
params.allowAutoContinue ?? true,
|
|
109
|
+
// Whether the message ran the AI is part of the turn's identity, not a
|
|
110
|
+
// presentation detail: replaying the same key with `respond` flipped
|
|
111
|
+
// would otherwise reuse the silent ACK and never queue the turn (or
|
|
112
|
+
// vice versa), with nothing to tell the caller it was ignored.
|
|
113
|
+
params.respond ?? true,
|
|
100
114
|
// Part of the identity of the turn: the same key asked in the channel
|
|
101
115
|
// and in a thread are different turns, and replaying one as the other
|
|
102
116
|
// would answer in the wrong place.
|
|
@@ -130,6 +144,7 @@ export function submitRuntimeGatewayTurn(
|
|
|
130
144
|
): SubmitRuntimeGatewayTurnResult {
|
|
131
145
|
const requestId = params.requestId ?? params.clientMessageId;
|
|
132
146
|
const actorUserId = params.actorUserId ?? params.userId;
|
|
147
|
+
const respond = params.respond ?? true;
|
|
133
148
|
const payloadHash = gatewayPayloadHash(params, requestId, actorUserId);
|
|
134
149
|
const existing = findRuntimeGatewaySubmission(params.clientMessageId, requestId);
|
|
135
150
|
if (existing) {
|
|
@@ -163,28 +178,34 @@ export function submitRuntimeGatewayTurn(
|
|
|
163
178
|
try {
|
|
164
179
|
db.transaction(() => {
|
|
165
180
|
appendApiMessage(message, { notify: false });
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
181
|
+
// The only thing `respond: false` skips. Everything else — the canonical
|
|
182
|
+
// message, the accepted event, the idempotency record — still happens, so
|
|
183
|
+
// a silent message is indistinguishable from a normal one everywhere
|
|
184
|
+
// except that no turn is queued for it.
|
|
185
|
+
if (respond) {
|
|
186
|
+
mergeRuntimeUserTurnRequest({
|
|
187
|
+
topicId: params.topic.id,
|
|
188
|
+
userId: params.userId,
|
|
189
|
+
userMessages: [
|
|
190
|
+
{
|
|
191
|
+
prompt: params.text,
|
|
192
|
+
actorUserId,
|
|
193
|
+
...(params.actorLabel ? { actorLabel: params.actorLabel } : {}),
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
allowAutoContinue: params.allowAutoContinue ?? true,
|
|
197
|
+
requestId,
|
|
198
|
+
topicEpoch: getRuntimeTopicEpoch(params.topic.id),
|
|
199
|
+
execution: {
|
|
200
|
+
sessionId: getTopicSessionId(params.topic.id),
|
|
201
|
+
sessionIdSpecified: true,
|
|
202
|
+
conversationPrompts: [params.text],
|
|
203
|
+
loggedUserMessageCount: 0,
|
|
204
|
+
vaultUserId: params.vaultUserId,
|
|
205
|
+
...(params.threadRootId ? { threadRootId: params.threadRootId } : {}),
|
|
174
206
|
},
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
requestId,
|
|
178
|
-
topicEpoch: getRuntimeTopicEpoch(params.topic.id),
|
|
179
|
-
execution: {
|
|
180
|
-
sessionId: getTopicSessionId(params.topic.id),
|
|
181
|
-
sessionIdSpecified: true,
|
|
182
|
-
conversationPrompts: [params.text],
|
|
183
|
-
loggedUserMessageCount: 0,
|
|
184
|
-
vaultUserId: params.vaultUserId,
|
|
185
|
-
...(params.threadRootId ? { threadRootId: params.threadRootId } : {}),
|
|
186
|
-
},
|
|
187
|
-
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
188
209
|
const acceptedEvent = appendRuntimeEvent("runtime-gateway-ingress", {
|
|
189
210
|
type: "ai-status",
|
|
190
211
|
topicId: params.topic.id,
|
|
@@ -213,7 +234,11 @@ export function submitRuntimeGatewayTurn(
|
|
|
213
234
|
// A new human message steers the active topic turn. The durable replacement
|
|
214
235
|
// is committed first; the provider observes this abort on its next lease
|
|
215
236
|
// heartbeat and the worker resumes the merged batch after unwind.
|
|
216
|
-
|
|
237
|
+
//
|
|
238
|
+
// That resume is the whole reason the abort is safe, so it is conditional on
|
|
239
|
+
// the merge: with `respond: false` there is no replacement batch, and
|
|
240
|
+
// aborting would kill a running answer with nothing left to resume it.
|
|
241
|
+
if (respond) requestRuntimeTurnAbort(params.topic.id, "internal");
|
|
217
242
|
|
|
218
243
|
return { ...submission, message, deduplicated: false };
|
|
219
244
|
}
|
|
@@ -385,6 +385,8 @@ export type {
|
|
|
385
385
|
RestartTopicSessionResult,
|
|
386
386
|
} from "#topics/session";
|
|
387
387
|
export { compactTopicSession, restartTopicSession } from "#topics/session";
|
|
388
|
+
export type { UpdateTopicSettingsOptions } from "#topics/update";
|
|
389
|
+
export { TopicUpdateConflictError, updateTopicSettings } from "#topics/update";
|
|
388
390
|
export * from "#types";
|
|
389
391
|
// ── Types ───────────────────────────────────────────────────────────
|
|
390
392
|
export { EFFORT_VALUES } from "#types";
|
|
@@ -77,6 +77,7 @@ export {
|
|
|
77
77
|
} from "#storage/runtime-events";
|
|
78
78
|
export { acquireRuntimeProcessLease } from "#storage/runtime-process-leases";
|
|
79
79
|
export { getTopicStats } from "#storage/token-stats";
|
|
80
|
+
export { TopicValidationError } from "#topics/create";
|
|
80
81
|
export {
|
|
81
82
|
getVisibleTopics,
|
|
82
83
|
isParticipant,
|
|
@@ -86,6 +87,7 @@ export {
|
|
|
86
87
|
} from "#topics/derive";
|
|
87
88
|
export { ensurePersonalGeneral } from "#topics/personal-general";
|
|
88
89
|
export { compactTopicSession } from "#topics/session";
|
|
90
|
+
export { TopicUpdateConflictError, updateTopicSettings } from "#topics/update";
|
|
89
91
|
export type { AgentKind } from "#types";
|
|
90
|
-
export type { AttachmentDto, TopicDto, TopicSurface } from "#types/api";
|
|
92
|
+
export type { AiMode, AttachmentDto, TopicDto, TopicSurface } from "#types/api";
|
|
91
93
|
export { NEGOTIUM_VERSION } from "#version";
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic topic settings update — the mutating counterpart to
|
|
3
|
+
* `registerTopic`. Every surface that changes a room's title, backend, model,
|
|
4
|
+
* effort or AI mode goes through this one function so validation, state
|
|
5
|
+
* normalization and the update broadcast never fork.
|
|
6
|
+
*
|
|
7
|
+
* It exists because a host that only mirrors rooms (Otium) had no way to write
|
|
8
|
+
* these fields back: the turn runner resolves execution from the *node's* own
|
|
9
|
+
* `topic.agent` / `defaultModel` / `defaultEffort`, so a picker on the host was
|
|
10
|
+
* cosmetic until the node's row changed too (D-1).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { resolveModelForAgent } from "#agents/model-catalog";
|
|
14
|
+
import { getRegistry } from "#agents/registry";
|
|
15
|
+
import { WsHub } from "#bus";
|
|
16
|
+
import { RESERVED_TOPIC_NAMES } from "#platform/constants";
|
|
17
|
+
import { getApiTopicConfig, setApiTopicConfig } from "#storage/api-topic-config";
|
|
18
|
+
import {
|
|
19
|
+
findTopicTitleConflict,
|
|
20
|
+
getTopic,
|
|
21
|
+
normalizeAiMode,
|
|
22
|
+
normalizeTopicKind,
|
|
23
|
+
normalizeTopicState,
|
|
24
|
+
upsertTopic,
|
|
25
|
+
} from "#storage/api-topics";
|
|
26
|
+
import { TopicValidationError } from "#topics/create";
|
|
27
|
+
import { type AgentKind, type EffortLevel, isAgentKind } from "#types";
|
|
28
|
+
import type { AiMode, TopicDto } from "#types/api";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Absent means "leave alone"; `agent: null` means "remove the AI". The two are
|
|
32
|
+
* different requests and a caller that only wants to rename a room must not
|
|
33
|
+
* have its backend reset as a side effect, so every field is optional and
|
|
34
|
+
* `null` is reserved for the one field where clearing is meaningful.
|
|
35
|
+
*/
|
|
36
|
+
export interface UpdateTopicSettingsOptions {
|
|
37
|
+
topicId: string;
|
|
38
|
+
title?: string;
|
|
39
|
+
agent?: AgentKind | null;
|
|
40
|
+
defaultModel?: string;
|
|
41
|
+
defaultEffort?: string;
|
|
42
|
+
aiMode?: AiMode;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A title that is already taken by another room on the same surface. */
|
|
46
|
+
export class TopicUpdateConflictError extends Error {
|
|
47
|
+
constructor(message: string) {
|
|
48
|
+
super(message);
|
|
49
|
+
this.name = "TopicUpdateConflictError";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Apply a partial settings change to an existing topic.
|
|
55
|
+
*
|
|
56
|
+
* Throws {@link TopicValidationError} on bad input and
|
|
57
|
+
* {@link TopicUpdateConflictError} on a duplicate title. Returns the stored
|
|
58
|
+
* topic as it now reads, which is what callers echo back.
|
|
59
|
+
*/
|
|
60
|
+
export function updateTopicSettings(opts: UpdateTopicSettingsOptions): TopicDto {
|
|
61
|
+
const current = getTopic(opts.topicId);
|
|
62
|
+
if (!current) throw new TopicValidationError("Topic not found");
|
|
63
|
+
if (current.kind === "manager") {
|
|
64
|
+
// Manager rooms are system-managed: their title is the user's identity in
|
|
65
|
+
// the personal-General namespace and their agent is not user-selectable.
|
|
66
|
+
throw new TopicValidationError("Manager rooms are system-managed");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let title = current.title;
|
|
70
|
+
if (opts.title !== undefined) {
|
|
71
|
+
title = opts.title.trim();
|
|
72
|
+
if (!title) throw new TopicValidationError("title is required");
|
|
73
|
+
if (RESERVED_TOPIC_NAMES.has(title.toLowerCase())) {
|
|
74
|
+
throw new TopicValidationError(`"${title}" is a reserved name`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (opts.agent !== undefined && opts.agent !== null && !isAgentKind(opts.agent)) {
|
|
79
|
+
throw new TopicValidationError(`Unknown agent '${opts.agent}'`);
|
|
80
|
+
}
|
|
81
|
+
const requestedAgent: AgentKind | undefined =
|
|
82
|
+
opts.agent === undefined ? current.agent : (opts.agent ?? undefined);
|
|
83
|
+
|
|
84
|
+
// The kind is derived, never sent: an agent room is "always", a channel is
|
|
85
|
+
// "mention"/"off". Feeding both through `normalizeTopicState` is what stops a
|
|
86
|
+
// caller from writing e.g. `aiMode: "off"` onto a room that still claims an
|
|
87
|
+
// agent, which the turn runner would read as an AI room that never answers.
|
|
88
|
+
const requestedAiMode = opts.aiMode ?? current.aiMode;
|
|
89
|
+
const requestedKind =
|
|
90
|
+
opts.agent === null || requestedAiMode === "off" || requestedAiMode === "mention"
|
|
91
|
+
? "channel"
|
|
92
|
+
: normalizeTopicKind(current.kind);
|
|
93
|
+
const { kind, aiMode, agent } = normalizeTopicState({
|
|
94
|
+
id: current.id,
|
|
95
|
+
kind: requestedKind,
|
|
96
|
+
agent: requestedAgent,
|
|
97
|
+
aiMode: normalizeAiMode(requestedAiMode),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Titles are unique per surface instance, so the conflict check must run on
|
|
101
|
+
// the row's own surface rather than this process's default (M-1).
|
|
102
|
+
if (title.toLowerCase() !== current.title.toLowerCase() || kind !== current.kind) {
|
|
103
|
+
const conflict = findTopicTitleConflict(title, kind, {
|
|
104
|
+
excludeTopicId: current.id,
|
|
105
|
+
surface: current.surface,
|
|
106
|
+
surfaceScope: current.surfaceScope ?? null,
|
|
107
|
+
});
|
|
108
|
+
if (conflict) {
|
|
109
|
+
throw new TopicUpdateConflictError(
|
|
110
|
+
`A topic named "${title}" already exists on ${current.surface ?? "this surface"}`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const registry = getRegistry(agent ?? "maestro");
|
|
116
|
+
if (opts.defaultModel !== undefined) {
|
|
117
|
+
if (!agent) throw new TopicValidationError("This topic has no AI model");
|
|
118
|
+
if (!registry.validateModel(opts.defaultModel)) {
|
|
119
|
+
throw new TopicValidationError(
|
|
120
|
+
`model '${opts.defaultModel}' is not valid for agent '${agent}'`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (opts.defaultEffort !== undefined) {
|
|
125
|
+
if (!agent) throw new TopicValidationError("This topic has no AI effort");
|
|
126
|
+
if (!registry.validateEffort(opts.defaultEffort as EffortLevel)) {
|
|
127
|
+
throw new TopicValidationError(
|
|
128
|
+
`effort '${opts.defaultEffort}' is not valid for agent '${agent}'`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// A model carried over from the previous backend is not selectable on the new
|
|
134
|
+
// one, so `resolveModelForAgent` falls back rather than persisting a token the
|
|
135
|
+
// registry will reject on the next turn.
|
|
136
|
+
const defaultModel = resolveModelForAgent(
|
|
137
|
+
agent ?? "maestro",
|
|
138
|
+
opts.defaultModel ?? (agent === current.agent ? current.defaultModel : undefined),
|
|
139
|
+
registry,
|
|
140
|
+
);
|
|
141
|
+
const requestedEffort = (opts.defaultEffort ??
|
|
142
|
+
(agent === current.agent ? current.defaultEffort : undefined)) as EffortLevel | undefined;
|
|
143
|
+
const defaultEffort =
|
|
144
|
+
requestedEffort && registry.validateEffort(requestedEffort)
|
|
145
|
+
? requestedEffort
|
|
146
|
+
: (registry.defaultEffort ?? "medium");
|
|
147
|
+
|
|
148
|
+
const next: TopicDto = {
|
|
149
|
+
...current,
|
|
150
|
+
title,
|
|
151
|
+
kind,
|
|
152
|
+
aiMode,
|
|
153
|
+
agent,
|
|
154
|
+
defaultModel,
|
|
155
|
+
defaultEffort,
|
|
156
|
+
};
|
|
157
|
+
upsertTopic(next);
|
|
158
|
+
|
|
159
|
+
// `resolveTopicTurnExecution` reads `config.model ?? topic.defaultModel`, so a
|
|
160
|
+
// stale per-topic override (set earlier by `/model` in the terminal) would
|
|
161
|
+
// silently shadow the value we just wrote and make this update look ignored —
|
|
162
|
+
// exactly the bug this route exists to fix. Clear only the fields the caller
|
|
163
|
+
// actually changed, so an untouched override survives.
|
|
164
|
+
const config = getApiTopicConfig(next.id);
|
|
165
|
+
if (config && (opts.defaultModel !== undefined || opts.defaultEffort !== undefined)) {
|
|
166
|
+
setApiTopicConfig(next.id, {
|
|
167
|
+
...config,
|
|
168
|
+
...(opts.defaultModel !== undefined ? { model: undefined, modelLocked: undefined } : {}),
|
|
169
|
+
...(opts.defaultEffort !== undefined ? { effort: undefined, effortLocked: undefined } : {}),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
WsHub.get().broadcastTopicUpdated(next.id);
|
|
174
|
+
return getTopic(next.id) ?? next;
|
|
175
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const NEGOTIUM_VERSION = "0.3.
|
|
1
|
+
export const NEGOTIUM_VERSION = "0.3.7";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const NEGOTIUM_VERSION = "0.3.
|
|
1
|
+
export declare const NEGOTIUM_VERSION = "0.3.7";
|