clementine-agent 1.7.0 → 1.8.0

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.
@@ -15255,6 +15255,12 @@ function switchBuildTab(tab) {
15255
15255
  if (typeof _ensureDrawflowLoaded === 'function') {
15256
15256
  _ensureDrawflowLoaded().catch(function() { /* */ });
15257
15257
  }
15258
+ // Populate the Owner picker once per session — agents rarely change
15259
+ // mid-session, so the cost is minimal and the dropdown is ready by
15260
+ // the time the user clicks New.
15261
+ if (typeof populateBuilderOwnerPicker === 'function') {
15262
+ populateBuilderOwnerPicker().catch(function() { /* */ });
15263
+ }
15258
15264
  // Focus chat input
15259
15265
  setTimeout(function() {
15260
15266
  var bi = document.getElementById('builder-input');
@@ -15263,10 +15269,10 @@ function switchBuildTab(tab) {
15263
15269
  }
15264
15270
  }
15265
15271
 
15266
- // "New" button in the Build header strip — context-aware: prompts for a
15267
- // name and creates the right artifact for the active tab. Workflows + Crons
15268
- // route through the workflow_create surface; Skills falls back to the
15269
- // existing chat-based Skill Studio reset.
15272
+ // "New" button in the Build header strip — context-aware. Crons open the
15273
+ // dedicated cron modal so the entry lands in CRON.md (not as a one-step
15274
+ // workflow file). Workflows route through /api/builder/workflows. Owner is
15275
+ // read from the header's Owner picker — empty string means global.
15270
15276
  async function newFromBuildHeader() {
15271
15277
  var activeTab = document.querySelector('#build-tabs button.active')?.getAttribute('data-build-tab') || 'workflows';
15272
15278
  if (activeTab === 'skills') {
@@ -15279,22 +15285,72 @@ async function newFromBuildHeader() {
15279
15285
  toast('Pick a template to fork from the cards.', 'info');
15280
15286
  return;
15281
15287
  }
15282
- var noun = activeTab === 'crons' ? 'cron' : 'workflow';
15283
- var name = prompt('Name your new ' + noun + ':');
15288
+ var owner = (document.getElementById('builder-owner') || {}).value || '';
15289
+ if (activeTab === 'crons') {
15290
+ if (typeof openCreateCronModal === 'function') {
15291
+ openCreateCronModal(owner);
15292
+ return;
15293
+ }
15294
+ }
15295
+ var name = prompt('Name your new workflow:');
15284
15296
  if (!name || !name.trim()) return;
15285
15297
  try {
15286
15298
  var body = { name: name.trim() };
15287
- if (activeTab === 'crons') body.schedule = '0 9 * * *'; // sensible default; user edits in canvas
15299
+ if (owner) body.agent = owner;
15288
15300
  var r = await apiJson('POST', '/api/builder/workflows', body);
15289
15301
  if (r && r.error) { toast('Create failed: ' + r.error, 'error'); return; }
15290
15302
  if (r && r.id) {
15291
- await refreshBuilderCanvasPicker(activeTab === 'crons' ? 'cron' : 'workflow');
15303
+ await refreshBuilderCanvasPicker('workflow');
15292
15304
  await openBuilderWorkflow(r.id);
15293
- toast('Created ' + noun + ': ' + name, 'success');
15305
+ toast('Created workflow: ' + name + (owner ? ' (' + owner + ')' : ''), 'success');
15294
15306
  }
15295
15307
  } catch (err) { toast('Create error: ' + err, 'error'); }
15296
15308
  }
15297
15309
 
15310
+ // Owner picker — populated from /api/agents on first build-tab activation
15311
+ // and refreshed on demand. Empty value = Clementine/global; any other value
15312
+ // is the agent slug for scoped reads/writes.
15313
+ var _builderOwnerPickerLoaded = false;
15314
+ async function populateBuilderOwnerPicker(force) {
15315
+ if (_builderOwnerPickerLoaded && !force) return;
15316
+ var sel = document.getElementById('builder-owner');
15317
+ if (!sel) return;
15318
+ try {
15319
+ var r = await apiFetch('/api/agents');
15320
+ var agents = r.ok ? await r.json() : [];
15321
+ var prev = sel.value;
15322
+ var opts = '<option value="">Clementine (global)</option>';
15323
+ if (Array.isArray(agents)) {
15324
+ for (var i = 0; i < agents.length; i++) {
15325
+ var slug = agents[i] && agents[i].slug;
15326
+ if (!slug) continue;
15327
+ var label = agents[i].name ? (agents[i].name + ' (' + slug + ')') : slug;
15328
+ opts += '<option value="' + esc(slug) + '">' + esc(label) + '</option>';
15329
+ }
15330
+ }
15331
+ sel.innerHTML = opts;
15332
+ if (prev) sel.value = prev;
15333
+ _builderOwnerPickerLoaded = true;
15334
+ } catch (err) {
15335
+ // Leave the default global option in place; not fatal.
15336
+ }
15337
+ }
15338
+
15339
+ // Mirror the visible owner selection into the legacy hidden builder-agent
15340
+ // input + label so chat/skill/agent flows that already read those keep
15341
+ // working, then refresh the canvas picker so the list re-filters.
15342
+ async function onBuilderOwnerChange() {
15343
+ var sel = document.getElementById('builder-owner');
15344
+ var owner = sel ? sel.value : '';
15345
+ var hidden = document.getElementById('builder-agent');
15346
+ var label = document.getElementById('builder-agent-label');
15347
+ if (hidden) hidden.value = owner || '';
15348
+ if (label) label.textContent = owner ? 'Owner: ' + owner : '';
15349
+ var typeSel = document.getElementById('builder-type');
15350
+ var type = typeSel && typeSel.value === 'cron' ? 'cron' : 'workflow';
15351
+ await refreshBuilderCanvasPicker(type);
15352
+ }
15353
+
15298
15354
  // ── Build templates: fork a starter pattern into a new workflow ─────
15299
15355
  async function forkBuildTemplate(templateId) {
15300
15356
  var templates = {
@@ -19503,10 +19559,19 @@ async function refreshBuilderCanvasPicker(type) {
19503
19559
  var picker = document.getElementById('builder-canvas-picker');
19504
19560
  if (!picker) return;
19505
19561
  try {
19562
+ var owner = (document.getElementById('builder-owner') || {}).value || '';
19506
19563
  var r = await apiFetch('/api/builder/workflows');
19507
19564
  var d = await r.json();
19508
- var items = (d.workflows || []).filter(function(w) { return w.origin === type; });
19509
- var opts = '<option value="">' + (items.length ? '— pick a ' + type + ' —' : '(none yet)') + '</option>';
19565
+ var items = (d.workflows || []).filter(function(w) {
19566
+ if (w.origin !== type) return false;
19567
+ // Owner filter: empty owner = Clementine/global only; named owner =
19568
+ // agent-scoped entries for that slug only. The serializer reports
19569
+ // scope='agent' for entries living under <AGENTS_DIR>/<slug>/.
19570
+ if (owner) return w.scope === 'agent' && w.agentSlug === owner;
19571
+ return w.scope !== 'agent';
19572
+ });
19573
+ var ownerLabel = owner ? '@' + owner : 'global';
19574
+ var opts = '<option value="">' + (items.length ? '— pick a ' + type + ' (' + ownerLabel + ') —' : '(none yet for ' + ownerLabel + ')') + '</option>';
19510
19575
  for (var i = 0; i < items.length; i++) {
19511
19576
  var w = items[i];
19512
19577
  var lbl = w.name + (w.schedule ? ' · ' + w.schedule : '') + (w.enabled ? '' : ' · off');
@@ -23,6 +23,7 @@ export declare class HeartbeatScheduler {
23
23
  private lastDenseBackfillAt;
24
24
  private denseBackfillInFlight;
25
25
  private lastSalienceDecayDate;
26
+ private lastMemoryPulseDate;
26
27
  /** Wire up the cron scheduler so daily plan suggestions can be applied. */
27
28
  setCronScheduler(cs: CronScheduler): void;
28
29
  private getLastAgentSiRun;
@@ -53,6 +54,14 @@ export declare class HeartbeatScheduler {
53
54
  * HeartbeatState. Pinned chunks exempt; soft-deleted and superseded skipped.
54
55
  */
55
56
  private maybeRunSalienceDecay;
57
+ /**
58
+ * Weekly Memory Pulse — once-per-week observability report on the memory
59
+ * subsystem. Aggregates the same signals visible on Brain → Health
60
+ * (coverage, recent writes, supersedes, recall contribution) into a
61
+ * compact message and dispatches it. Skipped if nothing meaningful
62
+ * happened this week (avoids empty noise on quiet weeks).
63
+ */
64
+ private maybeSendMemoryPulse;
56
65
  private runInsightCheck;
57
66
  /** Called when user replies to a proactive message — resets cooldown. */
58
67
  recordInsightAcknowledged(): void;
@@ -33,6 +33,7 @@ export class HeartbeatScheduler {
33
33
  lastDenseBackfillAt = 0;
34
34
  denseBackfillInFlight = false;
35
35
  lastSalienceDecayDate = '';
36
+ lastMemoryPulseDate = '';
36
37
  /** Wire up the cron scheduler so daily plan suggestions can be applied. */
37
38
  setCronScheduler(cs) { this.cronScheduler = cs; }
38
39
  getLastAgentSiRun(slug) {
@@ -53,6 +54,8 @@ export class HeartbeatScheduler {
53
54
  this.lastConsolidationDate = this.lastState.lastConsolidationDate;
54
55
  if (this.lastState.lastSalienceDecayDate)
55
56
  this.lastSalienceDecayDate = this.lastState.lastSalienceDecayDate;
57
+ if (this.lastState.lastMemoryPulseDate)
58
+ this.lastMemoryPulseDate = this.lastState.lastMemoryPulseDate;
56
59
  if (this.lastState.lastAgentSiRuns) {
57
60
  this.lastAgentSiRuns = new Map(Object.entries(this.lastState.lastAgentSiRuns));
58
61
  }
@@ -305,6 +308,9 @@ export class HeartbeatScheduler {
305
308
  }).catch(err => {
306
309
  logger.warn({ err }, 'Weekly review failed');
307
310
  });
311
+ // Memory Pulse — weekly observability report on the 5-phase memory
312
+ // system. Skipped if nothing happened (avoids empty noise on quiet weeks).
313
+ this.maybeSendMemoryPulse();
308
314
  }
309
315
  // First Monday of month: monthly assessment (between 8-9 PM)
310
316
  if (now.getDay() === 1 && now.getDate() <= 7 && hour >= 20 && hour < 21) {
@@ -805,6 +811,74 @@ export class HeartbeatScheduler {
805
811
  logger.debug({ err }, 'Salience decay sweep failed (non-fatal)');
806
812
  }
807
813
  }
814
+ /**
815
+ * Weekly Memory Pulse — once-per-week observability report on the memory
816
+ * subsystem. Aggregates the same signals visible on Brain → Health
817
+ * (coverage, recent writes, supersedes, recall contribution) into a
818
+ * compact message and dispatches it. Skipped if nothing meaningful
819
+ * happened this week (avoids empty noise on quiet weeks).
820
+ */
821
+ maybeSendMemoryPulse() {
822
+ const today = todayISO();
823
+ if (this.lastMemoryPulseDate === today)
824
+ return;
825
+ const store = this.gateway.getMemoryStore();
826
+ if (!store)
827
+ return;
828
+ try {
829
+ const stats = store.getMemoryStats();
830
+ const supersedeStats = typeof store.getSupersedeStats === 'function'
831
+ ? store.getSupersedeStats()
832
+ : { superseded: 0 };
833
+ const graphStats = typeof store.getGraphStats === 'function'
834
+ ? store.getGraphStats({ lookbackHours: 24 * 7 })
835
+ : { wikilinkCount: 0, recallContributionByType: {}, tracesAnalyzed: 0 };
836
+ const recentWrites = typeof store.getRecentWrites === 'function'
837
+ ? store
838
+ .getRecentWrites(500)
839
+ : [];
840
+ const weekAgoIso = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
841
+ const writesThisWeek = recentWrites.filter(w => w.extractedAt > weekAgoIso);
842
+ const supersedesThisWeek = writesThisWeek.filter(w => w.status === 'superseded').length;
843
+ const dedupedThisWeek = writesThisWeek.filter(w => w.status === 'dedup_skipped').length;
844
+ const writesWithSalience = writesThisWeek.filter(w => w.salienceHint != null).length;
845
+ // Skip if nothing happened (no writes, no traces) — don't spam empty reports
846
+ if (writesThisWeek.length === 0 && graphStats.tracesAnalyzed === 0)
847
+ return;
848
+ const coveragePct = stats.totalChunks > 0
849
+ ? Math.round((stats.chunksWithDenseEmbeddings / stats.totalChunks) * 100)
850
+ : 0;
851
+ const cb = graphStats.recallContributionByType;
852
+ const totalMatches = Object.values(cb).reduce((a, b) => a + b, 0);
853
+ const pctOf = (n) => totalMatches > 0 ? Math.round((n / totalMatches) * 100) : 0;
854
+ const lines = [
855
+ `**Memory Pulse — last 7 days**`,
856
+ ``,
857
+ `**Coverage:** ${coveragePct}% semantic (${stats.chunksWithDenseEmbeddings.toLocaleString()}/${stats.totalChunks.toLocaleString()} chunks)`,
858
+ ];
859
+ if (writesThisWeek.length > 0) {
860
+ lines.push(`**Writes:** ${writesThisWeek.length} captured`
861
+ + (writesWithSalience > 0 ? `, ${writesWithSalience} with salience hint` : '')
862
+ + (dedupedThisWeek > 0 ? `, ${dedupedThisWeek} reinforced` : ''));
863
+ }
864
+ if (supersedesThisWeek > 0 || supersedeStats.superseded > 0) {
865
+ lines.push(`**Self-correction:** ${supersedesThisWeek} this week, ${supersedeStats.superseded} all-time`);
866
+ }
867
+ if (graphStats.tracesAnalyzed > 0 && totalMatches > 0) {
868
+ lines.push(`**Recall mix:** ${pctOf(cb.fts ?? 0)}% lexical · ${pctOf(cb.vector ?? 0)}% semantic · ${pctOf(cb.graph ?? 0)}% graph · ${pctOf(cb.recency ?? 0)}% recent`);
869
+ }
870
+ lines.push(``);
871
+ lines.push(`Full breakdown on the dashboard: Brain → Health.`);
872
+ this.dispatcher.send(lines.join('\n')).catch(err => logger.debug({ err }, 'Failed to send memory pulse'));
873
+ this.lastMemoryPulseDate = today;
874
+ this.lastState.lastMemoryPulseDate = today;
875
+ this.saveState();
876
+ logger.info({ coveragePct, writesThisWeek: writesThisWeek.length, supersedesThisWeek }, 'Memory Pulse sent');
877
+ }
878
+ catch (err) {
879
+ logger.debug({ err }, 'Memory Pulse failed (non-fatal)');
880
+ }
881
+ }
808
882
  async runInsightCheck() {
809
883
  // Initialize insight state if needed
810
884
  if (!this.lastState.insightState) {
package/dist/types.d.ts CHANGED
@@ -216,6 +216,7 @@ export interface HeartbeatState {
216
216
  lastAgentSiRuns?: Record<string, string>;
217
217
  lastSkillDecayDate?: string;
218
218
  lastSalienceDecayDate?: string;
219
+ lastMemoryPulseDate?: string;
219
220
  /** Proactive insight engine state */
220
221
  insightState?: {
221
222
  sentToday: string[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",