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,178 @@
|
|
|
1
|
+
import { unlinkSync } from "node:fs";
|
|
2
|
+
import { checkAgentAuth } from "#agents/auth-check";
|
|
3
|
+
import { resolveModelForAgent } from "#agents/model-catalog";
|
|
4
|
+
import { getRegistry } from "#agents/registry";
|
|
5
|
+
import { logger } from "#platform/logger";
|
|
6
|
+
import { setApiTopicConfig, type TopicConfig } from "#storage/api-topic-config";
|
|
7
|
+
import { clearTopicSessionId, setApiTopicAgent, setTopicSessionId } from "#storage/api-topics";
|
|
8
|
+
import {
|
|
9
|
+
appendConversationEventStrict,
|
|
10
|
+
findLastSessionIdForAgent,
|
|
11
|
+
readConversation,
|
|
12
|
+
} from "#storage/conversations";
|
|
13
|
+
import type { AgentKind, EffortLevel } from "#types";
|
|
14
|
+
|
|
15
|
+
export type ApiTopicSwitchOutcome =
|
|
16
|
+
| { kind: "fresh"; agent: AgentKind; reason: "no-history" | "bridge-failed" }
|
|
17
|
+
| { kind: "bridged"; agent: AgentKind; bridgedSessionId: string; rolloutPath: string };
|
|
18
|
+
|
|
19
|
+
export type ApiTopicSwitchResult =
|
|
20
|
+
| { ok: true; outcome: ApiTopicSwitchOutcome }
|
|
21
|
+
| { ok: false; error: string };
|
|
22
|
+
|
|
23
|
+
export interface SwitchApiTopicAgentOptions {
|
|
24
|
+
topicId: string;
|
|
25
|
+
topicTitle: string;
|
|
26
|
+
userId: string;
|
|
27
|
+
fromAgent?: AgentKind;
|
|
28
|
+
agent: AgentKind;
|
|
29
|
+
cwd: string;
|
|
30
|
+
config: TopicConfig;
|
|
31
|
+
/** Topic base values used when config has no explicit override. */
|
|
32
|
+
defaultModel?: string;
|
|
33
|
+
defaultEffort?: EffortLevel;
|
|
34
|
+
reason: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function commitApiTopicSwitch(
|
|
38
|
+
opts: SwitchApiTopicAgentOptions,
|
|
39
|
+
bridgedSessionId: string | null,
|
|
40
|
+
): void {
|
|
41
|
+
const registry = getRegistry(opts.agent);
|
|
42
|
+
const model = resolveModelForAgent(opts.agent, opts.defaultModel, registry);
|
|
43
|
+
const effort =
|
|
44
|
+
opts.defaultEffort && registry.validateEffort(opts.defaultEffort)
|
|
45
|
+
? opts.defaultEffort
|
|
46
|
+
: registry.defaultEffort;
|
|
47
|
+
// Keep the durable topic DTO internally consistent too. The turn runner's
|
|
48
|
+
// model-owner guard remains the final safety net for legacy rows, but a
|
|
49
|
+
// Codex topic should never continue advertising Maestro's deepseek-pro.
|
|
50
|
+
setApiTopicAgent(opts.topicId, opts.agent, { model, effort });
|
|
51
|
+
setApiTopicConfig(opts.topicId, opts.config);
|
|
52
|
+
if (bridgedSessionId) {
|
|
53
|
+
setTopicSessionId(opts.topicId, bridgedSessionId, {
|
|
54
|
+
reason: opts.reason,
|
|
55
|
+
agent: opts.agent,
|
|
56
|
+
});
|
|
57
|
+
} else {
|
|
58
|
+
clearTopicSessionId(opts.topicId, opts.reason);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* API-topic equivalent of `switchTopicAgent`: all web/MCP agent changes must
|
|
64
|
+
* pass through this path so the synthetic SDK rollout, unified-log manifest,
|
|
65
|
+
* and durable topic session stay in sync.
|
|
66
|
+
*/
|
|
67
|
+
export function switchApiTopicAgent(opts: SwitchApiTopicAgentOptions): ApiTopicSwitchResult {
|
|
68
|
+
const auth = checkAgentAuth(opts.agent);
|
|
69
|
+
if (!auth.ok) return { ok: false, error: auth.error };
|
|
70
|
+
|
|
71
|
+
const entries = readConversation(opts.userId, opts.topicTitle);
|
|
72
|
+
if (entries.length === 0) {
|
|
73
|
+
try {
|
|
74
|
+
commitApiTopicSwitch(opts, null);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
logger.warn({ err, topicId: opts.topicId, agent: opts.agent }, "api-topic switch failed");
|
|
77
|
+
return { ok: false, error: `failed to set agent for "${opts.topicTitle}"` };
|
|
78
|
+
}
|
|
79
|
+
return { ok: true, outcome: { kind: "fresh", agent: opts.agent, reason: "no-history" } };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let bridgedSessionId: string;
|
|
83
|
+
let rolloutPath: string;
|
|
84
|
+
try {
|
|
85
|
+
const reuseSessionId = findLastSessionIdForAgent(entries, opts.agent) ?? undefined;
|
|
86
|
+
const registry = getRegistry(opts.agent);
|
|
87
|
+
const model = resolveModelForAgent(
|
|
88
|
+
opts.agent,
|
|
89
|
+
opts.config.model ?? opts.defaultModel,
|
|
90
|
+
registry,
|
|
91
|
+
);
|
|
92
|
+
const requestedEffort = opts.config.effort ?? opts.defaultEffort;
|
|
93
|
+
const effort =
|
|
94
|
+
requestedEffort && registry.validateEffort(requestedEffort)
|
|
95
|
+
? requestedEffort
|
|
96
|
+
: registry.defaultEffort;
|
|
97
|
+
const result = registry.writeRollout({
|
|
98
|
+
cwd: opts.cwd,
|
|
99
|
+
entries,
|
|
100
|
+
model,
|
|
101
|
+
...(effort ? { effort } : {}),
|
|
102
|
+
...(reuseSessionId ? { reuseSessionId } : {}),
|
|
103
|
+
});
|
|
104
|
+
bridgedSessionId = result.sessionId;
|
|
105
|
+
rolloutPath = result.rolloutPath;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
logger.warn(
|
|
108
|
+
{
|
|
109
|
+
err,
|
|
110
|
+
topicId: opts.topicId,
|
|
111
|
+
topicTitle: opts.topicTitle,
|
|
112
|
+
from: opts.fromAgent,
|
|
113
|
+
to: opts.agent,
|
|
114
|
+
},
|
|
115
|
+
"api-topic switch: rollout encoding failed — falling back to fresh session",
|
|
116
|
+
);
|
|
117
|
+
try {
|
|
118
|
+
commitApiTopicSwitch(opts, null);
|
|
119
|
+
} catch (commitErr) {
|
|
120
|
+
logger.warn(
|
|
121
|
+
{ err: commitErr, topicId: opts.topicId, agent: opts.agent },
|
|
122
|
+
"api-topic switch fallback commit failed",
|
|
123
|
+
);
|
|
124
|
+
return { ok: false, error: `failed to set agent for "${opts.topicTitle}"` };
|
|
125
|
+
}
|
|
126
|
+
return { ok: true, outcome: { kind: "fresh", agent: opts.agent, reason: "bridge-failed" } };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
appendConversationEventStrict(opts.userId, opts.topicTitle, opts.agent, {
|
|
131
|
+
type: "session",
|
|
132
|
+
sessionId: bridgedSessionId,
|
|
133
|
+
});
|
|
134
|
+
} catch (err) {
|
|
135
|
+
try {
|
|
136
|
+
unlinkSync(rolloutPath);
|
|
137
|
+
} catch {
|
|
138
|
+
// best-effort
|
|
139
|
+
}
|
|
140
|
+
logger.warn(
|
|
141
|
+
{ err, topicId: opts.topicId, rolloutPath },
|
|
142
|
+
"api-topic switch: manifest append failed, removed orphan rollout",
|
|
143
|
+
);
|
|
144
|
+
return { ok: false, error: `failed to record agent switch for "${opts.topicTitle}"` };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
commitApiTopicSwitch(opts, bridgedSessionId);
|
|
149
|
+
} catch (err) {
|
|
150
|
+
try {
|
|
151
|
+
unlinkSync(rolloutPath);
|
|
152
|
+
} catch {
|
|
153
|
+
// best-effort
|
|
154
|
+
}
|
|
155
|
+
logger.warn(
|
|
156
|
+
{ err, topicId: opts.topicId, rolloutPath },
|
|
157
|
+
"api-topic switch: config/session commit failed, removed orphan rollout",
|
|
158
|
+
);
|
|
159
|
+
return { ok: false, error: `failed to commit agent switch for "${opts.topicTitle}"` };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
logger.info(
|
|
163
|
+
{
|
|
164
|
+
topicId: opts.topicId,
|
|
165
|
+
from: opts.fromAgent,
|
|
166
|
+
to: opts.agent,
|
|
167
|
+
sessionId: bridgedSessionId,
|
|
168
|
+
rolloutPath,
|
|
169
|
+
entries: entries.length,
|
|
170
|
+
},
|
|
171
|
+
"api-topic switch: rollout bridged from unified log",
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
ok: true,
|
|
176
|
+
outcome: { kind: "bridged", agent: opts.agent, bridgedSessionId, rolloutPath },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { runAgent } from "#agents/index";
|
|
5
|
+
import { WsHub } from "#bus";
|
|
6
|
+
import { WORKSPACE_DIR } from "#platform/config";
|
|
7
|
+
import { logger } from "#platform/logger";
|
|
8
|
+
import { type AgentDef, loadAgentPrompt } from "#prompts/builders";
|
|
9
|
+
import { sanitizeTopicName } from "#security/sanitize";
|
|
10
|
+
import { appendApiMessage } from "#storage/api-messages";
|
|
11
|
+
import { getTopicBrief, setTopicBrief } from "#storage/api-topic-brief";
|
|
12
|
+
import { getSharedWikiDir } from "#storage/wiki";
|
|
13
|
+
import { wikiSummaryFilename } from "#storage/wiki-summary-names";
|
|
14
|
+
import { ensurePersonalGeneral } from "#topics/personal-general";
|
|
15
|
+
import type { AgentKind } from "#types";
|
|
16
|
+
import type { BackgroundSessionDto } from "#types/api";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Rolling cap on the #General digest. The General-brief injection embeds
|
|
20
|
+
* `briefMd` whole (no slice), so an unbounded digest would inflate every
|
|
21
|
+
* General turn's system prompt — keep it to the most recent N topics (~<2KB).
|
|
22
|
+
*/
|
|
23
|
+
const MAX_BRIEF_ENTRIES = 8;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Below this message count a session has no extractable substance worth a full
|
|
27
|
+
* archiver turn (greetings, a single quick question). Mirrors Otium's
|
|
28
|
+
* `MIN_EXCHANGE_COUNT` gate.
|
|
29
|
+
*/
|
|
30
|
+
const MIN_ARCHIVE_MESSAGES = 4;
|
|
31
|
+
const MAX_SESSION_STEPS = 20;
|
|
32
|
+
|
|
33
|
+
interface ActiveArchiverSession extends BackgroundSessionDto {
|
|
34
|
+
userId: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const activeArchiverSessions = new Map<string, ActiveArchiverSession>();
|
|
38
|
+
|
|
39
|
+
function boundedSessionText(value: string): string {
|
|
40
|
+
return value.replace(/\s+/g, " ").trim().slice(0, 160);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function updateArchiverSession(id: string, status: string, step?: string): void {
|
|
44
|
+
const session = activeArchiverSessions.get(id);
|
|
45
|
+
if (!session) return;
|
|
46
|
+
session.status = boundedSessionText(status) || session.status;
|
|
47
|
+
if (step) {
|
|
48
|
+
const text = boundedSessionText(step);
|
|
49
|
+
if (text && session.steps.at(-1) !== text) {
|
|
50
|
+
session.steps.push(text);
|
|
51
|
+
if (session.steps.length > MAX_SESSION_STEPS) session.steps.shift();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function listActiveMemoryArchiverSessions(userId: string): BackgroundSessionDto[] {
|
|
57
|
+
return [...activeArchiverSessions.values()]
|
|
58
|
+
.filter((session) => session.userId === userId)
|
|
59
|
+
.map(({ userId: _userId, ...session }) => ({ ...session, steps: [...session.steps] }));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The wiki-archiver prompt is loaded once and cached — a missing/!malformed
|
|
63
|
+
// file must not crash the delete path, so loading is wrapped at call sites.
|
|
64
|
+
let _archiverDef: AgentDef | null = null;
|
|
65
|
+
function getArchiverDef(): AgentDef {
|
|
66
|
+
if (!_archiverDef) _archiverDef = loadAgentPrompt("wiki-archiver.md");
|
|
67
|
+
return _archiverDef;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface RunArchiverTurnParams {
|
|
71
|
+
/** Stringified user id (route layer hands us strings). */
|
|
72
|
+
userId: string;
|
|
73
|
+
/** Wiki-memory topic id. Derived topics pass their root memory origin here. */
|
|
74
|
+
topicId?: string;
|
|
75
|
+
/** Human-readable topic title — becomes the wiki `topic` name. */
|
|
76
|
+
topicTitle: string;
|
|
77
|
+
/** Absolute path to the JSONL archive produced by `archiveTopicMessages`. */
|
|
78
|
+
archivePath: string;
|
|
79
|
+
/** Number of messages in the archive — gates the MIN threshold. */
|
|
80
|
+
messageCount: number;
|
|
81
|
+
/** Deleted topics update the #General memory hub; active idle snapshots do not. */
|
|
82
|
+
mode?: "deleted-topic" | "active-topic";
|
|
83
|
+
/** Override the archiver agent backend (default: maestro). */
|
|
84
|
+
agent?: AgentKind;
|
|
85
|
+
/** Override the archiver model (default: the prompt's frontmatter model). */
|
|
86
|
+
model?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface GeneralArchiverReply {
|
|
90
|
+
text: string;
|
|
91
|
+
agent: AgentKind;
|
|
92
|
+
model?: string;
|
|
93
|
+
usage?: { input: number; output: number };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Fire a background wiki-archiver turn for a deleted topic or active idle
|
|
98
|
+
* snapshot.
|
|
99
|
+
*
|
|
100
|
+
* Reads the forensic archive file produced by `archiveTopicMessages` and
|
|
101
|
+
* distils it into the **shared** wiki (summaries / articles / topic brief /
|
|
102
|
+
* indexes / skills) via the wiki MCP, which the turn launches in topic-id mode
|
|
103
|
+
* so its writes land in the same root normal forum turns query.
|
|
104
|
+
*
|
|
105
|
+
* Fire-and-forget: the async generator is drained on a detached promise so the
|
|
106
|
+
* caller's HTTP response returns immediately. Every failure is best-effort and
|
|
107
|
+
* logged only — a broken archiver must never block or fail a topic deletion.
|
|
108
|
+
*/
|
|
109
|
+
export function runArchiverTurn(params: RunArchiverTurnParams): void {
|
|
110
|
+
const { userId, topicId, topicTitle, archivePath, messageCount, mode = "deleted-topic" } = params;
|
|
111
|
+
|
|
112
|
+
if (messageCount < MIN_ARCHIVE_MESSAGES) {
|
|
113
|
+
logger.info(
|
|
114
|
+
{ userId, topicTitle, messageCount, min: MIN_ARCHIVE_MESSAGES },
|
|
115
|
+
"archiver: skipped — too few messages to distil",
|
|
116
|
+
);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let archiverDef: AgentDef;
|
|
121
|
+
try {
|
|
122
|
+
archiverDef = getArchiverDef();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
logger.warn({ err }, "archiver: failed to load wiki-archiver.md — skipping");
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const wikiDir = getSharedWikiDir();
|
|
129
|
+
const safeTopic = sanitizeTopicName(topicTitle, true);
|
|
130
|
+
// Keep the established Claude default for archive quality and compatibility.
|
|
131
|
+
// Every provider now receives the same host-resolved wiki MCP; callers may
|
|
132
|
+
// override both agent and model as a matching pair when desired.
|
|
133
|
+
const agent: AgentKind = params.agent ?? "claude";
|
|
134
|
+
const model = params.model;
|
|
135
|
+
|
|
136
|
+
const prompt =
|
|
137
|
+
mode === "active-topic"
|
|
138
|
+
? [
|
|
139
|
+
`세션 "${topicTitle}" 의 최근 idle 대화 snapshot입니다. 아래 아카이브에서 기억을 추출해 이 토픽 위키에 저장해줘.`,
|
|
140
|
+
`archive_path: ${archivePath}`,
|
|
141
|
+
`wiki_dir: ${wikiDir}`,
|
|
142
|
+
].join("\n")
|
|
143
|
+
: [
|
|
144
|
+
`세션 "${topicTitle}" 이(가) 삭제되었습니다. 아래 아카이브에서 기억을 추출해 위키에 저장해줘.`,
|
|
145
|
+
`archive_path: ${archivePath}`,
|
|
146
|
+
`wiki_dir: ${wikiDir}`,
|
|
147
|
+
"",
|
|
148
|
+
"#General에 표시될 짧은 한국어 완료 메시지로 최종 응답해줘. " +
|
|
149
|
+
"도구 호출 로그나 원문 전문은 쓰지 말고, 저장한 요약/브리프/문서만 간단히 말해줘.",
|
|
150
|
+
].join("\n");
|
|
151
|
+
|
|
152
|
+
// cwd = workspace root so the archiver's relative `wiki/...` Glob/Read paths
|
|
153
|
+
// resolve against the same shared wiki the MCP writes to.
|
|
154
|
+
const abortController = new AbortController();
|
|
155
|
+
const events = runAgent({
|
|
156
|
+
agent,
|
|
157
|
+
prompt,
|
|
158
|
+
cwd: WORKSPACE_DIR,
|
|
159
|
+
systemPrompt: archiverDef.prompt,
|
|
160
|
+
userId,
|
|
161
|
+
// Throwaway session name → wiki MCP runs in topic-id (shared-root) mode.
|
|
162
|
+
session: `__archiver_${safeTopic}`,
|
|
163
|
+
sessionType: "forum",
|
|
164
|
+
topicId,
|
|
165
|
+
abortController,
|
|
166
|
+
model,
|
|
167
|
+
// Limit MCP surface to the wiki server (no playwright/bg-bash/etc.).
|
|
168
|
+
mcpEnabled: ["wiki"],
|
|
169
|
+
// Hidden run: don't record to the cross-agent conversation log.
|
|
170
|
+
silent: true,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const activeSessionId = `memory:${randomUUID()}`;
|
|
174
|
+
activeArchiverSessions.set(activeSessionId, {
|
|
175
|
+
id: activeSessionId,
|
|
176
|
+
kind: "memory",
|
|
177
|
+
title: `Archive ${topicTitle}`,
|
|
178
|
+
topicId,
|
|
179
|
+
userId,
|
|
180
|
+
startedAt: new Date().toISOString(),
|
|
181
|
+
status: "Starting",
|
|
182
|
+
active: true,
|
|
183
|
+
agent,
|
|
184
|
+
model: model ?? archiverDef.model,
|
|
185
|
+
prompt,
|
|
186
|
+
promptTitle: "Prompt",
|
|
187
|
+
steps: ["Preparing archived conversation"],
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
logger.info(
|
|
191
|
+
{ userId, topicTitle, archivePath, agent, model },
|
|
192
|
+
"archiver: starting background turn",
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
// Baseline for locating the summary file the turn is about to write (fallback
|
|
196
|
+
// when the predicted filename misses). Stamped before draining.
|
|
197
|
+
const startMs = Date.now();
|
|
198
|
+
|
|
199
|
+
void (async () => {
|
|
200
|
+
let ok = false;
|
|
201
|
+
let sawDelta = false;
|
|
202
|
+
let accumulatedText = "";
|
|
203
|
+
let resultText = "";
|
|
204
|
+
let usage: GeneralArchiverReply["usage"] | undefined;
|
|
205
|
+
try {
|
|
206
|
+
// Drain the stream — the turn's side effects (wiki writes) are the point;
|
|
207
|
+
// only the final assistant text is surfaced to #General for deleted topics.
|
|
208
|
+
for await (const event of events) {
|
|
209
|
+
switch (event.type) {
|
|
210
|
+
case "tool_use":
|
|
211
|
+
updateArchiverSession(activeSessionId, `Running ${event.name}`, `Tool: ${event.name}`);
|
|
212
|
+
break;
|
|
213
|
+
case "tool_progress":
|
|
214
|
+
{
|
|
215
|
+
const progress = `${event.toolName === "thinking" ? "Thinking" : event.toolName} · ${Math.max(0, Math.floor(event.elapsed))}s`;
|
|
216
|
+
updateArchiverSession(activeSessionId, progress, progress);
|
|
217
|
+
}
|
|
218
|
+
break;
|
|
219
|
+
case "reasoning":
|
|
220
|
+
updateArchiverSession(activeSessionId, "Reasoning", `Reasoning: ${event.content}`);
|
|
221
|
+
break;
|
|
222
|
+
case "tool_use_summary":
|
|
223
|
+
updateArchiverSession(activeSessionId, event.summary, event.summary);
|
|
224
|
+
break;
|
|
225
|
+
case "status":
|
|
226
|
+
updateArchiverSession(activeSessionId, event.content, event.content);
|
|
227
|
+
break;
|
|
228
|
+
case "tool_result":
|
|
229
|
+
updateArchiverSession(activeSessionId, "Processing tool result");
|
|
230
|
+
break;
|
|
231
|
+
case "text_delta":
|
|
232
|
+
sawDelta = true;
|
|
233
|
+
accumulatedText += event.content;
|
|
234
|
+
break;
|
|
235
|
+
case "text":
|
|
236
|
+
if (!sawDelta) accumulatedText += event.content;
|
|
237
|
+
break;
|
|
238
|
+
case "result":
|
|
239
|
+
updateArchiverSession(activeSessionId, "Finalizing memory");
|
|
240
|
+
resultText = event.content;
|
|
241
|
+
usage = event.usage
|
|
242
|
+
? { input: event.usage.inputTokens, output: event.usage.outputTokens }
|
|
243
|
+
: undefined;
|
|
244
|
+
break;
|
|
245
|
+
default:
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
ok = true;
|
|
250
|
+
logger.info({ userId, topicTitle }, "archiver: background turn completed");
|
|
251
|
+
} catch (err) {
|
|
252
|
+
updateArchiverSession(activeSessionId, "Failed", "Memory archive failed");
|
|
253
|
+
logger.warn({ err, userId, topicTitle }, "archiver: background turn failed");
|
|
254
|
+
} finally {
|
|
255
|
+
activeArchiverSessions.delete(activeSessionId);
|
|
256
|
+
}
|
|
257
|
+
if (mode === "deleted-topic") {
|
|
258
|
+
// Roll the deleted topic into the #General memory hub regardless of LLM
|
|
259
|
+
// success — even a failed distillation should leave a digest breadcrumb.
|
|
260
|
+
const text = (accumulatedText.trim() ? accumulatedText : resultText).trimEnd();
|
|
261
|
+
finalizeGeneralMemory(
|
|
262
|
+
userId,
|
|
263
|
+
topicTitle,
|
|
264
|
+
messageCount,
|
|
265
|
+
startMs,
|
|
266
|
+
ok,
|
|
267
|
+
topicId,
|
|
268
|
+
ok && text ? { text, agent, model, usage } : undefined,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
})();
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// --- #General memory-hub digest ------------------------------------------
|
|
275
|
+
|
|
276
|
+
/** Pull the first non-heading, non-frontmatter line of a summary as a digest. */
|
|
277
|
+
function distillOneLine(summaryMd: string): string {
|
|
278
|
+
const body = summaryMd.replace(/^---[\s\S]*?---\n?/, "");
|
|
279
|
+
for (const raw of body.split("\n")) {
|
|
280
|
+
const line = raw.trim();
|
|
281
|
+
if (!line || line.startsWith("#")) continue;
|
|
282
|
+
return line.replace(/^[-*]\s*/, "").slice(0, 160);
|
|
283
|
+
}
|
|
284
|
+
return "";
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Locate the summary the archiver turn just wrote. The wiki MCP's
|
|
289
|
+
* `save_wiki_entry` writes `summaries/<date>-<slug>.md`, keyed by topic id
|
|
290
|
+
* when one is available and by title for legacy/ephemeral fallback paths.
|
|
291
|
+
*/
|
|
292
|
+
function findSummaryFile(
|
|
293
|
+
topicTitle: string,
|
|
294
|
+
date: string,
|
|
295
|
+
sinceMs: number,
|
|
296
|
+
topicId?: string,
|
|
297
|
+
): string | null {
|
|
298
|
+
const dir = join(getSharedWikiDir(), "summaries");
|
|
299
|
+
if (!existsSync(dir)) return null;
|
|
300
|
+
|
|
301
|
+
const predicted = join(dir, wikiSummaryFilename(date, topicTitle, topicId));
|
|
302
|
+
if (existsSync(predicted)) return predicted;
|
|
303
|
+
|
|
304
|
+
let best: { path: string; mtime: number } | null = null;
|
|
305
|
+
for (const f of readdirSync(dir)) {
|
|
306
|
+
if (!f.endsWith(".md")) continue;
|
|
307
|
+
const p = join(dir, f);
|
|
308
|
+
try {
|
|
309
|
+
const m = statSync(p).mtimeMs;
|
|
310
|
+
if (m >= sinceMs && (!best || m > best.mtime)) best = { path: p, mtime: m };
|
|
311
|
+
} catch {
|
|
312
|
+
/* skip unreadable */
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return best?.path ?? null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Update the #General brief with a rolling digest entry for the archived topic
|
|
320
|
+
* and post the archiver completion reply. The brief feeds the General turn's
|
|
321
|
+
* system prompt, so this is the channel that carries deleted-topic memory into
|
|
322
|
+
* the user's private hub. Best-effort — never throws.
|
|
323
|
+
*/
|
|
324
|
+
function finalizeGeneralMemory(
|
|
325
|
+
userId: string,
|
|
326
|
+
topicTitle: string,
|
|
327
|
+
messageCount: number,
|
|
328
|
+
startMs: number,
|
|
329
|
+
ok: boolean,
|
|
330
|
+
topicId?: string,
|
|
331
|
+
generalReply?: GeneralArchiverReply,
|
|
332
|
+
): void {
|
|
333
|
+
const generalTopicId = ensurePersonalGeneral(userId).id;
|
|
334
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
335
|
+
try {
|
|
336
|
+
const summaryPath = ok ? findSummaryFile(topicTitle, date, startMs, topicId) : null;
|
|
337
|
+
const summaryMd = summaryPath ? readFileSync(summaryPath, "utf-8") : "";
|
|
338
|
+
const oneLine = (summaryMd && distillOneLine(summaryMd)) || `${messageCount}개 메시지 아카이브`;
|
|
339
|
+
|
|
340
|
+
// Rolling digest: dedupe same-title, prepend newest, cap at MAX_BRIEF_ENTRIES.
|
|
341
|
+
const prev = getTopicBrief(generalTopicId);
|
|
342
|
+
const prevEntries = (prev?.briefMd ?? "")
|
|
343
|
+
.split("\n")
|
|
344
|
+
.map((l) => l.trim())
|
|
345
|
+
.filter((l) => l.startsWith("- "));
|
|
346
|
+
const newEntry = `- **${topicTitle}** (${date}): ${oneLine}`;
|
|
347
|
+
const rolled = [
|
|
348
|
+
newEntry,
|
|
349
|
+
...prevEntries.filter((l) => !l.startsWith(`- **${topicTitle}** `)),
|
|
350
|
+
].slice(0, MAX_BRIEF_ENTRIES);
|
|
351
|
+
const briefMd = `# 워크스페이스 메모리 허브\n\n삭제된 토픽에서 추출한 최근 기억 다이제스트입니다. 자세한 내용은 \`wiki_query\`로 조회하세요.\n\n## 최근 아카이브\n${rolled.join("\n")}`;
|
|
352
|
+
|
|
353
|
+
setTopicBrief(generalTopicId, {
|
|
354
|
+
briefMd,
|
|
355
|
+
...(summaryMd ? { latestSummaryMd: summaryMd, summaryDate: date } : {}),
|
|
356
|
+
});
|
|
357
|
+
logger.info(
|
|
358
|
+
{ topicTitle, summaryPath, entries: rolled.length },
|
|
359
|
+
"archiver: updated #General memory hub brief",
|
|
360
|
+
);
|
|
361
|
+
} catch (err) {
|
|
362
|
+
logger.warn({ err, topicTitle }, "archiver: failed to update #General brief");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// (a) Visible completion message in #General so participants see the hub absorbed the topic.
|
|
366
|
+
// Prefer the archiver's own final chat response; fall back to a terse system
|
|
367
|
+
// message if the run failed or produced no visible assistant text.
|
|
368
|
+
try {
|
|
369
|
+
const replyText = generalReply?.text.trim();
|
|
370
|
+
const text =
|
|
371
|
+
replyText ||
|
|
372
|
+
(ok
|
|
373
|
+
? `🗂 "${topicTitle}" 토픽이 삭제되어 #General 메모리에 아카이브됐어요.`
|
|
374
|
+
: `🗂 "${topicTitle}" 토픽을 아카이브했어요 (요약 추출은 실패 — 원본은 wiki/archive에 보존).`);
|
|
375
|
+
const replyMeta =
|
|
376
|
+
generalReply && replyText
|
|
377
|
+
? {
|
|
378
|
+
authorId: "ai",
|
|
379
|
+
agentType: generalReply.agent,
|
|
380
|
+
model: generalReply.model,
|
|
381
|
+
usage: generalReply.usage,
|
|
382
|
+
}
|
|
383
|
+
: { authorId: "system" };
|
|
384
|
+
const msg = {
|
|
385
|
+
id: randomUUID(),
|
|
386
|
+
topicId: generalTopicId,
|
|
387
|
+
text,
|
|
388
|
+
...replyMeta,
|
|
389
|
+
createdAt: new Date().toISOString(),
|
|
390
|
+
};
|
|
391
|
+
appendApiMessage(msg);
|
|
392
|
+
WsHub.get().broadcastMessage(generalTopicId, msg);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
logger.warn({ err, topicTitle }, "archiver: failed to post #General notification");
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cheap pre-flight auth check used to block agent switches that would
|
|
3
|
+
* otherwise flip the topic to a backend the user hasn't logged into,
|
|
4
|
+
* surfacing as a per-turn opaque error inside the provider. Runtime
|
|
5
|
+
* failures (revoked tokens, expired sessions) still classify normally
|
|
6
|
+
* via `event-processor.classifyError`.
|
|
7
|
+
*
|
|
8
|
+
* Each branch mirrors the auth mechanism the matching provider uses:
|
|
9
|
+
* - codex: `~/.codex/auth.json` (override `NEGOTIUM_CODEX_AUTH_FILE`)
|
|
10
|
+
* - claude: `ANTHROPIC_API_KEY` env, else macOS keychain entry
|
|
11
|
+
* `Claude Code-credentials`, else `~/.claude/.credentials.json`
|
|
12
|
+
* - maestro: `DEEPSEEK_API_KEY` env
|
|
13
|
+
*/
|
|
14
|
+
import { execFileSync } from "node:child_process";
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { homedir, platform } from "node:os";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { codexAuthFilePath } from "#platform/config";
|
|
19
|
+
import type { AgentKind } from "#types";
|
|
20
|
+
|
|
21
|
+
export type AuthCheckResult = { ok: true } | { ok: false; error: string };
|
|
22
|
+
|
|
23
|
+
export function checkAgentAuth(agent: AgentKind): AuthCheckResult {
|
|
24
|
+
switch (agent) {
|
|
25
|
+
case "codex": {
|
|
26
|
+
const path = codexAuthFilePath();
|
|
27
|
+
if (existsSync(path)) return { ok: true };
|
|
28
|
+
return {
|
|
29
|
+
ok: false,
|
|
30
|
+
error: `codex is not logged in (auth file missing at ${path}). Run \`codex login\` first`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
case "claude": {
|
|
34
|
+
if (process.env.ANTHROPIC_API_KEY) return { ok: true };
|
|
35
|
+
if (platform() === "darwin") {
|
|
36
|
+
try {
|
|
37
|
+
execFileSync("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
|
|
38
|
+
stdio: "ignore",
|
|
39
|
+
});
|
|
40
|
+
return { ok: true };
|
|
41
|
+
} catch {
|
|
42
|
+
return {
|
|
43
|
+
ok: false,
|
|
44
|
+
error:
|
|
45
|
+
"claude is not logged in (no macOS keychain entry 'Claude Code-credentials'). Run `claude` and complete login first",
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const path = join(homedir(), ".claude", ".credentials.json");
|
|
50
|
+
if (existsSync(path)) return { ok: true };
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
error: `claude is not logged in (credentials missing at ${path}). Run \`claude\` and complete login first`,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
case "maestro": {
|
|
57
|
+
if (process.env.DEEPSEEK_API_KEY) return { ok: true };
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
error: "maestro is not authenticated (DEEPSEEK_API_KEY env var not set)",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|