clementine-agent 1.18.179 → 1.18.181
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/assistant.js +2 -0
- package/dist/agent/background-tasks.d.ts +20 -0
- package/dist/agent/background-tasks.js +43 -0
- package/dist/gateway/cron-scheduler.d.ts +24 -0
- package/dist/gateway/cron-scheduler.js +103 -4
- package/dist/index.js +14 -0
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/agent/assistant.js
CHANGED
|
@@ -1208,6 +1208,8 @@ Obsidian vault with YAML frontmatter, [[wikilinks]], #tags.
|
|
|
1208
1208
|
**Remembering:** Durable facts → memory_write(action="update_memory"). Daily context → note_take / memory_write(action="append_daily"). New person → note_create. New task → task_add.
|
|
1209
1209
|
Save important facts immediately; a background agent also extracts after each exchange.
|
|
1210
1210
|
|
|
1211
|
+
**Recalling — REQUIRED behavior:** When the user references past work you don't have in immediate context — a URL, a deployment, a file you created, a task or background job you ran, a person/project/domain name you don't have inline — call \`memory_search\` (or \`transcript_search\` for chat history) BEFORE asking the user to provide it and BEFORE replying that you have no record. Saying "I don't see any record of that" without having searched is a memory failure, not an honest answer. Background tasks, cron runs, deployments, and prior chat turns are all in the SQLite memory store with dense embeddings — semantic search will surface them even when the wording doesn't match exactly.
|
|
1212
|
+
|
|
1211
1213
|
## Self-Configuration (never tell ${owner} to edit a config file)
|
|
1212
1214
|
|
|
1213
1215
|
Clementine is self-configuring. Every credential, every integration, every tool permission can be set by calling a tool — no hand-editing.
|
|
@@ -64,6 +64,26 @@ export declare function resumeBackgroundTask(id: string, opts?: BackgroundTaskOp
|
|
|
64
64
|
export declare function interruptStaleRunningTasks(opts?: BackgroundTaskOptions): number;
|
|
65
65
|
/** Backward-compatible export for callers/tests using the old name. */
|
|
66
66
|
export declare const abortStaleRunningTasks: typeof interruptStaleRunningTasks;
|
|
67
|
+
/**
|
|
68
|
+
* Find background tasks whose lifecycle messages were never mirrored into
|
|
69
|
+
* the originating chat session's memory — typically because they completed
|
|
70
|
+
* before 1.18.180 wired the mirror, or because the daemon was down when the
|
|
71
|
+
* delivery would have fired. Returns terminal-state tasks (done / failed /
|
|
72
|
+
* interrupted / aborted) that:
|
|
73
|
+
* - have a sessionKey (so we know where to mirror them)
|
|
74
|
+
* - lack a `mirroredAt` flag (haven't been mirrored yet)
|
|
75
|
+
* - completed within the recency window (default: last 7 days)
|
|
76
|
+
*
|
|
77
|
+
* Caller (typically the cron-scheduler on daemon start) is responsible for
|
|
78
|
+
* doing the actual mirror via gateway.injectContext and then stamping each
|
|
79
|
+
* task with `markBackgroundTaskMirrored(id)`. Keeping the injection out of
|
|
80
|
+
* this module avoids a dependency cycle on the gateway.
|
|
81
|
+
*/
|
|
82
|
+
export declare function findUnmirroredDeliveries(opts?: BackgroundTaskOptions & {
|
|
83
|
+
sinceMs?: number;
|
|
84
|
+
}): BackgroundTask[];
|
|
85
|
+
/** Stamp `mirroredAt` so future boots don't re-mirror the same delivery. */
|
|
86
|
+
export declare function markBackgroundTaskMirrored(id: string, opts?: BackgroundTaskOptions): void;
|
|
67
87
|
/** Delete a task file. Callers should avoid deleting active tasks. */
|
|
68
88
|
export declare function deleteBackgroundTask(id: string, opts?: BackgroundTaskOptions): void;
|
|
69
89
|
/** Backward-compatible test helper alias. */
|
|
@@ -205,6 +205,49 @@ export function interruptStaleRunningTasks(opts) {
|
|
|
205
205
|
}
|
|
206
206
|
/** Backward-compatible export for callers/tests using the old name. */
|
|
207
207
|
export const abortStaleRunningTasks = interruptStaleRunningTasks;
|
|
208
|
+
/**
|
|
209
|
+
* Find background tasks whose lifecycle messages were never mirrored into
|
|
210
|
+
* the originating chat session's memory — typically because they completed
|
|
211
|
+
* before 1.18.180 wired the mirror, or because the daemon was down when the
|
|
212
|
+
* delivery would have fired. Returns terminal-state tasks (done / failed /
|
|
213
|
+
* interrupted / aborted) that:
|
|
214
|
+
* - have a sessionKey (so we know where to mirror them)
|
|
215
|
+
* - lack a `mirroredAt` flag (haven't been mirrored yet)
|
|
216
|
+
* - completed within the recency window (default: last 7 days)
|
|
217
|
+
*
|
|
218
|
+
* Caller (typically the cron-scheduler on daemon start) is responsible for
|
|
219
|
+
* doing the actual mirror via gateway.injectContext and then stamping each
|
|
220
|
+
* task with `markBackgroundTaskMirrored(id)`. Keeping the injection out of
|
|
221
|
+
* this module avoids a dependency cycle on the gateway.
|
|
222
|
+
*/
|
|
223
|
+
export function findUnmirroredDeliveries(opts) {
|
|
224
|
+
const sinceMs = opts?.sinceMs ?? 7 * 24 * 60 * 60_000;
|
|
225
|
+
const cutoff = Date.now() - sinceMs;
|
|
226
|
+
const terminal = ['done', 'failed', 'interrupted', 'aborted'];
|
|
227
|
+
const out = [];
|
|
228
|
+
for (const status of terminal) {
|
|
229
|
+
for (const task of listBackgroundTasks({ status }, opts)) {
|
|
230
|
+
if (task.mirroredAt)
|
|
231
|
+
continue; // already mirrored on a prior boot
|
|
232
|
+
if (!task.sessionKey)
|
|
233
|
+
continue; // no chat to mirror back to
|
|
234
|
+
const stampIso = task.completedAt ?? task.interruptedAt ?? task.createdAt;
|
|
235
|
+
const stamp = Date.parse(stampIso ?? '');
|
|
236
|
+
if (Number.isFinite(stamp) && stamp < cutoff)
|
|
237
|
+
continue;
|
|
238
|
+
out.push(task);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return out;
|
|
242
|
+
}
|
|
243
|
+
/** Stamp `mirroredAt` so future boots don't re-mirror the same delivery. */
|
|
244
|
+
export function markBackgroundTaskMirrored(id, opts) {
|
|
245
|
+
const task = loadBackgroundTask(id, opts);
|
|
246
|
+
if (!task)
|
|
247
|
+
return;
|
|
248
|
+
task.mirroredAt = new Date().toISOString();
|
|
249
|
+
safeWrite(pathFor(id, opts), task);
|
|
250
|
+
}
|
|
208
251
|
/** Delete a task file. Callers should avoid deleting active tasks. */
|
|
209
252
|
export function deleteBackgroundTask(id, opts) {
|
|
210
253
|
try {
|
|
@@ -186,6 +186,30 @@ 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
|
+
* If `taskId` is provided, stamps the task with `mirroredAt` so the
|
|
200
|
+
* startup backfill won't replay it on the next daemon restart.
|
|
201
|
+
*/
|
|
202
|
+
private mirrorBackgroundTaskToChat;
|
|
203
|
+
/**
|
|
204
|
+
* Boot-time backfill. Mirrors any terminal-state background task whose
|
|
205
|
+
* lifecycle message never landed in the originating chat session's
|
|
206
|
+
* memory — typically because it finished before 1.18.180 wired the
|
|
207
|
+
* mirror, or because the daemon was down when delivery would have
|
|
208
|
+
* fired. Idempotent via the `mirroredAt` flag on each task file.
|
|
209
|
+
*/
|
|
210
|
+
mirrorOrphanedBackgroundDeliveries(): {
|
|
211
|
+
mirrored: number;
|
|
212
|
+
};
|
|
189
213
|
/** Same idea for workflows. Workflows can be agent-scoped via WorkflowDefinition.agentSlug. */
|
|
190
214
|
private dispatchContextForWorkflow;
|
|
191
215
|
private runJob;
|
|
@@ -39,7 +39,7 @@ import { parseAllWorkflows as parseAllWorkflowsSync } from '../agent/workflow-ru
|
|
|
39
39
|
import { SelfImproveLoop } from '../agent/self-improve.js';
|
|
40
40
|
import { loadPromptOverridesForJob, watchPromptOverrides } from '../agent/prompt-overrides/loader.js';
|
|
41
41
|
import { logAuditJsonl } from '../agent/hooks.js';
|
|
42
|
-
import { listBackgroundTasks, loadBackgroundTask, markDone as markBgTaskDone, markFailed as markBgTaskFailed, markRunning as markBgTaskRunning, updateBackgroundTask, } from '../agent/background-tasks.js';
|
|
42
|
+
import { findUnmirroredDeliveries, listBackgroundTasks, loadBackgroundTask, markBackgroundTaskMirrored, markDone as markBgTaskDone, markFailed as markBgTaskFailed, markRunning as markBgTaskRunning, updateBackgroundTask, } from '../agent/background-tasks.js';
|
|
43
43
|
import { outcomeStatusFromGoalDisposition, recentDecisions, recordDecisionOutcome, } from '../agent/proactive-ledger.js';
|
|
44
44
|
import { formatCreditBlock, getBackgroundCreditBlock, isCreditBalanceError, markBackgroundCreditBlocked, } from './credit-guard.js';
|
|
45
45
|
import { isRunHealthFailure } from './job-health.js';
|
|
@@ -944,6 +944,69 @@ 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
|
+
* If `taskId` is provided, stamps the task with `mirroredAt` so the
|
|
958
|
+
* startup backfill won't replay it on the next daemon restart.
|
|
959
|
+
*/
|
|
960
|
+
mirrorBackgroundTaskToChat(sessionKey, userTextPlaceholder, assistantText, taskId) {
|
|
961
|
+
if (!sessionKey)
|
|
962
|
+
return;
|
|
963
|
+
try {
|
|
964
|
+
this.gateway.injectContext(sessionKey, userTextPlaceholder, assistantText, {
|
|
965
|
+
pending: false,
|
|
966
|
+
model: 'bg-task',
|
|
967
|
+
countExchange: true,
|
|
968
|
+
});
|
|
969
|
+
if (taskId) {
|
|
970
|
+
try {
|
|
971
|
+
markBackgroundTaskMirrored(taskId);
|
|
972
|
+
}
|
|
973
|
+
catch { /* non-fatal */ }
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
catch (err) {
|
|
977
|
+
logger.debug({ err, sessionKey }, 'Failed to mirror background task message into chat memory');
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Boot-time backfill. Mirrors any terminal-state background task whose
|
|
982
|
+
* lifecycle message never landed in the originating chat session's
|
|
983
|
+
* memory — typically because it finished before 1.18.180 wired the
|
|
984
|
+
* mirror, or because the daemon was down when delivery would have
|
|
985
|
+
* fired. Idempotent via the `mirroredAt` flag on each task file.
|
|
986
|
+
*/
|
|
987
|
+
mirrorOrphanedBackgroundDeliveries() {
|
|
988
|
+
let mirrored = 0;
|
|
989
|
+
try {
|
|
990
|
+
for (const task of findUnmirroredDeliveries()) {
|
|
991
|
+
const promptSnippet = (task.prompt ?? '').slice(0, 200);
|
|
992
|
+
const headSummary = `${task.id} (${task.status})`;
|
|
993
|
+
const body = (task.result ?? task.error ?? '(no saved output)').slice(0, 1500);
|
|
994
|
+
const placeholder = `[Background task ${headSummary} delivered: ${promptSnippet}]`;
|
|
995
|
+
const message = task.status === 'done'
|
|
996
|
+
? `**Background task ${task.id} done** — ${promptSnippet}\n\n${body}`
|
|
997
|
+
: `**Background task ${task.id} ${task.status}** — ${promptSnippet}\n\n${body}`;
|
|
998
|
+
this.mirrorBackgroundTaskToChat(task.sessionKey, placeholder, message, task.id);
|
|
999
|
+
mirrored++;
|
|
1000
|
+
}
|
|
1001
|
+
if (mirrored > 0) {
|
|
1002
|
+
logger.info({ mirrored }, 'Mirrored orphaned background task deliveries into chat memory');
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
catch (err) {
|
|
1006
|
+
logger.warn({ err }, 'Background-task backfill failed — non-fatal');
|
|
1007
|
+
}
|
|
1008
|
+
return { mirrored };
|
|
1009
|
+
}
|
|
947
1010
|
/** Same idea for workflows. Workflows can be agent-scoped via WorkflowDefinition.agentSlug. */
|
|
948
1011
|
dispatchContextForWorkflow(name) {
|
|
949
1012
|
const wf = this.workflowDefs.find(w => w.name === name);
|
|
@@ -1915,9 +1978,18 @@ export class CronScheduler {
|
|
|
1915
1978
|
lastNotifiedAt: new Date().toISOString(),
|
|
1916
1979
|
progressMessageCount: 0,
|
|
1917
1980
|
});
|
|
1981
|
+
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
1982
|
this.dispatcher
|
|
1919
|
-
.send(
|
|
1983
|
+
.send(startMessage, this.dispatchContextForBackgroundTask(started))
|
|
1920
1984
|
.catch((err) => logger.debug({ err, id: started.id }, 'Failed to dispatch background task start'));
|
|
1985
|
+
// Mirror the start announcement into the originating chat session's
|
|
1986
|
+
// memory so the assistant remembers it has a task running. Without
|
|
1987
|
+
// this, the next chat turn the user sends comes back to a session
|
|
1988
|
+
// that has no idea any bg: work was ever queued.
|
|
1989
|
+
// Note: we do NOT stamp `mirroredAt` here — that's reserved for the
|
|
1990
|
+
// terminal-state mirror (done/failed) so the backfill only counts
|
|
1991
|
+
// deliveries, not intent-to-run.
|
|
1992
|
+
this.mirrorBackgroundTaskToChat(started.sessionKey, `[Background task ${started.id} queued: ${started.prompt.slice(0, 200)}]`, startMessage);
|
|
1921
1993
|
// Don't await — fire-and-forget. The 3s tick continues to scan.
|
|
1922
1994
|
const maxHours = Math.max(0.05, started.maxMinutes / 60);
|
|
1923
1995
|
const progressTimer = setInterval(() => {
|
|
@@ -1956,9 +2028,16 @@ export class CronScheduler {
|
|
|
1956
2028
|
const completed = loadBackgroundTask(started.id) ?? started;
|
|
1957
2029
|
const deliveryHead = `**Background task ${started.id} done** — ${started.prompt.slice(0, 100).replace(/\s+/g, ' ')}${started.prompt.length > 100 ? '...' : ''}\n\n`;
|
|
1958
2030
|
const body = (result ?? '').slice(0, 1500);
|
|
2031
|
+
const deliveryMessage = deliveryHead + body;
|
|
1959
2032
|
this.dispatcher
|
|
1960
|
-
.send(
|
|
2033
|
+
.send(deliveryMessage, this.dispatchContextForBackgroundTask(completed))
|
|
1961
2034
|
.catch((err) => logger.debug({ err, id: started.id }, 'Failed to dispatch background task result'));
|
|
2035
|
+
// Mirror into chat memory so a follow-up like "fix the site"
|
|
2036
|
+
// doesn't get a blank stare — the assistant needs to remember
|
|
2037
|
+
// it just deployed something and where it lives. Stamp
|
|
2038
|
+
// `mirroredAt` so the startup backfill won't replay this on the
|
|
2039
|
+
// next restart.
|
|
2040
|
+
this.mirrorBackgroundTaskToChat(completed.sessionKey, `[Background task ${completed.id} delivered: ${started.prompt.slice(0, 200)}]`, deliveryMessage, completed.id);
|
|
1962
2041
|
}).catch((err) => {
|
|
1963
2042
|
clearInterval(progressTimer);
|
|
1964
2043
|
const errStr = String(err).slice(0, 500);
|
|
@@ -1969,9 +2048,13 @@ export class CronScheduler {
|
|
|
1969
2048
|
logger.warn({ err: saveErr, id: started.id }, 'Failed to mark background task failed');
|
|
1970
2049
|
}
|
|
1971
2050
|
const failed = loadBackgroundTask(started.id) ?? started;
|
|
2051
|
+
const failMessage = `**Background task ${started.id} failed** — ${errStr.slice(0, 200)}`;
|
|
1972
2052
|
this.dispatcher
|
|
1973
|
-
.send(
|
|
2053
|
+
.send(failMessage, this.dispatchContextForBackgroundTask(failed))
|
|
1974
2054
|
.catch(() => { });
|
|
2055
|
+
// Mirror failures too — the next chat turn should know the task
|
|
2056
|
+
// died rather than silently pretending it never happened.
|
|
2057
|
+
this.mirrorBackgroundTaskToChat(failed.sessionKey, `[Background task ${failed.id} failed: ${started.prompt.slice(0, 200)}]`, failMessage, failed.id);
|
|
1975
2058
|
});
|
|
1976
2059
|
}
|
|
1977
2060
|
}
|
|
@@ -2353,6 +2436,14 @@ export class CronScheduler {
|
|
|
2353
2436
|
const response = await this.gateway.handleWorkflow(wf, inputs ?? {});
|
|
2354
2437
|
if (response && response !== '*(workflow completed — no output)*') {
|
|
2355
2438
|
await this.dispatcher.send(`**[Workflow: ${name}]**\n\n${response.slice(0, 1500)}`, this.dispatchContextForWorkflow(name));
|
|
2439
|
+
// Mirror under a workflow-scoped session so semantic search can
|
|
2440
|
+
// surface this run regardless of who triggered it.
|
|
2441
|
+
try {
|
|
2442
|
+
this.gateway.injectContext(`workflow:${name}`, `[Workflow ${name} ran]`, response, { pending: false, model: 'workflow', countExchange: true });
|
|
2443
|
+
}
|
|
2444
|
+
catch (err) {
|
|
2445
|
+
logger.debug({ err, workflow: name }, 'workflow transcript mirror failed (non-fatal)');
|
|
2446
|
+
}
|
|
2356
2447
|
// Inject into owner's DM session
|
|
2357
2448
|
if (DISCORD_OWNER_ID && DISCORD_OWNER_ID !== '0') {
|
|
2358
2449
|
this.gateway.injectContext(`discord:user:${DISCORD_OWNER_ID}`, `[Workflow: ${name}]`, response);
|
|
@@ -2366,6 +2457,14 @@ export class CronScheduler {
|
|
|
2366
2457
|
logger.error({ err, workflow: name }, `Workflow '${name}' failed`);
|
|
2367
2458
|
const errMsg = `Workflow '${name}' failed: ${String(err).slice(0, 300)}`;
|
|
2368
2459
|
await this.dispatcher.send(errMsg, this.dispatchContextForWorkflow(name));
|
|
2460
|
+
// Mirror failures into memory too — "what happened to that workflow?"
|
|
2461
|
+
// should find something instead of nothing.
|
|
2462
|
+
try {
|
|
2463
|
+
this.gateway.injectContext(`workflow:${name}`, `[Workflow ${name} failed]`, errMsg, { pending: false, model: 'workflow', countExchange: true });
|
|
2464
|
+
}
|
|
2465
|
+
catch (mirrorErr) {
|
|
2466
|
+
logger.debug({ err: mirrorErr, workflow: name }, 'workflow failure mirror failed (non-fatal)');
|
|
2467
|
+
}
|
|
2369
2468
|
return errMsg;
|
|
2370
2469
|
}
|
|
2371
2470
|
finally {
|
package/dist/index.js
CHANGED
|
@@ -901,6 +901,20 @@ async function asyncMain() {
|
|
|
901
901
|
catch (err) {
|
|
902
902
|
logger.warn({ err }, 'Background task hygiene check failed — non-fatal');
|
|
903
903
|
}
|
|
904
|
+
// Backfill orphaned bg-task deliveries into chat memory. Picks up any
|
|
905
|
+
// task that finished in the last 7 days whose lifecycle message was
|
|
906
|
+
// never mirrored (e.g. completed before 1.18.180 wired the mirror, or
|
|
907
|
+
// while the daemon was down). Idempotent via the `mirroredAt` flag on
|
|
908
|
+
// each task file — safe to run on every boot.
|
|
909
|
+
try {
|
|
910
|
+
const result = cronScheduler.mirrorOrphanedBackgroundDeliveries();
|
|
911
|
+
if (result.mirrored > 0) {
|
|
912
|
+
logger.info({ count: result.mirrored }, 'Backfilled orphaned background task deliveries into chat memory');
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
catch (err) {
|
|
916
|
+
logger.warn({ err }, 'Background-task delivery backfill failed — non-fatal');
|
|
917
|
+
}
|
|
904
918
|
const timerInterval = startTimerChecker(dispatcher, gateway);
|
|
905
919
|
// Start brain ingest scheduler (polls registered REST sources on their cron)
|
|
906
920
|
try {
|
package/dist/types.d.ts
CHANGED