negotium 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-helpers.js +3366 -3048
- package/dist/agent-helpers.js.map +49 -48
- package/dist/background-bash.js +1 -3
- package/dist/background-bash.js.map +4 -4
- package/dist/browser-runtime.js +137 -23
- package/dist/browser-runtime.js.map +11 -10
- package/dist/chunk-qmbcbhyy.js +2407 -0
- package/dist/chunk-qmbcbhyy.js.map +35 -0
- package/dist/hosted-agent.js +56 -39
- package/dist/hosted-agent.js.map +13 -13
- package/dist/main.js +3276 -2808
- package/dist/main.js.map +54 -51
- package/dist/mcp-factories.js +6471 -6148
- package/dist/mcp-factories.js.map +49 -48
- package/dist/prompts.js +1 -3
- package/dist/prompts.js.map +4 -4
- package/dist/query-runtime.js +22 -4
- package/dist/query-runtime.js.map +6 -6
- package/dist/registry.js +169 -213
- package/dist/registry.js.map +8 -9
- package/dist/rollout.js +1 -1
- package/dist/runtime/src/agents/api-topic-agent-switch.ts +6 -12
- package/dist/runtime/src/agents/claude-provider.ts +2 -2
- package/dist/runtime/src/agents/codex-app-server.ts +136 -0
- package/dist/runtime/src/agents/codex-native-multi-agent.ts +1 -1
- package/dist/runtime/src/agents/codex-provider.ts +22 -11
- package/dist/runtime/src/agents/codex-registry.ts +9 -17
- package/dist/runtime/src/agents/contracts.ts +2 -0
- package/dist/runtime/src/agents/execution-host.ts +6 -0
- package/dist/runtime/src/agents/fork.ts +24 -18
- package/dist/runtime/src/agents/idle-archiver.ts +7 -2
- package/dist/runtime/src/agents/index.ts +21 -15
- package/dist/runtime/src/agents/maestro-provider.ts +1 -1
- package/dist/runtime/src/agents/maestro-registry.ts +26 -7
- package/dist/runtime/src/agents/memory-archive-policy.ts +20 -0
- package/dist/runtime/src/agents/rollout/codex.ts +31 -23
- package/dist/runtime/src/agents/self-config-core.ts +2 -4
- package/dist/runtime/src/agents/topic-agent-switch.ts +4 -10
- package/dist/runtime/src/application/submit-runtime-gateway-turn.ts +77 -19
- package/dist/runtime/src/application/switch-topic-model.ts +7 -10
- package/dist/runtime/src/index.ts +4 -0
- package/dist/runtime/src/mcp/factories/session-comm.ts +12 -0
- package/dist/runtime/src/mcp/session-comm/default-host.ts +16 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +25 -0
- package/dist/runtime/src/mcp/wiki-server.ts +19 -12
- package/dist/runtime/src/platform/config.ts +4 -3
- package/dist/runtime/src/platform/log-rotation.ts +45 -0
- package/dist/runtime/src/platform/mcp-config.ts +9 -3
- package/dist/runtime/src/platform/playwright/manager.ts +57 -1
- package/dist/runtime/src/platform/playwright/profile-management.ts +33 -0
- package/dist/runtime/src/platform/playwright/public-runtime.ts +5 -0
- package/dist/runtime/src/platform/playwright/vault-broker.ts +9 -10
- package/dist/runtime/src/query/active-rooms.ts +7 -1
- package/dist/runtime/src/runtime/errors.ts +1 -0
- package/dist/runtime/src/runtime/turn-event-stream.ts +46 -1
- package/dist/runtime/src/runtime/turn-runner.ts +176 -100
- package/dist/runtime/src/runtime/user-turn-envelope.ts +19 -0
- package/dist/runtime/src/storage/browser-profiles.ts +35 -10
- package/dist/runtime/src/storage/runtime-gateway-submissions.ts +12 -3
- package/dist/runtime/src/storage/runtime-turn-requests.ts +68 -7
- package/dist/runtime/src/storage/storage-host.ts +31 -5
- package/dist/runtime/src/storage/vault.ts +30 -2
- package/dist/runtime/src/topics/derive.ts +38 -2
- package/dist/runtime/src/topics/lifecycle.ts +12 -2
- package/dist/runtime/src/types.ts +2 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js +22 -4
- package/dist/runtime-helpers.js.map +5 -5
- package/dist/storage.js +22 -2
- package/dist/storage.js.map +5 -5
- package/dist/types/packages/core/src/agents/api-topic-agent-switch.d.ts +1 -1
- package/dist/types/packages/core/src/agents/codex-app-server.d.ts +13 -0
- package/dist/types/packages/core/src/agents/codex-native-multi-agent.d.ts +1 -0
- package/dist/types/packages/core/src/agents/contracts.d.ts +2 -0
- package/dist/types/packages/core/src/agents/execution-host.d.ts +2 -0
- package/dist/types/packages/core/src/agents/fork.d.ts +8 -7
- package/dist/types/packages/core/src/agents/memory-archive-policy.d.ts +10 -0
- package/dist/types/packages/core/src/agents/rollout/codex.d.ts +4 -2
- package/dist/types/packages/core/src/agents/self-config-core.d.ts +1 -1
- package/dist/types/packages/core/src/mcp/factories/session-comm.d.ts +1 -0
- package/dist/types/packages/core/src/platform/config.d.ts +0 -1
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +3 -0
- package/dist/types/packages/core/src/platform/playwright/manager.d.ts +15 -0
- package/dist/types/packages/core/src/platform/playwright/profile-management.d.ts +8 -0
- package/dist/types/packages/core/src/platform/playwright/public-runtime.d.ts +2 -1
- package/dist/types/packages/core/src/query/active-rooms.d.ts +7 -1
- package/dist/types/packages/core/src/runtime/turn-runner.d.ts +42 -1
- package/dist/types/packages/core/src/runtime/user-turn-envelope.d.ts +7 -0
- package/dist/types/packages/core/src/storage/browser-profiles.d.ts +3 -1
- package/dist/types/packages/core/src/storage/runtime-turn-requests.d.ts +17 -0
- package/dist/types/packages/core/src/storage/storage-host.d.ts +1 -0
- package/dist/types/packages/core/src/storage/vault.d.ts +8 -0
- package/dist/types/packages/core/src/types.d.ts +2 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/vault.js +19 -5
- package/dist/vault.js.map +5 -5
- package/package.json +2 -2
- package/dist/chunk-hsqgb2hw.js +0 -1223
- package/dist/chunk-hsqgb2hw.js.map +0 -19
|
@@ -22,6 +22,10 @@ export interface SessionCommMcpHost {
|
|
|
22
22
|
context: SessionCommContext,
|
|
23
23
|
profile: string,
|
|
24
24
|
): MaybePromise<SessionCommMcpResult>;
|
|
25
|
+
deleteBrowserProfile?(
|
|
26
|
+
context: SessionCommContext,
|
|
27
|
+
profile: string,
|
|
28
|
+
): MaybePromise<SessionCommMcpResult>;
|
|
25
29
|
peekSession(context: SessionCommContext): MaybePromise<SessionCommMcpResult>;
|
|
26
30
|
setDescription(
|
|
27
31
|
context: SessionCommContext,
|
|
@@ -90,6 +94,14 @@ export function createSessionCommMcpServer(
|
|
|
90
94
|
async ({ profile }) => host.setBrowserProfile!(context, profile),
|
|
91
95
|
);
|
|
92
96
|
}
|
|
97
|
+
if (host.deleteBrowserProfile) {
|
|
98
|
+
server.tool(
|
|
99
|
+
"delete_browser_profile",
|
|
100
|
+
"Delete an unused named browser profile and its on-disk data. The default or an assigned profile cannot be deleted.",
|
|
101
|
+
{ profile: z.string() },
|
|
102
|
+
async ({ profile }) => host.deleteBrowserProfile!(context, profile),
|
|
103
|
+
);
|
|
104
|
+
}
|
|
93
105
|
server.tool(
|
|
94
106
|
"peek_session",
|
|
95
107
|
"Inspect running and idle sessions, including pending ask_session calls.",
|
|
@@ -11,6 +11,7 @@ import { ACTIVE_QUERY_STALE_MS, MAX_TELL_DEPTH, USERS_LOG_DIR } from "#platform/
|
|
|
11
11
|
import { appendJsonlEntry, readJsonFile } from "#platform/jsonl";
|
|
12
12
|
import { OPTIONAL_FORUM_MCP_SERVERS, REQUIRED_FORUM_MCP_SERVERS } from "#platform/mcp-config";
|
|
13
13
|
import { closeBrowserOwnerTabs } from "#platform/playwright/manager";
|
|
14
|
+
import { deleteManagedBrowserProfile } from "#platform/playwright/profile-management";
|
|
14
15
|
import { sessionInboxPath } from "#query/session-inbox-path";
|
|
15
16
|
import { sanitizeId } from "#security/sanitize";
|
|
16
17
|
import { getApiTopicConfig, setApiTopicConfig } from "#storage/api-topic-config";
|
|
@@ -199,6 +200,21 @@ export function createDefaultSessionCommMcpHost(): SessionCommMcpHost {
|
|
|
199
200
|
}
|
|
200
201
|
},
|
|
201
202
|
|
|
203
|
+
async deleteBrowserProfile(context, profile) {
|
|
204
|
+
const topic = currentTopic(context);
|
|
205
|
+
if (!topic) return error("Error: No current API topic.");
|
|
206
|
+
if (!isTopicBrowserProfileOwner(topic.id, context.userId)) {
|
|
207
|
+
return error("Error: Only the topic owner can delete its browser profiles.");
|
|
208
|
+
}
|
|
209
|
+
try {
|
|
210
|
+
const ownerId = getBrowserProfileOwner(topic.id, context.userId);
|
|
211
|
+
const result = await deleteManagedBrowserProfile(ownerId, profile);
|
|
212
|
+
return ok(JSON.stringify(result, null, 2));
|
|
213
|
+
} catch (err) {
|
|
214
|
+
return error(`Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
|
|
202
218
|
peekSession(context) {
|
|
203
219
|
const targets = targetCatalog(context).listTargets();
|
|
204
220
|
const running: string[] = [];
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
REQUIRED_FORUM_MCP_SERVERS,
|
|
13
13
|
} from "#platform/mcp-config";
|
|
14
14
|
import { closeBrowserOwnerTabs } from "#platform/playwright/manager";
|
|
15
|
+
import { deleteManagedBrowserProfile } from "#platform/playwright/profile-management";
|
|
15
16
|
import { sanitizeId } from "#security/sanitize";
|
|
16
17
|
import { getTopic } from "#storage/api-topics";
|
|
17
18
|
import {
|
|
@@ -318,6 +319,30 @@ server.tool(
|
|
|
318
319
|
},
|
|
319
320
|
);
|
|
320
321
|
|
|
322
|
+
server.tool(
|
|
323
|
+
"delete_browser_profile",
|
|
324
|
+
"Delete an unused named browser profile and its on-disk data. The default or an assigned profile cannot be deleted.",
|
|
325
|
+
{
|
|
326
|
+
profile: z
|
|
327
|
+
.string()
|
|
328
|
+
.describe("Unused named profile to delete; the default profile cannot be deleted"),
|
|
329
|
+
},
|
|
330
|
+
async ({ profile }) => {
|
|
331
|
+
const topicId = currentApiTopicId();
|
|
332
|
+
if (!topicId || !userId) return mcpError("Error: No current API topic.");
|
|
333
|
+
if (!isTopicBrowserProfileOwner(topicId, userId)) {
|
|
334
|
+
return mcpError("Error: Only the topic owner can delete its browser profiles.");
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const ownerId = getBrowserProfileOwner(topicId, userId);
|
|
338
|
+
const result = await deleteManagedBrowserProfile(ownerId, profile);
|
|
339
|
+
return mcpOk(JSON.stringify(result, null, 2));
|
|
340
|
+
} catch (err) {
|
|
341
|
+
return mcpError(`Error: ${errMsg(err)}`);
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
);
|
|
345
|
+
|
|
321
346
|
// --- always-available session inspection / self-config ---
|
|
322
347
|
|
|
323
348
|
server.tool(
|
|
@@ -439,23 +439,28 @@ function wikiLastConversation(args: Record<string, unknown>): CallToolResult {
|
|
|
439
439
|
|
|
440
440
|
const name = slugify(topicNameFrom(rawTopic));
|
|
441
441
|
|
|
442
|
-
// Try per-topic archive dir first, then flat file
|
|
442
|
+
// Try per-topic archive dir first, then flat file. This is a read path —
|
|
443
|
+
// it must never create directories on lookup (that previously left an
|
|
444
|
+
// empty `wiki/archive/<topic>/` behind for every topic name ever queried,
|
|
445
|
+
// even when nothing was ever archived under the per-topic layout).
|
|
443
446
|
const archiveDir = resolve(runtime().archiveDir, name);
|
|
444
|
-
ensureDir(archiveDir);
|
|
445
447
|
|
|
446
448
|
try {
|
|
447
449
|
// List archive files sorted by name (which should be date-sorted)
|
|
448
|
-
let files =
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
450
|
+
let files = existsSync(archiveDir)
|
|
451
|
+
? readdirSync(archiveDir)
|
|
452
|
+
.filter((f) => f.endsWith(".jsonl"))
|
|
453
|
+
.sort()
|
|
454
|
+
.reverse()
|
|
455
|
+
: [];
|
|
452
456
|
if (files.length === 0) {
|
|
453
457
|
// Try flat archive
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
458
|
+
files = existsSync(runtime().archiveDir)
|
|
459
|
+
? readdirSync(runtime().archiveDir)
|
|
460
|
+
.filter((f) => f.startsWith(name) && f.endsWith(".jsonl"))
|
|
461
|
+
.sort()
|
|
462
|
+
.reverse()
|
|
463
|
+
: [];
|
|
459
464
|
}
|
|
460
465
|
if (files.length === 0) {
|
|
461
466
|
return {
|
|
@@ -467,7 +472,9 @@ function wikiLastConversation(args: Record<string, unknown>): CallToolResult {
|
|
|
467
472
|
const actualPath = files[0].includes("/")
|
|
468
473
|
? files[0]
|
|
469
474
|
: resolve(
|
|
470
|
-
readdirSync(archiveDir).includes(files[0])
|
|
475
|
+
existsSync(archiveDir) && readdirSync(archiveDir).includes(files[0])
|
|
476
|
+
? archiveDir
|
|
477
|
+
: runtime().archiveDir,
|
|
471
478
|
files[0],
|
|
472
479
|
);
|
|
473
480
|
|
|
@@ -75,7 +75,6 @@ export const BROWSER_PROFILES_DIR = resolve(BROWSER_DIR, "profiles");
|
|
|
75
75
|
export const BINARIES_DIR = resolve(STATE_DIR, "binaries");
|
|
76
76
|
export const SECRETS_DIR = resolve(STATE_DIR, "secrets");
|
|
77
77
|
// Legacy names remain public, but their durable contents now live under data.
|
|
78
|
-
export const CONTEXTS_DIR = resolve(STATE_DIR, "data", "contexts");
|
|
79
78
|
export const DM_WORKSPACE_DIR = resolve(STATE_DIR, "data", "dm");
|
|
80
79
|
export const SESSION_WORKSPACE_DIR = resolve(STATE_DIR, "data", "sessions");
|
|
81
80
|
// The Claude Agent SDK ships a platform-matched Claude Code binary. Keep that
|
|
@@ -364,7 +363,6 @@ mkdirSync(WORKSPACE_DIR, { recursive: true });
|
|
|
364
363
|
mkdirSync(TOPIC_WORKSPACE_DIR, { recursive: true });
|
|
365
364
|
mkdirSync(SHARED_WIKI_DIR, { recursive: true });
|
|
366
365
|
mkdirSync(CRON_WORKSPACE_DIR, { recursive: true });
|
|
367
|
-
mkdirSync(CONTEXTS_DIR, { recursive: true });
|
|
368
366
|
mkdirSync(DM_WORKSPACE_DIR, { recursive: true });
|
|
369
367
|
mkdirSync(SESSION_WORKSPACE_DIR, { recursive: true });
|
|
370
368
|
mkdirSync(BROWSER_DIR, { recursive: true });
|
|
@@ -453,7 +451,10 @@ export const MAX_TELL_DEPTH =
|
|
|
453
451
|
/** Codex CLI auth file. 호출 시점에 env를 읽는다 — 테스트가 런타임에
|
|
454
452
|
* NEGOTIUM_CODEX_AUTH_FILE을 바꾸므로 모듈 로드 상수로 만들면 안 된다. */
|
|
455
453
|
export function codexAuthFilePath(): string {
|
|
456
|
-
return
|
|
454
|
+
return (
|
|
455
|
+
process.env.NEGOTIUM_CODEX_AUTH_FILE ||
|
|
456
|
+
join(process.env.CODEX_HOME || join(homedir(), ".codex"), "auth.json")
|
|
457
|
+
);
|
|
457
458
|
}
|
|
458
459
|
|
|
459
460
|
// System defaults moved to per-agent registries
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Size-based rotation for daemon stderr logs.
|
|
3
|
+
*
|
|
4
|
+
* Negotium adapters (`negotium-terminal`, `negotium-telegram`, `negotium-otium`)
|
|
5
|
+
* each spawn the node daemon with its stderr redirected straight to
|
|
6
|
+
* `<LOG_DIR>/node-daemon.log`. That file is opened once at spawn time and
|
|
7
|
+
* accumulates for the daemon's whole lifetime with no cap, so long-running
|
|
8
|
+
* hosts eventually carry an unbounded log file.
|
|
9
|
+
*
|
|
10
|
+
* True in-place rotation (SIGHUP-style reopen) would require the daemon to
|
|
11
|
+
* cooperate by reopening its own stderr fd, which Bun does not expose. The
|
|
12
|
+
* practical fix instead rotates *before* spawn: every time a CLI is about to
|
|
13
|
+
* start the daemon, check the previous log's size and, if it crossed the
|
|
14
|
+
* threshold, rename it aside with a timestamp so the new spawn starts a
|
|
15
|
+
* fresh, empty file.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { existsSync, renameSync, statSync } from "node:fs";
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
|
|
21
|
+
/** Rotate once the log exceeds this size (bytes) at the next daemon start. */
|
|
22
|
+
export const DEFAULT_DAEMON_LOG_ROTATE_BYTES = 20 * 1024 * 1024; // 20MB
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* If `logPath` exists and is at least `maxBytes`, rename it aside with an
|
|
26
|
+
* ISO-ish timestamp suffix so a fresh file can be created at `logPath`.
|
|
27
|
+
* No-op (and never throws) when the file is missing, under the threshold, or
|
|
28
|
+
* the rename fails for any reason — log rotation must never block startup.
|
|
29
|
+
*/
|
|
30
|
+
export function rotateOversizedLog(
|
|
31
|
+
logPath: string,
|
|
32
|
+
maxBytes: number = DEFAULT_DAEMON_LOG_ROTATE_BYTES,
|
|
33
|
+
): void {
|
|
34
|
+
try {
|
|
35
|
+
if (!existsSync(logPath)) return;
|
|
36
|
+
const { size } = statSync(logPath);
|
|
37
|
+
if (size < maxBytes) return;
|
|
38
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
39
|
+
const dir = join(logPath, "..");
|
|
40
|
+
const base = logPath.slice(dir.length + 1);
|
|
41
|
+
renameSync(logPath, join(dir, `${base}.${stamp}`));
|
|
42
|
+
} catch {
|
|
43
|
+
// Best-effort: a failed rotation should never prevent the daemon from starting.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -207,6 +207,8 @@ export function consumePlaywrightUnavailable(userId: string, topic: string | und
|
|
|
207
207
|
|
|
208
208
|
export interface RuntimeMcpBuildContext {
|
|
209
209
|
userId: string;
|
|
210
|
+
/** Vault namespace when credentials belong to a different principal. */
|
|
211
|
+
vaultUserId?: string;
|
|
210
212
|
/** "dm" for DM scope, topic/session name for forum/fork. */
|
|
211
213
|
session: string;
|
|
212
214
|
/** REST/WS topic id when known. Prefer for authorization/scope checks. */
|
|
@@ -527,13 +529,13 @@ const MCP_CATALOG: Record<string, RuntimeMcpCatalogEntry> = {
|
|
|
527
529
|
vault: {
|
|
528
530
|
...commonRuntimeMcpPolicy("vault"),
|
|
529
531
|
build(ctx) {
|
|
530
|
-
const { userId, agent } = ctx;
|
|
532
|
+
const { userId, vaultUserId, agent } = ctx;
|
|
531
533
|
// Normal turns use {{KEY}} directly in tool inputs. Keep the Vault MCP
|
|
532
534
|
// surface focused on key discovery; broker tools remain available from
|
|
533
535
|
// the public factory for compatibility but are no longer the default UX.
|
|
534
|
-
const args = [`--user-id=${userId}`];
|
|
536
|
+
const args = [`--user-id=${vaultUserId ?? userId}`];
|
|
535
537
|
if (agent !== "codex") args.push("--list-only=true");
|
|
536
|
-
return buildBuiltinMcpServer("vault", ctx, () =>
|
|
538
|
+
return buildBuiltinMcpServer("vault", { ...ctx, userId: vaultUserId ?? userId }, () =>
|
|
537
539
|
buildStdioMcpServer(agent, VAULT_SERVER, args),
|
|
538
540
|
);
|
|
539
541
|
},
|
|
@@ -767,6 +769,7 @@ export function getManagerMcpServers(opts: {
|
|
|
767
769
|
*/
|
|
768
770
|
export function getForumMcpServers(opts: {
|
|
769
771
|
userId: string;
|
|
772
|
+
vaultUserId?: string;
|
|
770
773
|
session: string;
|
|
771
774
|
topicId?: string;
|
|
772
775
|
subagentParentTopicId?: string;
|
|
@@ -790,6 +793,7 @@ export function getForumMcpServers(opts: {
|
|
|
790
793
|
}) {
|
|
791
794
|
const {
|
|
792
795
|
userId,
|
|
796
|
+
vaultUserId,
|
|
793
797
|
session,
|
|
794
798
|
topicId,
|
|
795
799
|
subagentParentTopicId,
|
|
@@ -824,6 +828,7 @@ export function getForumMcpServers(opts: {
|
|
|
824
828
|
"forum",
|
|
825
829
|
{
|
|
826
830
|
userId,
|
|
831
|
+
vaultUserId,
|
|
827
832
|
session,
|
|
828
833
|
topicId,
|
|
829
834
|
subagentParentTopicId,
|
|
@@ -945,6 +950,7 @@ export function getMcpServersForQuery(opts: AgentQueryOptions): Record<string, u
|
|
|
945
950
|
}
|
|
946
951
|
return getForumMcpServers({
|
|
947
952
|
userId: opts.userId || "local",
|
|
953
|
+
vaultUserId: opts.vaultUserId,
|
|
948
954
|
session: opts.session || "default",
|
|
949
955
|
topicId: opts.topicId,
|
|
950
956
|
subagentParentTopicId: opts.subagentParentTopicId,
|
|
@@ -84,10 +84,23 @@ export interface PlaywrightManagerHost {
|
|
|
84
84
|
) => NodeJS.ProcessEnv;
|
|
85
85
|
/** Reap an orphan that owns this exact host-managed data directory. */
|
|
86
86
|
readonly cleanupBrowserProcessesForDataDir: (userDataDir: string) => void;
|
|
87
|
+
/** Remove an exact host-managed profile directory, rejecting paths outside the host's root. */
|
|
88
|
+
readonly removeProfileDataDir?: (userDataDir: string) => boolean;
|
|
87
89
|
/** Reap host-managed browsers that are not represented by the supplied live directories. */
|
|
88
90
|
readonly reapOrphanBrowsers: (liveUserDataDirs: Iterable<string>) => void;
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
function removeDefaultProfileDataDir(userDataDir: string): boolean {
|
|
94
|
+
const root = resolve(BROWSER_PROFILES_DIR);
|
|
95
|
+
const target = resolve(userDataDir);
|
|
96
|
+
if (dirname(target) !== root) {
|
|
97
|
+
throw new Error(`Refusing to delete browser profile outside managed root: ${target}`);
|
|
98
|
+
}
|
|
99
|
+
const existed = existsSync(target);
|
|
100
|
+
rmSync(target, { recursive: true, force: true });
|
|
101
|
+
return existed;
|
|
102
|
+
}
|
|
103
|
+
|
|
91
104
|
export {
|
|
92
105
|
cleanupZombiePlaywright,
|
|
93
106
|
isBrowserJanitorOwner,
|
|
@@ -210,6 +223,7 @@ const defaultManagerHost: Readonly<PlaywrightManagerHost> = Object.freeze({
|
|
|
210
223
|
},
|
|
211
224
|
createChildEnvironment: defaultChildEnvironment,
|
|
212
225
|
cleanupBrowserProcessesForDataDir: killBrowserProcsForUserDataDir,
|
|
226
|
+
removeProfileDataDir: removeDefaultProfileDataDir,
|
|
213
227
|
reapOrphanBrowsers,
|
|
214
228
|
});
|
|
215
229
|
|
|
@@ -239,10 +253,12 @@ export function configurePlaywrightManagerHost(
|
|
|
239
253
|
next.resolveInstanceDataDir !== defaultManagerHost.resolveInstanceDataDir &&
|
|
240
254
|
(next.cleanupBrowserProcessesForDataDir ===
|
|
241
255
|
defaultManagerHost.cleanupBrowserProcessesForDataDir ||
|
|
256
|
+
!next.removeProfileDataDir ||
|
|
257
|
+
next.removeProfileDataDir === defaultManagerHost.removeProfileDataDir ||
|
|
242
258
|
next.reapOrphanBrowsers === defaultManagerHost.reapOrphanBrowsers)
|
|
243
259
|
) {
|
|
244
260
|
throw new Error(
|
|
245
|
-
"custom Playwright profile paths require host crash cleanup and orphan sweep hooks",
|
|
261
|
+
"custom Playwright profile paths require host crash cleanup, profile removal, and orphan sweep hooks",
|
|
246
262
|
);
|
|
247
263
|
}
|
|
248
264
|
managerHost = Object.freeze(next);
|
|
@@ -293,6 +309,7 @@ function deletePortFile(instanceKey: string) {
|
|
|
293
309
|
try {
|
|
294
310
|
unlinkSync(join(managerHost.portsDir, portFileName(instanceKey)));
|
|
295
311
|
} catch (e) {
|
|
312
|
+
if ((e as NodeJS.ErrnoException).code === "ENOENT") return;
|
|
296
313
|
logger.warn({ err: e, instanceKey }, "Failed to delete playwright port file");
|
|
297
314
|
}
|
|
298
315
|
}
|
|
@@ -433,6 +450,45 @@ export function stopPlaywrightProfile(ownerId: string, rawProfile: string): Prom
|
|
|
433
450
|
);
|
|
434
451
|
}
|
|
435
452
|
|
|
453
|
+
export interface RemovePlaywrightProfileDataResult {
|
|
454
|
+
dataDir: string;
|
|
455
|
+
processStopped: boolean;
|
|
456
|
+
dataRemoved: boolean;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export interface RemovePlaywrightProfileDataOptions {
|
|
460
|
+
/** Revalidate external ownership immediately before the synchronous removal. */
|
|
461
|
+
beforeRemove?: () => void;
|
|
462
|
+
/** Commit external metadata before releasing the profile lifecycle barrier. */
|
|
463
|
+
afterRemove?: () => void;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Stop all exact-profile processes and remove its data through the configured host boundary. */
|
|
467
|
+
export function removePlaywrightProfileData(
|
|
468
|
+
ownerId: string,
|
|
469
|
+
rawProfile: string,
|
|
470
|
+
options: RemovePlaywrightProfileDataOptions = {},
|
|
471
|
+
): Promise<RemovePlaywrightProfileDataResult> {
|
|
472
|
+
return withPlaywrightProfileMaintenance(
|
|
473
|
+
ownerId,
|
|
474
|
+
rawProfile,
|
|
475
|
+
async (binding, { stopInstance }) => {
|
|
476
|
+
const dataDir = resolveUserDataDir(binding.instanceKey);
|
|
477
|
+
const processStopped = await stopInstance(binding.instanceKey);
|
|
478
|
+
managerHost.cleanupBrowserProcessesForDataDir(dataDir);
|
|
479
|
+
deletePortFile(binding.instanceKey);
|
|
480
|
+
options.beforeRemove?.();
|
|
481
|
+
const removeProfileDataDir = managerHost.removeProfileDataDir;
|
|
482
|
+
if (!removeProfileDataDir) {
|
|
483
|
+
throw new Error("Browser profile deletion is not configured for this host.");
|
|
484
|
+
}
|
|
485
|
+
const dataRemoved = removeProfileDataDir(dataDir);
|
|
486
|
+
options.afterRemove?.();
|
|
487
|
+
return { dataDir, processStopped, dataRemoved };
|
|
488
|
+
},
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
436
492
|
// A shared profile may have several concurrent turns. Reference counts keep an
|
|
437
493
|
// idle sweep from evicting the process until every borrower has finished.
|
|
438
494
|
const pinnedInstances = new Map<string, number>();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { removePlaywrightProfileData } from "#platform/playwright/manager";
|
|
2
|
+
import { assertBrowserProfileDeletable, deleteBrowserProfile } from "#storage/browser-profiles";
|
|
3
|
+
|
|
4
|
+
export interface DeleteManagedBrowserProfileResult {
|
|
5
|
+
profile: string;
|
|
6
|
+
metadataRemoved: boolean;
|
|
7
|
+
dataRemoved: boolean;
|
|
8
|
+
processStopped: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Delete an unused named profile while serialized against browser startup. */
|
|
12
|
+
export async function deleteManagedBrowserProfile(
|
|
13
|
+
ownerId: string,
|
|
14
|
+
rawProfile: string,
|
|
15
|
+
): Promise<DeleteManagedBrowserProfileResult> {
|
|
16
|
+
const profile = assertBrowserProfileDeletable(ownerId, rawProfile);
|
|
17
|
+
let metadataRemoved = false;
|
|
18
|
+
const removed = await removePlaywrightProfileData(ownerId, profile, {
|
|
19
|
+
beforeRemove: () => {
|
|
20
|
+
assertBrowserProfileDeletable(ownerId, profile);
|
|
21
|
+
},
|
|
22
|
+
afterRemove: () => {
|
|
23
|
+
metadataRemoved = deleteBrowserProfile(ownerId, profile);
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
profile,
|
|
29
|
+
metadataRemoved,
|
|
30
|
+
dataRemoved: removed.dataRemoved,
|
|
31
|
+
processStopped: removed.processStopped,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -20,6 +20,7 @@ export {
|
|
|
20
20
|
type PlaywrightProfileBinding,
|
|
21
21
|
pinPlaywrightInstance,
|
|
22
22
|
reapPlaywrightOrphans,
|
|
23
|
+
removePlaywrightProfileData,
|
|
23
24
|
resetPlaywrightManagerHost,
|
|
24
25
|
resolvePlaywrightCapabilityOwner,
|
|
25
26
|
resolvePlaywrightProfileBinding,
|
|
@@ -39,6 +40,10 @@ export {
|
|
|
39
40
|
waitForChildProcessExit,
|
|
40
41
|
waitForChildProcessSpawnError,
|
|
41
42
|
} from "./manager-utils";
|
|
43
|
+
export {
|
|
44
|
+
type DeleteManagedBrowserProfileResult,
|
|
45
|
+
deleteManagedBrowserProfile,
|
|
46
|
+
} from "./profile-management";
|
|
42
47
|
export {
|
|
43
48
|
probeMcpTransport,
|
|
44
49
|
probePlaywrightMcpTransports,
|
|
@@ -3,7 +3,7 @@ import { chmodSync } from "node:fs";
|
|
|
3
3
|
import { createServer, type Server, type Socket } from "node:net";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import {
|
|
6
|
+
import { redactVaultSecrets, vaultGetValue, vaultSubstituteDetailed } from "#storage/vault-public";
|
|
7
7
|
|
|
8
8
|
const MAX_REQUEST_BYTES = 16 * 1024 * 1024;
|
|
9
9
|
const DEFAULT_OUTPUT_LIMIT = 100_000;
|
|
@@ -158,16 +158,15 @@ export async function createBrowserVaultBroker(userId: string): Promise<BrowserV
|
|
|
158
158
|
if (leases.size >= MAX_ACTIVE_LEASES) {
|
|
159
159
|
return { id, ok: false, error: "too many active leases" };
|
|
160
160
|
}
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
if (secret === undefined) return match;
|
|
161
|
+
const substituted = deepMapStrings(request.value, (text) => {
|
|
162
|
+
const result = vaultSubstituteDetailed(userId, text);
|
|
163
|
+
for (const key of result.usedKeys) {
|
|
164
|
+
const secret = vaultGetValue(userId, key);
|
|
165
|
+
if (secret === undefined) continue;
|
|
167
166
|
for (const form of encodedSecretForms(secret)) retainedForms.set(form, key);
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
);
|
|
167
|
+
}
|
|
168
|
+
return result.text;
|
|
169
|
+
});
|
|
171
170
|
const prepared = prepareInput(request.tool, substituted);
|
|
172
171
|
const lease = randomBytes(24).toString("hex");
|
|
173
172
|
leases.set(lease, { boundary: prepared.boundary, createdAt: Date.now() });
|
|
@@ -42,6 +42,8 @@ export interface DeferredInject {
|
|
|
42
42
|
/** Topic execution epoch captured when this work was accepted. */
|
|
43
43
|
runtimeEpoch?: number;
|
|
44
44
|
userId: string;
|
|
45
|
+
/** Credential namespace when the executor and Vault owner differ. */
|
|
46
|
+
vaultUserId?: string;
|
|
45
47
|
prompt: string;
|
|
46
48
|
/** Inject source — the topic name/id this inject came from (never "user"). */
|
|
47
49
|
origin: string;
|
|
@@ -120,8 +122,12 @@ export interface RoomQueryControl {
|
|
|
120
122
|
durableRequestIds?: string[];
|
|
121
123
|
/** Attachment ids currently being processed. User-turn preemption carries them forward. */
|
|
122
124
|
attachments?: string[];
|
|
123
|
-
/**
|
|
125
|
+
/** Latest provider session id observed for this turn. */
|
|
124
126
|
sessionId?: string | null;
|
|
127
|
+
/** True once the provider has published the native session backing this turn. */
|
|
128
|
+
providerSessionObserved?: boolean;
|
|
129
|
+
/** True once provider output proves that this turn's prompt reached the native session. */
|
|
130
|
+
providerTurnContentObserved?: boolean;
|
|
125
131
|
abortController: AbortController;
|
|
126
132
|
abortReason: AbortReason;
|
|
127
133
|
/** Specific failure surfaced when infrastructure aborts the turn. */
|
|
@@ -64,6 +64,7 @@ export function isSessionExpiredError(message: string): boolean {
|
|
|
64
64
|
return (
|
|
65
65
|
message.includes(SESSION_EXPIRED_MSG) ||
|
|
66
66
|
/session was recorded with model .+ but is resuming with/i.test(message) ||
|
|
67
|
+
/failed to resolve rollout path .+file does not exist/i.test(message) ||
|
|
67
68
|
/session.*not found|session.*expired|unknown conversation/i.test(message)
|
|
68
69
|
);
|
|
69
70
|
}
|
|
@@ -64,6 +64,10 @@ import {
|
|
|
64
64
|
updateApiMessageUsage,
|
|
65
65
|
} from "#storage/api-messages";
|
|
66
66
|
import { getTopic, setTopicSessionId } from "#storage/api-topics";
|
|
67
|
+
import {
|
|
68
|
+
getRuntimeUserTurnRequest,
|
|
69
|
+
markRuntimeUserTurnProviderSessionObserved,
|
|
70
|
+
} from "#storage/runtime-turn-requests";
|
|
67
71
|
import { recordUsage } from "#storage/token-stats";
|
|
68
72
|
import type { AgentKind, EffortLevel, PeerRuntimeBridgeContext, UnifiedEvent } from "#types";
|
|
69
73
|
import type { MessageDto } from "#types/api";
|
|
@@ -267,6 +271,30 @@ export async function runTurnEventStream(
|
|
|
267
271
|
for await (const event of events) {
|
|
268
272
|
if (abortController.signal.aborted) break;
|
|
269
273
|
|
|
274
|
+
// A provider may publish its session id before it appends the user
|
|
275
|
+
// prompt to the native rollout. Only semantic provider output proves
|
|
276
|
+
// that the prompt made it into that session. This distinction matters
|
|
277
|
+
// when a second user message preempts the turn in that short window:
|
|
278
|
+
// the replacement must replay both messages instead of dropping the
|
|
279
|
+
// first one merely because a session event was seen.
|
|
280
|
+
if (
|
|
281
|
+
!control.providerTurnContentObserved &&
|
|
282
|
+
(event.type === "text_delta" ||
|
|
283
|
+
event.type === "text" ||
|
|
284
|
+
event.type === "reasoning" ||
|
|
285
|
+
event.type === "tool_use" ||
|
|
286
|
+
event.type === "tool_result" ||
|
|
287
|
+
event.type === "file" ||
|
|
288
|
+
event.type === "result")
|
|
289
|
+
) {
|
|
290
|
+
control.providerTurnContentObserved = true;
|
|
291
|
+
if (control.sessionId) {
|
|
292
|
+
for (const requestId of control.durableRequestIds ?? []) {
|
|
293
|
+
markRuntimeUserTurnProviderSessionObserved(topicId, requestId, control.sessionId);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
270
298
|
switch (event.type) {
|
|
271
299
|
case "text_delta": {
|
|
272
300
|
pendingSawDelta = true;
|
|
@@ -672,6 +700,16 @@ export async function runTurnEventStream(
|
|
|
672
700
|
}
|
|
673
701
|
break;
|
|
674
702
|
case "session":
|
|
703
|
+
// Keep the control synchronized with the provider, not merely the
|
|
704
|
+
// topic row. A superseding user message can then resume the partial
|
|
705
|
+
// native rollout after this process has fully unwound.
|
|
706
|
+
control.sessionId = event.sessionId;
|
|
707
|
+
control.providerSessionObserved = true;
|
|
708
|
+
if (control.providerTurnContentObserved) {
|
|
709
|
+
for (const requestId of control.durableRequestIds ?? []) {
|
|
710
|
+
markRuntimeUserTurnProviderSessionObserved(topicId, requestId, event.sessionId);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
675
713
|
// Persist visible topic turns only. Silent ask_session forks use
|
|
676
714
|
// temporary session ids and must not clobber the topic's durable
|
|
677
715
|
// session; visible tell/reply injects do belong to the main topic
|
|
@@ -788,7 +826,14 @@ export async function runTurnEventStream(
|
|
|
788
826
|
// Drain one deferred session-inject if the room is now idle. If a
|
|
789
827
|
// replacement turn already occupies the slot, leave it queued — that
|
|
790
828
|
// turn's finally will drain it when it completes.
|
|
791
|
-
|
|
829
|
+
const queuedUserTurn = roomId === topicId ? getRuntimeUserTurnRequest(topicId) : null;
|
|
830
|
+
const hasReplacementUserTurn = queuedUserTurn !== null && queuedUserTurn.requestId !== queryId;
|
|
831
|
+
if (
|
|
832
|
+
roomId === topicId &&
|
|
833
|
+
outcome.kind !== "session-expired" &&
|
|
834
|
+
!getRoomQuery(topicId) &&
|
|
835
|
+
!hasReplacementUserTurn
|
|
836
|
+
) {
|
|
792
837
|
const next = takeDeferredInject(topicId);
|
|
793
838
|
if (next) redispatchInject(next);
|
|
794
839
|
}
|