negotium 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/agent-helpers.js +40 -24
  2. package/dist/agent-helpers.js.map +9 -9
  3. package/dist/background-bash.js +2 -2
  4. package/dist/background-bash.js.map +2 -2
  5. package/dist/browser-runtime.js +4 -3
  6. package/dist/browser-runtime.js.map +4 -4
  7. package/dist/{chunk-qmbcbhyy.js → chunk-knv045mn.js} +2 -2
  8. package/dist/{chunk-qmbcbhyy.js.map → chunk-knv045mn.js.map} +2 -2
  9. package/dist/hosted-agent.js +8 -4
  10. package/dist/hosted-agent.js.map +5 -5
  11. package/dist/main.js +40 -24
  12. package/dist/main.js.map +9 -9
  13. package/dist/mcp-factories.js +40 -24
  14. package/dist/mcp-factories.js.map +9 -9
  15. package/dist/prompts.js +14 -4
  16. package/dist/prompts.js.map +4 -4
  17. package/dist/query-runtime.js +2 -2
  18. package/dist/query-runtime.js.map +2 -2
  19. package/dist/registry.js +10 -5
  20. package/dist/registry.js.map +4 -4
  21. package/dist/rollout.js +1 -1
  22. package/dist/runtime/src/agents/claude-provider.ts +7 -0
  23. package/dist/runtime/src/agents/maestro-registry.ts +37 -2
  24. package/dist/runtime/src/agents/model-catalog.ts +18 -4
  25. package/dist/runtime/src/mcp/background-bash-server.ts +194 -17
  26. package/dist/runtime/src/platform/config.ts +1 -1
  27. package/dist/runtime/src/platform/playwright/vault-broker.ts +7 -0
  28. package/dist/runtime/src/runtime/turn-runner.ts +34 -11
  29. package/dist/runtime/src/version.ts +1 -1
  30. package/dist/runtime-helpers.js +2 -2
  31. package/dist/runtime-helpers.js.map +2 -2
  32. package/dist/types/packages/core/src/agents/model-catalog.d.ts +4 -3
  33. package/dist/types/packages/core/src/platform/config.d.ts +1 -1
  34. package/dist/types/packages/core/src/version.d.ts +1 -1
  35. package/dist/vault.js +2 -2
  36. package/dist/vault.js.map +2 -2
  37. package/install-browser-rs.mjs +3 -3
  38. package/package.json +2 -2
@@ -55,7 +55,8 @@ export interface SelectableModel {
55
55
  }
56
56
 
57
57
  /**
58
- * Pricing and quota observations were checked on 2026-07-19.
58
+ * Pricing and quota observations were checked on 2026-07-19, DeepSeek pricing
59
+ * re-checked 2026-08-04 for the new deepseek-v4-flash model.
59
60
  * Official references:
60
61
  * - https://learn.chatgpt.com/docs/pricing
61
62
  * - https://help.openai.com/en/articles/20001106
@@ -67,9 +68,9 @@ export interface SelectableModel {
67
68
  * meter cached input, fresh input, output, reasoning, speed, and model choice
68
69
  * differently and may change server-side weights without publishing a token cap.
69
70
  */
70
- export const MODEL_COST_RESEARCHED_AT = "2026-07-19";
71
+ export const MODEL_COST_RESEARCHED_AT = "2026-08-04";
71
72
  export const MODEL_COST_ROUTING_SUMMARY =
72
- "Cost basis (2026-07-19): Codex Pro 20x and Claude Max 20x are each $200/month; Maestro models are pay-per-token. DeepSeek Pro is cheapest.";
73
+ "Cost basis (2026-08-04): Codex Pro 20x and Claude Max 20x are each $200/month; Maestro models are pay-per-token. DeepSeek Flash is cheapest.";
73
74
 
74
75
  const CODEX_PRO_20X_COST = "ChatGPT Pro 20x subscription: $200/month";
75
76
  const CODEX_COMMUNITY_WEEKLY =
