clementine-agent 1.18.178 → 1.18.180
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/cli/dashboard.js +14 -5
- package/dist/config.d.ts +19 -8
- package/dist/config.js +21 -4
- package/dist/gateway/cron-scheduler.d.ts +11 -0
- package/dist/gateway/cron-scheduler.js +42 -3
- package/package.json +1 -1
package/dist/cli/dashboard.js
CHANGED
|
@@ -19,7 +19,7 @@ import { TunnelManager } from './tunnel.js';
|
|
|
19
19
|
import { AgentManager } from '../agent/agent-manager.js';
|
|
20
20
|
import { discoverMcpServers, getClaudeIntegrations, KNOWN_MCP_DESCRIPTIONS } from '../agent/mcp-bridge.js';
|
|
21
21
|
import { buildBuilderEnrichedMessage, builderSessionKey } from '../dashboard/builder/prompt.js';
|
|
22
|
-
import { AGENTS_DIR, MEMORY_FILE, SESSIONS_FILE, TIMEZONE, applyOneMillionContextRecovery, currentTimeZone, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
|
|
22
|
+
import { AGENTS_DIR, MEMORY_FILE, SESSIONS_FILE, TIMEZONE, applyOneMillionContextRecovery, currentTimeZone, looksLikeClaudeOneMillionContextError, normalizeClaudeSdkOptionsForOneMillionContext, setEnvOverride, } from '../config.js';
|
|
23
23
|
import { parseTasks } from '../tools/shared.js';
|
|
24
24
|
// 1.18.160 — also pull parseCronJobs + parseAgentCronJobs so getCronJobs()
|
|
25
25
|
// returns the same merged set the runtime fires (CRON.md + agent CRON +
|
|
@@ -8724,6 +8724,11 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
8724
8724
|
content = content.trimEnd() + `\n${key}=${value}\n`;
|
|
8725
8725
|
}
|
|
8726
8726
|
writeFileSync(ENV_PATH, content, { mode: 0o600 });
|
|
8727
|
+
// Always mirror the disk write into the live env cache. Without this,
|
|
8728
|
+
// BUDGET.* and any other getEnv-backed config stays at the value it
|
|
8729
|
+
// was first read with — that's how "Saved $0 in the dashboard" can
|
|
8730
|
+
// coexist with "Hit the $1.00 cron budget cap" in the same minute.
|
|
8731
|
+
setEnvOverride(key, value);
|
|
8727
8732
|
}
|
|
8728
8733
|
function deleteEnvValue(key) {
|
|
8729
8734
|
if (!existsSync(ENV_PATH))
|
|
@@ -8731,6 +8736,9 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
8731
8736
|
const re = new RegExp(`^${key}=.*\n?`, 'm');
|
|
8732
8737
|
const content = readFileSync(ENV_PATH, 'utf-8').replace(re, '');
|
|
8733
8738
|
writeFileSync(ENV_PATH, content, { mode: 0o600 });
|
|
8739
|
+
// Mirror the delete so live readers don't keep seeing the cached value.
|
|
8740
|
+
setEnvOverride(key, '');
|
|
8741
|
+
delete process.env[key];
|
|
8734
8742
|
}
|
|
8735
8743
|
const DASHBOARD_BUDGET_ROWS = [
|
|
8736
8744
|
{ key: 'BUDGET_CHAT_USD', value: '5', label: 'Chat', hint: 'Per interactive chat turn' },
|
|
@@ -8786,8 +8794,9 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
8786
8794
|
return { ok: false, error: 'Budget cap is too high for the dashboard. Use the CLI if you really need a cap above $1000.' };
|
|
8787
8795
|
}
|
|
8788
8796
|
const normalized = n === 0 ? '0' : String(Math.round(n * 100) / 100);
|
|
8797
|
+
// `writeEnvValue` mirrors into the live env cache, so BUDGET.* (now
|
|
8798
|
+
// backed by getters) sees the new value on the very next tool call.
|
|
8789
8799
|
writeEnvValue(key, normalized);
|
|
8790
|
-
process.env[key] = normalized;
|
|
8791
8800
|
return { ok: true, value: normalized };
|
|
8792
8801
|
}
|
|
8793
8802
|
function readRecentDashboardChatFailures(limit = 5) {
|
|
@@ -9046,7 +9055,7 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
9046
9055
|
}
|
|
9047
9056
|
res.json({
|
|
9048
9057
|
ok: true,
|
|
9049
|
-
message: `${key} set to ${formatDashboardBudgetValue(result.value)}.
|
|
9058
|
+
message: `${key} set to ${formatDashboardBudgetValue(result.value)}. Applied to running workers immediately.`,
|
|
9050
9059
|
});
|
|
9051
9060
|
}
|
|
9052
9061
|
catch (err) {
|
|
@@ -9060,11 +9069,11 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
|
|
|
9060
9069
|
let message;
|
|
9061
9070
|
if (preset === 'defaults' || preset === 'standard') {
|
|
9062
9071
|
writes = DASHBOARD_BUDGET_ROWS.map(row => ({ key: row.key, value: row.value }));
|
|
9063
|
-
message = 'Restored the standard spend caps.
|
|
9072
|
+
message = 'Restored the standard spend caps. Applied to running workers immediately.';
|
|
9064
9073
|
}
|
|
9065
9074
|
else if (preset === 'uncapped' || preset === 'off' || preset === 'none') {
|
|
9066
9075
|
writes = DASHBOARD_BUDGET_ROWS.map(row => ({ key: row.key, value: '0' }));
|
|
9067
|
-
message = 'Removed spend caps by setting all budget values to 0.
|
|
9076
|
+
message = 'Removed spend caps by setting all budget values to 0. Applied to running workers immediately. (1M context mode is separate — use Force 200K or Safe Recovery for 1M errors.)';
|
|
9068
9077
|
}
|
|
9069
9078
|
else {
|
|
9070
9079
|
res.status(400).json({ error: 'preset must be defaults or uncapped' });
|
package/dist/config.d.ts
CHANGED
|
@@ -40,6 +40,17 @@ export declare function usesOneMillionContext(model: string | null | undefined,
|
|
|
40
40
|
export declare function getEnv(key: string, fallback?: string): string;
|
|
41
41
|
/** Merged view of process.env overlaid with .env. Use for classifyIntegrations / summarizeIntegrationStatus. */
|
|
42
42
|
export declare function envSnapshot(): Record<string, string | undefined>;
|
|
43
|
+
/**
|
|
44
|
+
* Hot-update a config value at runtime. Call this from any code path that
|
|
45
|
+
* persists a config change (e.g. dashboard `/api/budgets/set`) so the
|
|
46
|
+
* in-module `env` cache stays in sync with what's on disk + in process.env.
|
|
47
|
+
*
|
|
48
|
+
* Without this, `getEnv` keeps returning the value that was read from .env
|
|
49
|
+
* at module init and frozen objects like BUDGET stay stale until the
|
|
50
|
+
* daemon restarts — that's how a "Budgets at zero in the dashboard" UI can
|
|
51
|
+
* coexist with a `Hit the $1.00 cron budget cap` error on the same minute.
|
|
52
|
+
*/
|
|
53
|
+
export declare function setEnvOverride(key: string, value: string): void;
|
|
43
54
|
/** Test-only: clear the keychain ref cache so re-resolution can be tested. */
|
|
44
55
|
export declare function _resetKeychainRefCache(): void;
|
|
45
56
|
/**
|
|
@@ -83,14 +94,14 @@ export declare const ASSISTANT_EXPERIENCE: {
|
|
|
83
94
|
export declare const shellEscape: typeof _shellEscape;
|
|
84
95
|
export declare const MODELS: Models;
|
|
85
96
|
export declare const BUDGET: {
|
|
86
|
-
heartbeat: number;
|
|
87
|
-
cronT1: number;
|
|
88
|
-
cronT2: number;
|
|
89
|
-
chat: number;
|
|
90
|
-
unleashedPhase: undefined;
|
|
91
|
-
memoryExtraction: undefined;
|
|
92
|
-
summarization: undefined;
|
|
93
|
-
reflection: undefined;
|
|
97
|
+
readonly heartbeat: number;
|
|
98
|
+
readonly cronT1: number;
|
|
99
|
+
readonly cronT2: number;
|
|
100
|
+
readonly chat: number;
|
|
101
|
+
readonly unleashedPhase: number | undefined;
|
|
102
|
+
readonly memoryExtraction: number | undefined;
|
|
103
|
+
readonly summarization: number | undefined;
|
|
104
|
+
readonly reflection: number | undefined;
|
|
94
105
|
};
|
|
95
106
|
export declare const MEMORY_JANITOR: {
|
|
96
107
|
consolidatedExpireDays: number;
|
package/dist/config.js
CHANGED
|
@@ -285,6 +285,20 @@ export function getEnv(key, fallback = '') {
|
|
|
285
285
|
export function envSnapshot() {
|
|
286
286
|
return { ...process.env, ...env };
|
|
287
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Hot-update a config value at runtime. Call this from any code path that
|
|
290
|
+
* persists a config change (e.g. dashboard `/api/budgets/set`) so the
|
|
291
|
+
* in-module `env` cache stays in sync with what's on disk + in process.env.
|
|
292
|
+
*
|
|
293
|
+
* Without this, `getEnv` keeps returning the value that was read from .env
|
|
294
|
+
* at module init and frozen objects like BUDGET stay stale until the
|
|
295
|
+
* daemon restarts — that's how a "Budgets at zero in the dashboard" UI can
|
|
296
|
+
* coexist with a `Hit the $1.00 cron budget cap` error on the same minute.
|
|
297
|
+
*/
|
|
298
|
+
export function setEnvOverride(key, value) {
|
|
299
|
+
env[key] = value;
|
|
300
|
+
process.env[key] = value;
|
|
301
|
+
}
|
|
288
302
|
/** Test-only: clear the keychain ref cache so re-resolution can be tested. */
|
|
289
303
|
export function _resetKeychainRefCache() {
|
|
290
304
|
resolvedKeychainRefs.clear();
|
|
@@ -379,11 +393,14 @@ export const MODELS = {
|
|
|
379
393
|
// User-tunable via `clementine config set BUDGET_<NAME>_USD <value>`
|
|
380
394
|
// (writes to ~/.clementine/.env, survives npm update -g) or via
|
|
381
395
|
// `budgets.*` keys in clementine.json.
|
|
396
|
+
// Live getters — each property re-reads .env + process.env on access so a
|
|
397
|
+
// dashboard write (via setEnvOverride) takes effect on the *next* tool call
|
|
398
|
+
// without needing a daemon restart. Defaults match the previous fixed values.
|
|
382
399
|
export const BUDGET = {
|
|
383
|
-
heartbeat
|
|
384
|
-
cronT1
|
|
385
|
-
cronT2
|
|
386
|
-
chat
|
|
400
|
+
get heartbeat() { return getEnvOrJsonNumber('BUDGET_HEARTBEAT_USD', json.budgets?.heartbeat, 0.25); },
|
|
401
|
+
get cronT1() { return getEnvOrJsonNumber('BUDGET_CRON_T1_USD', json.budgets?.cronT1, 0.75); },
|
|
402
|
+
get cronT2() { return getEnvOrJsonNumber('BUDGET_CRON_T2_USD', json.budgets?.cronT2, 1.50); },
|
|
403
|
+
get chat() { return getEnvOrJsonNumber('BUDGET_CHAT_USD', json.budgets?.chat, 5.00); },
|
|
387
404
|
unleashedPhase: undefined,
|
|
388
405
|
memoryExtraction: undefined,
|
|
389
406
|
summarization: undefined,
|
|
@@ -186,6 +186,17 @@ export declare class CronScheduler {
|
|
|
186
186
|
*/
|
|
187
187
|
private dispatchContextForJobName;
|
|
188
188
|
private dispatchContextForBackgroundTask;
|
|
189
|
+
/**
|
|
190
|
+
* Mirror a background-task message (start / done / failed) into the
|
|
191
|
+
* originating chat session's memory. Without this, bg: tasks finish,
|
|
192
|
+
* deliver their result to Discord/Slack, and then the very next chat
|
|
193
|
+
* turn comes back to an assistant that has zero memory of any of it —
|
|
194
|
+
* the user asks "fix the site you just deployed" and gets "I don't see
|
|
195
|
+
* any record of a Netlify site." injectContext writes into both the
|
|
196
|
+
* pending-context map (visible to the next SDK turn) and the memory
|
|
197
|
+
* store (searchable later by the assistant).
|
|
198
|
+
*/
|
|
199
|
+
private mirrorBackgroundTaskToChat;
|
|
189
200
|
/** Same idea for workflows. Workflows can be agent-scoped via WorkflowDefinition.agentSlug. */
|
|
190
201
|
private dispatchContextForWorkflow;
|
|
191
202
|
private runJob;
|
|
@@ -944,6 +944,30 @@ export class CronScheduler {
|
|
|
944
944
|
ctx.agentSlug = task.fromAgent;
|
|
945
945
|
return ctx;
|
|
946
946
|
}
|
|
947
|
+
/**
|
|
948
|
+
* Mirror a background-task message (start / done / failed) into the
|
|
949
|
+
* originating chat session's memory. Without this, bg: tasks finish,
|
|
950
|
+
* deliver their result to Discord/Slack, and then the very next chat
|
|
951
|
+
* turn comes back to an assistant that has zero memory of any of it —
|
|
952
|
+
* the user asks "fix the site you just deployed" and gets "I don't see
|
|
953
|
+
* any record of a Netlify site." injectContext writes into both the
|
|
954
|
+
* pending-context map (visible to the next SDK turn) and the memory
|
|
955
|
+
* store (searchable later by the assistant).
|
|
956
|
+
*/
|
|
957
|
+
mirrorBackgroundTaskToChat(sessionKey, userTextPlaceholder, assistantText) {
|
|
958
|
+
if (!sessionKey)
|
|
959
|
+
return;
|
|
960
|
+
try {
|
|
961
|
+
this.gateway.injectContext(sessionKey, userTextPlaceholder, assistantText, {
|
|
962
|
+
pending: false,
|
|
963
|
+
model: 'bg-task',
|
|
964
|
+
countExchange: true,
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
catch (err) {
|
|
968
|
+
logger.debug({ err, sessionKey }, 'Failed to mirror background task message into chat memory');
|
|
969
|
+
}
|
|
970
|
+
}
|
|
947
971
|
/** Same idea for workflows. Workflows can be agent-scoped via WorkflowDefinition.agentSlug. */
|
|
948
972
|
dispatchContextForWorkflow(name) {
|
|
949
973
|
const wf = this.workflowDefs.find(w => w.name === name);
|
|
@@ -1915,9 +1939,15 @@ export class CronScheduler {
|
|
|
1915
1939
|
lastNotifiedAt: new Date().toISOString(),
|
|
1916
1940
|
progressMessageCount: 0,
|
|
1917
1941
|
});
|
|
1942
|
+
const startMessage = `**Background task ${started.id} started** — ${started.prompt.slice(0, 120).replace(/\s+/g, ' ')}${started.prompt.length > 120 ? '...' : ''}\n\nI'll update you every 15 minutes or when it finishes.`;
|
|
1918
1943
|
this.dispatcher
|
|
1919
|
-
.send(
|
|
1944
|
+
.send(startMessage, this.dispatchContextForBackgroundTask(started))
|
|
1920
1945
|
.catch((err) => logger.debug({ err, id: started.id }, 'Failed to dispatch background task start'));
|
|
1946
|
+
// Mirror the start announcement into the originating chat session's
|
|
1947
|
+
// memory so the assistant remembers it has a task running. Without
|
|
1948
|
+
// this, the next chat turn the user sends comes back to a session
|
|
1949
|
+
// that has no idea any bg: work was ever queued.
|
|
1950
|
+
this.mirrorBackgroundTaskToChat(started.sessionKey, `[Background task ${started.id} queued: ${started.prompt.slice(0, 200)}]`, startMessage);
|
|
1921
1951
|
// Don't await — fire-and-forget. The 3s tick continues to scan.
|
|
1922
1952
|
const maxHours = Math.max(0.05, started.maxMinutes / 60);
|
|
1923
1953
|
const progressTimer = setInterval(() => {
|
|
@@ -1956,9 +1986,14 @@ export class CronScheduler {
|
|
|
1956
1986
|
const completed = loadBackgroundTask(started.id) ?? started;
|
|
1957
1987
|
const deliveryHead = `**Background task ${started.id} done** — ${started.prompt.slice(0, 100).replace(/\s+/g, ' ')}${started.prompt.length > 100 ? '...' : ''}\n\n`;
|
|
1958
1988
|
const body = (result ?? '').slice(0, 1500);
|
|
1989
|
+
const deliveryMessage = deliveryHead + body;
|
|
1959
1990
|
this.dispatcher
|
|
1960
|
-
.send(
|
|
1991
|
+
.send(deliveryMessage, this.dispatchContextForBackgroundTask(completed))
|
|
1961
1992
|
.catch((err) => logger.debug({ err, id: started.id }, 'Failed to dispatch background task result'));
|
|
1993
|
+
// Mirror into chat memory so a follow-up like "fix the site"
|
|
1994
|
+
// doesn't get a blank stare — the assistant needs to remember
|
|
1995
|
+
// it just deployed something and where it lives.
|
|
1996
|
+
this.mirrorBackgroundTaskToChat(completed.sessionKey, `[Background task ${completed.id} delivered: ${started.prompt.slice(0, 200)}]`, deliveryMessage);
|
|
1962
1997
|
}).catch((err) => {
|
|
1963
1998
|
clearInterval(progressTimer);
|
|
1964
1999
|
const errStr = String(err).slice(0, 500);
|
|
@@ -1969,9 +2004,13 @@ export class CronScheduler {
|
|
|
1969
2004
|
logger.warn({ err: saveErr, id: started.id }, 'Failed to mark background task failed');
|
|
1970
2005
|
}
|
|
1971
2006
|
const failed = loadBackgroundTask(started.id) ?? started;
|
|
2007
|
+
const failMessage = `**Background task ${started.id} failed** — ${errStr.slice(0, 200)}`;
|
|
1972
2008
|
this.dispatcher
|
|
1973
|
-
.send(
|
|
2009
|
+
.send(failMessage, this.dispatchContextForBackgroundTask(failed))
|
|
1974
2010
|
.catch(() => { });
|
|
2011
|
+
// Mirror failures too — the next chat turn should know the task
|
|
2012
|
+
// died rather than silently pretending it never happened.
|
|
2013
|
+
this.mirrorBackgroundTaskToChat(failed.sessionKey, `[Background task ${failed.id} failed: ${started.prompt.slice(0, 200)}]`, failMessage);
|
|
1975
2014
|
});
|
|
1976
2015
|
}
|
|
1977
2016
|
}
|