clementine-agent 1.18.58 → 1.18.60
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/run-agent-cron.js +8 -3
- package/dist/agent/run-agent-heartbeat.js +13 -13
- package/dist/agent/run-agent-team-task.d.ts +2 -0
- package/dist/agent/run-agent-team-task.js +14 -4
- package/dist/agent/run-agent.js +94 -64
- package/dist/cli/dashboard.js +43 -3
- package/dist/gateway/router.js +9 -3
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import pino from 'pino';
|
|
20
|
-
import { BASE_DIR, VAULT_DIR, CRON_PROGRESS_DIR, } from '../config.js';
|
|
20
|
+
import { BASE_DIR, VAULT_DIR, CRON_PROGRESS_DIR, BUDGET, } from '../config.js';
|
|
21
21
|
import { runAgent } from './run-agent.js';
|
|
22
22
|
import { buildExtraMcpForRunAgent } from './run-agent-mcp.js';
|
|
23
23
|
import { buildAutonomousMemoryContext } from './run-agent-context.js';
|
|
@@ -281,7 +281,12 @@ export async function runAgentCron(opts) {
|
|
|
281
281
|
profile: opts.profile,
|
|
282
282
|
});
|
|
283
283
|
// ── Run via canonical runAgent ────────────────────────────────────
|
|
284
|
-
|
|
284
|
+
// Per-tier cap from config (BUDGET.cronT1 / BUDGET.cronT2). Sourced
|
|
285
|
+
// from env / clementine.json / dashboard writes. 0 means uncapped —
|
|
286
|
+
// we pass undefined so runAgent omits the SDK option entirely.
|
|
287
|
+
// Caller can still override via opts.maxBudgetUsd.
|
|
288
|
+
const configuredCap = tier >= 2 ? BUDGET.cronT2 : BUDGET.cronT1;
|
|
289
|
+
const maxBudget = opts.maxBudgetUsd ?? (configuredCap > 0 ? configuredCap : undefined);
|
|
285
290
|
const effort = tier >= 2 ? 'high' : 'medium';
|
|
286
291
|
logger.info({
|
|
287
292
|
job: opts.jobName,
|
|
@@ -302,7 +307,7 @@ export async function runAgentCron(opts) {
|
|
|
302
307
|
memoryStore: opts.memoryStore,
|
|
303
308
|
model: opts.model,
|
|
304
309
|
effort,
|
|
305
|
-
maxBudgetUsd: maxBudget,
|
|
310
|
+
...(maxBudget !== undefined ? { maxBudgetUsd: maxBudget } : {}),
|
|
306
311
|
maxTurns: opts.maxTurns,
|
|
307
312
|
abortSignal: opts.abortSignal,
|
|
308
313
|
extraMcpServers: mcp.servers,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* through the canonical runAgent() instead of buildOptions+query.
|
|
15
15
|
*/
|
|
16
16
|
import pino from 'pino';
|
|
17
|
-
import { OWNER_NAME, MODELS, } from '../config.js';
|
|
17
|
+
import { OWNER_NAME, MODELS, BUDGET, } from '../config.js';
|
|
18
18
|
const OWNER = OWNER_NAME || 'the user';
|
|
19
19
|
function formatDate(d) {
|
|
20
20
|
return d.toLocaleDateString('en-US', {
|
|
@@ -65,6 +65,10 @@ export async function runAgentHeartbeat(opts) {
|
|
|
65
65
|
profile: opts.profile?.slug,
|
|
66
66
|
promptChars: prompt.length,
|
|
67
67
|
}, 'runAgentHeartbeat: dispatching to runAgent (no tools)');
|
|
68
|
+
// Heartbeat cap from config (BUDGET.heartbeat). Sourced from env /
|
|
69
|
+
// clementine.json / dashboard writes. 0 = uncapped — runAgent
|
|
70
|
+
// omits the SDK option in that case.
|
|
71
|
+
const heartbeatBudget = opts.maxBudgetUsd ?? (BUDGET.heartbeat > 0 ? BUDGET.heartbeat : undefined);
|
|
68
72
|
const sessionKey = `heartbeat:${opts.profile?.slug ?? 'clementine'}`;
|
|
69
73
|
const result = await runAgent(prompt, {
|
|
70
74
|
sessionKey,
|
|
@@ -73,7 +77,7 @@ export async function runAgentHeartbeat(opts) {
|
|
|
73
77
|
memoryStore: opts.memoryStore,
|
|
74
78
|
model: opts.model ?? MODELS.haiku,
|
|
75
79
|
effort: 'low',
|
|
76
|
-
maxBudgetUsd:
|
|
80
|
+
...(heartbeatBudget !== undefined ? { maxBudgetUsd: heartbeatBudget } : {}),
|
|
77
81
|
maxTurns: 1,
|
|
78
82
|
// No tools — heartbeats are decision-only. Empty list bypasses the
|
|
79
83
|
// CORE_TOOLS_FOR_AGENT_PARENT default and stops the SDK from
|
|
@@ -81,17 +85,13 @@ export async function runAgentHeartbeat(opts) {
|
|
|
81
85
|
allowedTools: [],
|
|
82
86
|
abortSignal: opts.abortSignal,
|
|
83
87
|
});
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
catch {
|
|
92
|
-
/* non-fatal */
|
|
93
|
-
}
|
|
94
|
-
}
|
|
88
|
+
// Heartbeat output is NOT mirrored to transcripts. Heartbeats fire
|
|
89
|
+
// up to 28x/day per agent and most output is low-value (status
|
|
90
|
+
// pings, dedup'd reminders). The heartbeat dedup that prior versions
|
|
91
|
+
// wanted recall for actually lives in the prompt itself (the
|
|
92
|
+
// dedupContext block + the __NOTHING__ sentinel), not in DB queries.
|
|
93
|
+
// Saving rows here just polluted FTS and the dashboard memory panel
|
|
94
|
+
// for no recall benefit.
|
|
95
95
|
return result;
|
|
96
96
|
}
|
|
97
97
|
//# sourceMappingURL=run-agent-heartbeat.js.map
|
|
@@ -7,6 +7,8 @@ import { type RunAgentResult } from './run-agent.js';
|
|
|
7
7
|
* the full assistant graph. */
|
|
8
8
|
export interface TeamTaskPostHooks {
|
|
9
9
|
triggerMemoryExtractionPostExchange: (userMessage: string, assistantResponse: string, sessionKey?: string, profile?: AgentProfile) => Promise<void>;
|
|
10
|
+
triggerSkillExtractionFromExecution: (source: 'unleashed' | 'cron' | 'chat', jobName: string, prompt: string, output: string, durationMs: number, agentSlug?: string) => Promise<void>;
|
|
11
|
+
triggerCronReflection: (jobName: string, jobPrompt: string, deliverable: string, successCriteria?: string[]) => Promise<void>;
|
|
10
12
|
}
|
|
11
13
|
export interface RunAgentTeamTaskOptions {
|
|
12
14
|
fromName: string;
|
|
@@ -56,6 +56,7 @@ export async function runAgentTeamTask(opts) {
|
|
|
56
56
|
promptChars: builtPrompt.length,
|
|
57
57
|
}, 'runAgentTeamTask: dispatching to runAgent');
|
|
58
58
|
const sessionKey = `team-task:${opts.fromSlug}->${opts.profile.slug}`;
|
|
59
|
+
const startedAt = Date.now();
|
|
59
60
|
const result = await runAgent(builtPrompt, {
|
|
60
61
|
sessionKey,
|
|
61
62
|
source: 'team-task',
|
|
@@ -82,14 +83,23 @@ export async function runAgentTeamTask(opts) {
|
|
|
82
83
|
/* non-fatal */
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
86
|
+
// Post-task hooks: memory + skill extraction + reflection. All
|
|
87
|
+
// fire-and-forget. Mirrors the cron wrapper's three-hook pattern.
|
|
88
|
+
// Team tasks often produce repeatable procedures (e.g. "draft a
|
|
89
|
+
// follow-up email after a discovery call") and reflection grades
|
|
90
|
+
// whether the response actually fulfilled the request.
|
|
89
91
|
if (opts.postTaskHooks && result.text?.trim()) {
|
|
92
|
+
const durationMs = Date.now() - startedAt;
|
|
90
93
|
opts.postTaskHooks
|
|
91
94
|
.triggerMemoryExtractionPostExchange(opts.content, result.text, sessionKey, opts.profile)
|
|
92
95
|
.catch(err => logger.debug({ err, fromSlug: opts.fromSlug, toSlug: opts.profile.slug }, 'runAgentTeamTask: memory extraction failed (non-fatal)'));
|
|
96
|
+
opts.postTaskHooks
|
|
97
|
+
.triggerSkillExtractionFromExecution('cron', // 'cron' covers autonomous-task skill source category
|
|
98
|
+
taskName, opts.content, result.text, durationMs, opts.profile.slug)
|
|
99
|
+
.catch(err => logger.debug({ err, fromSlug: opts.fromSlug, toSlug: opts.profile.slug }, 'runAgentTeamTask: skill extraction failed (non-fatal)'));
|
|
100
|
+
opts.postTaskHooks
|
|
101
|
+
.triggerCronReflection(taskName, opts.content, result.text)
|
|
102
|
+
.catch(err => logger.debug({ err, fromSlug: opts.fromSlug, toSlug: opts.profile.slug }, 'runAgentTeamTask: reflection failed (non-fatal)'));
|
|
93
103
|
}
|
|
94
104
|
return {
|
|
95
105
|
...result,
|
package/dist/agent/run-agent.js
CHANGED
|
@@ -62,8 +62,15 @@ function buildRunAgentEnv() {
|
|
|
62
62
|
return env;
|
|
63
63
|
}
|
|
64
64
|
const logger = pino({ name: 'clementine.run-agent' });
|
|
65
|
+
// Last-resort fallbacks for callers that pass NO maxBudgetUsd. The
|
|
66
|
+
// production callers (`runAgent` from gateway/router, runAgentCron,
|
|
67
|
+
// runAgentHeartbeat) read `BUDGET.*` from src/config.ts — which is
|
|
68
|
+
// itself sourced from env / clementine.json / dashboard writes — and
|
|
69
|
+
// pass it explicitly. Chat is intentionally omitted: the chat path
|
|
70
|
+
// must always go through `BUDGET.chat` (0 = uncapped), never a silent
|
|
71
|
+
// hardcoded floor. If `source: 'chat'` ever lands here without an
|
|
72
|
+
// explicit budget, we treat it as uncapped.
|
|
65
73
|
const DEFAULT_BUDGETS = {
|
|
66
|
-
chat: 0.50,
|
|
67
74
|
cron: 1.00,
|
|
68
75
|
heartbeat: 0.25,
|
|
69
76
|
'team-task': 1.00,
|
|
@@ -97,7 +104,13 @@ const CORE_TOOLS_FOR_AGENT_PARENT = [
|
|
|
97
104
|
export async function runAgent(prompt, opts) {
|
|
98
105
|
const source = opts.source ?? 'chat';
|
|
99
106
|
const effort = opts.effort ?? DEFAULT_EFFORTS[source] ?? 'medium';
|
|
100
|
-
|
|
107
|
+
// 0 (or undefined) means "no cap" — matches the dashboard's
|
|
108
|
+
// "Remove spend caps" preset contract. We omit `maxBudgetUsd` from
|
|
109
|
+
// sdkOptions entirely in that case so the SDK runs uncapped.
|
|
110
|
+
const requestedBudget = opts.maxBudgetUsd ?? DEFAULT_BUDGETS[source];
|
|
111
|
+
const maxBudgetUsd = typeof requestedBudget === 'number' && requestedBudget > 0
|
|
112
|
+
? requestedBudget
|
|
113
|
+
: undefined;
|
|
101
114
|
const startedAt = Date.now();
|
|
102
115
|
// Build the AgentDefinition map. Caller can override; otherwise we
|
|
103
116
|
// use the standard system subagents + hired-agent profiles.
|
|
@@ -187,8 +200,8 @@ export async function runAgent(prompt, opts) {
|
|
|
187
200
|
allowDangerouslySkipPermissions: true,
|
|
188
201
|
cwd: BASE_DIR,
|
|
189
202
|
env: subprocessEnv,
|
|
190
|
-
maxBudgetUsd,
|
|
191
203
|
effort,
|
|
204
|
+
...(maxBudgetUsd !== undefined ? { maxBudgetUsd } : {}),
|
|
192
205
|
...(opts.maxTurns ? { maxTurns: opts.maxTurns } : {}),
|
|
193
206
|
...(opts.model ? { model: opts.model } : {}),
|
|
194
207
|
...(opts.resumeSessionId ? { resume: opts.resumeSessionId } : {}),
|
|
@@ -201,7 +214,7 @@ export async function runAgent(prompt, opts) {
|
|
|
201
214
|
profile: opts.profile?.slug,
|
|
202
215
|
forceSubagent: opts.forceSubagent,
|
|
203
216
|
effort,
|
|
204
|
-
maxBudgetUsd,
|
|
217
|
+
maxBudgetUsd: maxBudgetUsd ?? 'uncapped',
|
|
205
218
|
agentCount: Object.keys(agents).length,
|
|
206
219
|
allowedToolCount: allowedTools.length,
|
|
207
220
|
}, 'runAgent: starting query');
|
|
@@ -212,76 +225,93 @@ export async function runAgent(prompt, opts) {
|
|
|
212
225
|
let subtype = 'unknown';
|
|
213
226
|
let usage;
|
|
214
227
|
const stream = query({ prompt: effectivePrompt, options: sdkOptions });
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
try {
|
|
229
|
+
for await (const message of stream) {
|
|
230
|
+
if (message.type === 'system' && message.subtype === 'init') {
|
|
231
|
+
sessionId = message.session_id ?? '';
|
|
232
|
+
logger.debug({ sessionKey: opts.sessionKey, sdkSessionId: sessionId }, 'runAgent: SDK session initialized');
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (message.type === 'assistant') {
|
|
236
|
+
const am = message;
|
|
237
|
+
const blocks = (am.message?.content ?? []);
|
|
238
|
+
for (const block of blocks) {
|
|
239
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
240
|
+
finalText += block.text;
|
|
241
|
+
if (opts.onText) {
|
|
242
|
+
try {
|
|
243
|
+
await opts.onText(block.text);
|
|
244
|
+
}
|
|
245
|
+
catch { /* streaming is best-effort */ }
|
|
230
246
|
}
|
|
231
|
-
catch { /* streaming is best-effort */ }
|
|
232
247
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
248
|
+
else if (block.type === 'tool_use' && typeof block.name === 'string') {
|
|
249
|
+
if (opts.onToolActivity) {
|
|
250
|
+
try {
|
|
251
|
+
await opts.onToolActivity({ tool: block.name, input: block.input ?? {} });
|
|
252
|
+
}
|
|
253
|
+
catch { /* best-effort */ }
|
|
238
254
|
}
|
|
239
|
-
catch { /* best-effort */ }
|
|
240
255
|
}
|
|
241
256
|
}
|
|
257
|
+
continue;
|
|
242
258
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (r)
|
|
258
|
-
finalText = r;
|
|
259
|
-
}
|
|
260
|
-
// Mirror cost to usage_log. Same shape as the existing
|
|
261
|
-
// logQueryResult, but standalone so we don't depend on
|
|
262
|
-
// PersonalAssistant's instance state.
|
|
263
|
-
const modelUsage = result.modelUsage;
|
|
264
|
-
if (opts.memoryStore && modelUsage) {
|
|
265
|
-
try {
|
|
266
|
-
opts.memoryStore.logUsage({
|
|
267
|
-
sessionKey: `${source}:${opts.sessionKey}`,
|
|
268
|
-
source: `runagent.${source}`,
|
|
269
|
-
modelUsage,
|
|
270
|
-
numTurns,
|
|
271
|
-
durationMs: Date.now() - startedAt,
|
|
272
|
-
agentSlug: opts.profile?.slug,
|
|
273
|
-
totalCostUsd: totalCostUsd,
|
|
274
|
-
});
|
|
259
|
+
if (message.type === 'result') {
|
|
260
|
+
const result = message;
|
|
261
|
+
sessionId = sessionId || (result.session_id ?? '');
|
|
262
|
+
subtype = result.subtype ?? 'unknown';
|
|
263
|
+
numTurns = result.num_turns ?? numTurns;
|
|
264
|
+
totalCostUsd = result.total_cost_usd ?? 0;
|
|
265
|
+
const u = result.usage;
|
|
266
|
+
if (u)
|
|
267
|
+
usage = u;
|
|
268
|
+
if (subtype === 'success') {
|
|
269
|
+
// success carries `result` field with the final text.
|
|
270
|
+
const r = result.result;
|
|
271
|
+
if (r)
|
|
272
|
+
finalText = r;
|
|
275
273
|
}
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
// Mirror cost to usage_log. Same shape as the existing
|
|
275
|
+
// logQueryResult, but standalone so we don't depend on
|
|
276
|
+
// PersonalAssistant's instance state.
|
|
277
|
+
const modelUsage = result.modelUsage;
|
|
278
|
+
if (opts.memoryStore && modelUsage) {
|
|
279
|
+
try {
|
|
280
|
+
opts.memoryStore.logUsage({
|
|
281
|
+
sessionKey: `${source}:${opts.sessionKey}`,
|
|
282
|
+
source: `runagent.${source}`,
|
|
283
|
+
modelUsage,
|
|
284
|
+
numTurns,
|
|
285
|
+
durationMs: Date.now() - startedAt,
|
|
286
|
+
agentSlug: opts.profile?.slug,
|
|
287
|
+
totalCostUsd: totalCostUsd,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
catch (err) {
|
|
291
|
+
logger.debug({ err }, 'runAgent: usage logging failed (non-fatal)');
|
|
292
|
+
}
|
|
278
293
|
}
|
|
294
|
+
continue;
|
|
279
295
|
}
|
|
280
|
-
|
|
296
|
+
// Other message types (UserMessage with tool_result, StreamEvent,
|
|
297
|
+
// SDKCompactBoundaryMessage) — observed but not acted on. The SDK
|
|
298
|
+
// handles compaction internally; we just let it run.
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
catch (err) {
|
|
302
|
+
// Translate the SDK's budget-exhaustion throw into a message that
|
|
303
|
+
// tells the user (a) what cap tripped and (b) how to raise it.
|
|
304
|
+
// The raw SDK string ("Claude Code returned an error result:
|
|
305
|
+
// Reached maximum budget ($0.5)") leaks through the channel layer
|
|
306
|
+
// as a generic "Something went wrong:" with no actionable hint.
|
|
307
|
+
const msg = String(err?.message ?? err);
|
|
308
|
+
if (/Reached maximum budget|error_max_budget_usd/i.test(msg)) {
|
|
309
|
+
const cap = maxBudgetUsd?.toFixed(2) ?? '?';
|
|
310
|
+
const envKey = `BUDGET_${source.toUpperCase().replace(/-/g, '_')}_USD`;
|
|
311
|
+
throw new Error(`Hit the $${cap} ${source} budget cap before finishing. ` +
|
|
312
|
+
`Raise it in the dashboard (Budgets & Costs) or set ${envKey}=0 to remove caps.`);
|
|
281
313
|
}
|
|
282
|
-
|
|
283
|
-
// SDKCompactBoundaryMessage) — observed but not acted on. The SDK
|
|
284
|
-
// handles compaction internally; we just let it run.
|
|
314
|
+
throw err;
|
|
285
315
|
}
|
|
286
316
|
logger.info({
|
|
287
317
|
sessionKey: opts.sessionKey,
|
package/dist/cli/dashboard.js
CHANGED
|
@@ -281,8 +281,48 @@ async function searchMemory(query, limit = 20, filters = {}) {
|
|
|
281
281
|
WHERE ${where.join(' AND ')}
|
|
282
282
|
ORDER BY ${orderBy}
|
|
283
283
|
LIMIT ?`;
|
|
284
|
-
const
|
|
285
|
-
|
|
284
|
+
const chunkRows = db.prepare(sql).all(...params, limit);
|
|
285
|
+
// Also surface transcripts from chat / cron / team-task. These
|
|
286
|
+
// are written by saveTurn and would otherwise be invisible to the
|
|
287
|
+
// main search panel (only the per-session viewer surfaced them).
|
|
288
|
+
// chunkType filter is chunk-only — if set, skip transcripts.
|
|
289
|
+
let transcriptRows = [];
|
|
290
|
+
if (words.length > 0 && !filters.chunkType && !filters.pinnedOnly) {
|
|
291
|
+
try {
|
|
292
|
+
const ftsQuery = words.map((w) => `"${w.replace(/"/g, '')}"`).join(' OR ');
|
|
293
|
+
const tWhere = ['transcripts_fts MATCH ?'];
|
|
294
|
+
const tParams = [ftsQuery];
|
|
295
|
+
if (filters.sinceDays && filters.sinceDays > 0) {
|
|
296
|
+
tWhere.push("t.created_at >= datetime('now', ?)");
|
|
297
|
+
tParams.push(`-${filters.sinceDays} days`);
|
|
298
|
+
}
|
|
299
|
+
const tSql = `SELECT t.id, t.session_key, t.role, t.content, t.model, t.created_at,
|
|
300
|
+
bm25(transcripts_fts) as score
|
|
301
|
+
FROM transcripts_fts f JOIN transcripts t ON t.id = f.rowid
|
|
302
|
+
WHERE ${tWhere.join(' AND ')}
|
|
303
|
+
ORDER BY bm25(transcripts_fts)
|
|
304
|
+
LIMIT ?`;
|
|
305
|
+
transcriptRows = db.prepare(tSql).all(...tParams, Math.min(limit, 10))
|
|
306
|
+
.map(r => ({
|
|
307
|
+
id: `transcript:${r.id}`,
|
|
308
|
+
source_file: `transcripts/${r.session_key}`,
|
|
309
|
+
section: `${r.role} @ ${r.created_at}`,
|
|
310
|
+
content: r.content,
|
|
311
|
+
chunk_type: 'transcript',
|
|
312
|
+
updated_at: r.created_at,
|
|
313
|
+
salience: 0,
|
|
314
|
+
pinned: 0,
|
|
315
|
+
score: r.score,
|
|
316
|
+
}));
|
|
317
|
+
}
|
|
318
|
+
catch { /* transcripts FTS may be empty/unavailable — non-fatal */ }
|
|
319
|
+
}
|
|
320
|
+
// Merge: transcripts interleaved by score with chunks. FTS bm25
|
|
321
|
+
// is comparable across both since they use the same tokenizer.
|
|
322
|
+
const merged = [...chunkRows, ...transcriptRows]
|
|
323
|
+
.sort((a, b) => Number(a.score ?? 0) - Number(b.score ?? 0))
|
|
324
|
+
.slice(0, limit);
|
|
325
|
+
return { results: merged, dbExists: true };
|
|
286
326
|
}
|
|
287
327
|
catch (err) {
|
|
288
328
|
return { results: [], dbExists: true, error: String(err) };
|
|
@@ -6909,7 +6949,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
6909
6949
|
}
|
|
6910
6950
|
else if (preset === 'uncapped' || preset === 'off' || preset === 'none') {
|
|
6911
6951
|
writes = DASHBOARD_BUDGET_ROWS.map(row => ({ key: row.key, value: '0' }));
|
|
6912
|
-
message = 'Removed spend caps by setting all budget values to 0.
|
|
6952
|
+
message = 'Removed spend caps by setting all budget values to 0. Restart Clementine for the change to take effect on running workers. (1M context mode is separate — use Force 200K or Safe Recovery for 1M errors.)';
|
|
6913
6953
|
}
|
|
6914
6954
|
else {
|
|
6915
6955
|
res.status(400).json({ error: 'preset must be defaults or uncapped' });
|
package/dist/gateway/router.js
CHANGED
|
@@ -10,7 +10,7 @@ import pino from 'pino';
|
|
|
10
10
|
import { oneMillionContextRecoveryMessage, PersonalAssistant, } from '../agent/assistant.js';
|
|
11
11
|
import { runWithTrace, logAuditJsonl } from '../agent/hooks.js';
|
|
12
12
|
import { SelfImproveLoop } from '../agent/self-improve.js';
|
|
13
|
-
import { MODELS, AGENTS_DIR, TEAM_COMMS_LOG, BASE_DIR, SEEN_CHANNELS_FILE, AUTO_DELEGATE_ENABLED, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, } from '../config.js';
|
|
13
|
+
import { MODELS, BUDGET, AGENTS_DIR, TEAM_COMMS_LOG, BASE_DIR, SEEN_CHANNELS_FILE, AUTO_DELEGATE_ENABLED, applyOneMillionContextRecovery, looksLikeClaudeOneMillionContextError, } from '../config.js';
|
|
14
14
|
import { scanner } from '../security/scanner.js';
|
|
15
15
|
import { lanes } from './lanes.js';
|
|
16
16
|
import { AgentManager } from '../agent/agent-manager.js';
|
|
@@ -1820,7 +1820,13 @@ export class Gateway {
|
|
|
1820
1820
|
// Builder cost knobs: Haiku is plenty for JSON drafting,
|
|
1821
1821
|
// tight budget, no tools surfaced in the system prompt.
|
|
1822
1822
|
const builderModel = isBuilderSession ? MODELS.haiku : effectiveModel;
|
|
1823
|
-
|
|
1823
|
+
// Builder stays tight ($0.10 — Haiku JSON drafting only).
|
|
1824
|
+
// Regular chat reads BUDGET.chat from config (env / clementine.json /
|
|
1825
|
+
// dashboard writes). 0 = uncapped — the runAgent layer omits the
|
|
1826
|
+
// SDK option entirely in that case.
|
|
1827
|
+
const chatBudget = isBuilderSession
|
|
1828
|
+
? 0.10
|
|
1829
|
+
: (BUDGET.chat > 0 ? BUDGET.chat : undefined);
|
|
1824
1830
|
const builderAllowedTools = isBuilderSession ? [] : undefined;
|
|
1825
1831
|
logger.info({
|
|
1826
1832
|
sessionKey: effectiveSessionKey,
|
|
@@ -1841,7 +1847,7 @@ export class Gateway {
|
|
|
1841
1847
|
memoryStore: this.assistant.getMemoryStore?.() ?? null,
|
|
1842
1848
|
...(builderModel ? { model: builderModel } : {}),
|
|
1843
1849
|
...(maxTurns ? { maxTurns } : {}),
|
|
1844
|
-
...(
|
|
1850
|
+
...(chatBudget !== undefined ? { maxBudgetUsd: chatBudget } : {}),
|
|
1845
1851
|
...(builderAllowedTools ? { allowedTools: builderAllowedTools } : {}),
|
|
1846
1852
|
...(chatSystemAppend ? { systemPromptAppend: chatSystemAppend } : {}),
|
|
1847
1853
|
...(priorSdkSessionId ? { resumeSessionId: priorSdkSessionId } : {}),
|