negotium 0.1.37 → 0.1.39
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 +19 -0
- package/dist/agent-helpers.js +15637 -5674
- package/dist/agent-helpers.js.map +85 -37
- package/dist/browser-runtime.js +1600 -0
- package/dist/browser-runtime.js.map +28 -0
- package/dist/hosted-agent.js +8 -8
- package/dist/hosted-agent.js.map +6 -5
- package/dist/main.js +3790 -3113
- package/dist/main.js.map +33 -29
- package/dist/mcp-factories.js +15141 -301
- package/dist/mcp-factories.js.map +100 -7
- package/dist/prompts.js +117 -49
- package/dist/prompts.js.map +3 -3
- package/dist/query-runtime.js +310 -1
- package/dist/query-runtime.js.map +10 -5
- package/dist/runtime/src/agents/archiver.ts +407 -296
- package/dist/runtime/src/agents/mcp-tools/ask-user.ts +440 -274
- package/dist/runtime/src/agents/mcp-tools/self-config.ts +139 -65
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +1037 -816
- package/dist/runtime/src/agents/public-helpers.ts +82 -0
- package/dist/runtime/src/agents/self-config-core.ts +574 -278
- package/dist/runtime/src/agents/topic-cleanup.ts +167 -129
- package/dist/runtime/src/index.ts +4 -1
- package/dist/runtime/src/mcp/factories/index.ts +9 -0
- package/dist/runtime/src/mcp/session-comm/topic-catalog.ts +132 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +23 -57
- package/dist/runtime/src/platform/mcp-config.ts +2 -4
- package/dist/runtime/src/platform/playwright/browser-processes.ts +3 -2
- package/dist/runtime/src/platform/playwright/capability.ts +5 -0
- package/dist/runtime/src/platform/playwright/manager.ts +380 -66
- package/dist/runtime/src/platform/playwright/public-runtime.ts +45 -0
- package/dist/runtime/src/platform/playwright/transport-probe.ts +89 -0
- package/dist/runtime/src/prompts/builders.ts +192 -51
- package/dist/runtime/src/query/active-rooms.ts +2 -0
- package/dist/runtime/src/query/public-runtime.ts +8 -0
- package/dist/runtime/src/query/state.ts +69 -33
- package/dist/runtime/src/query/types.ts +1 -0
- package/dist/runtime/src/runtime/playwright-turn-abort.ts +59 -0
- package/dist/runtime/src/runtime/turn-event-stream.ts +23 -6
- package/dist/runtime/src/runtime/turn-runner.ts +6 -1
- package/dist/runtime/src/version.ts +1 -1
- package/dist/types/packages/core/src/agents/api-topic-agent-switch.d.ts +38 -0
- package/dist/types/packages/core/src/agents/archiver.d.ts +52 -16
- package/dist/types/packages/core/src/agents/mcp-tools/ask-user.d.ts +75 -4
- package/dist/types/packages/core/src/agents/mcp-tools/self-config.d.ts +17 -0
- package/dist/types/packages/core/src/agents/mcp-tools/spawn-subagent.d.ts +112 -25
- package/dist/types/packages/core/src/agents/public-helpers.d.ts +7 -0
- package/dist/types/packages/core/src/agents/self-config-core.d.ts +185 -0
- package/dist/types/packages/core/src/agents/topic-cleanup.d.ts +30 -24
- package/dist/types/packages/core/src/mcp/factories/index.d.ts +1 -0
- package/dist/types/packages/core/src/mcp/session-comm/topic-catalog.d.ts +46 -0
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +2 -1
- package/dist/types/packages/core/src/platform/playwright/capability.d.ts +1 -0
- package/dist/types/packages/core/src/platform/playwright/manager.d.ts +65 -5
- package/dist/types/packages/core/src/platform/playwright/public-runtime.d.ts +4 -0
- package/dist/types/packages/core/src/platform/playwright/transport-probe.d.ts +13 -0
- package/dist/types/packages/core/src/prompts/builders.d.ts +29 -3
- package/dist/types/packages/core/src/query/active-rooms.d.ts +2 -0
- package/dist/types/packages/core/src/query/public-runtime.d.ts +1 -0
- package/dist/types/packages/core/src/query/state.d.ts +13 -0
- package/dist/types/packages/core/src/query/types.d.ts +2 -1
- package/dist/types/packages/core/src/runtime/playwright-turn-abort.d.ts +12 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/package.json +6 -1
|
@@ -13,8 +13,8 @@ import { getTopicBrief, setTopicBrief } from "#storage/api-topic-brief";
|
|
|
13
13
|
import { getSharedWikiDir } from "#storage/wiki";
|
|
14
14
|
import { wikiSummaryFilename } from "#storage/wiki-summary-names";
|
|
15
15
|
import { ensurePersonalGeneral } from "#topics/personal-general";
|
|
16
|
-
import type { AgentKind } from "#types";
|
|
17
|
-
import type { BackgroundSessionDto } from "#types/api";
|
|
16
|
+
import type { AgentKind, AgentQueryOptions, UnifiedEvent } from "#types";
|
|
17
|
+
import type { BackgroundSessionDto, MessageDto } from "#types/api";
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Rolling cap on the #General digest. The General-brief injection embeds
|
|
@@ -26,11 +26,9 @@ const MAX_BRIEF_ENTRIES = 8;
|
|
|
26
26
|
interface ActiveArchiverSession extends BackgroundSessionDto {
|
|
27
27
|
userId: string;
|
|
28
28
|
expiresAt?: number;
|
|
29
|
-
expiryTimer?:
|
|
29
|
+
expiryTimer?: unknown;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const activeArchiverSessions = new Map<string, ActiveArchiverSession>();
|
|
33
|
-
|
|
34
32
|
function sessionText(value: string, maxLength?: number): string {
|
|
35
33
|
const normalized = value.replace(/\s+/g, " ").trim();
|
|
36
34
|
return maxLength === undefined ? normalized : normalized.slice(0, maxLength);
|
|
@@ -52,42 +50,6 @@ function formatArchiverTool(name: string, input: Record<string, unknown>): strin
|
|
|
52
50
|
return `${name}${params ? `(${params})` : ""}`;
|
|
53
51
|
}
|
|
54
52
|
|
|
55
|
-
function updateArchiverSession(id: string, status: string, step?: string): void {
|
|
56
|
-
const session = activeArchiverSessions.get(id);
|
|
57
|
-
if (!session) return;
|
|
58
|
-
session.status = sessionText(status, 160) || session.status;
|
|
59
|
-
if (step) {
|
|
60
|
-
const text = sessionText(step);
|
|
61
|
-
if (text && session.steps.at(-1) !== text) {
|
|
62
|
-
session.steps.push(text);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function listActiveMemoryArchiverSessions(userId: string): BackgroundSessionDto[] {
|
|
68
|
-
const now = Date.now();
|
|
69
|
-
for (const [id, session] of activeArchiverSessions) {
|
|
70
|
-
if (session.expiresAt !== undefined && session.expiresAt <= now) {
|
|
71
|
-
if (session.expiryTimer) clearTimeout(session.expiryTimer);
|
|
72
|
-
activeArchiverSessions.delete(id);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return [...activeArchiverSessions.values()]
|
|
76
|
-
.filter((session) => session.userId === userId)
|
|
77
|
-
.map(({ userId: _userId, expiresAt: _expiresAt, expiryTimer: _expiryTimer, ...session }) => ({
|
|
78
|
-
...session,
|
|
79
|
-
steps: [...session.steps],
|
|
80
|
-
}));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// The wiki-archiver prompt is loaded once and cached — a missing/!malformed
|
|
84
|
-
// file must not crash the delete path, so loading is wrapped at call sites.
|
|
85
|
-
let _archiverDef: AgentDef | null = null;
|
|
86
|
-
function getArchiverDef(): AgentDef {
|
|
87
|
-
if (!_archiverDef) _archiverDef = loadAgentPrompt("wiki-archiver.md");
|
|
88
|
-
return _archiverDef;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
53
|
export interface RunArchiverTurnParams {
|
|
92
54
|
/** Stringified user id (route layer hands us strings). */
|
|
93
55
|
userId: string;
|
|
@@ -118,253 +80,350 @@ interface GeneralArchiverReply {
|
|
|
118
80
|
usage?: { input: number; output: number };
|
|
119
81
|
}
|
|
120
82
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const {
|
|
136
|
-
userId,
|
|
137
|
-
topicId,
|
|
138
|
-
topicTitle,
|
|
139
|
-
archivePath,
|
|
140
|
-
rawArchivePaths = [],
|
|
141
|
-
messageCount,
|
|
142
|
-
mode = "deleted-topic",
|
|
143
|
-
} = params;
|
|
144
|
-
|
|
145
|
-
let archiverDef: AgentDef;
|
|
146
|
-
try {
|
|
147
|
-
archiverDef = getArchiverDef();
|
|
148
|
-
} catch (err) {
|
|
149
|
-
logger.warn({ err }, "archiver: failed to load wiki-archiver.md — skipping");
|
|
150
|
-
return false;
|
|
151
|
-
}
|
|
83
|
+
export interface ArchiverStorageHost {
|
|
84
|
+
getWikiDir(): string;
|
|
85
|
+
fileExists(path: string): boolean;
|
|
86
|
+
listDirectory(path: string): string[];
|
|
87
|
+
readTextFile(path: string): string;
|
|
88
|
+
fileSize(path: string): number;
|
|
89
|
+
fileModifiedAt(path: string): number;
|
|
90
|
+
getGeneralTopicId(userId: string): string;
|
|
91
|
+
getTopicBrief(topicId: string): { briefMd: string } | null;
|
|
92
|
+
setTopicBrief(
|
|
93
|
+
topicId: string,
|
|
94
|
+
fields: { briefMd: string; latestSummaryMd?: string; summaryDate?: string },
|
|
95
|
+
): void;
|
|
96
|
+
}
|
|
152
97
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// override both agent and model as a matching pair when desired.
|
|
158
|
-
const agent: AgentKind = params.agent ?? "claude";
|
|
159
|
-
const model = params.model;
|
|
160
|
-
|
|
161
|
-
const prompt =
|
|
162
|
-
mode === "active-topic"
|
|
163
|
-
? [
|
|
164
|
-
`세션 "${topicTitle}" 의 최근 idle 대화 snapshot입니다. 아래 아카이브에서 기억을 추출해 이 토픽 위키에 저장해줘.`,
|
|
165
|
-
`archive_path: ${archivePath}`,
|
|
166
|
-
...rawArchivePaths.map((path) => `raw_archive_path: ${path}`),
|
|
167
|
-
`wiki_dir: ${wikiDir}`,
|
|
168
|
-
].join("\n")
|
|
169
|
-
: [
|
|
170
|
-
`세션 "${topicTitle}" 이(가) 삭제되었습니다. 아래 아카이브에서 기억을 추출해 위키에 저장해줘.`,
|
|
171
|
-
`archive_path: ${archivePath}`,
|
|
172
|
-
...rawArchivePaths.map((path) => `raw_archive_path: ${path}`),
|
|
173
|
-
`wiki_dir: ${wikiDir}`,
|
|
174
|
-
"",
|
|
175
|
-
"#General에 표시될 짧은 한국어 완료 메시지로 최종 응답해줘. " +
|
|
176
|
-
"도구 호출 로그나 원문 전문은 쓰지 말고, 저장한 요약/브리프/문서만 간단히 말해줘.",
|
|
177
|
-
].join("\n");
|
|
178
|
-
|
|
179
|
-
// cwd = workspace root so the archiver's relative `wiki/...` Glob/Read paths
|
|
180
|
-
// resolve against the same shared wiki the MCP writes to.
|
|
181
|
-
const abortController = new AbortController();
|
|
182
|
-
const events = runAgent({
|
|
183
|
-
agent,
|
|
184
|
-
prompt,
|
|
185
|
-
cwd: WORKSPACE_DIR,
|
|
186
|
-
systemPrompt: archiverDef.prompt,
|
|
187
|
-
userId,
|
|
188
|
-
// Throwaway session name → wiki MCP runs in topic-id (shared-root) mode.
|
|
189
|
-
session: `__archiver_${safeTopic}`,
|
|
190
|
-
sessionType: "forum",
|
|
191
|
-
topicId,
|
|
192
|
-
abortController,
|
|
193
|
-
model,
|
|
194
|
-
// Limit MCP surface to the wiki server (no playwright/bg-bash/etc.).
|
|
195
|
-
mcpEnabled: ["wiki"],
|
|
196
|
-
// Hidden run: don't record to the cross-agent conversation log.
|
|
197
|
-
silent: true,
|
|
198
|
-
});
|
|
98
|
+
export interface ArchiverMessagingHost {
|
|
99
|
+
appendMessage(message: MessageDto): void;
|
|
100
|
+
broadcastMessage(topicId: string, message: MessageDto): void;
|
|
101
|
+
}
|
|
199
102
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
startedAt: new Date().toISOString(),
|
|
214
|
-
status: "Starting",
|
|
215
|
-
active: true,
|
|
216
|
-
agent,
|
|
217
|
-
model: model ?? archiverDef.model,
|
|
218
|
-
prompt,
|
|
219
|
-
promptTitle: "Prompt",
|
|
220
|
-
steps: [
|
|
221
|
-
`Archive prepared · ${messageCount.toLocaleString()} messages · ${formatArchiverBytes(archiveBytes)}`,
|
|
222
|
-
],
|
|
223
|
-
});
|
|
103
|
+
export interface ArchiverConfigHost {
|
|
104
|
+
readonly workspaceDir: string;
|
|
105
|
+
readonly completedSessionRetentionMs: number;
|
|
106
|
+
loadAgentDefinition(): AgentDef;
|
|
107
|
+
sanitizeTopicName(topicTitle: string): string;
|
|
108
|
+
createId(): string;
|
|
109
|
+
now(): Date;
|
|
110
|
+
schedule(callback: () => void, delayMs: number): unknown;
|
|
111
|
+
cancelScheduled(handle: unknown): void;
|
|
112
|
+
unrefScheduled?(handle: unknown): void;
|
|
113
|
+
info(context: Record<string, unknown>, message: string): void;
|
|
114
|
+
warn(context: Record<string, unknown>, message: string): void;
|
|
115
|
+
}
|
|
224
116
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
case "reasoning":
|
|
264
|
-
updateArchiverSession(activeSessionId, "Reasoning", `Reasoning: ${event.content}`);
|
|
265
|
-
break;
|
|
266
|
-
case "tool_use_summary":
|
|
267
|
-
updateArchiverSession(activeSessionId, event.summary, event.summary);
|
|
268
|
-
break;
|
|
269
|
-
case "status":
|
|
270
|
-
updateArchiverSession(activeSessionId, event.content, event.content);
|
|
271
|
-
break;
|
|
272
|
-
case "tool_result":
|
|
273
|
-
updateArchiverSession(
|
|
274
|
-
activeSessionId,
|
|
275
|
-
event.isError ? "Tool failed" : "Processing tool result",
|
|
276
|
-
event.isError
|
|
277
|
-
? `Tool failed: ${sessionText(event.content) || "unknown error"}`
|
|
278
|
-
: `Tool result · ${formatArchiverBytes(event.metadata?.returnedBytes ?? Buffer.byteLength(event.content))}`,
|
|
279
|
-
);
|
|
280
|
-
break;
|
|
281
|
-
case "text_delta":
|
|
282
|
-
sawDelta = true;
|
|
283
|
-
accumulatedText += event.content;
|
|
284
|
-
break;
|
|
285
|
-
case "text":
|
|
286
|
-
if (!sawDelta) accumulatedText += event.content;
|
|
287
|
-
break;
|
|
288
|
-
case "result":
|
|
289
|
-
updateArchiverSession(
|
|
290
|
-
activeSessionId,
|
|
291
|
-
"Finalizing memory",
|
|
292
|
-
`Memory received · ${formatArchiverBytes(Buffer.byteLength(event.content))}${
|
|
293
|
-
event.usage
|
|
294
|
-
? ` · ${event.usage.inputTokens.toLocaleString()} in / ${event.usage.outputTokens.toLocaleString()} out`
|
|
295
|
-
: ""
|
|
296
|
-
}`,
|
|
297
|
-
);
|
|
298
|
-
resultText = event.content;
|
|
299
|
-
usage = event.usage
|
|
300
|
-
? { input: event.usage.inputTokens, output: event.usage.outputTokens }
|
|
301
|
-
: undefined;
|
|
302
|
-
break;
|
|
303
|
-
case "error":
|
|
304
|
-
errorText = event.content;
|
|
305
|
-
updateArchiverSession(
|
|
306
|
-
activeSessionId,
|
|
307
|
-
"Failed",
|
|
308
|
-
`Memory archive failed: ${sessionText(event.content)}`,
|
|
309
|
-
);
|
|
310
|
-
break;
|
|
311
|
-
default:
|
|
312
|
-
break;
|
|
117
|
+
export interface ArchiverAgentRuntimeHost {
|
|
118
|
+
run(options: AgentQueryOptions): AsyncIterable<UnifiedEvent>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ArchiverHost {
|
|
122
|
+
storage: ArchiverStorageHost;
|
|
123
|
+
messaging: ArchiverMessagingHost;
|
|
124
|
+
config: ArchiverConfigHost;
|
|
125
|
+
agentRuntime: ArchiverAgentRuntimeHost;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ArchiverRuntime {
|
|
129
|
+
runArchiverTurn(params: RunArchiverTurnParams): boolean;
|
|
130
|
+
listActiveMemoryArchiverSessions(userId: string): BackgroundSessionDto[];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function createArchiverRuntime(host: ArchiverHost): ArchiverRuntime {
|
|
134
|
+
const activeSessions = new Map<string, ActiveArchiverSession>();
|
|
135
|
+
let archiverDef: AgentDef | null = null;
|
|
136
|
+
|
|
137
|
+
const updateSession = (id: string, status: string, step?: string): void => {
|
|
138
|
+
const session = activeSessions.get(id);
|
|
139
|
+
if (!session) return;
|
|
140
|
+
session.status = sessionText(status, 160) || session.status;
|
|
141
|
+
if (step) {
|
|
142
|
+
const text = sessionText(step);
|
|
143
|
+
if (text && session.steps.at(-1) !== text) {
|
|
144
|
+
session.steps.push(text);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const listSessions = (userId: string): BackgroundSessionDto[] => {
|
|
150
|
+
const now = host.config.now().getTime();
|
|
151
|
+
for (const [id, session] of activeSessions) {
|
|
152
|
+
if (session.expiresAt !== undefined && session.expiresAt <= now) {
|
|
153
|
+
if (session.expiryTimer !== undefined) {
|
|
154
|
+
host.config.cancelScheduled(session.expiryTimer);
|
|
313
155
|
}
|
|
156
|
+
activeSessions.delete(id);
|
|
314
157
|
}
|
|
315
|
-
|
|
316
|
-
|
|
158
|
+
}
|
|
159
|
+
return [...activeSessions.values()]
|
|
160
|
+
.filter((session) => session.userId === userId)
|
|
161
|
+
.map(({ userId: _userId, expiresAt: _expiresAt, expiryTimer: _expiryTimer, ...session }) => ({
|
|
162
|
+
...session,
|
|
163
|
+
steps: [...session.steps],
|
|
164
|
+
}));
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const getAgentDefinition = (): AgentDef => {
|
|
168
|
+
archiverDef ??= host.config.loadAgentDefinition();
|
|
169
|
+
return archiverDef;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Fire a background wiki-archiver turn for a deleted topic or active idle
|
|
174
|
+
* snapshot.
|
|
175
|
+
*
|
|
176
|
+
* Reads the forensic archive file produced by `archiveTopicMessages` and
|
|
177
|
+
* distils it into the **shared** wiki (summaries / articles / topic brief /
|
|
178
|
+
* indexes / skills) via the wiki MCP, which the turn launches in topic-id mode
|
|
179
|
+
* so its writes land in the same root normal forum turns query.
|
|
180
|
+
*
|
|
181
|
+
* Fire-and-forget: the async generator is drained on a detached promise so the
|
|
182
|
+
* caller's HTTP response returns immediately. Every failure is best-effort and
|
|
183
|
+
* logged only — a broken archiver must never block or fail a topic deletion.
|
|
184
|
+
*/
|
|
185
|
+
const runTurn = (params: RunArchiverTurnParams): boolean => {
|
|
186
|
+
const {
|
|
187
|
+
userId,
|
|
188
|
+
topicId,
|
|
189
|
+
topicTitle,
|
|
190
|
+
archivePath,
|
|
191
|
+
rawArchivePaths = [],
|
|
192
|
+
messageCount,
|
|
193
|
+
mode = "deleted-topic",
|
|
194
|
+
} = params;
|
|
195
|
+
|
|
196
|
+
let definition: AgentDef;
|
|
197
|
+
try {
|
|
198
|
+
definition = getAgentDefinition();
|
|
317
199
|
} catch (err) {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
200
|
+
host.config.warn({ err }, "archiver: failed to load wiki-archiver.md — skipping");
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const wikiDir = host.storage.getWikiDir();
|
|
205
|
+
const safeTopic = host.config.sanitizeTopicName(topicTitle);
|
|
206
|
+
// Keep the established Claude default for archive quality and compatibility.
|
|
207
|
+
// Every provider now receives the same host-resolved wiki MCP; callers may
|
|
208
|
+
// override both agent and model as a matching pair when desired.
|
|
209
|
+
const agent: AgentKind = params.agent ?? "claude";
|
|
210
|
+
const model = params.model;
|
|
211
|
+
|
|
212
|
+
const prompt =
|
|
213
|
+
mode === "active-topic"
|
|
214
|
+
? [
|
|
215
|
+
`세션 "${topicTitle}" 의 최근 idle 대화 snapshot입니다. 아래 아카이브에서 기억을 추출해 이 토픽 위키에 저장해줘.`,
|
|
216
|
+
`archive_path: ${archivePath}`,
|
|
217
|
+
...rawArchivePaths.map((path) => `raw_archive_path: ${path}`),
|
|
218
|
+
`wiki_dir: ${wikiDir}`,
|
|
219
|
+
].join("\n")
|
|
220
|
+
: [
|
|
221
|
+
`세션 "${topicTitle}" 이(가) 삭제되었습니다. 아래 아카이브에서 기억을 추출해 위키에 저장해줘.`,
|
|
222
|
+
`archive_path: ${archivePath}`,
|
|
223
|
+
...rawArchivePaths.map((path) => `raw_archive_path: ${path}`),
|
|
224
|
+
`wiki_dir: ${wikiDir}`,
|
|
225
|
+
"",
|
|
226
|
+
"#General에 표시될 짧은 한국어 완료 메시지로 최종 응답해줘. " +
|
|
227
|
+
"도구 호출 로그나 원문 전문은 쓰지 말고, 저장한 요약/브리프/문서만 간단히 말해줘.",
|
|
228
|
+
].join("\n");
|
|
229
|
+
|
|
230
|
+
// cwd = workspace root so the archiver's relative `wiki/...` Glob/Read paths
|
|
231
|
+
// resolve against the same shared wiki the MCP writes to.
|
|
232
|
+
const abortController = new AbortController();
|
|
233
|
+
const events = host.agentRuntime.run({
|
|
234
|
+
agent,
|
|
235
|
+
prompt,
|
|
236
|
+
cwd: host.config.workspaceDir,
|
|
237
|
+
systemPrompt: definition.prompt,
|
|
238
|
+
userId,
|
|
239
|
+
// Throwaway session name → wiki MCP runs in topic-id (shared-root) mode.
|
|
240
|
+
session: `__archiver_${safeTopic}`,
|
|
241
|
+
sessionType: "forum",
|
|
242
|
+
topicId,
|
|
243
|
+
abortController,
|
|
244
|
+
model,
|
|
245
|
+
// Limit MCP surface to the wiki server (no playwright/bg-bash/etc.).
|
|
246
|
+
mcpEnabled: ["wiki"],
|
|
247
|
+
// Hidden run: don't record to the cross-agent conversation log.
|
|
248
|
+
silent: true,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const activeSessionId = `memory:${host.config.createId()}`;
|
|
252
|
+
let archiveBytes = 0;
|
|
253
|
+
try {
|
|
254
|
+
archiveBytes = host.storage.fileSize(archivePath);
|
|
255
|
+
} catch {
|
|
256
|
+
// The provider will report the actionable read failure.
|
|
257
|
+
}
|
|
258
|
+
activeSessions.set(activeSessionId, {
|
|
259
|
+
id: activeSessionId,
|
|
260
|
+
kind: "memory",
|
|
261
|
+
title: `Archive ${topicTitle}`,
|
|
262
|
+
topicId,
|
|
263
|
+
userId,
|
|
264
|
+
startedAt: host.config.now().toISOString(),
|
|
265
|
+
status: "Starting",
|
|
266
|
+
active: true,
|
|
267
|
+
agent,
|
|
268
|
+
model: model ?? definition.model,
|
|
269
|
+
prompt,
|
|
270
|
+
promptTitle: "Prompt",
|
|
271
|
+
steps: [
|
|
272
|
+
`Archive prepared · ${messageCount.toLocaleString()} messages · ${formatArchiverBytes(archiveBytes)}`,
|
|
273
|
+
],
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
host.config.info(
|
|
277
|
+
{ userId, topicTitle, archivePath, agent, model },
|
|
278
|
+
"archiver: starting background turn",
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
// Baseline for locating the summary file the turn is about to write (fallback
|
|
282
|
+
// when the predicted filename misses). Stamped before draining.
|
|
283
|
+
const startMs = host.config.now().getTime();
|
|
284
|
+
|
|
285
|
+
void (async () => {
|
|
286
|
+
let ok = false;
|
|
287
|
+
let sawDelta = false;
|
|
288
|
+
let accumulatedText = "";
|
|
289
|
+
let resultText = "";
|
|
290
|
+
let finalText = "";
|
|
291
|
+
let errorText = "";
|
|
292
|
+
let usage: GeneralArchiverReply["usage"] | undefined;
|
|
293
|
+
try {
|
|
294
|
+
// Drain the stream — the turn's side effects (wiki writes) are the point;
|
|
295
|
+
// only the final assistant text is surfaced to #General for deleted topics.
|
|
296
|
+
for await (const event of events) {
|
|
297
|
+
switch (event.type) {
|
|
298
|
+
case "session":
|
|
299
|
+
updateSession(activeSessionId, "Running", "Provider session started");
|
|
300
|
+
break;
|
|
301
|
+
case "tool_use":
|
|
302
|
+
updateSession(
|
|
303
|
+
activeSessionId,
|
|
304
|
+
`Running ${event.name}`,
|
|
305
|
+
formatArchiverTool(event.name, event.input),
|
|
306
|
+
);
|
|
307
|
+
break;
|
|
308
|
+
case "tool_progress":
|
|
309
|
+
{
|
|
310
|
+
const progress = `${event.toolName === "thinking" ? "Thinking" : event.toolName} · ${Math.max(0, Math.floor(event.elapsed))}s`;
|
|
311
|
+
updateSession(activeSessionId, progress, progress);
|
|
312
|
+
}
|
|
313
|
+
break;
|
|
314
|
+
case "reasoning":
|
|
315
|
+
updateSession(activeSessionId, "Reasoning", `Reasoning: ${event.content}`);
|
|
316
|
+
break;
|
|
317
|
+
case "tool_use_summary":
|
|
318
|
+
updateSession(activeSessionId, event.summary, event.summary);
|
|
319
|
+
break;
|
|
320
|
+
case "status":
|
|
321
|
+
updateSession(activeSessionId, event.content, event.content);
|
|
322
|
+
break;
|
|
323
|
+
case "tool_result":
|
|
324
|
+
updateSession(
|
|
325
|
+
activeSessionId,
|
|
326
|
+
event.isError ? "Tool failed" : "Processing tool result",
|
|
327
|
+
event.isError
|
|
328
|
+
? `Tool failed: ${sessionText(event.content) || "unknown error"}`
|
|
329
|
+
: `Tool result · ${formatArchiverBytes(event.metadata?.returnedBytes ?? Buffer.byteLength(event.content))}`,
|
|
330
|
+
);
|
|
331
|
+
break;
|
|
332
|
+
case "text_delta":
|
|
333
|
+
sawDelta = true;
|
|
334
|
+
accumulatedText += event.content;
|
|
335
|
+
break;
|
|
336
|
+
case "text":
|
|
337
|
+
if (!sawDelta) accumulatedText += event.content;
|
|
338
|
+
break;
|
|
339
|
+
case "result":
|
|
340
|
+
updateSession(
|
|
341
|
+
activeSessionId,
|
|
342
|
+
"Finalizing memory",
|
|
343
|
+
`Memory received · ${formatArchiverBytes(Buffer.byteLength(event.content))}${
|
|
344
|
+
event.usage
|
|
345
|
+
? ` · ${event.usage.inputTokens.toLocaleString()} in / ${event.usage.outputTokens.toLocaleString()} out`
|
|
346
|
+
: ""
|
|
347
|
+
}`,
|
|
348
|
+
);
|
|
349
|
+
resultText = event.content;
|
|
350
|
+
usage = event.usage
|
|
351
|
+
? { input: event.usage.inputTokens, output: event.usage.outputTokens }
|
|
352
|
+
: undefined;
|
|
353
|
+
break;
|
|
354
|
+
case "error":
|
|
355
|
+
errorText = event.content;
|
|
356
|
+
updateSession(
|
|
357
|
+
activeSessionId,
|
|
358
|
+
"Failed",
|
|
359
|
+
`Memory archive failed: ${sessionText(event.content)}`,
|
|
360
|
+
);
|
|
361
|
+
break;
|
|
362
|
+
default:
|
|
363
|
+
break;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
ok = !errorText;
|
|
367
|
+
host.config.info({ userId, topicTitle }, "archiver: background turn completed");
|
|
368
|
+
} catch (err) {
|
|
369
|
+
errorText = err instanceof Error ? err.message : String(err);
|
|
370
|
+
updateSession(
|
|
331
371
|
activeSessionId,
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
? `Memory archive completed · ${formatArchiverBytes(Buffer.byteLength(finalText))}`
|
|
335
|
-
: `Memory archive failed: ${sessionText(errorText) || "unknown error"}`,
|
|
372
|
+
"Failed",
|
|
373
|
+
`Memory archive failed: ${sessionText(errorText)}`,
|
|
336
374
|
);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
375
|
+
host.config.warn({ err, userId, topicTitle }, "archiver: background turn failed");
|
|
376
|
+
} finally {
|
|
377
|
+
finalText = (accumulatedText.trim() ? accumulatedText : resultText).trim();
|
|
378
|
+
const completedSession = activeSessions.get(activeSessionId);
|
|
379
|
+
if (completedSession) {
|
|
380
|
+
if (finalText) completedSession.output = finalText;
|
|
381
|
+
updateSession(
|
|
382
|
+
activeSessionId,
|
|
383
|
+
ok ? "Completed" : "Failed",
|
|
384
|
+
ok
|
|
385
|
+
? `Memory archive completed · ${formatArchiverBytes(Buffer.byteLength(finalText))}`
|
|
386
|
+
: `Memory archive failed: ${sessionText(errorText) || "unknown error"}`,
|
|
387
|
+
);
|
|
388
|
+
completedSession.active = false;
|
|
389
|
+
completedSession.expiresAt =
|
|
390
|
+
host.config.now().getTime() + host.config.completedSessionRetentionMs;
|
|
391
|
+
completedSession.expiryTimer = host.config.schedule(() => {
|
|
392
|
+
if (activeSessions.get(activeSessionId) === completedSession) {
|
|
393
|
+
activeSessions.delete(activeSessionId);
|
|
394
|
+
}
|
|
395
|
+
}, host.config.completedSessionRetentionMs);
|
|
396
|
+
host.config.unrefScheduled?.(completedSession.expiryTimer);
|
|
397
|
+
}
|
|
398
|
+
try {
|
|
399
|
+
params.onSettled?.(ok);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
host.config.warn({ err, userId, topicTitle }, "archiver: settlement callback failed");
|
|
402
|
+
}
|
|
345
403
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
404
|
+
if (mode === "deleted-topic") {
|
|
405
|
+
// Roll the deleted topic into the #General memory hub regardless of LLM
|
|
406
|
+
// success — even a failed distillation should leave a digest breadcrumb.
|
|
407
|
+
const text = finalText.trimEnd();
|
|
408
|
+
finalizeGeneralMemory(
|
|
409
|
+
host,
|
|
410
|
+
userId,
|
|
411
|
+
topicTitle,
|
|
412
|
+
messageCount,
|
|
413
|
+
startMs,
|
|
414
|
+
ok,
|
|
415
|
+
topicId,
|
|
416
|
+
ok && text ? { text, agent, model, usage } : undefined,
|
|
417
|
+
);
|
|
350
418
|
}
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
messageCount,
|
|
360
|
-
startMs,
|
|
361
|
-
ok,
|
|
362
|
-
topicId,
|
|
363
|
-
ok && text ? { text, agent, model, usage } : undefined,
|
|
364
|
-
);
|
|
365
|
-
}
|
|
366
|
-
})();
|
|
367
|
-
return true;
|
|
419
|
+
})();
|
|
420
|
+
return true;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
return Object.freeze({
|
|
424
|
+
runArchiverTurn: runTurn,
|
|
425
|
+
listActiveMemoryArchiverSessions: listSessions,
|
|
426
|
+
});
|
|
368
427
|
}
|
|
369
428
|
|
|
370
429
|
// --- #General memory-hub digest ------------------------------------------
|
|
@@ -386,23 +445,24 @@ function distillOneLine(summaryMd: string): string {
|
|
|
386
445
|
* when one is available and by title for legacy/ephemeral fallback paths.
|
|
387
446
|
*/
|
|
388
447
|
function findSummaryFile(
|
|
448
|
+
storage: ArchiverStorageHost,
|
|
389
449
|
topicTitle: string,
|
|
390
450
|
date: string,
|
|
391
451
|
sinceMs: number,
|
|
392
452
|
topicId?: string,
|
|
393
453
|
): string | null {
|
|
394
|
-
const dir = join(
|
|
395
|
-
if (!
|
|
454
|
+
const dir = join(storage.getWikiDir(), "summaries");
|
|
455
|
+
if (!storage.fileExists(dir)) return null;
|
|
396
456
|
|
|
397
457
|
const predicted = join(dir, wikiSummaryFilename(date, topicTitle, topicId));
|
|
398
|
-
if (
|
|
458
|
+
if (storage.fileExists(predicted)) return predicted;
|
|
399
459
|
|
|
400
460
|
let best: { path: string; mtime: number } | null = null;
|
|
401
|
-
for (const f of
|
|
461
|
+
for (const f of storage.listDirectory(dir)) {
|
|
402
462
|
if (!f.endsWith(".md")) continue;
|
|
403
463
|
const p = join(dir, f);
|
|
404
464
|
try {
|
|
405
|
-
const m =
|
|
465
|
+
const m = storage.fileModifiedAt(p);
|
|
406
466
|
if (m >= sinceMs && (!best || m > best.mtime)) best = { path: p, mtime: m };
|
|
407
467
|
} catch {
|
|
408
468
|
/* skip unreadable */
|
|
@@ -418,6 +478,7 @@ function findSummaryFile(
|
|
|
418
478
|
* the user's private hub. Best-effort — never throws.
|
|
419
479
|
*/
|
|
420
480
|
function finalizeGeneralMemory(
|
|
481
|
+
host: ArchiverHost,
|
|
421
482
|
userId: string,
|
|
422
483
|
topicTitle: string,
|
|
423
484
|
messageCount: number,
|
|
@@ -426,15 +487,17 @@ function finalizeGeneralMemory(
|
|
|
426
487
|
topicId?: string,
|
|
427
488
|
generalReply?: GeneralArchiverReply,
|
|
428
489
|
): void {
|
|
429
|
-
const generalTopicId =
|
|
430
|
-
const date =
|
|
490
|
+
const generalTopicId = host.storage.getGeneralTopicId(userId);
|
|
491
|
+
const date = host.config.now().toISOString().slice(0, 10);
|
|
431
492
|
try {
|
|
432
|
-
const summaryPath = ok
|
|
433
|
-
|
|
493
|
+
const summaryPath = ok
|
|
494
|
+
? findSummaryFile(host.storage, topicTitle, date, startMs, topicId)
|
|
495
|
+
: null;
|
|
496
|
+
const summaryMd = summaryPath ? host.storage.readTextFile(summaryPath) : "";
|
|
434
497
|
const oneLine = (summaryMd && distillOneLine(summaryMd)) || `${messageCount}개 메시지 아카이브`;
|
|
435
498
|
|
|
436
499
|
// Rolling digest: dedupe same-title, prepend newest, cap at MAX_BRIEF_ENTRIES.
|
|
437
|
-
const prev = getTopicBrief(generalTopicId);
|
|
500
|
+
const prev = host.storage.getTopicBrief(generalTopicId);
|
|
438
501
|
const prevEntries = (prev?.briefMd ?? "")
|
|
439
502
|
.split("\n")
|
|
440
503
|
.map((l) => l.trim())
|
|
@@ -446,16 +509,16 @@ function finalizeGeneralMemory(
|
|
|
446
509
|
].slice(0, MAX_BRIEF_ENTRIES);
|
|
447
510
|
const briefMd = `# 워크스페이스 메모리 허브\n\n삭제된 토픽에서 추출한 최근 기억 다이제스트입니다. 자세한 내용은 \`wiki_query\`로 조회하세요.\n\n## 최근 아카이브\n${rolled.join("\n")}`;
|
|
448
511
|
|
|
449
|
-
setTopicBrief(generalTopicId, {
|
|
512
|
+
host.storage.setTopicBrief(generalTopicId, {
|
|
450
513
|
briefMd,
|
|
451
514
|
...(summaryMd ? { latestSummaryMd: summaryMd, summaryDate: date } : {}),
|
|
452
515
|
});
|
|
453
|
-
|
|
516
|
+
host.config.info(
|
|
454
517
|
{ topicTitle, summaryPath, entries: rolled.length },
|
|
455
518
|
"archiver: updated #General memory hub brief",
|
|
456
519
|
);
|
|
457
520
|
} catch (err) {
|
|
458
|
-
|
|
521
|
+
host.config.warn({ err, topicTitle }, "archiver: failed to update #General brief");
|
|
459
522
|
}
|
|
460
523
|
|
|
461
524
|
// (a) Visible completion message in #General so participants see the hub absorbed the topic.
|
|
@@ -478,15 +541,63 @@ function finalizeGeneralMemory(
|
|
|
478
541
|
}
|
|
479
542
|
: { authorId: "system" };
|
|
480
543
|
const msg = {
|
|
481
|
-
id:
|
|
544
|
+
id: host.config.createId(),
|
|
482
545
|
topicId: generalTopicId,
|
|
483
546
|
text,
|
|
484
547
|
...replyMeta,
|
|
485
|
-
createdAt:
|
|
548
|
+
createdAt: host.config.now().toISOString(),
|
|
486
549
|
};
|
|
487
|
-
|
|
488
|
-
|
|
550
|
+
host.messaging.appendMessage(msg);
|
|
551
|
+
host.messaging.broadcastMessage(generalTopicId, msg);
|
|
489
552
|
} catch (err) {
|
|
490
|
-
|
|
553
|
+
host.config.warn({ err, topicTitle }, "archiver: failed to post #General notification");
|
|
491
554
|
}
|
|
492
555
|
}
|
|
556
|
+
|
|
557
|
+
const defaultArchiverRuntime = createArchiverRuntime({
|
|
558
|
+
storage: {
|
|
559
|
+
getWikiDir: getSharedWikiDir,
|
|
560
|
+
fileExists: existsSync,
|
|
561
|
+
listDirectory: readdirSync,
|
|
562
|
+
readTextFile: (path) => readFileSync(path, "utf-8"),
|
|
563
|
+
fileSize: (path) => statSync(path).size,
|
|
564
|
+
fileModifiedAt: (path) => statSync(path).mtimeMs,
|
|
565
|
+
getGeneralTopicId: (userId) => ensurePersonalGeneral(userId).id,
|
|
566
|
+
getTopicBrief,
|
|
567
|
+
setTopicBrief: (topicId, fields) => {
|
|
568
|
+
setTopicBrief(topicId, fields);
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
messaging: {
|
|
572
|
+
appendMessage: appendApiMessage,
|
|
573
|
+
broadcastMessage: (topicId, message) => {
|
|
574
|
+
WsHub.get().broadcastMessage(topicId, message);
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
config: {
|
|
578
|
+
workspaceDir: WORKSPACE_DIR,
|
|
579
|
+
completedSessionRetentionMs: COMPLETED_BACKGROUND_SESSION_RETENTION_MS,
|
|
580
|
+
loadAgentDefinition: () => loadAgentPrompt("wiki-archiver.md"),
|
|
581
|
+
sanitizeTopicName: (topicTitle) => sanitizeTopicName(topicTitle, true),
|
|
582
|
+
createId: randomUUID,
|
|
583
|
+
now: () => new Date(),
|
|
584
|
+
schedule: (callback, delayMs) => setTimeout(callback, delayMs),
|
|
585
|
+
cancelScheduled: (handle) => clearTimeout(handle as ReturnType<typeof setTimeout>),
|
|
586
|
+
unrefScheduled: (handle) => {
|
|
587
|
+
(handle as ReturnType<typeof setTimeout>).unref?.();
|
|
588
|
+
},
|
|
589
|
+
info: (context, message) => logger.info(context, message),
|
|
590
|
+
warn: (context, message) => logger.warn(context, message),
|
|
591
|
+
},
|
|
592
|
+
agentRuntime: {
|
|
593
|
+
run: runAgent,
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
export function runArchiverTurn(params: RunArchiverTurnParams): boolean {
|
|
598
|
+
return defaultArchiverRuntime.runArchiverTurn(params);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export function listActiveMemoryArchiverSessions(userId: string): BackgroundSessionDto[] {
|
|
602
|
+
return defaultArchiverRuntime.listActiveMemoryArchiverSessions(userId);
|
|
603
|
+
}
|