@@ -173,13 +174,26 @@ export const SELECTABLE_MODELS: readonly SelectableModel[] = [
173
174
  agent: "maestro",
174
175
  description: "API-priced Sonnet-level route for cost-efficient everyday work.",
175
176
  intelligenceTier: "sonnet",
176
- routingSummary: "cost-efficient everyday work; pay-per-token and cheapest route",
177
+ routingSummary: "cost-efficient everyday work; pay-per-token, pricier than DeepSeek Flash",
177
178
  accessCost: "DeepSeek V4 Pro pay-as-you-go API; no monthly subscription required",
178
179
  marginalTokenCost:
179
180
  "DeepSeek API: $0.435/M uncached input, $0.003625/M cached input, $0.87/M output",
180
181
  estimatedUsage:
181
182
  "No subscription token cap; pay per token. Official account concurrency limit is 500 requests.",
182
183
  },
184
+ {
185
+ model: "deepseek-flash",
186
+ agent: "maestro",
187
+ description:
188
+ "Faster, cheaper DeepSeek V4 variant (DeepSeek-V4-Flash-0731) at similar Sonnet-level quality for high-volume everyday work.",
189
+ intelligenceTier: "sonnet",
190
+ routingSummary: "cheapest overall route; pay-per-token, 1M context, 5x Pro's concurrency limit",
191
+ accessCost: "DeepSeek V4 Flash pay-as-you-go API; no monthly subscription required",
192
+ marginalTokenCost:
193
+ "DeepSeek API: $0.14/M uncached input, $0.0028/M cached input, $0.28/M output",
194
+ estimatedUsage:
195
+ "No subscription token cap; pay per token. Official account concurrency limit is 2500 requests.",
196
+ },
183
197
  ];
184
198
 
