clementine-agent 1.18.83 → 1.18.84

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.
@@ -70,8 +70,6 @@ export declare class PersonalAssistant {
70
70
  private memoryStore;
71
71
  private _lastUserMessage?;
72
72
  onSkillProposed: ((skill: import('../types.js').SkillDocument) => void) | null;
73
- private _lastMcpStatus;
74
- private _lastMcpStatusTime;
75
73
  /** Terminal reason from the last SDK query — consumed by cron scheduler for precise error classification. */
76
74
  private _lastTerminalReason?;
77
75
  /** Per-session stall nudge — set after a query shows stall signals, consumed on the next query. */
@@ -17,6 +17,7 @@ import { BASE_DIR, PKG_DIR, VAULT_DIR, DAILY_NOTES_DIR, SOUL_FILE, AGENTS_FILE,
17
17
  import { summarizeIntegrationStatus } from '../config/integrations-registry.js';
18
18
  import { loadToolPreferences, computeAvailability, buildPromptInstruction, buildComposioStatusBlock, KNOWN_SERVICES, } from '../integrations/tool-preferences.js';
19
19
  import { loadClaudeIntegrations } from './mcp-bridge.js';
20
+ import { getLatestMcpStatusSnapshot, recordMcpStatusFromSystemInit, invalidateMcpStatusEntry } from './run-agent.js';
20
21
  import { detectFrustrationSignals, detectRepeatedTopics } from './insight-engine.js';
21
22
  import { DEFAULT_CHANNEL_CAPABILITIES } from '../types.js';
22
23
  import { enforceToolPermissions, getSecurityPrompt, getHeartbeatSecurityPrompt, getCronSecurityPrompt, getHeartbeatDisallowedTools, logToolUse, logAuditJsonl, } from './hooks.js';
@@ -710,8 +711,10 @@ export class PersonalAssistant {
710
711
  memoryStore = null; // Typed as any — MemoryStore may not be available yet
711
712
  _lastUserMessage;
712
713
  onSkillProposed = null;
713
- _lastMcpStatus = [];
714
- _lastMcpStatusTime = '';
714
+ // PRD Phase 2 / 1.18.84: superseded by the shared module-level cache in
715
+ // run-agent.ts (getLatestMcpStatusSnapshot / recordMcpStatusFromSystemInit).
716
+ // The fields below were declared but never written pre-1.18.84; the
717
+ // module cache populates from every system/init message instead.
715
718
  /** Terminal reason from the last SDK query — consumed by cron scheduler for precise error classification. */
716
719
  _lastTerminalReason;
717
720
  /** Per-session stall nudge — set after a query shows stall signals, consumed on the next query. */
@@ -808,7 +811,12 @@ export class PersonalAssistant {
808
811
  this.onSkillProposed = cb;
809
812
  }
810
813
  getMcpStatus() {
811
- return { servers: this._lastMcpStatus, updatedAt: this._lastMcpStatusTime };
814
+ // 1.18.84 correctness fix: delegate to the shared module-level cache.
815
+ // Pre-1.18.84 we returned this._lastMcpStatus, which was declared but
816
+ // never written — getMcpStatus() always returned empty. Now run-agent
817
+ // and assistant query streams record into a shared snapshot via
818
+ // recordMcpStatusFromSystemInit when the SDK init message lands.
819
+ return getLatestMcpStatusSnapshot();
812
820
  }
813
821
  /**
814
822
  * PRD Phase 2.1: clear the cached status for one server so the next query
@@ -818,12 +826,9 @@ export class PersonalAssistant {
818
826
  * stale error/auth state. Returns the post-clear cached snapshot.
819
827
  */
820
828
  invalidateMcpStatus(serverName) {
821
- const beforeLen = this._lastMcpStatus.length;
822
- this._lastMcpStatus = this._lastMcpStatus.filter((s) => s.name !== serverName);
823
- const cleared = this._lastMcpStatus.length < beforeLen;
824
- if (cleared)
825
- this._lastMcpStatusTime = new Date().toISOString();
826
- return { servers: this._lastMcpStatus, updatedAt: this._lastMcpStatusTime, cleared };
829
+ const result = invalidateMcpStatusEntry(serverName);
830
+ const snapshot = getLatestMcpStatusSnapshot();
831
+ return { servers: snapshot.servers, updatedAt: snapshot.updatedAt, cleared: result.cleared };
827
832
  }
828
833
  /** Inject a background work result into the session as silent follow-up context. */
829
834
  injectPendingContext(sessionKey, userPrompt, result) {
@@ -2999,6 +3004,15 @@ You have a cost budget per message — not a hard turn limit. Work until the tas
2999
3004
  const trace = [];
3000
3005
  const stream = query({ prompt, options: sdkOptions });
3001
3006
  for await (const message of stream) {
3007
+ // 1.18.84 correctness: capture MCP server status from SDK init.
3008
+ if (message.type === 'system' && message.subtype === 'init') {
3009
+ const mcpServersRaw = message.mcp_servers;
3010
+ if (mcpServersRaw)
3011
+ try {
3012
+ recordMcpStatusFromSystemInit(mcpServersRaw);
3013
+ }
3014
+ catch { /* non-fatal */ }
3015
+ }
3002
3016
  if (message.type === 'assistant') {
3003
3017
  const blocks = getContentBlocks(message);
3004
3018
  for (const block of blocks) {
@@ -22,6 +22,21 @@
22
22
  * long-task preflight, NO mode=unleashed wrapper.
23
23
  */
24
24
  import { type AgentDefinition } from '@anthropic-ai/claude-agent-sdk';
25
+ /** Read the latest MCP status snapshot. Safe to call from any module. */
26
+ export declare function getLatestMcpStatusSnapshot(): {
27
+ servers: Array<{
28
+ name: string;
29
+ status: string;
30
+ }>;
31
+ updatedAt: string;
32
+ };
33
+ /** Write a fresh snapshot. Called from system/init handlers. */
34
+ export declare function recordMcpStatusFromSystemInit(rawMcpServers: unknown): void;
35
+ /** Drop one server from the cache so the next query repopulates it. */
36
+ export declare function invalidateMcpStatusEntry(name: string): {
37
+ cleared: boolean;
38
+ updatedAt: string;
39
+ };
25
40
  import type { AgentProfile } from '../types.js';
26
41
  import type { AgentManager } from './agent-manager.js';
27
42
  import type { MemoryStore } from '../memory/store.js';
@@ -24,6 +24,49 @@
24
24
  import path from 'node:path';
25
25
  import { query } from '@anthropic-ai/claude-agent-sdk';
26
26
  import pino from 'pino';
27
+ /**
28
+ * Module-level cache of MCP server statuses from the most recent SDK
29
+ * init message. Populated by every runAgent / PersonalAssistant query
30
+ * stream that captures `system/init`. Read by Assistant.getMcpStatus()
31
+ * and the dashboard's Tools & MCP catalog page.
32
+ *
33
+ * Pre-1.18.84 the assistant declared a private _lastMcpStatus but no
34
+ * code wrote to it — getMcpStatus() always returned empty, making the
35
+ * catalog status pills misleading. The shared module cache fixes that
36
+ * without coupling assistant.ts to runAgent's stream loop.
37
+ */
38
+ let _lastMcpStatusSnapshot = {
39
+ servers: [],
40
+ updatedAt: '',
41
+ };
42
+ /** Read the latest MCP status snapshot. Safe to call from any module. */
43
+ export function getLatestMcpStatusSnapshot() {
44
+ return { servers: [..._lastMcpStatusSnapshot.servers], updatedAt: _lastMcpStatusSnapshot.updatedAt };
45
+ }
46
+ /** Write a fresh snapshot. Called from system/init handlers. */
47
+ export function recordMcpStatusFromSystemInit(rawMcpServers) {
48
+ if (!Array.isArray(rawMcpServers))
49
+ return;
50
+ const servers = [];
51
+ for (const entry of rawMcpServers) {
52
+ if (!entry || typeof entry !== 'object')
53
+ continue;
54
+ const e = entry;
55
+ if (typeof e.name !== 'string' || !e.name)
56
+ continue;
57
+ servers.push({ name: e.name, status: typeof e.status === 'string' ? e.status : 'unknown' });
58
+ }
59
+ _lastMcpStatusSnapshot = { servers, updatedAt: new Date().toISOString() };
60
+ }
61
+ /** Drop one server from the cache so the next query repopulates it. */
62
+ export function invalidateMcpStatusEntry(name) {
63
+ const before = _lastMcpStatusSnapshot.servers.length;
64
+ _lastMcpStatusSnapshot = {
65
+ servers: _lastMcpStatusSnapshot.servers.filter((s) => s.name !== name),
66
+ updatedAt: new Date().toISOString(),
67
+ };
68
+ return { cleared: _lastMcpStatusSnapshot.servers.length < before, updatedAt: _lastMcpStatusSnapshot.updatedAt };
69
+ }
27
70
  import { BASE_DIR, PKG_DIR, CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY as CONFIG_ANTHROPIC_API_KEY, normalizeClaudeSdkOptionsForOneMillionContext, } from '../config.js';
28
71
  import { buildAgentMap } from './agent-definitions.js';
29
72
  const MCP_SERVER_SCRIPT = path.join(PKG_DIR, 'dist', 'tools', 'mcp-server.js');
@@ -229,6 +272,17 @@ export async function runAgent(prompt, opts) {
229
272
  for await (const message of stream) {
230
273
  if (message.type === 'system' && message.subtype === 'init') {
231
274
  sessionId = message.session_id ?? '';
275
+ // PRD Phase 2 / 1.18.84 correctness: capture the SDK-reported MCP
276
+ // server status so getMcpStatus() (and the dashboard's Tools & MCP
277
+ // catalog) actually has data. The init message includes
278
+ // mcp_servers: Array<{ name, status }> per the SDK protocol.
279
+ const mcpServersRaw = message.mcp_servers;
280
+ if (mcpServersRaw) {
281
+ try {
282
+ recordMcpStatusFromSystemInit(mcpServersRaw);
283
+ }
284
+ catch { /* non-fatal */ }
285
+ }
232
286
  logger.debug({ sessionKey: opts.sessionKey, sdkSessionId: sessionId }, 'runAgent: SDK session initialized');
233
287
  continue;
234
288
  }
package/dist/cli/cron.js CHANGED
@@ -140,6 +140,10 @@ export async function cmdCronRun(jobName) {
140
140
  try {
141
141
  const response = await gateway.handleCronJob(job.name, job.prompt, job.tier, job.maxTurns, job.model, job.workDir, job.mode, job.maxHours);
142
142
  const finishedAt = new Date();
143
+ // 1.18.84: trigger source comes from the CRON_RUN_TRIGGER env var set
144
+ // by the dashboard's manual-run endpoint. Defaults to 'scheduled' for
145
+ // any other invocation path (CLI, daemon-internal callsites).
146
+ const trigger = process.env.CRON_RUN_TRIGGER || 'scheduled';
143
147
  const entry = {
144
148
  jobName: job.name,
145
149
  startedAt: startedAt.toISOString(),
@@ -148,6 +152,7 @@ export async function cmdCronRun(jobName) {
148
152
  durationMs: finishedAt.getTime() - startedAt.getTime(),
149
153
  attempt: 1,
150
154
  outputPreview: response ? response.slice(0, 200) : undefined,
155
+ trigger,
151
156
  };
152
157
  // PRD Phase 1.1: goal-orientation evaluator (mirrors the daemon path).
153
158
  if (job.successSchema || (job.successCriteriaText && job.successCriteriaText.trim())) {
@@ -170,6 +175,7 @@ export async function cmdCronRun(jobName) {
170
175
  }
171
176
  catch (err) {
172
177
  const finishedAt = new Date();
178
+ const trigger = process.env.CRON_RUN_TRIGGER || 'scheduled';
173
179
  runLog.append({
174
180
  jobName: job.name,
175
181
  startedAt: startedAt.toISOString(),
@@ -179,6 +185,7 @@ export async function cmdCronRun(jobName) {
179
185
  error: String(err).slice(0, 500),
180
186
  errorType: classifyError(err),
181
187
  attempt: 1,
188
+ trigger,
182
189
  });
183
190
  console.error(`Error: ${err}`);
184
191
  process.exit(1);
@@ -4613,7 +4613,8 @@ export async function cmdDashboard(opts) {
4613
4613
  detached: true,
4614
4614
  stdio: ['ignore', 'pipe', 'pipe'],
4615
4615
  cwd: BASE_DIR,
4616
- env: { ...process.env, CLEMENTINE_HOME: BASE_DIR },
4616
+ // 1.18.84: pass the trigger source so cron.ts stamps it on the run entry.
4617
+ env: { ...process.env, CLEMENTINE_HOME: BASE_DIR, CRON_RUN_TRIGGER: 'manual' },
4617
4618
  });
4618
4619
  // Capture stderr for error reporting
4619
4620
  let stderr = '';
@@ -23811,12 +23812,13 @@ function renderRunListBody(allRuns) {
23811
23812
  var startedAt = entry.startedAt ? new Date(entry.startedAt) : null;
23812
23813
  var startedLabel = startedAt ? startedAt.toLocaleString() : '—';
23813
23814
  var durationLabel = entry.durationMs != null ? formatDurationMs(entry.durationMs) : '—';
23814
- // Trigger heuristic until we persist it for real (Phase 3.1):
23815
- // 'unleashed' mode + manual cron run = manual; otherwise scheduled.
23816
- // The cron-running.json sidecar already carries pid which can hint at
23817
- // manual but isn't on terminal entries. Best-effort label only.
23818
- var triggerLabel = entry.attempt > 1 ? 'retry' : 'scheduled';
23819
- var triggerColor = 'var(--text-muted)';
23815
+ // 1.18.84: real persisted trigger field. Falls back to a heuristic for
23816
+ // pre-1.18.84 run entries that don't have the field set.
23817
+ var triggerLabel = entry.trigger || (entry.attempt > 1 ? 'retry' : 'scheduled');
23818
+ var triggerColor = entry.trigger === 'manual' ? 'var(--accent)'
23819
+ : entry.trigger === 'after' ? 'var(--purple)'
23820
+ : entry.trigger === 'discord' ? 'var(--blue)'
23821
+ : 'var(--text-muted)';
23820
23822
  // Goal cell
23821
23823
  var goalCell = '<div></div>';
23822
23824
  if (entry.goalCheck) {
@@ -23932,7 +23934,14 @@ async function refreshToolsMcpCatalog() {
23932
23934
  try {
23933
23935
  var sR = await apiFetch('/api/mcp-status');
23934
23936
  var statusJson = await sR.json();
23935
- statusMap = statusJson || {};
23937
+ // /api/mcp-status returns { servers: [{name, status}], updatedAt }.
23938
+ // Build a name → entry lookup so renderMcpCatalogCard can probe by name.
23939
+ if (statusJson && Array.isArray(statusJson.servers)) {
23940
+ for (var si = 0; si < statusJson.servers.length; si++) {
23941
+ var entry = statusJson.servers[si];
23942
+ if (entry && entry.name) statusMap[entry.name] = entry;
23943
+ }
23944
+ }
23936
23945
  } catch (e) { /* status is optional — servers still render without it */ }
23937
23946
  try {
23938
23947
  var lR = await apiFetch('/api/mcp-servers');
@@ -888,7 +888,7 @@ export class CronScheduler {
888
888
  ctx.agentSlug = wf.agentSlug;
889
889
  return ctx;
890
890
  }
891
- async runJob(job) {
891
+ async runJob(job, trigger = 'scheduled') {
892
892
  const creditBlock = getBackgroundCreditBlock();
893
893
  if (creditBlock) {
894
894
  logger.warn({ job: job.name, until: creditBlock.until }, 'Cron job skipped — Claude credit block active');
@@ -1206,6 +1206,8 @@ export class CronScheduler {
1206
1206
  outputPreview: response ? response.slice(0, 200) : undefined,
1207
1207
  advisorApplied,
1208
1208
  terminalReason,
1209
+ // 1.18.84: persist the actual trigger source for the Run list filter.
1210
+ trigger,
1209
1211
  // Trick capability metadata — surfaced by the dashboard's
1210
1212
  // "ran with: …" line. Omit empty arrays to keep the JSONL light.
1211
1213
  ...(cronMetadata?.skillsApplied?.length ? { skillsApplied: cronMetadata.skillsApplied } : {}),
@@ -1268,7 +1270,8 @@ export class CronScheduler {
1268
1270
  const dependents = this.jobs.filter(j => j.after === job.name && j.enabled && !this.disabledJobs.has(j.name));
1269
1271
  for (const dep of dependents) {
1270
1272
  logger.info(`Chain: '${job.name}' succeeded — triggering '${dep.name}'`);
1271
- this.runJob(dep).catch((err) => {
1273
+ // 1.18.84: chained-after triggers carry trigger='after' for the Run list filter.
1274
+ this.runJob(dep, 'after').catch((err) => {
1272
1275
  logger.error({ err, job: dep.name }, `Chained job '${dep.name}' failed`);
1273
1276
  });
1274
1277
  }
package/dist/types.d.ts CHANGED
@@ -447,6 +447,11 @@ export interface CronRunEntry {
447
447
  allowedToolsApplied?: string[];
448
448
  /** MCP servers live for this run (post profile + trick allowlist intersection). */
449
449
  mcpServersApplied?: string[];
450
+ /** PRD §6 / 1.18.84: how this run was triggered. Persisted by the
451
+ * scheduler (cron tick / chained 'after' / manual-run endpoint /
452
+ * Discord) so the Run list can filter by source instead of guessing
453
+ * via heuristics on attempt count. */
454
+ trigger?: 'manual' | 'scheduled' | 'webhook' | 'api' | 'fork' | 'resume' | 'discord' | 'after';
450
455
  /** PRD Phase 1: did the run accomplish what it was supposed to?
451
456
  * Computed at run-end when the Task has successSchema or successCriteriaText.
452
457
  * - status='pass' both configured checks passed (or the only one configured did)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.83",
3
+ "version": "1.18.84",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",