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,1372 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
const net = require('net');
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
const { resolvePluginData } = require(path.join(__dirname, '..', 'lib', 'plugin-paths.cjs'));
|
|
10
|
+
const { readSection } = require(path.join(__dirname, '..', 'lib', 'config-cjs.cjs'));
|
|
11
|
+
|
|
12
|
+
// Mirror selected stderr lines to a plugin-data log file so cycle1 traces
|
|
13
|
+
// remain inspectable after the host shell scrolls past. Best-effort: any
|
|
14
|
+
// fs error is swallowed so logging never breaks the hook.
|
|
15
|
+
let _SESSION_START_LOG_PATH = null;
|
|
16
|
+
const SESSION_START_TRACE_ENABLED =
|
|
17
|
+
process.env.MIXDOG_DEBUG_SESSION_START === '1' ||
|
|
18
|
+
process.env.MIXDOG_DEBUG_SESSION_START === 'true';
|
|
19
|
+
function sessionStartLogPath() {
|
|
20
|
+
if (_SESSION_START_LOG_PATH) return _SESSION_START_LOG_PATH;
|
|
21
|
+
const base = (typeof DATA_DIR === 'string' && DATA_DIR)
|
|
22
|
+
? DATA_DIR
|
|
23
|
+
: path.join(os.homedir(), '.claude', 'plugins', 'data', 'mixdog-trib-plugin');
|
|
24
|
+
_SESSION_START_LOG_PATH = path.join(base, 'session-start.log');
|
|
25
|
+
return _SESSION_START_LOG_PATH;
|
|
26
|
+
}
|
|
27
|
+
// Always append to session-start.log so fail-open reasons (skip / cycle1
|
|
28
|
+
// failure / missing-dirs / null dispatch) stay diagnosable without requiring
|
|
29
|
+
// MIXDOG_DEBUG_SESSION_START to be pre-set. Stderr output remains gated by
|
|
30
|
+
// the trace flag — log file is the durable record, stderr is the live tail.
|
|
31
|
+
// Size-based rotation: when the log exceeds LOG_MAX_BYTES, head-trim down to
|
|
32
|
+
// LOG_KEEP_BYTES so a long-running daemon doesn't grow it unbounded.
|
|
33
|
+
const SESSION_START_LOG_MAX_BYTES = 256 * 1024;
|
|
34
|
+
const SESSION_START_LOG_KEEP_BYTES = 64 * 1024;
|
|
35
|
+
function teeStderr(line) {
|
|
36
|
+
try {
|
|
37
|
+
const p = sessionStartLogPath();
|
|
38
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
39
|
+
try {
|
|
40
|
+
const st = fs.statSync(p);
|
|
41
|
+
if (st.size > SESSION_START_LOG_MAX_BYTES) {
|
|
42
|
+
const buf = fs.readFileSync(p);
|
|
43
|
+
fs.writeFileSync(p, buf.subarray(buf.length - SESSION_START_LOG_KEEP_BYTES));
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
fs.appendFileSync(p, line);
|
|
47
|
+
} catch {}
|
|
48
|
+
if (SESSION_START_TRACE_ENABLED) {
|
|
49
|
+
try { process.stderr.write(line); } catch {}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// argv parsing — supports `--part rules`, `--part=rules`.
|
|
55
|
+
// Invalid/unknown part falls back to `rules`.
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
function parseArgs(argv) {
|
|
58
|
+
const out = { part: 'rules' };
|
|
59
|
+
for (let i = 2; i < argv.length; i++) {
|
|
60
|
+
const a = argv[i];
|
|
61
|
+
if (!a) continue;
|
|
62
|
+
let key = null;
|
|
63
|
+
let val = null;
|
|
64
|
+
if (a.startsWith('--') && a.includes('=')) {
|
|
65
|
+
const eq = a.indexOf('=');
|
|
66
|
+
key = a.slice(2, eq);
|
|
67
|
+
val = a.slice(eq + 1);
|
|
68
|
+
} else if (a.startsWith('--')) {
|
|
69
|
+
key = a.slice(2);
|
|
70
|
+
const next = argv[i + 1];
|
|
71
|
+
if (typeof next === 'string' && !next.startsWith('--')) {
|
|
72
|
+
val = next;
|
|
73
|
+
i++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (key === 'part' && typeof val === 'string') out.part = val;
|
|
77
|
+
}
|
|
78
|
+
if (!['rules', 'core', 'recap'].includes(out.part)) out.part = 'rules';
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const ARGS = parseArgs(process.argv);
|
|
83
|
+
let PART = ARGS.part;
|
|
84
|
+
|
|
85
|
+
let _event = {};
|
|
86
|
+
const IS_DAEMON_REQUIRE = !!process.env.MIXDOG_SKIP_TOP_STDIN;
|
|
87
|
+
// In-daemon `require()` would otherwise read fd 0 (the daemon's MCP stdio
|
|
88
|
+
// pipe), corrupting it. Skip the top-level stdin read when this env-var is
|
|
89
|
+
// set by hook-pipe-server.mjs around the require boundary.
|
|
90
|
+
if (!IS_DAEMON_REQUIRE) {
|
|
91
|
+
try {
|
|
92
|
+
const _input = fs.readFileSync(0, 'utf8');
|
|
93
|
+
if (_input) _event = JSON.parse(_input);
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (_event.isSidechain) {
|
|
98
|
+
teeStderr(`[session-start] skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=isSidechain\n`);
|
|
99
|
+
process.exit(0);
|
|
100
|
+
}
|
|
101
|
+
if (_event.agentId) {
|
|
102
|
+
teeStderr(`[session-start] skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=agentId=${_event.agentId}\n`);
|
|
103
|
+
process.exit(0);
|
|
104
|
+
}
|
|
105
|
+
if (_event.is_sidechain) {
|
|
106
|
+
teeStderr(`[session-start] skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=is_sidechain\n`);
|
|
107
|
+
process.exit(0);
|
|
108
|
+
}
|
|
109
|
+
if (_event.agent_id) {
|
|
110
|
+
teeStderr(`[session-start] skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=agent_id=${_event.agent_id}\n`);
|
|
111
|
+
process.exit(0);
|
|
112
|
+
}
|
|
113
|
+
if (_event.kind && _event.kind !== 'interactive') {
|
|
114
|
+
teeStderr(`[session-start] skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=kind=${_event.kind}\n`);
|
|
115
|
+
process.exit(0);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const DATA_DIR = resolvePluginData();
|
|
119
|
+
const PLUGIN_ROOT = process.env.CLAUDE_PLUGIN_ROOT;
|
|
120
|
+
const SESSION_START_CYCLE1_TIMEOUT_MS = Number.parseInt(process.env.MIXDOG_SESSION_START_CYCLE1_TIMEOUT_MS || '110000', 10);
|
|
121
|
+
const MIXDOG_RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT
|
|
122
|
+
? path.resolve(process.env.MIXDOG_RUNTIME_ROOT)
|
|
123
|
+
: path.join(os.tmpdir(), 'mixdog');
|
|
124
|
+
const ACTIVE_INSTANCE_FILE = path.join(MIXDOG_RUNTIME_ROOT, 'active-instance.json');
|
|
125
|
+
if (!DATA_DIR || !PLUGIN_ROOT) {
|
|
126
|
+
teeStderr(`[session-start] skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=missing-dirs DATA_DIR=${!!DATA_DIR} PLUGIN_ROOT=${!!PLUGIN_ROOT}\n`);
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!IS_DAEMON_REQUIRE) {
|
|
131
|
+
teeStderr(`[session-start] enter PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} sessionId=${_event.session_id || _event.sessionId || ''}\n`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Common helpers (used by all parts).
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
function readJson(filePath) {
|
|
138
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { return {}; }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parsePositiveNumber(value) {
|
|
142
|
+
const n = Number(value);
|
|
143
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readAdvertisedMemoryPort() {
|
|
147
|
+
const active = JSON.parse(fs.readFileSync(ACTIVE_INSTANCE_FILE, 'utf8'));
|
|
148
|
+
const port = parsePositiveNumber(active && active.memory_port);
|
|
149
|
+
if (!port) return null;
|
|
150
|
+
const activeServerPid = parsePositiveNumber(active && active.server_pid);
|
|
151
|
+
const memoryServerPid = parsePositiveNumber(active && active.memory_server_pid);
|
|
152
|
+
if (activeServerPid && memoryServerPid && activeServerPid !== memoryServerPid) {
|
|
153
|
+
return { stale: true, port, activeServerPid, memoryServerPid };
|
|
154
|
+
}
|
|
155
|
+
return { stale: false, port, active };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function sleepMs(ms) {
|
|
159
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readOwnerSecretFor(ownerInstanceId) {
|
|
163
|
+
if (!ownerInstanceId) return '';
|
|
164
|
+
try {
|
|
165
|
+
const file = path.join(MIXDOG_RUNTIME_ROOT, `owner-secret-${String(ownerInstanceId)}.json`);
|
|
166
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
167
|
+
return parsed && typeof parsed.secret === 'string' ? parsed.secret : '';
|
|
168
|
+
} catch {
|
|
169
|
+
return '';
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function ownerAuthHeaders(active) {
|
|
174
|
+
const ownerInstanceId = active && active.instanceId;
|
|
175
|
+
const ownerSecret = readOwnerSecretFor(ownerInstanceId);
|
|
176
|
+
if (!ownerSecret) return null;
|
|
177
|
+
return { 'x-owner-token': ownerSecret, 'x-owner-instance': String(ownerInstanceId) };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let _emitSink = null;
|
|
181
|
+
function setEmitSink(fn) { _emitSink = fn || null; }
|
|
182
|
+
function emit(additionalContext) {
|
|
183
|
+
if (!additionalContext) {
|
|
184
|
+
teeStderr(`[session-start] emit skipped: falsy context (empty string or null)\n`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const byteLen = Buffer.byteLength(additionalContext, 'utf8');
|
|
188
|
+
teeStderr(`[session-start] emit PART=${PART} bytes=${byteLen}\n`);
|
|
189
|
+
const out = JSON.stringify({
|
|
190
|
+
hookSpecificOutput: {
|
|
191
|
+
hookEventName: 'SessionStart',
|
|
192
|
+
additionalContext,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
if (_emitSink) _emitSink(out);
|
|
196
|
+
else process.stdout.write(out);
|
|
197
|
+
}
|
|
198
|
+
function setEvent(e) { _event = e || {}; }
|
|
199
|
+
function setPart(part) {
|
|
200
|
+
if (['rules', 'core', 'recap'].includes(part)) PART = part;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Memory-service HTTP port discovery. Single source of truth:
|
|
204
|
+
// `<tmpdir>/mixdog/active-instance.json` `memory_port` field, written by
|
|
205
|
+
// supervisor (server-main.mjs) on memory worker `ready` IPC. Throws when
|
|
206
|
+
// missing — caller surfaces the failure rather than silently falling back
|
|
207
|
+
// to a stale or default port.
|
|
208
|
+
function getMemoryServicePort() {
|
|
209
|
+
const advertised = readAdvertisedMemoryPort();
|
|
210
|
+
if (!advertised || advertised.stale) {
|
|
211
|
+
throw new Error(advertised && advertised.stale
|
|
212
|
+
? 'active-instance.json stale memory_port owner'
|
|
213
|
+
: 'active-instance.json missing memory_port');
|
|
214
|
+
}
|
|
215
|
+
return advertised.port;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function getLiveMemoryServicePort(probeMs = 200) {
|
|
219
|
+
try {
|
|
220
|
+
const advertised = readAdvertisedMemoryPort();
|
|
221
|
+
if (!advertised || advertised.stale) return null;
|
|
222
|
+
const port = advertised.port;
|
|
223
|
+
const alive = await probeTcpPort(port, Math.max(1, probeMs));
|
|
224
|
+
teeStderr(`[session-start] memoryDirect probePort=${port} probeMs=${probeMs} alive=${alive}\n`);
|
|
225
|
+
return alive ? port : null;
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function memoryServicePost(urlPath, body, timeoutMs = 5000) {
|
|
232
|
+
const port = getMemoryServicePort();
|
|
233
|
+
const res = await httpPostJson({
|
|
234
|
+
hostname: '127.0.0.1',
|
|
235
|
+
port,
|
|
236
|
+
path: urlPath,
|
|
237
|
+
timeoutMs,
|
|
238
|
+
body: body || {},
|
|
239
|
+
});
|
|
240
|
+
if (res.statusCode !== 200) {
|
|
241
|
+
throw new Error(`memory-service ${urlPath} non-200 status=${res.statusCode}`);
|
|
242
|
+
}
|
|
243
|
+
try { return JSON.parse(res.body); }
|
|
244
|
+
catch (e) { throw new Error(`memory-service ${urlPath} invalid JSON: ${e.message}`); }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function formatTs(ts) {
|
|
248
|
+
const n = Number(ts);
|
|
249
|
+
if (Number.isFinite(n) && n > 1e12) {
|
|
250
|
+
return new Date(n).toLocaleString('sv-SE').slice(0, 16);
|
|
251
|
+
}
|
|
252
|
+
return String(ts ?? '').slice(0, 16);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// MM-DD HH:MM in local time for compact recap rendering.
|
|
256
|
+
function formatTsShort(ts) {
|
|
257
|
+
const n = Number(ts);
|
|
258
|
+
if (!Number.isFinite(n) || n <= 1e12) return String(ts ?? '').slice(0, 16);
|
|
259
|
+
const full = new Date(n).toLocaleString('sv-SE');
|
|
260
|
+
return full.slice(5, 16);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Single source of truth: lib/text-utils.cjs (also imported by memory-extraction.mjs).
|
|
264
|
+
const { cleanMemoryText: cleanText } = require(path.join(PLUGIN_ROOT, 'lib', 'text-utils.cjs'));
|
|
265
|
+
|
|
266
|
+
function resolveCwdScope(cwd) {
|
|
267
|
+
try {
|
|
268
|
+
let dir = path.resolve(cwd || process.cwd());
|
|
269
|
+
while (true) {
|
|
270
|
+
const candidate = path.join(dir, '.mixdog', 'project.id');
|
|
271
|
+
try {
|
|
272
|
+
const val = fs.readFileSync(candidate, 'utf8').trim();
|
|
273
|
+
if (val.toLowerCase() === 'common') return null;
|
|
274
|
+
if (val) {
|
|
275
|
+
// Validate slug before returning — reject path traversal attempts.
|
|
276
|
+
if (val.includes('..') || val.includes('\\')) return null;
|
|
277
|
+
return val;
|
|
278
|
+
}
|
|
279
|
+
} catch {}
|
|
280
|
+
const parent = path.dirname(dir);
|
|
281
|
+
if (parent === dir) break;
|
|
282
|
+
dir = parent;
|
|
283
|
+
}
|
|
284
|
+
} catch {}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function buildContext(cwd) {
|
|
289
|
+
try {
|
|
290
|
+
const _t0 = Date.now();
|
|
291
|
+
const result = await memoryServicePost('/session-start/core-memory', {
|
|
292
|
+
cwd: cwd || process.cwd(),
|
|
293
|
+
});
|
|
294
|
+
const _elapsed = Date.now() - _t0;
|
|
295
|
+
if (!result || result.ok !== true) return '';
|
|
296
|
+
const dbLines = Array.isArray(result.dbLines) ? result.dbLines : [];
|
|
297
|
+
const userLines = Array.isArray(result.userLines) ? result.userLines : [];
|
|
298
|
+
teeStderr(`[session-start] buildContext POST /session-start/core-memory elapsed=${_elapsed}ms dbLines=${dbLines.length} userLines=${userLines.length}\n`);
|
|
299
|
+
const seen = new Set();
|
|
300
|
+
const lines = [];
|
|
301
|
+
// userLines (user-curated) and dbLines accumulate under the same char cap.
|
|
302
|
+
// userLines go first so they win the budget. Each line is already short
|
|
303
|
+
// (core_summary <=120, manual core <=120 at write time), so the cap is a
|
|
304
|
+
// safety net rather than the primary limiter.
|
|
305
|
+
const HEADER_LEN = '## Core Memory\n'.length;
|
|
306
|
+
// CAP = 5000 chars — documented host-preview envelope. This is a byte cap,
|
|
307
|
+
// not token-aware (adding a token counter would require a tokeniser dependency).
|
|
308
|
+
// Deferred: token-aware cap once a lightweight counter is available.
|
|
309
|
+
const CAP = 5000;
|
|
310
|
+
let total = HEADER_LEN;
|
|
311
|
+
for (const line of userLines) {
|
|
312
|
+
const key = line.toLowerCase().replace(/\s+/g, ' ').slice(0, 120);
|
|
313
|
+
if (seen.has(key)) continue;
|
|
314
|
+
const add = line.length + 1;
|
|
315
|
+
if (total + add > CAP) break;
|
|
316
|
+
seen.add(key);
|
|
317
|
+
lines.push(line);
|
|
318
|
+
total += add;
|
|
319
|
+
}
|
|
320
|
+
// dbLines: accumulate score-DESC rows until the char cap is reached.
|
|
321
|
+
for (const line of dbLines) {
|
|
322
|
+
const key = line.toLowerCase().replace(/\s+/g, ' ').slice(0, 120);
|
|
323
|
+
if (seen.has(key)) continue;
|
|
324
|
+
const add = line.length + 1;
|
|
325
|
+
if (total + add > CAP) break;
|
|
326
|
+
seen.add(key);
|
|
327
|
+
lines.push(line);
|
|
328
|
+
total += add;
|
|
329
|
+
}
|
|
330
|
+
if (lines.length === 0) return '';
|
|
331
|
+
return `## Core Memory\n${lines.join('\n')}`;
|
|
332
|
+
} catch (e) {
|
|
333
|
+
teeStderr(`[session-start] buildContext catch endpoint=/session-start/core-memory err=${e.message}\n`);
|
|
334
|
+
process.stderr.write(`[session-start] context build failed: ${e.message}\n`);
|
|
335
|
+
return '';
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Returns { lines } — chronological "[MM-DD HH:MM] <summary>" entries
|
|
340
|
+
// (oldest → newest), trimmed from the front so the rendered block fits the
|
|
341
|
+
// SessionStart hook output cap (10,000 chars total — leaves margin for the
|
|
342
|
+
// JSON wrapper around additionalContext; header "## Recap\n" reserved).
|
|
343
|
+
async function buildRecapData(cwd) {
|
|
344
|
+
const out = { lines: [] };
|
|
345
|
+
try {
|
|
346
|
+
const _t0 = Date.now();
|
|
347
|
+
const result = await memoryServicePost('/session-start/recap', {
|
|
348
|
+
cwd: cwd || process.cwd(),
|
|
349
|
+
});
|
|
350
|
+
const _elapsed = Date.now() - _t0;
|
|
351
|
+
if (!result || result.ok !== true) return out;
|
|
352
|
+
const rows = Array.isArray(result.rows) ? result.rows : [];
|
|
353
|
+
teeStderr(`[session-start] buildRecapData POST /session-start/recap elapsed=${_elapsed}ms rows=${rows.length}\n`);
|
|
354
|
+
if (rows.length === 0) return out;
|
|
355
|
+
|
|
356
|
+
const rendered = rows.map(r => {
|
|
357
|
+
const tsStr = formatTsShort(r.ts);
|
|
358
|
+
const summary = String(r.summary || '').trim().slice(0, 1000);
|
|
359
|
+
return summary ? `[${tsStr}] ${summary}` : '';
|
|
360
|
+
}).filter(Boolean);
|
|
361
|
+
if (rendered.length === 0) return out;
|
|
362
|
+
|
|
363
|
+
// Dedup by normalized summary — newest-first, so older repeats drop.
|
|
364
|
+
const seen = new Set();
|
|
365
|
+
const uniq = [];
|
|
366
|
+
for (const line of rendered) {
|
|
367
|
+
const key = line.replace(/^\[[^\]]+\]\s*/, '').toLowerCase().replace(/\s+/g, ' ').slice(0, 80);
|
|
368
|
+
if (seen.has(key)) continue;
|
|
369
|
+
seen.add(key);
|
|
370
|
+
uniq.push(line);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Newest → oldest; keep accumulating from the newest end until the
|
|
374
|
+
// running total would exceed the cap, then reverse to chronological.
|
|
375
|
+
const HEADER_LEN = '## Recap\n'.length;
|
|
376
|
+
const CAP = 5000;
|
|
377
|
+
let total = HEADER_LEN;
|
|
378
|
+
const kept = [];
|
|
379
|
+
for (const line of uniq) {
|
|
380
|
+
const add = line.length + 1;
|
|
381
|
+
if (total + add > CAP) break;
|
|
382
|
+
kept.push(line);
|
|
383
|
+
total += add;
|
|
384
|
+
}
|
|
385
|
+
out.lines = kept.reverse();
|
|
386
|
+
return out;
|
|
387
|
+
} catch (e) {
|
|
388
|
+
teeStderr(`[session-start] buildRecapData catch endpoint=/session-start/recap err=${e.message}\n`);
|
|
389
|
+
process.stderr.write(`[session-start] recap build failed: ${e.message}\n`);
|
|
390
|
+
return out;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ---------------------------------------------------------------------------
|
|
395
|
+
// Skip flag — resume / compact reuses the existing context, so re-injecting
|
|
396
|
+
// memory just bloats tokens. Rules still flow through so any rule changes
|
|
397
|
+
// since the last turn take effect.
|
|
398
|
+
// ---------------------------------------------------------------------------
|
|
399
|
+
function _skipMemoryInject() { return _event.source === 'resume' || _event.source === 'compact'; }
|
|
400
|
+
if (!IS_DAEMON_REQUIRE) {
|
|
401
|
+
teeStderr(`[session-start] skipMemoryInject=${_skipMemoryInject()} PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()}\n`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ---------------------------------------------------------------------------
|
|
405
|
+
// Part: rules (slot 1) — owns ALL one-shot session bootstrap work and emits
|
|
406
|
+
// the rules block. Static .md content only; cycle1 is triggered by
|
|
407
|
+
// core/recap slots (dedupe coalesces concurrent calls into one run).
|
|
408
|
+
// ---------------------------------------------------------------------------
|
|
409
|
+
|
|
410
|
+
function hasManagedClaudeMdBlock(targetPath) {
|
|
411
|
+
if (!targetPath) return false;
|
|
412
|
+
try {
|
|
413
|
+
const { expandHome, MARKER_START, MARKER_END } = require(path.join(PLUGIN_ROOT, 'lib', 'claude-md-writer.cjs'));
|
|
414
|
+
const resolved = expandHome(targetPath);
|
|
415
|
+
if (!resolved || !fs.existsSync(resolved)) return false;
|
|
416
|
+
const content = fs.readFileSync(resolved, 'utf8');
|
|
417
|
+
return content.includes(MARKER_START) && content.includes(MARKER_END);
|
|
418
|
+
} catch {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Stable, NON-versioned launcher path in mixdog-owned plugin data — survives
|
|
424
|
+
// cache cleanup and version rotation, so settings.json can point at it forever
|
|
425
|
+
// (the launcher resolves the active install at runtime each tick).
|
|
426
|
+
function stableLauncherPath() {
|
|
427
|
+
return path.join(DATA_DIR, 'statusline-launcher.mjs').replace(/\\/g, '/');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function injectStatusLine(pluginRoot) {
|
|
431
|
+
try {
|
|
432
|
+
// Refresh the stable launcher copy from the current install each session
|
|
433
|
+
// start so it tracks the latest committed launcher code.
|
|
434
|
+
const launcherDst = stableLauncherPath();
|
|
435
|
+
try {
|
|
436
|
+
const launcherSrc = path.join(pluginRoot, 'bin', 'statusline-launcher.mjs');
|
|
437
|
+
fs.mkdirSync(path.dirname(launcherDst), { recursive: true });
|
|
438
|
+
fs.copyFileSync(launcherSrc, launcherDst);
|
|
439
|
+
} catch (e) {
|
|
440
|
+
process.stderr.write(`[session-start] statusLine launcher copy failed: ${e.message}\n`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
|
|
444
|
+
let raw;
|
|
445
|
+
try { raw = fs.readFileSync(settingsPath, 'utf8'); } catch { return; }
|
|
446
|
+
let settings;
|
|
447
|
+
try { settings = JSON.parse(raw); } catch { return; }
|
|
448
|
+
if (!settings || typeof settings !== 'object') return;
|
|
449
|
+
|
|
450
|
+
const desiredCommand = `bun "${launcherDst}"`;
|
|
451
|
+
// Launcher delegates to the fast native shim when present, so a constant
|
|
452
|
+
// short tick is fine and avoids cadence ping-pong across versions.
|
|
453
|
+
const desiredRefreshInterval = 5;
|
|
454
|
+
const existing = settings.statusLine;
|
|
455
|
+
// Recognize legacy mixdog-managed entries (versioned shim / statusline.mjs
|
|
456
|
+
// commands and the new stable launcher) so old pinned commands are MIGRATED
|
|
457
|
+
// to the stable launcher. A genuine user-custom statusLine is left alone.
|
|
458
|
+
const _cmd = (existing && typeof existing === 'object' && typeof existing.command === 'string')
|
|
459
|
+
? existing.command : '';
|
|
460
|
+
// Normalize Windows backslashes once, then require a mixdog-SPECIFIC marker
|
|
461
|
+
// so a generic user command that merely mentions statusline.mjs is not
|
|
462
|
+
// hijacked. A bare statusline.mjs NOT under a mixdog path must NOT match.
|
|
463
|
+
const c = _cmd.replace(/\\/g, '/');
|
|
464
|
+
// No standalone --kind=statusline arm: the real mixdog shim command always
|
|
465
|
+
// carries the /mixdog-shim path (caught below), and a bare flag could be a
|
|
466
|
+
// genuine user command (e.g. `node user-tool.mjs --kind=statusline`).
|
|
467
|
+
const _looksMixdog = /\/mixdog-shim(?:\.exe)?/.test(c)
|
|
468
|
+
|| c.includes('/trib-plugin/mixdog/')
|
|
469
|
+
|| (c.includes('mixdog-trib-plugin') && c.includes('statusline-launcher.mjs'));
|
|
470
|
+
const isOurs = existing && typeof existing === 'object'
|
|
471
|
+
&& (existing.source === 'mixdog-auto' || _looksMixdog);
|
|
472
|
+
|
|
473
|
+
if (existing && !isOurs) return;
|
|
474
|
+
if (
|
|
475
|
+
isOurs
|
|
476
|
+
&& existing.command === desiredCommand
|
|
477
|
+
&& existing.type === 'command'
|
|
478
|
+
&& existing.refreshInterval === desiredRefreshInterval
|
|
479
|
+
) return;
|
|
480
|
+
|
|
481
|
+
settings.statusLine = {
|
|
482
|
+
type: 'command',
|
|
483
|
+
command: desiredCommand,
|
|
484
|
+
refreshInterval: desiredRefreshInterval,
|
|
485
|
+
source: 'mixdog-auto',
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
const tmpPath = settingsPath + '.mixdog-tmp';
|
|
489
|
+
fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n');
|
|
490
|
+
fs.renameSync(tmpPath, settingsPath);
|
|
491
|
+
} catch (e) {
|
|
492
|
+
process.stderr.write(`[session-start] statusLine inject failed: ${e.message}\n`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function cwdToProjectSlug(cwd) {
|
|
497
|
+
return path.resolve(cwd).replace(/\\/g, '/').replace(/^([A-Za-z]):/, '$1-').replace(/\//g, '-');
|
|
498
|
+
}
|
|
499
|
+
function resolveTranscriptPath() {
|
|
500
|
+
const direct = _event.transcript_path || _event.transcriptPath;
|
|
501
|
+
if (typeof direct === 'string' && direct && fs.existsSync(direct)) return direct;
|
|
502
|
+
const sessionId = _event.session_id || _event.sessionId;
|
|
503
|
+
const cwd = _event.cwd || process.cwd();
|
|
504
|
+
if (typeof sessionId === 'string' && sessionId) {
|
|
505
|
+
const candidate = path.join(os.homedir(), '.claude', 'projects', cwdToProjectSlug(cwd), `${sessionId}.jsonl`);
|
|
506
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
507
|
+
}
|
|
508
|
+
return '';
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function rebindActiveInstance() {
|
|
512
|
+
try {
|
|
513
|
+
const activePath = ACTIVE_INSTANCE_FILE;
|
|
514
|
+
if (!fs.existsSync(activePath)) return;
|
|
515
|
+
const active = JSON.parse(fs.readFileSync(activePath, 'utf8'));
|
|
516
|
+
if (!active.httpPort) return;
|
|
517
|
+
const transcriptPath = resolveTranscriptPath();
|
|
518
|
+
const payload = transcriptPath ? JSON.stringify({ transcriptPath }) : '';
|
|
519
|
+
const authHeaders = ownerAuthHeaders(active);
|
|
520
|
+
if (!authHeaders) return;
|
|
521
|
+
const headers = { 'Content-Type': 'application/json', ...authHeaders };
|
|
522
|
+
if (payload) headers['Content-Length'] = Buffer.byteLength(payload);
|
|
523
|
+
const req2 = http.request({
|
|
524
|
+
hostname: '127.0.0.1',
|
|
525
|
+
port: active.httpPort,
|
|
526
|
+
path: '/rebind',
|
|
527
|
+
method: 'POST',
|
|
528
|
+
timeout: 3000,
|
|
529
|
+
headers,
|
|
530
|
+
});
|
|
531
|
+
req2.on('error', () => {});
|
|
532
|
+
req2.on('timeout', () => req2.destroy());
|
|
533
|
+
if (payload) req2.write(payload);
|
|
534
|
+
req2.end();
|
|
535
|
+
} catch {}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// TCP probe — resolves true if the port accepts a connection within probeMs,
|
|
539
|
+
// false on ECONNREFUSED / EHOSTUNREACH / timeout / any other socket error.
|
|
540
|
+
// Used by pollActiveInstance to skip stale active-instance.json entries left
|
|
541
|
+
// over from a previous session whose channels owner is already dead.
|
|
542
|
+
function probeTcpPort(port, probeMs) {
|
|
543
|
+
return new Promise((resolve) => {
|
|
544
|
+
let settled = false;
|
|
545
|
+
const done = (alive) => {
|
|
546
|
+
if (settled) return;
|
|
547
|
+
settled = true;
|
|
548
|
+
try { socket.destroy(); } catch {}
|
|
549
|
+
resolve(alive);
|
|
550
|
+
};
|
|
551
|
+
const socket = net.createConnection({ port, host: '127.0.0.1' });
|
|
552
|
+
socket.setTimeout(Math.max(1, probeMs));
|
|
553
|
+
socket.once('connect', () => done(true));
|
|
554
|
+
socket.once('error', () => done(false));
|
|
555
|
+
socket.once('timeout', () => done(false));
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Memory-runtime readiness probe. A promoted-but-uninitialized fork-proxy keeps
|
|
560
|
+
// a live TCP listener while its `db` is still null (it short-circuited
|
|
561
|
+
// _initRuntime), so a raw TCP accept does NOT prove the memory worker can serve
|
|
562
|
+
// queries — recap/core would then 500 and silently drop context. GET /health
|
|
563
|
+
// returns 200 only once the worker is _initialized with a live DB; 503 while
|
|
564
|
+
// booting/promoting and 500 if the DB is broken. Gate awaitMemoryPort on a 200.
|
|
565
|
+
function probeMemoryHealthy(port, timeoutMs) {
|
|
566
|
+
return new Promise((resolve) => {
|
|
567
|
+
let settled = false;
|
|
568
|
+
const done = (ok) => { if (settled) return; settled = true; resolve(ok); };
|
|
569
|
+
let req;
|
|
570
|
+
try {
|
|
571
|
+
req = http.request(
|
|
572
|
+
{ host: '127.0.0.1', port, path: '/health', method: 'GET', timeout: Math.max(1, timeoutMs) },
|
|
573
|
+
(res) => {
|
|
574
|
+
const ok = res.statusCode === 200;
|
|
575
|
+
res.resume();
|
|
576
|
+
res.once('end', () => done(ok));
|
|
577
|
+
res.once('error', () => done(false));
|
|
578
|
+
},
|
|
579
|
+
);
|
|
580
|
+
} catch { done(false); return; }
|
|
581
|
+
req.once('error', () => done(false));
|
|
582
|
+
req.once('timeout', () => { try { req.destroy(); } catch {} done(false); });
|
|
583
|
+
req.end();
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function pollActiveInstance(graceMs) {
|
|
588
|
+
const activeDir = MIXDOG_RUNTIME_ROOT;
|
|
589
|
+
const activeFile = ACTIVE_INSTANCE_FILE;
|
|
590
|
+
const _pollStart = Date.now();
|
|
591
|
+
const deadline = _pollStart + Math.max(0, graceMs);
|
|
592
|
+
teeStderr(`[session-start] pollActiveInstance start graceMs=${graceMs}\n`);
|
|
593
|
+
try { fs.mkdirSync(activeDir, { recursive: true }); } catch {}
|
|
594
|
+
|
|
595
|
+
// Single read+probe attempt. Stale guard: file may be left by a dead owner,
|
|
596
|
+
// so TCP-probe httpPort before accepting. Returns null on any failure
|
|
597
|
+
// (missing file, missing httpPort, dead port, parse error, deadline hit).
|
|
598
|
+
const tryRead = async () => {
|
|
599
|
+
try {
|
|
600
|
+
if (!fs.existsSync(activeFile)) return null;
|
|
601
|
+
const active = JSON.parse(fs.readFileSync(activeFile, 'utf8'));
|
|
602
|
+
if (!active || !active.httpPort) return null;
|
|
603
|
+
const remaining = deadline - Date.now();
|
|
604
|
+
if (remaining <= 0) return null;
|
|
605
|
+
const probeMs = Math.min(200, remaining);
|
|
606
|
+
const alive = await probeTcpPort(active.httpPort, probeMs);
|
|
607
|
+
teeStderr(`[session-start] pollActiveInstance probePort=${active.httpPort} probeMs=${probeMs} alive=${alive}\n`);
|
|
608
|
+
return alive ? active : null;
|
|
609
|
+
} catch {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
// Fast path: supervisor already up. Most sessions hit this — skips watch setup.
|
|
615
|
+
const initial = await tryRead();
|
|
616
|
+
if (initial) {
|
|
617
|
+
teeStderr(`[session-start] pollActiveInstance done elapsed=${Date.now() - _pollStart}ms port=${initial.httpPort} via=initial\n`);
|
|
618
|
+
return initial;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Bounded poll loop. Windows fs.watch first-event latency averaged ~3.3s
|
|
622
|
+
// on this directory (measured over recent sessions), so a fixed 200ms
|
|
623
|
+
// tick is deterministic and faster on average. server.mjs writes
|
|
624
|
+
// active-instance.json atomically via .tmp + rename, so every poll sees
|
|
625
|
+
// either no file or a fully-written one — no torn-read window. setTimeout
|
|
626
|
+
// caps total wait at graceMs so the function honors its contract.
|
|
627
|
+
return new Promise((resolve) => {
|
|
628
|
+
let settled = false;
|
|
629
|
+
let inFlight = false;
|
|
630
|
+
let pollTimer = null;
|
|
631
|
+
let deadlineTimer = null;
|
|
632
|
+
|
|
633
|
+
const finish = (result, via) => {
|
|
634
|
+
if (settled) return;
|
|
635
|
+
settled = true;
|
|
636
|
+
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
637
|
+
if (deadlineTimer) { clearTimeout(deadlineTimer); deadlineTimer = null; }
|
|
638
|
+
if (result) {
|
|
639
|
+
teeStderr(`[session-start] pollActiveInstance done elapsed=${Date.now() - _pollStart}ms port=${result.httpPort} via=${via}\n`);
|
|
640
|
+
} else {
|
|
641
|
+
teeStderr(`[session-start] pollActiveInstance end elapsed=${Date.now() - _pollStart}ms result=null\n`);
|
|
642
|
+
}
|
|
643
|
+
resolve(result);
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
const checkOnce = async () => {
|
|
647
|
+
if (settled || inFlight) return;
|
|
648
|
+
inFlight = true;
|
|
649
|
+
try {
|
|
650
|
+
const a = await tryRead();
|
|
651
|
+
if (a) finish(a, 'poll');
|
|
652
|
+
} finally {
|
|
653
|
+
inFlight = false;
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// Node setInterval / setTimeout are ref'd by default, so they keep the
|
|
658
|
+
// event loop alive. The outer caller also awaits this Promise, which is
|
|
659
|
+
// itself a liveness guarantee.
|
|
660
|
+
pollTimer = setInterval(checkOnce, 200);
|
|
661
|
+
const remaining = Math.max(0, deadline - Date.now());
|
|
662
|
+
deadlineTimer = setTimeout(() => finish(null, null), remaining);
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Wait until memory_port appears in active-instance.json and /health is ready.
|
|
667
|
+
// pollActiveInstance only proves channels httpPort;
|
|
668
|
+
// memory_port is written later via memory worker `ready` IPC. Without this,
|
|
669
|
+
// getMemoryServicePort throws on fast /clear paths and recap emits 0 lines.
|
|
670
|
+
async function awaitMemoryPort(graceMs) {
|
|
671
|
+
const _t0 = Date.now();
|
|
672
|
+
const deadline = _t0 + Math.max(0, graceMs);
|
|
673
|
+
teeStderr(`[session-start] awaitMemoryPort start graceMs=${graceMs}\n`);
|
|
674
|
+
|
|
675
|
+
while (Date.now() < deadline) {
|
|
676
|
+
const remaining = deadline - Date.now();
|
|
677
|
+
if (remaining <= 0) break;
|
|
678
|
+
try {
|
|
679
|
+
if (!fs.existsSync(ACTIVE_INSTANCE_FILE)) {
|
|
680
|
+
await sleepMs(Math.min(200, remaining));
|
|
681
|
+
continue;
|
|
682
|
+
}
|
|
683
|
+
const advertised = readAdvertisedMemoryPort();
|
|
684
|
+
if (!advertised) {
|
|
685
|
+
await sleepMs(Math.min(200, remaining));
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
if (advertised.stale) {
|
|
689
|
+
teeStderr(`[session-start] awaitMemoryPort stalePort=${advertised.port} activeServerPid=${advertised.activeServerPid} memoryServerPid=${advertised.memoryServerPid}\n`);
|
|
690
|
+
await sleepMs(Math.min(200, remaining));
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
const port = advertised.port;
|
|
694
|
+
const probeMs = Math.min(200, remaining);
|
|
695
|
+
const healthy = await probeMemoryHealthy(port, probeMs);
|
|
696
|
+
let supersededBy = null;
|
|
697
|
+
let staleAfterProbe = false;
|
|
698
|
+
try {
|
|
699
|
+
const latest = readAdvertisedMemoryPort();
|
|
700
|
+
if (latest && latest.stale) staleAfterProbe = true;
|
|
701
|
+
else if (latest && latest.port !== port) supersededBy = latest.port;
|
|
702
|
+
} catch {}
|
|
703
|
+
const suffix = `${staleAfterProbe ? ' staleAfterProbe=true' : ''}${supersededBy ? ` supersededBy=${supersededBy}` : ''}`;
|
|
704
|
+
teeStderr(`[session-start] awaitMemoryPort probePort=${port} probeMs=${probeMs} healthy=${healthy}${suffix}\n`);
|
|
705
|
+
if (staleAfterProbe || supersededBy) continue;
|
|
706
|
+
if (healthy) {
|
|
707
|
+
teeStderr(`[session-start] awaitMemoryPort done elapsed=${Date.now() - _t0}ms port=${port} via=poll\n`);
|
|
708
|
+
return port;
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
// Re-read active-instance.json on the next tick; atomic rename can
|
|
712
|
+
// transiently hide the file on Windows.
|
|
713
|
+
}
|
|
714
|
+
await sleepMs(Math.min(200, Math.max(0, deadline - Date.now())));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
teeStderr(`[session-start] awaitMemoryPort end elapsed=${Date.now() - _t0}ms result=null\n`);
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function httpPostJson({ hostname, port, path: urlPath, timeoutMs, body, ownerActive, authHeaders: explicitAuthHeaders }) {
|
|
722
|
+
return new Promise((resolve, reject) => {
|
|
723
|
+
const _t0 = Date.now();
|
|
724
|
+
const payload = typeof body === 'string' ? body : JSON.stringify(body);
|
|
725
|
+
const authHeaders = explicitAuthHeaders || (ownerActive ? ownerAuthHeaders(ownerActive) : {});
|
|
726
|
+
if (ownerActive && !authHeaders) {
|
|
727
|
+
resolve({
|
|
728
|
+
statusCode: 0,
|
|
729
|
+
body: JSON.stringify({ ok: false, reason: 'owner-route-unavailable' }),
|
|
730
|
+
ownerRouteUnavailable: true,
|
|
731
|
+
});
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const req = http.request({
|
|
735
|
+
hostname,
|
|
736
|
+
port,
|
|
737
|
+
path: urlPath,
|
|
738
|
+
method: 'POST',
|
|
739
|
+
headers: {
|
|
740
|
+
'Content-Type': 'application/json',
|
|
741
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
742
|
+
...authHeaders,
|
|
743
|
+
},
|
|
744
|
+
timeout: Math.max(1, timeoutMs),
|
|
745
|
+
}, (res) => {
|
|
746
|
+
const chunks = [];
|
|
747
|
+
res.on('data', (c) => chunks.push(c));
|
|
748
|
+
res.on('end', () => {
|
|
749
|
+
const _body = Buffer.concat(chunks).toString('utf8');
|
|
750
|
+
const _elapsed = Date.now() - _t0;
|
|
751
|
+
let _bodyReason = null;
|
|
752
|
+
try { const _p = JSON.parse(_body); if (_p && typeof _p.reason === 'string') _bodyReason = _p.reason; } catch {}
|
|
753
|
+
teeStderr(`[session-start] httpPostJson path=${urlPath} port=${port} timeoutMs=${timeoutMs} statusCode=${res.statusCode} bodyReason=${_bodyReason || ''} elapsed=${_elapsed}ms\n`);
|
|
754
|
+
resolve({ statusCode: res.statusCode, body: _body });
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
req.on('error', (e) => reject(e));
|
|
758
|
+
req.on('timeout', () => { req.destroy(new Error('timeout')); });
|
|
759
|
+
req.end(payload);
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// Pull cycle1 signals from a cycle1 response. Memory worker returns an MCP
|
|
764
|
+
// envelope { content:[{type:'text', text:'cycle1: chunks=N processed=M
|
|
765
|
+
// skipped=K pending=P inFlight=B'}], isError }; channels owner returns
|
|
766
|
+
// { ok, result } where result may carry the same text shape. Each field is
|
|
767
|
+
// null when not parseable (treated downstream as "unknown / cannot verify").
|
|
768
|
+
function extractCycleSignals(parsed) {
|
|
769
|
+
const empty = { processed: null, pendingRows: null, skippedInFlight: null };
|
|
770
|
+
if (!parsed) return empty;
|
|
771
|
+
let text = '';
|
|
772
|
+
if (Array.isArray(parsed.content) && parsed.content[0] && typeof parsed.content[0].text === 'string') {
|
|
773
|
+
text = parsed.content[0].text;
|
|
774
|
+
} else if (parsed.result && typeof parsed.result === 'object'
|
|
775
|
+
&& Array.isArray(parsed.result.content)
|
|
776
|
+
&& parsed.result.content[0]
|
|
777
|
+
&& typeof parsed.result.content[0].text === 'string') {
|
|
778
|
+
// Channels owner wraps the memory worker's MCP envelope as
|
|
779
|
+
// { ok, result: { content:[{type:'text',text:'cycle1: ...'}], isError } },
|
|
780
|
+
// so the signal text lives at parsed.result.content[0].text.
|
|
781
|
+
text = parsed.result.content[0].text;
|
|
782
|
+
} else if (parsed.result && typeof parsed.result === 'object' && typeof parsed.result.text === 'string') {
|
|
783
|
+
text = parsed.result.text;
|
|
784
|
+
} else if (typeof parsed.result === 'string') {
|
|
785
|
+
text = parsed.result;
|
|
786
|
+
}
|
|
787
|
+
if (typeof text !== 'string') return empty;
|
|
788
|
+
const proc = text.match(/processed=(\d+)/);
|
|
789
|
+
const pend = text.match(/pending=(\d+)/);
|
|
790
|
+
const inflight = text.match(/inFlight=(true|false)/);
|
|
791
|
+
return {
|
|
792
|
+
processed: proc ? parseInt(proc[1], 10) : null,
|
|
793
|
+
pendingRows: pend ? parseInt(pend[1], 10) : null,
|
|
794
|
+
skippedInFlight: inflight ? inflight[1] === 'true' : null,
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
async function requestCycle1MemoryDirect(deadline, opts = {}, priorReason = '') {
|
|
799
|
+
const slot = opts.slot || 'unknown';
|
|
800
|
+
const start = Date.now();
|
|
801
|
+
const remainingForProbe = deadline - Date.now();
|
|
802
|
+
if (remainingForProbe <= 0) {
|
|
803
|
+
teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=timeout-before-probe prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
const port = await getLiveMemoryServicePort(Math.min(200, remainingForProbe));
|
|
807
|
+
if (!port) {
|
|
808
|
+
teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=no-memory-port prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
|
|
809
|
+
return null;
|
|
810
|
+
}
|
|
811
|
+
const remaining = deadline - Date.now();
|
|
812
|
+
if (remaining <= 0) {
|
|
813
|
+
teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=timeout-before-post prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
|
|
814
|
+
return null;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
try {
|
|
818
|
+
const ON_DEMAND_CYCLE1_ARGS = { min_batch: 1, session_cap: 5, batch_size: 20, concurrency: 5 };
|
|
819
|
+
const res = await httpPostJson({
|
|
820
|
+
hostname: '127.0.0.1',
|
|
821
|
+
port,
|
|
822
|
+
path: '/api/tool',
|
|
823
|
+
timeoutMs: remaining,
|
|
824
|
+
body: {
|
|
825
|
+
name: 'memory',
|
|
826
|
+
arguments: {
|
|
827
|
+
action: 'cycle1',
|
|
828
|
+
...ON_DEMAND_CYCLE1_ARGS,
|
|
829
|
+
_callerDeadlineMs: remaining,
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
});
|
|
833
|
+
if (res.statusCode !== 200) {
|
|
834
|
+
teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=non-200 statusCode=${res.statusCode} prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
|
|
835
|
+
return { ok: false, reason: 'memory-direct-non-200', statusCode: res.statusCode, bodyReason: null, elapsedMs: Date.now() - start, route: 'memory-direct' };
|
|
836
|
+
}
|
|
837
|
+
let parsed;
|
|
838
|
+
try { parsed = JSON.parse(res.body); }
|
|
839
|
+
catch {
|
|
840
|
+
teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=parse-error prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
|
|
841
|
+
return { ok: false, reason: 'memory-direct-parse-error', statusCode: 200, bodyReason: null, elapsedMs: Date.now() - start, route: 'memory-direct' };
|
|
842
|
+
}
|
|
843
|
+
if (parsed && parsed.isError) {
|
|
844
|
+
const sigText = Array.isArray(parsed.content) && parsed.content[0] ? parsed.content[0].text : '';
|
|
845
|
+
teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=body-is-error text=${String(sigText || '').slice(0, 200)} prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
|
|
846
|
+
return { ok: false, reason: 'memory-direct-body-is-error', statusCode: 200, bodyReason: null, elapsedMs: Date.now() - start, route: 'memory-direct' };
|
|
847
|
+
}
|
|
848
|
+
const sig = extractCycleSignals(parsed);
|
|
849
|
+
const procStr = sig.processed != null ? sig.processed : '?';
|
|
850
|
+
const pendStr = sig.pendingRows != null ? sig.pendingRows : '?';
|
|
851
|
+
const inFlightStr = sig.skippedInFlight === true ? 'true'
|
|
852
|
+
: sig.skippedInFlight === false ? 'false' : '?';
|
|
853
|
+
teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=ok processed=${procStr} pending=${pendStr} inFlight=${inFlightStr} prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
|
|
854
|
+
return {
|
|
855
|
+
ok: true,
|
|
856
|
+
processed: sig.processed,
|
|
857
|
+
pendingRows: sig.pendingRows,
|
|
858
|
+
skippedInFlight: sig.skippedInFlight,
|
|
859
|
+
route: 'memory-direct',
|
|
860
|
+
elapsedMs: Date.now() - start,
|
|
861
|
+
};
|
|
862
|
+
} catch (e) {
|
|
863
|
+
const msg = (e && e.message) || e;
|
|
864
|
+
const reason = /timeout/i.test(String(msg)) ? 'memory-direct-timeout' : 'memory-direct-http-error';
|
|
865
|
+
teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct reason=${reason} err=${String(msg).slice(0, 200)} prior=${priorReason} elapsed=${Date.now() - start}ms\n`);
|
|
866
|
+
return { ok: false, reason, statusCode: null, bodyReason: null, elapsedMs: Date.now() - start, route: 'memory-direct' };
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
async function waitCycle1MemoryDirect(deadline, opts = {}, waitMs = 1800) {
|
|
871
|
+
const slot = opts.slot || 'unknown';
|
|
872
|
+
const start = Date.now();
|
|
873
|
+
const waitUntil = Math.min(deadline, start + Math.max(0, waitMs));
|
|
874
|
+
const pollMs = 50;
|
|
875
|
+
let attempt = 0;
|
|
876
|
+
while (Date.now() < waitUntil) {
|
|
877
|
+
const sleepMs = Math.min(pollMs, waitUntil - Date.now());
|
|
878
|
+
if (sleepMs > 0) await new Promise((r) => setTimeout(r, sleepMs));
|
|
879
|
+
const direct = await requestCycle1MemoryDirect(deadline, opts, `pre-channels-wait-${attempt}`);
|
|
880
|
+
if (direct && direct.ok) {
|
|
881
|
+
teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct wait-hit attempt=${attempt} elapsed=${Date.now() - start}ms\n`);
|
|
882
|
+
return direct;
|
|
883
|
+
}
|
|
884
|
+
if (direct && !direct.ok) {
|
|
885
|
+
teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct wait-stop attempt=${attempt} reason=${direct.reason || 'unknown'} elapsed=${Date.now() - start}ms\n`);
|
|
886
|
+
return null;
|
|
887
|
+
}
|
|
888
|
+
attempt++;
|
|
889
|
+
}
|
|
890
|
+
teeStderr(`[session-start] cycle1 slot=${slot} route=memory-direct wait-miss attempts=${attempt} elapsed=${Date.now() - start}ms\n`);
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// One full cycle1 attempt. Prefer the memory service when it is already
|
|
895
|
+
// advertised and alive: channels /cycle1 is only a proxy to the same memory
|
|
896
|
+
// action, while the channels owner can lag during session handoff. If memory
|
|
897
|
+
// is not ready, fall back to the channels owner readiness path.
|
|
898
|
+
async function requestCycle1Once(deadline, opts) {
|
|
899
|
+
const slot = opts.slot || 'unknown';
|
|
900
|
+
const graceMs = Number.isFinite(opts.graceMs) ? opts.graceMs : 5000;
|
|
901
|
+
const start = Date.now();
|
|
902
|
+
|
|
903
|
+
const finish = (payload) => {
|
|
904
|
+
const elapsedMs = Date.now() - start;
|
|
905
|
+
if (payload.ok) {
|
|
906
|
+
const procStr = payload.processed != null ? payload.processed : '?';
|
|
907
|
+
const pendStr = payload.pendingRows != null ? payload.pendingRows : '?';
|
|
908
|
+
const inFlightStr = payload.skippedInFlight === true ? 'true'
|
|
909
|
+
: payload.skippedInFlight === false ? 'false' : '?';
|
|
910
|
+
teeStderr(`[session-start] cycle1 slot=${slot} route=channels reason=ok processed=${procStr} pending=${pendStr} inFlight=${inFlightStr} elapsed=${elapsedMs}ms\n`);
|
|
911
|
+
return {
|
|
912
|
+
ok: true,
|
|
913
|
+
processed: payload.processed,
|
|
914
|
+
pendingRows: payload.pendingRows,
|
|
915
|
+
skippedInFlight: payload.skippedInFlight,
|
|
916
|
+
route: 'channels',
|
|
917
|
+
elapsedMs,
|
|
918
|
+
};
|
|
919
|
+
}
|
|
920
|
+
const sc = payload.statusCode != null ? ` statusCode=${payload.statusCode}` : '';
|
|
921
|
+
teeStderr(`[session-start] cycle1 slot=${slot} route=channels reason=${payload.reason}${sc} elapsed=${elapsedMs}ms\n`);
|
|
922
|
+
return { ok: false, reason: payload.reason, statusCode: payload.statusCode, bodyReason: payload.bodyReason || null, elapsedMs, route: 'channels' };
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
const classifyError = (e) => {
|
|
926
|
+
const msg = (e && e.message) || '';
|
|
927
|
+
if (/timeout/i.test(msg)) return 'timeout';
|
|
928
|
+
if ((e && e.code === 'ECONNREFUSED') || /ECONNREFUSED/i.test(msg)) return 'connect-refused';
|
|
929
|
+
return 'http-error';
|
|
930
|
+
};
|
|
931
|
+
|
|
932
|
+
const directFirst = await requestCycle1MemoryDirect(deadline, opts, 'pre-channels');
|
|
933
|
+
if (directFirst && directFirst.ok) return directFirst;
|
|
934
|
+
if (!directFirst) {
|
|
935
|
+
const directAfterWait = await waitCycle1MemoryDirect(deadline, opts, Math.min(graceMs, 1800));
|
|
936
|
+
if (directAfterWait && directAfterWait.ok) return directAfterWait;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
const remainingForGrace = deadline - Date.now();
|
|
940
|
+
if (remainingForGrace <= 0) return finish({ ok: false, reason: 'timeout' });
|
|
941
|
+
const active = await pollActiveInstance(Math.min(graceMs, remainingForGrace));
|
|
942
|
+
const tPollEnd = Date.now();
|
|
943
|
+
if (!active) {
|
|
944
|
+
const direct = await requestCycle1MemoryDirect(deadline, opts, 'no-active-instance');
|
|
945
|
+
if (direct && direct.ok) return direct;
|
|
946
|
+
const reason = (Date.now() >= deadline) ? 'timeout' : 'no-active-instance';
|
|
947
|
+
return finish({ ok: false, reason });
|
|
948
|
+
}
|
|
949
|
+
const port = active.httpPort;
|
|
950
|
+
const remaining = deadline - Date.now();
|
|
951
|
+
if (remaining <= 0) return finish({ ok: false, reason: 'timeout' });
|
|
952
|
+
const authHeaders = ownerAuthHeaders(active);
|
|
953
|
+
if (!authHeaders) return finish({ ok: false, reason: 'owner-route-unavailable' });
|
|
954
|
+
|
|
955
|
+
try {
|
|
956
|
+
const tPostStart = Date.now();
|
|
957
|
+
// On-demand cycle1 (SessionStart hook path): min_batch=1 triggers on a
|
|
958
|
+
// single pending row; session_cap=5 × batch_size=20 caps a single hook
|
|
959
|
+
// pass at 100 rows so wallclock stays low. Use modest 2-way fan-out when
|
|
960
|
+
// multiple windows are ready; periodic path also runs 2×50 in parallel.
|
|
961
|
+
const ON_DEMAND_CYCLE1_ARGS = { min_batch: 1, session_cap: 5, batch_size: 20, concurrency: 5 };
|
|
962
|
+
const res = await httpPostJson({
|
|
963
|
+
hostname: '127.0.0.1',
|
|
964
|
+
port,
|
|
965
|
+
path: '/cycle1',
|
|
966
|
+
timeoutMs: remaining,
|
|
967
|
+
ownerActive: active,
|
|
968
|
+
authHeaders,
|
|
969
|
+
body: { timeout_ms: remaining, args: ON_DEMAND_CYCLE1_ARGS },
|
|
970
|
+
});
|
|
971
|
+
teeStderr(`[session-start] cycle1 slot=${slot} timing pollMs=${tPollEnd - start} postMs=${Date.now() - tPostStart}\n`);
|
|
972
|
+
if (res.ownerRouteUnavailable) {
|
|
973
|
+
return finish({ ok: false, reason: 'owner-route-unavailable' });
|
|
974
|
+
}
|
|
975
|
+
if (res.statusCode !== 200) {
|
|
976
|
+
// Surface the channels endpoint's body `reason` (memory-not-ready,
|
|
977
|
+
// worker-unavailable, ipc-error, memory-timeout, ...) so downstream
|
|
978
|
+
// retry logic and operators can see the precise transient class.
|
|
979
|
+
let bodyReason = null;
|
|
980
|
+
try {
|
|
981
|
+
const parsed = JSON.parse(res.body);
|
|
982
|
+
if (parsed && typeof parsed.reason === 'string') bodyReason = parsed.reason;
|
|
983
|
+
} catch {}
|
|
984
|
+
const reason = bodyReason ? `non-200/${bodyReason}` : 'non-200';
|
|
985
|
+
return finish({ ok: false, reason, statusCode: res.statusCode, bodyReason });
|
|
986
|
+
}
|
|
987
|
+
try {
|
|
988
|
+
const parsed = JSON.parse(res.body);
|
|
989
|
+
if (parsed && parsed.ok) {
|
|
990
|
+
const sig = extractCycleSignals(parsed);
|
|
991
|
+
return finish({
|
|
992
|
+
ok: true,
|
|
993
|
+
processed: sig.processed,
|
|
994
|
+
pendingRows: sig.pendingRows,
|
|
995
|
+
skippedInFlight: sig.skippedInFlight,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
return finish({ ok: false, reason: 'body-not-ok', statusCode: 200 });
|
|
999
|
+
} catch {
|
|
1000
|
+
return finish({ ok: false, reason: 'parse-error', statusCode: 200 });
|
|
1001
|
+
}
|
|
1002
|
+
} catch (e) {
|
|
1003
|
+
const reason = classifyError(e);
|
|
1004
|
+
if (reason === 'connect-refused' || reason === 'timeout') {
|
|
1005
|
+
const direct = await requestCycle1MemoryDirect(deadline, opts, reason);
|
|
1006
|
+
if (direct && direct.ok) return direct;
|
|
1007
|
+
}
|
|
1008
|
+
return finish({ ok: false, reason });
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// Public entry point. Single in-flight call — server-main.callWorker now
|
|
1013
|
+
// awaits the worker's first 'ready' IPC, so a pre-ready /cycle1 holds until
|
|
1014
|
+
// memory is up instead of bouncing 503. Keep one follow-up retry for the
|
|
1015
|
+
// processed=0 case: that means either an in-flight dedup hit (server
|
|
1016
|
+
// returned the prior run's empty result) or a pre-ingest race
|
|
1017
|
+
// (transcript-watch had not yet ingested pending raw entries). A short sleep
|
|
1018
|
+
// + second call covers both. If the second pass also returns 0, genuinely
|
|
1019
|
+
// empty.
|
|
1020
|
+
async function requestCycle1(timeoutMs, opts = {}) {
|
|
1021
|
+
const slot = opts.slot || 'unknown';
|
|
1022
|
+
const graceMs = Number.isFinite(opts.graceMs) ? opts.graceMs : 5000;
|
|
1023
|
+
const start = Date.now();
|
|
1024
|
+
const deadline = start + Math.max(0, timeoutMs);
|
|
1025
|
+
teeStderr(`[session-start] cycle1 slot=${slot} start graceMs=${graceMs} timeoutMs=${timeoutMs}\n`);
|
|
1026
|
+
teeStderr(`[boot-time] tag=cycle1-entry slot=${slot} tMs=${start}\n`);
|
|
1027
|
+
|
|
1028
|
+
// Boot-race transient classifier: a fresh session can fire cycle1 while the
|
|
1029
|
+
// new owner's channels worker is still binding 3462 (connect-refused), the
|
|
1030
|
+
// active-instance.json is briefly empty (no-active-instance), or channels is
|
|
1031
|
+
// up but the parent's memory worker hasn't sent its first 'ready' IPC yet
|
|
1032
|
+
// (503 with bodyReason in {memory-not-ready, worker-unavailable, ipc-error,
|
|
1033
|
+
// memory-timeout}). Warm peer boots often clear in 1–3s; cold first-boot
|
|
1034
|
+
// runtime init (PG attach + embedding warmup under multi-instance contention)
|
|
1035
|
+
// can take longer, so retry with backoff up to a wider budget rather than
|
|
1036
|
+
// skipping recap entirely.
|
|
1037
|
+
const TRANSIENT_BOOT_BODY_REASONS = new Set([
|
|
1038
|
+
'memory-not-ready',
|
|
1039
|
+
'worker-unavailable',
|
|
1040
|
+
'ipc-error',
|
|
1041
|
+
'memory-timeout',
|
|
1042
|
+
'backend-not-ready',
|
|
1043
|
+
'beacon-booting',
|
|
1044
|
+
]);
|
|
1045
|
+
// memory-degraded is NOT transient: restart cap exceeded or PG PANIC.
|
|
1046
|
+
// Retrying it within the transient budget (~45s) would only delay the
|
|
1047
|
+
// session with no benefit, so it is excluded before transient classification.
|
|
1048
|
+
const NON_TRANSIENT_BOOT_BODY_REASONS = new Set([
|
|
1049
|
+
'memory-degraded',
|
|
1050
|
+
]);
|
|
1051
|
+
const TRANSIENT_TOP_REASONS = new Set([
|
|
1052
|
+
'connect-refused',
|
|
1053
|
+
'no-active-instance',
|
|
1054
|
+
]);
|
|
1055
|
+
const isTransientBootFailure = (r) => {
|
|
1056
|
+
if (!r || r.ok) return false;
|
|
1057
|
+
if (r.bodyReason && NON_TRANSIENT_BOOT_BODY_REASONS.has(r.bodyReason)) return false;
|
|
1058
|
+
if (r.bodyReason && TRANSIENT_BOOT_BODY_REASONS.has(r.bodyReason)) return true;
|
|
1059
|
+
if (TRANSIENT_TOP_REASONS.has(r.reason)) return true;
|
|
1060
|
+
// 5xx + missing body — boot/restart race: a worker socket closed
|
|
1061
|
+
// mid-request surfaces as a channels /cycle1 500 (bodyReason empty), and
|
|
1062
|
+
// a pre-stub 503 looks the same. Both resolve once the worker finishes
|
|
1063
|
+
// (re)starting, so retry within the boot budget instead of aborting
|
|
1064
|
+
// recap/core. Known degraded states carry a bodyReason and are filtered
|
|
1065
|
+
// by NON_TRANSIENT_BOOT_BODY_REASONS above before reaching here.
|
|
1066
|
+
if (r.statusCode >= 500 && !r.bodyReason) return true;
|
|
1067
|
+
return false;
|
|
1068
|
+
};
|
|
1069
|
+
const TRANSIENT_RETRY_DELAY_MS = 250;
|
|
1070
|
+
const TRANSIENT_RETRY_BUDGET_MS = 45000;
|
|
1071
|
+
const transientDeadline = start + TRANSIENT_RETRY_BUDGET_MS;
|
|
1072
|
+
|
|
1073
|
+
try {
|
|
1074
|
+
let r1 = await requestCycle1Once(deadline, opts);
|
|
1075
|
+
let transientAttempt = 0;
|
|
1076
|
+
while (
|
|
1077
|
+
isTransientBootFailure(r1)
|
|
1078
|
+
&& Date.now() < transientDeadline
|
|
1079
|
+
&& (deadline - Date.now()) > TRANSIENT_RETRY_DELAY_MS + 500
|
|
1080
|
+
) {
|
|
1081
|
+
transientAttempt++;
|
|
1082
|
+
teeStderr(`[session-start] cycle1 slot=${slot} transient-retry attempt=${transientAttempt} bodyReason=${r1.bodyReason || ''} statusCode=${r1.statusCode != null ? r1.statusCode : ''} elapsed=${r1.elapsedMs}ms nextDelay=${TRANSIENT_RETRY_DELAY_MS}ms reason=${r1.reason}\n`);
|
|
1083
|
+
await new Promise((r) => setTimeout(r, TRANSIENT_RETRY_DELAY_MS));
|
|
1084
|
+
r1 = await requestCycle1Once(deadline, { ...opts, slot: `${slot}:t${transientAttempt}` });
|
|
1085
|
+
}
|
|
1086
|
+
if (!r1.ok) return r1;
|
|
1087
|
+
if (r1.processed != null && r1.processed > 0) return r1;
|
|
1088
|
+
// Genuine empty (no pending raw rows AND no in-flight dedup hit) — retry
|
|
1089
|
+
// would do nothing useful, skip the second pass.
|
|
1090
|
+
if (r1.pendingRows === 0 && r1.skippedInFlight === false) return r1;
|
|
1091
|
+
const RETRY_DELAY_MS = 800;
|
|
1092
|
+
const remaining = deadline - Date.now();
|
|
1093
|
+
if (remaining <= RETRY_DELAY_MS + 200) return r1;
|
|
1094
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
1095
|
+
const r2 = await requestCycle1Once(deadline, { ...opts, slot: `${slot}:r` });
|
|
1096
|
+
if (!r2.ok) {
|
|
1097
|
+
teeStderr(`[session-start] cycle1 slot=${slot} r2-failed stale-fallback r1.pendingRows=${r1.pendingRows != null ? r1.pendingRows : '?'} r1.skippedInFlight=${r1.skippedInFlight != null ? r1.skippedInFlight : '?'}\n`);
|
|
1098
|
+
}
|
|
1099
|
+
return r2.ok ? r2 : r1;
|
|
1100
|
+
} catch (e) {
|
|
1101
|
+
teeStderr(`[session-start] cycle1 slot=${slot} exception=${(e && e.message) || e}\n`);
|
|
1102
|
+
return { ok: false, reason: 'exception', elapsedMs: Date.now() - start };
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Best-effort POST /recap/reset to the channels owner. Used on `/clear` so
|
|
1107
|
+
// the forked status server's recapState (which lives in a child process the
|
|
1108
|
+
// hook can't reach via IPC) drops the prior session's badge. Bounded by
|
|
1109
|
+
// graceMs and silent on failure — recap reset is cosmetic, never block
|
|
1110
|
+
// SessionStart on it.
|
|
1111
|
+
async function requestRecapReset(graceMs) {
|
|
1112
|
+
try {
|
|
1113
|
+
const active = await pollActiveInstance(Math.max(0, graceMs));
|
|
1114
|
+
if (!active || !active.httpPort) {
|
|
1115
|
+
teeStderr('[session-start] recap-reset skipped: no active instance\n');
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const authHeaders = ownerAuthHeaders(active);
|
|
1119
|
+
if (!authHeaders) {
|
|
1120
|
+
teeStderr('[session-start] recap-reset skipped: owner route unavailable in this session\n');
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
const res = await httpPostJson({
|
|
1124
|
+
hostname: '127.0.0.1',
|
|
1125
|
+
port: active.httpPort,
|
|
1126
|
+
path: '/recap/reset',
|
|
1127
|
+
timeoutMs: 2000,
|
|
1128
|
+
ownerActive: active,
|
|
1129
|
+
authHeaders,
|
|
1130
|
+
body: {},
|
|
1131
|
+
});
|
|
1132
|
+
if (res.statusCode !== 200) {
|
|
1133
|
+
teeStderr(`[session-start] recap-reset non-200 status=${res.statusCode}\n`);
|
|
1134
|
+
}
|
|
1135
|
+
} catch (e) {
|
|
1136
|
+
teeStderr(`[session-start] recap-reset failed: ${(e && e.message) || e}\n`);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async function runRulesPart() {
|
|
1141
|
+
teeStderr(`[session-start] runRulesPart enter PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()}\n`);
|
|
1142
|
+
// First-boot one-shot work — only slot 1 (rules) runs this. Other slots
|
|
1143
|
+
// skip it entirely so they stay read-only and side-effect free.
|
|
1144
|
+
try {
|
|
1145
|
+
const flagPath = path.join(DATA_DIR, '.first-boot-seen');
|
|
1146
|
+
if (!fs.existsSync(flagPath)) {
|
|
1147
|
+
// No first-boot config-window open here: it double-booted the
|
|
1148
|
+
// setup-server against the unconditional every-session `--prewarm`
|
|
1149
|
+
// below (both racing to bind the port). The server is warmed by the
|
|
1150
|
+
// prewarm; the window opens only on an explicit `/mixdog:config`.
|
|
1151
|
+
fs.writeFileSync(flagPath, '');
|
|
1152
|
+
}
|
|
1153
|
+
} catch {}
|
|
1154
|
+
|
|
1155
|
+
// Every-session pre-warm: boot the config-UI setup-server in the
|
|
1156
|
+
// background (window-free) so the first `/mixdog:config` open is instant.
|
|
1157
|
+
// Fire-and-forget + hidden; `--prewarm` makes launch-core boot the server
|
|
1158
|
+
// only when it is not already alive and never opens a browser window, so
|
|
1159
|
+
// this is a cheap no-op once the server is running.
|
|
1160
|
+
try {
|
|
1161
|
+
spawn('bun', [path.join(PLUGIN_ROOT, 'setup', 'launch.mjs'), '--prewarm'], {
|
|
1162
|
+
detached: true,
|
|
1163
|
+
stdio: 'ignore',
|
|
1164
|
+
windowsHide: true,
|
|
1165
|
+
}).unref();
|
|
1166
|
+
} catch {}
|
|
1167
|
+
|
|
1168
|
+
try {
|
|
1169
|
+
const asp = path.join(DATA_DIR, 'active-session.txt');
|
|
1170
|
+
if (fs.existsSync(asp)) fs.unlinkSync(asp);
|
|
1171
|
+
} catch {}
|
|
1172
|
+
|
|
1173
|
+
// Persist user cwd so the MCP server (spawned from cache dir) can resolve
|
|
1174
|
+
// the correct sandbox root. Atomic rename prevents partial reads.
|
|
1175
|
+
try {
|
|
1176
|
+
const eventCwd = typeof _event.cwd === 'string' ? _event.cwd.trim() : '';
|
|
1177
|
+
if (eventCwd) {
|
|
1178
|
+
const cwdTxtPath = path.join(DATA_DIR, 'user-cwd.txt');
|
|
1179
|
+
const cwdTmpPath = cwdTxtPath + '.tmp';
|
|
1180
|
+
fs.writeFileSync(cwdTmpPath, eventCwd);
|
|
1181
|
+
fs.renameSync(cwdTmpPath, cwdTxtPath);
|
|
1182
|
+
}
|
|
1183
|
+
} catch {}
|
|
1184
|
+
|
|
1185
|
+
try {
|
|
1186
|
+
const stalePending = path.join(DATA_DIR, 'recap-pending.json');
|
|
1187
|
+
if (fs.existsSync(stalePending)) fs.unlinkSync(stalePending);
|
|
1188
|
+
} catch {}
|
|
1189
|
+
|
|
1190
|
+
injectStatusLine(PLUGIN_ROOT);
|
|
1191
|
+
rebindActiveInstance();
|
|
1192
|
+
|
|
1193
|
+
let _channelsConfig = {};
|
|
1194
|
+
try { _channelsConfig = readSection('channels'); } catch {}
|
|
1195
|
+
const injection = _channelsConfig && typeof _channelsConfig.promptInjection === 'object' ? _channelsConfig.promptInjection : {};
|
|
1196
|
+
const claudeMdMode = injection.mode === 'claude_md';
|
|
1197
|
+
const claudeMdTargetPath = typeof injection.targetPath === 'string' && injection.targetPath
|
|
1198
|
+
? injection.targetPath
|
|
1199
|
+
: '~/.claude/CLAUDE.md';
|
|
1200
|
+
const needsBootstrapInjection = claudeMdMode && !hasManagedClaudeMdBlock(claudeMdTargetPath);
|
|
1201
|
+
|
|
1202
|
+
let additionalContext = '';
|
|
1203
|
+
if (!claudeMdMode || needsBootstrapInjection) {
|
|
1204
|
+
try {
|
|
1205
|
+
const { buildInjectionContent } = require(path.join(PLUGIN_ROOT, 'lib', 'rules-builder.cjs'));
|
|
1206
|
+
additionalContext = buildInjectionContent({ PLUGIN_ROOT, DATA_DIR }) || '';
|
|
1207
|
+
} catch {}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
// claude_md mode + missing managed block: persist the file alongside emit so the
|
|
1211
|
+
// next session boots from the managed block instead of the inline fallback.
|
|
1212
|
+
if (claudeMdMode && needsBootstrapInjection && additionalContext) {
|
|
1213
|
+
try {
|
|
1214
|
+
const { upsertManagedBlock } = require(path.join(PLUGIN_ROOT, 'lib', 'claude-md-writer.cjs'));
|
|
1215
|
+
upsertManagedBlock(claudeMdTargetPath, additionalContext);
|
|
1216
|
+
teeStderr(`[session-start] claude_md: regenerated ${claudeMdTargetPath} after missing managed block\n`);
|
|
1217
|
+
} catch (e) {
|
|
1218
|
+
teeStderr(`[session-start] claude_md regenerate failed: ${e && e.message || e}\n`);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// On `/clear`, drop the prior session's recap badge from the forked status
|
|
1223
|
+
// server. Hook runs in a separate cjs process with no IPC handle to that
|
|
1224
|
+
// child, so we POST /recap/reset to the channels owner instead. Best
|
|
1225
|
+
// effort, short grace — channels owner is usually already up on /clear.
|
|
1226
|
+
if (_event.source === 'clear') {
|
|
1227
|
+
// Fire-and-forget — recap reset is cosmetic, never block response path.
|
|
1228
|
+
requestRecapReset(3000).catch(() => {});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// Surface the session-entry working directory so the Lead knows where
|
|
1232
|
+
// relative paths resolve before any `cwd set`. Reads the user-cwd.txt seed
|
|
1233
|
+
// written just above (mirrors the host's _event.cwd). Best-effort — never
|
|
1234
|
+
// let it break rule injection.
|
|
1235
|
+
let _startDir = '';
|
|
1236
|
+
try {
|
|
1237
|
+
_startDir = fs.readFileSync(path.join(DATA_DIR, 'user-cwd.txt'), 'utf8').trim();
|
|
1238
|
+
if (_startDir) {
|
|
1239
|
+
const _startBlock = `## Starting directory\n${_startDir}`;
|
|
1240
|
+
additionalContext = additionalContext ? `${additionalContext}\n\n${_startBlock}` : _startBlock;
|
|
1241
|
+
}
|
|
1242
|
+
} catch {}
|
|
1243
|
+
|
|
1244
|
+
// Other owned directories — full paths (the cwd tool's background scan
|
|
1245
|
+
// persists them to cwd-projects.json). Best-effort — never let it break
|
|
1246
|
+
// rule injection. Dynamic, so it lives here not in static rules.
|
|
1247
|
+
try {
|
|
1248
|
+
const _projParsed = JSON.parse(fs.readFileSync(path.join(DATA_DIR, 'cwd-projects.json'), 'utf8'));
|
|
1249
|
+
const _projects = Array.isArray(_projParsed && _projParsed.projects) ? _projParsed.projects : [];
|
|
1250
|
+
const _paths = _projects
|
|
1251
|
+
.map((p) => String(p.path || '').trim())
|
|
1252
|
+
.filter((p) => p && p !== _startDir);
|
|
1253
|
+
if (_paths.length) {
|
|
1254
|
+
const _otherBlock = `## Other directories\n${_paths.map((p) => `- ${p}`).join('\n')}`;
|
|
1255
|
+
additionalContext = additionalContext ? `${additionalContext}\n\n${_otherBlock}` : _otherBlock;
|
|
1256
|
+
}
|
|
1257
|
+
} catch {}
|
|
1258
|
+
|
|
1259
|
+
emit(additionalContext);
|
|
1260
|
+
teeStderr(`[session-start] runRulesPart done\n`);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// ---------------------------------------------------------------------------
|
|
1264
|
+
// Part: core (slot 2) — Core Memory only. Runs in its own process so each
|
|
1265
|
+
// SessionStart additionalContext is sized independently against the host
|
|
1266
|
+
// preview cap. Pairs with recap (slot 3); both are spawned in parallel by
|
|
1267
|
+
// the host and share the cycle1 await on the server side.
|
|
1268
|
+
// ---------------------------------------------------------------------------
|
|
1269
|
+
async function runCorePart() {
|
|
1270
|
+
if (_skipMemoryInject()) {
|
|
1271
|
+
teeStderr(`[session-start] runCorePart skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=skipMemoryInject\n`);
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
// graceMs=8000 covers supervisor cold-start ceiling. fs.watch unblocks
|
|
1275
|
+
// immediately on active-instance.json creation, so normal boots pay only
|
|
1276
|
+
// the actual startup time (no extra wait). Without this, first attempt
|
|
1277
|
+
// timed out at 3000ms before supervisor finished spawning, forcing a
|
|
1278
|
+
// transient-retry round-trip that added ~3s to wall-clock.
|
|
1279
|
+
const r = await requestCycle1(SESSION_START_CYCLE1_TIMEOUT_MS, { graceMs: 8000, slot: 'core' });
|
|
1280
|
+
if (r.ok !== true) {
|
|
1281
|
+
if (r.reason === 'owner-route-unavailable') {
|
|
1282
|
+
teeStderr('[session-start] core cycle1 skipped: owner route unavailable in this session\n');
|
|
1283
|
+
} else {
|
|
1284
|
+
teeStderr(`[session-start] core aborted: cycle1 await failed reason=${r.reason}\n`);
|
|
1285
|
+
return;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
// cycle1 ok guarantees channels owner liveness only; memory_port lands
|
|
1289
|
+
// later via worker `ready` IPC. Wait so getMemoryServicePort below does
|
|
1290
|
+
// not throw on fast /clear paths.
|
|
1291
|
+
const memPort = await awaitMemoryPort(8000);
|
|
1292
|
+
if (!memPort) {
|
|
1293
|
+
teeStderr(`[session-start] core aborted: memory_port unavailable within graceMs\n`);
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
const tStart = Date.now();
|
|
1297
|
+
try {
|
|
1298
|
+
const tCtxStart = Date.now();
|
|
1299
|
+
const ctx = await buildContext(_event.cwd || process.cwd());
|
|
1300
|
+
teeStderr(`[session-start] core stage=buildContext elapsed=${Date.now() - tCtxStart}ms hasCtx=${!!ctx}\n`);
|
|
1301
|
+
if (ctx) {
|
|
1302
|
+
const tEmitStart = Date.now();
|
|
1303
|
+
emit(ctx);
|
|
1304
|
+
teeStderr(`[session-start] core stage=emit elapsed=${Date.now() - tEmitStart}ms\n`);
|
|
1305
|
+
}
|
|
1306
|
+
} catch (e) {
|
|
1307
|
+
process.stderr.write(`[session-start] core build failed: ${e.message}\n`);
|
|
1308
|
+
}
|
|
1309
|
+
teeStderr(`[session-start] core done totalElapsed=${Date.now() - tStart}ms\n`);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
// ---------------------------------------------------------------------------
|
|
1313
|
+
// Part: recap (slot 3) — Recap entries only. Spawned in parallel with core.
|
|
1314
|
+
// ---------------------------------------------------------------------------
|
|
1315
|
+
async function runRecapPart() {
|
|
1316
|
+
if (_skipMemoryInject()) {
|
|
1317
|
+
teeStderr(`[session-start] runRecapPart skip PART=${PART} source=${_event.source || ''} cwd=${_event.cwd || process.cwd()} reason=skipMemoryInject\n`);
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
// graceMs=8000: see runCorePart for invariant rationale.
|
|
1321
|
+
const r = await requestCycle1(SESSION_START_CYCLE1_TIMEOUT_MS, { graceMs: 8000, slot: 'recap' });
|
|
1322
|
+
if (r.ok !== true) {
|
|
1323
|
+
if (r.reason === 'owner-route-unavailable') {
|
|
1324
|
+
teeStderr('[session-start] recap cycle1 skipped: owner route unavailable in this session\n');
|
|
1325
|
+
} else {
|
|
1326
|
+
teeStderr(`[session-start] recap aborted: cycle1 await failed reason=${r.reason}\n`);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
// See runCorePart: cycle1 success does not imply memory_port readiness.
|
|
1331
|
+
const memPort = await awaitMemoryPort(8000);
|
|
1332
|
+
if (!memPort) {
|
|
1333
|
+
teeStderr(`[session-start] recap aborted: memory_port unavailable within graceMs\n`);
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
const tStart = Date.now();
|
|
1337
|
+
try {
|
|
1338
|
+
const tRecapStart = Date.now();
|
|
1339
|
+
const recapData = await buildRecapData(_event.cwd || process.cwd());
|
|
1340
|
+
const lines = (recapData && recapData.lines) || [];
|
|
1341
|
+
teeStderr(`[session-start] recap stage=buildRecapData elapsed=${Date.now() - tRecapStart}ms lines=${lines.length}\n`);
|
|
1342
|
+
if (lines.length > 0) {
|
|
1343
|
+
const tEmitStart = Date.now();
|
|
1344
|
+
emit(`## Recap\n${lines.join('\n')}`);
|
|
1345
|
+
teeStderr(`[session-start] recap stage=emit elapsed=${Date.now() - tEmitStart}ms\n`);
|
|
1346
|
+
}
|
|
1347
|
+
} catch (e) {
|
|
1348
|
+
process.stderr.write(`[session-start] recap build failed: ${e.message}\n`);
|
|
1349
|
+
}
|
|
1350
|
+
teeStderr(`[session-start] recap done totalElapsed=${Date.now() - tStart}ms\n`);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// ---------------------------------------------------------------------------
|
|
1354
|
+
// Exports for in-process daemon use (mixdog-shim → hook-pipe-server.mjs).
|
|
1355
|
+
// ---------------------------------------------------------------------------
|
|
1356
|
+
module.exports = { runRulesPart, runCorePart, runRecapPart, setEvent, setEmitSink, setPart };
|
|
1357
|
+
|
|
1358
|
+
// ---------------------------------------------------------------------------
|
|
1359
|
+
// Main IIFE — dispatch on PART. Only runs when invoked as the entry script;
|
|
1360
|
+
// require'd from the daemon stays a no-op (PART is undefined).
|
|
1361
|
+
// ---------------------------------------------------------------------------
|
|
1362
|
+
if (require.main === module) {
|
|
1363
|
+
(async () => {
|
|
1364
|
+
if (PART === 'rules') {
|
|
1365
|
+
await runRulesPart();
|
|
1366
|
+
} else if (PART === 'core') {
|
|
1367
|
+
await runCorePart();
|
|
1368
|
+
} else if (PART === 'recap') {
|
|
1369
|
+
await runRecapPart();
|
|
1370
|
+
}
|
|
1371
|
+
})();
|
|
1372
|
+
}
|