reflectt-node 0.1.14 → 0.1.16
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/README.md +54 -0
- package/defaults/TEAM-ROLES.starter.yaml +87 -0
- package/defaults/lane-templates/ops.json +32 -0
- package/defaults/lane-templates/workflow.json +32 -0
- package/defaults/reviewer-routing.yaml +34 -0
- package/dist/activationEvents.d.ts +7 -1
- package/dist/activationEvents.d.ts.map +1 -1
- package/dist/activationEvents.js +29 -3
- package/dist/activationEvents.js.map +1 -1
- package/dist/activity-stream-normalizer.d.ts +37 -0
- package/dist/activity-stream-normalizer.d.ts.map +1 -0
- package/dist/activity-stream-normalizer.js +101 -0
- package/dist/activity-stream-normalizer.js.map +1 -0
- package/dist/agent-exec-guardrail.d.ts +9 -0
- package/dist/agent-exec-guardrail.d.ts.map +1 -0
- package/dist/agent-exec-guardrail.js +24 -0
- package/dist/agent-exec-guardrail.js.map +1 -0
- package/dist/agent-exec-guardrail.test.d.ts +2 -0
- package/dist/agent-exec-guardrail.test.d.ts.map +1 -0
- package/dist/agent-exec-guardrail.test.js +55 -0
- package/dist/agent-exec-guardrail.test.js.map +1 -0
- package/dist/agent-interface.d.ts +137 -0
- package/dist/agent-interface.d.ts.map +1 -0
- package/dist/agent-interface.js +463 -0
- package/dist/agent-interface.js.map +1 -0
- package/dist/agent-notifications.d.ts +51 -0
- package/dist/agent-notifications.d.ts.map +1 -0
- package/dist/agent-notifications.js +104 -0
- package/dist/agent-notifications.js.map +1 -0
- package/dist/agent-runs.d.ts +31 -7
- package/dist/agent-runs.d.ts.map +1 -1
- package/dist/agent-runs.js +137 -28
- package/dist/agent-runs.js.map +1 -1
- package/dist/artifact-mirror.d.ts.map +1 -1
- package/dist/artifact-mirror.js +4 -1
- package/dist/artifact-mirror.js.map +1 -1
- package/dist/assignment.d.ts.map +1 -1
- package/dist/assignment.js +54 -2
- package/dist/assignment.js.map +1 -1
- package/dist/boardHealthWorker.d.ts.map +1 -1
- package/dist/boardHealthWorker.js +15 -1
- package/dist/boardHealthWorker.js.map +1 -1
- package/dist/canvas-auto-state.d.ts +58 -0
- package/dist/canvas-auto-state.d.ts.map +1 -0
- package/dist/canvas-auto-state.js +89 -0
- package/dist/canvas-auto-state.js.map +1 -0
- package/dist/canvas-routes.d.ts +36 -0
- package/dist/canvas-routes.d.ts.map +1 -0
- package/dist/canvas-routes.js +47 -0
- package/dist/canvas-routes.js.map +1 -0
- package/dist/capability-readiness.d.ts +28 -0
- package/dist/capability-readiness.d.ts.map +1 -0
- package/dist/capability-readiness.js +162 -0
- package/dist/capability-readiness.js.map +1 -0
- package/dist/channels.d.ts.map +1 -1
- package/dist/channels.js +1 -0
- package/dist/channels.js.map +1 -1
- package/dist/cli.js +179 -4
- package/dist/cli.js.map +1 -1
- package/dist/cloud.d.ts +5 -0
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +485 -18
- package/dist/cloud.js.map +1 -1
- package/dist/comms-routing-policy.d.ts +31 -0
- package/dist/comms-routing-policy.d.ts.map +1 -0
- package/dist/comms-routing-policy.js +128 -0
- package/dist/comms-routing-policy.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/continuity-loop.d.ts.map +1 -1
- package/dist/continuity-loop.js +26 -0
- package/dist/continuity-loop.js.map +1 -1
- package/dist/cost-enforcement.d.ts.map +1 -1
- package/dist/cost-enforcement.js +22 -0
- package/dist/cost-enforcement.js.map +1 -1
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +56 -5
- package/dist/db.js.map +1 -1
- package/dist/doctor.js +2 -2
- package/dist/e2e-loop-proof.test.js +11 -1
- package/dist/e2e-loop-proof.test.js.map +1 -1
- package/dist/events.d.ts +4 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +22 -1
- package/dist/events.js.map +1 -1
- package/dist/executionSweeper.d.ts.map +1 -1
- package/dist/executionSweeper.js +155 -0
- package/dist/executionSweeper.js.map +1 -1
- package/dist/health.d.ts +21 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +164 -19
- package/dist/health.js.map +1 -1
- package/dist/inbox.d.ts +4 -0
- package/dist/inbox.d.ts.map +1 -1
- package/dist/inbox.js +38 -1
- package/dist/inbox.js.map +1 -1
- package/dist/index.js +90 -14
- package/dist/index.js.map +1 -1
- package/dist/insight-auto-tagger.d.ts +58 -0
- package/dist/insight-auto-tagger.d.ts.map +1 -0
- package/dist/insight-auto-tagger.js +331 -0
- package/dist/insight-auto-tagger.js.map +1 -0
- package/dist/insight-task-bridge.d.ts +9 -0
- package/dist/insight-task-bridge.d.ts.map +1 -1
- package/dist/insight-task-bridge.js +43 -7
- package/dist/insight-task-bridge.js.map +1 -1
- package/dist/insights.d.ts +6 -0
- package/dist/insights.d.ts.map +1 -1
- package/dist/insights.js +13 -0
- package/dist/insights.js.map +1 -1
- package/dist/lane-config.d.ts.map +1 -1
- package/dist/lane-config.js +1 -0
- package/dist/lane-config.js.map +1 -1
- package/dist/lane-template-successor.d.ts +13 -0
- package/dist/lane-template-successor.d.ts.map +1 -0
- package/dist/lane-template-successor.js +132 -0
- package/dist/lane-template-successor.js.map +1 -0
- package/dist/local-whisper.d.ts +21 -0
- package/dist/local-whisper.d.ts.map +1 -0
- package/dist/local-whisper.js +137 -0
- package/dist/local-whisper.js.map +1 -0
- package/dist/macos-accessibility.d.ts +50 -0
- package/dist/macos-accessibility.d.ts.map +1 -0
- package/dist/macos-accessibility.js +185 -0
- package/dist/macos-accessibility.js.map +1 -0
- package/dist/manage.d.ts.map +1 -1
- package/dist/manage.js +47 -1
- package/dist/manage.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +123 -0
- package/dist/mcp.js.map +1 -1
- package/dist/notification-worker.d.ts +66 -0
- package/dist/notification-worker.d.ts.map +1 -0
- package/dist/notification-worker.js +232 -0
- package/dist/notification-worker.js.map +1 -0
- package/dist/openclaw-usage-sync.d.ts +28 -0
- package/dist/openclaw-usage-sync.d.ts.map +1 -0
- package/dist/openclaw-usage-sync.js +161 -0
- package/dist/openclaw-usage-sync.js.map +1 -0
- package/dist/policy.js +1 -1
- package/dist/policy.js.map +1 -1
- package/dist/pr-link-reconciler.d.ts +61 -0
- package/dist/pr-link-reconciler.d.ts.map +1 -0
- package/dist/pr-link-reconciler.js +127 -0
- package/dist/pr-link-reconciler.js.map +1 -0
- package/dist/preflight.js +2 -2
- package/dist/presence-narrator.d.ts +52 -0
- package/dist/presence-narrator.d.ts.map +1 -0
- package/dist/presence-narrator.js +193 -0
- package/dist/presence-narrator.js.map +1 -0
- package/dist/presence.d.ts +2 -0
- package/dist/presence.d.ts.map +1 -1
- package/dist/presence.js +23 -3
- package/dist/presence.js.map +1 -1
- package/dist/product-observation-source.d.ts +52 -0
- package/dist/product-observation-source.d.ts.map +1 -0
- package/dist/product-observation-source.js +326 -0
- package/dist/product-observation-source.js.map +1 -0
- package/dist/reflection-automation.d.ts +25 -0
- package/dist/reflection-automation.d.ts.map +1 -1
- package/dist/reflection-automation.js +163 -42
- package/dist/reflection-automation.js.map +1 -1
- package/dist/review-autoclose.d.ts +62 -0
- package/dist/review-autoclose.d.ts.map +1 -0
- package/dist/review-autoclose.js +75 -0
- package/dist/review-autoclose.js.map +1 -0
- package/dist/routing-enforcement.test.js +32 -56
- package/dist/routing-enforcement.test.js.map +1 -1
- package/dist/sentry-webhook.d.ts +69 -0
- package/dist/sentry-webhook.d.ts.map +1 -0
- package/dist/sentry-webhook.js +88 -0
- package/dist/sentry-webhook.js.map +1 -0
- package/dist/sentry.d.ts +29 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +86 -0
- package/dist/sentry.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +5130 -233
- package/dist/server.js.map +1 -1
- package/dist/stale-candidate-reconciler.d.ts +69 -0
- package/dist/stale-candidate-reconciler.d.ts.map +1 -0
- package/dist/stale-candidate-reconciler.js +236 -0
- package/dist/stale-candidate-reconciler.js.map +1 -0
- package/dist/system-loop-state.d.ts +1 -1
- package/dist/system-loop-state.d.ts.map +1 -1
- package/dist/system-loop-state.js +1 -0
- package/dist/system-loop-state.js.map +1 -1
- package/dist/taskPrecheck.d.ts.map +1 -1
- package/dist/taskPrecheck.js +47 -2
- package/dist/taskPrecheck.js.map +1 -1
- package/dist/tasks.d.ts.map +1 -1
- package/dist/tasks.js +130 -0
- package/dist/tasks.js.map +1 -1
- package/dist/trust-events.d.ts +61 -0
- package/dist/trust-events.d.ts.map +1 -0
- package/dist/trust-events.js +148 -0
- package/dist/trust-events.js.map +1 -0
- package/dist/types.d.ts +1 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/usage-tracking.d.ts +6 -0
- package/dist/usage-tracking.d.ts.map +1 -1
- package/dist/usage-tracking.js +14 -0
- package/dist/usage-tracking.js.map +1 -1
- package/dist/voice-sessions.d.ts +51 -0
- package/dist/voice-sessions.d.ts.map +1 -0
- package/dist/voice-sessions.js +143 -0
- package/dist/voice-sessions.js.map +1 -0
- package/dist/workflow-templates.d.ts.map +1 -1
- package/dist/workflow-templates.js +18 -3
- package/dist/workflow-templates.js.map +1 -1
- package/dist/working-contract.d.ts +22 -1
- package/dist/working-contract.d.ts.map +1 -1
- package/dist/working-contract.js +31 -2
- package/dist/working-contract.js.map +1 -1
- package/package.json +4 -4
- package/public/dashboard.js +12 -4
- package/public/docs.md +98 -10
package/dist/cloud.js
CHANGED
|
@@ -18,14 +18,15 @@ import { chatManager } from './chat.js';
|
|
|
18
18
|
import { remapGitHubMentions } from './github-webhook-attribution.js';
|
|
19
19
|
import { slotManager } from './canvas-slots.js';
|
|
20
20
|
import { getDb } from './db.js';
|
|
21
|
-
import { getUsageSummary, getUsageByAgent, getUsageByModel, listCaps, checkCaps, getRoutingSuggestions } from './usage-tracking.js';
|
|
21
|
+
import { getUsageSummary, getUsageByAgent, getUsageByModel, listCaps, checkCaps, getRoutingSuggestions, getCostForTaskId } from './usage-tracking.js';
|
|
22
22
|
import { listReflections } from './reflections.js';
|
|
23
23
|
import { listInsights } from './insights.js';
|
|
24
24
|
import { readFileSync, existsSync, watch } from 'fs';
|
|
25
25
|
import { join } from 'path';
|
|
26
26
|
import { REFLECTT_HOME } from './config.js';
|
|
27
27
|
import { getRequestMetrics } from './request-tracker.js';
|
|
28
|
-
import { listApprovalQueue, listAgentEvents } from './agent-runs.js';
|
|
28
|
+
import { listApprovalQueue, listAgentEvents, listAgentRuns } from './agent-runs.js';
|
|
29
|
+
import { getUnpushedTrustEvents, markTrustEventsPushed } from './trust-events.js';
|
|
29
30
|
/**
|
|
30
31
|
* Docker identity guard: detect when a container has inherited cloud
|
|
31
32
|
* credentials from a host volume mount. Without explicit opt-in, skip
|
|
@@ -68,6 +69,33 @@ let lastActivityAt = Date.now();
|
|
|
68
69
|
export function markCloudActivity() {
|
|
69
70
|
lastActivityAt = Date.now();
|
|
70
71
|
}
|
|
72
|
+
/** Request immediate canvas sync to cloud (called on canvas_render events) */
|
|
73
|
+
export function requestImmediateCanvasSync() {
|
|
74
|
+
markCloudActivity();
|
|
75
|
+
// syncCanvas is module-scoped; we use a deferred call pattern
|
|
76
|
+
if (immediateSyncFn)
|
|
77
|
+
immediateSyncFn();
|
|
78
|
+
}
|
|
79
|
+
let immediateSyncFn = null;
|
|
80
|
+
export function _registerImmediateSync(fn) {
|
|
81
|
+
immediateSyncFn = fn;
|
|
82
|
+
}
|
|
83
|
+
// ── canvas_push relay buffer ─────────────────────────────────────────────────
|
|
84
|
+
// canvas_push events (utterance, work_released, approval_requested) are emitted
|
|
85
|
+
// on the node event bus but never reached the cloud SSE stream. This buffer
|
|
86
|
+
// collects them and flushes them in the next syncCanvas POST as push_events[].
|
|
87
|
+
// The cloud then broadcasts each as a `canvas_push` SSE event to all subscribers.
|
|
88
|
+
const MAX_PENDING_PUSH_EVENTS = 20;
|
|
89
|
+
const pendingPushEvents = [];
|
|
90
|
+
/** Queue a canvas_push event for relay to cloud in the next sync cycle. */
|
|
91
|
+
export function queueCanvasPushEvent(event) {
|
|
92
|
+
pendingPushEvents.push({ ...event, _queuedAt: Date.now() });
|
|
93
|
+
// Cap buffer to prevent unbounded growth between syncs
|
|
94
|
+
while (pendingPushEvents.length > MAX_PENDING_PUSH_EVENTS)
|
|
95
|
+
pendingPushEvents.shift();
|
|
96
|
+
// Trigger immediate sync so the event reaches browsers quickly
|
|
97
|
+
requestImmediateCanvasSync();
|
|
98
|
+
}
|
|
71
99
|
/** Check if the system is idle */
|
|
72
100
|
function isIdle() {
|
|
73
101
|
return Date.now() - lastActivityAt > IDLE_THRESHOLD_MS;
|
|
@@ -249,7 +277,7 @@ export async function startCloudIntegration() {
|
|
|
249
277
|
return;
|
|
250
278
|
}
|
|
251
279
|
config = {
|
|
252
|
-
cloudUrl: (process.env.REFLECTT_CLOUD_URL || fileConfig?.cloudUrl || 'https://
|
|
280
|
+
cloudUrl: (process.env.REFLECTT_CLOUD_URL || fileConfig?.cloudUrl || 'https://api.reflectt.ai').replace(/\/+$/, ''),
|
|
253
281
|
token: process.env.REFLECTT_HOST_TOKEN || '',
|
|
254
282
|
hostName: process.env.REFLECTT_HOST_NAME || fileConfig?.hostName || 'unnamed-host',
|
|
255
283
|
hostType: process.env.REFLECTT_HOST_TYPE || fileConfig?.hostType || 'openclaw',
|
|
@@ -349,6 +377,10 @@ export async function startCloudIntegration() {
|
|
|
349
377
|
// Canvas sync — adaptive: 5s when active, 60s when idle
|
|
350
378
|
// Uses a single 5s tick that skips when idle (unless enough time has passed)
|
|
351
379
|
let lastCanvasSyncAt = 0;
|
|
380
|
+
_registerImmediateSync(() => {
|
|
381
|
+
syncCanvas().catch(() => { });
|
|
382
|
+
lastCanvasSyncAt = Date.now();
|
|
383
|
+
});
|
|
352
384
|
syncCanvas().catch(() => { });
|
|
353
385
|
state.canvasSyncTimer = setInterval(() => {
|
|
354
386
|
const now = Date.now();
|
|
@@ -362,12 +394,24 @@ export async function startCloudIntegration() {
|
|
|
362
394
|
syncRunApprovals().catch(() => { });
|
|
363
395
|
state.approvalSyncTimer = setInterval(() => {
|
|
364
396
|
syncRunApprovals().catch(() => { });
|
|
397
|
+
pollAgentDecisions().catch(() => { }); // poll queued relay decisions (NAT-behind hosts)
|
|
398
|
+
pollCanvasQueryRelay().catch(() => { }); // poll canvas/query relay queue (NAT-behind hosts)
|
|
365
399
|
}, APPROVAL_SYNC_INTERVAL_MS);
|
|
366
400
|
// Run event sync — every 5s
|
|
367
401
|
syncRunEvents().catch(() => { });
|
|
368
402
|
state.runEventSyncTimer = setInterval(() => {
|
|
369
403
|
syncRunEvents().catch(() => { });
|
|
370
404
|
}, RUN_EVENT_SYNC_INTERVAL_MS);
|
|
405
|
+
// Agent runs sync — every 30s (pushes run records to cloud action_runs table)
|
|
406
|
+
syncAgentRuns().catch(() => { });
|
|
407
|
+
setInterval(() => {
|
|
408
|
+
syncAgentRuns().catch(() => { });
|
|
409
|
+
}, 30_000);
|
|
410
|
+
// Trust event sync — every 60s (pushes unpushed trust signals to cloud)
|
|
411
|
+
syncTrustEvents().catch(() => { });
|
|
412
|
+
setInterval(() => {
|
|
413
|
+
syncTrustEvents().catch(() => { });
|
|
414
|
+
}, 60_000);
|
|
371
415
|
// Usage sync — adaptive: 15s when active, 60s when idle
|
|
372
416
|
let lastUsageSyncAt = 0;
|
|
373
417
|
syncUsage().catch(() => { });
|
|
@@ -540,6 +584,17 @@ export function stopCloudIntegration() {
|
|
|
540
584
|
}
|
|
541
585
|
logConnectionEvent({ type: 'disconnected', timestamp: Date.now(), reason: 'shutdown' });
|
|
542
586
|
console.log('☁️ Cloud integration: stopped');
|
|
587
|
+
// Clear canvas state on Fly so subscribers see an empty room, not ghost agents
|
|
588
|
+
if (state.hostId && state.credential && config) {
|
|
589
|
+
const clearUrl = `${config.cloudUrl}/api/hosts/${state.hostId}/canvas/clear`;
|
|
590
|
+
fetch(clearUrl, {
|
|
591
|
+
method: 'POST',
|
|
592
|
+
headers: {
|
|
593
|
+
'Content-Type': 'application/json',
|
|
594
|
+
Authorization: `Bearer ${state.credential}`,
|
|
595
|
+
},
|
|
596
|
+
}).catch(() => { });
|
|
597
|
+
}
|
|
543
598
|
}
|
|
544
599
|
// ---- Data providers ----
|
|
545
600
|
function getAgents() {
|
|
@@ -556,11 +611,16 @@ function getAgents() {
|
|
|
556
611
|
name: role.name,
|
|
557
612
|
status: p
|
|
558
613
|
? (p.status === 'working' || p.status === 'reviewing' ? 'active'
|
|
559
|
-
: p.status === '
|
|
560
|
-
: '
|
|
614
|
+
: p.status === 'waiting' ? 'waiting'
|
|
615
|
+
: p.status === 'offline' ? 'offline'
|
|
616
|
+
: 'idle')
|
|
561
617
|
: 'offline',
|
|
562
618
|
currentTask: p?.task,
|
|
563
619
|
lastSeen: p?.lastUpdate,
|
|
620
|
+
...(p?.status === 'waiting' && p.waiting ? {
|
|
621
|
+
waitingFor: p.waiting.waitingFor,
|
|
622
|
+
waitingTaskId: p.waiting.taskId,
|
|
623
|
+
} : {}),
|
|
564
624
|
});
|
|
565
625
|
}
|
|
566
626
|
// Also include any presence entries not in TEAM-ROLES (shouldn't happen, but defensive)
|
|
@@ -569,10 +629,15 @@ function getAgents() {
|
|
|
569
629
|
agents.push({
|
|
570
630
|
name: p.agent,
|
|
571
631
|
status: p.status === 'working' || p.status === 'reviewing' ? 'active'
|
|
572
|
-
: p.status === '
|
|
573
|
-
: '
|
|
632
|
+
: p.status === 'waiting' ? 'waiting'
|
|
633
|
+
: p.status === 'offline' ? 'offline'
|
|
634
|
+
: 'idle',
|
|
574
635
|
currentTask: p.task,
|
|
575
636
|
lastSeen: p.lastUpdate,
|
|
637
|
+
...(p.status === 'waiting' && p.waiting ? {
|
|
638
|
+
waitingFor: p.waiting.waitingFor,
|
|
639
|
+
waitingTaskId: p.waiting.taskId,
|
|
640
|
+
} : {}),
|
|
576
641
|
});
|
|
577
642
|
}
|
|
578
643
|
}
|
|
@@ -587,6 +652,7 @@ function getTasks() {
|
|
|
587
652
|
assignee: t.assignee,
|
|
588
653
|
priority: t.priority,
|
|
589
654
|
updatedAt: t.updatedAt || t.createdAt,
|
|
655
|
+
createdAt: t.createdAt,
|
|
590
656
|
}));
|
|
591
657
|
}
|
|
592
658
|
// ---- Cloud communication ----
|
|
@@ -596,6 +662,29 @@ async function sendHeartbeat() {
|
|
|
596
662
|
const agents = getAgents();
|
|
597
663
|
const tasks = getTasks();
|
|
598
664
|
const doingTasks = tasks.filter(t => t.status === 'doing');
|
|
665
|
+
// ── Slow task detection ───────────────────────────────────────────────
|
|
666
|
+
// Include tasks that have been doing >4h with no activity (slow-flagged).
|
|
667
|
+
// These are NOT explicitly blocked — they're just stale.
|
|
668
|
+
const SLOW_HEARTBEAT_MS = 4 * 60 * 60 * 1000;
|
|
669
|
+
const nowTs = Date.now();
|
|
670
|
+
const slowTasks = doingTasks.reduce((acc, t) => {
|
|
671
|
+
const comments = taskManager.getTaskComments(t.id);
|
|
672
|
+
const lastComment = comments.length > 0 ? comments[comments.length - 1] : null;
|
|
673
|
+
const lastActivityAt = lastComment?.timestamp ?? t.updatedAt ?? t.createdAt ?? nowTs;
|
|
674
|
+
const age = nowTs - lastActivityAt;
|
|
675
|
+
if (age > SLOW_HEARTBEAT_MS) {
|
|
676
|
+
acc.push({
|
|
677
|
+
id: t.id,
|
|
678
|
+
title: t.title,
|
|
679
|
+
assignee: t.assignee || undefined,
|
|
680
|
+
priority: t.priority || undefined,
|
|
681
|
+
slowSinceMs: age,
|
|
682
|
+
slowSinceHours: Math.round(age / 36_000) / 100,
|
|
683
|
+
lastActivityAt: lastActivityAt,
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
return acc;
|
|
687
|
+
}, []);
|
|
599
688
|
// Cloud API: POST /api/hosts/:hostId/heartbeat
|
|
600
689
|
// Expects: { status, agents?, activeTasks? }
|
|
601
690
|
// Host is "online" if the server is running and responding.
|
|
@@ -606,13 +695,24 @@ async function sendHeartbeat() {
|
|
|
606
695
|
contractVersion: 'host-heartbeat.v1',
|
|
607
696
|
status: hostStatus,
|
|
608
697
|
timestamp: Date.now(),
|
|
609
|
-
agents: agents.map(a =>
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
status
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
698
|
+
agents: agents.map(a => {
|
|
699
|
+
const agentAliases = [a.name];
|
|
700
|
+
const todoCount = tasks.filter(t => t.status === 'todo' && agentAliases.includes(t.assignee || '')).length;
|
|
701
|
+
const doingCount = tasks.filter(t => t.status === 'doing' && agentAliases.includes(t.assignee || '')).length;
|
|
702
|
+
const blockedCount = tasks.filter(t => t.status === 'blocked' && agentAliases.includes(t.assignee || '')).length;
|
|
703
|
+
return {
|
|
704
|
+
id: a.name,
|
|
705
|
+
name: a.name,
|
|
706
|
+
status: a.status,
|
|
707
|
+
currentTaskId: a.currentTask || undefined,
|
|
708
|
+
lastSeenAt: a.lastSeen || Date.now(),
|
|
709
|
+
taskCounts: { todo: todoCount, doing: doingCount, blocked: blockedCount },
|
|
710
|
+
...(a.status === 'waiting' ? {
|
|
711
|
+
waitingFor: a.waitingFor,
|
|
712
|
+
waitingTaskId: a.waitingTaskId,
|
|
713
|
+
} : {}),
|
|
714
|
+
};
|
|
715
|
+
}),
|
|
616
716
|
activeTasks: doingTasks.map(t => ({
|
|
617
717
|
id: t.id,
|
|
618
718
|
title: t.title,
|
|
@@ -621,6 +721,7 @@ async function sendHeartbeat() {
|
|
|
621
721
|
priority: t.priority || undefined,
|
|
622
722
|
updatedAt: t.updatedAt || Date.now(),
|
|
623
723
|
})),
|
|
724
|
+
slowTasks: slowTasks.length > 0 ? slowTasks : undefined,
|
|
624
725
|
metrics: (() => {
|
|
625
726
|
const m = getRequestMetrics();
|
|
626
727
|
return {
|
|
@@ -888,7 +989,7 @@ async function syncChat() {
|
|
|
888
989
|
// CRITICAL: use oldestFirst=true so the cursor walks forward through ALL
|
|
889
990
|
// messages without skipping. Default getMessages returns newest-N (DESC
|
|
890
991
|
// then reversed), which drops older messages in high-traffic windows.
|
|
891
|
-
// This was the root cause of cloud chat sync gaps
|
|
992
|
+
// This was the root cause of cloud chat sync gaps reported.
|
|
892
993
|
const recentMessages = chatManager.getMessages({
|
|
893
994
|
after: chatSyncCursor,
|
|
894
995
|
limit: 100,
|
|
@@ -969,6 +1070,53 @@ async function syncChat() {
|
|
|
969
1070
|
}
|
|
970
1071
|
// ---- Canvas sync ----
|
|
971
1072
|
let canvasSyncErrors = 0;
|
|
1073
|
+
// ── Needs-attention call hook ─────────────────────────────────────────────
|
|
1074
|
+
// Track previous agent states to detect needs-attention transitions.
|
|
1075
|
+
// When an agent newly enters needs-attention/urgent, fire POST /call on the
|
|
1076
|
+
// Fly API for org members who have call_on_needs_attention=true.
|
|
1077
|
+
// The Fly API resolves phone numbers from team_members.notification_phone.
|
|
1078
|
+
const prevAgentStates = new Map(); // agentId → state
|
|
1079
|
+
function checkNeedsAttentionTransitions(agents, hostId, cloudUrl, credential) {
|
|
1080
|
+
for (const [agentId, agentData] of Object.entries(agents)) {
|
|
1081
|
+
const agentState = agentData?.state;
|
|
1082
|
+
if (!agentState)
|
|
1083
|
+
continue;
|
|
1084
|
+
const prev = prevAgentStates.get(agentId);
|
|
1085
|
+
const isAlert = agentState === 'needs-attention' || agentState === 'urgent';
|
|
1086
|
+
const wasAlert = prev === 'needs-attention' || prev === 'urgent';
|
|
1087
|
+
prevAgentStates.set(agentId, agentState);
|
|
1088
|
+
// Only fire on NEW transitions into alert state
|
|
1089
|
+
if (!isAlert || wasAlert)
|
|
1090
|
+
continue;
|
|
1091
|
+
const taskData = agentData?.payload;
|
|
1092
|
+
const taskTitle = taskData?.task || taskData?.title || undefined;
|
|
1093
|
+
console.log(`☁️ [Canvas] needs-attention: @${agentId} → auto-call hook`);
|
|
1094
|
+
// POST /call to Fly — Fly resolves phones for members with call_on_needs_attention=true
|
|
1095
|
+
// If no members have that preference set, the call is a no-op (400 with no phone).
|
|
1096
|
+
const callUrl = `${cloudUrl}/api/hosts/${hostId}/call`;
|
|
1097
|
+
fetch(callUrl, {
|
|
1098
|
+
method: 'POST',
|
|
1099
|
+
headers: {
|
|
1100
|
+
'Content-Type': 'application/json',
|
|
1101
|
+
Authorization: `Bearer ${credential}`,
|
|
1102
|
+
},
|
|
1103
|
+
body: JSON.stringify({
|
|
1104
|
+
agentId,
|
|
1105
|
+
agentName: agentId,
|
|
1106
|
+
taskTitle,
|
|
1107
|
+
// No `to` — Fly resolves phone from team_members.notification_phone
|
|
1108
|
+
// for members with call_on_needs_attention=true
|
|
1109
|
+
}),
|
|
1110
|
+
}).then(r => {
|
|
1111
|
+
if (!r.ok && r.status !== 400) {
|
|
1112
|
+
console.warn(`☁️ [Canvas] auto-call failed: ${r.status}`);
|
|
1113
|
+
}
|
|
1114
|
+
}).catch(err => {
|
|
1115
|
+
// Non-fatal: call is best-effort
|
|
1116
|
+
console.warn(`☁️ [Canvas] auto-call error: ${err instanceof Error ? err.message : err}`);
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
972
1120
|
async function syncCanvas() {
|
|
973
1121
|
if (!state.hostId || !config)
|
|
974
1122
|
return;
|
|
@@ -984,8 +1132,137 @@ async function syncCanvas() {
|
|
|
984
1132
|
}
|
|
985
1133
|
}
|
|
986
1134
|
catch { /* local API not ready */ }
|
|
987
|
-
//
|
|
988
|
-
|
|
1135
|
+
// ── Task-derived agent presence ─────────────────────────────────────────
|
|
1136
|
+
// Agents that have open tasks are present even if they haven't pushed native
|
|
1137
|
+
// canvas state. Any agent with a doing/validating task → "working".
|
|
1138
|
+
// Any agent with a todo task (but no doing) → "working" (queued work).
|
|
1139
|
+
// Native canvas state takes precedence when present — only fill gaps.
|
|
1140
|
+
try {
|
|
1141
|
+
const ACTIVE_STATUSES = ['doing', 'validating', 'todo'];
|
|
1142
|
+
const byAgent = {};
|
|
1143
|
+
for (const status of ACTIVE_STATUSES) {
|
|
1144
|
+
const res = await fetch(`http://127.0.0.1:4445/tasks?status=${status}&limit=100`);
|
|
1145
|
+
if (!res.ok)
|
|
1146
|
+
continue;
|
|
1147
|
+
const data = await res.json();
|
|
1148
|
+
const tasks = data.tasks ?? [];
|
|
1149
|
+
for (const task of tasks) {
|
|
1150
|
+
const assignee = task.assignee;
|
|
1151
|
+
if (!assignee || assignee === 'unassigned')
|
|
1152
|
+
continue;
|
|
1153
|
+
// Higher-priority status wins: doing > validating > todo
|
|
1154
|
+
const existing = byAgent[assignee];
|
|
1155
|
+
const priority = { doing: 0, validating: 1, todo: 2 };
|
|
1156
|
+
const newPriority = priority[status] ?? 99;
|
|
1157
|
+
const existingPriority = existing ? (priority[existing.bestStatus] ?? 99) : 99;
|
|
1158
|
+
if (!existing || newPriority < existingPriority) {
|
|
1159
|
+
byAgent[assignee] = { bestStatus: status, taskTitle: task.title };
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
// Merge derived states into agents — native canvas state takes precedence
|
|
1164
|
+
const now = Date.now();
|
|
1165
|
+
for (const [agentId, info] of Object.entries(byAgent)) {
|
|
1166
|
+
if (agents[agentId])
|
|
1167
|
+
continue; // native state present — don't override
|
|
1168
|
+
const derivedState = info.bestStatus === 'doing' ? 'working'
|
|
1169
|
+
: info.bestStatus === 'validating' ? 'working'
|
|
1170
|
+
: 'working'; // todo → working (has queued work)
|
|
1171
|
+
agents[agentId] = {
|
|
1172
|
+
state: derivedState,
|
|
1173
|
+
currentTask: info.taskTitle,
|
|
1174
|
+
updatedAt: now,
|
|
1175
|
+
source: 'task-derived',
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
// ── Waiting state overlay ───────────────────────────────────────────
|
|
1179
|
+
// Agents in waiting status get state='needs-attention' (amber pulse) on canvas.
|
|
1180
|
+
// This runs AFTER task-derived but BEFORE thinking inference — waiting overrides working.
|
|
1181
|
+
// Native canvas state still wins if explicitly set.
|
|
1182
|
+
const allAgentInfos = getAgents();
|
|
1183
|
+
for (const agent of allAgentInfos) {
|
|
1184
|
+
if (agent.status !== 'waiting')
|
|
1185
|
+
continue;
|
|
1186
|
+
if (agents[agent.name])
|
|
1187
|
+
continue; // native state — don't override
|
|
1188
|
+
agents[agent.name] = {
|
|
1189
|
+
state: 'waiting', // soft amber drift — distinct from needs-attention (bright pulse)
|
|
1190
|
+
updatedAt: now,
|
|
1191
|
+
source: 'waiting-derived',
|
|
1192
|
+
waitingFor: agent.waitingFor ?? null,
|
|
1193
|
+
waitingTaskId: agent.waitingTaskId ?? null,
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
catch { /* task API not ready — not fatal */ }
|
|
1198
|
+
// ── Thinking state inference ────────────────────────────────────────────
|
|
1199
|
+
// Agent has an active (running, non-completed) run AND hasn't sent a message
|
|
1200
|
+
// in >2min → auto-derive state = 'thinking'. Explicit native canvas state always
|
|
1201
|
+
// wins; this only fills gaps left after task-derived and native state passes.
|
|
1202
|
+
// @swift @kotlin: once this ships, local heuristics for thinking can be removed.
|
|
1203
|
+
try {
|
|
1204
|
+
const THINKING_SILENCE_MS = 2 * 60 * 1000; // 2 minutes
|
|
1205
|
+
const now2 = Date.now();
|
|
1206
|
+
const presences = presenceManager.getAllPresence();
|
|
1207
|
+
const presenceByAgent = new Map(presences.map(p => [p.agent, p]));
|
|
1208
|
+
const allAgents = getAgents();
|
|
1209
|
+
for (const agent of allAgents) {
|
|
1210
|
+
// Skip if already has an explicit state (native or task-derived)
|
|
1211
|
+
if (agents[agent.name])
|
|
1212
|
+
continue;
|
|
1213
|
+
// Check for an active (incomplete) run
|
|
1214
|
+
const runs = listAgentRuns(agent.name, 'default', { limit: 5 });
|
|
1215
|
+
const hasActiveRun = runs.some(r => r.status === 'working' && r.completedAt === null);
|
|
1216
|
+
if (!hasActiveRun)
|
|
1217
|
+
continue;
|
|
1218
|
+
// Check message silence window
|
|
1219
|
+
const presence = presenceByAgent.get(agent.name);
|
|
1220
|
+
const lastMsgTs = presence?.lastUpdate ?? 0;
|
|
1221
|
+
if (now2 - lastMsgTs > THINKING_SILENCE_MS) {
|
|
1222
|
+
agents[agent.name] = {
|
|
1223
|
+
state: 'thinking',
|
|
1224
|
+
updatedAt: now2,
|
|
1225
|
+
source: 'thinking-inferred',
|
|
1226
|
+
};
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
catch { /* non-fatal */ }
|
|
1231
|
+
// ── Needs-attention call hook ───────────────────────────────────────────
|
|
1232
|
+
// Check for new needs-attention transitions BEFORE pushing to cloud.
|
|
1233
|
+
// The Fly canvas handler also triggers auto-calls, but this node-side hook
|
|
1234
|
+
// fires immediately on state detection — no waiting for Fly SSE round-trip.
|
|
1235
|
+
if (Object.keys(agents).length > 0 && state.hostId && state.credential && config.cloudUrl) {
|
|
1236
|
+
checkNeedsAttentionTransitions(agents, state.hostId, config.cloudUrl, state.credential);
|
|
1237
|
+
}
|
|
1238
|
+
// Inject agent avatars into sync payload — browsers on app.reflectt.ai read avatar
|
|
1239
|
+
// from agent state (canvasStore), not from a separate API call. We merge avatar
|
|
1240
|
+
// into each agent entry here so cloud browsers render custom orbs instead of circles.
|
|
1241
|
+
// Agents with avatars who haven't posted a canvas state get a floor stub so their
|
|
1242
|
+
// custom orb always reaches the cloud (not just when canvas/state is called).
|
|
1243
|
+
// task-1773690756100
|
|
1244
|
+
try {
|
|
1245
|
+
const db = getDb();
|
|
1246
|
+
const avatarRows = db.prepare("SELECT agent_id, settings FROM agent_config WHERE settings LIKE '%avatar%'").all();
|
|
1247
|
+
for (const row of avatarRows) {
|
|
1248
|
+
try {
|
|
1249
|
+
const s = JSON.parse(row.settings);
|
|
1250
|
+
if (s.avatar?.content) {
|
|
1251
|
+
if (!agents[row.agent_id]) {
|
|
1252
|
+
// Agent has an avatar but no active canvas state — add a floor stub so
|
|
1253
|
+
// the custom orb reaches the cloud even without a canvas/state post.
|
|
1254
|
+
agents[row.agent_id] = { state: 'floor', sensors: null, payload: {}, updatedAt: Date.now() };
|
|
1255
|
+
}
|
|
1256
|
+
agents[row.agent_id].avatar = s.avatar.content;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
catch { /* skip */ }
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
catch { /* non-blocking */ }
|
|
1263
|
+
// Push to cloud — include slots, agent states, and any buffered canvas_push events
|
|
1264
|
+
const pushEventsToSend = pendingPushEvents.splice(0, pendingPushEvents.length);
|
|
1265
|
+
const result = await cloudPost(`/api/hosts/${state.hostId}/canvas`, { slots: activeSlots, agents, push_events: pushEventsToSend.length > 0 ? pushEventsToSend : undefined });
|
|
989
1266
|
if (result.success && result.data) {
|
|
990
1267
|
state.lastCanvasSync = Date.now();
|
|
991
1268
|
if (canvasSyncErrors > 0) {
|
|
@@ -998,6 +1275,10 @@ async function syncCanvas() {
|
|
|
998
1275
|
if (canvasSyncErrors <= 3 || canvasSyncErrors % 20 === 0) {
|
|
999
1276
|
console.warn(`☁️ [Canvas] Sync failed (${canvasSyncErrors}): ${result.error}`);
|
|
1000
1277
|
}
|
|
1278
|
+
// Re-queue events that failed to send (up to cap)
|
|
1279
|
+
if (pushEventsToSend.length > 0) {
|
|
1280
|
+
pendingPushEvents.unshift(...pushEventsToSend.slice(-MAX_PENDING_PUSH_EVENTS));
|
|
1281
|
+
}
|
|
1001
1282
|
}
|
|
1002
1283
|
}
|
|
1003
1284
|
// ---- Run Approval Sync ----
|
|
@@ -1012,7 +1293,19 @@ async function syncRunApprovals() {
|
|
|
1012
1293
|
return;
|
|
1013
1294
|
lastApprovalSyncAt = now;
|
|
1014
1295
|
try {
|
|
1015
|
-
const
|
|
1296
|
+
const KNOWN_AGENTS_SYNC = new Set([
|
|
1297
|
+
'link', 'kai', 'pixel', 'sage', 'scout', 'echo',
|
|
1298
|
+
'rhythm', 'spark', 'swift', 'kotlin', 'harmony',
|
|
1299
|
+
'artdirector', 'uipolish', 'coo', 'cos', 'pm', 'qa',
|
|
1300
|
+
'shield', 'kindling', 'quill', 'funnel', 'attribution',
|
|
1301
|
+
'bookkeeper', 'legal-counsel', 'evi-scout',
|
|
1302
|
+
]);
|
|
1303
|
+
const rawItems = listApprovalQueue({ category: 'review', limit: 20 });
|
|
1304
|
+
// Filter out agent-to-agent reviews — only sync human-required approvals to cloud
|
|
1305
|
+
const items = rawItems.filter(item => {
|
|
1306
|
+
const reviewer = (item.agentId ?? '').toLowerCase().trim();
|
|
1307
|
+
return !reviewer || !KNOWN_AGENTS_SYNC.has(reviewer);
|
|
1308
|
+
});
|
|
1016
1309
|
if (items.length === 0 && approvalSyncErrors === 0)
|
|
1017
1310
|
return; // Skip push when empty and no prior errors
|
|
1018
1311
|
const payload = items.map(item => ({
|
|
@@ -1045,6 +1338,180 @@ async function syncRunApprovals() {
|
|
|
1045
1338
|
}
|
|
1046
1339
|
}
|
|
1047
1340
|
}
|
|
1341
|
+
// ---- Agent Decision Relay Poll ----
|
|
1342
|
+
// When decisions are made via the cloud canvas while the node is behind NAT,
|
|
1343
|
+
// they are queued at GET /api/hosts/:id/agent-interface/decisions.
|
|
1344
|
+
// This function polls that queue and processes each decision locally.
|
|
1345
|
+
let lastDecisionPollAt = 0;
|
|
1346
|
+
const DECISION_POLL_INTERVAL_MS = 10_000; // 10s — same cadence as approval sync
|
|
1347
|
+
async function pollAgentDecisions() {
|
|
1348
|
+
if (!state.hostId || !config)
|
|
1349
|
+
return;
|
|
1350
|
+
const now = Date.now();
|
|
1351
|
+
if (now - lastDecisionPollAt < DECISION_POLL_INTERVAL_MS)
|
|
1352
|
+
return;
|
|
1353
|
+
lastDecisionPollAt = now;
|
|
1354
|
+
try {
|
|
1355
|
+
const result = await cloudGet(`/api/hosts/${state.hostId}/agent-interface/decisions`);
|
|
1356
|
+
if (!result.success)
|
|
1357
|
+
return;
|
|
1358
|
+
const decisions = Array.isArray(result.data?.decisions) ? result.data.decisions : [];
|
|
1359
|
+
if (decisions.length === 0)
|
|
1360
|
+
return;
|
|
1361
|
+
const acked = [];
|
|
1362
|
+
for (const d of decisions) {
|
|
1363
|
+
try {
|
|
1364
|
+
const endpoint = `/agent-interface/runs/${d.eventId}/${d.decision === 'approve' ? 'approve' : 'reject'}`;
|
|
1365
|
+
const res = await fetch(`http://127.0.0.1:4445${endpoint}`, {
|
|
1366
|
+
method: 'POST',
|
|
1367
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1368
|
+
signal: AbortSignal.timeout(3000),
|
|
1369
|
+
});
|
|
1370
|
+
// ACK on success OR 404/409 (run already decided / not found — still remove from queue)
|
|
1371
|
+
if (res.ok || res.status === 404 || res.status === 409) {
|
|
1372
|
+
acked.push(d.eventId);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
catch {
|
|
1376
|
+
// Individual failure — leave in queue, retry next cycle
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
if (acked.length > 0) {
|
|
1380
|
+
await cloudPost(`/api/hosts/${state.hostId}/agent-interface/decisions/ack`, { eventIds: acked });
|
|
1381
|
+
console.log(`☁️ [DecisionRelay] Processed ${acked.length}/${decisions.length} queued decisions`);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
catch {
|
|
1385
|
+
// Non-critical — decisions will be retried next cycle
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
// ── Canvas query relay polling ────────────────────────────────────────────────
|
|
1389
|
+
// When canvas/query is called from a NAT-behind node, the cloud queues the query
|
|
1390
|
+
// at GET /api/hosts/:id/canvas/query/pending. We poll here, POST each query to
|
|
1391
|
+
// the local node, and ACK. The node emits canvas_message via eventBus → canvas_push
|
|
1392
|
+
// relay → cloud → browser pulse subscribers.
|
|
1393
|
+
let lastCanvasQueryPollAt = 0;
|
|
1394
|
+
const CANVAS_QUERY_POLL_INTERVAL_MS = 8_000; // 8s — faster than decisions (user-facing)
|
|
1395
|
+
async function pollCanvasQueryRelay() {
|
|
1396
|
+
if (!state.hostId || !config)
|
|
1397
|
+
return;
|
|
1398
|
+
const now = Date.now();
|
|
1399
|
+
if (now - lastCanvasQueryPollAt < CANVAS_QUERY_POLL_INTERVAL_MS)
|
|
1400
|
+
return;
|
|
1401
|
+
lastCanvasQueryPollAt = now;
|
|
1402
|
+
try {
|
|
1403
|
+
const result = await cloudGet(`/api/hosts/${state.hostId}/canvas/query/pending`);
|
|
1404
|
+
if (!result.success)
|
|
1405
|
+
return;
|
|
1406
|
+
const queries = Array.isArray(result.data?.queries) ? result.data.queries : [];
|
|
1407
|
+
if (queries.length === 0)
|
|
1408
|
+
return;
|
|
1409
|
+
const acked = [];
|
|
1410
|
+
for (const q of queries) {
|
|
1411
|
+
try {
|
|
1412
|
+
// POST to local node — canvas/query processes it and emits canvas_message via eventBus.
|
|
1413
|
+
// Process query locally and capture card for relay back to the browser.
|
|
1414
|
+
// The card is included in the ACK payload → cloud broadcasts canvas_message to pulse SSE.
|
|
1415
|
+
const res = await fetch('http://127.0.0.1:4445/canvas/query', {
|
|
1416
|
+
method: 'POST',
|
|
1417
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1418
|
+
body: JSON.stringify({ query: q.query, sessionId: q.sessionId ?? undefined }),
|
|
1419
|
+
signal: AbortSignal.timeout(12000), // LLM calls can take ~10s
|
|
1420
|
+
});
|
|
1421
|
+
if (res.ok) {
|
|
1422
|
+
try {
|
|
1423
|
+
const data = await res.json();
|
|
1424
|
+
if (data.card) {
|
|
1425
|
+
// Include card in acked payload so cloud can broadcast it to browser subscribers
|
|
1426
|
+
;
|
|
1427
|
+
q._card = data.card;
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
catch { /* card extraction optional — still ACK */ }
|
|
1431
|
+
acked.push(q.queryId);
|
|
1432
|
+
}
|
|
1433
|
+
else if (res.status === 400) {
|
|
1434
|
+
// Invalid query — still ACK to remove from queue
|
|
1435
|
+
acked.push(q.queryId);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
catch {
|
|
1439
|
+
// Individual failure — leave in queue, retry next cycle
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (acked.length > 0) {
|
|
1443
|
+
// Collect response cards for broadcast: cloud will emit canvas_message to pulse SSE
|
|
1444
|
+
const cards = queries
|
|
1445
|
+
.filter(q => acked.includes(q.queryId) && q._card)
|
|
1446
|
+
.map(q => q._card);
|
|
1447
|
+
await cloudPost(`/api/hosts/${state.hostId}/canvas/query/ack`, { queryIds: acked, cards });
|
|
1448
|
+
console.log(`☁️ [CanvasQueryRelay] Processed ${acked.length}/${queries.length} relay queries, ${cards.length} cards broadcast`);
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
catch {
|
|
1452
|
+
// Non-critical — queries will be retried next cycle
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
// ---- Agent Runs Sync ----
|
|
1456
|
+
// Push agent run records to cloud action_runs table (used by cloud Runs screen)
|
|
1457
|
+
let agentRunSyncErrors = 0;
|
|
1458
|
+
async function syncAgentRuns() {
|
|
1459
|
+
if (!state.hostId || !config)
|
|
1460
|
+
return;
|
|
1461
|
+
try {
|
|
1462
|
+
const agents = getAgents();
|
|
1463
|
+
if (agents.length === 0)
|
|
1464
|
+
return;
|
|
1465
|
+
const allRuns = [];
|
|
1466
|
+
for (const agent of agents) {
|
|
1467
|
+
const runs = listAgentRuns(agent.name, 'default', { limit: 20 });
|
|
1468
|
+
allRuns.push(...runs);
|
|
1469
|
+
}
|
|
1470
|
+
if (allRuns.length === 0)
|
|
1471
|
+
return;
|
|
1472
|
+
// Enrich runs with cost attribution from local model_usage table.
|
|
1473
|
+
// task_id lives in contextSnapshot.taskId — only attributed when present.
|
|
1474
|
+
// No time-window fallback (too error-prone for financial metrics).
|
|
1475
|
+
const enrichedRuns = allRuns.map(run => {
|
|
1476
|
+
const taskId = typeof run.contextSnapshot?.taskId === 'string' ? run.contextSnapshot.taskId : null;
|
|
1477
|
+
return {
|
|
1478
|
+
...run,
|
|
1479
|
+
taskId,
|
|
1480
|
+
costUsd: taskId ? getCostForTaskId(taskId) : null,
|
|
1481
|
+
};
|
|
1482
|
+
});
|
|
1483
|
+
const result = await cloudPost(`/api/hosts/${state.hostId}/runs/sync`, { runs: enrichedRuns });
|
|
1484
|
+
if (result.success || result.data) {
|
|
1485
|
+
if (agentRunSyncErrors > 0) {
|
|
1486
|
+
console.log('☁️ [RunSync] Recovered after errors');
|
|
1487
|
+
agentRunSyncErrors = 0;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
catch (err) {
|
|
1492
|
+
agentRunSyncErrors++;
|
|
1493
|
+
if (agentRunSyncErrors <= 3) {
|
|
1494
|
+
console.warn(`☁️ [RunSync] Error: ${err?.message}`);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
// ---- Trust Event Sync ----
|
|
1499
|
+
// Push unpushed trust-collapse signals to cloud agent_trust_events table.
|
|
1500
|
+
async function syncTrustEvents() {
|
|
1501
|
+
const { hostId } = state;
|
|
1502
|
+
if (!hostId)
|
|
1503
|
+
return;
|
|
1504
|
+
const events = getUnpushedTrustEvents(50);
|
|
1505
|
+
if (events.length === 0)
|
|
1506
|
+
return;
|
|
1507
|
+
try {
|
|
1508
|
+
const result = await cloudPost(`/api/hosts/${hostId}/trust-events/sync`, { events: events.map(e => ({ id: e.id, agentId: e.agentId, type: e.eventType, severity: e.severity, taskId: e.taskId ?? null, summary: e.summary, metadata: e.context, emittedAt: e.occurredAt })) });
|
|
1509
|
+
if (result.success || result.data?.ok) {
|
|
1510
|
+
markTrustEventsPushed(events.map(e => e.id));
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
catch { /* non-fatal */ }
|
|
1514
|
+
}
|
|
1048
1515
|
// ---- Run Event Sync ----
|
|
1049
1516
|
// Push recent run events to cloud so Presence SSE relay has data
|
|
1050
1517
|
let lastRunEventSyncAt = 0;
|