negotium 0.1.19 → 0.1.21

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 (46) hide show
  1. package/dist/agent-helpers.js +146 -23
  2. package/dist/agent-helpers.js.map +14 -12
  3. package/dist/cron.js +137 -52
  4. package/dist/cron.js.map +20 -19
  5. package/dist/hosted-agent.js +29 -17
  6. package/dist/hosted-agent.js.map +7 -7
  7. package/dist/main.js +145 -55
  8. package/dist/main.js.map +20 -19
  9. package/dist/mcp-factories.js +23 -22
  10. package/dist/mcp-factories.js.map +3 -3
  11. package/dist/prompts.js +98 -9
  12. package/dist/prompts.js.map +6 -5
  13. package/dist/runtime/scripts/browser-passkey-policy.mjs +36 -0
  14. package/dist/runtime/scripts/browser-vault-transform.mjs +33 -0
  15. package/dist/runtime/scripts/mcp-patchright-http.mjs +22 -4
  16. package/dist/runtime/src/agents/archiver.ts +0 -14
  17. package/dist/runtime/src/agents/claude-provider.ts +11 -7
  18. package/dist/runtime/src/agents/execution-host.ts +10 -1
  19. package/dist/runtime/src/agents/idle-archiver.ts +21 -0
  20. package/dist/runtime/src/agents/maestro-provider.ts +9 -14
  21. package/dist/runtime/src/agents/mcp-tools/self-config.ts +1 -1
  22. package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +6 -1
  23. package/dist/runtime/src/agents/memory-archive-policy.ts +34 -0
  24. package/dist/runtime/src/agents/model-catalog.ts +84 -18
  25. package/dist/runtime/src/mcp/factories/vault.ts +36 -34
  26. package/dist/runtime/src/mcp/vault-server.ts +1 -0
  27. package/dist/runtime/src/platform/mcp-config.ts +4 -8
  28. package/dist/runtime/src/platform/playwright/manager.ts +5 -2
  29. package/dist/runtime/src/prompts/builders.ts +36 -7
  30. package/dist/runtime/src/runtime/turn-runner.ts +2 -0
  31. package/dist/runtime/src/storage/topic-archive.ts +5 -2
  32. package/dist/runtime/src/topics/lifecycle.ts +2 -1
  33. package/dist/runtime/src/topics/session.ts +2 -0
  34. package/dist/runtime/src/version.ts +1 -1
  35. package/dist/storage.js +23 -3
  36. package/dist/storage.js.map +5 -4
  37. package/dist/types/packages/core/src/agents/claude-provider.d.ts +1 -0
  38. package/dist/types/packages/core/src/agents/execution-host.d.ts +2 -0
  39. package/dist/types/packages/core/src/agents/idle-archiver.d.ts +2 -0
  40. package/dist/types/packages/core/src/agents/memory-archive-policy.d.ts +14 -0
  41. package/dist/types/packages/core/src/agents/model-catalog.d.ts +77 -0
  42. package/dist/types/packages/core/src/mcp/factories/vault.d.ts +1 -0
  43. package/dist/types/packages/core/src/prompts/builders.d.ts +5 -1
  44. package/dist/types/packages/core/src/storage/topic-archive.d.ts +1 -0
  45. package/dist/types/packages/core/src/version.d.ts +1 -1
  46. package/package.json +2 -2
@@ -63,13 +63,13 @@
63
63
  import "#platform/maestro-bootstrap-env";
64
64
  import type { HookRegistration, McpResolver } from "maestro-agent-sdk";
65
65
  import { maestroProvider as sdkMaestroProvider, setMcpResolver } from "maestro-agent-sdk";
66
+ import { deepMapStrings } from "#agents/deep-map";
66
67
  import {
67
68
  hostedMcpServers,
68
69
  redactHostedSecrets,
69
70
  referencesHostedSecretStorage,
70
- shouldRedirectHostedVaultTool,
71
+ substituteHostedSecrets,
71
72
  } from "#agents/execution-host";
72
- import { VAULT_BROKER_REDIRECT_ERROR } from "#agents/vault-tool-policy";
73
73
  import type { AgentQueryOptions, UnifiedEvent } from "#types";
74
74
 
75
75
  /**
@@ -110,28 +110,23 @@ export function buildMaestroDisallowedTools(
110
110
  *
111
111
  * Pre hook — two responsibilities in one pass:
112
112
  * 1. Block direct vault.db filesystem access (security guard).
113
- * 2. Block direct placeholder substitution and redirect the model to the
114
- * Vault MCP credential broker, where expanded input stays outside the
115
- * provider process.
113
+ * 2. Substitute {{KEY}} placeholders immediately before tool execution.
116
114
  *
117
115
  * Post hook — scrub raw/common encoded secret forms before tool output is sent
118
- * back to the model. HTTP credentials should still prefer vault_http_request,
119
- * whose broker never exposes the expanded request to the provider at all.
116
+ * back to the model.
120
117
  */