185
199
  const SELECTABLE_MODEL_ALIASES: Readonly<Record<string, string>> = {
@@ -65,10 +65,29 @@ const COMPLETED_RETENTION_MS = 60 * 60_000;
65
65
  const MAX_COMPLETED_PROCS = 100;
66
66
  /** Count-based pruning ignores jobs this fresh; their turn may still be queued. */
67
67
  const PRUNE_GRACE_MS = 5 * 60_000;
68
+ const DEFAULT_WATCH_TIMEOUT_SECONDS = 3_600;
69
+ const MAX_WATCH_TIMEOUT_SECONDS = 86_400;
68
70
 
69
71
  /** Root for full-output spill files, one directory per bash id. */
70
72
  const SPILL_ROOT = join(RUN_DIR, "bg-bash-output");
71
73
 
74
+ /**
75
+ * One-shot match-and-notify state for `background_bash_watch`. `matched` and
76
+ * `finished` are distinct: `matched` records the specific outcome (a line hit
77
+ * `regex`) for the injected message; `finished` guards against `finishProc`
78
+ * double-delivering once a match or timeout has already sent the one turn a
79
+ * watch job promises.
80
+ */
81
+ interface WatchState {
82
+ regex: RegExp;
83
+ target: "stdout" | "stderr" | "both";
84
+ matched: boolean;
85
+ finished: boolean;
86
+ timeoutTimer?: ReturnType<typeof setTimeout>;
87
+ stdoutCarry: string;
88
+ stderrCarry: string;
89
+ }
90
+
72
91
  interface BgProc {
73
92
  bashId: string;
74
93
  userId: string;
@@ -86,6 +105,8 @@ interface BgProc {
86
105
  startedAt: number;
87
106
  killTimer?: ReturnType<typeof setTimeout>;
88
107
  cleanupTimer?: ReturnType<typeof setTimeout>;
108
+ /** Present only for jobs started via `background_bash_watch`. */
109
+ watch?: WatchState;
89
110
  }
90
111
 
91
112
  const procs = new Map<string, BgProc>();
@@ -175,30 +196,33 @@ function describeStream(label: string, snapshot: OutputSnapshot): string | undef
175
196
  return `${label} (${notes.join(" · ")}):\n${body}`;
176
197
  }
177
198
 
178
- /**
179
- * Deliver the completion turn.
180
- *
181
- * Returns false when the inbox write failed. The tool promises the caller it
182
- * need not poll, so a dropped completion is the one failure that leaves a
183
- * background job silently unfinished — the caller must not treat it as done.
184
- */
185
- function injectCompletion(proc: BgProc): boolean {
199
+ /** Render the shared header+output body used by every injected message. */
200
+ function buildOutputMessage(proc: BgProc, header: string): string {
186
201
  const parts: string[] = [];
187
202
  const stdout = describeStream("stdout", proc.stdout.snapshot());
188
203
  const stderr = describeStream("stderr", proc.stderr.snapshot());
189
204
  if (stdout) parts.push(stdout);
190
205
  if (stderr) parts.push(stderr);
191
206
  const output = parts.join("\n") || "(no output)";
192
-
193
207
  // English on purpose: this text is injected into the model's context, and
194
208
  // every other model-facing string in this server — the tool descriptions it
195
209
  // sits alongside — is English. It is also cheaper in tokens.
196
- const message =
197
- `[background_bash ${proc.bashId} finished]\n` +
210
+ return (
211
+ `${header}\n` +
198
212
  `command: ${proc.command.slice(0, 200)}\n` +
199
213
  `exit code: ${proc.exitCode ?? "unknown"}\n` +
200
- output;
214
+ output
215
+ );
216
+ }
201
217
 
218
+ /**
219
+ * Deliver one turn to the topic's inbox.
220
+ *
221
+ * Returns false when the inbox write failed. Every caller promises the
222
+ * caller it need not poll, so a dropped delivery is the one failure that
223
+ * leaves a background job silently unfinished — treat it as not-done.
224
+ */
225
+ function injectMessage(proc: BgProc, message: string, label: string): boolean {
202
226
  // `proc.topic` is the canonical topic id (see mcp-config background-bash
203
227
  // build: `topicId ?? session`). Route through the shared helper so the
204
228
  // filename is the `topic-id-{base64url}` form the session-inbox worker
@@ -212,11 +236,11 @@ function injectCompletion(proc: BgProc): boolean {
212
236
  depth: 0,
213
237
  timestamp: new Date().toISOString(),
214
238
  });
215
- process.stderr.write(`[bg-bash] injected completion ${proc.bashId} exit=${proc.exitCode}\n`);
239
+ process.stderr.write(`[bg-bash] injected ${label} ${proc.bashId} exit=${proc.exitCode}\n`);
216
240
  return true;
217
241
  } catch (e) {
218
242
  process.stderr.write(
219
- `[bg-bash] FAILED to deliver completion ${proc.bashId} (exit=${proc.exitCode}): ${e}\n` +
243
+ `[bg-bash] FAILED to deliver ${label} ${proc.bashId} (exit=${proc.exitCode}): ${e}\n` +
220
244
  `[bg-bash] the topic will never be told this job finished; ` +
221
245
  `its output is kept at ${spillDir(proc.bashId)}\n`,
222
246
  );
@@ -224,6 +248,61 @@ function injectCompletion(proc: BgProc): boolean {
224
248
  }
225
249
  }
226
250
 
251
+ function injectCompletion(proc: BgProc): boolean {
252
+ return injectMessage(
253
+ proc,
254
+ buildOutputMessage(proc, `[background_bash ${proc.bashId} finished]`),
255
+ "completion",
256
+ );
257
+ }
258
+
259
+ /**
260
+ * Test newly arrived bytes for `watch.regex`, line by line, carrying any
261
+ * trailing partial line to the next chunk so a match split across two reads
262
+ * is never missed. Returns the first matching line, if any.
263
+ */
264
+ function checkWatchMatch(
265
+ proc: BgProc,
266
+ stream: "stdout" | "stderr",
267
+ chunk: Buffer,
268
+ ): string | undefined {
269
+ const watch = proc.watch;
270
+ if (!watch || watch.matched) return undefined;
271
+ if (watch.target !== "both" && watch.target !== stream) return undefined;
272
+ const carryKey = stream === "stdout" ? "stdoutCarry" : "stderrCarry";
273
+ const lines = (watch[carryKey] + chunk.toString("utf8")).split("\n");
274
+ watch[carryKey] = lines.pop() ?? "";
275
+ for (const line of lines) {
276
+ if (watch.regex.test(line)) return line;
277
+ }
278
+ return undefined;
279
+ }
280
+
281
+ /** A watch line matched: deliver the one promised turn and stop the process. */
282
+ function handleWatchMatch(proc: BgProc, line: string): void {
283
+ const watch = proc.watch;
284
+ if (!watch || watch.matched) return;
285
+ watch.matched = true;
286
+ watch.finished = true;
287
+ if (watch.timeoutTimer) {
288
+ clearTimeout(watch.timeoutTimer);
289
+ watch.timeoutTimer = undefined;
290
+ }
291
+ const header = `[background_bash_watch ${proc.bashId} matched]\nmatched line: ${line.slice(0, 500)}`;
292
+ proc.delivered = injectMessage(proc, buildOutputMessage(proc, header), "watch match");
293
+ terminateProc(proc);
294
+ }
295
+
296
+ /** No line matched within the deadline: deliver a timeout turn and stop. */
297
+ function handleWatchTimeout(proc: BgProc): void {
298
+ const watch = proc.watch;
299
+ if (!watch || watch.finished) return;
300
+ watch.finished = true;
301
+ const header = `[background_bash_watch ${proc.bashId} timed out without a match]`;
302
+ proc.delivered = injectMessage(proc, buildOutputMessage(proc, header), "watch timeout");
303
+ terminateProc(proc);
304
+ }
305
+
227
306
  function signalProcessTree(child: ChildProcess, signal: NodeJS.Signals): boolean {
228
307
  if (!child.pid) return child.kill(signal);
229
308
  try {
@@ -288,7 +367,23 @@ function finishProc(proc: BgProc, exitCode: number | null): void {
288
367
  // paths, so anything reading them sees a closed, complete file.
289
368
  proc.stdout.close();
290
369
  proc.stderr.close();
291
- proc.delivered = injectCompletion(proc);
370
+ if (proc.watch) {
371
+ if (proc.watch.timeoutTimer) {
372
+ clearTimeout(proc.watch.timeoutTimer);
373
+ proc.watch.timeoutTimer = undefined;
374
+ }
375
+ if (!proc.watch.finished) {
376
+ // The command exited on its own — no match, no timeout yet. This is
377
+ // still exactly one delivered turn, same one-shot guarantee.
378
+ proc.watch.finished = true;
379
+ const header = `[background_bash_watch ${proc.bashId} exited before matching]`;
380
+ proc.delivered = injectMessage(proc, buildOutputMessage(proc, header), "watch exit");
381
+ }
382
+ // Else: a match or timeout already delivered the one promised turn
383
+ // (`proc.delivered` was set there) — do not inject a second one.
384
+ } else {
385
+ proc.delivered = injectCompletion(proc);
386
+ }
292
387
  if (proc.delivered) {
293
388
  proc.cleanupTimer = setTimeout(() => forgetProc(proc), COMPLETED_RETENTION_MS);
294
389
  proc.cleanupTimer.unref?.();
@@ -299,10 +394,17 @@ function finishProc(proc: BgProc, exitCode: number | null): void {
299
394
  pruneCompletedProcs();
300
395
  }
301
396
 
397
+ interface WatchConfig {
398
+ regex: RegExp;
399
+ target: "stdout" | "stderr" | "both";
400
+ timeoutSeconds: number;
401
+ }
402
+
302
403
  function spawnBash(
303
404
  context: BgContext,
304
405
  command: string,
305
406
  cwd?: string,
407
+ watchConfig?: WatchConfig,
306
408
  ): { bashId: string } | { error: string } {
307
409
  if (!command.trim()) return { error: "empty command" };
308
410
  const bashId = newBashId();
@@ -337,11 +439,29 @@ function spawnBash(
337
439
  exitCode: null,
338
440
  delivered: false,
339
441
  startedAt: Date.now(),
442
+ watch: watchConfig
443
+ ? {
444
+ regex: watchConfig.regex,
445
+ target: watchConfig.target,
446
+ matched: false,
447
+ finished: false,
448
+ stdoutCarry: "",
449
+ stderrCarry: "",
450
+ }
451
+ : undefined,
340
452
  };
341
453
  // Raw Buffers: decoding per chunk would split multi-byte characters that
342
454
  // straddle a chunk boundary. The buffer decodes at read time instead.
343
- child.stdout?.on("data", (c: Buffer) => proc.stdout.append(c));
344
- child.stderr?.on("data", (c: Buffer) => proc.stderr.append(c));
455
+ child.stdout?.on("data", (c: Buffer) => {
456
+ proc.stdout.append(c);
457
+ const line = checkWatchMatch(proc, "stdout", c);
458
+ if (line !== undefined) handleWatchMatch(proc, line);
459
+ });
460
+ child.stderr?.on("data", (c: Buffer) => {
461
+ proc.stderr.append(c);
462
+ const line = checkWatchMatch(proc, "stderr", c);
463
+ if (line !== undefined) handleWatchMatch(proc, line);
464
+ });
345
465
  child.on("close", (code) => {
346
466
  finishProc(proc, code);
347
467
  });
@@ -350,6 +470,13 @@ function spawnBash(
350
470
  finishProc(proc, -1);
351
471
  });
352
472
  procs.set(bashId, proc);
473
+ if (proc.watch) {
474
+ proc.watch.timeoutTimer = setTimeout(
475
+ () => handleWatchTimeout(proc),
476
+ watchConfig!.timeoutSeconds * 1_000,
477
+ );
478
+ proc.watch.timeoutTimer.unref?.();
479
+ }
353
480
  process.stderr.write(`[bg-bash] started ${bashId}: ${command.slice(0, 80)}\n`);
354
481
  return { bashId };
355
482
  }
@@ -421,6 +548,56 @@ function buildMcpServer(context: BgContext): McpServer {
421
548
  },
422
549
  );
423
550
 
551
+ server.tool(
552
+ "background_bash_watch",
553
+ [
554
+ "Start a background shell command and watch its stdout/stderr for a regex match, one line at a time.",
555
+ "The moment a line matches `match`, the process is stopped and the matching line plus buffered",
556
+ "output is injected into this session as a new turn — you do NOT need to poll.",
557
+ "If nothing matches within `timeout_seconds` (default 3600), or the command exits on its own first,",
558
+ "a final status turn is injected instead. Exactly one turn is ever injected per watch — this is",
559
+ "one-shot only, there is no repeat/streaming mode yet.",
560
+ "Prefer this over `background_bash_run` + manual `background_bash_output` polling when you are",
561
+ "waiting for a specific condition to appear (a deploy readiness line, an error) rather than for",
562
+ "the command itself to finish.",
563
+ ].join(" "),
564
+ {
565
+ command: z.string().describe("Shell command (executed via bash -c)"),
566
+ match: z.string().describe("Regular expression tested against each output line"),
567
+ cwd: z.string().optional().describe("Working directory (absolute path)"),
568
+ stream: z
569
+ .enum(["stdout", "stderr", "both"])
570
+ .optional()
571
+ .describe("Which stream(s) to test against `match` (default both)"),
572
+ timeout_seconds: z
573
+ .number()
574
+ .int()
575
+ .positive()
576
+ .max(MAX_WATCH_TIMEOUT_SECONDS)
577
+ .optional()
578
+ .describe(
579
+ `Give up waiting for a match after this many seconds (default ${DEFAULT_WATCH_TIMEOUT_SECONDS}, max ${MAX_WATCH_TIMEOUT_SECONDS})`,
580
+ ),
581
+ },
582
+ async ({ command, match, cwd, stream, timeout_seconds }) => {
583
+ let regex: RegExp;
584
+ try {
585
+ regex = new RegExp(match);
586
+ } catch (e) {
587
+ return mcpError(
588
+ `invalid regex in \`match\`: ${e instanceof Error ? e.message : String(e)}`,
589
+ );
590
+ }
591
+ const result = spawnBash(context, command, cwd, {
592
+ regex,
593
+ target: stream ?? "both",
594
+ timeoutSeconds: timeout_seconds ?? DEFAULT_WATCH_TIMEOUT_SECONDS,
595
+ });
596
+ if ("error" in result) return mcpError(result.error);
597
+ return mcpOk(JSON.stringify({ bash_id: result.bashId, status: "watching" }));
598
+ },
599
+ );
600
+
424
601
  server.tool(
425
602
  "background_bash_kill",
426
603
  "Terminate a background process (SIGTERM → SIGKILL after 5s). Idempotent.",
@@ -99,7 +99,7 @@ export function resolveOutputLanguage(): string {
99
99
  }
100
100
 
101
101
  /** Browser.rs release tested with this Negotium version. */
102
- export const BROWSER_RS_VERSION = "v0.1.16";
102
+ export const BROWSER_RS_VERSION = "v0.1.17";
103
103
  /** Require the authenticated listener and the current Browser.rs tool contract. */
104
104
  export const BROWSER_RS_MIN_SECURE_VERSION = "0.1.15";
105
105
 
@@ -14,6 +14,13 @@ const BOUNDED_OUTPUTS: Record<string, { argument: string; field: string }> = {
14
14
  browser_api_request: { argument: "maxBytes", field: "body" },
15
15
  browser_get_visible_text: { argument: "maxLength", field: "text" },
16
16
  browser_get_visible_html: { argument: "maxLength", field: "html" },
17
+ // browser-rs 0.1.17's own `maxLength` doc comment: "Managed hosts may raise
18
+ // this so secrets are redacted before the caller-visible limit is applied."
19
+ // `field` is unused here (the tool returns a plain string, not `{content}`
20
+ // JSON — see `redactTextEntry`'s non-JSON fallback), but the `argument`
21
+ // override is what matters: it raises the request to MAX_SAFE_INTEGER so
22
+ // browser-rs never truncates a secret mid-string before redaction sees it.
23
+ browser_iframe_read: { argument: "maxLength", field: "content" },
17
24
  };
18
25
 
19
26
  interface RedactionBoundary {
@@ -1423,9 +1423,29 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
1423
1423
  ].join("\n");
1424
1424
  }
1425
1425
 
1426
+ /**
1427
+ * Per-turn `<system-reminder>` blocks, appended to the *user* prompt rather
1428
+ * than the system prompt.
1429
+ *
1430
+ * These facts flip between turns on the same model — whether the browser MCP
1431
+ * came up, whether this turn is an auto-resume. A prompt cache invalidates
1432
+ * from the first diverging byte, so putting them in the system prompt means
1433
+ * every flip re-reads the entire prompt: measured against the live DeepSeek
1434
+ * API, one flip at turn 15 of a 20-turn conversation cost 17,852 uncached
1435
+ * tokens (hit=0) versus 1,215 with the reminder on the user turn — 40% more
1436
+ * uncached tokens across the run. On the user turn the divergence lands at
1437
+ * the tail, where the text is new anyway.
1438
+ *
1439
+ * Applied after the conversation log is written, so a reminder never becomes
1440
+ * part of the recorded user message (it would then persist into synthesized
1441
+ * rollouts and defeat the point).
1442
+ */
1443
+ const turnReminders: string[] = [];
1444
+
1426
1445
  if (params.from === FROM_AUTO_CONTINUE) {
1427
- systemPrompt +=
1428
- "\n\n<system-reminder>이 턴은 설정 자동 조정(effort/model/agent) 후 자동 재개된 턴이다. effort/model/agent 난이도 재평가 및 설정 변경 없이 즉시 작업을 시작할 것.</system-reminder>";
1446
+ turnReminders.push(
1447
+ "<system-reminder>이 턴은 설정 자동 조정(effort/model/agent) 후 자동 재개된 턴이다. effort/model/agent 난이도 재평가 및 설정 변경 없이 즉시 작업을 시작할 것.</system-reminder>",
1448
+ );
1429
1449
  }
1430
1450
 
1431
1451
  if (!silent && !sessionRetried) {
@@ -1538,25 +1558,28 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
1538
1558
  );
1539
1559
  }
1540
1560
  }
1541
- let effectiveSystemPrompt = systemPrompt;
1542
1561
  if (playwrightRequested && !browserProfileOwner) {
1543
- const ownerOnlyNote =
1544
- "<system-reminder>Browser tools are unavailable in this turn because browser profiles are private to the topic owner. Do not attempt browser calls or ask to reuse the owner's login state.</system-reminder>";
1545
- effectiveSystemPrompt = `${effectiveSystemPrompt}\n\n${ownerOnlyNote}`;
1562
+ turnReminders.push(
1563
+ "<system-reminder>Browser tools are unavailable in this turn because browser profiles are private to the topic owner. Do not attempt browser calls or ask to reuse the owner's login state.</system-reminder>",
1564
+ );
1546
1565
  }
1547
1566
  if (consumePlaywrightUnavailable(userId, topic.title)) {
1548
- const playwrightNote =
1549
- "<system-reminder>Playwright browser tools are UNAVAILABLE this turn. The `mcp__playwright__*` tools have been removed from this turn's catalog because the long-lived browser MCP could not be prepared. Do not attempt to call browser tools. If browser interaction is required, ask the user to retry shortly or use a non-browser alternative.</system-reminder>";
1550
- effectiveSystemPrompt = `${effectiveSystemPrompt}\n\n${playwrightNote}`;
1567
+ turnReminders.push(
1568
+ "<system-reminder>Playwright browser tools are UNAVAILABLE this turn. The `mcp__playwright__*` tools have been removed from this turn's catalog because the long-lived browser MCP could not be prepared. Do not attempt to call browser tools. If browser interaction is required, ask the user to retry shortly or use a non-browser alternative.</system-reminder>",
1569
+ );
1551
1570
  }
1571
+ // The system prompt is now byte-identical across turns of a topic, so the
1572
+ // provider's prefix cache survives; per-turn facts ride the prompt tail.
1573
+ const effectivePrompt =
1574
+ turnReminders.length > 0 ? `${agentPrompt}\n\n${turnReminders.join("\n\n")}` : agentPrompt;
1552
1575
 
1553
1576
  try {
1554
1577
  yield* runAgent({
1555
1578
  agent: agentKind,
1556
- prompt: agentPrompt,
1579
+ prompt: effectivePrompt,
1557
1580
  attachments: promptAttachments,
1558
1581
  cwd: workspaceCwd,
1559
- systemPrompt: effectiveSystemPrompt,
1582
+ systemPrompt,
1560
1583
  sessionId,
1561
1584
  userId,
1562
1585
  vaultUserId,
@@ -1 +1 @@
1
- export const NEGOTIUM_VERSION = "0.2.7";
1
+ export const NEGOTIUM_VERSION = "0.2.9";
@@ -451,7 +451,7 @@ var DM_WORKSPACE_DIR = resolve(STATE_DIR, "data", "dm");
451
451
  var SESSION_WORKSPACE_DIR = resolve(STATE_DIR, "data", "sessions");
452
452
  var CLAUDE_EXECUTABLE_ENV = envText("NEGOTIUM_CLAUDE_EXECUTABLE");
453
453
  var CLAUDE_EXECUTABLE = CLAUDE_EXECUTABLE_ENV ? resolve(CLAUDE_EXECUTABLE_ENV) : undefined;
454
- var BROWSER_RS_VERSION = "v0.1.16";
454
+ var BROWSER_RS_VERSION = "v0.1.17";
455
455
  var BROWSER_RS_MIN_SECURE_VERSION = "0.1.15";
456
456
  function versionAtLeast(actualVersion, minimumVersion) {
457
457
  const actual = actualVersion.split(".").map(Number);
@@ -1132,4 +1132,4 @@ export {
1132
1132
  CLAUDE_EFFORT_VALUES
1133
1133
  };
1134
1134
 
1135
- //# debugId=38CF47A0C3C063A564756E2164756E21
1135
+ //# debugId=078B7BFBF513B96264756E2164756E21