clementine-agent 1.18.180 → 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.
@@ -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 {
@@ -195,8 +195,21 @@ export declare class CronScheduler {
195
195
  * any record of a Netlify site." injectContext writes into both the
196
196
  * pending-context map (visible to the next SDK turn) and the memory
197
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.
198
201
  */
199
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
+ };
200
213
  /** Same idea for workflows. Workflows can be agent-scoped via WorkflowDefinition.agentSlug. */
201
214
  private dispatchContextForWorkflow;
202
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';
@@ -953,8 +953,11 @@ export class CronScheduler {
953
953
  * any record of a Netlify site." injectContext writes into both the
954
954
  * pending-context map (visible to the next SDK turn) and the memory
955
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.
956
959
  */
957
- mirrorBackgroundTaskToChat(sessionKey, userTextPlaceholder, assistantText) {
960
+ mirrorBackgroundTaskToChat(sessionKey, userTextPlaceholder, assistantText, taskId) {
958
961
  if (!sessionKey)
959
962
  return;
960
963
  try {
@@ -963,11 +966,47 @@ export class CronScheduler {
963
966
  model: 'bg-task',
964
967
  countExchange: true,
965
968
  });
969
+ if (taskId) {
970
+ try {
971
+ markBackgroundTaskMirrored(taskId);
972
+ }
973
+ catch { /* non-fatal */ }
974
+ }
966
975
  }
967
976
  catch (err) {
968
977
  logger.debug({ err, sessionKey }, 'Failed to mirror background task message into chat memory');
969
978
  }
970
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
+ }
971
1010
  /** Same idea for workflows. Workflows can be agent-scoped via WorkflowDefinition.agentSlug. */
972
1011
  dispatchContextForWorkflow(name) {
973
1012
  const wf = this.workflowDefs.find(w => w.name === name);
@@ -1947,6 +1986,9 @@ export class CronScheduler {
1947
1986
  // memory so the assistant remembers it has a task running. Without
1948
1987
  // this, the next chat turn the user sends comes back to a session
1949
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.
1950
1992
  this.mirrorBackgroundTaskToChat(started.sessionKey, `[Background task ${started.id} queued: ${started.prompt.slice(0, 200)}]`, startMessage);
1951
1993
  // Don't await — fire-and-forget. The 3s tick continues to scan.
1952
1994
  const maxHours = Math.max(0.05, started.maxMinutes / 60);
@@ -1992,8 +2034,10 @@ export class CronScheduler {
1992
2034
  .catch((err) => logger.debug({ err, id: started.id }, 'Failed to dispatch background task result'));
1993
2035
  // Mirror into chat memory so a follow-up like "fix the site"
1994
2036
  // 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);
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);
1997
2041
  }).catch((err) => {
1998
2042
  clearInterval(progressTimer);
1999
2043
  const errStr = String(err).slice(0, 500);
@@ -2010,7 +2054,7 @@ export class CronScheduler {
2010
2054
  .catch(() => { });
2011
2055
  // Mirror failures too — the next chat turn should know the task
2012
2056
  // died rather than silently pretending it never happened.
2013
- this.mirrorBackgroundTaskToChat(failed.sessionKey, `[Background task ${failed.id} failed: ${started.prompt.slice(0, 200)}]`, failMessage);
2057
+ this.mirrorBackgroundTaskToChat(failed.sessionKey, `[Background task ${failed.id} failed: ${started.prompt.slice(0, 200)}]`, failMessage, failed.id);
2014
2058
  });
2015
2059
  }
2016
2060
  }
@@ -2392,6 +2436,14 @@ export class CronScheduler {
2392
2436
  const response = await this.gateway.handleWorkflow(wf, inputs ?? {});
2393
2437
  if (response && response !== '*(workflow completed — no output)*') {
2394
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
+ }
2395
2447
  // Inject into owner's DM session
2396
2448
  if (DISCORD_OWNER_ID && DISCORD_OWNER_ID !== '0') {
2397
2449
  this.gateway.injectContext(`discord:user:${DISCORD_OWNER_ID}`, `[Workflow: ${name}]`, response);
@@ -2405,6 +2457,14 @@ export class CronScheduler {
2405
2457
  logger.error({ err, workflow: name }, `Workflow '${name}' failed`);
2406
2458
  const errMsg = `Workflow '${name}' failed: ${String(err).slice(0, 300)}`;
2407
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
+ }
2408
2468
  return errMsg;
2409
2469
  }
2410
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
@@ -295,6 +295,7 @@ export interface BackgroundTask {
295
295
  resultPath?: string;
296
296
  error?: string;
297
297
  deliverableNote?: string;
298
+ mirroredAt?: string;
298
299
  }
299
300
  /**
300
301
  * State for one specialist agent's heartbeat scheduler. Persisted at
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.180",
3
+ "version": "1.18.181",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",