@lightcone-ai/daemon 0.15.57 → 0.15.59

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.
@@ -4,6 +4,7 @@ export const DEFAULT_LIMIT = 200;
4
4
  export const MAX_LIMIT = 1000;
5
5
 
6
6
  const READ_QUERY_PREFIX = /^(SELECT|SHOW|DESCRIBE|DESC|EXPLAIN|WITH)\b/i;
7
+ const AUTO_LIMIT_PREFIX = /^(SELECT|WITH)\b/i;
7
8
  const REQUIRED_DB_KEYS = ['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'];
8
9
 
9
10
  function trimTrailingSemicolons(sql) {
@@ -27,6 +28,10 @@ export function isReadQuery(sql) {
27
28
  return READ_QUERY_PREFIX.test(sql);
28
29
  }
29
30
 
31
+ export function supportsAutoLimit(sql) {
32
+ return AUTO_LIMIT_PREFIX.test(sql);
33
+ }
34
+
30
35
  export function hasLimitClause(sql) {
31
36
  return /\bLIMIT\b/i.test(sql);
32
37
  }
@@ -36,7 +41,7 @@ export function prepareSql(sql, limit) {
36
41
  if (!normalizedSql) throw new Error('sql must be a non-empty string');
37
42
 
38
43
  const effectiveLimit = normalizeLimit(limit);
39
- if (!isReadQuery(normalizedSql) || hasLimitClause(normalizedSql)) {
44
+ if (!supportsAutoLimit(normalizedSql) || hasLimitClause(normalizedSql)) {
40
45
  return {
41
46
  sql: normalizedSql,
42
47
  limit: effectiveLimit,
@@ -138,13 +138,14 @@ export async function callOfficialTool({
138
138
  toolName,
139
139
  argumentsPayload = {},
140
140
  timeoutMs = 10000,
141
+ env = {},
141
142
  }) {
142
- const entrypointPath = resolveMcpServerEntrypoint(serverId, { strict: true });
143
+ const entrypointPath = resolveMcpServerEntrypoint(serverId);
143
144
  if (!entrypointPath) {
144
145
  throw new Error(`official_server_not_found:${serverId}`);
145
146
  }
146
147
 
147
- const client = new StdioJsonRpcClient(process.execPath, [entrypointPath]);
148
+ const client = new StdioJsonRpcClient(process.execPath, [entrypointPath], env);
148
149
  try {
149
150
  await client.start();
150
151
  await client.request('initialize', {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightcone-ai/daemon",
3
- "version": "0.15.57",
3
+ "version": "0.15.59",
4
4
  "type": "module",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -183,6 +183,7 @@ export class AgentManager {
183
183
  switch (msg.type) {
184
184
  case 'agent:start': return this._startAgent(msg, connection);
185
185
  case 'agent:stop': return this._stopAgent(msg.agentId, msg.workspaceId, connection);
186
+ case 'agent:cancel_turn': return this._cancelAgentTurn(msg.agentId, msg.workspaceId, connection);
186
187
  case 'agent:deliver': return this._deliverMessage(msg, connection);
187
188
  case 'agent:reprobe': return this._reprobeCapability(msg, connection);
188
189
  case 'publish:job': return this._handlePublishJob(msg, connection);
@@ -1714,6 +1715,61 @@ export class AgentManager {
1714
1715
  // exit handler will report status
1715
1716
  }
1716
1717
 
1718
+ _cancelAgentTurn(agentId, workspaceId, connection) {
1719
+ const key = this._key(agentId, workspaceId);
1720
+ const queued = this._pendingMessages?.get(key)?.length ?? 0;
1721
+ if (queued > 0) {
1722
+ this._pendingMessages.delete(key);
1723
+ }
1724
+
1725
+ const agent = this.agents.get(key);
1726
+ if (!agent?.proc) {
1727
+ console.log(`[AgentManager] Cancel turn requested for inactive agent ${agentId} workspace=${workspaceId ?? 'none'} queued_cleared=${queued}`);
1728
+ connection.send({
1729
+ type: 'agent:cancel_turn:ack',
1730
+ agentId,
1731
+ workspaceId,
1732
+ ok: queued > 0,
1733
+ queuedCleared: queued,
1734
+ running: false,
1735
+ });
1736
+ return;
1737
+ }
1738
+
1739
+ console.log(`[AgentManager] Cancelling current turn for agent ${agentId} workspace=${workspaceId ?? 'none'} queued_cleared=${queued}`);
1740
+ agent.stopCause = 'user_cancelled';
1741
+ connection.send({
1742
+ type: 'agent:cancel_turn:ack',
1743
+ agentId,
1744
+ workspaceId,
1745
+ ok: true,
1746
+ queuedCleared: queued,
1747
+ running: true,
1748
+ });
1749
+ this._emitLifecycle(connection, {
1750
+ agentId,
1751
+ workspaceId,
1752
+ runtime: agent.runtime ?? 'claude',
1753
+ reachability: 'reachable',
1754
+ availability: 'available',
1755
+ runtimeState: 'running',
1756
+ reason: 'user_cancelled',
1757
+ sessionId: agent.sessionId ?? null,
1758
+ });
1759
+
1760
+ try {
1761
+ agent.proc.kill('SIGINT');
1762
+ } catch (err) {
1763
+ console.error(`[AgentManager] cancel SIGINT failed for ${key}:`, err?.message ?? err);
1764
+ }
1765
+ setTimeout(() => {
1766
+ if (this.agents.get(key) !== agent) return;
1767
+ try {
1768
+ agent.proc.kill('SIGTERM');
1769
+ } catch {}
1770
+ }, 1500);
1771
+ }
1772
+
1717
1773
  _deliverMessage(msg, connection) {
1718
1774
  const { agentId, workspaceId, seq, message } = msg;
1719
1775
  const key = this._key(agentId, workspaceId);
@@ -141,6 +141,7 @@ const DEFAULT_TOOL_CLASSIFICATION = {
141
141
  write_governance_correction: 'local',
142
142
  get_orchestrate_context: 'local',
143
143
  complete_orchestrate_trigger: 'local',
144
+ propose_goal_candidate: 'local',
144
145
 
145
146
  send_message: 'mandatory',
146
147
  create_tasks: 'mandatory',
@@ -302,6 +303,7 @@ function inferToolForApi(method, apiPath, body) {
302
303
  if (method === 'POST' && cleanPath === '/orchestrate/correction') return 'write_governance_correction';
303
304
  if (method === 'GET' && cleanPath === '/orchestrate/context') return 'get_orchestrate_context';
304
305
  if (method === 'POST' && cleanPath === '/orchestrate/complete') return 'complete_orchestrate_trigger';
306
+ if (method === 'POST' && cleanPath === '/orchestrate/propose-goal-candidate') return 'propose_goal_candidate';
305
307
  if (method === 'POST' && cleanPath === '/context-proposals') return 'promote_context';
306
308
 
307
309
  if (method === 'GET' && cleanPath === '/host/scenarios/search') return 'search_scenarios';
@@ -1747,6 +1749,49 @@ server.tool('get_orchestrate_context',
1747
1749
  }
1748
1750
  );
1749
1751
 
1752
+ // ── propose_goal_candidate ────────────────────────────────────────────────────
1753
+ server.tool('propose_goal_candidate',
1754
+ 'Orchestrator-only: propose a goal_state_candidate for human review. Use when D1 (goal_extraction_opportunity) fires and you have extracted a goal/constraint shape from the user\'s recent messages. The candidate enters the existing candidate→confirm→promote flow; ONLY Human can promote it to active goal_states. Always include `source_excerpts` (verbatim user quotes the proposal is derived from) so the confirm card can show provenance.',
1755
+ {
1756
+ workspace_id: z.string().describe('Target workspace id; you must be the registered orchestrator for it.'),
1757
+ goal: z.record(z.any()).describe('Proposed goal object. Required: a non-empty object describing the workspace goal, e.g. { title, description, kpis, ... }. Be concrete — vague platitudes like "comply with brand" are useless.'),
1758
+ constraints: z.array(z.any()).optional().describe('Proposed constraints array (optional). Each item should be enforceable by detector A4 / worker review, not a slogan.'),
1759
+ current_phase: z.string().optional().describe('Current phase label (optional, e.g. "exploration" / "stable_publish").'),
1760
+ source_excerpts: z.array(z.string()).describe('Verbatim user quotes the proposal is derived from. REQUIRED for non-trivial proposals — the confirm card surfaces these to the user as provenance.'),
1761
+ source_risk: z.enum(['trusted', 'normal', 'untrusted', 'hostile']).optional().describe('Source risk classification: "trusted" if quoting user directly, "normal" if interpreting user intent, "untrusted" if drawing from worker output / external content. Default normal.'),
1762
+ trigger_id: z.string().optional().describe('Originating trigger id, e.g. "D1". Default "D1".'),
1763
+ detector_version: z.string().optional().describe('Detector version that fired, e.g. "rule-v1".'),
1764
+ reason: z.string().optional().describe('Human-readable rationale for the proposal.'),
1765
+ expires_in_minutes: z.number().int().optional().describe('Candidate TTL in minutes (default 30, max 7 days).'),
1766
+ },
1767
+ async ({ workspace_id, goal, constraints, current_phase, source_excerpts, source_risk, trigger_id, detector_version, reason, expires_in_minutes }) => {
1768
+ try {
1769
+ const data = await api('POST', '/orchestrate/propose-goal-candidate', {
1770
+ workspaceId: workspace_id,
1771
+ goal,
1772
+ constraints,
1773
+ currentPhase: current_phase,
1774
+ sourceExcerpts: source_excerpts,
1775
+ sourceRisk: source_risk,
1776
+ triggerId: trigger_id,
1777
+ detectorVersion: detector_version,
1778
+ reason,
1779
+ expiresInMinutes: expires_in_minutes,
1780
+ });
1781
+ const c = data?.candidate;
1782
+ return {
1783
+ content: [{
1784
+ type: 'text',
1785
+ text: `Goal candidate proposed. id=${c?.id} status=${c?.status} expires_at=${c?.expiresAt}\n`
1786
+ + `Awaiting human confirm via UI; no active goal_states change yet.`,
1787
+ }],
1788
+ };
1789
+ } catch (err) {
1790
+ return { isError: true, content: [{ type: 'text', text: `Error: ${err.message}` }] };
1791
+ }
1792
+ }
1793
+ );
1794
+
1750
1795
  // ── complete_orchestrate_trigger ──────────────────────────────────────────────
1751
1796
  server.tool('complete_orchestrate_trigger',
1752
1797
  'Orchestrator-only: mark a processing orchestrator trigger as completed (status processing→completed) and reset its circuit breaker. Call this once you have written all governance actions for the current trigger.',
@@ -1,5 +1,6 @@
1
1
  export function resolveExitOfflineDetail({ code, signal, stopCause }) {
2
2
  if (stopCause === 'manual_stop') return '';
3
+ if (stopCause === 'user_cancelled') return '';
3
4
  if (stopCause === 'credential_revoked') return 'credential_revoked';
4
5
  if (stopCause === 'contract_violation') return 'contract_violation:visible_text_without_send_message';
5
6
  if (stopCause === 'outbound_rate_limited') return 'outbound_rate_limited';
@@ -27,6 +28,14 @@ export function resolveLifecycleExitState({ runtime, code, signal, stopCause })
27
28
  reason: 'manual_stop',
28
29
  };
29
30
  }
31
+ if (stopCause === 'user_cancelled') {
32
+ return {
33
+ reachability: 'reachable',
34
+ availability: 'available',
35
+ runtimeState: 'stopped',
36
+ reason: 'user_cancelled',
37
+ };
38
+ }
30
39
  if (stopCause === 'contract_violation') {
31
40
  return {
32
41
  reachability: 'reachable',
@@ -608,7 +608,14 @@ export async function runPublishJob({ serverUrl, machineApiKey, agentId, workspa
608
608
  text,
609
609
  tags,
610
610
  payload,
611
- callTool: callOfficialTool,
611
+ callTool: (toolArgs) => callOfficialTool({
612
+ ...toolArgs,
613
+ env: {
614
+ SERVER_URL: serverUrl,
615
+ MACHINE_API_KEY: machineApiKey,
616
+ AGENT_ID: agentId,
617
+ },
618
+ }),
612
619
  });
613
620
  } catch (error) {
614
621
  emitPublishJobProgress(onProgress, 'precheck_error', {