negotium 0.1.7 → 0.1.9
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/README.md +26 -2
- package/dist/canonical-mcp-bridge.js +14 -0
- package/dist/canonical-mcp-bridge.js.map +9 -0
- package/dist/chunk-10ct8814.js +429 -0
- package/dist/chunk-10ct8814.js.map +11 -0
- package/dist/chunk-18fzftjf.js +214 -0
- package/dist/chunk-18fzftjf.js.map +10 -0
- package/dist/chunk-1k7t7bnx.js +97 -0
- package/dist/chunk-1k7t7bnx.js.map +10 -0
- package/dist/chunk-1msje2zf.js +23 -0
- package/dist/chunk-1msje2zf.js.map +10 -0
- package/dist/chunk-2dgkah1z.js +706 -0
- package/dist/chunk-2dgkah1z.js.map +14 -0
- package/dist/chunk-3t4dtm26.js +61 -0
- package/dist/chunk-3t4dtm26.js.map +10 -0
- package/dist/chunk-3vn2vnj1.js +51 -0
- package/dist/chunk-3vn2vnj1.js.map +10 -0
- package/dist/chunk-6hknphtc.js +2054 -0
- package/dist/chunk-6hknphtc.js.map +29 -0
- package/dist/chunk-6tfzpcvk.js +2149 -0
- package/dist/chunk-6tfzpcvk.js.map +15 -0
- package/dist/chunk-8g0c4as0.js +23 -0
- package/dist/chunk-8g0c4as0.js.map +10 -0
- package/dist/chunk-9spx3h9n.js +279 -0
- package/dist/chunk-9spx3h9n.js.map +10 -0
- package/dist/chunk-c500xe9z.js +33 -0
- package/dist/chunk-c500xe9z.js.map +10 -0
- package/dist/chunk-e9b2xpff.js +147 -0
- package/dist/chunk-e9b2xpff.js.map +10 -0
- package/dist/chunk-edb07sjp.js +567 -0
- package/dist/chunk-edb07sjp.js.map +14 -0
- package/dist/chunk-eexqsee7.js +368 -0
- package/dist/chunk-eexqsee7.js.map +11 -0
- package/dist/chunk-efyxk97v.js +500 -0
- package/dist/chunk-efyxk97v.js.map +13 -0
- package/dist/chunk-epqxwkvh.js +3534 -0
- package/dist/chunk-epqxwkvh.js.map +20 -0
- package/dist/chunk-f1pzh3xh.js +115 -0
- package/dist/chunk-f1pzh3xh.js.map +10 -0
- package/dist/chunk-f5xgprg1.js +59 -0
- package/dist/chunk-f5xgprg1.js.map +10 -0
- package/dist/chunk-f97qybwk.js +584 -0
- package/dist/chunk-f97qybwk.js.map +11 -0
- package/dist/chunk-fvc7ncxd.js +252 -0
- package/dist/chunk-fvc7ncxd.js.map +10 -0
- package/dist/chunk-g7pvfj5e.js +574 -0
- package/dist/chunk-g7pvfj5e.js.map +14 -0
- package/dist/chunk-h8vmb9gf.js +315 -0
- package/dist/chunk-h8vmb9gf.js.map +10 -0
- package/dist/chunk-hgw7n8cm.js +43 -0
- package/dist/chunk-hgw7n8cm.js.map +10 -0
- package/dist/chunk-hsq7j9jk.js +126 -0
- package/dist/chunk-hsq7j9jk.js.map +10 -0
- package/dist/chunk-hwy3n31w.js +21 -0
- package/dist/chunk-hwy3n31w.js.map +9 -0
- package/dist/chunk-j0kjscb7.js +53 -0
- package/dist/chunk-j0kjscb7.js.map +10 -0
- package/dist/chunk-jzz91jwh.js +70 -0
- package/dist/chunk-jzz91jwh.js.map +10 -0
- package/dist/chunk-k9z47mab.js +247 -0
- package/dist/chunk-k9z47mab.js.map +12 -0
- package/dist/chunk-kbm35t7w.js +1331 -0
- package/dist/chunk-kbm35t7w.js.map +16 -0
- package/dist/chunk-m4veth0c.js +1649 -0
- package/dist/chunk-m4veth0c.js.map +17 -0
- package/dist/chunk-mhty3nna.js +150 -0
- package/dist/chunk-mhty3nna.js.map +11 -0
- package/dist/chunk-mmgh670x.js +248 -0
- package/dist/chunk-mmgh670x.js.map +11 -0
- package/dist/chunk-n0kk5xbq.js +2358 -0
- package/dist/chunk-n0kk5xbq.js.map +26 -0
- package/dist/chunk-nmv41qx9.js +2847 -0
- package/dist/chunk-nmv41qx9.js.map +23 -0
- package/dist/chunk-nqdp52aj.js +113 -0
- package/dist/chunk-nqdp52aj.js.map +12 -0
- package/dist/chunk-ns2m25dz.js +139 -0
- package/dist/chunk-ns2m25dz.js.map +10 -0
- package/dist/chunk-pkdmypdd.js +147 -0
- package/dist/chunk-pkdmypdd.js.map +11 -0
- package/dist/chunk-rmyfmrrr.js +1634 -0
- package/dist/chunk-rmyfmrrr.js.map +15 -0
- package/dist/chunk-sa874j5c.js +9 -0
- package/dist/chunk-sa874j5c.js.map +10 -0
- package/dist/chunk-sgsz2fpq.js +289 -0
- package/dist/chunk-sgsz2fpq.js.map +10 -0
- package/dist/chunk-sn51wnf1.js +3020 -0
- package/dist/chunk-sn51wnf1.js.map +32 -0
- package/dist/chunk-sqhr1a7e.js +587 -0
- package/dist/chunk-sqhr1a7e.js.map +11 -0
- package/dist/chunk-tmj5r2zv.js +2933 -0
- package/dist/chunk-tmj5r2zv.js.map +20 -0
- package/dist/chunk-vefj3880.js +56 -0
- package/dist/chunk-vefj3880.js.map +10 -0
- package/dist/chunk-vp2db42d.js +121 -0
- package/dist/chunk-vp2db42d.js.map +10 -0
- package/dist/chunk-wry79q55.js +108 -0
- package/dist/chunk-wry79q55.js.map +10 -0
- package/dist/chunk-x1arr7ze.js +63 -0
- package/dist/chunk-x1arr7ze.js.map +10 -0
- package/dist/chunk-z2xy5n2q.js +103 -0
- package/dist/chunk-z2xy5n2q.js.map +10 -0
- package/dist/hosted-agent.js +53 -0
- package/dist/hosted-agent.js.map +10 -0
- package/dist/main.js +132 -20
- package/dist/main.js.map +4 -4
- package/dist/runtime/scripts/faster-whisper-wrapper.py +87 -0
- package/dist/runtime/scripts/mcp-patchright-http.mjs +349 -0
- package/dist/runtime/src/agents/api-topic-agent-switch.ts +178 -0
- package/dist/runtime/src/agents/archiver.ts +396 -0
- package/dist/runtime/src/agents/auth-check.ts +64 -0
- package/dist/runtime/src/agents/claude-provider.ts +737 -0
- package/dist/runtime/src/agents/claude-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +177 -0
- package/dist/runtime/src/agents/codex-provider.ts +785 -0
- package/dist/runtime/src/agents/codex-registry.ts +115 -0
- package/dist/runtime/src/agents/codex-tree-kill.ts +365 -0
- package/dist/runtime/src/agents/contracts.ts +69 -0
- package/dist/runtime/src/agents/deep-map.ts +13 -0
- package/dist/runtime/src/agents/execution-host.ts +91 -0
- package/dist/runtime/src/agents/fixtures/claude-attachments.jsonl +2 -0
- package/dist/runtime/src/agents/fixtures/codex-shell.jsonl +5 -0
- package/dist/runtime/src/agents/fork.ts +65 -0
- package/dist/runtime/src/agents/hosted-agent.ts +49 -0
- package/dist/runtime/src/agents/idle-archiver.ts +128 -0
- package/dist/runtime/src/agents/index.ts +205 -0
- package/dist/runtime/src/agents/maestro-provider.ts +206 -0
- package/dist/runtime/src/agents/maestro-registry.ts +31 -0
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +280 -0
- package/dist/runtime/src/agents/mcp-tools/common.ts +21 -0
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +220 -0
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +525 -0
- package/dist/runtime/src/agents/mcp-tools/visuals.ts +88 -0
- package/dist/runtime/src/agents/model-catalog.ts +151 -0
- package/dist/runtime/src/agents/registry.ts +24 -0
- package/dist/runtime/src/agents/rollout/claude.ts +331 -0
- package/dist/runtime/src/agents/rollout/codex.ts +569 -0
- package/dist/runtime/src/agents/rollout/shared.ts +205 -0
- package/dist/runtime/src/agents/self-config-core.ts +415 -0
- package/dist/runtime/src/agents/task-events.ts +44 -0
- package/dist/runtime/src/agents/tool-format.ts +352 -0
- package/dist/runtime/src/agents/topic-agent-switch.ts +193 -0
- package/dist/runtime/src/agents/topic-cleanup.ts +231 -0
- package/dist/runtime/src/agents/vault-tool-policy.ts +33 -0
- package/dist/runtime/src/application/execute-external-user-turn.ts +23 -0
- package/dist/runtime/src/application/submit-user-message.ts +50 -0
- package/dist/runtime/src/application/switch-topic-effort.ts +48 -0
- package/dist/runtime/src/application/switch-topic-model.ts +79 -0
- package/dist/runtime/src/application/topic-service.ts +139 -0
- package/dist/runtime/src/application/vault-command.ts +170 -0
- package/dist/runtime/src/bus.ts +319 -0
- package/dist/runtime/src/index.ts +333 -0
- package/dist/runtime/src/mcp/agent-health-server.ts +309 -0
- package/dist/runtime/src/mcp/background-bash-server.ts +346 -0
- package/dist/runtime/src/mcp/canonical-bridge-config.ts +84 -0
- package/dist/runtime/src/mcp/canonical-proxy-server.ts +171 -0
- package/dist/runtime/src/mcp/dm-ipc.ts +60 -0
- package/dist/runtime/src/mcp/mcp-helpers.ts +40 -0
- package/dist/runtime/src/mcp/paddleocr-server.ts +105 -0
- package/dist/runtime/src/mcp/peer-bridge.ts +114 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +137 -0
- package/dist/runtime/src/mcp/session-comm/bridge-ipc-config.ts +27 -0
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +165 -0
- package/dist/runtime/src/mcp/session-comm/runtime.ts +115 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +649 -0
- package/dist/runtime/src/mcp/session-comm/topic-config.ts +131 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +229 -0
- package/dist/runtime/src/mcp/stdio-protect.ts +84 -0
- package/dist/runtime/src/mcp/system-health-server.ts +130 -0
- package/dist/runtime/src/mcp/task-server.ts +173 -0
- package/dist/runtime/src/mcp/token-stats-server.ts +98 -0
- package/dist/runtime/src/mcp/vault-http.ts +219 -0
- package/dist/runtime/src/mcp/vault-run.ts +153 -0
- package/dist/runtime/src/mcp/vault-server.ts +104 -0
- package/dist/runtime/src/mcp/wiki-server.ts +755 -0
- package/dist/runtime/src/media/file-events.ts +33 -0
- package/dist/runtime/src/media/text-extractor.ts +312 -0
- package/dist/runtime/src/media/video.ts +92 -0
- package/dist/runtime/src/outbox/file-ops.ts +210 -0
- package/dist/runtime/src/outbox/index.ts +8 -0
- package/dist/runtime/src/outbox/utils.ts +51 -0
- package/dist/runtime/src/platform/background-bash/manager.ts +279 -0
- package/dist/runtime/src/platform/config.ts +371 -0
- package/dist/runtime/src/platform/constants.ts +18 -0
- package/dist/runtime/src/platform/debug-users.ts +14 -0
- package/dist/runtime/src/platform/delay.ts +1 -0
- package/dist/runtime/src/platform/error.ts +9 -0
- package/dist/runtime/src/platform/file-utils.ts +16 -0
- package/dist/runtime/src/platform/jsonl.ts +202 -0
- package/dist/runtime/src/platform/lifecycle.ts +174 -0
- package/dist/runtime/src/platform/logger.ts +31 -0
- package/dist/runtime/src/platform/maestro-bootstrap-env.ts +3 -0
- package/dist/runtime/src/platform/mcp-config.ts +878 -0
- package/dist/runtime/src/platform/modules.ts +145 -0
- package/dist/runtime/src/platform/node-plugins.ts +46 -0
- package/dist/runtime/src/platform/playwright/manager.ts +1233 -0
- package/dist/runtime/src/prompts/agents/wiki-archiver.md +261 -0
- package/dist/runtime/src/prompts/builders.ts +368 -0
- package/dist/runtime/src/prompts/sessions/channel-system.md +48 -0
- package/dist/runtime/src/prompts/sessions/manager-system.md +37 -0
- package/dist/runtime/src/prompts/sessions/topic-system.md +47 -0
- package/dist/runtime/src/prompts/sessions/visual-design.md +86 -0
- package/dist/runtime/src/query/active-rooms.ts +534 -0
- package/dist/runtime/src/query/control.ts +313 -0
- package/dist/runtime/src/query/index.ts +24 -0
- package/dist/runtime/src/query/session-inbox-cleanup.ts +44 -0
- package/dist/runtime/src/query/session-inbox-path.ts +42 -0
- package/dist/runtime/src/query/state.ts +94 -0
- package/dist/runtime/src/query/types.ts +83 -0
- package/dist/runtime/src/runtime/ask-callbacks.ts +153 -0
- package/dist/runtime/src/runtime/attachments.ts +151 -0
- package/dist/runtime/src/runtime/background-sessions.ts +83 -0
- package/dist/runtime/src/runtime/channel-context.ts +107 -0
- package/dist/runtime/src/runtime/errors.ts +69 -0
- package/dist/runtime/src/runtime/event-heartbeat.ts +77 -0
- package/dist/runtime/src/runtime/file-hooks.ts +81 -0
- package/dist/runtime/src/runtime/footer.ts +23 -0
- package/dist/runtime/src/runtime/inbox.ts +946 -0
- package/dist/runtime/src/runtime/tasks.ts +56 -0
- package/dist/runtime/src/runtime/topic-config.ts +29 -0
- package/dist/runtime/src/runtime/turn-runner.ts +2298 -0
- package/dist/runtime/src/runtime/usage-alert.ts +84 -0
- package/dist/runtime/src/runtime/visual-html.ts +109 -0
- package/dist/runtime/src/runtime/visual-store.ts +430 -0
- package/dist/runtime/src/runtime/visuals.ts +171 -0
- package/dist/runtime/src/security/sanitize.ts +27 -0
- package/dist/runtime/src/security/sensitive-path.ts +34 -0
- package/dist/runtime/src/storage/activity-log.ts +152 -0
- package/dist/runtime/src/storage/api-messages.ts +617 -0
- package/dist/runtime/src/storage/api-topic-brief.ts +127 -0
- package/dist/runtime/src/storage/api-topic-config.ts +192 -0
- package/dist/runtime/src/storage/api-topics.ts +844 -0
- package/dist/runtime/src/storage/app-settings.ts +44 -0
- package/dist/runtime/src/storage/browser-profiles.ts +133 -0
- package/dist/runtime/src/storage/conversations.ts +268 -0
- package/dist/runtime/src/storage/forum/agent-settings.ts +20 -0
- package/dist/runtime/src/storage/forum/index.ts +4 -0
- package/dist/runtime/src/storage/forum/repository.ts +283 -0
- package/dist/runtime/src/storage/forum/schema.ts +300 -0
- package/dist/runtime/src/storage/forum-db.ts +23 -0
- package/dist/runtime/src/storage/runtime-events.ts +130 -0
- package/dist/runtime/src/storage/runtime-leases.ts +189 -0
- package/dist/runtime/src/storage/runtime-process-leases.ts +216 -0
- package/dist/runtime/src/storage/runtime-topic-state.ts +148 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +301 -0
- package/dist/runtime/src/storage/self-schedules.ts +298 -0
- package/dist/runtime/src/storage/session-asks.ts +347 -0
- package/dist/runtime/src/storage/sqlite.ts +129 -0
- package/dist/runtime/src/storage/tasks.ts +179 -0
- package/dist/runtime/src/storage/token-stats.ts +144 -0
- package/dist/runtime/src/storage/topic-archive-state.ts +59 -0
- package/dist/runtime/src/storage/topic-archive.ts +74 -0
- package/dist/runtime/src/storage/topic-settings.ts +67 -0
- package/dist/runtime/src/storage/topic-transcript.ts +98 -0
- package/dist/runtime/src/storage/vault-crypto.ts +65 -0
- package/dist/runtime/src/storage/vault.ts +225 -0
- package/dist/runtime/src/storage/wiki-summary-names.ts +33 -0
- package/dist/runtime/src/storage/wiki.ts +85 -0
- package/dist/runtime/src/topics/create.ts +123 -0
- package/dist/runtime/src/topics/derive.ts +398 -0
- package/dist/runtime/src/topics/lifecycle.ts +311 -0
- package/dist/runtime/src/topics/links.ts +7 -0
- package/dist/runtime/src/topics/personal-general.ts +65 -0
- package/dist/runtime/src/topics/session.ts +381 -0
- package/dist/runtime/src/types/api.ts +497 -0
- package/dist/runtime/src/types.ts +257 -0
- package/dist/runtime/src/version.ts +1 -0
- package/dist/runtime/tsconfig.json +13 -0
- package/package.json +28 -3
- package/dist/main.d.ts +0 -3
- package/dist/main.d.ts.map +0 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { unlinkSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { AgentRegistry } from "#agents/contracts";
|
|
5
|
+
import { writeCodexRollout } from "#agents/rollout/codex";
|
|
6
|
+
import { logger } from "#platform/logger";
|
|
7
|
+
import { readConversation } from "#storage/conversations";
|
|
8
|
+
import { CODEX_EFFORT_VALUES, type EffortLevel } from "#types";
|
|
9
|
+
|
|
10
|
+
const VALID_EFFORTS = new Set<EffortLevel>(CODEX_EFFORT_VALUES);
|
|
11
|
+
|
|
12
|
+
// Codex CLI's own empirical default is gpt-5.6-sol (`codex exec` 2026-07-10 →
|
|
13
|
+
// "model: gpt-5.6-sol"), but we deliberately default to gpt-5.6-luna — the
|
|
14
|
+
// cheapest/fastest GPT-5.6 tier — for a general always-on assistant where most
|
|
15
|
+
// queries are light. Heavier work escalates to terra/sol via set_model. This
|
|
16
|
+
// value is passed explicitly to the SDK (see event-processor resolveDefaultModel),
|
|
17
|
+
// so the footer and the actual model stay in sync.
|
|
18
|
+
export const codexRegistry: AgentRegistry = {
|
|
19
|
+
kind: "codex",
|
|
20
|
+
defaultModel: "gpt-5.6-luna",
|
|
21
|
+
// defaultEffort intentionally omitted — Codex SDK treats absence as
|
|
22
|
+
// "reasoning off". Setting "high"/etc. would silently flip on reasoning.
|
|
23
|
+
|
|
24
|
+
expandModelAlias(s) {
|
|
25
|
+
return s;
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
validateModel(s) {
|
|
29
|
+
// Codex doesn't publish a closed model list and OpenAI ships new IDs
|
|
30
|
+
// (gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, o3, ...) frequently. Best-effort: accept any
|
|
31
|
+
// non-empty string. Bad IDs surface at SDK call time with a clear error.
|
|
32
|
+
return typeof s === "string" && s.length > 0;
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
validEfforts: CODEX_EFFORT_VALUES,
|
|
36
|
+
validateEffort(s) {
|
|
37
|
+
// GPT-5.6 accepts low/medium/high/xhigh/max; 'minimal'
|
|
38
|
+
// was removed because the Codex API rejects it when default tools
|
|
39
|
+
// (image_gen, web_search) are active.
|
|
40
|
+
return VALID_EFFORTS.has(s);
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
footerLabel(model, effort) {
|
|
44
|
+
// Codex omits effort to mean "reasoning off". Show `(off)` explicitly so
|
|
45
|
+
// the user can distinguish from claude (which always has a default).
|
|
46
|
+
return `${model} · ${effort ?? "(off)"}`;
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
writeRollout(opts) {
|
|
50
|
+
// Codex SDK exposes the resume key as `threadId`; AgentRegistry unifies
|
|
51
|
+
// the name to `sessionId` so callers don't branch on agent.
|
|
52
|
+
// `reuseSessionId` (if any) is forwarded as `threadId` so claude→codex→claude
|
|
53
|
+
// round-trips also keep one continuous codex thread instead of orphaning a
|
|
54
|
+
// fresh uuidv7 on every switch.
|
|
55
|
+
const { threadId, rolloutPath } = writeCodexRollout({
|
|
56
|
+
cwd: opts.cwd,
|
|
57
|
+
entries: opts.entries,
|
|
58
|
+
model: opts.model ?? codexRegistry.defaultModel,
|
|
59
|
+
...(opts.effort ? { effort: opts.effort } : {}),
|
|
60
|
+
...(opts.reuseSessionId ? { threadId: opts.reuseSessionId } : {}),
|
|
61
|
+
});
|
|
62
|
+
return { sessionId: threadId, rolloutPath };
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// Codex SDK has no fork API. We synthesize a rollout from the
|
|
66
|
+
// provider-agnostic conversation log (same path as `set_agent` cross-agent
|
|
67
|
+
// bridging). Caveats: extractChatPairs folds tool_use/tool_result into
|
|
68
|
+
// assistant text as `[Tool: ...]` annotations, so structural tool history
|
|
69
|
+
// is lost.
|
|
70
|
+
async forkSession({ cwd, userId, topicName, model, effort }) {
|
|
71
|
+
const entries = readConversation(userId, topicName);
|
|
72
|
+
const { threadId, rolloutPath } = writeCodexRollout({
|
|
73
|
+
cwd,
|
|
74
|
+
entries,
|
|
75
|
+
model: model ?? codexRegistry.defaultModel,
|
|
76
|
+
...(effort ? { effort } : {}),
|
|
77
|
+
});
|
|
78
|
+
return { forkId: threadId, rolloutPath };
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
// Codex stores rollouts at `~/.codex/sessions/<YYYY>/<MM>/<DD>/rollout-<ts>-<threadId>.jsonl`.
|
|
82
|
+
// The date prefix is unknown at cleanup time (it's the *original* write
|
|
83
|
+
// timestamp, not "now"), so we glob across the whole sessions tree by
|
|
84
|
+
// threadId suffix. With at most a few thousand files in active use this
|
|
85
|
+
// is well under a millisecond on a warm filesystem.
|
|
86
|
+
async cleanupRollouts({ sessionIds }) {
|
|
87
|
+
if (sessionIds.length === 0) return;
|
|
88
|
+
const sessionsDir = join(process.env.CODEX_HOME || join(homedir(), ".codex"), "sessions");
|
|
89
|
+
const failures: unknown[] = [];
|
|
90
|
+
// One Glob per threadId so a single corrupt entry can't poison the rest.
|
|
91
|
+
// Bun.Glob's `scan` yields paths relative to its base dir.
|
|
92
|
+
for (const tid of sessionIds) {
|
|
93
|
+
try {
|
|
94
|
+
const glob = new Bun.Glob(`**/rollout-*-${tid}.jsonl`);
|
|
95
|
+
for await (const rel of glob.scan({ cwd: sessionsDir, onlyFiles: true })) {
|
|
96
|
+
const path = join(sessionsDir, rel);
|
|
97
|
+
try {
|
|
98
|
+
unlinkSync(path);
|
|
99
|
+
} catch (e) {
|
|
100
|
+
if ((e as NodeJS.ErrnoException)?.code !== "ENOENT") {
|
|
101
|
+
logger.warn({ err: e, path }, "codex cleanupRollouts: unlink failed");
|
|
102
|
+
failures.push(e);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
} catch (e) {
|
|
107
|
+
logger.warn({ err: e, threadId: tid }, "codex cleanupRollouts: scan failed");
|
|
108
|
+
failures.push(e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (failures.length > 0) {
|
|
112
|
+
throw new AggregateError(failures, "codex cleanupRollouts failed");
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
};
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex process tree kill helper.
|
|
3
|
+
*
|
|
4
|
+
* The @openai/codex-sdk spawns `codex` as a direct child of our bot process but
|
|
5
|
+
* does not expose the resulting PID and does not detach the child into its own
|
|
6
|
+
* process group. So when we call `abortController.abort()`, the SDK sends a
|
|
7
|
+
* single SIGTERM that may or may not reach grandchild tool processes (shells,
|
|
8
|
+
* MCP servers spawned by codex itself). To guarantee abort actually stops
|
|
9
|
+
* activity we identify the codex children we spawned and SIGTERM their whole
|
|
10
|
+
* descendant tree, with a SIGKILL fallback for stragglers.
|
|
11
|
+
*
|
|
12
|
+
* Strategy (post-2026-05-14 fix):
|
|
13
|
+
* 1. Acquire a global spawn lock that's held across the entire window
|
|
14
|
+
* from baseline snapshot through the first SDK event (when codex is
|
|
15
|
+
* actually spawned — `runStreamed()` returns a lazy async generator,
|
|
16
|
+
* so the real `child_process.spawn()` happens on the first iterator
|
|
17
|
+
* `.next()`, not when `runStreamed()` is awaited).
|
|
18
|
+
* 2. Snapshot direct codex children of `process.pid` BEFORE forcing the
|
|
19
|
+
* first iterator step.
|
|
20
|
+
* 3. After the first event lands, diff against a fresh snapshot to
|
|
21
|
+
* identify our PIDs and register them in `globalOwnedCodexPids` so
|
|
22
|
+
* concurrent codex callers don't claim them as their own.
|
|
23
|
+
* 4. On abort, walk each PID's descendant tree via `pgrep -P` and signal
|
|
24
|
+
* every node. Unregister on exit.
|
|
25
|
+
*
|
|
26
|
+
* Platform: relies on `pgrep` and `ps` — macOS/Linux only. Windows is not
|
|
27
|
+
* supported (Otium already requires POSIX for the rest of the stack).
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { execFileSync } from "node:child_process";
|
|
31
|
+
import { logger } from "#platform/logger";
|
|
32
|
+
|
|
33
|
+
// 5s gives MCP stdio servers (codex spawns these as grandchildren) a realistic
|
|
34
|
+
// shutdown window. 2s was empirically too aggressive: stdio MCP servers in the
|
|
35
|
+
// middle of writing a response would get SIGKILL'd before they finished.
|
|
36
|
+
const SIGKILL_DELAY_MS = 5_000;
|
|
37
|
+
const CODEX_PROC_NAME_MATCH = /codex/i;
|
|
38
|
+
// Cap tree-walk depth. Defends against pathological or malicious cycles that
|
|
39
|
+
// slip past the visited-set guard (e.g. via PID reuse), and against deeply
|
|
40
|
+
// nested codex+MCP+shell trees blowing the stack.
|
|
41
|
+
const TREE_DEPTH_CAP = 16;
|
|
42
|
+
|
|
43
|
+
/** Return direct child PIDs of `parentPid`. Returns [] on error or no children. */
|
|
44
|
+
function getDirectChildren(parentPid: number): number[] {
|
|
45
|
+
try {
|
|
46
|
+
const out = execFileSync("pgrep", ["-P", String(parentPid)], {
|
|
47
|
+
encoding: "utf8",
|
|
48
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
49
|
+
}).trim();
|
|
50
|
+
if (!out) return [];
|
|
51
|
+
return out
|
|
52
|
+
.split("\n")
|
|
53
|
+
.map((s) => Number(s))
|
|
54
|
+
.filter((n) => Number.isFinite(n) && n > 0);
|
|
55
|
+
} catch {
|
|
56
|
+
// pgrep exits 1 when no matches; treat as empty.
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Return the executable name (comm) of a process, or "" if unavailable. */
|
|
62
|
+
function getProcName(pid: number): string {
|
|
63
|
+
try {
|
|
64
|
+
return execFileSync("ps", ["-p", String(pid), "-o", "comm="], {
|
|
65
|
+
encoding: "utf8",
|
|
66
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
67
|
+
}).trim();
|
|
68
|
+
} catch {
|
|
69
|
+
return "";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Return the start time of `pid` as the raw `ps -o lstart=` string (e.g.
|
|
75
|
+
* "Tue May 12 10:49:35 2026"), or "" if unavailable. Used as a fingerprint
|
|
76
|
+
* to defend against PID reuse — even if the OS reassigns a baseline PID to
|
|
77
|
+
* a fresh codex spawn, the lstart string will differ.
|
|
78
|
+
*
|
|
79
|
+
* macOS and Linux both support `ps -o lstart=` with the same date format.
|
|
80
|
+
*/
|
|
81
|
+
function getProcLstart(pid: number): string {
|
|
82
|
+
try {
|
|
83
|
+
return execFileSync("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
84
|
+
encoding: "utf8",
|
|
85
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
86
|
+
}).trim();
|
|
87
|
+
} catch {
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** (pid, lstart) fingerprint pair — same PID + same lstart = same process. */
|
|
93
|
+
export type CodexProcStamp = { pid: number; lstart: string };
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Recursively collect `rootPid` and all its descendants, up to `TREE_DEPTH_CAP`
|
|
97
|
+
* levels deep. Cycle-safe via the `out` Set; depth-cap-safe via the `depth`
|
|
98
|
+
* parameter (defends against pathological trees the Set guard might miss,
|
|
99
|
+
* e.g. PID reuse mid-walk).
|
|
100
|
+
*/
|
|
101
|
+
function collectTree(rootPid: number, out: Set<number> = new Set(), depth = 0): Set<number> {
|
|
102
|
+
if (out.has(rootPid)) return out;
|
|
103
|
+
out.add(rootPid);
|
|
104
|
+
if (depth >= TREE_DEPTH_CAP) {
|
|
105
|
+
logger.warn(
|
|
106
|
+
{ rootPid, depth, cap: TREE_DEPTH_CAP },
|
|
107
|
+
"codex-tree-kill: tree depth cap reached, descendants beyond this point not collected",
|
|
108
|
+
);
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
for (const child of getDirectChildren(rootPid)) {
|
|
112
|
+
collectTree(child, out, depth + 1);
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Snapshot current codex-named direct children of `process.pid` along with
|
|
119
|
+
* each PID's lstart (start time) fingerprint. Returning the lstart map lets
|
|
120
|
+
* `findNewCodexChildren` distinguish "PID X was in baseline and is the same
|
|
121
|
+
* process" from "PID X was in baseline but the OS reassigned that PID to a
|
|
122
|
+
* fresh codex" — only the latter is novel.
|
|
123
|
+
*/
|
|
124
|
+
export function snapshotCodexChildren(): Map<number, string> {
|
|
125
|
+
const out = new Map<number, string>();
|
|
126
|
+
for (const pid of getDirectChildren(process.pid)) {
|
|
127
|
+
const name = getProcName(pid);
|
|
128
|
+
if (!name || !CODEX_PROC_NAME_MATCH.test(name)) continue;
|
|
129
|
+
const lstart = getProcLstart(pid);
|
|
130
|
+
// If lstart can't be read (race: process died between getDirectChildren
|
|
131
|
+
// and getProcLstart), still record the PID with an empty fingerprint —
|
|
132
|
+
// any later snapshot with a real lstart will compare as "different" and
|
|
133
|
+
// count it as novel, which is the safer side to err on.
|
|
134
|
+
out.set(pid, lstart);
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* PIDs already owned by an active codex caller (normal provider or fork).
|
|
141
|
+
* `findNewCodexChildren` excludes these so concurrent callers can't claim
|
|
142
|
+
* each other's PIDs when their baselines happen to miss them (e.g. the other
|
|
143
|
+
* caller's spawn raced in between baseline and first-event diff).
|
|
144
|
+
*
|
|
145
|
+
* Callers must `registerOwnedCodexPids` after diff, and `unregisterOwnedCodexPids`
|
|
146
|
+
* in their cleanup `finally` block to avoid leaking entries on long-running bots.
|
|
147
|
+
*/
|
|
148
|
+
const globalOwnedCodexPids = new Set<number>();
|
|
149
|
+
|
|
150
|
+
export function registerOwnedCodexPids(pids: Iterable<number>): void {
|
|
151
|
+
for (const p of pids) globalOwnedCodexPids.add(p);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function unregisterOwnedCodexPids(pids: Iterable<number>): void {
|
|
155
|
+
for (const p of pids) globalOwnedCodexPids.delete(p);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Codex children that are novel relative to `baseline`. A PID is novel if:
|
|
160
|
+
* - it wasn't in baseline, OR
|
|
161
|
+
* - it was in baseline but with a different lstart (PID reuse — old codex
|
|
162
|
+
* died and the OS reassigned the PID to our new spawn)
|
|
163
|
+
* AND it isn't already owned by a concurrent codex caller.
|
|
164
|
+
*/
|
|
165
|
+
export function findNewCodexChildren(baseline: Map<number, string>): number[] {
|
|
166
|
+
const current = snapshotCodexChildren();
|
|
167
|
+
const novel: number[] = [];
|
|
168
|
+
for (const [pid, lstart] of current) {
|
|
169
|
+
const baselineLstart = baseline.get(pid);
|
|
170
|
+
// Same PID + same lstart fingerprint → continuation of the same process,
|
|
171
|
+
// not novel. Same PID + different lstart → PID reuse, treat as novel.
|
|
172
|
+
if (baselineLstart !== undefined && baselineLstart === lstart) continue;
|
|
173
|
+
if (globalOwnedCodexPids.has(pid)) continue;
|
|
174
|
+
novel.push(pid);
|
|
175
|
+
}
|
|
176
|
+
return novel;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Serialize the spawn critical section across concurrent codex callers
|
|
181
|
+
* (`codexProvider` and `queryCodexForkSession`). The lock MUST be held from
|
|
182
|
+
* baseline snapshot through the first generator event, because the SDK's
|
|
183
|
+
* `runStreamed()` is lazy — the actual `child_process.spawn()` happens when
|
|
184
|
+
* the iterator advances, not when `runStreamed()` resolves. Without a lock
|
|
185
|
+
* spanning that window, two callers see overlapping codex children and
|
|
186
|
+
* mis-attribute them on abort, leading to cross-kills.
|
|
187
|
+
*
|
|
188
|
+
* Only the spawn phase is serialized (typically a few hundred ms — codex's
|
|
189
|
+
* first event lands quickly). The actual LLM streaming after the first event
|
|
190
|
+
* runs concurrently as before.
|
|
191
|
+
*/
|
|
192
|
+
let codexSpawnChain: Promise<void> = Promise.resolve();
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Acquire the codex-spawn lock. Returns a release callback. Caller MUST invoke
|
|
196
|
+
* the release callback in a `finally` block (or via the convenience wrapper
|
|
197
|
+
* `withCodexSpawnSerial`) — otherwise the chain stalls indefinitely.
|
|
198
|
+
*/
|
|
199
|
+
export async function acquireCodexSpawnLock(): Promise<() => void> {
|
|
200
|
+
const prev = codexSpawnChain;
|
|
201
|
+
let release!: () => void;
|
|
202
|
+
let released = false;
|
|
203
|
+
codexSpawnChain = new Promise<void>((r) => {
|
|
204
|
+
release = () => {
|
|
205
|
+
if (released) return;
|
|
206
|
+
released = true;
|
|
207
|
+
r();
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
await prev;
|
|
211
|
+
return release;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Convenience wrapper for short, single-await critical sections. Prefer
|
|
216
|
+
* `acquireCodexSpawnLock` when the lock must span multiple awaits including
|
|
217
|
+
* iterator advancement (the codex spawn case).
|
|
218
|
+
*/
|
|
219
|
+
export async function withCodexSpawnSerial<T>(fn: () => Promise<T>): Promise<T> {
|
|
220
|
+
const release = await acquireCodexSpawnLock();
|
|
221
|
+
try {
|
|
222
|
+
return await fn();
|
|
223
|
+
} finally {
|
|
224
|
+
release();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** True iff `pid` is still alive. */
|
|
229
|
+
function isAlive(pid: number): boolean {
|
|
230
|
+
try {
|
|
231
|
+
process.kill(pid, 0);
|
|
232
|
+
return true;
|
|
233
|
+
} catch {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* True iff `pid` is alive and still appears to be the same process captured by
|
|
240
|
+
* `lstart`. An empty lstart means we could not fingerprint the original process,
|
|
241
|
+
* so we refuse to SIGKILL it later rather than risk killing a reused PID.
|
|
242
|
+
*/
|
|
243
|
+
function isSameProcess(pid: number, lstart: string): boolean {
|
|
244
|
+
return lstart !== "" && isAlive(pid) && getProcLstart(pid) === lstart;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
type CodexKillPlan = {
|
|
248
|
+
rootPids: number[];
|
|
249
|
+
targets: Map<number, string>;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/** Capture the complete tree before signalling changes parent/child relationships. */
|
|
253
|
+
function signalCodexTrees(rootPids: number[]): CodexKillPlan | null {
|
|
254
|
+
if (rootPids.length === 0) return null;
|
|
255
|
+
|
|
256
|
+
const targets = new Map<number, string>();
|
|
257
|
+
for (const root of rootPids) {
|
|
258
|
+
for (const pid of collectTree(root)) {
|
|
259
|
+
if (!targets.has(pid)) targets.set(pid, getProcLstart(pid));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
logger.warn({ rootPids, totalPids: targets.size }, "codex-tree-kill: SIGTERM process tree");
|
|
264
|
+
for (const pid of targets.keys()) {
|
|
265
|
+
try {
|
|
266
|
+
process.kill(pid, "SIGTERM");
|
|
267
|
+
} catch {
|
|
268
|
+
// already dead or permission denied
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { rootPids, targets };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function hasSameProcessSurvivors(targets: Map<number, string>): boolean {
|
|
275
|
+
for (const [pid, lstart] of targets) {
|
|
276
|
+
if (isSameProcess(pid, lstart)) return true;
|
|
277
|
+
}
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Kill survivors from a previously fingerprinted plan without risking PID reuse. */
|
|
282
|
+
function killCodexStragglers({ rootPids, targets }: CodexKillPlan): void {
|
|
283
|
+
const aliveTargets: number[] = [];
|
|
284
|
+
let droppedAsPossibleReuse = 0;
|
|
285
|
+
for (const [pid, lstart] of targets) {
|
|
286
|
+
if (isSameProcess(pid, lstart)) {
|
|
287
|
+
aliveTargets.push(pid);
|
|
288
|
+
} else if (isAlive(pid)) {
|
|
289
|
+
droppedAsPossibleReuse++;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (aliveTargets.length === 0) return;
|
|
293
|
+
|
|
294
|
+
// PID-reuse defence: re-walk the trees (children may have spawned siblings
|
|
295
|
+
// mid-SIGTERM), but ONLY signal PIDs that were in the SIGTERM-time targets.
|
|
296
|
+
const reachable = new Set<number>(aliveTargets);
|
|
297
|
+
for (const root of rootPids) {
|
|
298
|
+
const rootLstart = targets.get(root);
|
|
299
|
+
if (rootLstart && isSameProcess(root, rootLstart)) {
|
|
300
|
+
for (const pid of collectTree(root)) reachable.add(pid);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const stragglers: number[] = [];
|
|
304
|
+
for (const pid of reachable) {
|
|
305
|
+
const lstart = targets.get(pid);
|
|
306
|
+
if (lstart && isSameProcess(pid, lstart)) stragglers.push(pid);
|
|
307
|
+
else if (isAlive(pid)) droppedAsPossibleReuse++;
|
|
308
|
+
}
|
|
309
|
+
logger.warn(
|
|
310
|
+
{
|
|
311
|
+
aliveRoots: rootPids.filter((pid) => {
|
|
312
|
+
const lstart = targets.get(pid);
|
|
313
|
+
return lstart ? isSameProcess(pid, lstart) : false;
|
|
314
|
+
}),
|
|
315
|
+
totalStragglers: stragglers.length,
|
|
316
|
+
droppedAsPossibleReuse,
|
|
317
|
+
},
|
|
318
|
+
"codex-tree-kill: SIGKILL fallback after SIGTERM timeout",
|
|
319
|
+
);
|
|
320
|
+
for (const pid of stragglers) {
|
|
321
|
+
try {
|
|
322
|
+
process.kill(pid, "SIGKILL");
|
|
323
|
+
} catch {
|
|
324
|
+
// already dead
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* SIGTERM every PID in the descendant tree of each root in `rootPids`, then
|
|
331
|
+
* schedule a SIGKILL fallback after `SIGKILL_DELAY_MS` for any survivors.
|
|
332
|
+
* Safe to call multiple times; no-op if all PIDs are already dead.
|
|
333
|
+
*/
|
|
334
|
+
export function killCodexTrees(rootPids: number[]): void {
|
|
335
|
+
const plan = signalCodexTrees(rootPids);
|
|
336
|
+
if (!plan) return;
|
|
337
|
+
|
|
338
|
+
const killTimer = setTimeout(() => {
|
|
339
|
+
killCodexStragglers(plan);
|
|
340
|
+
}, SIGKILL_DELAY_MS);
|
|
341
|
+
killTimer.unref?.();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Reap every Codex tree owned by this node during process shutdown.
|
|
346
|
+
*
|
|
347
|
+
* The direct-child snapshot covers the narrow spawn window before a provider
|
|
348
|
+
* has registered its PID. Unlike the normal abort path, this function awaits
|
|
349
|
+
* the grace period with a referenced timer so node shutdown cannot exit before
|
|
350
|
+
* SIGKILL fallback has had a chance to remove resistant MCP/shell descendants.
|
|
351
|
+
*/
|
|
352
|
+
export async function killOwnedCodexTreesForShutdown(graceMs = 3_000): Promise<void> {
|
|
353
|
+
const roots = new Set(globalOwnedCodexPids);
|
|
354
|
+
for (const pid of snapshotCodexChildren().keys()) roots.add(pid);
|
|
355
|
+
const plan = signalCodexTrees([...roots]);
|
|
356
|
+
if (!plan) return;
|
|
357
|
+
|
|
358
|
+
const deadline = Date.now() + Math.max(0, graceMs);
|
|
359
|
+
while (Date.now() < deadline && hasSameProcessSurvivors(plan.targets)) {
|
|
360
|
+
await new Promise<void>((resolve) =>
|
|
361
|
+
setTimeout(resolve, Math.min(100, Math.max(1, deadline - Date.now()))),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
killCodexStragglers(plan);
|
|
365
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ConversationEntry } from "#storage/conversations";
|
|
2
|
+
import type { AgentKind, EffortLevel } from "#types";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Inputs to a rollout synthesis call. The conversation log is provider-agnostic
|
|
6
|
+
* — each registry's encoder reshapes it into the SDK-native rollout format
|
|
7
|
+
* (claude jsonl / codex jsonl) so a subsequent `resume`/`resumeThread` picks
|
|
8
|
+
* up the cross-agent dialogue as if it had always been native.
|
|
9
|
+
*/
|
|
10
|
+
export interface WriteRolloutOptions {
|
|
11
|
+
/** Working directory the resumed session will report. */
|
|
12
|
+
cwd: string;
|
|
13
|
+
/** Provider-agnostic event log to encode. */
|
|
14
|
+
entries: ConversationEntry[];
|
|
15
|
+
/**
|
|
16
|
+
* If set, the synthetic rollout is written using this id as the SDK
|
|
17
|
+
* session/thread key — i.e. the file lands at the SAME path the SDK
|
|
18
|
+
* already manages for an earlier run on this agent.
|
|
19
|
+
*/
|
|
20
|
+
reuseSessionId?: string;
|
|
21
|
+
/** Effective model the synthesized provider session will resume with. */
|
|
22
|
+
model?: string;
|
|
23
|
+
/** Effective reasoning effort recorded by providers whose rollout format carries it. */
|
|
24
|
+
effort?: EffortLevel;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface WriteRolloutResult {
|
|
28
|
+
sessionId: string;
|
|
29
|
+
rolloutPath: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ForkRegistryOptions {
|
|
33
|
+
parentSessionId: string;
|
|
34
|
+
cwd: string;
|
|
35
|
+
userId: number | string;
|
|
36
|
+
topicName: string;
|
|
37
|
+
title?: string;
|
|
38
|
+
/** Effective model/effort of the fork's first resumed turn. */
|
|
39
|
+
model?: string;
|
|
40
|
+
effort?: EffortLevel;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ForkRegistryResult {
|
|
44
|
+
forkId: string;
|
|
45
|
+
rolloutPath: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CleanupRolloutsOptions {
|
|
49
|
+
cwd: string;
|
|
50
|
+
sessionIds: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Per-agent runtime config registry. Centralizes the bits that differ between
|
|
55
|
+
* agents so caller code can stay agent-agnostic.
|
|
56
|
+
*/
|
|
57
|
+
export interface AgentRegistry {
|
|
58
|
+
kind: AgentKind;
|
|
59
|
+
defaultModel: string;
|
|
60
|
+
defaultEffort?: EffortLevel;
|
|
61
|
+
expandModelAlias(s: string): string;
|
|
62
|
+
validateModel(s: string): boolean;
|
|
63
|
+
validEfforts: readonly EffortLevel[];
|
|
64
|
+
validateEffort(s: EffortLevel): boolean;
|
|
65
|
+
footerLabel(model: string, effort?: EffortLevel): string;
|
|
66
|
+
writeRollout(opts: WriteRolloutOptions): WriteRolloutResult;
|
|
67
|
+
forkSession(opts: ForkRegistryOptions): Promise<ForkRegistryResult>;
|
|
68
|
+
cleanupRollouts(opts: CleanupRolloutsOptions): Promise<void>;
|
|
69
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Recursively apply fn to every string value in a nested object/array. */
|
|
2
|
+
export function deepMapStrings(value: unknown, fn: (s: string) => string): unknown {
|
|
3
|
+
if (typeof value === "string") return fn(value);
|
|
4
|
+
if (Array.isArray(value)) return value.map((v) => deepMapStrings(v, fn));
|
|
5
|
+
if (value && typeof value === "object") {
|
|
6
|
+
const out: Record<string, unknown> = {};
|
|
7
|
+
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
|
8
|
+
out[k] = deepMapStrings(v, fn);
|
|
9
|
+
}
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
referencesRuntimeSecretStorage as defaultReferencesRuntimeSecretStorage,
|
|
3
|
+
shouldRedirectVaultTool as defaultShouldRedirectVaultTool,
|
|
4
|
+
} from "#agents/vault-tool-policy";
|
|
5
|
+
import { CLAUDE_EXECUTABLE, codexAuthFilePath } from "#platform/config";
|
|
6
|
+
import { getMcpServersForQuery as defaultGetMcpServersForQuery } from "#platform/mcp-config";
|
|
7
|
+
import { redactVaultSecrets as defaultRedactVaultSecrets } from "#storage/vault";
|
|
8
|
+
import type { AgentQueryOptions } from "#types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Host-owned services used by the provider execution layer.
|
|
12
|
+
*
|
|
13
|
+
* The default implementation preserves standalone Negotium behaviour. An
|
|
14
|
+
* embedding host may replace these callbacks once during process bootstrap so
|
|
15
|
+
* providers use that host's MCP catalog and device-local secret store without
|
|
16
|
+
* importing any host-specific runtime code into Negotium.
|
|
17
|
+
*/
|
|
18
|
+
export interface AgentExecutionHost {
|
|
19
|
+
getMcpServersForQuery(opts: AgentQueryOptions): Record<string, unknown>;
|
|
20
|
+
redactVaultSecrets(userId: string, value: string): string;
|
|
21
|
+
referencesRuntimeSecretStorage(value: unknown): boolean;
|
|
22
|
+
shouldRedirectVaultTool(userId: string, toolName: string, input: unknown): boolean;
|
|
23
|
+
claudeCodeExecutablePath(): string | undefined;
|
|
24
|
+
codexAuthFilePath(): string;
|
|
25
|
+
transformQueryOptions?(opts: AgentQueryOptions): AgentQueryOptions;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const defaultHost: AgentExecutionHost = {
|
|
29
|
+
getMcpServersForQuery: defaultGetMcpServersForQuery,
|
|
30
|
+
redactVaultSecrets: defaultRedactVaultSecrets,
|
|
31
|
+
referencesRuntimeSecretStorage: defaultReferencesRuntimeSecretStorage,
|
|
32
|
+
shouldRedirectVaultTool: defaultShouldRedirectVaultTool,
|
|
33
|
+
claudeCodeExecutablePath: () => CLAUDE_EXECUTABLE,
|
|
34
|
+
codexAuthFilePath,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const hostRegistrations: Array<{
|
|
38
|
+
id: symbol;
|
|
39
|
+
overrides: Partial<AgentExecutionHost>;
|
|
40
|
+
}> = [];
|
|
41
|
+
|
|
42
|
+
function activeHost(): AgentExecutionHost {
|
|
43
|
+
const host = { ...defaultHost };
|
|
44
|
+
for (const registration of hostRegistrations) Object.assign(host, registration.overrides);
|
|
45
|
+
return host;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Configure provider host services. Returns a disposer useful for tests. */
|
|
49
|
+
export function configureAgentExecutionHost(overrides: Partial<AgentExecutionHost>): () => void {
|
|
50
|
+
const registration = { id: Symbol("agent-execution-host"), overrides };
|
|
51
|
+
hostRegistrations.push(registration);
|
|
52
|
+
let disposed = false;
|
|
53
|
+
return () => {
|
|
54
|
+
if (disposed) return;
|
|
55
|
+
disposed = true;
|
|
56
|
+
const index = hostRegistrations.findIndex((entry) => entry.id === registration.id);
|
|
57
|
+
if (index >= 0) hostRegistrations.splice(index, 1);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function hostedMcpServers(opts: AgentQueryOptions): Record<string, unknown> {
|
|
62
|
+
return activeHost().getMcpServersForQuery(opts);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function redactHostedSecrets(userId: string, value: string): string {
|
|
66
|
+
return activeHost().redactVaultSecrets(userId, value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function referencesHostedSecretStorage(value: unknown): boolean {
|
|
70
|
+
return activeHost().referencesRuntimeSecretStorage(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function shouldRedirectHostedVaultTool(
|
|
74
|
+
userId: string,
|
|
75
|
+
toolName: string,
|
|
76
|
+
input: unknown,
|
|
77
|
+
): boolean {
|
|
78
|
+
return activeHost().shouldRedirectVaultTool(userId, toolName, input);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function transformHostedQueryOptions(opts: AgentQueryOptions): AgentQueryOptions {
|
|
82
|
+
return activeHost().transformQueryOptions?.(opts) ?? opts;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function hostedClaudeCodeExecutablePath(): string | undefined {
|
|
86
|
+
return activeHost().claudeCodeExecutablePath();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function hostedCodexAuthFilePath(): string {
|
|
90
|
+
return activeHost().codexAuthFilePath();
|
|
91
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
{"parentUuid":"88e296c8-370a-4c40-a248-10fb661fc81d","isSidechain":false,"attachment":{"type":"deferred_tools_delta","addedNames":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","PushNotification","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch","mcp__claude_ai_Google_Drive__authenticate","mcp__claude_ai_Google_Drive__complete_authentication"],"addedLines":["AskUserQuestion","CronCreate","CronDelete","CronList","EnterPlanMode","EnterWorktree","ExitPlanMode","ExitWorktree","Monitor","NotebookEdit","PushNotification","RemoteTrigger","TaskOutput","TaskStop","TodoWrite","WebFetch","WebSearch","mcp__claude_ai_Google_Drive__authenticate","mcp__claude_ai_Google_Drive__complete_authentication"],"removedNames":[]},"type":"attachment","uuid":"72cd3544-9370-45f4-97c7-700ac90fcb7f","timestamp":"2026-05-01T13:31:34.909Z","userType":"external","entrypoint":"sdk-ts","cwd":"/private/tmp/claude-poc-cwd","sessionId":"104a73cb-ae08-4464-b655-753b906cb197","version":"2.1.126","gitBranch":"HEAD"}
|
|
2
|
+
{"parentUuid":"72cd3544-9370-45f4-97c7-700ac90fcb7f","isSidechain":false,"attachment":{"type":"skill_listing","content":"- update-config: Use this skill to configure the Claude Code harness via settings.json. Automated behaviors (\"from now on when X\", \"each time X\", \"whenever X\", \"before/after X\") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions (\"allow X\", \"add permission\", \"move permission to\"), env vars (\"set X=Y\"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: \"allow npm commands\", \"add bq permission to global settings\", \"move permission to user settings\", \"set DEBUG=true\", \"when claude stops show X\". For simple settings like theme/model, suggest the /config command.\n- keybindings-help: Use when the user wants to customize keyboard shortcuts, rebind keys, add chord bindings, or modify ~/.claude/keybindings.json. Examples: \"rebind ctrl+s\", \"add a chord shortcut\", \"change the submit key\", \"customize keybindings\".\n- simplify: Review changed code for reuse, quality, and efficiency, then fix any issues found.\n- fewer-permission-prompts: Scan your transcripts for common read-only Bash and MCP tool calls, then add a prioritized allowlist to project .claude/settings.json to reduce permission prompts.\n- loop: Run a prompt or slash command on a recurring interval (e.g. /loop 5m /foo). Omit the interval to let the model self-pace. - When the user wants to set up a recurring task, poll for status, or run something repeatedly on an interval (e.g. \"check the deploy every 5 minutes\", \"keep running /babysit-prs\"). Do NOT invoke for one-off tasks.\n- schedule: Create, update, list, or run scheduled remote agents (routines) that execute on a cron schedule. - When the user wants to schedule a recurring or one-time remote agent (\"run this every Monday\", \"open a cleanup PR for X in 2 weeks\"), or to manage existing routines. ALSO OFFER PROACTIVELY: after you finish work that has a natural future follow-up, end your reply with a one-line offer to schedule a background agent to do it. Strong signals: a feature flag / gate / experiment / staged rollout was just shipped (offer a one-time agent in ~2 weeks to open a cleanup PR or evaluate results), a new alert/monitor was created (offer a recurring agent to triage it), a TODO/migration with a \"remove once X\" condition was left behind (offer a one-time agent to do the removal). Skip the offer for refactors, bug fixes, and anything that is done once it ships. Name a concrete action and cadence (\"in 2 weeks\", \"every Monday\") and only offer when the run just succeeded — do not pitch a schedule for something that has not happened yet.\n- claude-api: Build, debug, and optimize Claude API / Anthropic SDK apps. Apps built with this skill should include prompt caching. Also handles migrating existing Claude API code between Claude model versions (4.5 → 4.6, 4.6 → 4.7, retired-model replacements).\nTRIGGER when: code imports `anthropic`/`@anthropic-ai/sdk`; user asks for the Claude API, Anthropic SDK, or Managed Agents; user adds/modifies/tunes a Claude feature (caching, thinking, compaction, tool use, batch, files, citations, memory) or model (Opus/Sonnet/Haiku) in a file; questions about prompt caching / cache hit rate in an Anthropic SDK project.\nSKIP: file imports `openai`/other-provider SDK, filename like `*-openai.py`/`*-generic.py`, provider-neutral code, general programming/ML.\n- init: Initialize a new CLAUDE.md file with codebase documentation\n- review: Review a pull request\n- security-review: Complete a security review of the pending changes on the current branch","skillCount":10,"isInitial":true},"type":"attachment","uuid":"51fe817e-380d-42d7-a4f4-b6d75e19ba63","timestamp":"2026-05-01T13:31:34.909Z","userType":"external","entrypoint":"sdk-ts","cwd":"/private/tmp/claude-poc-cwd","sessionId":"104a73cb-ae08-4464-b655-753b906cb197","version":"2.1.126","gitBranch":"HEAD"}
|