mixdog 0.7.1
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/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.gitattributes +34 -0
- package/.mcp.json +14 -0
- package/ARCHITECTURE.md +77 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +45 -0
- package/DATA-FLOW.md +79 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/SECURITY.md +138 -0
- package/UNINSTALL.md +112 -0
- package/agents/maintenance.md +5 -0
- package/agents/memory-classification.md +30 -0
- package/agents/scheduler-task.md +18 -0
- package/agents/webhook-handler.md +27 -0
- package/agents/worker.md +24 -0
- package/bin/bridge +133 -0
- package/bin/statusline-launcher.mjs +78 -0
- package/bin/statusline-lib.mjs +550 -0
- package/bin/statusline.mjs +607 -0
- package/bun.lock +802 -0
- package/commands/config.md +16 -0
- package/commands/doctor.md +13 -0
- package/commands/setup.md +17 -0
- package/defaults/cycle3-review-prompt.md +90 -0
- package/defaults/hidden-roles.json +65 -0
- package/defaults/memory-chunk-prompt.md +63 -0
- package/defaults/memory-promote-prompt.md +135 -0
- package/defaults/mixdog-config.template.json +27 -0
- package/defaults/user-workflow.json +8 -0
- package/defaults/user-workflow.md +12 -0
- package/hooks/hooks.json +73 -0
- package/hooks/lib/active-instance.cjs +77 -0
- package/hooks/lib/permission-evaluator.cjs +411 -0
- package/hooks/lib/permission-route.cjs +63 -0
- package/hooks/lib/permission-rules.cjs +170 -0
- package/hooks/lib/settings-loader.cjs +116 -0
- package/hooks/post-tool-use.cjs +84 -0
- package/hooks/pre-mcp-sandbox.cjs +158 -0
- package/hooks/pre-tool-subagent.cjs +253 -0
- package/hooks/session-start.cjs +1372 -0
- package/hooks/turn-timer.cjs +82 -0
- package/lib/claude-md-writer.cjs +386 -0
- package/lib/config-cjs.cjs +61 -0
- package/lib/hook-pipe-path.cjs +10 -0
- package/lib/keychain-cjs.cjs +263 -0
- package/lib/plugin-paths.cjs +61 -0
- package/lib/rules-builder.cjs +241 -0
- package/lib/text-utils.cjs +61 -0
- package/native/README.md +117 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +107 -0
- package/prompts/code-review.txt +16 -0
- package/prompts/security-audit.txt +17 -0
- package/rules/bridge/00-common.md +39 -0
- package/rules/bridge/20-skip-protocol.md +18 -0
- package/rules/bridge/30-explorer.md +33 -0
- package/rules/bridge/40-cycle1-agent.md +52 -0
- package/rules/bridge/41-cycle2-agent.md +62 -0
- package/rules/bridge/42-cycle3-agent.md +44 -0
- package/rules/lead/00-tool-lead.md +61 -0
- package/rules/lead/01-general.md +23 -0
- package/rules/lead/02-channels.md +49 -0
- package/rules/lead/03-team.md +27 -0
- package/rules/lead/04-workflow.md +20 -0
- package/rules/shared/00-language.md +14 -0
- package/rules/shared/01-tool.md +138 -0
- package/scripts/bootstrap.mjs +184 -0
- package/scripts/bridge-unify-smoke.mjs +308 -0
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/builtin-utils-smoke.mjs +392 -0
- package/scripts/check-json.mjs +45 -0
- package/scripts/check-syntax-changed.mjs +102 -0
- package/scripts/check-syntax.mjs +58 -0
- package/scripts/code-graph-batch.test.mjs +33 -0
- package/scripts/config-preserve-smoke.mjs +180 -0
- package/scripts/doctor.mjs +484 -0
- package/scripts/edit-normalize-fuzz.mjs +130 -0
- package/scripts/edit-normalize-smoke.mjs +401 -0
- package/scripts/edit-operation-smoke.mjs +369 -0
- package/scripts/edit2-smoke.mjs +63 -0
- package/scripts/fuzzy-e2e.mjs +28 -0
- package/scripts/fuzzy-smoke.mjs +26 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/guard-smoke.mjs +66 -0
- package/scripts/hidden-role-schema-smoke.mjs +162 -0
- package/scripts/hook-routing-smoke.mjs +29 -0
- package/scripts/inject-input.ps1 +204 -0
- package/scripts/io-complex-smoke.mjs +667 -0
- package/scripts/io-explore-bench.mjs +424 -0
- package/scripts/io-guardrails-smoke.mjs +205 -0
- package/scripts/io-mini-bench-baseline.json +11 -0
- package/scripts/io-mini-bench.mjs +216 -0
- package/scripts/io-route-harness.mjs +933 -0
- package/scripts/io-telemetry-report.mjs +691 -0
- package/scripts/mutation-bench.mjs +564 -0
- package/scripts/mutation-io-smoke.mjs +1081 -0
- package/scripts/native-patch-bridge-smoke.mjs +288 -0
- package/scripts/native-patch-smoke.mjs +304 -0
- package/scripts/patch-interior-context-smoke.mjs +49 -0
- package/scripts/patch-newline-utf8-smoke.mjs +157 -0
- package/scripts/perf-hook-smoke.mjs +71 -0
- package/scripts/permission-eval-smoke.mjs +426 -0
- package/scripts/prep-patch.mjs +53 -0
- package/scripts/prep-shim.mjs +96 -0
- package/scripts/provider-cache-smoke.mjs +687 -0
- package/scripts/report-runtime-health.mjs +132 -0
- package/scripts/run-mcp.mjs +1547 -0
- package/scripts/salvage-v4a-shatter.test.mjs +58 -0
- package/scripts/scoped-cache-io-smoke.mjs +103 -0
- package/scripts/shell-policy-round3-smoke.mjs +46 -0
- package/scripts/smoke-runtime-negative.ps1 +100 -0
- package/scripts/smoke-runtime-negative.sh +95 -0
- package/scripts/stall-policy-smoke.mjs +50 -0
- package/scripts/start-memory-worker.mjs +23 -0
- package/scripts/statusline-launcher-smoke.mjs +82 -0
- package/scripts/stress-atomic-write.mjs +1028 -0
- package/scripts/test-config-rmw-restore.mjs +122 -0
- package/scripts/test-fault-inject.mjs +164 -0
- package/scripts/test-large-file.mjs +174 -0
- package/scripts/tool-edge-smoke.mjs +209 -0
- package/scripts/uninstall.mjs +201 -0
- package/scripts/webhook-selfheal-smoke.mjs +29 -0
- package/scripts/write-overwrite-guard-smoke.mjs +56 -0
- package/server-main.mjs +3055 -0
- package/server.mjs +468 -0
- package/setup/config-merge.mjs +254 -0
- package/setup/install.mjs +120 -0
- package/setup/launch-core.mjs +507 -0
- package/setup/launch.mjs +101 -0
- package/setup/setup-server.mjs +3206 -0
- package/setup/setup.html +3693 -0
- package/skills/retro-skill-proposer/SKILL.md +92 -0
- package/skills/schedule-add/SKILL.md +77 -0
- package/skills/setup/SKILL.md +346 -0
- package/skills/webhook-add/SKILL.md +81 -0
- package/src/agent/bridge-stall-watchdog.mjs +337 -0
- package/src/agent/index.mjs +2138 -0
- package/src/agent/orchestrator/activity-bus.mjs +38 -0
- package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
- package/src/agent/orchestrator/bridge-retry.mjs +220 -0
- package/src/agent/orchestrator/bridge-trace.mjs +583 -0
- package/src/agent/orchestrator/cache-mtime.mjs +58 -0
- package/src/agent/orchestrator/config.mjs +358 -0
- package/src/agent/orchestrator/context/collect.mjs +651 -0
- package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
- package/src/agent/orchestrator/drain-registry.mjs +50 -0
- package/src/agent/orchestrator/explore-validator.mjs +8 -0
- package/src/agent/orchestrator/internal-roles.mjs +118 -0
- package/src/agent/orchestrator/internal-tools.mjs +88 -0
- package/src/agent/orchestrator/jobs.mjs +116 -0
- package/src/agent/orchestrator/mcp/client.mjs +364 -0
- package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
- package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
- package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
- package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
- package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
- package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
- package/src/agent/orchestrator/providers/registry.mjs +192 -0
- package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
- package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
- package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
- package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
- package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
- package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
- package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
- package/src/agent/orchestrator/session/cache/util.mjs +49 -0
- package/src/agent/orchestrator/session/loop.mjs +1478 -0
- package/src/agent/orchestrator/session/manager.mjs +1975 -0
- package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
- package/src/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/agent/orchestrator/session/store.mjs +624 -0
- package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
- package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
- package/src/agent/orchestrator/session/trim.mjs +491 -0
- package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
- package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
- package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
- package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
- package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
- package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
- package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
- package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
- package/src/agent/orchestrator/stall-policy.mjs +195 -0
- package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
- package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
- package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
- package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
- package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
- package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
- package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
- package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
- package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
- package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
- package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
- package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
- package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
- package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
- package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
- package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
- package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
- package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
- package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
- package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
- package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
- package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
- package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
- package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
- package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
- package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
- package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
- package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
- package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
- package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
- package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
- package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
- package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
- package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
- package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
- package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
- package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
- package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
- package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
- package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
- package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
- package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
- package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
- package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
- package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
- package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
- package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
- package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
- package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
- package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
- package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
- package/src/agent/orchestrator/tools/builtin.mjs +500 -0
- package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
- package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
- package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
- package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
- package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
- package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
- package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
- package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
- package/src/agent/orchestrator/tools/host-input.mjs +204 -0
- package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
- package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
- package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
- package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
- package/src/agent/orchestrator/tools/patch.mjs +2754 -0
- package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
- package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
- package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
- package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
- package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
- package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
- package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
- package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
- package/src/agent/orchestrator/workflow-store.mjs +93 -0
- package/src/agent/tool-defs.mjs +103 -0
- package/src/channels/backends/discord.mjs +784 -0
- package/src/channels/data/voice-runtime-manifest.json +138 -0
- package/src/channels/index.mjs +3229 -0
- package/src/channels/lib/cli-worker-host.mjs +12 -0
- package/src/channels/lib/config-lock.mjs +13 -0
- package/src/channels/lib/config.mjs +292 -0
- package/src/channels/lib/drop-trace.mjs +71 -0
- package/src/channels/lib/event-pipeline.mjs +81 -0
- package/src/channels/lib/event-queue.mjs +345 -0
- package/src/channels/lib/executor.mjs +168 -0
- package/src/channels/lib/format.mjs +188 -0
- package/src/channels/lib/holidays.mjs +138 -0
- package/src/channels/lib/hook-pipe-server.mjs +802 -0
- package/src/channels/lib/interaction-workflows.mjs +184 -0
- package/src/channels/lib/memory-client.mjs +149 -0
- package/src/channels/lib/output-forwarder.mjs +765 -0
- package/src/channels/lib/runtime-paths.mjs +479 -0
- package/src/channels/lib/scheduler.mjs +723 -0
- package/src/channels/lib/session-control.mjs +36 -0
- package/src/channels/lib/session-discovery.mjs +103 -0
- package/src/channels/lib/settings.mjs +11 -0
- package/src/channels/lib/state-file.mjs +68 -0
- package/src/channels/lib/status-snapshot.mjs +219 -0
- package/src/channels/lib/tool-format.mjs +140 -0
- package/src/channels/lib/transcript-discovery.mjs +195 -0
- package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
- package/src/channels/lib/webhook.mjs +1179 -0
- package/src/channels/lib/whisper-server.mjs +477 -0
- package/src/channels/tool-defs.mjs +170 -0
- package/src/daemon/host.mjs +118 -0
- package/src/daemon/mcp-transport.mjs +47 -0
- package/src/daemon/session.mjs +100 -0
- package/src/daemon/thin-client.mjs +71 -0
- package/src/daemon/transport.mjs +163 -0
- package/src/memory/data/runtime-manifest.json +40 -0
- package/src/memory/index.mjs +3305 -0
- package/src/memory/lib/agent-ipc.mjs +93 -0
- package/src/memory/lib/bridge-trace-queries.mjs +120 -0
- package/src/memory/lib/core-memory-store.mjs +330 -0
- package/src/memory/lib/embedding-provider.mjs +269 -0
- package/src/memory/lib/embedding-worker.mjs +323 -0
- package/src/memory/lib/llm-worker-host.mjs +17 -0
- package/src/memory/lib/memory-cycle.mjs +11 -0
- package/src/memory/lib/memory-cycle1.mjs +641 -0
- package/src/memory/lib/memory-cycle2.mjs +1284 -0
- package/src/memory/lib/memory-cycle3.mjs +540 -0
- package/src/memory/lib/memory-embed.mjs +299 -0
- package/src/memory/lib/memory-extraction.mjs +5 -0
- package/src/memory/lib/memory-maintenance-store.mjs +32 -0
- package/src/memory/lib/memory-ops-policy.mjs +190 -0
- package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
- package/src/memory/lib/memory-recall-read-query.mjs +7 -0
- package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
- package/src/memory/lib/memory-recall-store.mjs +621 -0
- package/src/memory/lib/memory-retrievers.mjs +112 -0
- package/src/memory/lib/memory-score.mjs +71 -0
- package/src/memory/lib/memory-text-utils.mjs +58 -0
- package/src/memory/lib/memory.mjs +412 -0
- package/src/memory/lib/model-profile.mjs +85 -0
- package/src/memory/lib/pg/adapter.mjs +308 -0
- package/src/memory/lib/pg/process.mjs +360 -0
- package/src/memory/lib/pg/supervisor.mjs +396 -0
- package/src/memory/lib/project-id-resolver.mjs +86 -0
- package/src/memory/lib/runtime-fetcher.mjs +442 -0
- package/src/memory/lib/trace-store.mjs +728 -0
- package/src/memory/tool-defs.mjs +79 -0
- package/src/search/index.mjs +1173 -0
- package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
- package/src/search/lib/backends/exa.mjs +50 -0
- package/src/search/lib/backends/firecrawl.mjs +61 -0
- package/src/search/lib/backends/gemini-api.mjs +83 -0
- package/src/search/lib/backends/grok-oauth.mjs +86 -0
- package/src/search/lib/backends/index.mjs +150 -0
- package/src/search/lib/backends/openai-api.mjs +144 -0
- package/src/search/lib/backends/openai-oauth.mjs +98 -0
- package/src/search/lib/backends/openai-web-search.mjs +76 -0
- package/src/search/lib/backends/tavily.mjs +55 -0
- package/src/search/lib/backends/xai-api.mjs +113 -0
- package/src/search/lib/cache.mjs +131 -0
- package/src/search/lib/config.mjs +192 -0
- package/src/search/lib/formatter.mjs +115 -0
- package/src/search/lib/provider-usage.mjs +67 -0
- package/src/search/lib/providers.mjs +47 -0
- package/src/search/lib/search-intent.mjs +109 -0
- package/src/search/lib/setup-handler.mjs +261 -0
- package/src/search/lib/state.mjs +201 -0
- package/src/search/lib/web-tools.mjs +1207 -0
- package/src/search/tool-defs.mjs +83 -0
- package/src/setup/defender-exclusion.mjs +183 -0
- package/src/shared/abort-controller.mjs +15 -0
- package/src/shared/atomic-file.mjs +420 -0
- package/src/shared/config.mjs +350 -0
- package/src/shared/daemon-recycle.mjs +108 -0
- package/src/shared/disable-claude-builtins.mjs +88 -0
- package/src/shared/err-text.mjs +12 -0
- package/src/shared/llm/cost.mjs +66 -0
- package/src/shared/llm/http-agent.mjs +123 -0
- package/src/shared/llm/index.mjs +41 -0
- package/src/shared/llm/pid-cleanup.mjs +27 -0
- package/src/shared/llm/usage-log.mjs +47 -0
- package/src/shared/plugin-paths.mjs +58 -0
- package/src/shared/schedules-store.mjs +70 -0
- package/src/shared/seed.mjs +119 -0
- package/src/shared/user-cwd.mjs +213 -0
- package/src/shared/user-data-guard.mjs +238 -0
- package/src/status/aggregator.mjs +584 -0
- package/src/status/server.mjs +413 -0
- package/tools.json +1653 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bridge-status aggregator.
|
|
3
|
+
*
|
|
4
|
+
* Builds the JSON / text payload consumed by statusline.sh and setup.html.
|
|
5
|
+
* Extracted from setup-server.mjs (0.1.25 and earlier) so both the setup
|
|
6
|
+
* server (on-demand, port 3458) and the MCP-embedded status server (always
|
|
7
|
+
* on, ephemeral port) can serve the same response without drifting.
|
|
8
|
+
*
|
|
9
|
+
* All reads are best-effort; any single source failing leaves that segment
|
|
10
|
+
* empty rather than failing the whole response.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import http from 'http';
|
|
14
|
+
import { listSchedules } from '../shared/schedules-store.mjs';
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
existsSync,
|
|
18
|
+
readFileSync,
|
|
19
|
+
readdirSync,
|
|
20
|
+
} from 'fs';
|
|
21
|
+
import { join } from 'path';
|
|
22
|
+
|
|
23
|
+
const RECENT_MS = 30 * 60 * 1000;
|
|
24
|
+
const SNAPSHOT_STALE_MS = 30_000;
|
|
25
|
+
|
|
26
|
+
// Negative cache for ngrok offline state. Invariant: a /api/tunnels probe
|
|
27
|
+
// that fails or returns no tunnel proves ngrok is offline; re-probing
|
|
28
|
+
// within NGROK_OFFLINE_CACHE_MS adds zero information but pays a ~300ms
|
|
29
|
+
// HTTP timeout per statusline tick. Cache the negative result; any
|
|
30
|
+
// positive result (snapshot or successful probe) bypasses the cache.
|
|
31
|
+
let _ngrokOfflineUntilMs = 0;
|
|
32
|
+
const NGROK_OFFLINE_CACHE_MS = 30_000;
|
|
33
|
+
|
|
34
|
+
// Whole-result cache for buildBridgeStatus. statusline polls at ~1s; a
|
|
35
|
+
// short TTL collapses bursts (paired GET + setup-server tick + occasional
|
|
36
|
+
// duplicate frame) into one IO pass. Sidecar files / sessions scan don't
|
|
37
|
+
// change meaningfully within 800ms, so callers see consistent state.
|
|
38
|
+
// Any caller that needs strictly-fresh data should pass options.noCache.
|
|
39
|
+
const _buildCache = new Map();
|
|
40
|
+
const BUILD_CACHE_MS = 800;
|
|
41
|
+
const TWELVE_H = 12 * 60 * 60 * 1000;
|
|
42
|
+
|
|
43
|
+
// Sessions directory scan cache. statusline polls aggregator at a tight
|
|
44
|
+
// cadence (≤1s); without memoisation the readdirSync + per-file readFileSync +
|
|
45
|
+
// JSON.parse pass burns a single core when sessions/ accumulates hundreds of
|
|
46
|
+
// closed-session JSONs. TTL aligned with statusline polling cadence so the
|
|
47
|
+
// CLOSED_GRACE_MS jitter window below is visible without strobing.
|
|
48
|
+
const SESSIONS_CACHE_TTL_MS = 1_000;
|
|
49
|
+
// Ephemeral maintenance roles (cycle1-agent, cycle2-agent, etc.) close
|
|
50
|
+
// inside ~15-20s, faster than two statusline ticks can render. Keeping a
|
|
51
|
+
// closed bridge session visible for this grace window past its terminal
|
|
52
|
+
// updatedAt smooths the badge so short-lived workers don't strobe in and
|
|
53
|
+
// out invisibly. STREAM_FRESH_MS still drops genuinely stalled sessions.
|
|
54
|
+
const CLOSED_GRACE_MS = 5_000;
|
|
55
|
+
let _sessionsCache = null;
|
|
56
|
+
let _sessionsCacheAt = 0;
|
|
57
|
+
|
|
58
|
+
function readSessionsScan(sessionsDir, now) {
|
|
59
|
+
if (_sessionsCache && (now - _sessionsCacheAt) < SESSIONS_CACHE_TTL_MS) {
|
|
60
|
+
return _sessionsCache;
|
|
61
|
+
}
|
|
62
|
+
const allSessions = [];
|
|
63
|
+
const hbMap = new Map();
|
|
64
|
+
let files = [];
|
|
65
|
+
try {
|
|
66
|
+
files = readdirSync(sessionsDir);
|
|
67
|
+
} catch {
|
|
68
|
+
// ENOENT (fresh install, no sessions/ yet) / EACCES (locked dir) ->
|
|
69
|
+
// empty scan; caller treats as no sessions. Avoids the check-then-
|
|
70
|
+
// readdir race where existsSync says yes but the dir vanishes before
|
|
71
|
+
// readdir, propagating an error into statusline.
|
|
72
|
+
files = [];
|
|
73
|
+
}
|
|
74
|
+
for (const f of files) {
|
|
75
|
+
if (f.endsWith('.json')) {
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(readFileSync(join(sessionsDir, f), 'utf-8'));
|
|
78
|
+
// Reject non-plain-object payloads (null, arrays, strings, numbers).
|
|
79
|
+
// isSessionRunning + downstream consumers do bare property access
|
|
80
|
+
// (s.owner, s.closed, s.id) which throws on null and misbehaves on
|
|
81
|
+
// arrays/primitives — a single malformed file would otherwise sink
|
|
82
|
+
// the whole status build.
|
|
83
|
+
if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
84
|
+
allSessions.push(parsed);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch { /* skip corrupt */ }
|
|
88
|
+
} else if (f.endsWith('.hb')) {
|
|
89
|
+
const id = f.slice(0, -3);
|
|
90
|
+
try {
|
|
91
|
+
const ts = parseInt(readFileSync(join(sessionsDir, f), 'utf-8').trim(), 10);
|
|
92
|
+
if (Number.isFinite(ts)) hbMap.set(id, ts);
|
|
93
|
+
} catch { /* skip */ }
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
_sessionsCache = { allSessions, hbMap };
|
|
97
|
+
_sessionsCacheAt = now;
|
|
98
|
+
return _sessionsCache;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeTimestamp(value) {
|
|
102
|
+
if (value === null || value === undefined || value === '') return null;
|
|
103
|
+
const n = Number(value);
|
|
104
|
+
return Number.isFinite(n) ? n : null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function buildRecapSegment(recap = {}) {
|
|
108
|
+
const validStates = new Set(['idle', 'running', 'injected', 'empty', 'error']);
|
|
109
|
+
const rawState = typeof recap.state === 'string' && validStates.has(recap.state) ? recap.state : 'idle';
|
|
110
|
+
return {
|
|
111
|
+
state: rawState,
|
|
112
|
+
running: recap.running === true,
|
|
113
|
+
startedAt: normalizeTimestamp(recap.startedAt),
|
|
114
|
+
lastCompletedAt: normalizeTimestamp(recap.lastCompletedAt),
|
|
115
|
+
updatedAt: normalizeTimestamp(recap.updatedAt),
|
|
116
|
+
errorMessage: typeof recap.errorMessage === 'string' ? recap.errorMessage.slice(0, 200) : null,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Hidden maintenance roles (see defaults/hidden-roles.json kind="maintenance").
|
|
121
|
+
// Plugin-internal background workers without per-terminal ownership: cycle1/2/3,
|
|
122
|
+
// scheduler-task, webhook-handler. Spawned via
|
|
123
|
+
// callBridgeLlm WITHOUT an explicit ownerSessionId, so the session-builder
|
|
124
|
+
// fallback inherits the spawning instance's MIXDOG_OWNER_SESSION_ID rather
|
|
125
|
+
// than leaving it null. The set is still used by the closed-grace / last-
|
|
126
|
+
// completed visibility gates below; owner-scoping (matchesOwnerSession) now
|
|
127
|
+
// keeps each terminal's statusline limited to the workers it actually spawned.
|
|
128
|
+
const SHARED_BACKGROUND_ROLES = new Set([
|
|
129
|
+
'cycle1-agent',
|
|
130
|
+
'cycle2-agent',
|
|
131
|
+
'cycle3-agent',
|
|
132
|
+
'scheduler-task',
|
|
133
|
+
'webhook-handler',
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
function matchesOwnerSession(session, ownerSessionId) {
|
|
137
|
+
if (!ownerSessionId) return true;
|
|
138
|
+
return session?.ownerSessionId === ownerSessionId;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function matchesClientHostPid(session, clientHostPid) {
|
|
142
|
+
if (!clientHostPid) return true;
|
|
143
|
+
return session?.clientHostPid === clientHostPid;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function keepClosedSessionVisible(session) {
|
|
147
|
+
// Only grace a genuine just-finished close. idle-sweep / abort batch-closes
|
|
148
|
+
// bump updatedAt on many stale tombstones at once; without this reason gate
|
|
149
|
+
// they all re-enter the 5s closed-grace simultaneously and strobe the badge
|
|
150
|
+
// (the SHARED_BACKGROUND_ROLES "phantom flash" at idle). Genuine completions
|
|
151
|
+
// (e.g. ephemeral-done) are not suppressed, so real finishes still show.
|
|
152
|
+
return session?.role
|
|
153
|
+
&& SHARED_BACKGROUND_ROLES.has(session.role)
|
|
154
|
+
&& shouldShowLastCompleted(session);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const SUPPRESSED_LAST_COMPLETED_REASONS = new Set([
|
|
158
|
+
'manual',
|
|
159
|
+
'request-abort',
|
|
160
|
+
'request-aborted',
|
|
161
|
+
'retry-replaced',
|
|
162
|
+
'idle-sweep',
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
function shouldShowLastCompleted(session) {
|
|
166
|
+
const reason = session?.closedReason ? String(session.closedReason) : '';
|
|
167
|
+
return !SUPPRESSED_LAST_COMPLETED_REASONS.has(reason);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Active-session filter anchored on stream-recency via the heartbeat
|
|
171
|
+
// file. .hb is the single source of truth — no disk session.status /
|
|
172
|
+
// lastStreamDeltaAt fallback. The 5min window keeps the badge alive
|
|
173
|
+
// across long reasoning blocks (xhigh effort, deep tool chains) where
|
|
174
|
+
// the gap between SSE chunks routinely exceeds 30s. Genuine stalls
|
|
175
|
+
// eventually trip the bridge-stall-watchdog or the RUNNING_STALL_MS
|
|
176
|
+
// sweep at 10min; closed:true plants land on disk synchronously so the
|
|
177
|
+
// badge drops immediately on real completion.
|
|
178
|
+
const STREAM_FRESH_MS = 5 * 60 * 1000;
|
|
179
|
+
|
|
180
|
+
// Mirror of the store-side running-stall window (store.mjs:489). A not-yet-
|
|
181
|
+
// closed session whose heartbeat lapsed during a long non-streaming tool call
|
|
182
|
+
// stays visible until this window — matching when the store sweep would
|
|
183
|
+
// actually close it — so deep tool chains don't blink out mid-run.
|
|
184
|
+
const RUNNING_STALL_MS = 10 * 60 * 1000;
|
|
185
|
+
|
|
186
|
+
// Shared running-session predicate used by both the full and compact
|
|
187
|
+
// builders. Multi-CC isolation: owner-scoped status servers must not
|
|
188
|
+
// show bridge sessions from another terminal — untagged legacy
|
|
189
|
+
// sessions are hidden once the request is owner-scoped. Heartbeat-less
|
|
190
|
+
// grace is reserved for shared background roles; user worker sessions
|
|
191
|
+
// must have a fresh heartbeat, otherwise stale tombstones can flash in
|
|
192
|
+
// unrelated statuslines during spawn/claim races.
|
|
193
|
+
function isSessionRunning(s, { hbMap, now, ownerSessionId, clientHostPid, ownerHostPid, includeClosedGrace = true }) {
|
|
194
|
+
if (s.owner !== 'bridge') return false;
|
|
195
|
+
if (!matchesOwnerSession(s, ownerSessionId)) return false;
|
|
196
|
+
const isSharedBackground = s.role && SHARED_BACKGROUND_ROLES.has(s.role);
|
|
197
|
+
// Scoping unit differs by role kind (invariant, not a fallback):
|
|
198
|
+
// - Maintenance/background roles (cycle1/2/3-agent, scheduler-task,
|
|
199
|
+
// webhook-handler) are spawned by the memory daemon via
|
|
200
|
+
// callBridgeLlm WITHOUT a clientHostPid, so their session JSON has
|
|
201
|
+
// none. They belong to the ACTIVE OWNER TERMINAL (owner = one live
|
|
202
|
+
// session, NOT the daemon): on a shared daemon every attached
|
|
203
|
+
// terminal carries the same ownerSessionId, so owner-scoping alone
|
|
204
|
+
// would leak maintenance chips onto EVERY terminal. Gate them
|
|
205
|
+
// on the requesting terminal being the active-instance owner —
|
|
206
|
+
// clientHostPid === ownerHostPid (active-instance.json SSOT). When
|
|
207
|
+
// ownerHostPid or the request clientHostPid is absent (legacy /
|
|
208
|
+
// single-instance), fall through to owner-scoping unchanged.
|
|
209
|
+
// - User worker sessions (worker/reviewer/debugger/tester) carry a
|
|
210
|
+
// per-terminal clientHostPid and stay strictly isolated to the
|
|
211
|
+
// terminal that spawned them.
|
|
212
|
+
if (isSharedBackground) {
|
|
213
|
+
if (ownerHostPid && clientHostPid && clientHostPid !== ownerHostPid) return false;
|
|
214
|
+
} else if (!matchesClientHostPid(s, clientHostPid)) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
if (s.closed === true) {
|
|
218
|
+
if (!includeClosedGrace) return false;
|
|
219
|
+
return keepClosedSessionVisible(s) && (now - (s.updatedAt || 0)) <= CLOSED_GRACE_MS;
|
|
220
|
+
}
|
|
221
|
+
const hb = hbMap.get(s.id);
|
|
222
|
+
if (hb && (now - hb) <= STREAM_FRESH_MS) return true;
|
|
223
|
+
if (s.status && s.status !== 'running') return false;
|
|
224
|
+
const statusIsRunning = s.status === 'running';
|
|
225
|
+
// Heartbeat-less grace is only for plugin-owned background work. Public
|
|
226
|
+
// worker/reviewer/debugger sessions clear their heartbeat on completion but
|
|
227
|
+
// may remain on disk as idle tombstones, so treating updatedAt as liveness
|
|
228
|
+
// makes finished workers linger in L2 and leak across terminals.
|
|
229
|
+
if (isSharedBackground && statusIsRunning && (now - (s.updatedAt || 0)) <= RUNNING_STALL_MS) return true;
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Idle bridge-worker predicate. A worker that has finished its turn clears
|
|
234
|
+
// its heartbeat (so isSessionRunning is false) but may remain on disk as an
|
|
235
|
+
// idle tombstone until the store reaps it. Reaped sessions are deleted from
|
|
236
|
+
// sessions/ entirely, so they fall out of allSessions and vanish from L2 with
|
|
237
|
+
// no extra logic here. We surface the surviving idle workers (greyed) so the
|
|
238
|
+
// user can see a worker is parked-but-present, distinct from RUNNING.
|
|
239
|
+
// Maintenance/background roles are excluded — only owner-scoped user workers.
|
|
240
|
+
function isIdleWorkerSession(s, { ownerSessionId, clientHostPid }) {
|
|
241
|
+
if (s.owner !== 'bridge') return false;
|
|
242
|
+
if (!matchesOwnerSession(s, ownerSessionId)) return false;
|
|
243
|
+
if (!matchesClientHostPid(s, clientHostPid)) return false;
|
|
244
|
+
if (s.closed === true) return false; // closed → handled by closed-grace / lastCompleted
|
|
245
|
+
if (s.role && SHARED_BACKGROUND_ROLES.has(s.role)) return false; // maintenance, not a user worker
|
|
246
|
+
return true; // on-disk, not running (callers exclude running ids), not closed → idle
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Build the unified worker list consumed by L2: running workers carry
|
|
250
|
+
// status 'running', surviving idle tombstones carry 'idle'. Each entry keeps
|
|
251
|
+
// its bridgeTag so the statusline can render tag + status. This is the single
|
|
252
|
+
// thread that carries running/idle status from aggregator → statusline-lib.
|
|
253
|
+
function buildWorkerList(running, idle) {
|
|
254
|
+
return [
|
|
255
|
+
...running.map((s) => ({ tag: s.bridgeTag || s.role || 'agent', status: 'running' })),
|
|
256
|
+
...idle.map((s) => ({ tag: s.bridgeTag || s.role || 'agent', status: 'idle' })),
|
|
257
|
+
].filter((w) => w.tag);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Snapshot → nextSchedule parser. Used by both builders. Returns null
|
|
261
|
+
// when the snapshot has no schedules.next entry or the fireAt is not a
|
|
262
|
+
// finite number; otherwise returns { name, fireAt }.
|
|
263
|
+
function parseNextScheduleFromSnap(snap) {
|
|
264
|
+
if (!snap?.schedules?.next) return null;
|
|
265
|
+
const fireAt = Number(snap.schedules.next.fireAt);
|
|
266
|
+
if (!Number.isFinite(fireAt)) return null;
|
|
267
|
+
// ECMAScript caps valid Date instants at ±8.64e15 ms from epoch; finite
|
|
268
|
+
// numbers outside that range yield an Invalid Date whose toISOString()
|
|
269
|
+
// throws RangeError. Treat out-of-range snapshots as best-effort-absent
|
|
270
|
+
// so a malformed schedules.next does not sink buildBridgeStatus.
|
|
271
|
+
if (Math.abs(fireAt) > 8.64e15) return null;
|
|
272
|
+
return { name: snap.schedules.next.name, fireAt };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export async function buildBridgeStatus(dataDir, options = {}) {
|
|
276
|
+
const now = Date.now();
|
|
277
|
+
// Short-TTL whole-result cache. statusline tick is ~1s, so an 800ms TTL
|
|
278
|
+
// turns repeated GETs (paired status + setup duplicate) into zero-cost
|
|
279
|
+
// returns while keeping per-tick freshness for the user.
|
|
280
|
+
const _cacheKey = `${options.ownerSessionId || ''}|${options.clientHostPid ?? ''}`;
|
|
281
|
+
if (!options.noCache) {
|
|
282
|
+
const _cached = _buildCache.get(_cacheKey);
|
|
283
|
+
if (_cached && now - _cached.at < BUILD_CACHE_MS) return _cached.result;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const SESSIONS_DIR = join(dataDir, 'sessions');
|
|
287
|
+
const STATUS_SNAPSHOT_PATH = join(dataDir, 'channels', 'status-snapshot.json');
|
|
288
|
+
const JOBS_STATE_PATH = join(dataDir, 'jobs', 'state.json');
|
|
289
|
+
const CACHE_STATS_PATH = join(dataDir, 'cache-stats.json');
|
|
290
|
+
|
|
291
|
+
// ── 1. Active + recently-completed bridge sessions ────────────────
|
|
292
|
+
// Single readdir over sessions/ separates `<id>.json` (full session
|
|
293
|
+
// payload) from `<id>.hb` (lightweight heartbeat published by
|
|
294
|
+
// markSessionStreamDelta on a ≤5s self-throttle). The .hb file is the
|
|
295
|
+
// authoritative fresh signal because the heavy session.json save is
|
|
296
|
+
// throttled to 60s.
|
|
297
|
+
const { allSessions, hbMap } = readSessionsScan(SESSIONS_DIR, now);
|
|
298
|
+
const running = allSessions.filter(s => isSessionRunning(s, {
|
|
299
|
+
hbMap, now, ownerSessionId: options.ownerSessionId, clientHostPid: options.clientHostPid, ownerHostPid: options.ownerHostPid,
|
|
300
|
+
}));
|
|
301
|
+
const runningRoles = running.map(s => s.bridgeTag || s.role || 'agent').filter(Boolean);
|
|
302
|
+
// Surviving idle worker tombstones (running ones excluded so a session is
|
|
303
|
+
// never counted twice). Reaped sessions are gone from allSessions already.
|
|
304
|
+
const runningIds = new Set(running.map(s => s.id));
|
|
305
|
+
const idle = allSessions.filter(s => !runningIds.has(s.id)
|
|
306
|
+
&& isIdleWorkerSession(s, { ownerSessionId: options.ownerSessionId, clientHostPid: options.clientHostPid }));
|
|
307
|
+
const workers = buildWorkerList(running, idle);
|
|
308
|
+
|
|
309
|
+
const recentClosed = allSessions
|
|
310
|
+
.filter(s => s.owner === 'bridge'
|
|
311
|
+
&& s.closed === true
|
|
312
|
+
&& shouldShowLastCompleted(s)
|
|
313
|
+
&& matchesOwnerSession(s, options.ownerSessionId)
|
|
314
|
+
&& matchesClientHostPid(s, options.clientHostPid)
|
|
315
|
+
&& (now - (s.updatedAt || 0)) <= RECENT_MS)
|
|
316
|
+
.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
317
|
+
const lastCompleted = recentClosed[0] || null;
|
|
318
|
+
|
|
319
|
+
// ── 2. Scheduler state ────────────────────────────────────────────
|
|
320
|
+
let scheduleActive = 0;
|
|
321
|
+
let scheduleDeferred = 0;
|
|
322
|
+
let nextSchedule = null;
|
|
323
|
+
let discordTotalUnread = null;
|
|
324
|
+
let ngrokTunnelUrl = null;
|
|
325
|
+
let snapshotFresh = false;
|
|
326
|
+
try {
|
|
327
|
+
if (existsSync(STATUS_SNAPSHOT_PATH)) {
|
|
328
|
+
const snap = JSON.parse(readFileSync(STATUS_SNAPSHOT_PATH, 'utf-8'));
|
|
329
|
+
if (snap && typeof snap.writtenAt === 'number' && (now - snap.writtenAt) <= SNAPSHOT_STALE_MS) {
|
|
330
|
+
snapshotFresh = true;
|
|
331
|
+
// Schedules truth source: `${CLAUDE_PLUGIN_DATA}/schedules/<name>/`.
|
|
332
|
+
// listSchedules() is shared with channels/lib/config.mjs and
|
|
333
|
+
// setup-server so all three see the same entry list.
|
|
334
|
+
const allSchedules = listSchedules().filter(s => s.enabled !== false && s.name);
|
|
335
|
+
scheduleActive = allSchedules.length;
|
|
336
|
+
scheduleDeferred = snap.schedules?.deferredCount ?? 0;
|
|
337
|
+
nextSchedule = parseNextScheduleFromSnap(snap);
|
|
338
|
+
if (typeof snap.discord?.totalUnread === 'number') {
|
|
339
|
+
discordTotalUnread = snap.discord.totalUnread;
|
|
340
|
+
}
|
|
341
|
+
if (snap.ngrok?.tunnelUrl) {
|
|
342
|
+
ngrokTunnelUrl = snap.ngrok.tunnelUrl;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
} catch { /* snapshot unreadable */ }
|
|
347
|
+
|
|
348
|
+
if (!snapshotFresh) {
|
|
349
|
+
try {
|
|
350
|
+
const allSchedules = listSchedules().filter(s => s.enabled !== false && s.name);
|
|
351
|
+
scheduleActive = allSchedules.length;
|
|
352
|
+
|
|
353
|
+
const candidates = [];
|
|
354
|
+
for (const s of allSchedules) {
|
|
355
|
+
if (!s.time || !/^\d{2}:\d{2}$/.test(s.time)) continue;
|
|
356
|
+
// Skip entries the snapshot-less fallback cannot faithfully
|
|
357
|
+
// evaluate: cron expressions, non-local timezones, day-of-week
|
|
358
|
+
// restrictions, and any deferrals (only carried in the snapshot).
|
|
359
|
+
// Emitting a falsely-imminent upcoming entry is worse than
|
|
360
|
+
// showing none — the statusline simply omits the badge.
|
|
361
|
+
if (s.cron) continue;
|
|
362
|
+
if (s.timezone) continue;
|
|
363
|
+
if (Array.isArray(s.days) && s.days.length > 0) continue;
|
|
364
|
+
const [hh, mm] = s.time.split(':').map(Number);
|
|
365
|
+
for (const offsetDays of [0, 1]) {
|
|
366
|
+
const candidate = new Date(now);
|
|
367
|
+
candidate.setDate(candidate.getDate() + offsetDays);
|
|
368
|
+
candidate.setHours(hh, mm, 0, 0);
|
|
369
|
+
const diff = candidate.getTime() - now;
|
|
370
|
+
if (diff > 0 && diff <= TWELVE_H) {
|
|
371
|
+
candidates.push({ name: s.name, fireAt: candidate.getTime(), diff });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
candidates.sort((a, b) => a.diff - b.diff);
|
|
376
|
+
if (candidates.length > 0) nextSchedule = candidates[0];
|
|
377
|
+
} catch { /* config unreadable */ }
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ── 3. Jobs count ────────────────────────────────────────────────
|
|
381
|
+
let jobsCount = 0;
|
|
382
|
+
if (existsSync(JOBS_STATE_PATH)) {
|
|
383
|
+
try {
|
|
384
|
+
const jobsState = JSON.parse(readFileSync(JOBS_STATE_PATH, 'utf-8'));
|
|
385
|
+
if (Array.isArray(jobsState)) {
|
|
386
|
+
jobsCount = jobsState.filter(j => j.status === 'running').length;
|
|
387
|
+
}
|
|
388
|
+
} catch { /* unreadable */ }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ── 5. Ngrok online ──────────────────────────────────────────────
|
|
392
|
+
let ngrokOnline = false;
|
|
393
|
+
if (ngrokTunnelUrl) {
|
|
394
|
+
ngrokOnline = true;
|
|
395
|
+
_ngrokOfflineUntilMs = 0; // positive evidence resets negative cache
|
|
396
|
+
} else if (Date.now() < _ngrokOfflineUntilMs) {
|
|
397
|
+
// Negative cache hit — skip the ~300ms HTTP probe.
|
|
398
|
+
ngrokOnline = false;
|
|
399
|
+
} else {
|
|
400
|
+
await new Promise((resolve) => {
|
|
401
|
+
const timer = setTimeout(() => { try { req_ng && req_ng.destroy(); } catch {} resolve(); }, 300);
|
|
402
|
+
let req_ng;
|
|
403
|
+
try {
|
|
404
|
+
req_ng = http.get('http://127.0.0.1:4040/api/tunnels', (r) => {
|
|
405
|
+
clearTimeout(timer);
|
|
406
|
+
let body = '';
|
|
407
|
+
r.on('data', d => { body += d; });
|
|
408
|
+
r.on('end', () => {
|
|
409
|
+
try {
|
|
410
|
+
const parsed = JSON.parse(body);
|
|
411
|
+
const tunnel = (parsed.tunnels || []).find(t => t.public_url);
|
|
412
|
+
if (tunnel) {
|
|
413
|
+
ngrokOnline = true;
|
|
414
|
+
ngrokTunnelUrl = tunnel.public_url;
|
|
415
|
+
}
|
|
416
|
+
} catch { /* ignore */ }
|
|
417
|
+
resolve();
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
req_ng.on('error', () => { clearTimeout(timer); resolve(); });
|
|
421
|
+
req_ng.setTimeout(300, () => { clearTimeout(timer); try { req_ng.destroy(); } catch {} resolve(); });
|
|
422
|
+
} catch { clearTimeout(timer); resolve(); }
|
|
423
|
+
});
|
|
424
|
+
if (ngrokOnline) {
|
|
425
|
+
_ngrokOfflineUntilMs = 0;
|
|
426
|
+
} else {
|
|
427
|
+
_ngrokOfflineUntilMs = Date.now() + NGROK_OFFLINE_CACHE_MS;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// ── Assemble payload ─────────────────────────────────────────────
|
|
432
|
+
const sessionSegment = running.length > 0
|
|
433
|
+
? { active: running.length, roles: runningRoles, workers }
|
|
434
|
+
: { active: 0, roles: [], workers };
|
|
435
|
+
|
|
436
|
+
let lastCompletedSegment = null;
|
|
437
|
+
if (lastCompleted) {
|
|
438
|
+
const ageMs = now - (lastCompleted.updatedAt || 0);
|
|
439
|
+
lastCompletedSegment = {
|
|
440
|
+
role: lastCompleted.role || 'agent',
|
|
441
|
+
agoMinutes: Math.round(ageMs / 60000),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const scheduleSegment = {
|
|
446
|
+
active: scheduleActive,
|
|
447
|
+
deferred: scheduleDeferred,
|
|
448
|
+
next: nextSchedule ? {
|
|
449
|
+
name: nextSchedule.name,
|
|
450
|
+
fireAt: nextSchedule.fireAt,
|
|
451
|
+
fireAtISO: new Date(nextSchedule.fireAt).toISOString(),
|
|
452
|
+
} : null,
|
|
453
|
+
};
|
|
454
|
+
const recapSegment = buildRecapSegment(options.recap);
|
|
455
|
+
|
|
456
|
+
// ── 6. Cache stats ───────────────────────────────────────────────
|
|
457
|
+
let cacheStats = null;
|
|
458
|
+
if (existsSync(CACHE_STATS_PATH)) {
|
|
459
|
+
try {
|
|
460
|
+
const raw = JSON.parse(readFileSync(CACHE_STATS_PATH, 'utf-8'));
|
|
461
|
+
if (raw && typeof raw.writtenAt === 'number' && typeof raw.totals === 'object') {
|
|
462
|
+
const t = raw.totals;
|
|
463
|
+
cacheStats = {
|
|
464
|
+
writtenAt: raw.writtenAt,
|
|
465
|
+
totals: {
|
|
466
|
+
sets: t.sets ?? 0,
|
|
467
|
+
hits: t.hits ?? 0,
|
|
468
|
+
misses: t.misses ?? 0,
|
|
469
|
+
clears: t.clears ?? 0,
|
|
470
|
+
},
|
|
471
|
+
perSession: Array.isArray(raw.perSession) ? raw.perSession : [],
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
} catch (e) { try { process.stderr.write(`[status-aggregator] partial-fetch swallow: ${e?.message ?? e}\n`); } catch {} }
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const _result = {
|
|
478
|
+
sessions: sessionSegment,
|
|
479
|
+
lastCompleted: lastCompletedSegment,
|
|
480
|
+
schedule: scheduleSegment,
|
|
481
|
+
jobs: { count: jobsCount },
|
|
482
|
+
recap: recapSegment,
|
|
483
|
+
ngrok: { online: ngrokOnline, tunnelUrl: ngrokTunnelUrl ?? undefined },
|
|
484
|
+
...(discordTotalUnread !== null ? { discord: { totalUnread: discordTotalUnread } } : {}),
|
|
485
|
+
...(cacheStats !== null ? { cacheStats } : {}),
|
|
486
|
+
snapshotFresh,
|
|
487
|
+
generatedAt: new Date(now).toISOString(),
|
|
488
|
+
};
|
|
489
|
+
if (!options.noCache) {
|
|
490
|
+
_buildCache.set(_cacheKey, { result: _result, at: now });
|
|
491
|
+
}
|
|
492
|
+
return _result;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Compact payload for the statusline 1-5 s polling cadence. Builds only
|
|
496
|
+
// the fields bin/statusline.mjs actually consumes (sessions.roles +
|
|
497
|
+
// schedule.next.{name,fireAt}); skips ngrok probe (~300 ms IO),
|
|
498
|
+
// cache-stats, lastCompleted, jobs.count, discord, recap. Reuses the
|
|
499
|
+
// 1 s readSessionsScan cache so repeated ticks see one disk scan
|
|
500
|
+
// amortised across all statusline-json polls within the window. Compact L2
|
|
501
|
+
// intentionally excludes closed-grace workers; completed work belongs in the
|
|
502
|
+
// full `lastCompleted` segment, not the running badge.
|
|
503
|
+
export async function buildBridgeStatusCompact(dataDir, options = {}) {
|
|
504
|
+
const now = Date.now();
|
|
505
|
+
const SESSIONS_DIR = join(dataDir, 'sessions');
|
|
506
|
+
const STATUS_SNAPSHOT_PATH = join(dataDir, 'channels', 'status-snapshot.json');
|
|
507
|
+
|
|
508
|
+
const { allSessions, hbMap } = readSessionsScan(SESSIONS_DIR, now);
|
|
509
|
+
const running = allSessions.filter((s) => isSessionRunning(s, {
|
|
510
|
+
hbMap,
|
|
511
|
+
now,
|
|
512
|
+
ownerSessionId: options.ownerSessionId,
|
|
513
|
+
clientHostPid: options.clientHostPid,
|
|
514
|
+
ownerHostPid: options.ownerHostPid,
|
|
515
|
+
includeClosedGrace: false,
|
|
516
|
+
}));
|
|
517
|
+
const runningRoles = running.map((s) => s.bridgeTag || s.role || 'agent').filter(Boolean);
|
|
518
|
+
// Mirror buildBridgeStatus: surface surviving idle worker tombstones too.
|
|
519
|
+
// Note compact deliberately excludes closed-grace from `running`; idle here
|
|
520
|
+
// means on-disk, not running, not closed — reaped sessions are already gone.
|
|
521
|
+
const runningIds = new Set(running.map((s) => s.id));
|
|
522
|
+
const idle = allSessions.filter((s) => !runningIds.has(s.id)
|
|
523
|
+
&& isIdleWorkerSession(s, { ownerSessionId: options.ownerSessionId, clientHostPid: options.clientHostPid }));
|
|
524
|
+
const workers = buildWorkerList(running, idle);
|
|
525
|
+
|
|
526
|
+
let nextSchedule = null;
|
|
527
|
+
try {
|
|
528
|
+
if (existsSync(STATUS_SNAPSHOT_PATH)) {
|
|
529
|
+
const snap = JSON.parse(readFileSync(STATUS_SNAPSHOT_PATH, 'utf-8'));
|
|
530
|
+
if (snap && typeof snap.writtenAt === 'number' && (now - snap.writtenAt) <= SNAPSHOT_STALE_MS) {
|
|
531
|
+
nextSchedule = parseNextScheduleFromSnap(snap);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
} catch { /* snapshot unreadable */ }
|
|
535
|
+
|
|
536
|
+
return {
|
|
537
|
+
sessions: { active: running.length, roles: runningRoles, workers },
|
|
538
|
+
schedule: { next: nextSchedule },
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
export function renderBridgeStatusText(payload) {
|
|
543
|
+
const parts = [];
|
|
544
|
+
const running = payload.sessions?.active || 0;
|
|
545
|
+
const roles = payload.sessions?.roles || [];
|
|
546
|
+
|
|
547
|
+
if (running > 0) {
|
|
548
|
+
const roleList = [...new Set(roles)].join(',');
|
|
549
|
+
parts.push(`⚙ ${running} running (${roleList})`);
|
|
550
|
+
} else {
|
|
551
|
+
parts.push('idle');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (payload.lastCompleted) {
|
|
555
|
+
const ageMins = payload.lastCompleted.agoMinutes || 0;
|
|
556
|
+
const timeAgo = ageMins <= 0 ? 'just now' : `${ageMins}m`;
|
|
557
|
+
parts.push(`✓ ${payload.lastCompleted.role} ${timeAgo}`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (payload.schedule?.next) {
|
|
561
|
+
const d = new Date(payload.schedule.next.fireAt);
|
|
562
|
+
const hh = String(d.getHours()).padStart(2, '0');
|
|
563
|
+
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
564
|
+
parts.push(`⏰ ${hh}:${mm} ${payload.schedule.next.name}`);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (payload.schedule?.active > 0) {
|
|
568
|
+
const def = payload.schedule.deferred || 0;
|
|
569
|
+
parts.push(def > 0
|
|
570
|
+
? `📋 ${payload.schedule.active}/${def}def`
|
|
571
|
+
: `📋 ${payload.schedule.active}`);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (payload.cacheStats?.totals) {
|
|
575
|
+
const { hits, misses } = payload.cacheStats.totals;
|
|
576
|
+
const total = hits + misses;
|
|
577
|
+
if (total >= 1) {
|
|
578
|
+
const rate = Math.round((hits / total) * 100);
|
|
579
|
+
parts.push(`cache ${hits}/${total} (${rate}%)`);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return parts.join(' · ');
|
|
584
|
+
}
|