121
118
  function buildVaultHook(userId: string): HookRegistration {
122
119
  return {
123
120
  name: "vault-guard",
124
- pre({ toolName, input }) {
121
+ pre({ input }) {
125
122
  if (referencesHostedSecretStorage(input)) {
126
123
  return { decision: "block", error: "Runtime secret storage access is not permitted" };
127
124
  }
128
125
 
129
- // Detect key references from metadata only. No Vault value is decrypted
130
- // or injected into the provider/tool registry on this path.
131
- if (shouldRedirectHostedVaultTool(userId, toolName, input)) {
132
- return { decision: "block", error: VAULT_BROKER_REDIRECT_ERROR };
133
- }
134
- return { decision: "allow" };
126
+ const substituted = deepMapStrings(input, (value) => substituteHostedSecrets(userId, value));
127
+ return JSON.stringify(substituted) === JSON.stringify(input)
128
+ ? { decision: "allow" }
129
+ : { decision: "modify", input: substituted as Record<string, unknown> };
135
130
  },
136
131
  post({ output }) {
137
132
  const redacted = redactHostedSecrets(userId, output);
@@ -48,7 +48,7 @@ export function createSelfConfigToolDefinitions(
48
48
  {
49
49
  name: "set_model",
50
50
  description:
51
- "Set the model for THIS topic. Persists and applies from the NEXT turn. Claude: 'sonnet' / 'opus' / 'fable'. Codex: 'gpt-5.6-luna' (default) / 'gpt-5.6-terra' / 'gpt-5.6-sol'. Maestro: 'deepseek-pro' / 'deepseek-flash' / 'deepseek'. The model must match the topic's current agent's accepted list. Switch agent first via set_agent only when crossing the claude/codex/maestro runtime boundary. Fails if the user locked this setting. NEVER use 'fable' unless the user explicitly requests it.",
51
+ "Set the model for THIS topic; it persists and applies from the NEXT turn. Use a model from the system prompt's same-agent catalog. Cross-agent changes require an explicit user-requested set_agent first. Fails if the user locked this setting. NEVER use 'fable' unless explicitly requested.",
52
52
  schema: { model: z.string().describe("Model id valid for the topic's current agent.") },
53
53
  async handler({ model }: { model: string }) {
54
54
  return mcpResult(setSelfConfigModel(getCtx(), model));
@@ -590,7 +590,12 @@ export function createSpawnSubagentToolDefinition(ctx: SpawnSubagentToolContext)
590
590
  .enum(["claude", "codex", "maestro"])
591
591
  .optional()
592
592
  .describe("Agent backend override. Defaults to this room's agent."),
593
- model: z.string().optional().describe("Model override. Defaults to the agent's default."),
593
+ model: z
594
+ .string()
595
+ .optional()
596
+ .describe(
597
+ `Best-fit model override from the system prompt catalog. Omit agent+model to inherit ${ctx.agent}/${ctx.model ?? "default"}; overriding agent without model uses that agent's default.`,
598
+ ),
594
599
  },
595
600
  async handler(input) {
596
601
  return spawnSubagent(ctx, input);
@@ -0,0 +1,34 @@
1
+ export interface MemoryArchiveMessage {
2
+ author_id: string;
3
+ agent_type?: string | null;
4
+ kind?: string | null;
5
+ }
6
+
7
+ /**
8
+ * Shared lifecycle gate for reset/delete memory distillation. Keep the raw
9
+ * forensic archive even below this threshold, but do not spend an agent turn
10
+ * on a session with five or fewer completed user/assistant exchanges. This
11
+ * mirrors Clawgram's lifecycle-level MIN_EXCHANGE_COUNT policy.
12
+ */
13
+ export const MIN_MEMORY_ARCHIVE_EXCHANGES = 6;
14
+
15
+ /** Count completed conversational exchanges while ignoring system/tool/card noise. */
16
+ export function countMemoryArchiveExchanges(rows: readonly MemoryArchiveMessage[]): number {
17
+ let waitingForAssistant = false;
18
+ let completed = 0;
19
+
20
+ for (const row of rows) {
21
+ const assistant = row.author_id === "ai" || Boolean(row.agent_type);
22
+ const conversational = row.kind == null || row.kind === "message";
23
+ if (!assistant && row.author_id !== "system" && conversational) {
24
+ waitingForAssistant = true;
25
+ continue;
26
+ }
27
+ if (assistant && conversational && waitingForAssistant) {
28
+ completed++;
29
+ waitingForAssistant = false;
30
+ }
31
+ }
32
+
33
+ return completed;
34
+ }
@@ -37,8 +37,40 @@ export interface SelectableModel {
37
37
  agent: AgentKind;
38
38
  /** Short comparison copy shown alongside the model in picker UIs. */
39
39
  description: string;
40
+ /** User-defined relative intelligence band used for routing across providers. */
41
+ intelligenceTier: "sonnet" | "opus" | "fable";
42
+ /** Compact capability/cost hint safe to inject into every turn and tool schema. */
43
+ routingSummary: string;
44
+ /** Subscription or API basis used when comparing operating cost. */
45
+ accessCost: string;
46
+ /** Marginal per-token rate after included subscription usage, or the API rate. */
47
+ marginalTokenCost: string;
48
+ /** Best available quota estimate; never presented as a provider-guaranteed token cap. */
49
+ estimatedUsage: string;
40
50
  }
41
51
 
52
+ /**
53
+ * Pricing and quota observations were checked on 2026-07-19.
54
+ * Official references:
55
+ * - https://learn.chatgpt.com/docs/pricing
56
+ * - https://help.openai.com/en/articles/20001106
57
+ * - https://support.claude.com/en/articles/11049741-what-is-the-max-plan
58
+ * - https://api-docs.deepseek.com/quick_start/pricing
59
+ * Community token counts are deliberately labelled estimates because providers
60
+ * meter cached input, fresh input, output, reasoning, speed, and model choice
61
+ * differently and may change server-side weights without publishing a token cap.
62
+ */
63
+ export const MODEL_COST_RESEARCHED_AT = "2026-07-19";
64
+ export const MODEL_COST_ROUTING_SUMMARY =
65
+ "Cost basis (2026-07-19): Codex Pro 20x and Claude Max 20x are each $200/month; DeepSeek Pro is pay-per-token. Relative marginal token cost: DeepSeek Pro << Codex < Claude.";
66
+
67
+ const CODEX_PRO_20X_COST = "ChatGPT Pro 20x subscription: $200/month";
68
+ const CODEX_COMMUNITY_WEEKLY =
69
+ "Community plan-level observation: roughly 2–4B raw/cached tokens per week; fresh-input equivalent is much lower and unstable (low confidence)";
70
+ const CLAUDE_MAX_20X_COST = "Claude Max 20x subscription: $200/month";
71
+ const CLAUDE_COMMUNITY_SESSION =
72
+ "Community observations vary from roughly 220–250K locally displayed tokens per 5-hour session to billions of cache-heavy raw tokens per week; calibrated reports value a full weekly allowance around $680–$1,900 at API rates. Recent heavy-model reports reach the weekly cap after about 4–5 full sessions (low confidence; not a token cap)";
73
+
42
74
  /**
43
75
  * Canonical model picker shared by every channel. Keep this deliberately
44
76
  * finite even though the Codex backend accepts arbitrary future model ids:
@@ -48,51 +80,85 @@ export const SELECTABLE_MODELS: readonly SelectableModel[] = [
48
80
  {
49
81
  model: "gpt-5.6-sol",
50
82
  agent: "codex",
51
- description: "Latest frontier agentic coding model.",
83
+ description: "Highest-capability Codex route for the hardest agentic coding work.",
84
+ intelligenceTier: "fable",
85
+ routingSummary: "hardest coding work; 5x Codex quota cost",
86
+ accessCost: CODEX_PRO_20X_COST,
87
+ marginalTokenCost: "Codex credits: $5/M uncached input, $0.50/M cached input, $30/M output",
88
+ estimatedUsage: `Official Pro 20x range: 300–1,800 local messages per 5 hours; quota weight 5x Luna. ${CODEX_COMMUNITY_WEEKLY}`,
52
89
  },
53
90
  {
54
91
  model: "gpt-5.6-terra",
55
92
  agent: "codex",
56
- description: "Balanced agentic coding model for everyday work.",
93
+ description: "High-capability Codex route for complex coding and reasoning.",
94
+ intelligenceTier: "opus",
95
+ routingSummary: "complex coding and reasoning; 2.5x Codex quota cost",
96
+ accessCost: CODEX_PRO_20X_COST,
97
+ marginalTokenCost: "Codex credits: $2.50/M uncached input, $0.25/M cached input, $15/M output",
98
+ estimatedUsage: `Official Pro 20x range: 400–2,200 local messages per 5 hours; quota weight 2.5x Luna. ${CODEX_COMMUNITY_WEEKLY}`,
57
99
  },
58
100
  {
59
101
  model: "gpt-5.6-luna",
60
102
  agent: "codex",
61
- description: "Fast and affordable agentic coding model.",
62
- },
63
- {
64
- model: "gpt-5.5",
65
- agent: "codex",
66
- description: "Frontier model for complex coding, research, and real-world work.",
103
+ description: "Default Codex route with strong everyday coding intelligence.",
104
+ intelligenceTier: "sonnet",
105
+ routingSummary: "everyday coding default; lowest Codex quota cost (1x)",
106
+ accessCost: CODEX_PRO_20X_COST,
107
+ marginalTokenCost: "Codex credits: $1/M uncached input, $0.10/M cached input, $6/M output",
108
+ estimatedUsage: `Official Pro 20x range: 1,000–5,600 local messages per 5 hours; lowest Codex quota weight (1x). ${CODEX_COMMUNITY_WEEKLY}`,
67
109
  },
68
110
  {
69
111
  model: "fable",
70
112
  agent: "claude",
71
- description:
72
- "Fable 5 · Most capable for your hardest and longest-running tasks · $10/$50 per Mtok",
113
+ description: "Highest-capability Claude route for the hardest and longest-running tasks.",
114
+ intelligenceTier: "fable",
115
+ routingSummary: "hardest long-running work; highest Claude cost; explicit request only",
116
+ accessCost: CLAUDE_MAX_20X_COST,
117
+ marginalTokenCost:
118
+ "Claude API/extra usage: $10/M input, $12.50/M cache write, $1/M cache read, $50/M output",
119
+ estimatedUsage: `${CLAUDE_COMMUNITY_SESSION}; Fable drains weighted quota fastest, so use only on explicit user request. No stable per-model token cap is published.`,
73
120
  },
74
121
  {
75
122
  model: "opus",
76
123
  agent: "claude",
77
- description: "Opus 4.8 with 1M context · Best for everyday, complex tasks · $5/$25 per Mtok",
124
+ description: "High-capability Claude route for complex reasoning and tool-heavy work.",
125
+ intelligenceTier: "opus",
126
+ routingSummary: "complex reasoning and tool-heavy work; about 2.5x Sonnet marginal cost",
127
+ accessCost: CLAUDE_MAX_20X_COST,
128
+ marginalTokenCost:
129
+ "Claude API/extra usage: $5/M input, $6.25/M cache write, $0.50/M cache read, $25/M output",
130
+ estimatedUsage: `${CLAUDE_COMMUNITY_SESSION}; Opus uses the shared all-model weekly pool more quickly than Sonnet. No stable per-model token cap is published.`,
78
131
  },
79
132
  {
80
133
  model: "sonnet",
81
134
  agent: "claude",
82
- description: "Sonnet 5 · Efficient for routine tasks · $2/$10 per Mtok · promo through Aug 31",
135
+ description: "Default Claude route for capable, efficient everyday work.",
136
+ intelligenceTier: "sonnet",
137
+ routingSummary: "capable everyday default; lowest Claude model cost",
138
+ accessCost: CLAUDE_MAX_20X_COST,
139
+ marginalTokenCost:
140
+ "Claude API/extra usage introductory rate: $2/M input, $2.50/M cache write, $0.20/M cache read, $10/M output through 2026-08-31; then $3/M input and $15/M output",
141
+ estimatedUsage: `${CLAUDE_COMMUNITY_SESSION}; Sonnet also has a separate weekly allowance and normally provides the highest Claude throughput. No stable weekly token cap is published.`,
83
142
  },
84
143
  {
85
144
  model: "deepseek-pro",
86
145
  agent: "maestro",
87
- description: "Sonnet-level performance at lower cost · Best for everyday tasks over coding",
88
- },
89
- {
90
- model: "deepseek-flash",
91
- agent: "maestro",
92
- description: "DeepSeek V4 Flash · Fast and low-cost for lightweight everyday tasks",
146
+ description: "API-priced Sonnet-level route for cost-efficient everyday work.",
147
+ intelligenceTier: "sonnet",
148
+ routingSummary: "cost-efficient everyday work; pay-per-token and cheapest route",
149
+ accessCost: "DeepSeek V4 Pro pay-as-you-go API; no monthly subscription required",
150
+ marginalTokenCost:
151
+ "DeepSeek API: $0.435/M uncached input, $0.003625/M cached input, $0.87/M output",
152
+ estimatedUsage:
153
+ "No subscription token cap; pay per token. Official account concurrency limit is 500 requests.",
93
154
  },
94
155
  ];
95
156
 
157
+ export function formatSelectableModel(candidate: SelectableModel): string {
158
+ const tier = `${candidate.intelligenceTier[0].toUpperCase()}${candidate.intelligenceTier.slice(1)}`;
159
+ return `${candidate.agent} / \`${candidate.model}\` [${tier}-level]: ${candidate.routingSummary}`;
160
+ }
161
+
96
162
  export function selectableModel(value: string): SelectableModel | undefined {
97
163
  const normalized = value.trim().toLowerCase();
98
164
  return SELECTABLE_MODELS.find((candidate) => candidate.model === normalized);
@@ -7,6 +7,7 @@ import type { VaultCredentialHost } from "./vault-host";
7
7
 
8
8
  export interface VaultMcpContext {
9
9
  userId?: string;
10
+ listOnly?: boolean;
10
11
  httpOnly?: boolean;
11
12
  cwd?: string;
12
13
  }
@@ -50,7 +51,7 @@ export function createVaultMcpServer(
50
51
  },
51
52
  );
52
53
 
53
- if (!context.httpOnly) {
54
+ if (!context.listOnly && !context.httpOnly) {
54
55
  server.tool(
55
56
  "vault_run",
56
57
  "Run a shell command containing {{KEY}} references inside Otium's credential broker. Expanded command input never reaches the model/provider, and stdout/stderr are redacted before return. Prefer vault_http_request for HTTP APIs.",
@@ -86,39 +87,40 @@ export function createVaultMcpServer(
86
87
  );
87
88
  }
88
89
 
89
- server.tool(
90
- "vault_http_request",
91
- "Make an HTTPS request with {{KEY}} references resolved inside Otium. Put secrets in headers or body, never in the URL. The expanded request is not returned; the response is redacted before the model sees it.",
92
- {
93
- method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET"),
94
- url: z.string().url().describe("Absolute HTTPS URL without Vault placeholders"),
95
- headers: z.record(z.string(), z.string()).optional(),
96
- body: z.string().optional(),
97
- timeout_ms: z.number().int().min(1_000).max(120_000).optional(),
98
- max_response_bytes: z
99
- .number()
100
- .int()
101
- .min(1_024)
102
- .max(1024 * 1024)
103
- .optional(),
104
- },
105
- async ({ method, url, headers, body, timeout_ms, max_response_bytes }) => {
106
- if (!context.userId) return mcpError("Vault unavailable: no user context");
107
- const result = await http(
108
- context.userId,
109
- {
110
- method,
111
- url,
112
- headers,
113
- body,
114
- timeoutMs: timeout_ms,
115
- maxResponseBytes: max_response_bytes,
116
- },
117
- host,
118
- );
119
- return result.error ? mcpError(result.error) : mcpOk(JSON.stringify(result, null, 2));
120
- },
121
- );
90
+ if (!context.listOnly)
91
+ server.tool(
92
+ "vault_http_request",
93
+ "Make an HTTPS request with {{KEY}} references resolved inside Otium. Put secrets in headers or body, never in the URL. The expanded request is not returned; the response is redacted before the model sees it.",
94
+ {
95
+ method: z.enum(["GET", "POST", "PUT", "PATCH", "DELETE"]).default("GET"),
96
+ url: z.string().url().describe("Absolute HTTPS URL without Vault placeholders"),
97
+ headers: z.record(z.string(), z.string()).optional(),
98
+ body: z.string().optional(),
99
+ timeout_ms: z.number().int().min(1_000).max(120_000).optional(),
100
+ max_response_bytes: z
101
+ .number()
102
+ .int()
103
+ .min(1_024)
104
+ .max(1024 * 1024)
105
+ .optional(),
106
+ },
107
+ async ({ method, url, headers, body, timeout_ms, max_response_bytes }) => {
108
+ if (!context.userId) return mcpError("Vault unavailable: no user context");
109
+ const result = await http(
110
+ context.userId,
111
+ {
112
+ method,
113
+ url,
114
+ headers,
115
+ body,
116
+ timeoutMs: timeout_ms,
117
+ maxResponseBytes: max_response_bytes,
118
+ },
119
+ host,
120
+ );
121
+ return result.error ? mcpError(result.error) : mcpOk(JSON.stringify(result, null, 2));
122
+ },
123
+ );
122
124
 
123
125
  return server;
124
126
  }
@@ -19,6 +19,7 @@ await connectStdio(
19
19
  createVaultMcpServer(
20
20
  {
21
21
  userId: parseUserIdArg(args),
22
+ listOnly: args.includes("--list-only=true"),
22
23
  httpOnly: args.includes("--http-only=true"),
23
24
  cwd: process.cwd(),
24
25
  },
@@ -455,15 +455,11 @@ const MCP_CATALOG: Record<string, RuntimeMcpCatalogEntry> = {
455
455
  },
456
456
  vault: {
457
457
  ...commonRuntimeMcpPolicy("vault"),
458
- // Credential references must never dead-end because a topic whitelist
459
- // omitted the broker while provider hooks correctly block raw expansion.
460
458
  build({ userId, agent }) {
461
- const args = [`--user-id=${userId}`];
462
- // Codex has no host-side PostToolUse redaction. Keep its Vault surface
463
- // broker-only and non-persistent: HTTPS responses are scrubbed before the
464
- // MCP result reaches Codex, while arbitrary shell could write a secret to
465
- // disk and reveal it through a later Read.
466
- if (agent === "codex") args.push("--http-only=true");
459
+ // Normal turns use {{KEY}} directly in tool inputs. Keep the Vault MCP
460
+ // surface focused on key discovery; broker tools remain available from
461
+ // the public factory for compatibility but are no longer the default UX.
462
+ const args = [`--user-id=${userId}`, "--list-only=true"];
467
463
  return buildStdioMcpServer(agent, VAULT_SERVER, args);
468
464
  },
469
465
  },
@@ -726,6 +726,10 @@ async function spawnPlaywright(
726
726
  // browser tools would silently disappear from maestro turns.
727
727
  "--host",
728
728
  "127.0.0.1",
729
+ // Keep the browser visible. Do not rely on the launcher's implicit
730
+ // default: an explicit flag prevents wrapper/upstream default changes
731
+ // from silently switching topic browsers back to headless mode.
732
+ "--headed",
729
733
  "--user-data-dir",
730
734
  userDataDir,
731
735
  // NOTE: mcp-patchright throws on unknown CLI args, so the old
@@ -736,8 +740,6 @@ async function spawnPlaywright(
736
740
  // (requires real Google Chrome at /opt/google/chrome/chrome).
737
741
  // --init-script <stealth> → unneeded; Patchright is stealth by default.
738
742
  ];
739
- // Headed by default, matching clawgram's production behavior.
740
-
741
743
  // Pass the egress proxy to the child through the environment rather than
742
744
  // argv so the credentials never surface in `ps`/`/proc` command lines. The
743
745
  // launcher (scripts/mcp-patchright-http.mjs) reads these NEGOTIUM_BROWSER_PROXY_*
@@ -747,6 +749,7 @@ async function spawnPlaywright(
747
749
  const childEnv = {
748
750
  ...process.env,
749
751
  NEGOTIUM_BROWSER_CAPABILITY: capability,
752
+ NEGOTIUM_BROWSER_VAULT_USER_ID: userId,
750
753
  ...(proxy
751
754
  ? {
752
755
  NEGOTIUM_BROWSER_PROXY_SERVER: proxy.server,
@@ -1,8 +1,13 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
+ import {
4
+ formatSelectableModel,
5
+ MODEL_COST_ROUTING_SUMMARY,
6
+ SELECTABLE_MODELS,
7
+ } from "#agents/model-catalog";
3
8
  import { AGENTS_PROMPTS_DIR, PROJECT_ROOT, RESOURCES_DIR } from "#platform/config";
4
9
  import { logger } from "#platform/logger";
5
- import type { AgentKind } from "#types";
10
+ import type { AgentKind, EffortLevel } from "#types";
6
11
 
7
12
  const PROMPTS_DIR = resolve(PROJECT_ROOT, "src/prompts");
8
13
  const SESSIONS_DIR = resolve(PROMPTS_DIR, "sessions");
@@ -151,6 +156,10 @@ export interface SessionSystemPromptOpts {
151
156
  topicTitle: string;
152
157
  workspaceCwd: string;
153
158
  agentKind: AgentKind;
159
+ /** Resolved model actually used for this turn. */
160
+ currentModel?: string;
161
+ /** Resolved effort actually used for this turn; absent means provider default/off. */
162
+ currentEffort?: EffortLevel;
154
163
  description?: string | null;
155
164
  /** True only for top-level agent rooms — the runtime spawn_subagent tool is registered there. */
156
165
  canSpawnSubagents?: boolean;
@@ -165,6 +174,8 @@ function buildRuntimeToolSection(
165
174
  canSpawnSubagents = false,
166
175
  visualTools = false,
167
176
  fileDeliveryTools = false,
177
+ currentModel?: string,
178
+ currentEffort?: EffortLevel,
168
179
  ): string {
169
180
  const runtimeNamespace = "mcp__runtime";
170
181
  const taskNamespace = "mcp__task";
@@ -262,20 +273,28 @@ function buildRuntimeToolSection(
262
273
  ...spawnSubagentSection,
263
274
  ];
264
275
 
276
+ const modelCatalog = SELECTABLE_MODELS.map(
277
+ (candidate) => `- ${formatSelectableModel(candidate)}`,
278
+ );
265
279
  const topicConfig = [
266
280
  "",
267
281
  "## Topic Configuration (model / agent / effort)",
282
+ `Current execution: agent=\`${agentKind}\`, model=\`${currentModel ?? "unknown (call get_model)"}\`, effort=\`${currentEffort ?? "provider default/off"}\`.`,
268
283
  "The user's configured agent/model/effort is intentional. Preserve it by default.",
269
284
  `When the user explicitly asks to change the model, agent backend, or reasoning effort for THIS topic, call "${runtimeNamespace}__set_model", "${runtimeNamespace}__set_agent", or "${runtimeNamespace}__set_effort". The change applies from your NEXT turn. After calling, briefly confirm and the system will continue with the new setting.`,
270
285
  "`set_effort` is available but discouraged; use it only when the user explicitly requests an effort change.",
271
- "`set_model` may be called autonomously only when the current model is clearly below the task's required capability, such as complex algorithm design, proof-level math, or broad multi-file refactoring. In that case, move up one step within the same agent and end the turn. Do not use vague task complexity as a trigger.",
286
+ "`set_model` may be called autonomously only when the current model is clearly below the task's required capability, such as complex algorithm design, proof-level math, or broad multi-file refactoring. Choose the best-fit model directly from the same-agent catalog; model selection is not a mandatory one-step ladder. End the turn after changing it. Do not use vague task complexity as a trigger.",
272
287
  "`set_agent` autonomous calls are forbidden. Only switch agent when the user explicitly asks to switch runtime, e.g. “codex로 가”, “claude로 바꿔”.",
273
288
  "Never use `fable` unless the user explicitly requests it; it is expensive.",
274
289
  "",
275
- "Accepted values:",
276
- "- claude models: `sonnet` (default), `opus`, `fable`; efforts: `low`, `medium`, `high`, `xhigh`, `max`.",
277
- "- codex models: `gpt-5.6-luna` (default), `gpt-5.6-terra`, `gpt-5.6-sol`; efforts: `low`, `medium`, `high`, `xhigh`, `max`.",
278
- "- maestro models: `deepseek-pro` (default), `deepseek-flash`, `deepseek`; efforts: `low`, `medium`, `high`, `xhigh`, `max`.",
290
+ "Model catalog (capability/cost routing guidance):",
291
+ MODEL_COST_ROUTING_SUMMARY,
292
+ ...modelCatalog,
293
+ "",
294
+ "Accepted effort values:",
295
+ "- claude: `low`, `medium`, `high`, `xhigh`, `max`.",
296
+ "- codex: `low`, `medium`, `high`, `xhigh`, `max`.",
297
+ "- maestro: `low`, `medium`, `high`, `xhigh`, `max`.",
279
298
  "Agent guidance when the user explicitly asks to switch: `codex` for deepest reasoning and complex code/math; `claude` for tool-heavy MCP/file automation; `maestro` for inexpensive fast drafts and lighter experiments.",
280
299
  ];
281
300
 
@@ -306,6 +325,8 @@ export function buildTopicSystemPrompt(opts: SessionSystemPromptOpts): string {
306
325
  opts.canSpawnSubagents,
307
326
  opts.visualTools,
308
327
  opts.fileDeliveryTools,
328
+ opts.currentModel,
329
+ opts.currentEffort,
309
330
  );
310
331
  if (opts.description?.trim()) {
311
332
  prompt += `\n\n## Topic-Specific Instructions\n${opts.description.trim()}`;
@@ -321,7 +342,15 @@ export function buildChannelSystemPrompt(opts: SessionSystemPromptOpts): string
321
342
  TOPIC_TITLE: opts.topicTitle,
322
343
  WORKSPACE_CWD: opts.workspaceCwd,
323
344
  UPLOADS_DIR: uploadsDir,
324
- }) + buildRuntimeToolSection(opts.agentKind, false, opts.visualTools, opts.fileDeliveryTools)
345
+ }) +
346
+ buildRuntimeToolSection(
347
+ opts.agentKind,
348
+ false,
349
+ opts.visualTools,
350
+ opts.fileDeliveryTools,
351
+ opts.currentModel,
352
+ opts.currentEffort,
353
+ )
325
354
  );
326
355
  }
327
356
 
@@ -1897,6 +1897,8 @@ export function startAiTurn(params: StartAiTurnParams): string | null {
1897
1897
  topicTitle: topic.title,
1898
1898
  workspaceCwd,
1899
1899
  agentKind,
1900
+ currentModel: resolvedModel,
1901
+ currentEffort: resolvedEffort,
1900
1902
  description: topic.description,
1901
1903
  canSpawnSubagents:
1902
1904
  peerBridge?.canSpawnSubagents ?? (topicRecord?.kind === "agent" && !topicRecord.isSubagent),
@@ -1,5 +1,6 @@
1
1
  import { mkdirSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
+ import { countMemoryArchiveExchanges } from "#agents/memory-archive-policy";
3
4
  import { logger } from "#platform/logger";
4
5
  import { sanitizeTopicName } from "#security/sanitize";
5
6
  import { getAllMessagesForTopic, getMessagesForTopicAfterRowid } from "#storage/api-messages";
@@ -9,6 +10,7 @@ import { getSharedWikiDir } from "#storage/wiki";
9
10
  export interface TopicArchiveResult {
10
11
  path: string;
11
12
  messageCount: number;
13
+ exchangeCount: number;
12
14
  lastRowid: number;
13
15
  }
14
16
 
@@ -71,9 +73,10 @@ export function archiveTopicMessages(
71
73
  }
72
74
  }
73
75
 
76
+ const exchangeCount = countMemoryArchiveExchanges(rows);
74
77
  logger.info(
75
- { topicId, topicTitle, archive: path, messageCount: rows.length, lastRowid },
78
+ { topicId, topicTitle, archive: path, messageCount: rows.length, exchangeCount, lastRowid },
76
79
  "archiveTopicMessages: archived topic messages",
77
80
  );
78
- return { path, messageCount: rows.length, lastRowid };
81
+ return { path, messageCount: rows.length, exchangeCount, lastRowid };
79
82
  }
@@ -9,6 +9,7 @@ import { rmSync } from "node:fs";
9
9
  import { runArchiverTurn } from "#agents/archiver";
10
10
  import { cancelIdleArchiveForTopic } from "#agents/idle-archiver";
11
11
  import { cancelSubagentWatchForDeletedTopic } from "#agents/mcp-tools/spawn-subagent";
12
+ import { MIN_MEMORY_ARCHIVE_EXCHANGES } from "#agents/memory-archive-policy";
12
13
  import { type PurgeSessionRef, purgeTopicLogs } from "#agents/topic-cleanup";
13
14
  import { WsHub } from "#bus";
14
15
  import { killBgBash } from "#platform/background-bash/manager";
@@ -210,7 +211,7 @@ async function deleteTopicCascadeImpl(
210
211
  }
211
212
  }
212
213
 
213
- if (archived) {
214
+ if (archived && archived.exchangeCount >= MIN_MEMORY_ARCHIVE_EXCHANGES) {
214
215
  try {
215
216
  const memoryTopic = getTopicMemoryOrigin(topicId) ?? topic;
216
217
  runArchiverTurn({
@@ -3,6 +3,7 @@
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { archiveActiveTopicForMemory, cancelIdleArchiveForTopic } from "#agents/idle-archiver";
5
5
  import { runAgent } from "#agents/index";
6
+ import { MIN_MEMORY_ARCHIVE_EXCHANGES } from "#agents/memory-archive-policy";
6
7
  import { resolveModelForAgent } from "#agents/model-catalog";
7
8
  import { getRegistry } from "#agents/registry";
8
9
  import { cleanupTopicRollouts, purgeTopicLogs } from "#agents/topic-cleanup";
@@ -119,6 +120,7 @@ export async function restartTopicSession(
119
120
  (options.archiveMemory ?? archiveActiveTopicForMemory)(topicId, userId, {
120
121
  reason: "reset",
121
122
  minMessages: 1,
123
+ minExchanges: MIN_MEMORY_ARCHIVE_EXCHANGES,
122
124
  allowMentionOnly: true,
123
125
  skipBusyCheck: true,
124
126
  });
@@ -1 +1 @@
1
- export const NEGOTIUM_VERSION = "0.1.19";
1
+ export const NEGOTIUM_VERSION = "0.1.21";
package/dist/storage.js CHANGED
@@ -2823,6 +2823,25 @@ __export(exports_topic_archive, {
2823
2823
  import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync7 } from "fs";
2824
2824
  import { join as join9 } from "path";
2825
2825
 
2826
+ // ../../packages/core/src/agents/memory-archive-policy.ts
2827
+ function countMemoryArchiveExchanges(rows) {
2828
+ let waitingForAssistant = false;
2829
+ let completed = 0;
2830
+ for (const row of rows) {
2831
+ const assistant = row.author_id === "ai" || Boolean(row.agent_type);
2832
+ const conversational = row.kind == null || row.kind === "message";
2833
+ if (!assistant && row.author_id !== "system" && conversational) {
2834
+ waitingForAssistant = true;
2835
+ continue;
2836
+ }
2837
+ if (assistant && conversational && waitingForAssistant) {
2838
+ completed++;
2839
+ waitingForAssistant = false;
2840
+ }
2841
+ }
2842
+ return completed;
2843
+ }
2844
+
2826
2845
  // ../../packages/core/src/storage/topic-transcript.ts
2827
2846
  var exports_topic_transcript = {};
2828
2847
  __export(exports_topic_transcript, {
@@ -2966,8 +2985,9 @@ function archiveTopicMessages(topicId, topicTitle, options = {}) {
2966
2985
  counter++;
2967
2986
  }
2968
2987
  }
2969
- logger.info({ topicId, topicTitle, archive: path, messageCount: rows.length, lastRowid }, "archiveTopicMessages: archived topic messages");
2970
- return { path, messageCount: rows.length, lastRowid };
2988
+ const exchangeCount = countMemoryArchiveExchanges(rows);
2989
+ logger.info({ topicId, topicTitle, archive: path, messageCount: rows.length, exchangeCount, lastRowid }, "archiveTopicMessages: archived topic messages");
2990
+ return { path, messageCount: rows.length, exchangeCount, lastRowid };
2971
2991
  }
2972
2992
  // ../../packages/core/src/storage/topic-archive-state.ts
2973
2993
  var exports_topic_archive_state = {};
@@ -3340,4 +3360,4 @@ export {
3340
3360
  DEFAULT_AI_NAME
3341
3361
  };
3342
3362
 
3343
- //# debugId=AE15B7A45D14E36264756E2164756E21
3363
+ //# debugId=957BDB73C3C8AF8A64756E2164756E21