reflectt-node 0.1.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.
- package/LICENSE +178 -0
- package/README.md +188 -0
- package/dist/activationEvents.d.ts +110 -0
- package/dist/activationEvents.d.ts.map +1 -0
- package/dist/activationEvents.js +378 -0
- package/dist/activationEvents.js.map +1 -0
- package/dist/activity-signal.d.ts +30 -0
- package/dist/activity-signal.d.ts.map +1 -0
- package/dist/activity-signal.js +93 -0
- package/dist/activity-signal.js.map +1 -0
- package/dist/alert-integrity.d.ts +100 -0
- package/dist/alert-integrity.d.ts.map +1 -0
- package/dist/alert-integrity.js +333 -0
- package/dist/alert-integrity.js.map +1 -0
- package/dist/alert-preflight.d.ts +40 -0
- package/dist/alert-preflight.d.ts.map +1 -0
- package/dist/alert-preflight.js +235 -0
- package/dist/alert-preflight.js.map +1 -0
- package/dist/analytics.d.ts +131 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +371 -0
- package/dist/analytics.js.map +1 -0
- package/dist/artifact-mirror.d.ts +26 -0
- package/dist/artifact-mirror.d.ts.map +1 -0
- package/dist/artifact-mirror.js +170 -0
- package/dist/artifact-mirror.js.map +1 -0
- package/dist/artifact-resolver.d.ts +48 -0
- package/dist/artifact-resolver.d.ts.map +1 -0
- package/dist/artifact-resolver.js +164 -0
- package/dist/artifact-resolver.js.map +1 -0
- package/dist/assignment.d.ts +116 -0
- package/dist/assignment.d.ts.map +1 -0
- package/dist/assignment.js +475 -0
- package/dist/assignment.js.map +1 -0
- package/dist/auditLedger.d.ts +50 -0
- package/dist/auditLedger.d.ts.map +1 -0
- package/dist/auditLedger.js +136 -0
- package/dist/auditLedger.js.map +1 -0
- package/dist/boardHealthWorker.d.ts +134 -0
- package/dist/boardHealthWorker.d.ts.map +1 -0
- package/dist/boardHealthWorker.js +882 -0
- package/dist/boardHealthWorker.js.map +1 -0
- package/dist/bootstrap-team.d.ts +42 -0
- package/dist/bootstrap-team.d.ts.map +1 -0
- package/dist/bootstrap-team.js +111 -0
- package/dist/bootstrap-team.js.map +1 -0
- package/dist/buildInfo.d.ts +17 -0
- package/dist/buildInfo.d.ts.map +1 -0
- package/dist/buildInfo.js +56 -0
- package/dist/buildInfo.js.map +1 -0
- package/dist/calendar-events.d.ts +133 -0
- package/dist/calendar-events.d.ts.map +1 -0
- package/dist/calendar-events.js +615 -0
- package/dist/calendar-events.js.map +1 -0
- package/dist/calendar-ical.d.ts +41 -0
- package/dist/calendar-ical.d.ts.map +1 -0
- package/dist/calendar-ical.js +413 -0
- package/dist/calendar-ical.js.map +1 -0
- package/dist/calendar-reminder-engine.d.ts +10 -0
- package/dist/calendar-reminder-engine.d.ts.map +1 -0
- package/dist/calendar-reminder-engine.js +143 -0
- package/dist/calendar-reminder-engine.js.map +1 -0
- package/dist/calendar.d.ts +75 -0
- package/dist/calendar.d.ts.map +1 -0
- package/dist/calendar.js +391 -0
- package/dist/calendar.js.map +1 -0
- package/dist/canvas-multiplexer.d.ts +44 -0
- package/dist/canvas-multiplexer.d.ts.map +1 -0
- package/dist/canvas-multiplexer.js +150 -0
- package/dist/canvas-multiplexer.js.map +1 -0
- package/dist/canvas-slots.d.ts +83 -0
- package/dist/canvas-slots.d.ts.map +1 -0
- package/dist/canvas-slots.js +144 -0
- package/dist/canvas-slots.js.map +1 -0
- package/dist/canvas-types.d.ts +56 -0
- package/dist/canvas-types.d.ts.map +1 -0
- package/dist/canvas-types.js +54 -0
- package/dist/canvas-types.js.map +1 -0
- package/dist/cf-keepalive.d.ts +40 -0
- package/dist/cf-keepalive.d.ts.map +1 -0
- package/dist/cf-keepalive.js +153 -0
- package/dist/cf-keepalive.js.map +1 -0
- package/dist/changeFeed.d.ts +38 -0
- package/dist/changeFeed.d.ts.map +1 -0
- package/dist/changeFeed.js +324 -0
- package/dist/changeFeed.js.map +1 -0
- package/dist/channels.d.ts +28 -0
- package/dist/channels.d.ts.map +1 -0
- package/dist/channels.js +23 -0
- package/dist/channels.js.map +1 -0
- package/dist/chat-approval-detector.d.ts +47 -0
- package/dist/chat-approval-detector.d.ts.map +1 -0
- package/dist/chat-approval-detector.js +224 -0
- package/dist/chat-approval-detector.js.map +1 -0
- package/dist/chat.d.ts +119 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +666 -0
- package/dist/chat.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1142 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloud.d.ts +45 -0
- package/dist/cloud.d.ts.map +1 -0
- package/dist/cloud.js +962 -0
- package/dist/cloud.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +33 -0
- package/dist/config.js.map +1 -0
- package/dist/connectivity.d.ts +59 -0
- package/dist/connectivity.d.ts.map +1 -0
- package/dist/connectivity.js +173 -0
- package/dist/connectivity.js.map +1 -0
- package/dist/contacts.d.ts +59 -0
- package/dist/contacts.d.ts.map +1 -0
- package/dist/contacts.js +183 -0
- package/dist/contacts.js.map +1 -0
- package/dist/content.d.ts +130 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +186 -0
- package/dist/content.js.map +1 -0
- package/dist/context-budget.d.ts +87 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +459 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/continuity-loop.d.ts +55 -0
- package/dist/continuity-loop.d.ts.map +1 -0
- package/dist/continuity-loop.js +267 -0
- package/dist/continuity-loop.js.map +1 -0
- package/dist/dashboard.d.ts +6 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +2348 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/db.d.ts +44 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +648 -0
- package/dist/db.js.map +1 -0
- package/dist/doctor.d.ts +30 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +159 -0
- package/dist/doctor.js.map +1 -0
- package/dist/duplicateClosureGuard.d.ts +31 -0
- package/dist/duplicateClosureGuard.d.ts.map +1 -0
- package/dist/duplicateClosureGuard.js +83 -0
- package/dist/duplicateClosureGuard.js.map +1 -0
- package/dist/embeddings.d.ts +13 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +78 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/escalation.d.ts +80 -0
- package/dist/escalation.d.ts.map +1 -0
- package/dist/escalation.js +213 -0
- package/dist/escalation.js.map +1 -0
- package/dist/events.d.ts +130 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +382 -0
- package/dist/events.js.map +1 -0
- package/dist/executionSweeper.d.ts +97 -0
- package/dist/executionSweeper.d.ts.map +1 -0
- package/dist/executionSweeper.js +875 -0
- package/dist/executionSweeper.js.map +1 -0
- package/dist/experiments.d.ts +47 -0
- package/dist/experiments.d.ts.map +1 -0
- package/dist/experiments.js +133 -0
- package/dist/experiments.js.map +1 -0
- package/dist/feedback.d.ts +179 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +397 -0
- package/dist/feedback.js.map +1 -0
- package/dist/files.d.ts +52 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +172 -0
- package/dist/files.js.map +1 -0
- package/dist/format-duration.d.ts +19 -0
- package/dist/format-duration.d.ts.map +1 -0
- package/dist/format-duration.js +33 -0
- package/dist/format-duration.js.map +1 -0
- package/dist/github-actor-auth.d.ts +20 -0
- package/dist/github-actor-auth.d.ts.map +1 -0
- package/dist/github-actor-auth.js +54 -0
- package/dist/github-actor-auth.js.map +1 -0
- package/dist/github-ci.d.ts +16 -0
- package/dist/github-ci.d.ts.map +1 -0
- package/dist/github-ci.js +37 -0
- package/dist/github-ci.js.map +1 -0
- package/dist/github-identity.d.ts +30 -0
- package/dist/github-identity.d.ts.map +1 -0
- package/dist/github-identity.js +96 -0
- package/dist/github-identity.js.map +1 -0
- package/dist/github-reviews.d.ts +24 -0
- package/dist/github-reviews.d.ts.map +1 -0
- package/dist/github-reviews.js +56 -0
- package/dist/github-reviews.js.map +1 -0
- package/dist/health.d.ts +391 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +1841 -0
- package/dist/health.js.map +1 -0
- package/dist/host-keepalive.d.ts +22 -0
- package/dist/host-keepalive.d.ts.map +1 -0
- package/dist/host-keepalive.js +126 -0
- package/dist/host-keepalive.js.map +1 -0
- package/dist/host-registry.d.ts +43 -0
- package/dist/host-registry.d.ts.map +1 -0
- package/dist/host-registry.js +93 -0
- package/dist/host-registry.js.map +1 -0
- package/dist/inbox.d.ts +87 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +410 -0
- package/dist/inbox.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +306 -0
- package/dist/index.js.map +1 -0
- package/dist/insight-mutation.d.ts +32 -0
- package/dist/insight-mutation.d.ts.map +1 -0
- package/dist/insight-mutation.js +160 -0
- package/dist/insight-mutation.js.map +1 -0
- package/dist/insight-promotion.d.ts +89 -0
- package/dist/insight-promotion.d.ts.map +1 -0
- package/dist/insight-promotion.js +278 -0
- package/dist/insight-promotion.js.map +1 -0
- package/dist/insight-task-bridge.d.ts +77 -0
- package/dist/insight-task-bridge.d.ts.map +1 -0
- package/dist/insight-task-bridge.js +556 -0
- package/dist/insight-task-bridge.js.map +1 -0
- package/dist/insights.d.ts +222 -0
- package/dist/insights.d.ts.map +1 -0
- package/dist/insights.js +871 -0
- package/dist/insights.js.map +1 -0
- package/dist/intake-pipeline.d.ts +74 -0
- package/dist/intake-pipeline.d.ts.map +1 -0
- package/dist/intake-pipeline.js +199 -0
- package/dist/intake-pipeline.js.map +1 -0
- package/dist/intensity.d.ts +31 -0
- package/dist/intensity.d.ts.map +1 -0
- package/dist/intensity.js +94 -0
- package/dist/intensity.js.map +1 -0
- package/dist/knowledge-auto-index.d.ts +37 -0
- package/dist/knowledge-auto-index.d.ts.map +1 -0
- package/dist/knowledge-auto-index.js +149 -0
- package/dist/knowledge-auto-index.js.map +1 -0
- package/dist/knowledge-docs.d.ts +45 -0
- package/dist/knowledge-docs.d.ts.map +1 -0
- package/dist/knowledge-docs.js +188 -0
- package/dist/knowledge-docs.js.map +1 -0
- package/dist/lane-config.d.ts +25 -0
- package/dist/lane-config.d.ts.map +1 -0
- package/dist/lane-config.js +105 -0
- package/dist/lane-config.js.map +1 -0
- package/dist/lineage.d.ts +86 -0
- package/dist/lineage.d.ts.map +1 -0
- package/dist/lineage.js +303 -0
- package/dist/lineage.js.map +1 -0
- package/dist/logStore.d.ts +25 -0
- package/dist/logStore.d.ts.map +1 -0
- package/dist/logStore.js +83 -0
- package/dist/logStore.js.map +1 -0
- package/dist/manage.d.ts +12 -0
- package/dist/manage.d.ts.map +1 -0
- package/dist/manage.js +253 -0
- package/dist/manage.js.map +1 -0
- package/dist/mcp.d.ts +5 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +604 -0
- package/dist/mcp.js.map +1 -0
- package/dist/memory.d.ts +47 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +149 -0
- package/dist/memory.js.map +1 -0
- package/dist/mention-ack.d.ts +80 -0
- package/dist/mention-ack.d.ts.map +1 -0
- package/dist/mention-ack.js +175 -0
- package/dist/mention-ack.js.map +1 -0
- package/dist/messageRouter.d.ts +60 -0
- package/dist/messageRouter.d.ts.map +1 -0
- package/dist/messageRouter.js +309 -0
- package/dist/messageRouter.js.map +1 -0
- package/dist/mutationAlert.d.ts +44 -0
- package/dist/mutationAlert.d.ts.map +1 -0
- package/dist/mutationAlert.js +174 -0
- package/dist/mutationAlert.js.map +1 -0
- package/dist/noise-budget.d.ts +136 -0
- package/dist/noise-budget.d.ts.map +1 -0
- package/dist/noise-budget.js +340 -0
- package/dist/noise-budget.js.map +1 -0
- package/dist/notifications.d.ts +67 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +253 -0
- package/dist/notifications.js.map +1 -0
- package/dist/openclaw.d.ts +34 -0
- package/dist/openclaw.d.ts.map +1 -0
- package/dist/openclaw.js +208 -0
- package/dist/openclaw.js.map +1 -0
- package/dist/pause-controls.d.ts +31 -0
- package/dist/pause-controls.d.ts.map +1 -0
- package/dist/pause-controls.js +130 -0
- package/dist/pause-controls.js.map +1 -0
- package/dist/pidlock.d.ts +25 -0
- package/dist/pidlock.d.ts.map +1 -0
- package/dist/pidlock.js +179 -0
- package/dist/pidlock.js.map +1 -0
- package/dist/policy.d.ts +139 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +264 -0
- package/dist/policy.js.map +1 -0
- package/dist/polls.d.ts +47 -0
- package/dist/polls.d.ts.map +1 -0
- package/dist/polls.js +162 -0
- package/dist/polls.js.map +1 -0
- package/dist/portability.d.ts +55 -0
- package/dist/portability.d.ts.map +1 -0
- package/dist/portability.js +292 -0
- package/dist/portability.js.map +1 -0
- package/dist/pr-integrity.d.ts +45 -0
- package/dist/pr-integrity.d.ts.map +1 -0
- package/dist/pr-integrity.js +124 -0
- package/dist/pr-integrity.js.map +1 -0
- package/dist/prAutoMerge.d.ts +62 -0
- package/dist/prAutoMerge.d.ts.map +1 -0
- package/dist/prAutoMerge.js +493 -0
- package/dist/prAutoMerge.js.map +1 -0
- package/dist/preflight.d.ts +66 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +864 -0
- package/dist/preflight.js.map +1 -0
- package/dist/presence.d.ts +98 -0
- package/dist/presence.d.ts.map +1 -0
- package/dist/presence.js +347 -0
- package/dist/presence.js.map +1 -0
- package/dist/provisioning.d.ts +101 -0
- package/dist/provisioning.d.ts.map +1 -0
- package/dist/provisioning.js +430 -0
- package/dist/provisioning.js.map +1 -0
- package/dist/reflection-automation.d.ts +59 -0
- package/dist/reflection-automation.d.ts.map +1 -0
- package/dist/reflection-automation.js +350 -0
- package/dist/reflection-automation.js.map +1 -0
- package/dist/reflections.d.ts +65 -0
- package/dist/reflections.d.ts.map +1 -0
- package/dist/reflections.js +306 -0
- package/dist/reflections.js.map +1 -0
- package/dist/release.d.ts +67 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +275 -0
- package/dist/release.js.map +1 -0
- package/dist/request-tracker.d.ts +36 -0
- package/dist/request-tracker.d.ts.map +1 -0
- package/dist/request-tracker.js +109 -0
- package/dist/request-tracker.js.map +1 -0
- package/dist/research.d.ts +75 -0
- package/dist/research.d.ts.map +1 -0
- package/dist/research.js +171 -0
- package/dist/research.js.map +1 -0
- package/dist/routing-approvals.d.ts +73 -0
- package/dist/routing-approvals.d.ts.map +1 -0
- package/dist/routing-approvals.js +88 -0
- package/dist/routing-approvals.js.map +1 -0
- package/dist/routing-override.d.ts +94 -0
- package/dist/routing-override.d.ts.map +1 -0
- package/dist/routing-override.js +290 -0
- package/dist/routing-override.js.map +1 -0
- package/dist/scope-routing.d.ts +18 -0
- package/dist/scope-routing.d.ts.map +1 -0
- package/dist/scope-routing.js +29 -0
- package/dist/scope-routing.js.map +1 -0
- package/dist/secrets.d.ts +77 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +287 -0
- package/dist/secrets.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +10887 -0
- package/dist/server.js.map +1 -0
- package/dist/service-probe.d.ts +53 -0
- package/dist/service-probe.d.ts.map +1 -0
- package/dist/service-probe.js +225 -0
- package/dist/service-probe.js.map +1 -0
- package/dist/shared-workspace-api.d.ts +73 -0
- package/dist/shared-workspace-api.d.ts.map +1 -0
- package/dist/shared-workspace-api.js +281 -0
- package/dist/shared-workspace-api.js.map +1 -0
- package/dist/shipped-heartbeat.d.ts +91 -0
- package/dist/shipped-heartbeat.d.ts.map +1 -0
- package/dist/shipped-heartbeat.js +272 -0
- package/dist/shipped-heartbeat.js.map +1 -0
- package/dist/starter-team.d.ts +23 -0
- package/dist/starter-team.d.ts.map +1 -0
- package/dist/starter-team.js +88 -0
- package/dist/starter-team.js.map +1 -0
- package/dist/suppression-ledger.d.ts +73 -0
- package/dist/suppression-ledger.d.ts.map +1 -0
- package/dist/suppression-ledger.js +125 -0
- package/dist/suppression-ledger.js.map +1 -0
- package/dist/system-loop-state.d.ts +4 -0
- package/dist/system-loop-state.d.ts.map +1 -0
- package/dist/system-loop-state.js +40 -0
- package/dist/system-loop-state.js.map +1 -0
- package/dist/taskCommentIngest.d.ts +43 -0
- package/dist/taskCommentIngest.d.ts.map +1 -0
- package/dist/taskCommentIngest.js +59 -0
- package/dist/taskCommentIngest.js.map +1 -0
- package/dist/taskPrecheck.d.ts +20 -0
- package/dist/taskPrecheck.d.ts.map +1 -0
- package/dist/taskPrecheck.js +329 -0
- package/dist/taskPrecheck.js.map +1 -0
- package/dist/taskStateSync.d.ts +8 -0
- package/dist/taskStateSync.d.ts.map +1 -0
- package/dist/taskStateSync.js +79 -0
- package/dist/taskStateSync.js.map +1 -0
- package/dist/tasks.d.ts +140 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +1281 -0
- package/dist/tasks.js.map +1 -0
- package/dist/team-config.d.ts +24 -0
- package/dist/team-config.d.ts.map +1 -0
- package/dist/team-config.js +221 -0
- package/dist/team-config.js.map +1 -0
- package/dist/team-doctor.d.ts +22 -0
- package/dist/team-doctor.d.ts.map +1 -0
- package/dist/team-doctor.js +270 -0
- package/dist/team-doctor.js.map +1 -0
- package/dist/team-pulse.d.ts +52 -0
- package/dist/team-pulse.d.ts.map +1 -0
- package/dist/team-pulse.js +176 -0
- package/dist/team-pulse.js.map +1 -0
- package/dist/telemetry.d.ts +74 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +256 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/test-task-filter.d.ts +21 -0
- package/dist/test-task-filter.d.ts.map +1 -0
- package/dist/test-task-filter.js +48 -0
- package/dist/test-task-filter.js.map +1 -0
- package/dist/types.d.ts +126 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/usage-tracking.d.ts +101 -0
- package/dist/usage-tracking.d.ts.map +1 -0
- package/dist/usage-tracking.js +325 -0
- package/dist/usage-tracking.js.map +1 -0
- package/dist/vector-store.d.ts +87 -0
- package/dist/vector-store.d.ts.map +1 -0
- package/dist/vector-store.js +247 -0
- package/dist/vector-store.js.map +1 -0
- package/dist/watchdog/idleNudgeLane.d.ts +22 -0
- package/dist/watchdog/idleNudgeLane.d.ts.map +1 -0
- package/dist/watchdog/idleNudgeLane.js +98 -0
- package/dist/watchdog/idleNudgeLane.js.map +1 -0
- package/dist/webhooks.d.ts +103 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +398 -0
- package/dist/webhooks.js.map +1 -0
- package/dist/working-contract.d.ts +42 -0
- package/dist/working-contract.d.ts.map +1 -0
- package/dist/working-contract.js +228 -0
- package/dist/working-contract.js.map +1 -0
- package/dist/ws-heartbeat.d.ts +66 -0
- package/dist/ws-heartbeat.d.ts.map +1 -0
- package/dist/ws-heartbeat.js +174 -0
- package/dist/ws-heartbeat.js.map +1 -0
- package/package.json +87 -0
- package/plugins/reflectt-channel/README.md +96 -0
- package/plugins/reflectt-channel/index.ts +789 -0
- package/plugins/reflectt-channel/openclaw.plugin.json +23 -0
- package/plugins/reflectt-channel/package.json +23 -0
- package/plugins/reflectt-channel/src/channel.ts +433 -0
- package/plugins/reflectt-channel/src/types.ts +29 -0
- package/public/avatars/echo.png +0 -0
- package/public/avatars/harmony.png +0 -0
- package/public/avatars/kai.png +0 -0
- package/public/avatars/link.png +0 -0
- package/public/avatars/pixel.png +0 -0
- package/public/avatars/rhythm.png +0 -0
- package/public/avatars/ryan.png +0 -0
- package/public/avatars/sage.png +0 -0
- package/public/avatars/scout.png +0 -0
- package/public/avatars/spark.png +0 -0
- package/public/dashboard-animations.css +381 -0
- package/public/dashboard.js +3479 -0
- package/public/docs.md +1062 -0
- package/public/file-upload-mock.html +1097 -0
- package/public/og-card.png +0 -0
- package/public/ui-kit.html +318 -0
- package/public/widget/feedback.js +194 -0
package/dist/cloud.js
ADDED
|
@@ -0,0 +1,962 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) Reflectt AI
|
|
3
|
+
/**
|
|
4
|
+
* Cloud Integration Module
|
|
5
|
+
*
|
|
6
|
+
* Connects reflectt-node to Reflectt Cloud via @reflectt/host-agent SDK.
|
|
7
|
+
* When REFLECTT_HOST_TOKEN is set, this module:
|
|
8
|
+
* 1. Registers with the cloud on startup
|
|
9
|
+
* 2. Sends periodic heartbeats with agent presence + task counts
|
|
10
|
+
* 3. Syncs local task state to the cloud
|
|
11
|
+
*
|
|
12
|
+
* When env vars are not set, this module does nothing (graceful skip).
|
|
13
|
+
*/
|
|
14
|
+
import { presenceManager } from './presence.js';
|
|
15
|
+
import { taskManager } from './tasks.js';
|
|
16
|
+
import { chatManager } from './chat.js';
|
|
17
|
+
import { slotManager } from './canvas-slots.js';
|
|
18
|
+
import { getDb } from './db.js';
|
|
19
|
+
import { getUsageSummary, getUsageByAgent, getUsageByModel, listCaps, checkCaps, getRoutingSuggestions } from './usage-tracking.js';
|
|
20
|
+
import { readFileSync, existsSync, watch } from 'fs';
|
|
21
|
+
import { join } from 'path';
|
|
22
|
+
import { REFLECTT_HOME } from './config.js';
|
|
23
|
+
/**
|
|
24
|
+
* Docker identity guard: detect when a container has inherited cloud
|
|
25
|
+
* credentials from a host volume mount. Without explicit opt-in, skip
|
|
26
|
+
* cloud integration to prevent the container from silently appearing
|
|
27
|
+
* as the host team.
|
|
28
|
+
*/
|
|
29
|
+
function isDockerIdentityInherited(fileConfig) {
|
|
30
|
+
// Only applies inside Docker
|
|
31
|
+
const isDocker = existsSync('/.dockerenv') || process.env.REFLECTT_HOME === '/data';
|
|
32
|
+
if (!isDocker)
|
|
33
|
+
return false;
|
|
34
|
+
// If credentials come from env vars (not config.json), the user explicitly set them — allow
|
|
35
|
+
if (process.env.REFLECTT_HOST_TOKEN || process.env.REFLECTT_HOST_ID)
|
|
36
|
+
return false;
|
|
37
|
+
// If config.json has cloud credentials and user didn't opt in, flag it
|
|
38
|
+
if (fileConfig?.hostId && fileConfig?.credential) {
|
|
39
|
+
if (process.env.REFLECTT_INHERIT_IDENTITY === '1' || process.env.REFLECTT_INHERIT_IDENTITY === 'true') {
|
|
40
|
+
console.log(`☁️ Docker identity guard: inheriting identity from config.json (REFLECTT_INHERIT_IDENTITY=1)`);
|
|
41
|
+
console.log(` Host: ${fileConfig.hostName || 'unnamed'} (hostId: ${fileConfig.hostId})`);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
const DEFAULT_HEARTBEAT_MS = 30_000;
|
|
49
|
+
const DEFAULT_TASK_SYNC_MS = 60_000;
|
|
50
|
+
const DEFAULT_CHAT_SYNC_MS = 5_000;
|
|
51
|
+
const DEFAULT_CHAT_SYNC_MIN_INTERVAL_MS = 1_500;
|
|
52
|
+
const DEFAULT_CHAT_SYNC_MAX_BACKOFF_MS = 30_000;
|
|
53
|
+
// Adaptive sync: idle detection + interval scaling
|
|
54
|
+
const IDLE_THRESHOLD_MS = 2 * 60_000; // 2 min without activity → idle mode
|
|
55
|
+
const IDLE_SYNC_MS = 60_000; // Slow sync when idle (60s)
|
|
56
|
+
const ACTIVE_CANVAS_SYNC_MS = 5_000; // Fast canvas sync when active
|
|
57
|
+
const ACTIVE_USAGE_SYNC_MS = 15_000; // Fast usage sync when active
|
|
58
|
+
let lastActivityAt = Date.now();
|
|
59
|
+
/** Mark recent activity (call from event handlers) */
|
|
60
|
+
export function markCloudActivity() {
|
|
61
|
+
lastActivityAt = Date.now();
|
|
62
|
+
}
|
|
63
|
+
/** Check if the system is idle */
|
|
64
|
+
function isIdle() {
|
|
65
|
+
return Date.now() - lastActivityAt > IDLE_THRESHOLD_MS;
|
|
66
|
+
}
|
|
67
|
+
let config = null;
|
|
68
|
+
let state = {
|
|
69
|
+
hostId: null,
|
|
70
|
+
credential: null,
|
|
71
|
+
heartbeatTimer: null,
|
|
72
|
+
taskSyncTimer: null,
|
|
73
|
+
chatSyncTimer: null,
|
|
74
|
+
canvasSyncTimer: null,
|
|
75
|
+
usageSyncTimer: null,
|
|
76
|
+
heartbeatCount: 0,
|
|
77
|
+
lastHeartbeat: null,
|
|
78
|
+
lastTaskSync: null,
|
|
79
|
+
lastChatSync: null,
|
|
80
|
+
lastCanvasSync: null,
|
|
81
|
+
errors: 0,
|
|
82
|
+
running: false,
|
|
83
|
+
startedAt: Date.now(),
|
|
84
|
+
};
|
|
85
|
+
let configWatcher = null;
|
|
86
|
+
/**
|
|
87
|
+
* Load cloud config from ~/.reflectt/config.json (written by `reflectt host connect`)
|
|
88
|
+
*/
|
|
89
|
+
function loadCloudConfigFromFile() {
|
|
90
|
+
try {
|
|
91
|
+
const configPath = join(REFLECTT_HOME, 'config.json');
|
|
92
|
+
if (!existsSync(configPath))
|
|
93
|
+
return null;
|
|
94
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
95
|
+
if (!config?.cloud)
|
|
96
|
+
return null;
|
|
97
|
+
return {
|
|
98
|
+
cloudUrl: config.cloud.cloudUrl,
|
|
99
|
+
hostId: config.cloud.hostId,
|
|
100
|
+
credential: config.cloud.credential,
|
|
101
|
+
hostName: config.cloud.hostName,
|
|
102
|
+
hostType: config.cloud.hostType,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Check if cloud integration is configured
|
|
111
|
+
*/
|
|
112
|
+
export function isCloudConfigured() {
|
|
113
|
+
// Check env vars first
|
|
114
|
+
if (process.env.REFLECTT_HOST_TOKEN)
|
|
115
|
+
return true;
|
|
116
|
+
if (process.env.REFLECTT_HOST_ID && process.env.REFLECTT_HOST_CREDENTIAL)
|
|
117
|
+
return true;
|
|
118
|
+
// Check config.json (written by `reflectt host connect`)
|
|
119
|
+
const fileConfig = loadCloudConfigFromFile();
|
|
120
|
+
return Boolean(fileConfig?.hostId && fileConfig?.credential);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get current cloud connection status
|
|
124
|
+
*/
|
|
125
|
+
export function getCloudStatus() {
|
|
126
|
+
const configured = isCloudConfigured();
|
|
127
|
+
const registered = state.hostId !== null;
|
|
128
|
+
const connected = registered && state.running && state.heartbeatCount > 0;
|
|
129
|
+
// Derive a user-friendly connection phase for the UI
|
|
130
|
+
let phase;
|
|
131
|
+
if (!configured)
|
|
132
|
+
phase = 'unconfigured';
|
|
133
|
+
else if (state.errors > 0 && !connected)
|
|
134
|
+
phase = 'error';
|
|
135
|
+
else if (!registered)
|
|
136
|
+
phase = 'registering';
|
|
137
|
+
else if (!connected)
|
|
138
|
+
phase = 'configured';
|
|
139
|
+
else
|
|
140
|
+
phase = 'connected';
|
|
141
|
+
// Sync health: count dirty/pending records
|
|
142
|
+
let dirtyTaskCount = 0;
|
|
143
|
+
try {
|
|
144
|
+
const db = getDb();
|
|
145
|
+
const row = db.prepare("SELECT COUNT(*) as cnt FROM sync_ledger WHERE record_type='task' AND (cloud_synced_at IS NULL OR cloud_synced_at < local_updated_at OR sync_status != 'synced')").get();
|
|
146
|
+
dirtyTaskCount = row.cnt;
|
|
147
|
+
}
|
|
148
|
+
catch { /* DB may not be ready */ }
|
|
149
|
+
return {
|
|
150
|
+
configured,
|
|
151
|
+
registered,
|
|
152
|
+
connected,
|
|
153
|
+
phase,
|
|
154
|
+
hostId: state.hostId,
|
|
155
|
+
running: state.running,
|
|
156
|
+
heartbeatCount: state.heartbeatCount,
|
|
157
|
+
lastHeartbeat: state.lastHeartbeat,
|
|
158
|
+
lastTaskSync: state.lastTaskSync,
|
|
159
|
+
lastChatSync: state.lastChatSync,
|
|
160
|
+
lastCanvasSync: state.lastCanvasSync,
|
|
161
|
+
errors: state.errors,
|
|
162
|
+
uptimeMs: state.running ? Date.now() - state.startedAt : 0,
|
|
163
|
+
syncHealth: {
|
|
164
|
+
dirtyTaskCount,
|
|
165
|
+
healthy: dirtyTaskCount < 50,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Initialize and start cloud integration.
|
|
171
|
+
* Call this after the server is listening.
|
|
172
|
+
*/
|
|
173
|
+
export async function startCloudIntegration() {
|
|
174
|
+
if (!isCloudConfigured()) {
|
|
175
|
+
console.log('☁️ Cloud integration: skipped (REFLECTT_HOST_TOKEN not set)');
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Load from env vars first, then fall back to config.json
|
|
179
|
+
const fileConfig = loadCloudConfigFromFile();
|
|
180
|
+
// Docker identity guard: refuse to connect with inherited credentials
|
|
181
|
+
if (isDockerIdentityInherited(fileConfig)) {
|
|
182
|
+
console.warn('');
|
|
183
|
+
console.warn('⚠️ Docker identity guard: found cloud credentials in config.json');
|
|
184
|
+
console.warn(` This container would connect as "${fileConfig?.hostName || 'unknown'}" (hostId: ${fileConfig?.hostId})`);
|
|
185
|
+
console.warn(' This likely means you mounted a host directory containing existing team data.');
|
|
186
|
+
console.warn('');
|
|
187
|
+
console.warn(' To fix:');
|
|
188
|
+
console.warn(' • Use a named volume (docker-compose default) for a clean identity');
|
|
189
|
+
console.warn(' • Or set REFLECTT_INHERIT_IDENTITY=1 to intentionally reuse this identity');
|
|
190
|
+
console.warn('');
|
|
191
|
+
console.warn(' Cloud integration skipped to prevent identity collision.');
|
|
192
|
+
console.warn('');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
config = {
|
|
196
|
+
cloudUrl: (process.env.REFLECTT_CLOUD_URL || fileConfig?.cloudUrl || 'https://api.reflectt.ai').replace(/\/+$/, ''),
|
|
197
|
+
token: process.env.REFLECTT_HOST_TOKEN || '',
|
|
198
|
+
hostName: process.env.REFLECTT_HOST_NAME || fileConfig?.hostName || 'unnamed-host',
|
|
199
|
+
hostType: process.env.REFLECTT_HOST_TYPE || fileConfig?.hostType || 'openclaw',
|
|
200
|
+
heartbeatIntervalMs: Number(process.env.REFLECTT_HEARTBEAT_MS) || DEFAULT_HEARTBEAT_MS,
|
|
201
|
+
taskSyncIntervalMs: Number(process.env.REFLECTT_TASK_SYNC_MS) || DEFAULT_TASK_SYNC_MS,
|
|
202
|
+
capabilities: (process.env.REFLECTT_HOST_CAPABILITIES || 'tasks,chat,presence').split(',').map(s => s.trim()),
|
|
203
|
+
};
|
|
204
|
+
console.log(`☁️ Cloud integration: connecting to ${config.cloudUrl}`);
|
|
205
|
+
console.log(` Host: ${config.hostName} (${config.hostType})`);
|
|
206
|
+
if (fileConfig?.hostId)
|
|
207
|
+
console.log(` Source: config.json (auto-connect from host connect)`);
|
|
208
|
+
// Check if we already have a persisted host ID + credential (env or config.json)
|
|
209
|
+
const persistedHostId = process.env.REFLECTT_HOST_ID || fileConfig?.hostId;
|
|
210
|
+
const persistedCredential = process.env.REFLECTT_HOST_CREDENTIAL || fileConfig?.credential;
|
|
211
|
+
if (persistedHostId && persistedCredential) {
|
|
212
|
+
state.hostId = persistedHostId;
|
|
213
|
+
state.credential = persistedCredential;
|
|
214
|
+
console.log(` ✅ Using persisted credential (hostId: ${state.hostId})`);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
// Register with cloud via /api/hosts/claim
|
|
218
|
+
// Cloud API expects: { joinToken, name, capabilities? }
|
|
219
|
+
// Cloud API returns: { host: { id, ... }, credential: { token, revealPolicy } }
|
|
220
|
+
try {
|
|
221
|
+
const result = await cloudPost('/api/hosts/claim', {
|
|
222
|
+
joinToken: config.token,
|
|
223
|
+
name: config.hostName,
|
|
224
|
+
capabilities: config.capabilities,
|
|
225
|
+
});
|
|
226
|
+
if (result.data?.host?.id && result.data?.credential?.token) {
|
|
227
|
+
state.hostId = result.data.host.id;
|
|
228
|
+
state.credential = result.data.credential.token;
|
|
229
|
+
console.log(` ✅ Registered (hostId: ${state.hostId})`);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
console.warn(` ⚠ Registration failed: ${result.error || 'unexpected response shape'}`);
|
|
233
|
+
state.errors++;
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
console.warn(` ⚠ Registration failed: ${err?.message || 'network error'}`);
|
|
239
|
+
state.errors++;
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
// Start loops
|
|
244
|
+
state.running = true;
|
|
245
|
+
state.startedAt = Date.now();
|
|
246
|
+
// Immediate first heartbeat
|
|
247
|
+
sendHeartbeat().catch(() => { });
|
|
248
|
+
state.heartbeatTimer = setInterval(() => {
|
|
249
|
+
sendHeartbeat().catch(() => { });
|
|
250
|
+
pollAndProcessCommands().catch(() => { }); // Piggyback on heartbeat tick
|
|
251
|
+
}, config.heartbeatIntervalMs);
|
|
252
|
+
state.taskSyncTimer = setInterval(() => {
|
|
253
|
+
syncTasks().catch(() => { });
|
|
254
|
+
}, config.taskSyncIntervalMs);
|
|
255
|
+
// Chat sync — event-driven with adaptive polling fallback
|
|
256
|
+
// When active: 5s poll. When idle: 60s poll. Events always trigger immediate sync.
|
|
257
|
+
const chatSyncActiveMs = Number(process.env.REFLECTT_CHAT_SYNC_MS) || DEFAULT_CHAT_SYNC_MS;
|
|
258
|
+
let lastChatPollAt = 0;
|
|
259
|
+
requestChatSync('startup').catch(() => { });
|
|
260
|
+
state.chatSyncTimer = setInterval(() => {
|
|
261
|
+
const now = Date.now();
|
|
262
|
+
const interval = isIdle() ? IDLE_SYNC_MS : chatSyncActiveMs;
|
|
263
|
+
if (now - lastChatPollAt < interval)
|
|
264
|
+
return;
|
|
265
|
+
lastChatPollAt = now;
|
|
266
|
+
requestChatSync('interval').catch(() => { });
|
|
267
|
+
}, chatSyncActiveMs);
|
|
268
|
+
// Event-driven: sync immediately when new messages arrive (debounced 500ms)
|
|
269
|
+
let chatSyncDebounce = null;
|
|
270
|
+
chatManager.subscribe(() => {
|
|
271
|
+
if (!state.running)
|
|
272
|
+
return;
|
|
273
|
+
markCloudActivity(); // Mark as active on new chat
|
|
274
|
+
if (chatSyncDebounce)
|
|
275
|
+
clearTimeout(chatSyncDebounce);
|
|
276
|
+
chatSyncDebounce = setTimeout(() => {
|
|
277
|
+
requestChatSync('event').catch(() => { });
|
|
278
|
+
}, 500);
|
|
279
|
+
});
|
|
280
|
+
// Task changes also mark activity (ensures burst mode on task updates)
|
|
281
|
+
taskManager.subscribe(() => {
|
|
282
|
+
if (!state.running)
|
|
283
|
+
return;
|
|
284
|
+
markCloudActivity();
|
|
285
|
+
});
|
|
286
|
+
// Canvas slot updates mark activity (ensures burst mode on canvas changes)
|
|
287
|
+
slotManager.subscribe(() => {
|
|
288
|
+
if (!state.running)
|
|
289
|
+
return;
|
|
290
|
+
markCloudActivity();
|
|
291
|
+
});
|
|
292
|
+
// Canvas sync — adaptive: 5s when active, 60s when idle
|
|
293
|
+
// Uses a single 5s tick that skips when idle (unless enough time has passed)
|
|
294
|
+
let lastCanvasSyncAt = 0;
|
|
295
|
+
syncCanvas().catch(() => { });
|
|
296
|
+
state.canvasSyncTimer = setInterval(() => {
|
|
297
|
+
const now = Date.now();
|
|
298
|
+
const interval = isIdle() ? IDLE_SYNC_MS : ACTIVE_CANVAS_SYNC_MS;
|
|
299
|
+
if (now - lastCanvasSyncAt < interval)
|
|
300
|
+
return;
|
|
301
|
+
lastCanvasSyncAt = now;
|
|
302
|
+
syncCanvas().catch(() => { });
|
|
303
|
+
}, ACTIVE_CANVAS_SYNC_MS);
|
|
304
|
+
// Usage sync — adaptive: 15s when active, 60s when idle
|
|
305
|
+
let lastUsageSyncAt = 0;
|
|
306
|
+
syncUsage().catch(() => { });
|
|
307
|
+
state.usageSyncTimer = setInterval(() => {
|
|
308
|
+
const now = Date.now();
|
|
309
|
+
const interval = isIdle() ? IDLE_SYNC_MS : ACTIVE_USAGE_SYNC_MS;
|
|
310
|
+
if (now - lastUsageSyncAt < interval)
|
|
311
|
+
return;
|
|
312
|
+
lastUsageSyncAt = now;
|
|
313
|
+
syncUsage().catch(() => { });
|
|
314
|
+
}, ACTIVE_USAGE_SYNC_MS);
|
|
315
|
+
// Command polling — adaptive: 10s active, 60s idle
|
|
316
|
+
// Uses the same tick as canvas (5s) with interval gate
|
|
317
|
+
pollAndProcessCommands().catch(() => { });
|
|
318
|
+
console.log(` ✅ Heartbeat every ${config.heartbeatIntervalMs / 1000}s, task sync every ${config.taskSyncIntervalMs / 1000}s`);
|
|
319
|
+
console.log(` 📊 Adaptive sync: chat/canvas/usage ${chatSyncActiveMs / 1000}s active → ${IDLE_SYNC_MS / 1000}s idle (idle after ${IDLE_THRESHOLD_MS / 1000}s)`);
|
|
320
|
+
console.log(` 📬 Command polling: ${COMMAND_POLL_ACTIVE_MS / 1000}s active → ${COMMAND_POLL_IDLE_MS / 1000}s idle`);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Stop cloud integration (call on shutdown)
|
|
324
|
+
*/
|
|
325
|
+
/**
|
|
326
|
+
* Watch config.json for changes and auto-start cloud integration.
|
|
327
|
+
* Enables zero-restart enrollment: agent writes config.json via
|
|
328
|
+
* `reflectt host connect`, running server auto-detects and connects.
|
|
329
|
+
*/
|
|
330
|
+
export function watchConfigForCloudChanges() {
|
|
331
|
+
if (configWatcher)
|
|
332
|
+
return;
|
|
333
|
+
try {
|
|
334
|
+
let debounce = null;
|
|
335
|
+
configWatcher = watch(join(REFLECTT_HOME), { persistent: false }, (_event, filename) => {
|
|
336
|
+
if (filename !== 'config.json')
|
|
337
|
+
return;
|
|
338
|
+
if (debounce)
|
|
339
|
+
clearTimeout(debounce);
|
|
340
|
+
debounce = setTimeout(async () => {
|
|
341
|
+
debounce = null;
|
|
342
|
+
if (state.running)
|
|
343
|
+
return; // already connected, skip
|
|
344
|
+
const fileConfig = loadCloudConfigFromFile();
|
|
345
|
+
if (!fileConfig?.hostId || !fileConfig?.credential)
|
|
346
|
+
return;
|
|
347
|
+
console.log('☁️ Config change detected — auto-starting cloud integration...');
|
|
348
|
+
try {
|
|
349
|
+
await startCloudIntegration();
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
console.warn(`☁️ Cloud auto-start failed: ${err?.message || err}`);
|
|
353
|
+
}
|
|
354
|
+
}, 1000);
|
|
355
|
+
});
|
|
356
|
+
console.log(`☁️ Watching ${REFLECTT_HOME}/config.json for cloud config changes`);
|
|
357
|
+
}
|
|
358
|
+
catch (err) {
|
|
359
|
+
console.warn(`☁️ Config watcher setup failed: ${err?.message || err}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
export function stopConfigWatcher() {
|
|
363
|
+
if (configWatcher) {
|
|
364
|
+
configWatcher.close();
|
|
365
|
+
configWatcher = null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
export function stopCloudIntegration() {
|
|
369
|
+
state.running = false;
|
|
370
|
+
if (state.heartbeatTimer) {
|
|
371
|
+
clearInterval(state.heartbeatTimer);
|
|
372
|
+
state.heartbeatTimer = null;
|
|
373
|
+
}
|
|
374
|
+
if (state.taskSyncTimer) {
|
|
375
|
+
clearInterval(state.taskSyncTimer);
|
|
376
|
+
state.taskSyncTimer = null;
|
|
377
|
+
}
|
|
378
|
+
if (state.chatSyncTimer) {
|
|
379
|
+
clearInterval(state.chatSyncTimer);
|
|
380
|
+
state.chatSyncTimer = null;
|
|
381
|
+
}
|
|
382
|
+
if (state.canvasSyncTimer) {
|
|
383
|
+
clearInterval(state.canvasSyncTimer);
|
|
384
|
+
state.canvasSyncTimer = null;
|
|
385
|
+
}
|
|
386
|
+
if (state.usageSyncTimer) {
|
|
387
|
+
clearInterval(state.usageSyncTimer);
|
|
388
|
+
state.usageSyncTimer = null;
|
|
389
|
+
}
|
|
390
|
+
console.log('☁️ Cloud integration: stopped');
|
|
391
|
+
}
|
|
392
|
+
// ---- Data providers ----
|
|
393
|
+
function getAgents() {
|
|
394
|
+
const presences = presenceManager.getAllPresence();
|
|
395
|
+
return presences.map(p => ({
|
|
396
|
+
name: p.agent,
|
|
397
|
+
status: p.status === 'working' || p.status === 'reviewing' ? 'active'
|
|
398
|
+
: p.status === 'offline' ? 'offline'
|
|
399
|
+
: 'idle',
|
|
400
|
+
currentTask: p.task,
|
|
401
|
+
lastSeen: p.lastUpdate,
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
function getTasks() {
|
|
405
|
+
const tasks = taskManager.listTasks({});
|
|
406
|
+
return tasks.map(t => ({
|
|
407
|
+
id: t.id,
|
|
408
|
+
title: t.title,
|
|
409
|
+
status: t.status,
|
|
410
|
+
assignee: t.assignee,
|
|
411
|
+
priority: t.priority,
|
|
412
|
+
updatedAt: t.updatedAt || t.createdAt,
|
|
413
|
+
}));
|
|
414
|
+
}
|
|
415
|
+
// ---- Cloud communication ----
|
|
416
|
+
async function sendHeartbeat() {
|
|
417
|
+
if (!state.hostId || !config)
|
|
418
|
+
return;
|
|
419
|
+
const agents = getAgents();
|
|
420
|
+
const tasks = getTasks();
|
|
421
|
+
const doingTasks = tasks.filter(t => t.status === 'doing');
|
|
422
|
+
// Cloud API: POST /api/hosts/:hostId/heartbeat
|
|
423
|
+
// Expects: { status, agents?, activeTasks? }
|
|
424
|
+
const hostStatus = agents.some(a => a.status === 'active') ? 'online'
|
|
425
|
+
: agents.length > 0 ? 'degraded'
|
|
426
|
+
: 'online';
|
|
427
|
+
const result = await cloudPost(`/api/hosts/${state.hostId}/heartbeat`, {
|
|
428
|
+
contractVersion: 'host-heartbeat.v1',
|
|
429
|
+
status: hostStatus,
|
|
430
|
+
timestamp: Date.now(),
|
|
431
|
+
agents: agents.map(a => ({
|
|
432
|
+
id: a.name,
|
|
433
|
+
name: a.name,
|
|
434
|
+
status: a.status,
|
|
435
|
+
currentTaskId: a.currentTask || undefined,
|
|
436
|
+
lastSeenAt: a.lastSeen || Date.now(),
|
|
437
|
+
})),
|
|
438
|
+
activeTasks: doingTasks.map(t => ({
|
|
439
|
+
id: t.id,
|
|
440
|
+
title: t.title,
|
|
441
|
+
status: t.status,
|
|
442
|
+
assignee: t.assignee || undefined,
|
|
443
|
+
priority: t.priority || undefined,
|
|
444
|
+
updatedAt: t.updatedAt || Date.now(),
|
|
445
|
+
})),
|
|
446
|
+
source: {
|
|
447
|
+
hostId: state.hostId,
|
|
448
|
+
hostName: config.hostName,
|
|
449
|
+
hostType: config.hostType,
|
|
450
|
+
uptimeMs: Date.now() - state.startedAt,
|
|
451
|
+
},
|
|
452
|
+
});
|
|
453
|
+
if (result.success || result.data) {
|
|
454
|
+
state.lastHeartbeat = Date.now();
|
|
455
|
+
state.heartbeatCount++;
|
|
456
|
+
// Reset consecutive error count on success
|
|
457
|
+
if (state.errors > 0) {
|
|
458
|
+
console.log(`☁️ Heartbeat recovered after ${state.errors} errors`);
|
|
459
|
+
state.errors = 0;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
state.errors++;
|
|
464
|
+
if (state.errors <= 5 || state.errors % 20 === 0) {
|
|
465
|
+
console.warn(`☁️ Heartbeat failed (${state.errors} consecutive): ${result.error}`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function refreshTaskLedger(tasks) {
|
|
470
|
+
const db = getDb();
|
|
471
|
+
const snapshotIds = new Set(tasks.map((task) => task.id));
|
|
472
|
+
const upsert = db.prepare(`
|
|
473
|
+
INSERT INTO sync_ledger (
|
|
474
|
+
record_type,
|
|
475
|
+
record_id,
|
|
476
|
+
local_updated_at,
|
|
477
|
+
cloud_synced_at,
|
|
478
|
+
sync_status,
|
|
479
|
+
attempt_count,
|
|
480
|
+
last_error
|
|
481
|
+
) VALUES ('task', ?, ?, NULL, 'pending', 0, NULL)
|
|
482
|
+
ON CONFLICT(record_type, record_id) DO UPDATE SET
|
|
483
|
+
local_updated_at = excluded.local_updated_at,
|
|
484
|
+
sync_status = CASE
|
|
485
|
+
WHEN sync_ledger.local_updated_at = excluded.local_updated_at THEN sync_ledger.sync_status
|
|
486
|
+
ELSE 'pending'
|
|
487
|
+
END,
|
|
488
|
+
last_error = CASE
|
|
489
|
+
WHEN sync_ledger.local_updated_at = excluded.local_updated_at THEN sync_ledger.last_error
|
|
490
|
+
ELSE NULL
|
|
491
|
+
END
|
|
492
|
+
`);
|
|
493
|
+
const listTaskLedgerIds = db.prepare(`
|
|
494
|
+
SELECT record_id
|
|
495
|
+
FROM sync_ledger
|
|
496
|
+
WHERE record_type = 'task'
|
|
497
|
+
`);
|
|
498
|
+
const deleteLedgerRow = db.prepare(`
|
|
499
|
+
DELETE FROM sync_ledger
|
|
500
|
+
WHERE record_type = 'task' AND record_id = ?
|
|
501
|
+
`);
|
|
502
|
+
const tx = db.transaction((snapshot) => {
|
|
503
|
+
for (const task of snapshot) {
|
|
504
|
+
const updatedAt = Number(task.updatedAt || Date.now());
|
|
505
|
+
upsert.run(task.id, updatedAt);
|
|
506
|
+
}
|
|
507
|
+
const ledgerIds = listTaskLedgerIds.all();
|
|
508
|
+
for (const row of ledgerIds) {
|
|
509
|
+
if (!snapshotIds.has(row.record_id)) {
|
|
510
|
+
deleteLedgerRow.run(row.record_id);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
tx(tasks);
|
|
515
|
+
}
|
|
516
|
+
function getDirtyTaskLedgerRows(limit = 200) {
|
|
517
|
+
const db = getDb();
|
|
518
|
+
return db.prepare(`
|
|
519
|
+
SELECT record_id, local_updated_at
|
|
520
|
+
FROM sync_ledger
|
|
521
|
+
WHERE record_type = 'task'
|
|
522
|
+
AND (
|
|
523
|
+
cloud_synced_at IS NULL
|
|
524
|
+
OR cloud_synced_at < local_updated_at
|
|
525
|
+
OR sync_status != 'synced'
|
|
526
|
+
)
|
|
527
|
+
ORDER BY local_updated_at ASC
|
|
528
|
+
LIMIT ?
|
|
529
|
+
`).all(limit);
|
|
530
|
+
}
|
|
531
|
+
function markTaskRowsSynced(rows, syncedAt) {
|
|
532
|
+
if (rows.length === 0)
|
|
533
|
+
return;
|
|
534
|
+
const db = getDb();
|
|
535
|
+
const markSynced = db.prepare(`
|
|
536
|
+
UPDATE sync_ledger
|
|
537
|
+
SET cloud_synced_at = ?,
|
|
538
|
+
sync_status = 'synced',
|
|
539
|
+
attempt_count = attempt_count + 1,
|
|
540
|
+
last_error = NULL
|
|
541
|
+
WHERE record_type = 'task'
|
|
542
|
+
AND record_id = ?
|
|
543
|
+
AND local_updated_at = ?
|
|
544
|
+
`);
|
|
545
|
+
const tx = db.transaction((items) => {
|
|
546
|
+
for (const row of items) {
|
|
547
|
+
markSynced.run(syncedAt, row.id, row.local_updated_at);
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
tx(rows);
|
|
551
|
+
}
|
|
552
|
+
function markTaskRowsErrored(rows, errorMessage) {
|
|
553
|
+
if (rows.length === 0)
|
|
554
|
+
return;
|
|
555
|
+
const db = getDb();
|
|
556
|
+
const markError = db.prepare(`
|
|
557
|
+
UPDATE sync_ledger
|
|
558
|
+
SET sync_status = 'error',
|
|
559
|
+
attempt_count = attempt_count + 1,
|
|
560
|
+
last_error = ?
|
|
561
|
+
WHERE record_type = 'task'
|
|
562
|
+
AND record_id = ?
|
|
563
|
+
AND local_updated_at = ?
|
|
564
|
+
`);
|
|
565
|
+
const tx = db.transaction((items) => {
|
|
566
|
+
for (const row of items) {
|
|
567
|
+
markError.run(errorMessage, row.id, row.local_updated_at);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
tx(rows);
|
|
571
|
+
}
|
|
572
|
+
async function syncTasks() {
|
|
573
|
+
if (!state.hostId || !config)
|
|
574
|
+
return;
|
|
575
|
+
const tasksSnapshot = getTasks();
|
|
576
|
+
refreshTaskLedger(tasksSnapshot);
|
|
577
|
+
const taskById = new Map(tasksSnapshot.map((task) => [task.id, task]));
|
|
578
|
+
const dirtyLedgerRows = getDirtyTaskLedgerRows();
|
|
579
|
+
const dirtyRows = dirtyLedgerRows
|
|
580
|
+
.map((ledgerRow) => {
|
|
581
|
+
const task = taskById.get(ledgerRow.record_id);
|
|
582
|
+
if (!task)
|
|
583
|
+
return null;
|
|
584
|
+
return {
|
|
585
|
+
id: task.id,
|
|
586
|
+
title: task.title,
|
|
587
|
+
status: task.status,
|
|
588
|
+
assignee: task.assignee,
|
|
589
|
+
priority: task.priority,
|
|
590
|
+
local_updated_at: ledgerRow.local_updated_at,
|
|
591
|
+
};
|
|
592
|
+
})
|
|
593
|
+
.filter((row) => row !== null);
|
|
594
|
+
if (dirtyRows.length === 0) {
|
|
595
|
+
state.lastTaskSync = Date.now();
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
// Alert when dirty count is high (potential sync backlog)
|
|
599
|
+
const DIRTY_ALERT_THRESHOLD = 50;
|
|
600
|
+
if (dirtyRows.length >= DIRTY_ALERT_THRESHOLD) {
|
|
601
|
+
console.warn(`[cloud-sync] High dirty task count: ${dirtyRows.length} records pending sync (threshold: ${DIRTY_ALERT_THRESHOLD})`);
|
|
602
|
+
}
|
|
603
|
+
const tasksPayload = dirtyRows.map((row) => ({
|
|
604
|
+
id: row.id,
|
|
605
|
+
title: row.title,
|
|
606
|
+
status: row.status,
|
|
607
|
+
assignee: row.assignee ?? undefined,
|
|
608
|
+
priority: row.priority ?? undefined,
|
|
609
|
+
updatedAt: new Date(row.local_updated_at).toISOString(),
|
|
610
|
+
}));
|
|
611
|
+
const result = await cloudPost(`/api/hosts/${state.hostId}/tasks/sync`, {
|
|
612
|
+
tasks: tasksPayload,
|
|
613
|
+
});
|
|
614
|
+
if (result.success || result.data) {
|
|
615
|
+
const syncedAt = Date.now();
|
|
616
|
+
markTaskRowsSynced(dirtyRows, syncedAt);
|
|
617
|
+
state.lastTaskSync = syncedAt;
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
const errorMessage = result.error || 'task sync failed';
|
|
621
|
+
markTaskRowsErrored(dirtyRows, errorMessage);
|
|
622
|
+
state.errors++;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// ---- Chat sync ----
|
|
626
|
+
/** Timestamp of last chat sync — only send messages newer than this */
|
|
627
|
+
let chatSyncCursor = Date.now() - 60_000; // Start with last minute of history
|
|
628
|
+
let chatSyncErrors = 0;
|
|
629
|
+
let chatSyncInFlight = false;
|
|
630
|
+
let chatSyncQueued = false;
|
|
631
|
+
let chatSyncTimerRef = null;
|
|
632
|
+
let chatSyncBackoffMs = 0;
|
|
633
|
+
let chatSyncNextAllowedAt = 0;
|
|
634
|
+
const chatSyncMinIntervalMs = Number(process.env.REFLECTT_CHAT_SYNC_MIN_INTERVAL_MS) || DEFAULT_CHAT_SYNC_MIN_INTERVAL_MS;
|
|
635
|
+
const chatSyncMaxBackoffMs = Number(process.env.REFLECTT_CHAT_SYNC_MAX_BACKOFF_MS) || DEFAULT_CHAT_SYNC_MAX_BACKOFF_MS;
|
|
636
|
+
function computeBackoffWithJitter(currentMs) {
|
|
637
|
+
const base = currentMs > 0 ? Math.min(currentMs * 2, chatSyncMaxBackoffMs) : 1_000;
|
|
638
|
+
const jitter = Math.floor(Math.random() * 500);
|
|
639
|
+
return Math.min(base + jitter, chatSyncMaxBackoffMs);
|
|
640
|
+
}
|
|
641
|
+
async function requestChatSync(_reason) {
|
|
642
|
+
if (!state.running)
|
|
643
|
+
return;
|
|
644
|
+
if (chatSyncInFlight) {
|
|
645
|
+
chatSyncQueued = true;
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
const now = Date.now();
|
|
649
|
+
if (now < chatSyncNextAllowedAt) {
|
|
650
|
+
const waitMs = Math.max(0, chatSyncNextAllowedAt - now);
|
|
651
|
+
if (!chatSyncTimerRef) {
|
|
652
|
+
chatSyncTimerRef = setTimeout(() => {
|
|
653
|
+
chatSyncTimerRef = null;
|
|
654
|
+
requestChatSync('event').catch(() => { });
|
|
655
|
+
}, waitMs);
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
await syncChat();
|
|
660
|
+
if (chatSyncQueued) {
|
|
661
|
+
chatSyncQueued = false;
|
|
662
|
+
await requestChatSync('event');
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
async function syncChat() {
|
|
666
|
+
if (!state.hostId || !config)
|
|
667
|
+
return;
|
|
668
|
+
if (chatSyncInFlight)
|
|
669
|
+
return;
|
|
670
|
+
chatSyncInFlight = true;
|
|
671
|
+
// Enforce minimum sync interval + active backoff window
|
|
672
|
+
const now = Date.now();
|
|
673
|
+
if (now < chatSyncNextAllowedAt) {
|
|
674
|
+
chatSyncInFlight = false;
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
// Get recent messages since last sync
|
|
678
|
+
const recentMessages = chatManager.getMessages({
|
|
679
|
+
since: chatSyncCursor,
|
|
680
|
+
limit: 50,
|
|
681
|
+
});
|
|
682
|
+
// Send to cloud and get pending outbound messages
|
|
683
|
+
const payload = recentMessages.map(m => ({
|
|
684
|
+
id: m.id,
|
|
685
|
+
from: m.from,
|
|
686
|
+
content: m.content,
|
|
687
|
+
timestamp: m.timestamp,
|
|
688
|
+
channel: m.channel || 'general',
|
|
689
|
+
}));
|
|
690
|
+
const result = await cloudPost(`/api/hosts/${state.hostId}/chat/sync`, { messages: payload });
|
|
691
|
+
if (result.success && result.data) {
|
|
692
|
+
state.lastChatSync = Date.now();
|
|
693
|
+
if (chatSyncErrors > 0) {
|
|
694
|
+
console.log(`☁️ [Chat] Sync recovered after ${chatSyncErrors} errors`);
|
|
695
|
+
chatSyncErrors = 0;
|
|
696
|
+
}
|
|
697
|
+
// Reset backoff window on success, enforce minimum interval
|
|
698
|
+
chatSyncBackoffMs = 0;
|
|
699
|
+
chatSyncNextAllowedAt = Date.now() + chatSyncMinIntervalMs;
|
|
700
|
+
// Update cursor to now
|
|
701
|
+
if (recentMessages.length > 0) {
|
|
702
|
+
chatSyncCursor = Math.max(...recentMessages.map(m => m.timestamp));
|
|
703
|
+
}
|
|
704
|
+
// Process pending outbound messages from cloud (dashboard user messages)
|
|
705
|
+
if (result.data.pending && result.data.pending.length > 0) {
|
|
706
|
+
for (const msg of result.data.pending) {
|
|
707
|
+
// Inject into local chat as if the user posted it
|
|
708
|
+
try {
|
|
709
|
+
await chatManager.sendMessage({
|
|
710
|
+
from: msg.from,
|
|
711
|
+
content: msg.content,
|
|
712
|
+
channel: msg.channel || 'general',
|
|
713
|
+
metadata: { source: 'cloud-relay', cloudMessageId: msg.id },
|
|
714
|
+
});
|
|
715
|
+
console.log(`☁️ [Chat] Relayed message from ${msg.from}: "${msg.content.slice(0, 50)}..."`);
|
|
716
|
+
}
|
|
717
|
+
catch (err) {
|
|
718
|
+
console.warn(`☁️ [Chat] Failed to relay message: ${err?.message}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
else {
|
|
724
|
+
chatSyncErrors++;
|
|
725
|
+
// Exponential backoff + jitter on repeated failures
|
|
726
|
+
chatSyncBackoffMs = computeBackoffWithJitter(chatSyncBackoffMs);
|
|
727
|
+
chatSyncNextAllowedAt = Date.now() + Math.max(chatSyncBackoffMs, chatSyncMinIntervalMs);
|
|
728
|
+
// Log first few, then every 20th to avoid spam
|
|
729
|
+
if (chatSyncErrors <= 3 || chatSyncErrors % 20 === 0) {
|
|
730
|
+
console.warn(`☁️ [Chat] Sync failed (${chatSyncErrors}): ${result.error}; next attempt in ~${chatSyncBackoffMs}ms`);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
chatSyncInFlight = false;
|
|
734
|
+
}
|
|
735
|
+
// ---- Canvas sync ----
|
|
736
|
+
let canvasSyncErrors = 0;
|
|
737
|
+
async function syncCanvas() {
|
|
738
|
+
if (!state.hostId || !config)
|
|
739
|
+
return;
|
|
740
|
+
// Get active (non-stale) canvas slots
|
|
741
|
+
const activeSlots = slotManager.getActive();
|
|
742
|
+
// Push to cloud
|
|
743
|
+
const result = await cloudPost(`/api/hosts/${state.hostId}/canvas`, { slots: activeSlots });
|
|
744
|
+
if (result.success && result.data) {
|
|
745
|
+
state.lastCanvasSync = Date.now();
|
|
746
|
+
if (canvasSyncErrors > 0) {
|
|
747
|
+
console.log(`☁️ [Canvas] Sync recovered after ${canvasSyncErrors} errors`);
|
|
748
|
+
canvasSyncErrors = 0;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
else {
|
|
752
|
+
canvasSyncErrors++;
|
|
753
|
+
if (canvasSyncErrors <= 3 || canvasSyncErrors % 20 === 0) {
|
|
754
|
+
console.warn(`☁️ [Canvas] Sync failed (${canvasSyncErrors}): ${result.error}`);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// ---- Usage Sync ----
|
|
759
|
+
let usageSyncErrors = 0;
|
|
760
|
+
async function syncUsage() {
|
|
761
|
+
if (!state.hostId || !config)
|
|
762
|
+
return;
|
|
763
|
+
try {
|
|
764
|
+
const since = Date.now() - 30 * 24 * 60 * 60 * 1000; // last 30 days
|
|
765
|
+
const summaries = getUsageSummary({ since, group_by: 'month' });
|
|
766
|
+
const summary = summaries.length > 0 ? summaries[0] : { period: 'monthly', total_cost_usd: 0, total_input_tokens: 0, total_output_tokens: 0, event_count: 0 };
|
|
767
|
+
const byAgent = getUsageByAgent({ since });
|
|
768
|
+
const byModel = getUsageByModel({ since });
|
|
769
|
+
const caps = listCaps();
|
|
770
|
+
const capStatuses = checkCaps();
|
|
771
|
+
const routingSuggestions = getRoutingSuggestions({ since });
|
|
772
|
+
const result = await cloudPost(`/api/hosts/${state.hostId}/usage/sync`, { summary, byAgent, byModel, caps, capStatuses, routingSuggestions });
|
|
773
|
+
if (result.success) {
|
|
774
|
+
if (usageSyncErrors > 0) {
|
|
775
|
+
console.log(`☁️ [Usage] Sync recovered after ${usageSyncErrors} errors`);
|
|
776
|
+
usageSyncErrors = 0;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
usageSyncErrors++;
|
|
781
|
+
if (usageSyncErrors <= 3 || usageSyncErrors % 20 === 0) {
|
|
782
|
+
console.warn(`☁️ [Usage] Sync failed (${usageSyncErrors}): ${result.error}`);
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
catch (err) {
|
|
787
|
+
usageSyncErrors++;
|
|
788
|
+
if (usageSyncErrors <= 3) {
|
|
789
|
+
console.warn(`☁️ [Usage] Sync error: ${err.message}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
// ---- Command polling + context_sync handler ----
|
|
794
|
+
const COMMAND_POLL_ACTIVE_MS = 10_000; // 10s when active
|
|
795
|
+
const COMMAND_POLL_IDLE_MS = 60_000; // 60s when idle
|
|
796
|
+
let commandPollErrors = 0;
|
|
797
|
+
let lastCommandPollAt = 0;
|
|
798
|
+
async function pollAndProcessCommands() {
|
|
799
|
+
if (!state.hostId || !config || !state.running)
|
|
800
|
+
return;
|
|
801
|
+
const now = Date.now();
|
|
802
|
+
const interval = isIdle() ? COMMAND_POLL_IDLE_MS : COMMAND_POLL_ACTIVE_MS;
|
|
803
|
+
if (now - lastCommandPollAt < interval)
|
|
804
|
+
return;
|
|
805
|
+
lastCommandPollAt = now;
|
|
806
|
+
const result = await cloudGet(`/api/hosts/${state.hostId}/commands?status=pending`);
|
|
807
|
+
if (!result.success || !result.data?.commands) {
|
|
808
|
+
commandPollErrors++;
|
|
809
|
+
if (commandPollErrors <= 3 || commandPollErrors % 20 === 0) {
|
|
810
|
+
console.warn(`☁️ [Commands] Poll failed (${commandPollErrors}): ${result.error}`);
|
|
811
|
+
}
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
if (commandPollErrors > 0) {
|
|
815
|
+
console.log(`☁️ [Commands] Poll recovered after ${commandPollErrors} errors`);
|
|
816
|
+
commandPollErrors = 0;
|
|
817
|
+
}
|
|
818
|
+
for (const cmd of result.data.commands) {
|
|
819
|
+
try {
|
|
820
|
+
await handleCommand(cmd);
|
|
821
|
+
}
|
|
822
|
+
catch (err) {
|
|
823
|
+
console.warn(`☁️ [Commands] Failed to handle ${cmd.type} (${cmd.id}): ${err?.message}`);
|
|
824
|
+
// Ack as failed so it doesn't re-run
|
|
825
|
+
await cloudPost(`/api/hosts/${state.hostId}/commands/${cmd.id}/ack`, {
|
|
826
|
+
action: 'fail',
|
|
827
|
+
error: err?.message || 'Handler error',
|
|
828
|
+
}).catch(() => { });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async function handleCommand(cmd) {
|
|
833
|
+
if (cmd.type === 'context_sync') {
|
|
834
|
+
await handleContextSync(cmd);
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
console.log(`☁️ [Commands] Unknown command type: ${cmd.type} (${cmd.id}) — skipping`);
|
|
838
|
+
// Ack unknown commands so they don't pile up
|
|
839
|
+
await cloudPost(`/api/hosts/${state.hostId}/commands/${cmd.id}/ack`, {
|
|
840
|
+
action: 'complete',
|
|
841
|
+
result: { skipped: true, reason: 'unknown_type' },
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
async function handleContextSync(cmd) {
|
|
846
|
+
if (!state.hostId)
|
|
847
|
+
return;
|
|
848
|
+
// Require explicit agent — no hardcoded fallback
|
|
849
|
+
const agent = cmd.payload?.agent?.trim();
|
|
850
|
+
if (!agent) {
|
|
851
|
+
console.warn(`☁️ [Commands] context_sync missing payload.agent (${cmd.id}) — failing`);
|
|
852
|
+
await cloudPost(`/api/hosts/${state.hostId}/commands/${cmd.id}/ack`, {
|
|
853
|
+
action: 'fail',
|
|
854
|
+
error: 'payload.agent is required',
|
|
855
|
+
});
|
|
856
|
+
return;
|
|
857
|
+
}
|
|
858
|
+
console.log(`☁️ [Commands] Processing context_sync for agent=${agent} (${cmd.id})`);
|
|
859
|
+
// Ack immediately (in-progress)
|
|
860
|
+
await cloudPost(`/api/hosts/${state.hostId}/commands/${cmd.id}/ack`, {
|
|
861
|
+
action: 'ack',
|
|
862
|
+
});
|
|
863
|
+
// Fetch context snapshot from local node
|
|
864
|
+
const port = process.env.REFLECTT_NODE_PORT || '4445';
|
|
865
|
+
let contextData;
|
|
866
|
+
try {
|
|
867
|
+
const localRes = await fetch(`http://127.0.0.1:${port}/context/inject/${encodeURIComponent(agent)}`);
|
|
868
|
+
if (!localRes.ok)
|
|
869
|
+
throw new Error(`Local context fetch failed: ${localRes.status}`);
|
|
870
|
+
contextData = await localRes.json();
|
|
871
|
+
}
|
|
872
|
+
catch (err) {
|
|
873
|
+
await cloudPost(`/api/hosts/${state.hostId}/commands/${cmd.id}/ack`, {
|
|
874
|
+
action: 'fail',
|
|
875
|
+
error: `Failed to fetch local context: ${err?.message}`,
|
|
876
|
+
});
|
|
877
|
+
throw err;
|
|
878
|
+
}
|
|
879
|
+
// Push to cloud — use computed_at from injection payload when available
|
|
880
|
+
const computedAt = (typeof contextData.computed_at === 'number' && contextData.computed_at > 0)
|
|
881
|
+
? contextData.computed_at
|
|
882
|
+
: Date.now();
|
|
883
|
+
const syncResult = await cloudPost(`/api/hosts/${state.hostId}/context/sync`, {
|
|
884
|
+
agent,
|
|
885
|
+
computed_at: computedAt,
|
|
886
|
+
budgets: contextData.budgets || { totalTokens: 0, layers: {} },
|
|
887
|
+
autosummary_enabled: Boolean(contextData.autosummary_enabled),
|
|
888
|
+
layers: contextData.layers || {},
|
|
889
|
+
});
|
|
890
|
+
if (syncResult.success) {
|
|
891
|
+
console.log(`☁️ [Commands] context_sync completed for ${agent} (${cmd.id})`);
|
|
892
|
+
await cloudPost(`/api/hosts/${state.hostId}/commands/${cmd.id}/ack`, {
|
|
893
|
+
action: 'complete',
|
|
894
|
+
result: { syncedAt: Date.now(), agent },
|
|
895
|
+
});
|
|
896
|
+
markCloudActivity(); // Mark as active
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
console.warn(`☁️ [Commands] context_sync failed for ${agent}: ${syncResult.error}`);
|
|
900
|
+
await cloudPost(`/api/hosts/${state.hostId}/commands/${cmd.id}/ack`, {
|
|
901
|
+
action: 'fail',
|
|
902
|
+
error: syncResult.error,
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
async function cloudGet(path) {
|
|
907
|
+
if (!config)
|
|
908
|
+
return { success: false, error: 'Not configured' };
|
|
909
|
+
try {
|
|
910
|
+
const url = `${config.cloudUrl}${path}`;
|
|
911
|
+
const headers = {};
|
|
912
|
+
if (state.credential) {
|
|
913
|
+
headers['Authorization'] = `Bearer ${state.credential}`;
|
|
914
|
+
}
|
|
915
|
+
else {
|
|
916
|
+
headers['Authorization'] = `Bearer ${config.token}`;
|
|
917
|
+
}
|
|
918
|
+
const response = await fetch(url, { method: 'GET', headers });
|
|
919
|
+
if (!response.ok) {
|
|
920
|
+
const errBody = await response.json().catch(() => ({}));
|
|
921
|
+
return { success: false, error: errBody.error || `HTTP ${response.status}` };
|
|
922
|
+
}
|
|
923
|
+
const payload = await response.json();
|
|
924
|
+
return { success: true, data: payload };
|
|
925
|
+
}
|
|
926
|
+
catch (err) {
|
|
927
|
+
return { success: false, error: err?.message || 'Request failed' };
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
async function cloudPost(path, body) {
|
|
931
|
+
if (!config)
|
|
932
|
+
return { success: false, error: 'Not configured' };
|
|
933
|
+
try {
|
|
934
|
+
const url = `${config.cloudUrl}${path}`;
|
|
935
|
+
const headers = {
|
|
936
|
+
'Content-Type': 'application/json',
|
|
937
|
+
};
|
|
938
|
+
// Use credential if registered, otherwise join token for enrollment
|
|
939
|
+
if (state.credential) {
|
|
940
|
+
headers['Authorization'] = `Bearer ${state.credential}`;
|
|
941
|
+
}
|
|
942
|
+
else {
|
|
943
|
+
headers['Authorization'] = `Bearer ${config.token}`;
|
|
944
|
+
}
|
|
945
|
+
const response = await fetch(url, {
|
|
946
|
+
method: 'POST',
|
|
947
|
+
headers,
|
|
948
|
+
body: JSON.stringify(body),
|
|
949
|
+
});
|
|
950
|
+
if (!response.ok) {
|
|
951
|
+
const errBody = await response.json().catch(() => ({}));
|
|
952
|
+
return { success: false, error: errBody.error || `HTTP ${response.status}` };
|
|
953
|
+
}
|
|
954
|
+
const payload = await response.json();
|
|
955
|
+
return { success: true, data: payload };
|
|
956
|
+
}
|
|
957
|
+
catch (err) {
|
|
958
|
+
// Don't increment errors here — callers handle error counting
|
|
959
|
+
return { success: false, error: err?.message || 'Request failed' };
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
//# sourceMappingURL=cloud.js.map
|