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
|
@@ -18,13 +18,18 @@ import { SUPPORTED_AGENTS } from "#agents/index";
|
|
|
18
18
|
import { getRegistry } from "#agents/registry";
|
|
19
19
|
import { logger } from "#platform/logger";
|
|
20
20
|
import {
|
|
21
|
-
type ConversationEntry,
|
|
22
21
|
getActiveConversationPath,
|
|
23
22
|
getConversationPath,
|
|
24
23
|
readConversation,
|
|
25
24
|
readRawConversation,
|
|
26
25
|
} from "#storage/conversations";
|
|
27
|
-
import type { AgentKind } from "#types";
|
|
26
|
+
import type { AgentKind, UnifiedEvent } from "#types";
|
|
27
|
+
|
|
28
|
+
export interface TopicConversationEntry {
|
|
29
|
+
ts: string;
|
|
30
|
+
agent: AgentKind;
|
|
31
|
+
event: UnifiedEvent;
|
|
32
|
+
}
|
|
28
33
|
|
|
29
34
|
export interface PurgeSessionRef {
|
|
30
35
|
agent: AgentKind;
|
|
@@ -37,7 +42,7 @@ export interface PurgeSessionRef {
|
|
|
37
42
|
* switch back), so we collect into a Set to deduplicate before deletion.
|
|
38
43
|
*/
|
|
39
44
|
function collectSessionIdsByAgent(
|
|
40
|
-
entries:
|
|
45
|
+
entries: TopicConversationEntry[],
|
|
41
46
|
extraSessions: PurgeSessionRef[] = [],
|
|
42
47
|
): Map<AgentKind, Set<string>> {
|
|
43
48
|
const out = new Map<AgentKind, Set<string>>();
|
|
@@ -77,25 +82,49 @@ export interface PurgeTopicLogsOptions {
|
|
|
77
82
|
extraSessions?: PurgeSessionRef[];
|
|
78
83
|
}
|
|
79
84
|
|
|
85
|
+
export interface TopicLogMaintenanceHost {
|
|
86
|
+
readonly agents: readonly AgentKind[];
|
|
87
|
+
readonly readActiveConversation: (
|
|
88
|
+
userId: number | string,
|
|
89
|
+
topicName: string,
|
|
90
|
+
) => TopicConversationEntry[];
|
|
91
|
+
readonly readRawConversation: (
|
|
92
|
+
userId: number | string,
|
|
93
|
+
topicName: string,
|
|
94
|
+
) => TopicConversationEntry[];
|
|
95
|
+
readonly activeConversationPath: (userId: number | string, topicName: string) => string;
|
|
96
|
+
readonly rawConversationPath: (userId: number | string, topicName: string) => string;
|
|
97
|
+
readonly cleanupRollouts: (agent: AgentKind, cwd: string, sessionIds: string[]) => Promise<void>;
|
|
98
|
+
readonly warn: (context: Record<string, unknown>, message: string) => void;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface TopicLogMaintenance {
|
|
102
|
+
cleanupTopicRollouts(opts: PurgeTopicLogsOptions): Promise<boolean>;
|
|
103
|
+
cleanupTopicRolloutsFromEntries(
|
|
104
|
+
opts: PurgeTopicLogsOptions,
|
|
105
|
+
entries: TopicConversationEntry[],
|
|
106
|
+
): Promise<boolean>;
|
|
107
|
+
rotateTopicLogs(opts: RotateTopicLogsOptions): Promise<RotateTopicLogsResult>;
|
|
108
|
+
purgeTopicLogs(opts: PurgeTopicLogsOptions): Promise<boolean>;
|
|
109
|
+
}
|
|
110
|
+
|
|
80
111
|
async function cleanupSessionRollouts(
|
|
112
|
+
host: TopicLogMaintenanceHost,
|
|
81
113
|
opts: PurgeTopicLogsOptions,
|
|
82
|
-
entries:
|
|
114
|
+
entries: TopicConversationEntry[],
|
|
83
115
|
): Promise<boolean> {
|
|
84
116
|
const { userId, topicName, cwd, extraSessions = [] } = opts;
|
|
85
117
|
const idsByAgent = collectSessionIdsByAgent(entries, extraSessions);
|
|
86
118
|
let cleanupFailed = false;
|
|
87
119
|
|
|
88
120
|
await Promise.all(
|
|
89
|
-
|
|
121
|
+
host.agents.map(async (agent) => {
|
|
90
122
|
const ids = idsByAgent.get(agent);
|
|
91
123
|
if (!ids || ids.size === 0) return;
|
|
92
124
|
try {
|
|
93
|
-
await
|
|
94
|
-
cwd,
|
|
95
|
-
sessionIds: Array.from(ids),
|
|
96
|
-
});
|
|
125
|
+
await host.cleanupRollouts(agent, cwd, Array.from(ids));
|
|
97
126
|
} catch (e) {
|
|
98
|
-
|
|
127
|
+
host.warn(
|
|
99
128
|
{ err: e, userId, topicName, agent, count: ids.size },
|
|
100
129
|
"topic logs: agent cleanup failed",
|
|
101
130
|
);
|
|
@@ -107,19 +136,6 @@ async function cleanupSessionRollouts(
|
|
|
107
136
|
return !cleanupFailed;
|
|
108
137
|
}
|
|
109
138
|
|
|
110
|
-
/** Remove every provider rollout currently manifested by a topic log. */
|
|
111
|
-
export async function cleanupTopicRollouts(opts: PurgeTopicLogsOptions): Promise<boolean> {
|
|
112
|
-
return cleanupSessionRollouts(opts, readRawConversation(opts.userId, opts.topicName));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/** Remove rollout ids captured from a point-in-time conversation manifest. */
|
|
116
|
-
export async function cleanupTopicRolloutsFromEntries(
|
|
117
|
-
opts: PurgeTopicLogsOptions,
|
|
118
|
-
entries: ConversationEntry[],
|
|
119
|
-
): Promise<boolean> {
|
|
120
|
-
return cleanupSessionRollouts(opts, entries);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
139
|
export interface RotateTopicLogsOptions extends PurgeTopicLogsOptions {
|
|
124
140
|
/** Number of complete user turns carried into the replacement session. */
|
|
125
141
|
retainTurns: number;
|
|
@@ -132,116 +148,138 @@ export interface RotateTopicLogsResult {
|
|
|
132
148
|
retainedEntries: number;
|
|
133
149
|
}
|
|
134
150
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
): Promise<
|
|
144
|
-
const retainTurns = Math.max(0, Math.floor(opts.retainTurns));
|
|
145
|
-
const entries = readConversation(opts.userId, opts.topicName);
|
|
146
|
-
const userEntryIndexes = entries.flatMap((entry, index) =>
|
|
147
|
-
entry.event.type === "user_message" ? [index] : [],
|
|
148
|
-
);
|
|
149
|
-
const totalTurns = userEntryIndexes.length;
|
|
150
|
-
const firstRetainedIndex =
|
|
151
|
-
retainTurns === 0
|
|
152
|
-
? entries.length
|
|
153
|
-
: (userEntryIndexes[Math.max(0, totalTurns - retainTurns)] ?? 0);
|
|
154
|
-
// Native session ids point at rollouts that are removed below. Keeping
|
|
155
|
-
// those manifest entries would make later cleanup treat deleted files as
|
|
156
|
-
// live context, so only conversational events cross the rotation boundary.
|
|
157
|
-
const retained = entries
|
|
158
|
-
.slice(firstRetainedIndex)
|
|
159
|
-
.filter((entry) => entry.event.type !== "session");
|
|
160
|
-
|
|
161
|
-
if (!(await cleanupSessionRollouts(opts, entries))) {
|
|
162
|
-
logger.warn(
|
|
163
|
-
{ userId: opts.userId, topicName: opts.topicName },
|
|
164
|
-
"rotateTopicLogs: keeping current context because rollout cleanup failed",
|
|
165
|
-
);
|
|
166
|
-
return {
|
|
167
|
-
rotated: false,
|
|
168
|
-
totalTurns,
|
|
169
|
-
retainedTurns: Math.min(totalTurns, retainTurns),
|
|
170
|
-
retainedEntries: retained.length,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
151
|
+
export function createTopicLogMaintenance(host: TopicLogMaintenanceHost): TopicLogMaintenance {
|
|
152
|
+
const runtimeHost: TopicLogMaintenanceHost = Object.freeze({
|
|
153
|
+
...host,
|
|
154
|
+
agents: Object.freeze([...host.agents]),
|
|
155
|
+
});
|
|
156
|
+
const cleanupFromEntries = (
|
|
157
|
+
opts: PurgeTopicLogsOptions,
|
|
158
|
+
entries: TopicConversationEntry[],
|
|
159
|
+
): Promise<boolean> => cleanupSessionRollouts(runtimeHost, opts, entries);
|
|
173
160
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
161
|
+
return Object.freeze({
|
|
162
|
+
cleanupTopicRollouts(opts: PurgeTopicLogsOptions) {
|
|
163
|
+
return cleanupFromEntries(opts, runtimeHost.readRawConversation(opts.userId, opts.topicName));
|
|
164
|
+
},
|
|
165
|
+
cleanupTopicRolloutsFromEntries: cleanupFromEntries,
|
|
166
|
+
async rotateTopicLogs(opts: RotateTopicLogsOptions) {
|
|
167
|
+
const retainTurns = Math.max(0, Math.floor(opts.retainTurns));
|
|
168
|
+
const entries = runtimeHost.readActiveConversation(opts.userId, opts.topicName);
|
|
169
|
+
const userEntryIndexes = entries.flatMap((entry, index) =>
|
|
170
|
+
entry.event.type === "user_message" ? [index] : [],
|
|
171
|
+
);
|
|
172
|
+
const totalTurns = userEntryIndexes.length;
|
|
173
|
+
const firstRetainedIndex =
|
|
174
|
+
retainTurns === 0
|
|
175
|
+
? entries.length
|
|
176
|
+
: (userEntryIndexes[Math.max(0, totalTurns - retainTurns)] ?? 0);
|
|
177
|
+
const retained = entries
|
|
178
|
+
.slice(firstRetainedIndex)
|
|
179
|
+
.filter((entry) => entry.event.type !== "session");
|
|
180
|
+
|
|
181
|
+
if (!(await cleanupFromEntries(opts, entries))) {
|
|
182
|
+
runtimeHost.warn(
|
|
183
|
+
{ userId: opts.userId, topicName: opts.topicName },
|
|
184
|
+
"rotateTopicLogs: keeping current context because rollout cleanup failed",
|
|
185
|
+
);
|
|
186
|
+
return {
|
|
187
|
+
rotated: false,
|
|
188
|
+
totalTurns,
|
|
189
|
+
retainedTurns: Math.min(totalTurns, retainTurns),
|
|
190
|
+
retainedEntries: retained.length,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const path = runtimeHost.activeConversationPath(opts.userId, opts.topicName);
|
|
195
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
196
|
+
try {
|
|
197
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
198
|
+
writeFileSync(
|
|
199
|
+
tempPath,
|
|
200
|
+
retained.length > 0
|
|
201
|
+
? `${retained.map((entry) => JSON.stringify(entry)).join("\n")}\n`
|
|
202
|
+
: "",
|
|
203
|
+
{ flag: "wx" },
|
|
204
|
+
);
|
|
205
|
+
renameSync(tempPath, path);
|
|
206
|
+
} catch (e) {
|
|
207
|
+
try {
|
|
208
|
+
unlinkSync(tempPath);
|
|
209
|
+
} catch {}
|
|
210
|
+
runtimeHost.warn({ err: e, path }, "rotateTopicLogs: conversation replacement failed");
|
|
211
|
+
return {
|
|
212
|
+
rotated: false,
|
|
213
|
+
totalTurns,
|
|
214
|
+
retainedTurns: Math.min(totalTurns, retainTurns),
|
|
215
|
+
retainedEntries: retained.length,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
rotated: true,
|
|
221
|
+
totalTurns,
|
|
222
|
+
retainedTurns: Math.min(totalTurns, retainTurns),
|
|
223
|
+
retainedEntries: retained.length,
|
|
224
|
+
};
|
|
225
|
+
},
|
|
226
|
+
async purgeTopicLogs(opts: PurgeTopicLogsOptions) {
|
|
227
|
+
const { userId, topicName } = opts;
|
|
228
|
+
const entries = runtimeHost.readRawConversation(userId, topicName);
|
|
229
|
+
if (!(await cleanupFromEntries(opts, entries))) {
|
|
230
|
+
runtimeHost.warn(
|
|
231
|
+
{ userId, topicName },
|
|
232
|
+
"purgeTopicLogs: keeping unified log because one or more rollout cleanups failed",
|
|
233
|
+
);
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
196
236
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
237
|
+
let unlinkFailed = false;
|
|
238
|
+
for (const path of [
|
|
239
|
+
runtimeHost.activeConversationPath(userId, topicName),
|
|
240
|
+
runtimeHost.rawConversationPath(userId, topicName),
|
|
241
|
+
]) {
|
|
242
|
+
try {
|
|
243
|
+
unlinkSync(path);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
if ((e as NodeJS.ErrnoException)?.code !== "ENOENT") {
|
|
246
|
+
runtimeHost.warn({ err: e, path }, "purgeTopicLogs: conversation log unlink failed");
|
|
247
|
+
unlinkFailed = true;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return !unlinkFailed;
|
|
252
|
+
},
|
|
253
|
+
});
|
|
203
254
|
}
|
|
204
255
|
|
|
256
|
+
const defaultTopicLogMaintenance = createTopicLogMaintenance({
|
|
257
|
+
agents: SUPPORTED_AGENTS,
|
|
258
|
+
readActiveConversation: readConversation,
|
|
259
|
+
readRawConversation,
|
|
260
|
+
activeConversationPath: getActiveConversationPath,
|
|
261
|
+
rawConversationPath: getConversationPath,
|
|
262
|
+
async cleanupRollouts(agent, cwd, sessionIds) {
|
|
263
|
+
await getRegistry(agent).cleanupRollouts({ cwd, sessionIds });
|
|
264
|
+
},
|
|
265
|
+
warn: logger.warn.bind(logger),
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
/** Remove every provider rollout currently manifested by a topic log. */
|
|
269
|
+
export const cleanupTopicRollouts = defaultTopicLogMaintenance.cleanupTopicRollouts;
|
|
270
|
+
|
|
271
|
+
/** Remove rollout ids captured from a point-in-time conversation manifest. */
|
|
272
|
+
export const cleanupTopicRolloutsFromEntries =
|
|
273
|
+
defaultTopicLogMaintenance.cleanupTopicRolloutsFromEntries;
|
|
274
|
+
|
|
205
275
|
/**
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
* 1. Read the unified log first — it's the authoritative manifest of
|
|
210
|
-
* every SDK session id the topic has emitted. Merge any DB session id
|
|
211
|
-
* passed via `extraSessions` for synthetic rollouts not yet emitted.
|
|
212
|
-
* 2. Dispatch per-agent rollout cleanup in parallel (independent
|
|
213
|
-
* filesystems, no shared state).
|
|
214
|
-
* 3. Unlink the unified log LAST so a partial rollout-cleanup failure
|
|
215
|
-
* leaves the manifest in place for a future retry.
|
|
216
|
-
*
|
|
217
|
-
* Errors at every step are logged and reported through the return value.
|
|
218
|
-
* Callers must not commit a reset/delete state transition when this returns
|
|
219
|
-
* false, because the retained manifest can otherwise resurrect old context.
|
|
276
|
+
* Replace a topic's native provider sessions while retaining a bounded tail
|
|
277
|
+
* of its provider-neutral conversation log.
|
|
220
278
|
*/
|
|
221
|
-
export
|
|
222
|
-
const { userId, topicName } = opts;
|
|
223
|
-
const entries = readRawConversation(userId, topicName);
|
|
224
|
-
if (!(await cleanupSessionRollouts(opts, entries))) {
|
|
225
|
-
logger.warn(
|
|
226
|
-
{ userId, topicName },
|
|
227
|
-
"purgeTopicLogs: keeping unified log because one or more rollout cleanups failed",
|
|
228
|
-
);
|
|
229
|
-
return false;
|
|
230
|
-
}
|
|
279
|
+
export const rotateTopicLogs = defaultTopicLogMaintenance.rotateTopicLogs;
|
|
231
280
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
try {
|
|
238
|
-
unlinkSync(path);
|
|
239
|
-
} catch (e) {
|
|
240
|
-
if ((e as NodeJS.ErrnoException)?.code !== "ENOENT") {
|
|
241
|
-
logger.warn({ err: e, path }, "purgeTopicLogs: conversation log unlink failed");
|
|
242
|
-
unlinkFailed = true;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return !unlinkFailed;
|
|
247
|
-
}
|
|
281
|
+
/**
|
|
282
|
+
* Best-effort teardown of every artifact associated with a topic's
|
|
283
|
+
* conversation lifecycle.
|
|
284
|
+
*/
|
|
285
|
+
export const purgeTopicLogs = defaultTopicLogMaintenance.purgeTopicLogs;
|
|
@@ -197,7 +197,10 @@ export {
|
|
|
197
197
|
runNodeRequestHandlers,
|
|
198
198
|
unregisterNodeRequestHandler,
|
|
199
199
|
} from "#platform/node-plugins";
|
|
200
|
-
export {
|
|
200
|
+
export {
|
|
201
|
+
killAllPlaywright,
|
|
202
|
+
reapPlaywrightOrphans as reapOrphanBrowsers,
|
|
203
|
+
} from "#platform/playwright/manager";
|
|
201
204
|
// ── Query control ───────────────────────────────────────────────────
|
|
202
205
|
export {
|
|
203
206
|
abortAllRooms,
|
|
@@ -3,6 +3,15 @@ export {
|
|
|
3
3
|
type SessionCommContext,
|
|
4
4
|
type SessionCommContextDefaults,
|
|
5
5
|
} from "../session-comm/context";
|
|
6
|
+
export {
|
|
7
|
+
createSessionTargetCatalog,
|
|
8
|
+
type SessionTarget,
|
|
9
|
+
type SessionTargetCatalog,
|
|
10
|
+
type SessionTargetCatalogHost,
|
|
11
|
+
type SessionTopicEntry,
|
|
12
|
+
type SessionTopicRow,
|
|
13
|
+
type ValidateSessionTargetResult,
|
|
14
|
+
} from "../session-comm/topic-catalog";
|
|
6
15
|
export { executeVaultHttpRequest } from "../vault-http";
|
|
7
16
|
export { executeVaultRun } from "../vault-run";
|
|
8
17
|
export {
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
export interface SessionTopicRow {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
kind: string | null;
|
|
5
|
+
agent: string | null;
|
|
6
|
+
sessionId: string | null;
|
|
7
|
+
description: string | null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface SessionTopicEntry<TAgent extends string = string> {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
messageThreadId: number;
|
|
13
|
+
name: string;
|
|
14
|
+
kind: "agent" | "channel";
|
|
15
|
+
description?: string;
|
|
16
|
+
topicId?: string;
|
|
17
|
+
agent?: TAgent;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SessionTarget<TAgent extends string = string> {
|
|
21
|
+
key: string;
|
|
22
|
+
topic: SessionTopicEntry<TAgent>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type ValidateSessionTargetResult<TAgent extends string = string> =
|
|
26
|
+
| { ok: true; target: SessionTopicEntry<TAgent> }
|
|
27
|
+
| {
|
|
28
|
+
ok: false;
|
|
29
|
+
error: {
|
|
30
|
+
content: Array<{ type: "text"; text: string }>;
|
|
31
|
+
isError: true;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export interface SessionTargetCatalogHost<TAgent extends string = string> {
|
|
36
|
+
readonly listRows: () => SessionTopicRow[];
|
|
37
|
+
readonly currentTopicId?: string;
|
|
38
|
+
readonly currentTopicName?: string;
|
|
39
|
+
readonly isAgent: (value: string | null) => value is TAgent;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SessionTargetCatalog<TAgent extends string = string> {
|
|
43
|
+
listTargets(): SessionTarget<TAgent>[];
|
|
44
|
+
getTopics(): Record<string, SessionTopicEntry<TAgent>>;
|
|
45
|
+
validateTarget(to: string): ValidateSessionTargetResult<TAgent>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function createSessionTargetCatalog<TAgent extends string = string>(
|
|
49
|
+
host: SessionTargetCatalogHost<TAgent>,
|
|
50
|
+
): SessionTargetCatalog<TAgent> {
|
|
51
|
+
const { currentTopicId, currentTopicName, isAgent, listRows } = host;
|
|
52
|
+
|
|
53
|
+
function listTargets(): SessionTarget<TAgent>[] {
|
|
54
|
+
const eligibleRows = listRows().filter((row) => row.kind !== "manager");
|
|
55
|
+
const titleCounts = new Map<string, number>();
|
|
56
|
+
const qualifiedCounts = new Map<string, number>();
|
|
57
|
+
for (const row of eligibleRows) {
|
|
58
|
+
const normalized = row.title.toLowerCase();
|
|
59
|
+
titleCounts.set(normalized, (titleCounts.get(normalized) ?? 0) + 1);
|
|
60
|
+
const kind = row.kind === "agent" ? "agent" : "channel";
|
|
61
|
+
const qualified = `${kind}:${normalized}`;
|
|
62
|
+
qualifiedCounts.set(qualified, (qualifiedCounts.get(qualified) ?? 0) + 1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const rows = eligibleRows.filter((row) =>
|
|
66
|
+
currentTopicId ? row.id !== currentTopicId : row.title !== currentTopicName,
|
|
67
|
+
);
|
|
68
|
+
return rows.map((row) => {
|
|
69
|
+
const agent = isAgent(row.agent) ? row.agent : undefined;
|
|
70
|
+
const kind = row.kind === "agent" ? "agent" : "channel";
|
|
71
|
+
const topic: SessionTopicEntry<TAgent> = {
|
|
72
|
+
sessionId: row.sessionId ?? "",
|
|
73
|
+
messageThreadId: 0,
|
|
74
|
+
name: row.title,
|
|
75
|
+
kind,
|
|
76
|
+
topicId: row.id,
|
|
77
|
+
...(agent && { agent }),
|
|
78
|
+
...(row.description && { description: row.description }),
|
|
79
|
+
};
|
|
80
|
+
const collision = (titleCounts.get(row.title.toLowerCase()) ?? 0) > 1;
|
|
81
|
+
const qualified = `${kind}:${row.title}`;
|
|
82
|
+
const sameKindCollision =
|
|
83
|
+
(qualifiedCounts.get(`${kind}:${row.title.toLowerCase()}`) ?? 0) > 1;
|
|
84
|
+
return {
|
|
85
|
+
key: sameKindCollision ? `${qualified}:${row.id}` : collision ? qualified : row.title,
|
|
86
|
+
topic,
|
|
87
|
+
};
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getTopics(): Record<string, SessionTopicEntry<TAgent>> {
|
|
92
|
+
const result: Record<string, SessionTopicEntry<TAgent>> = {};
|
|
93
|
+
const targets = listTargets();
|
|
94
|
+
const qualifiedCounts = new Map<string, number>();
|
|
95
|
+
for (const { topic } of targets) {
|
|
96
|
+
const qualified = `${topic.kind}:${topic.name.toLowerCase()}`;
|
|
97
|
+
qualifiedCounts.set(qualified, (qualifiedCounts.get(qualified) ?? 0) + 1);
|
|
98
|
+
}
|
|
99
|
+
for (const { key, topic } of targets) {
|
|
100
|
+
const qualified = `${topic.kind}:${topic.name}`;
|
|
101
|
+
if ((qualifiedCounts.get(qualified.toLowerCase()) ?? 0) === 1) {
|
|
102
|
+
result[qualified] = topic;
|
|
103
|
+
}
|
|
104
|
+
result[key] = topic;
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function validateTarget(to: string): ValidateSessionTargetResult<TAgent> {
|
|
110
|
+
const topics = getTopics();
|
|
111
|
+
const target = topics[to];
|
|
112
|
+
if (target) return { ok: true, target };
|
|
113
|
+
|
|
114
|
+
const available = listTargets()
|
|
115
|
+
.filter(({ topic }) => Boolean(topic.agent))
|
|
116
|
+
.map(({ key }) => key);
|
|
117
|
+
return {
|
|
118
|
+
ok: false,
|
|
119
|
+
error: {
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: "text",
|
|
123
|
+
text: `Error: Session "${to}" not found.\nAvailable: ${available.join(", ") || "none"}`,
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
isError: true,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return Object.freeze({ listTargets, getTopics, validateTarget });
|
|
132
|
+
}
|
|
@@ -22,6 +22,10 @@ import {
|
|
|
22
22
|
userId,
|
|
23
23
|
withDb,
|
|
24
24
|
} from "./runtime";
|
|
25
|
+
import {
|
|
26
|
+
type SessionTarget as CatalogSessionTarget,
|
|
27
|
+
createSessionTargetCatalog,
|
|
28
|
+
} from "./topic-catalog";
|
|
25
29
|
|
|
26
30
|
export type { QueryState };
|
|
27
31
|
|
|
@@ -39,10 +43,7 @@ export interface TopicEntry {
|
|
|
39
43
|
agent?: AgentKind;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
|
-
export
|
|
43
|
-
key: string;
|
|
44
|
-
topic: TopicEntry;
|
|
45
|
-
}
|
|
46
|
+
export type SessionTarget = CatalogSessionTarget<AgentKind>;
|
|
46
47
|
|
|
47
48
|
export interface McpTopicEntry {
|
|
48
49
|
sessionId: string;
|
|
@@ -67,29 +68,7 @@ export type ValidateTargetResult =
|
|
|
67
68
|
// --- Queries ---
|
|
68
69
|
|
|
69
70
|
export function validateTarget(to: string): ValidateTargetResult {
|
|
70
|
-
|
|
71
|
-
const target = topics[to];
|
|
72
|
-
if (!target) {
|
|
73
|
-
const available = Object.entries(topics)
|
|
74
|
-
.filter(([name, topic]) => name !== currentTopic && Boolean(topic.agent))
|
|
75
|
-
.map(([name]) => name);
|
|
76
|
-
return {
|
|
77
|
-
ok: false,
|
|
78
|
-
error: {
|
|
79
|
-
content: [
|
|
80
|
-
{
|
|
81
|
-
type: "text" as const,
|
|
82
|
-
text: `Error: Session "${to}" not found.\nAvailable: ${available.join(", ") || "none"}`,
|
|
83
|
-
},
|
|
84
|
-
],
|
|
85
|
-
isError: true,
|
|
86
|
-
},
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
// Topics without an active session (sessionId === "") are still valid
|
|
90
|
-
// targets — ask_session / tell_session deliveries will wake them with a
|
|
91
|
-
// fresh session via the session-inbox worker. See `session-inbox.ts`.
|
|
92
|
-
return { ok: true, target };
|
|
71
|
+
return sessionTargetCatalog.validateTarget(to);
|
|
93
72
|
}
|
|
94
73
|
|
|
95
74
|
function sessionTargetRows(): Array<{
|
|
@@ -130,42 +109,29 @@ function sessionTargetRows(): Array<{
|
|
|
130
109
|
|
|
131
110
|
/** Canonical, deduplicated targets for display and status inspection. */
|
|
132
111
|
export function listSessionTargetsForUser(): SessionTarget[] {
|
|
133
|
-
|
|
134
|
-
const titleCounts = new Map<string, number>();
|
|
135
|
-
for (const row of eligibleRows) {
|
|
136
|
-
const normalized = row.title.toLowerCase();
|
|
137
|
-
titleCounts.set(normalized, (titleCounts.get(normalized) ?? 0) + 1);
|
|
138
|
-
}
|
|
139
|
-
const rows = eligibleRows.filter((row) =>
|
|
140
|
-
currentTopicId ? row.id !== currentTopicId : row.title !== currentTopic,
|
|
141
|
-
);
|
|
142
|
-
return rows.map((row) => {
|
|
143
|
-
const agent = isAgentKind(row.agent) ? row.agent : undefined;
|
|
144
|
-
const kind = row.kind === "agent" ? "agent" : "channel";
|
|
145
|
-
const topic: TopicEntry = {
|
|
146
|
-
sessionId: row.session_id ?? "",
|
|
147
|
-
messageThreadId: 0,
|
|
148
|
-
name: row.title,
|
|
149
|
-
kind,
|
|
150
|
-
topicId: row.id,
|
|
151
|
-
...(agent && { agent }),
|
|
152
|
-
...(row.description && { description: row.description }),
|
|
153
|
-
};
|
|
154
|
-
const collision = (titleCounts.get(row.title.toLowerCase()) ?? 0) > 1;
|
|
155
|
-
return { key: collision ? `${kind}:${row.title}` : row.title, topic };
|
|
156
|
-
});
|
|
112
|
+
return sessionTargetCatalog.listTargets();
|
|
157
113
|
}
|
|
158
114
|
|
|
159
115
|
/** Lookup index with qualified aliases for every target and plain aliases when unambiguous. */
|
|
160
116
|
export function getTopicsForUser(): { [name: string]: TopicEntry } {
|
|
161
|
-
|
|
162
|
-
for (const { key, topic } of listSessionTargetsForUser()) {
|
|
163
|
-
result[`${topic.kind}:${topic.name}`] = topic;
|
|
164
|
-
result[key] = topic;
|
|
165
|
-
}
|
|
166
|
-
return result;
|
|
117
|
+
return sessionTargetCatalog.getTopics();
|
|
167
118
|
}
|
|
168
119
|
|
|
120
|
+
const sessionTargetCatalog = createSessionTargetCatalog<AgentKind>({
|
|
121
|
+
currentTopicId,
|
|
122
|
+
currentTopicName: currentTopic,
|
|
123
|
+
isAgent: isAgentKind,
|
|
124
|
+
listRows: () =>
|
|
125
|
+
sessionTargetRows().map((row) => ({
|
|
126
|
+
id: row.id,
|
|
127
|
+
title: row.title,
|
|
128
|
+
kind: row.kind,
|
|
129
|
+
agent: row.agent,
|
|
130
|
+
sessionId: row.session_id,
|
|
131
|
+
description: row.description,
|
|
132
|
+
})),
|
|
133
|
+
});
|
|
134
|
+
|
|
169
135
|
export function getMcpUserConfig(): McpUserConfig | null {
|
|
170
136
|
if (!existsSync(SESSIONS_DB)) return null;
|
|
171
137
|
try {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHmac } from "node:crypto";
|
|
2
1
|
import { canonicalMcpBridgeEnv } from "#mcp/canonical-bridge-config";
|
|
3
2
|
import { buildRuntimeMcpSpec, RUNTIME_MCP_KEY } from "#mcp/runtime-spec";
|
|
4
3
|
import { peerSessionBridgeIpcEnv } from "#mcp/session-comm/bridge-ipc-config";
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
commonRuntimeMcpPolicy,
|
|
25
24
|
type RuntimeMcpScope,
|
|
26
25
|
} from "#platform/mcp-catalog-policy";
|
|
26
|
+
import { browserOwnerCapability } from "#platform/playwright/capability";
|
|
27
27
|
import type { AgentKind, AgentQueryOptions, PeerRuntimeBridgeContext } from "#types";
|
|
28
28
|
|
|
29
29
|
export type { RuntimeMcpScope } from "#platform/mcp-catalog-policy";
|
|
@@ -216,9 +216,7 @@ export interface RuntimeMcpCatalogEntry {
|
|
|
216
216
|
|
|
217
217
|
export const CODEX_BROWSER_CAPABILITY_ENV = "NEGOTIUM_BROWSER_CAPABILITY";
|
|
218
218
|
|
|
219
|
-
export
|
|
220
|
-
return createHmac("sha256", capability).update(owner).digest("hex");
|
|
221
|
-
}
|
|
219
|
+
export { browserOwnerCapability };
|
|
222
220
|
|
|
223
221
|
export function browserOwnerForContext(ctx: {
|
|
224
222
|
userId?: string;
|
|
@@ -145,7 +145,8 @@ export function cleanupZombiePlaywright(): void {
|
|
|
145
145
|
*/
|
|
146
146
|
export function killBrowserProcsForUserDataDir(userDataDir: string): void {
|
|
147
147
|
const target = resolve(userDataDir);
|
|
148
|
-
|
|
148
|
+
const profileRoot = resolve(BROWSER_PROFILES_DIR);
|
|
149
|
+
if (target !== profileRoot && !target.startsWith(`${profileRoot}${sep}`)) return;
|
|
149
150
|
let pids: string;
|
|
150
151
|
try {
|
|
151
152
|
pids = execFileSync("pgrep", ["-f", "--", userDataDir], { stdio: "pipe" }).toString().trim();
|
|
@@ -194,7 +195,7 @@ export function selectOrphanBrowserPids(
|
|
|
194
195
|
for (const { pid, userDataDir } of procs) {
|
|
195
196
|
if (pid === selfPid || !userDataDir) continue;
|
|
196
197
|
const dir = resolve(userDataDir);
|
|
197
|
-
if (!dir.startsWith(root)) continue;
|
|
198
|
+
if (dir !== root && !dir.startsWith(`${root}${sep}`)) continue;
|
|
198
199
|
if (live.has(dir)) continue; // belongs to a tracked instance
|
|
199
200
|
out.push(pid);
|
|
200
201
|
}
|