create-walle 0.9.22 → 0.9.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -0
- package/package.json +1 -1
- package/template/CLAUDE.md +2 -2
- package/template/LICENSE +1 -1
- package/template/bin/ctm-dev-cleanup.js +24 -3
- package/template/bin/ctm-launch.sh +13 -0
- package/template/bin/dev.sh +156 -18
- package/template/bin/node-bin.sh +84 -0
- package/template/bin/pin-node.sh +51 -0
- package/template/claude-task-manager/api-prompts.js +1190 -182
- package/template/claude-task-manager/api-reviews.js +104 -13
- package/template/claude-task-manager/approval-agent.js +1360 -280
- package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
- package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
- package/template/claude-task-manager/db.js +4071 -282
- package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
- package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
- package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
- package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
- package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
- package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
- package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
- package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
- package/template/claude-task-manager/docs/image-paste-ux.md +10 -0
- package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
- package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
- package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
- package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
- package/template/claude-task-manager/docs/phone-access-design.md +53 -15
- package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
- package/template/claude-task-manager/docs/phone-setup.md +3 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
- package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
- package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
- package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
- package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
- package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
- package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
- package/template/claude-task-manager/docs/session-title-authority.md +32 -0
- package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
- package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
- package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
- package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
- package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
- package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
- package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
- package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
- package/template/claude-task-manager/git-utils.js +751 -10
- package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
- package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
- package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
- package/template/claude-task-manager/lib/agent-presets.js +17 -1
- package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
- package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
- package/template/claude-task-manager/lib/async-semaphore.js +44 -0
- package/template/claude-task-manager/lib/auth-context.js +5 -0
- package/template/claude-task-manager/lib/auth-rate-limit.js +24 -1
- package/template/claude-task-manager/lib/auth-rules.js +26 -2
- package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
- package/template/claude-task-manager/lib/background-llm.js +144 -17
- package/template/claude-task-manager/lib/branch-inventory.js +212 -0
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
- package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
- package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
- package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
- package/template/claude-task-manager/lib/codex-zst.js +124 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
- package/template/claude-task-manager/lib/connection-health.js +232 -0
- package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
- package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
- package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
- package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
- package/template/claude-task-manager/lib/document-review.js +109 -5
- package/template/claude-task-manager/lib/escalation-review.js +152 -0
- package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
- package/template/claude-task-manager/lib/headless-term-service.js +678 -0
- package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
- package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
- package/template/claude-task-manager/lib/main-db-census.js +216 -0
- package/template/claude-task-manager/lib/message-pagination.js +106 -4
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +669 -28
- package/template/claude-task-manager/lib/mobile-auth-api.js +260 -7
- package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
- package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
- package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
- package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
- package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
- package/template/claude-task-manager/lib/perf-tracker.js +242 -6
- package/template/claude-task-manager/lib/permission-match.js +76 -0
- package/template/claude-task-manager/lib/permission-sync.js +133 -20
- package/template/claude-task-manager/lib/process-title.js +35 -0
- package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
- package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
- package/template/claude-task-manager/lib/prompt-intent.js +132 -0
- package/template/claude-task-manager/lib/provider-user-context.js +34 -0
- package/template/claude-task-manager/lib/read-pool-client.js +313 -0
- package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
- package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
- package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
- package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
- package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
- package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
- package/template/claude-task-manager/lib/restart-guard.js +46 -5
- package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
- package/template/claude-task-manager/lib/restore-policy.js +13 -0
- package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
- package/template/claude-task-manager/lib/restore-runtime.js +68 -0
- package/template/claude-task-manager/lib/restore-storm.js +34 -0
- package/template/claude-task-manager/lib/resume-cwd.js +36 -0
- package/template/claude-task-manager/lib/resume-preflight.js +313 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
- package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
- package/template/claude-task-manager/lib/scheduler.js +21 -1
- package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
- package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
- package/template/claude-task-manager/lib/server-listeners.js +239 -0
- package/template/claude-task-manager/lib/session-capture.js +42 -7
- package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
- package/template/claude-task-manager/lib/session-history.js +388 -43
- package/template/claude-task-manager/lib/session-host-manager.js +287 -0
- package/template/claude-task-manager/lib/session-image-refs.js +209 -0
- package/template/claude-task-manager/lib/session-jobs.js +399 -59
- package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
- package/template/claude-task-manager/lib/session-restore.js +53 -0
- package/template/claude-task-manager/lib/session-standup.js +87 -10
- package/template/claude-task-manager/lib/session-state-bus.js +14 -0
- package/template/claude-task-manager/lib/session-stream.js +53 -12
- package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
- package/template/claude-task-manager/lib/session-token-usage.js +494 -0
- package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
- package/template/claude-task-manager/lib/setup-network-config.js +9 -0
- package/template/claude-task-manager/lib/size-cap.js +45 -0
- package/template/claude-task-manager/lib/size-cap.test.js +62 -0
- package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
- package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
- package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
- package/template/claude-task-manager/lib/standup-attention.js +7 -3
- package/template/claude-task-manager/lib/status-authority.js +39 -0
- package/template/claude-task-manager/lib/status-hooks.js +4 -0
- package/template/claude-task-manager/lib/storage-migration.js +235 -0
- package/template/claude-task-manager/lib/structured-capture.js +298 -0
- package/template/claude-task-manager/lib/sync-io-census.js +163 -0
- package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
- package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
- package/template/claude-task-manager/lib/terminal-choice.js +364 -0
- package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
- package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
- package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
- package/template/claude-task-manager/lib/timeline-order.js +122 -0
- package/template/claude-task-manager/lib/transcript-store.js +348 -43
- package/template/claude-task-manager/lib/transport-security.js +34 -1
- package/template/claude-task-manager/lib/wait-state.js +184 -0
- package/template/claude-task-manager/lib/walle-client.js +47 -5
- package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
- package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
- package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
- package/template/claude-task-manager/lib/walle-native-health.js +403 -0
- package/template/claude-task-manager/lib/walle-repair.js +701 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
- package/template/claude-task-manager/lib/walle-session-context.js +57 -21
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
- package/template/claude-task-manager/lib/walle-transcript.js +36 -0
- package/template/claude-task-manager/lib/worktree-active-sync.js +5 -4
- package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
- package/template/claude-task-manager/package.json +1 -1
- package/template/claude-task-manager/prompt-harvest.js +89 -66
- package/template/claude-task-manager/providers/claude-code.js +51 -3
- package/template/claude-task-manager/providers/cursor.js +140 -45
- package/template/claude-task-manager/public/css/reviews.css +541 -61
- package/template/claude-task-manager/public/css/setup.css +178 -0
- package/template/claude-task-manager/public/css/walle-session.css +865 -10
- package/template/claude-task-manager/public/css/walle.css +9 -0
- package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
- package/template/claude-task-manager/public/index.html +18043 -2080
- package/template/claude-task-manager/public/js/document-review-links.js +106 -1
- package/template/claude-task-manager/public/js/image-normalize.js +69 -36
- package/template/claude-task-manager/public/js/message-renderer.js +1252 -75
- package/template/claude-task-manager/public/js/prompts.js +66 -29
- package/template/claude-task-manager/public/js/reviews.js +871 -127
- package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
- package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
- package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
- package/template/claude-task-manager/public/js/setup.js +1238 -181
- package/template/claude-task-manager/public/js/stream-view.js +671 -72
- package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
- package/template/claude-task-manager/public/js/walle-session.js +2455 -158
- package/template/claude-task-manager/public/js/walle.js +141 -10
- package/template/claude-task-manager/public/m/app.css +2033 -164
- package/template/claude-task-manager/public/m/app.js +5633 -433
- package/template/claude-task-manager/public/m/claim.html +219 -19
- package/template/claude-task-manager/public/m/index.html +105 -16
- package/template/claude-task-manager/public/m/sw.js +3 -1
- package/template/claude-task-manager/public/manifest.json +2 -2
- package/template/claude-task-manager/public/prompts.html +30 -14
- package/template/claude-task-manager/queue-engine.js +507 -28
- package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
- package/template/claude-task-manager/server.js +13981 -2107
- package/template/claude-task-manager/session-integrity.js +156 -18
- package/template/claude-task-manager/session-search-ranking.js +1 -0
- package/template/claude-task-manager/session-utils.js +25 -5
- package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
- package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
- package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
- package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
- package/template/claude-task-manager/workers/harvest-worker.js +9 -55
- package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
- package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
- package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
- package/template/claude-task-manager/workers/session-host-process.js +146 -0
- package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
- package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
- package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
- package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
- package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
- package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
- package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
- package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
- package/template/docs/design/markdown-review-pane.md +206 -0
- package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +54 -14
- package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
- package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
- package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
- package/template/docs/private-memory-and-pii-policy.md +69 -0
- package/template/package.json +2 -1
- package/template/scripts/check-private-data.js +201 -0
- package/template/shared/sqlite-owner-guard.js +30 -0
- package/template/shared/sqlite-owner-write-queue.js +225 -0
- package/template/shared/sqlite-storage-policy.js +111 -0
- package/template/shared/sqlite-write-lock.js +428 -0
- package/template/wall-e/agent-runners/claude-code.js +5 -0
- package/template/wall-e/agent.js +166 -22
- package/template/wall-e/api-walle.js +505 -69
- package/template/wall-e/auth/provider-flows.js +11 -1
- package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
- package/template/wall-e/brain.js +1463 -136
- package/template/wall-e/chat/attachment-blocks.js +96 -0
- package/template/wall-e/chat/attachments.js +2 -1
- package/template/wall-e/chat/capability-resolver.js +7 -7
- package/template/wall-e/chat/context-messages.js +28 -0
- package/template/wall-e/chat/conversation-frame.js +630 -0
- package/template/wall-e/chat/provider-messages.js +125 -0
- package/template/wall-e/chat.js +926 -242
- package/template/wall-e/coding/acceptance-contract.js +170 -0
- package/template/wall-e/coding/acp-adapter.js +1 -1
- package/template/wall-e/coding/agent-catalog.js +3 -0
- package/template/wall-e/coding/artifact-store.js +93 -0
- package/template/wall-e/coding/capability-router.js +120 -0
- package/template/wall-e/coding/coding-run-controller.js +423 -0
- package/template/wall-e/coding/compaction-service.js +157 -12
- package/template/wall-e/coding/frontend-verification.js +258 -0
- package/template/wall-e/coding/lifecycle-hooks.js +75 -0
- package/template/wall-e/coding/local-preview-contract.js +157 -0
- package/template/wall-e/coding/permission-service.js +57 -13
- package/template/wall-e/coding/prompt-bundle.js +19 -1
- package/template/wall-e/coding/prompt-section-registry.js +227 -0
- package/template/wall-e/coding/provider-compat.js +15 -0
- package/template/wall-e/coding/runtime-events.js +224 -0
- package/template/wall-e/coding/runtime-mode.js +3 -0
- package/template/wall-e/coding/side-git-snapshot.js +160 -4
- package/template/wall-e/coding/snapshot-service.js +143 -1
- package/template/wall-e/coding/stream-processor.js +388 -34
- package/template/wall-e/coding/task-tool.js +141 -4
- package/template/wall-e/coding/tool-execution-controller.js +365 -0
- package/template/wall-e/coding/tool-registry.js +43 -5
- package/template/wall-e/coding/user-hooks.js +217 -0
- package/template/wall-e/coding-orchestrator.js +1224 -209
- package/template/wall-e/coding-prompts.js +20 -4
- package/template/wall-e/context/context-builder.js +15 -2
- package/template/wall-e/decision/confidence.js +1 -1
- package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
- package/template/wall-e/docs/external-action-controller.md +26 -6
- package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
- package/template/wall-e/embeddings.js +591 -53
- package/template/wall-e/external-action-controller.js +12 -0
- package/template/wall-e/http/auth.js +1 -0
- package/template/wall-e/http/chat-api.js +46 -11
- package/template/wall-e/http/model-admin.js +727 -56
- package/template/wall-e/lib/boot-profile.js +88 -0
- package/template/wall-e/lib/event-loop-monitor.js +93 -0
- package/template/wall-e/llm/anthropic.js +123 -5
- package/template/wall-e/llm/client.js +236 -67
- package/template/wall-e/llm/default-fallback.js +382 -0
- package/template/wall-e/llm/health.js +19 -0
- package/template/wall-e/llm/message-guard.js +78 -0
- package/template/wall-e/llm/model-catalog.js +252 -1
- package/template/wall-e/llm/openai.js +10 -3
- package/template/wall-e/llm/portkey-sync.js +489 -36
- package/template/wall-e/llm/provider-error.js +30 -2
- package/template/wall-e/llm/registry.js +5 -1
- package/template/wall-e/llm/request-compat.js +67 -0
- package/template/wall-e/loops/backfill.js +79 -23
- package/template/wall-e/loops/brain-optimize.js +67 -0
- package/template/wall-e/loops/ingest.js +25 -10
- package/template/wall-e/loops/question-digest.js +160 -0
- package/template/wall-e/loops/reflect.js +6 -4
- package/template/wall-e/loops/think.js +39 -12
- package/template/wall-e/mcp-server.js +318 -36
- package/template/wall-e/memory/ctm-context-client.js +52 -14
- package/template/wall-e/memory/ctm-operational-context.js +237 -0
- package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
- package/template/wall-e/memory/ctm-session-context.js +111 -63
- package/template/wall-e/prompts/coding/deepseek.txt +3 -0
- package/template/wall-e/prompts/coding/gemini.txt +6 -0
- package/template/wall-e/prompts/coding/gpt.txt +6 -0
- package/template/wall-e/prompts/coding/local.txt +7 -0
- package/template/wall-e/runtime/decision-hooks.js +115 -0
- package/template/wall-e/runtime/devbox-gateway.js +82 -8
- package/template/wall-e/runtime/prompt-manifest.js +86 -0
- package/template/wall-e/runtime/tool-executor.js +269 -0
- package/template/wall-e/runtime/tool-result-envelope.js +138 -0
- package/template/wall-e/runtime/transcript-projection.js +60 -0
- package/template/wall-e/runtime/walle-runtime.js +224 -0
- package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
- package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
- package/template/wall-e/server.js +2 -0
- package/template/wall-e/session-files.js +9 -0
- package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
- package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
- package/template/wall-e/skills/claude-code-reader.js +7 -3
- package/template/wall-e/skills/script-skill-runner.js +10 -0
- package/template/wall-e/skills/skill-planner.js +38 -0
- package/template/wall-e/tools/builtin-middleware.js +19 -9
- package/template/wall-e/tools/local-tools.js +1428 -16
- package/template/wall-e/tools/permission-checker.js +73 -5
- package/template/wall-e/tools/question-manager.js +117 -7
- package/template/wall-e/training/harvester.js +12 -28
- package/template/wall-e/training/replay.js +25 -80
- package/template/wall-e/eval/ab-test.js +0 -203
- package/template/wall-e/eval/agent-runner.js +0 -772
- package/template/wall-e/eval/agent-scorer.js +0 -461
- package/template/wall-e/eval/aggregator.js +0 -414
- package/template/wall-e/eval/allowed-test-commands.js +0 -34
- package/template/wall-e/eval/benchmark-generator.js +0 -113
- package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
- package/template/wall-e/eval/benchmarks/chat.json +0 -82
- package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
- package/template/wall-e/eval/benchmarks/coding.json +0 -122
- package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
- package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
- package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
- package/template/wall-e/eval/benchmarks.js +0 -669
- package/template/wall-e/eval/cc-replay.js +0 -719
- package/template/wall-e/eval/chat-eval.js +0 -525
- package/template/wall-e/eval/check-keys.js +0 -15
- package/template/wall-e/eval/check-providers.js +0 -42
- package/template/wall-e/eval/codex-cli-baseline.js +0 -669
- package/template/wall-e/eval/coding-agent-real.js +0 -570
- package/template/wall-e/eval/context-compactor.js +0 -251
- package/template/wall-e/eval/debug-agent003.js +0 -68
- package/template/wall-e/eval/diagnostics.js +0 -216
- package/template/wall-e/eval/eval-orchestrator.js +0 -642
- package/template/wall-e/eval/evaluate.js +0 -202
- package/template/wall-e/eval/evaluator.js +0 -373
- package/template/wall-e/eval/exporter.js +0 -212
- package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
- package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
- package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
- package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
- package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
- package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
- package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
- package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
- package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
- package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
- package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
- package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
- package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
- package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
- package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
- package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
- package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
- package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
- package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
- package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
- package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
- package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
- package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
- package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
- package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
- package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
- package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
- package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
- package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
- package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
- package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
- package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
- package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
- package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
- package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
- package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
- package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
- package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
- package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
- package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
- package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
- package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
- package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
- package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
- package/template/wall-e/eval/harvester.js +0 -685
- package/template/wall-e/eval/head-to-head.js +0 -388
- package/template/wall-e/eval/humaneval-adapter.js +0 -321
- package/template/wall-e/eval/list-models.js +0 -31
- package/template/wall-e/eval/livecodebench-adapter.js +0 -291
- package/template/wall-e/eval/mail-integration.js +0 -443
- package/template/wall-e/eval/manifest.js +0 -186
- package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
- package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
- package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
- package/template/wall-e/eval/meta-harness/cli.js +0 -86
- package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
- package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
- package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
- package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
- package/template/wall-e/eval/meta-harness/frontier.js +0 -96
- package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
- package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
- package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
- package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
- package/template/wall-e/eval/meta-harness/reporting.js +0 -58
- package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
- package/template/wall-e/eval/meta-harness/validation.js +0 -81
- package/template/wall-e/eval/promoter.js +0 -228
- package/template/wall-e/eval/provider-normalizer.js +0 -33
- package/template/wall-e/eval/replay.js +0 -395
- package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
- package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
- package/template/wall-e/eval/run-coding-agent-real.js +0 -187
- package/template/wall-e/eval/run-eval.js +0 -435
- package/template/wall-e/eval/run-model-comparison.js +0 -142
- package/template/wall-e/eval/session-evaluator.js +0 -187
- package/template/wall-e/eval/session-miner.js +0 -207
- package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
- package/template/wall-e/eval/session-transcripts.js +0 -509
- package/template/wall-e/eval/shadow.js +0 -161
- package/template/wall-e/eval/swebench-adapter.js +0 -345
- package/template/wall-e/eval/swebench-docker.js +0 -192
- package/template/wall-e/eval/train.py +0 -320
- package/template/wall-e/eval/trainer.js +0 -232
- package/template/wall-e/eval/weekly-eval-loop.js +0 -241
|
@@ -5,28 +5,91 @@ const os = require('os');
|
|
|
5
5
|
const db = require('./db');
|
|
6
6
|
const queueEngine = require('./queue-engine');
|
|
7
7
|
const harvest = require('./prompt-harvest');
|
|
8
|
-
const
|
|
8
|
+
const approvalAgent = require('./approval-agent');
|
|
9
|
+
const escalationReview = require('./lib/escalation-review');
|
|
10
|
+
// permission-sync (Claude/Codex settings mirroring) intentionally removed —
|
|
11
|
+
// CTM permissions are a standalone global config in the CTM DB.
|
|
9
12
|
const walleClient = require('./lib/walle-client');
|
|
10
13
|
const claudeDesktopSessions = require('./lib/claude-desktop-sessions');
|
|
11
14
|
const skillAutocomplete = require('./lib/skill-autocomplete');
|
|
15
|
+
const skillIntentResolver = require('./lib/skill-intent-resolver');
|
|
12
16
|
const resourceLinks = require('./lib/resource-links');
|
|
17
|
+
const storageMigration = require('./lib/storage-migration');
|
|
18
|
+
const { runSync } = require('./lib/perf-tracker');
|
|
13
19
|
const {
|
|
14
20
|
ingestJsonlFile,
|
|
15
21
|
normalizeProvider: normalizeTranscriptProvider,
|
|
16
22
|
sourceIdFromPath: transcriptSourceIdFromPath,
|
|
17
23
|
} = require('./lib/transcript-store');
|
|
24
|
+
const { queryPromptExecutions } = require('./lib/prompt-executions-query');
|
|
25
|
+
const { claudeFileSessionId, _usageMetadata } = require('./lib/jsonl-conversation-parser');
|
|
26
|
+
const structuredCapture = require('./lib/structured-capture');
|
|
18
27
|
// AI search uses direct HTTP calls to Claude API (supports Portkey proxy)
|
|
19
28
|
|
|
20
29
|
let dbMaintenanceRunner = null;
|
|
30
|
+
let imageSaveRunner = null;
|
|
21
31
|
const MOBILE_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
22
32
|
const TRANSCRIPT_IMPORT_MAX_BYTES = Math.max(1024 * 1024, Number(process.env.CTM_TRANSCRIPT_IMPORT_MAX_BYTES || 10 * 1024 * 1024));
|
|
23
33
|
const TRANSCRIPT_IMPORT_LARGE_FILE_BYTES = Math.max(1024 * 1024, Number(process.env.CTM_TRANSCRIPT_IMPORT_LARGE_FILE_BYTES || 64 * 1024 * 1024));
|
|
24
34
|
const CONVERSATION_IMPORT_RETRY_AFTER_MS = 30 * 1000;
|
|
35
|
+
const BACKGROUND_TRANSCRIPT_IMPORT_DEFAULT_BYTES = 2 * 1024 * 1024;
|
|
25
36
|
|
|
26
37
|
function setDbMaintenanceRunner(fn) {
|
|
27
38
|
dbMaintenanceRunner = typeof fn === 'function' ? fn : null;
|
|
28
39
|
}
|
|
29
40
|
|
|
41
|
+
function setImageSaveRunner(fn) {
|
|
42
|
+
imageSaveRunner = typeof fn === 'function' ? fn : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// deferRow=true returns as soon as the file is on disk (id:null), firing the
|
|
46
|
+
// images-table INSERT in the background — used by the terminal paste hot path,
|
|
47
|
+
// which forwards the file path/token to the provider and never needs the row id.
|
|
48
|
+
// Callers that need the real id (feedback attachments, prompt-editor annotation)
|
|
49
|
+
// leave deferRow falsy and get the awaited row.
|
|
50
|
+
async function saveImageViaOwner(promptId, buffer, filename, mimeType, deferRow = false) {
|
|
51
|
+
if (imageSaveRunner) {
|
|
52
|
+
return await imageSaveRunner({ promptId, buffer, filename, mimeType, deferRow });
|
|
53
|
+
}
|
|
54
|
+
return await db.saveImageDecoupled(promptId, buffer, filename, mimeType, { deferRow });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Measurement scaffolding for "image paste sometimes takes a long time". Logs one
|
|
58
|
+
// `[img-upload]` line per save (→ ctm.log) and returns the result with the internal
|
|
59
|
+
// `_timing` stripped so it isn't sent to the client. `roundTripMs` is the full
|
|
60
|
+
// saveImageViaOwner/saveImageFromFile call; `queueMs` ≈ roundTrip minus the
|
|
61
|
+
// worker-side hash/write/insert (i.e. db-owner queue wait + IPC/structured-clone of
|
|
62
|
+
// the buffer). writeMs/copyMs is the suspected Dropbox file-provider cost.
|
|
63
|
+
function _logImageTiming(label, meta, result) {
|
|
64
|
+
try {
|
|
65
|
+
const t = (result && result._timing) || {};
|
|
66
|
+
const workMs = (t.hashMs || 0) + (t.writeMs || 0) + (t.copyMs || 0) + (t.insertMs || 0);
|
|
67
|
+
const rt = meta.roundTripMs;
|
|
68
|
+
const queueMs = rt != null ? Math.max(0, Math.round(rt - workMs)) : null;
|
|
69
|
+
const bytes = meta.bytes != null ? meta.bytes : (t.bytes != null ? t.bytes : '?');
|
|
70
|
+
const writePart = ('writeMs' in t)
|
|
71
|
+
? `writeMs=${t.writeMs}${t.wrote === false ? '(dedup)' : ''}`
|
|
72
|
+
: ('copyMs' in t ? `copyMs=${t.copyMs}` : 'writeMs=-');
|
|
73
|
+
// `path` (caller source, e.g. terminal/prompt/mobile) + `defer` (was the row INSERT
|
|
74
|
+
// deferred = the terminal fast path) disambiguate which upload route a slow line came
|
|
75
|
+
// from. defer=1 with insertMs=0 + tiny roundTrip is the terminal paste fast path;
|
|
76
|
+
// a slow line with defer=0 is a route that intentionally awaits the row (prompt
|
|
77
|
+
// editor / mobile / feedback).
|
|
78
|
+
const deferMark = (meta.defer != null) ? (meta.defer ? 1 : 0) : (t.deferred ? 1 : null);
|
|
79
|
+
console.log(
|
|
80
|
+
`[img-upload] ${label} bytes=${bytes}`
|
|
81
|
+
+ (meta.source ? ` path=${meta.source}` : '')
|
|
82
|
+
+ (deferMark != null ? ` defer=${deferMark}` : '')
|
|
83
|
+
+ (meta.readMs != null ? ` readMs=${Math.round(meta.readMs)}` : '')
|
|
84
|
+
+ (rt != null ? ` roundTripMs=${Math.round(rt)}` : '')
|
|
85
|
+
+ (queueMs != null ? ` queueMs=${queueMs}` : '')
|
|
86
|
+
+ ` hashMs=${t.hashMs != null ? t.hashMs : '-'} ${writePart} insertMs=${t.insertMs != null ? t.insertMs : '-'}`
|
|
87
|
+
);
|
|
88
|
+
} catch { /* never let logging break an upload */ }
|
|
89
|
+
if (result && result._timing) { const r = { ...result }; delete r._timing; return r; }
|
|
90
|
+
return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
30
93
|
// Embed a prompt (async, fire-and-forget)
|
|
31
94
|
async function _embedPrompt(promptId, title, content) {
|
|
32
95
|
try {
|
|
@@ -142,6 +205,7 @@ function handlePromptApi(req, res, url) {
|
|
|
142
205
|
|
|
143
206
|
// --- Images ---
|
|
144
207
|
if (p === '/api/images/upload' && m === 'POST') return handleUploadImage(req, res, url);
|
|
208
|
+
if (p === '/api/images/ingest-path' && m === 'POST') return handleIngestImagePath(req, res);
|
|
145
209
|
if (p === '/api/mobile/attachments/upload' && m === 'POST') return handleUploadMobileAttachment(req, res, url);
|
|
146
210
|
if (p === '/api/session/image-refs' && m === 'POST') return handleSessionImageRefs(req, res);
|
|
147
211
|
if (p.match(/^\/api\/images\/\d+$/) && m === 'GET') return handleGetImage(req, res, url);
|
|
@@ -195,6 +259,10 @@ function handlePromptApi(req, res, url) {
|
|
|
195
259
|
if (p === '/api/backups' && m === 'POST') return handleCreateBackup(req, res);
|
|
196
260
|
if (p === '/api/backups/restore' && m === 'POST') return handleRestoreBackup(req, res);
|
|
197
261
|
if (p.match(/^\/api\/backups\/[^/]+$/) && m === 'DELETE') return handleDeleteBackup(req, res, url);
|
|
262
|
+
if (p === '/api/storage/locations' && m === 'GET') return handleGetStorageLocations(req, res);
|
|
263
|
+
if (p === '/api/storage/backup-dirs' && m === 'PUT') return handlePutBackupDirs(req, res);
|
|
264
|
+
if (p === '/api/storage/migration/preview' && m === 'POST') return handlePreviewStorageMigration(req, res);
|
|
265
|
+
if (p === '/api/storage/migration/apply' && m === 'POST') return handleApplyStorageMigration(req, res);
|
|
198
266
|
|
|
199
267
|
// --- Tool Permissions (Claude Code native) ---
|
|
200
268
|
if (p === '/api/tool-permissions/scan' && m === 'POST') return handleScanToolUsage(req, res);
|
|
@@ -229,6 +297,10 @@ function handlePromptApi(req, res, url) {
|
|
|
229
297
|
return handleResolveApprovalDecision(req, res, parseInt(p.split('/')[3]));
|
|
230
298
|
}
|
|
231
299
|
|
|
300
|
+
// --- Escalation review (grouped "Needs Review" surface in the Permission page) ---
|
|
301
|
+
if (p === '/api/approval-escalations' && m === 'GET') return handleListEscalations(req, res);
|
|
302
|
+
if (p === '/api/approval-escalations/resolve' && m === 'POST') return handleResolveEscalations(req, res);
|
|
303
|
+
|
|
232
304
|
// --- Dangerous-command blocklist (opt-in safety net) ---
|
|
233
305
|
if (p === '/api/approval/blocklist' && m === 'GET') return handleGetBlocklist(req, res);
|
|
234
306
|
if (p === '/api/approval/blocklist' && m === 'POST') return handleSetBlocklistEnabled(req, res);
|
|
@@ -248,7 +320,7 @@ function handlePromptApi(req, res, url) {
|
|
|
248
320
|
if (draftMatch && m === 'PUT') return handleSaveQueueDraft(req, res, draftMatch[1]);
|
|
249
321
|
if (draftMatch && m === 'DELETE') return handleDeleteQueueDraft(req, res, draftMatch[1]);
|
|
250
322
|
const queueMatch = p.match(/^\/api\/queues\/([^/]+)$/);
|
|
251
|
-
const queueActionMatch = p.match(/^\/api\/queues\/([^/]+)\/(start|pause|resume|next|skip|stop|mode)$/);
|
|
323
|
+
const queueActionMatch = p.match(/^\/api\/queues\/([^/]+)\/(start|pause|resume|next|skip|stop|mode|remove|reorder)$/);
|
|
252
324
|
if (queueMatch && !queueActionMatch && m === 'GET') return handleGetQueue(req, res, queueMatch[1]);
|
|
253
325
|
if (queueMatch && !queueActionMatch && m === 'DELETE') return handleDeleteQueue(req, res, queueMatch[1]);
|
|
254
326
|
if (queueActionMatch && m === 'POST') return handleQueueAction(req, res, queueActionMatch[1], queueActionMatch[2]);
|
|
@@ -266,6 +338,7 @@ function handlePromptApi(req, res, url) {
|
|
|
266
338
|
if (p === '/api/copilot/chat' && m === 'POST') return handleCopilotChat(req, res);
|
|
267
339
|
if (p === '/api/prompt-quality' && m === 'GET') return handlePromptQuality(req, res, url);
|
|
268
340
|
if (p === '/api/prompt-executions' && m === 'GET') return handleListExecutions(req, res, url);
|
|
341
|
+
if (p === '/api/prompt-executions/stats' && m === 'GET') return handlePromptExecutionStats(req, res);
|
|
269
342
|
const execSessionMatch = p.match(/^\/api\/prompt-executions\/session\/([^/]+)$/);
|
|
270
343
|
if (execSessionMatch && m === 'GET') return handleSessionExecutions(req, res, execSessionMatch[1]);
|
|
271
344
|
const execOutcomeMatch = p.match(/^\/api\/prompt-executions\/(\d+)\/outcome$/);
|
|
@@ -284,6 +357,7 @@ function handlePromptApi(req, res, url) {
|
|
|
284
357
|
if (p === '/api/prompts/hybrid-search' && m === 'GET') return handleHybridSearch(req, res, url);
|
|
285
358
|
if (p === '/api/prompts/improve' && m === 'POST') return handleImprovePrompt(req, res);
|
|
286
359
|
if (p === '/api/skills/autocomplete' && m === 'GET') return handleSkillAutocomplete(req, res, url);
|
|
360
|
+
if (p === '/api/skills/resolve-intent' && m === 'GET') return handleSkillResolveIntent(req, res, url);
|
|
287
361
|
if (p === '/api/harvest/stats' && m === 'GET') return handleHarvestStats(req, res);
|
|
288
362
|
if (p === '/api/prompts/pattern-suggestions' && m === 'GET') return handlePatternSuggestions(req, res);
|
|
289
363
|
|
|
@@ -647,12 +721,120 @@ async function handleUploadImage(req, res, url) {
|
|
|
647
721
|
const promptId = parseInt(url.searchParams.get('prompt_id') || '0');
|
|
648
722
|
const filename = url.searchParams.get('filename') || 'image.png';
|
|
649
723
|
const mimeType = req.headers['content-type'] || 'image/png';
|
|
724
|
+
// defer_row=1: return as soon as the file is written (id:null), firing the DB
|
|
725
|
+
// INSERT in the background. The terminal paste path sets this so a paste isn't
|
|
726
|
+
// stuck behind the db-owner write queue.
|
|
727
|
+
const deferRow = url.searchParams.get('defer_row') === '1';
|
|
728
|
+
const _tRead0 = performance.now();
|
|
650
729
|
const buffer = await readRawBody(req);
|
|
651
|
-
const
|
|
652
|
-
|
|
730
|
+
const _tRead1 = performance.now();
|
|
731
|
+
const result = await saveImageViaOwner(promptId, buffer, filename, mimeType, deferRow);
|
|
732
|
+
const safe = _logImageTiming('upload', {
|
|
733
|
+
bytes: buffer.length, readMs: _tRead1 - _tRead0, roundTripMs: performance.now() - _tRead1,
|
|
734
|
+
source: url.searchParams.get('source') || '', defer: deferRow,
|
|
735
|
+
}, result);
|
|
736
|
+
jsonResponse(res, 201, safe);
|
|
653
737
|
} catch (e) { jsonResponse(res, 400, { error: e.message }); }
|
|
654
738
|
}
|
|
655
739
|
|
|
740
|
+
const INGEST_IMAGE_EXT_MIME = {
|
|
741
|
+
'.png': 'image/png',
|
|
742
|
+
'.jpg': 'image/jpeg',
|
|
743
|
+
'.jpeg': 'image/jpeg',
|
|
744
|
+
'.gif': 'image/gif',
|
|
745
|
+
'.webp': 'image/webp',
|
|
746
|
+
'.heic': 'image/heic',
|
|
747
|
+
'.heif': 'image/heif',
|
|
748
|
+
'.bmp': 'image/bmp',
|
|
749
|
+
'.tif': 'image/tiff',
|
|
750
|
+
'.tiff': 'image/tiff',
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
// Decode a file:// URL or accept a plain absolute path. Returns '' if neither.
|
|
754
|
+
function _ingestPathFromInput(value) {
|
|
755
|
+
const raw = String(value || '').trim();
|
|
756
|
+
if (!raw) return '';
|
|
757
|
+
if (/^file:\/\//i.test(raw)) {
|
|
758
|
+
try { return path.resolve(decodeURIComponent(new URL(raw).pathname || '')); }
|
|
759
|
+
catch { return ''; }
|
|
760
|
+
}
|
|
761
|
+
if (path.isAbsolute(raw)) return path.resolve(raw);
|
|
762
|
+
return '';
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Only stabilize image references that live in user-controlled temp/home roots.
|
|
766
|
+
// This is the macOS screenshot/drag case (e.g. /var/folders/.../TemporaryItems/...png).
|
|
767
|
+
// Refuse arbitrary system paths so this localhost-only endpoint can't be coaxed
|
|
768
|
+
// into copying sensitive files into the served images dir.
|
|
769
|
+
//
|
|
770
|
+
// SECURITY: `candidate` MUST already be a realpath-resolved path (symlinks
|
|
771
|
+
// followed) and each root is realpath-resolved here, so a `.png` symlink living
|
|
772
|
+
// inside an allowed temp root but pointing at e.g. /etc/shadow cannot pass the
|
|
773
|
+
// containment check (its real target is outside the allowed roots). A purely
|
|
774
|
+
// string-based path.resolve() check would be bypassable by such a symlink.
|
|
775
|
+
function _ingestSourceAllowed(candidate) {
|
|
776
|
+
const realRoots = [];
|
|
777
|
+
const pushRoot = (p) => {
|
|
778
|
+
if (!p) return;
|
|
779
|
+
// Resolve each root through realpath too (e.g. macOS /tmp -> /private/tmp,
|
|
780
|
+
// /var -> /private/var) so the comparison is symlink-stable on both sides.
|
|
781
|
+
try { realRoots.push(fs.realpathSync(p)); } catch { /* root may not exist */ }
|
|
782
|
+
};
|
|
783
|
+
try { pushRoot(os.tmpdir()); } catch {}
|
|
784
|
+
for (const env of ['TMPDIR', 'HOME']) {
|
|
785
|
+
if (process.env[env]) pushRoot(process.env[env]);
|
|
786
|
+
}
|
|
787
|
+
// macOS per-user temp + screenshot scratch live under /var/folders and /private/var/folders.
|
|
788
|
+
for (const r of ['/var/folders', '/private/var/folders', '/tmp', '/private/tmp']) pushRoot(r);
|
|
789
|
+
return realRoots.some(root => {
|
|
790
|
+
const rel = path.relative(root, candidate);
|
|
791
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Ingest a local image FILE by path (not bytes) and return a stable, served copy.
|
|
796
|
+
// Used by the terminal image-reference paste path so CTM forwards a stable
|
|
797
|
+
// images-dir path to the provider instead of the ephemeral macOS temp path
|
|
798
|
+
// (which leaks the local fs and disappears once the screenshot scratch is reaped).
|
|
799
|
+
async function handleIngestImagePath(req, res) {
|
|
800
|
+
try {
|
|
801
|
+
const body = await readBody(req, 64 * 1024);
|
|
802
|
+
const resolved = _ingestPathFromInput(body && (body.path || body.url || body.reference));
|
|
803
|
+
if (!resolved) return jsonResponse(res, 400, { error: 'invalid_path' });
|
|
804
|
+
const ext = path.extname(resolved).toLowerCase();
|
|
805
|
+
const mimeType = INGEST_IMAGE_EXT_MIME[ext];
|
|
806
|
+
if (!mimeType) return jsonResponse(res, 400, { error: 'unsupported_image_type' });
|
|
807
|
+
// SECURITY: reject symlinks outright, then resolve the real (symlink-followed)
|
|
808
|
+
// path BEFORE the allowlist check. Otherwise a `.png` symlink inside an allowed
|
|
809
|
+
// temp root could point at an arbitrary file (e.g. /etc/shadow) and slip past a
|
|
810
|
+
// string-only containment check, then be copied into the served images dir.
|
|
811
|
+
let lst;
|
|
812
|
+
try { lst = await fs.promises.lstat(resolved); }
|
|
813
|
+
catch { return jsonResponse(res, 404, { error: 'file_not_found' }); }
|
|
814
|
+
if (lst.isSymbolicLink()) return jsonResponse(res, 403, { error: 'path_not_allowed' });
|
|
815
|
+
let realPath;
|
|
816
|
+
try { realPath = await fs.promises.realpath(resolved); }
|
|
817
|
+
catch { return jsonResponse(res, 404, { error: 'file_not_found' }); }
|
|
818
|
+
if (!_ingestSourceAllowed(realPath)) return jsonResponse(res, 403, { error: 'path_not_allowed' });
|
|
819
|
+
let stat;
|
|
820
|
+
try { stat = await fs.promises.stat(realPath); }
|
|
821
|
+
catch { return jsonResponse(res, 404, { error: 'file_not_found' }); }
|
|
822
|
+
if (!stat.isFile() || stat.size <= 0) return jsonResponse(res, 400, { error: 'not_a_file' });
|
|
823
|
+
// Copy (deleteSource omitted → default false): never disturb the user's temp file.
|
|
824
|
+
const _tSave0 = performance.now();
|
|
825
|
+
const result = await db.saveImageFromFile(0, realPath, path.basename(realPath), mimeType);
|
|
826
|
+
_logImageTiming('ingest-path', { bytes: stat.size, roundTripMs: performance.now() - _tSave0 }, result);
|
|
827
|
+
jsonResponse(res, 201, {
|
|
828
|
+
id: result.id,
|
|
829
|
+
filename: result.filename,
|
|
830
|
+
path: result.path,
|
|
831
|
+
url: `/api/images/file/${encodeURIComponent(result.filename)}`,
|
|
832
|
+
});
|
|
833
|
+
} catch (e) {
|
|
834
|
+
jsonResponse(res, 400, { error: e.message });
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
656
838
|
async function handleUploadMobileAttachment(req, res, url) {
|
|
657
839
|
try {
|
|
658
840
|
const kind = url.searchParams.get('kind') === 'image' ? 'image' : 'file';
|
|
@@ -665,9 +847,11 @@ async function handleUploadMobileAttachment(req, res, url) {
|
|
|
665
847
|
jsonResponse(res, 400, { error: 'attachment_empty' });
|
|
666
848
|
return;
|
|
667
849
|
}
|
|
668
|
-
const
|
|
850
|
+
const _tSave0 = performance.now();
|
|
851
|
+
const result = await saveImageViaOwner(0, buffer, storedFilename, mimeType);
|
|
852
|
+
const safe = _logImageTiming('mobile', { bytes: buffer.length, roundTripMs: performance.now() - _tSave0 }, result);
|
|
669
853
|
jsonResponse(res, 201, {
|
|
670
|
-
...
|
|
854
|
+
...safe,
|
|
671
855
|
kind,
|
|
672
856
|
originalName: filename,
|
|
673
857
|
mimeType,
|
|
@@ -869,13 +1053,30 @@ function handleListConversations(req, res, url) {
|
|
|
869
1053
|
|
|
870
1054
|
// Core import logic shared by API handler and auto-import
|
|
871
1055
|
const { getAllSessionFiles, getAllSessionFilesAsync, parseSessionFile } = require('./session-utils');
|
|
1056
|
+
const walleTranscript = require('./lib/walle-transcript');
|
|
1057
|
+
const { readWalleCtmHistory } = require('./lib/walle-ctm-history');
|
|
1058
|
+
const {
|
|
1059
|
+
persistWalleSessionConversation,
|
|
1060
|
+
WALLE_SESSION_CACHE_PARSER_VERSION,
|
|
1061
|
+
} = require('./lib/walle-session-cache');
|
|
872
1062
|
const {
|
|
873
|
-
|
|
1063
|
+
createCodexUserDeduper,
|
|
1064
|
+
parseCodexJsonlFileIntoMessagesAsync,
|
|
874
1065
|
parseCodexJsonlFileIntoMessages,
|
|
875
1066
|
parseCodexJsonlIntoMessages,
|
|
876
1067
|
readCodexRolloutMetadata,
|
|
877
1068
|
} = require('./lib/session-history');
|
|
878
1069
|
const fsp = require('fs').promises;
|
|
1070
|
+
const CODEX_CONVERSATION_IMPORT_PARSER_VERSION = 3;
|
|
1071
|
+
const DEFAULT_CONVERSATION_IMPORT_PARSER_VERSION = 1;
|
|
1072
|
+
// Claude Code / Claude Desktop sessions are Anthropic, but their JSONL carries no explicit
|
|
1073
|
+
// `modelProvider` field, so the parser leaves it ''. Storing '' makes the import's
|
|
1074
|
+
// `missingModel` candidate predicate (`!existing.model_provider`) re-select the session on
|
|
1075
|
+
// EVERY scan tick forever — re-stringifying + rewriting the whole conversation each time
|
|
1076
|
+
// (the re-import storm: 75 sessions, db-owner-worker saturation, write-lock contention).
|
|
1077
|
+
// Default to a concrete provider so the row converges after one import. (Codex uses 'openai'
|
|
1078
|
+
// and Cursor 'cursor' at their own call sites.)
|
|
1079
|
+
const CLAUDE_MODEL_PROVIDER = 'anthropic';
|
|
879
1080
|
|
|
880
1081
|
// Parse JSONL content string into conversation messages (sync, CPU-bound but
|
|
881
1082
|
// called after async file read so only the parse blocks — not disk I/O).
|
|
@@ -981,7 +1182,7 @@ function _parseLargeConversationLine(line) {
|
|
|
981
1182
|
return null;
|
|
982
1183
|
}
|
|
983
1184
|
|
|
984
|
-
async function _parseConversationContent(content) {
|
|
1185
|
+
async function _parseConversationContent(content, opts = {}) {
|
|
985
1186
|
const messages = [];
|
|
986
1187
|
const searchMessages = [];
|
|
987
1188
|
let assistantCount = 0;
|
|
@@ -990,6 +1191,29 @@ async function _parseConversationContent(content) {
|
|
|
990
1191
|
let firstAssistantText = '';
|
|
991
1192
|
let renameName = '';
|
|
992
1193
|
|
|
1194
|
+
// Cross-file stitching (mirrors parseSessionFiles in lib/jsonl-conversation-parser):
|
|
1195
|
+
// a post-compact Claude file inherits the PARENT file's lines (old sessionId at the
|
|
1196
|
+
// top). When the owner file still exists next to this one, drop the inherited copy
|
|
1197
|
+
// so the owner's import is the only one that stores those turns. Orphan prefixes
|
|
1198
|
+
// (owner deleted) are kept — they are the only surviving copy.
|
|
1199
|
+
const fileSessionId = String(opts.fileSessionId || '');
|
|
1200
|
+
const fileDir = String(opts.fileDir || '');
|
|
1201
|
+
const ownerOnDisk = new Map();
|
|
1202
|
+
const dropInheritedEntry = (entryId) => {
|
|
1203
|
+
if (!fileSessionId || !fileDir || !entryId || entryId === fileSessionId) return false;
|
|
1204
|
+
if (!ownerOnDisk.has(entryId)) {
|
|
1205
|
+
let exists = false;
|
|
1206
|
+
try { exists = fs.existsSync(path.join(fileDir, `${entryId}.jsonl`)); } catch {}
|
|
1207
|
+
ownerOnDisk.set(entryId, exists);
|
|
1208
|
+
}
|
|
1209
|
+
return ownerOnDisk.get(entryId);
|
|
1210
|
+
};
|
|
1211
|
+
const seenUuids = new Set();
|
|
1212
|
+
const summaries = [];
|
|
1213
|
+
// Last contiguous run emitted for one assistant parentUuid (structured
|
|
1214
|
+
// emission replaces the whole run when the same row re-streams).
|
|
1215
|
+
let lastAssistantRun = null;
|
|
1216
|
+
|
|
993
1217
|
// Yield while scanning so a large JSONL does not monopolize the event loop.
|
|
994
1218
|
// Avoid content.split('\n') here: building the full line array for a 50MB+
|
|
995
1219
|
// file blocks before the parser gets its first chance to yield.
|
|
@@ -1014,6 +1238,12 @@ async function _parseConversationContent(content) {
|
|
|
1014
1238
|
i++;
|
|
1015
1239
|
try {
|
|
1016
1240
|
if (line.length > _LARGE_JSONL_LINE_BYTES) {
|
|
1241
|
+
if (dropInheritedEntry(_readJsonStringPrefix(line, '"sessionId":"', 64))) {
|
|
1242
|
+
await maybeYield();
|
|
1243
|
+
continue;
|
|
1244
|
+
}
|
|
1245
|
+
const largeUuid = _readJsonStringPrefix(line, '"uuid":"', 64);
|
|
1246
|
+
if (largeUuid) seenUuids.add(largeUuid);
|
|
1017
1247
|
const large = _parseLargeConversationLine(line);
|
|
1018
1248
|
if (large?.role === 'user') {
|
|
1019
1249
|
messages.push({ role: 'user', text: large.text.slice(0, _CONVERSATION_CACHE_TEXT_LIMIT), timestamp: large.timestamp });
|
|
@@ -1044,11 +1274,64 @@ async function _parseConversationContent(content) {
|
|
|
1044
1274
|
}
|
|
1045
1275
|
|
|
1046
1276
|
const entry = JSON.parse(line);
|
|
1047
|
-
if (entry.
|
|
1277
|
+
if (dropInheritedEntry(typeof entry.sessionId === 'string' ? entry.sessionId : '')) {
|
|
1278
|
+
await maybeYield();
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
if (typeof entry.uuid === 'string' && entry.uuid) seenUuids.add(entry.uuid);
|
|
1282
|
+
if (entry.type === 'summary' && typeof entry.summary === 'string' && entry.summary.trim()) {
|
|
1283
|
+
// Async-generated session title ({summary, leafUuid}); not a turn.
|
|
1284
|
+
summaries.push({ summary: entry.summary.trim(), leafUuid: entry.leafUuid || '' });
|
|
1285
|
+
} else if (entry.type === 'system' && entry.subtype === 'compact_boundary'
|
|
1286
|
+
&& structuredCapture.structuredCaptureEnabled()) {
|
|
1287
|
+
const compactMeta = entry.compactMetadata || {};
|
|
1288
|
+
const boundary = structuredCapture.compactBoundaryMessage({
|
|
1289
|
+
provider: 'claude',
|
|
1290
|
+
trigger: compactMeta.trigger,
|
|
1291
|
+
preTokens: Number(compactMeta.preTokens),
|
|
1292
|
+
logicalParentUuid: entry.logicalParentUuid,
|
|
1293
|
+
timestamp: entry.timestamp,
|
|
1294
|
+
});
|
|
1295
|
+
messages.push(boundary);
|
|
1296
|
+
searchMessages.push(boundary);
|
|
1297
|
+
} else if (entry.type === 'user' && entry.message?.role === 'user') {
|
|
1048
1298
|
const c = entry.message.content;
|
|
1049
1299
|
const text = typeof c === 'string' ? c
|
|
1050
1300
|
: Array.isArray(c) ? c.filter(b => b.type === 'text').map(b => b.text).join('\n') : '';
|
|
1051
|
-
|
|
1301
|
+
const captureOn = structuredCapture.structuredCaptureEnabled();
|
|
1302
|
+
if (captureOn && (entry.isCompactSummary || entry.isVisibleInTranscriptOnly)) {
|
|
1303
|
+
// Post-compact synthetic context summary — a collapsed structured
|
|
1304
|
+
// row, never a user prompt (keeps first/last-user title signals clean).
|
|
1305
|
+
if (text) {
|
|
1306
|
+
const summaryMsg = structuredCapture.compactSummaryMessage({ provider: 'claude', text, timestamp: entry.timestamp });
|
|
1307
|
+
messages.push(summaryMsg);
|
|
1308
|
+
searchMessages.push(summaryMsg);
|
|
1309
|
+
}
|
|
1310
|
+
} else if (captureOn && Array.isArray(c) && c.some(b => b && b.type === 'tool_result')) {
|
|
1311
|
+
// Mirrors lib/jsonl-conversation-parser: one structured tool_result
|
|
1312
|
+
// row per block, enriched from the top-level toolUseResult.
|
|
1313
|
+
const resultBlocks = c.filter(b => b && b.type === 'tool_result');
|
|
1314
|
+
const tur = resultBlocks.length === 1 && entry.toolUseResult && typeof entry.toolUseResult === 'object'
|
|
1315
|
+
? entry.toolUseResult : null;
|
|
1316
|
+
for (const block of resultBlocks) {
|
|
1317
|
+
const blockText = typeof block.content === 'string' ? block.content
|
|
1318
|
+
: Array.isArray(block.content)
|
|
1319
|
+
? block.content.filter(b => b && b.type === 'text' && b.text).map(b => b.text).join('\n')
|
|
1320
|
+
: '';
|
|
1321
|
+
const resultMsg = structuredCapture.toolResultMessage({
|
|
1322
|
+
provider: 'claude',
|
|
1323
|
+
callId: block.tool_use_id,
|
|
1324
|
+
output: blockText || (tur && typeof tur.stdout === 'string' ? tur.stdout : ''),
|
|
1325
|
+
isError: block.is_error === true,
|
|
1326
|
+
filePath: tur ? (tur.filePath || tur.file_path) : undefined,
|
|
1327
|
+
durationMs: tur ? Number(tur.durationMs) : undefined,
|
|
1328
|
+
structuredPatch: tur ? tur.structuredPatch : undefined,
|
|
1329
|
+
timestamp: entry.timestamp,
|
|
1330
|
+
});
|
|
1331
|
+
messages.push(resultMsg);
|
|
1332
|
+
searchMessages.push(resultMsg);
|
|
1333
|
+
}
|
|
1334
|
+
} else if (text) {
|
|
1052
1335
|
messages.push({ role: 'user', text: text.slice(0, _CONVERSATION_CACHE_TEXT_LIMIT), timestamp: entry.timestamp });
|
|
1053
1336
|
searchMessages.push({ role: 'user', text, timestamp: entry.timestamp });
|
|
1054
1337
|
if (!firstUserContent) firstUserContent = text.slice(0, _TITLE_SIGNAL_TEXT_LIMIT);
|
|
@@ -1060,6 +1343,80 @@ async function _parseConversationContent(content) {
|
|
|
1060
1343
|
} else if (entry.type === 'assistant' && entry.message?.role === 'assistant') {
|
|
1061
1344
|
const c = entry.message.content;
|
|
1062
1345
|
if (!Array.isArray(c)) continue;
|
|
1346
|
+
if (structuredCapture.structuredCaptureEnabled()) {
|
|
1347
|
+
// Mirrors lib/jsonl-conversation-parser: thinking → reasoning rows,
|
|
1348
|
+
// tool_use → tool_call rows, text blocks → ONE assistant message
|
|
1349
|
+
// (parentUuid + usage/model metadata), in block order. A re-streamed
|
|
1350
|
+
// parentUuid replaces the previous contiguous run.
|
|
1351
|
+
const run = [];
|
|
1352
|
+
const textParts = [];
|
|
1353
|
+
let textInsertIndex = -1;
|
|
1354
|
+
for (const block of c) {
|
|
1355
|
+
if (!block || typeof block !== 'object') continue;
|
|
1356
|
+
if (block.type === 'text' && block.text) {
|
|
1357
|
+
if (textInsertIndex === -1) textInsertIndex = run.length;
|
|
1358
|
+
textParts.push(block.text);
|
|
1359
|
+
} else if (block.type === 'thinking' && (block.thinking || block.text)) {
|
|
1360
|
+
run.push(structuredCapture.reasoningMessage({ provider: 'claude', text: block.thinking || block.text, timestamp: entry.timestamp }));
|
|
1361
|
+
} else if (block.type === 'tool_use') {
|
|
1362
|
+
run.push(structuredCapture.toolCallMessage({
|
|
1363
|
+
provider: 'claude', tool: block.name, callId: block.id, args: block.input, timestamp: entry.timestamp,
|
|
1364
|
+
}));
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
let searchText = '';
|
|
1368
|
+
if (textParts.length) {
|
|
1369
|
+
searchText = textParts.join('\n');
|
|
1370
|
+
if (!firstAssistantText) firstAssistantText = searchText.slice(0, _TITLE_SIGNAL_TEXT_LIMIT);
|
|
1371
|
+
const msg = {
|
|
1372
|
+
role: 'assistant',
|
|
1373
|
+
text: searchText.slice(0, _CONVERSATION_CACHE_TEXT_LIMIT),
|
|
1374
|
+
timestamp: entry.timestamp,
|
|
1375
|
+
parentUuid: entry.parentUuid,
|
|
1376
|
+
_parent: entry.parentUuid,
|
|
1377
|
+
};
|
|
1378
|
+
const usage = _usageMetadata(entry.message.usage);
|
|
1379
|
+
const model = typeof entry.message.model === 'string' ? entry.message.model : '';
|
|
1380
|
+
if (usage || model) {
|
|
1381
|
+
msg.metadata = {};
|
|
1382
|
+
if (usage) msg.metadata.usage = usage;
|
|
1383
|
+
if (model) msg.metadata.model = model;
|
|
1384
|
+
}
|
|
1385
|
+
run.splice(textInsertIndex === -1 ? run.length : textInsertIndex, 0, msg);
|
|
1386
|
+
}
|
|
1387
|
+
if (run.length) {
|
|
1388
|
+
if (entry.parentUuid && lastAssistantRun
|
|
1389
|
+
&& lastAssistantRun.parent === entry.parentUuid
|
|
1390
|
+
&& lastAssistantRun.mEnd === messages.length
|
|
1391
|
+
&& lastAssistantRun.sEnd === searchMessages.length) {
|
|
1392
|
+
messages.splice(lastAssistantRun.mStart);
|
|
1393
|
+
searchMessages.splice(lastAssistantRun.sStart);
|
|
1394
|
+
assistantCount -= lastAssistantRun.assistantCount;
|
|
1395
|
+
}
|
|
1396
|
+
const mStart = messages.length;
|
|
1397
|
+
const sStart = searchMessages.length;
|
|
1398
|
+
let runAssistantCount = 0;
|
|
1399
|
+
for (const m of run) {
|
|
1400
|
+
messages.push(m);
|
|
1401
|
+
if (m.role === 'assistant') {
|
|
1402
|
+
runAssistantCount++;
|
|
1403
|
+
// Search index keeps the FULL text (the messages copy is capped).
|
|
1404
|
+
searchMessages.push({ ...m, text: searchText });
|
|
1405
|
+
} else {
|
|
1406
|
+
searchMessages.push(m);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
assistantCount += runAssistantCount;
|
|
1410
|
+
lastAssistantRun = {
|
|
1411
|
+
parent: entry.parentUuid,
|
|
1412
|
+
mStart, mEnd: messages.length,
|
|
1413
|
+
sStart, sEnd: searchMessages.length,
|
|
1414
|
+
assistantCount: runAssistantCount,
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
await maybeYield();
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1063
1420
|
const parts = [];
|
|
1064
1421
|
for (const block of c) {
|
|
1065
1422
|
if (block.type === 'text' && block.text) parts.push(block.text);
|
|
@@ -1094,6 +1451,12 @@ async function _parseConversationContent(content) {
|
|
|
1094
1451
|
}
|
|
1095
1452
|
messages.forEach(m => delete m._parent);
|
|
1096
1453
|
searchMessages.forEach(m => delete m._parent);
|
|
1454
|
+
// Title candidate (lowest precedence): the last summary whose leafUuid we
|
|
1455
|
+
// actually saw in this content — or one with no leafUuid at all.
|
|
1456
|
+
let summaryTitle = '';
|
|
1457
|
+
for (const s of summaries) {
|
|
1458
|
+
if (!s.leafUuid || seenUuids.has(s.leafUuid)) summaryTitle = s.summary.slice(0, 200);
|
|
1459
|
+
}
|
|
1097
1460
|
return {
|
|
1098
1461
|
messages,
|
|
1099
1462
|
searchMessages,
|
|
@@ -1102,6 +1465,7 @@ async function _parseConversationContent(content) {
|
|
|
1102
1465
|
lastUserContent,
|
|
1103
1466
|
firstAssistantText,
|
|
1104
1467
|
renameName,
|
|
1468
|
+
summaryTitle,
|
|
1105
1469
|
};
|
|
1106
1470
|
}
|
|
1107
1471
|
|
|
@@ -1157,8 +1521,9 @@ async function _importCompactPair(parsed, jsonlPath, bakPath, jsonlSize, bakSize
|
|
|
1157
1521
|
fsp.readFile(jsonlPath, 'utf8').catch(() => ''),
|
|
1158
1522
|
]);
|
|
1159
1523
|
|
|
1160
|
-
const
|
|
1161
|
-
const
|
|
1524
|
+
const stitchOpts = { fileSessionId: claudeFileSessionId(jsonlPath), fileDir: path.dirname(jsonlPath) };
|
|
1525
|
+
const bakParsed = await _parseConversationContent(bakContent, stitchOpts);
|
|
1526
|
+
const jsonlParsed = await _parseConversationContent(jsonlContent, stitchOpts);
|
|
1162
1527
|
|
|
1163
1528
|
// Concatenate then sort by timestamp so out-of-order writes (rare but
|
|
1164
1529
|
// possible mid-compact) end up in chronological order.
|
|
@@ -1189,7 +1554,7 @@ async function _importCompactPair(parsed, jsonlPath, bakPath, jsonlSize, bakSize
|
|
|
1189
1554
|
search_messages: allSearchMessages,
|
|
1190
1555
|
user_msg_count: signals.userCount,
|
|
1191
1556
|
assistant_msg_count: signals.assistantCount,
|
|
1192
|
-
title: parsed.title || (existing && existing.title) || '',
|
|
1557
|
+
title: parsed.title || (existing && existing.title) || jsonlParsed.summaryTitle || bakParsed.summaryTitle || '',
|
|
1193
1558
|
first_message: mergedFirstUser,
|
|
1194
1559
|
last_user_content: mergedLastUser,
|
|
1195
1560
|
first_assistant_text: mergedFirstAssistant,
|
|
@@ -1198,8 +1563,9 @@ async function _importCompactPair(parsed, jsonlPath, bakPath, jsonlSize, bakSize
|
|
|
1198
1563
|
file_size: totalSize,
|
|
1199
1564
|
session_created_at: parsed.timestamp,
|
|
1200
1565
|
hostname: parsed.hostname,
|
|
1201
|
-
model_provider: parsed.modelProvider || (existing && existing.model_provider) ||
|
|
1566
|
+
model_provider: parsed.modelProvider || (existing && existing.model_provider) || CLAUDE_MODEL_PROVIDER,
|
|
1202
1567
|
model_id: parsed.modelId || (existing && existing.model_id) || '',
|
|
1568
|
+
import_parser_version: DEFAULT_CONVERSATION_IMPORT_PARSER_VERSION,
|
|
1203
1569
|
});
|
|
1204
1570
|
return true;
|
|
1205
1571
|
}
|
|
@@ -1232,15 +1598,8 @@ function _loadIndexedSessionMessages(sessionId) {
|
|
|
1232
1598
|
}
|
|
1233
1599
|
}
|
|
1234
1600
|
|
|
1235
|
-
function
|
|
1236
|
-
|
|
1237
|
-
for (const msg of Array.isArray(messages) ? messages : []) {
|
|
1238
|
-
if (msg && msg.role === 'user') {
|
|
1239
|
-
const key = codexUserKey(msg.text || msg.content || '');
|
|
1240
|
-
if (key) seen.add(key);
|
|
1241
|
-
}
|
|
1242
|
-
}
|
|
1243
|
-
return seen;
|
|
1601
|
+
function _codexUserDeduperFromMessages(messages) {
|
|
1602
|
+
return createCodexUserDeduper((Array.isArray(messages) ? messages : []).filter(msg => msg && msg.role === 'user'));
|
|
1244
1603
|
}
|
|
1245
1604
|
|
|
1246
1605
|
async function _readFileRange(filePath, start, length) {
|
|
@@ -1255,21 +1614,28 @@ async function _readFileRange(filePath, start, length) {
|
|
|
1255
1614
|
}
|
|
1256
1615
|
|
|
1257
1616
|
function _conversationImportIndexRows() {
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1617
|
+
// Attribution: two full-table index scans built on every conversation-import
|
|
1618
|
+
// tick. They run as the sync prefix of _conversationImportCandidates, which is
|
|
1619
|
+
// itself called AFTER the job's `await getAllSessionFilesAsync()` — so the
|
|
1620
|
+
// scheduler's tag is already cleared and a slow scan would log as `(unknown)`.
|
|
1621
|
+
// Phase 2.
|
|
1622
|
+
return runSync('conversation-import:buildIndexRows', () => {
|
|
1623
|
+
const conversations = new Map();
|
|
1624
|
+
const linkedAgentIds = new Set();
|
|
1625
|
+
try {
|
|
1626
|
+
const rows = db.getDb().prepare(
|
|
1627
|
+
'SELECT ctm_session_id, file_size, model_provider, import_parser_version FROM session_conversations'
|
|
1628
|
+
).all();
|
|
1629
|
+
for (const row of rows) conversations.set(row.ctm_session_id, row);
|
|
1630
|
+
} catch {}
|
|
1631
|
+
try {
|
|
1632
|
+
const rows = db.getDb().prepare(
|
|
1633
|
+
'SELECT agent_session_id FROM agent_sessions WHERE agent_session_id IS NOT NULL AND agent_session_id != ""'
|
|
1634
|
+
).all();
|
|
1635
|
+
for (const row of rows) linkedAgentIds.add(row.agent_session_id);
|
|
1636
|
+
} catch {}
|
|
1637
|
+
return { conversations, linkedAgentIds };
|
|
1638
|
+
});
|
|
1273
1639
|
}
|
|
1274
1640
|
|
|
1275
1641
|
async function _conversationImportEffectiveSize(filePath, stat) {
|
|
@@ -1296,14 +1662,29 @@ async function _conversationImportCandidates(allFiles, lastScanAt) {
|
|
|
1296
1662
|
const stat = await fsp.stat(claudeDesktopSessions.sourcePathForStat(filePath));
|
|
1297
1663
|
if (!stat.isFile()) continue;
|
|
1298
1664
|
const sessionId = listedSessionId || path.basename(filePath).replace(/\.jsonl(\.bak)?$/, '');
|
|
1665
|
+
// Desktop sessions re-candidate forever via `linkedMissingCache`: importSessionFile
|
|
1666
|
+
// returns false for an empty desktop session (no messages), so no session_conversations
|
|
1667
|
+
// row is ever written, so `!existing` stays true and the session is re-selected every
|
|
1668
|
+
// import tick. Skip dead/empty desktop sessions here — there is nothing to import, and a
|
|
1669
|
+
// healthy one (messages present) writes a cache row and stops matching this guard.
|
|
1670
|
+
if (claudeDesktopSessions.isVirtualSessionPath(filePath)
|
|
1671
|
+
&& (claudeDesktopSessions.getMessages(sessionId) || []).length === 0) {
|
|
1672
|
+
continue;
|
|
1673
|
+
}
|
|
1299
1674
|
const existing = conversations.get(sessionId);
|
|
1300
1675
|
const existingSize = Number(existing?.file_size || 0);
|
|
1301
1676
|
const { effectiveSize, hasCompactSibling } = await _conversationImportEffectiveSize(filePath, stat);
|
|
1677
|
+
const isCodexRollout = projectEntry === 'codex' || String(filePath || '').includes(`${path.sep}.codex${path.sep}sessions${path.sep}`);
|
|
1678
|
+
const isWalleTranscript = projectEntry === walleTranscript.WALLE_PROJECT_ENTRY;
|
|
1302
1679
|
const changedSinceScan = stat.mtimeMs > lastScanAt;
|
|
1303
1680
|
const cacheBehind = !!existing && effectiveSize > existingSize;
|
|
1304
1681
|
const cacheShrankAfterChange = !!existing && effectiveSize < existingSize && changedSinceScan;
|
|
1305
1682
|
const linkedMissingCache = linkedAgentIds.has(sessionId) && !existing;
|
|
1306
1683
|
const missingModel = !!existing && !existing.model_provider;
|
|
1684
|
+
const staleParser = !!existing && (
|
|
1685
|
+
(isCodexRollout && Number(existing.import_parser_version || 0) < CODEX_CONVERSATION_IMPORT_PARSER_VERSION) ||
|
|
1686
|
+
(isWalleTranscript && Number(existing.import_parser_version || 0) < WALLE_SESSION_CACHE_PARSER_VERSION)
|
|
1687
|
+
);
|
|
1307
1688
|
const changedColdFile = changedSinceScan && !existing;
|
|
1308
1689
|
|
|
1309
1690
|
if (
|
|
@@ -1311,18 +1692,20 @@ async function _conversationImportCandidates(allFiles, lastScanAt) {
|
|
|
1311
1692
|
!cacheBehind &&
|
|
1312
1693
|
!cacheShrankAfterChange &&
|
|
1313
1694
|
!linkedMissingCache &&
|
|
1314
|
-
!missingModel
|
|
1695
|
+
!missingModel &&
|
|
1696
|
+
!staleParser
|
|
1315
1697
|
) {
|
|
1316
1698
|
continue;
|
|
1317
1699
|
}
|
|
1318
1700
|
|
|
1319
1701
|
let priority = 6;
|
|
1320
1702
|
if (cacheBehind) priority = 0;
|
|
1321
|
-
else if (
|
|
1322
|
-
else if (
|
|
1323
|
-
else if (
|
|
1324
|
-
else if (changedColdFile) priority = 4;
|
|
1325
|
-
else if (
|
|
1703
|
+
else if (staleParser) priority = 1;
|
|
1704
|
+
else if (linkedMissingCache) priority = 2;
|
|
1705
|
+
else if (cacheShrankAfterChange) priority = 3;
|
|
1706
|
+
else if (hasCompactSibling && changedColdFile) priority = 4;
|
|
1707
|
+
else if (changedColdFile) priority = 5;
|
|
1708
|
+
else if (missingModel) priority = 6;
|
|
1326
1709
|
|
|
1327
1710
|
candidates.push({
|
|
1328
1711
|
filePath,
|
|
@@ -1356,32 +1739,79 @@ async function _conversationImportCandidates(allFiles, lastScanAt) {
|
|
|
1356
1739
|
return candidates;
|
|
1357
1740
|
}
|
|
1358
1741
|
|
|
1359
|
-
|
|
1742
|
+
function _backgroundTranscriptImportMaxBytes() {
|
|
1743
|
+
const raw = Number(process.env.CTM_BACKGROUND_TRANSCRIPT_IMPORT_MAX_BYTES || BACKGROUND_TRANSCRIPT_IMPORT_DEFAULT_BYTES);
|
|
1744
|
+
return Math.max(256 * 1024, Number.isFinite(raw) ? raw : BACKGROUND_TRANSCRIPT_IMPORT_DEFAULT_BYTES);
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
function _codexImportedFileSize(parsedFileSize, prevFileSize, parsedTail) {
|
|
1748
|
+
const fileSize = Math.max(0, Number(parsedFileSize || 0));
|
|
1749
|
+
const prev = Math.max(0, Number(prevFileSize || 0));
|
|
1750
|
+
const base = prev > 0 && fileSize >= prev ? prev : 0;
|
|
1751
|
+
const rawConsumed = Number.isFinite(Number(parsedTail?.completeBytesRead))
|
|
1752
|
+
? Number(parsedTail.completeBytesRead)
|
|
1753
|
+
: Number(parsedTail?.bytesRead || 0);
|
|
1754
|
+
const consumed = Math.max(0, Number.isFinite(rawConsumed) ? rawConsumed : 0);
|
|
1755
|
+
return Math.min(fileSize, base + consumed);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
async function _importCodexSessionFile(parsed, filePath, options = {}) {
|
|
1360
1759
|
const sessionId = parsed.sessionId;
|
|
1361
1760
|
if (!sessionId) return false;
|
|
1362
1761
|
|
|
1363
1762
|
const existing = db.getSessionConversation(sessionId);
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1763
|
+
const parserVersion = CODEX_CONVERSATION_IMPORT_PARSER_VERSION;
|
|
1764
|
+
const parserStale = !!existing && Number(existing.import_parser_version || 0) < parserVersion;
|
|
1765
|
+
if (existing && existing.file_size === parsed.fileSize && existing.model_provider && !parserStale) return false;
|
|
1766
|
+
|
|
1767
|
+
const prevFileSize = parserStale ? 0 : Number(existing?.file_size || 0);
|
|
1768
|
+
// Hard size cap (fail-open): a multi-GB transcript would make the existing-blob
|
|
1769
|
+
// parse + whole-array concat/sort below block the loop and exceed V8's ~512MB
|
|
1770
|
+
// string limit. Over cap ⇒ don't load the full base from the blob; import only
|
|
1771
|
+
// the freshly-read tail (the downstream blob write is also capped to '[]' and
|
|
1772
|
+
// the transcript-store rows carry full history).
|
|
1773
|
+
let _isOverParseCap = null;
|
|
1774
|
+
try { _isOverParseCap = require('./lib/size-cap').isOverParseCap; } catch { /* optional */ }
|
|
1775
|
+
const _overCap = typeof _isOverParseCap === 'function' ? _isOverParseCap(parsed.fileSize) : false;
|
|
1776
|
+
const _loadBase = !_overCap && prevFileSize > 0 && parsed.fileSize > prevFileSize;
|
|
1777
|
+
// Phase 6: load the incremental base from the faithful per-message rows when they're
|
|
1778
|
+
// known-complete (column read, no multi-MB JSON.parse of the blob — and it survives blob
|
|
1779
|
+
// retirement in Phase 7). Falls back to the blob when rows are absent/half-migrated.
|
|
1780
|
+
const _rowsComplete = _loadBase
|
|
1781
|
+
&& typeof db.sessionContentRowsAvailable === 'function'
|
|
1782
|
+
&& db.sessionContentRowsAvailable(sessionId);
|
|
1783
|
+
const baseMessages = _loadBase
|
|
1784
|
+
? (_rowsComplete ? db.getSessionMessagesArray(sessionId, { fallbackToBlob: false }) : _safeParseMessagesJson(existing.messages))
|
|
1372
1785
|
: [];
|
|
1373
|
-
const
|
|
1786
|
+
const indexedMessages = _loadBase ? _loadIndexedSessionMessages(sessionId) : [];
|
|
1787
|
+
const baseSearchMessages = _loadBase
|
|
1374
1788
|
? (indexedMessages.length ? indexedMessages : baseMessages)
|
|
1375
1789
|
: [];
|
|
1376
|
-
const
|
|
1790
|
+
const codexUserDeduper = _codexUserDeduperFromMessages(baseMessages);
|
|
1377
1791
|
|
|
1378
1792
|
const newMessages = [];
|
|
1379
1793
|
let parsedTail;
|
|
1380
1794
|
if (prevFileSize > 0 && parsed.fileSize > prevFileSize) {
|
|
1381
1795
|
const content = await _readFileRange(filePath, prevFileSize, parsed.fileSize - prevFileSize);
|
|
1382
|
-
parsedTail = parseCodexJsonlIntoMessages(content, newMessages, {
|
|
1796
|
+
parsedTail = parseCodexJsonlIntoMessages(content, newMessages, { codexUserDeduper });
|
|
1797
|
+
} else if (options.cooperative) {
|
|
1798
|
+
parsedTail = await parseCodexJsonlFileIntoMessagesAsync(filePath, newMessages, {
|
|
1799
|
+
codexUserDeduper,
|
|
1800
|
+
yieldAfterMs: options.yieldAfterMs || 25,
|
|
1801
|
+
});
|
|
1383
1802
|
} else {
|
|
1384
|
-
parsedTail = parseCodexJsonlFileIntoMessages(filePath, newMessages, {
|
|
1803
|
+
parsedTail = parseCodexJsonlFileIntoMessages(filePath, newMessages, { codexUserDeduper });
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
// Re-import storm guard: codex `importedFileSize` tracks CONSUMED bytes, which can stay below
|
|
1807
|
+
// parsed.fileSize indefinitely (trailing non-message lines / a partial record still being
|
|
1808
|
+
// written), so the candidate selector's `cacheBehind` (effectiveSize > stored file_size) never
|
|
1809
|
+
// converges and this importer is re-run every scan tick. When the grown tail held NO new
|
|
1810
|
+
// messages, the whole-conversation re-stringify + replaceSessionMessages below is pure waste
|
|
1811
|
+
// that saturates the db-owner worker — skip it. The resume offset (stored file_size) is left
|
|
1812
|
+
// unchanged, so a record that only completes later is still picked up on a subsequent pass.
|
|
1813
|
+
if (prevFileSize > 0 && parsed.fileSize > prevFileSize && newMessages.length === 0) {
|
|
1814
|
+
return false;
|
|
1385
1815
|
}
|
|
1386
1816
|
|
|
1387
1817
|
const allMessages = baseMessages.concat(newMessages);
|
|
@@ -1392,6 +1822,8 @@ async function _importCodexSessionFile(parsed, filePath) {
|
|
|
1392
1822
|
const assistantMessages = allMessages.filter(m => m.role === 'assistant' && (m.text || m.content));
|
|
1393
1823
|
if (allMessages.length === 0 || userMessages.length === 0) return false;
|
|
1394
1824
|
|
|
1825
|
+
const importedFileSize = _codexImportedFileSize(parsed.fileSize, prevFileSize, parsedTail);
|
|
1826
|
+
|
|
1395
1827
|
const fileMeta = readCodexRolloutMetadata(filePath) || {};
|
|
1396
1828
|
const meta = parsedTail.sessionMeta || fileMeta || {};
|
|
1397
1829
|
const firstUser = userMessages[0]?.text || userMessages[0]?.content || '';
|
|
@@ -1417,14 +1849,45 @@ async function _importCodexSessionFile(parsed, filePath) {
|
|
|
1417
1849
|
first_assistant_text: firstAssistant.slice(0, 500),
|
|
1418
1850
|
rename_name: existing?.rename_name || '',
|
|
1419
1851
|
git_branch: meta.git_branch || parsed.gitBranch || '',
|
|
1420
|
-
file_size:
|
|
1852
|
+
file_size: importedFileSize,
|
|
1421
1853
|
session_created_at: meta.timestamp || parsed.timestamp || '',
|
|
1422
1854
|
hostname: parsed.hostname,
|
|
1423
1855
|
model_provider: modelProvider,
|
|
1424
1856
|
model_id: model || (existing && existing.model_id) || '',
|
|
1857
|
+
import_parser_version: parserVersion,
|
|
1425
1858
|
});
|
|
1426
1859
|
|
|
1427
1860
|
try {
|
|
1861
|
+
const threadSource = String(meta.thread_source || meta.threadSource || '').trim().toLowerCase();
|
|
1862
|
+
const parentAgentSessionId = String(
|
|
1863
|
+
meta.parent_agent_session_id
|
|
1864
|
+
|| meta.parentAgentSessionId
|
|
1865
|
+
|| meta.parent_thread_id
|
|
1866
|
+
|| meta.parentThreadId
|
|
1867
|
+
|| ''
|
|
1868
|
+
).trim();
|
|
1869
|
+
if (threadSource === 'subagent' && parentAgentSessionId) {
|
|
1870
|
+
db.upsertAgentSessionIdentity(sessionId, {
|
|
1871
|
+
provider: 'codex',
|
|
1872
|
+
providerResumeId: sessionId,
|
|
1873
|
+
cwd: projectPath,
|
|
1874
|
+
projectPath,
|
|
1875
|
+
title,
|
|
1876
|
+
jsonlPath: filePath,
|
|
1877
|
+
fileSize: parsed.fileSize,
|
|
1878
|
+
modifiedAt: parsed.modifiedAt,
|
|
1879
|
+
model,
|
|
1880
|
+
gitBranch: meta.git_branch || parsed.gitBranch || '',
|
|
1881
|
+
hostname: parsed.hostname,
|
|
1882
|
+
userMsgCount: userMessages.length,
|
|
1883
|
+
firstMessage: firstUser.slice(0, 500),
|
|
1884
|
+
threadSource,
|
|
1885
|
+
parentAgentSessionId,
|
|
1886
|
+
agentNickname: meta.agent_nickname || meta.agentNickname || '',
|
|
1887
|
+
agentRole: meta.agent_role || meta.agentRole || '',
|
|
1888
|
+
});
|
|
1889
|
+
return true;
|
|
1890
|
+
}
|
|
1428
1891
|
const owner = db.getDb().prepare('SELECT ctm_session_id FROM agent_sessions WHERE agent_session_id = ?').get(sessionId);
|
|
1429
1892
|
db.upsertSession(owner?.ctm_session_id || sessionId, {
|
|
1430
1893
|
agentSessionId: sessionId,
|
|
@@ -1449,21 +1912,28 @@ async function _importCodexSessionFile(parsed, filePath) {
|
|
|
1449
1912
|
return true;
|
|
1450
1913
|
}
|
|
1451
1914
|
|
|
1452
|
-
function _ingestTranscriptStoreForParsedFile(filePath, parsed) {
|
|
1915
|
+
function _ingestTranscriptStoreForParsedFile(filePath, parsed, options = {}) {
|
|
1916
|
+
// Claude Desktop sessions use a virtual path (`…#ctm-claude-desktop=<uuid>`) that is never a
|
|
1917
|
+
// real file on disk — fs.statSync / ingestJsonlFile always throw ENOENT on it. Their
|
|
1918
|
+
// transcript data lives in the Desktop cache (read via getMessages), not the JSONL transcript
|
|
1919
|
+
// store, so this ingest can only ever fail for them. Skipping removes a guaranteed-failing
|
|
1920
|
+
// synchronous statSync that was firing ~97×/sec in a hot loop on dead desktop sessions.
|
|
1921
|
+
if (claudeDesktopSessions.isVirtualSessionPath(filePath)) return null;
|
|
1453
1922
|
try {
|
|
1454
1923
|
const size = Number(parsed?.fileSize || fs.statSync(filePath).size || 0);
|
|
1455
1924
|
const agentSessionId = String(parsed?.sessionId || transcriptSourceIdFromPath(filePath) || '').trim();
|
|
1456
1925
|
if (!agentSessionId) return null;
|
|
1457
1926
|
const provider = normalizeTranscriptProvider(parsed?.agent || parsed?.modelProvider || '', filePath);
|
|
1458
1927
|
const largeColdMode = size >= TRANSCRIPT_IMPORT_LARGE_FILE_BYTES ? 'tail' : undefined;
|
|
1928
|
+
const maxBytes = Math.max(256 * 1024, Number(options.transcriptMaxBytes || TRANSCRIPT_IMPORT_MAX_BYTES));
|
|
1459
1929
|
const result = ingestJsonlFile(db.getDb(), {
|
|
1460
1930
|
filePath,
|
|
1461
1931
|
agentSessionId,
|
|
1462
1932
|
ctmSessionId: agentSessionId,
|
|
1463
1933
|
provider,
|
|
1464
1934
|
mode: largeColdMode,
|
|
1465
|
-
initialTailBytes:
|
|
1466
|
-
maxBytes: largeColdMode ?
|
|
1935
|
+
initialTailBytes: maxBytes,
|
|
1936
|
+
maxBytes: largeColdMode ? maxBytes : Math.min(size || maxBytes, maxBytes),
|
|
1467
1937
|
});
|
|
1468
1938
|
if ((result.inserted || 0) > 0 || (result.bytesRead || 0) > 0) {
|
|
1469
1939
|
console.log(
|
|
@@ -1478,11 +1948,55 @@ function _ingestTranscriptStoreForParsedFile(filePath, parsed) {
|
|
|
1478
1948
|
}
|
|
1479
1949
|
}
|
|
1480
1950
|
|
|
1481
|
-
async function
|
|
1951
|
+
async function _importWalleSessionFile(parsed, filePath) {
|
|
1952
|
+
const sessionId = String(parsed?.sessionId || '').trim();
|
|
1953
|
+
if (!sessionId) return false;
|
|
1954
|
+
|
|
1955
|
+
const existing = db.getSessionConversation(sessionId);
|
|
1956
|
+
const fileStat = await fsp.stat(filePath).catch(() => null);
|
|
1957
|
+
if (!fileStat || !fileStat.isFile()) return false;
|
|
1958
|
+
const parserStale = !!existing &&
|
|
1959
|
+
Number(existing.import_parser_version || 0) < WALLE_SESSION_CACHE_PARSER_VERSION;
|
|
1960
|
+
if (existing && existing.file_size === fileStat.size && existing.model_provider && !parserStale) {
|
|
1961
|
+
return false;
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
const history = readWalleCtmHistory(filePath);
|
|
1965
|
+
if (!history.some((message) => message && message.role === 'user' && (message.content || message.text))) {
|
|
1966
|
+
return false;
|
|
1967
|
+
}
|
|
1968
|
+
const meta = walleTranscript.readSessionMeta(filePath) || {};
|
|
1969
|
+
const chatSessionId = String(meta.chatSessionId || meta.sessionId || sessionId).trim() || sessionId;
|
|
1970
|
+
const wrote = persistWalleSessionConversation({
|
|
1971
|
+
db,
|
|
1972
|
+
source: {
|
|
1973
|
+
...parsed,
|
|
1974
|
+
ctmSessionId: sessionId,
|
|
1975
|
+
conversationSessionId: sessionId,
|
|
1976
|
+
agentSessionId: sessionId,
|
|
1977
|
+
chatSessionId,
|
|
1978
|
+
jsonlPath: filePath,
|
|
1979
|
+
cwd: meta.cwd || parsed.cwd || parsed.project || '',
|
|
1980
|
+
title: meta.label || parsed.title || '',
|
|
1981
|
+
label: meta.label || parsed.title || '',
|
|
1982
|
+
model_provider: meta.modelProvider || parsed.modelProvider || '',
|
|
1983
|
+
model_id: meta.modelId || parsed.modelId || '',
|
|
1984
|
+
},
|
|
1985
|
+
history,
|
|
1986
|
+
fileStat,
|
|
1987
|
+
hostname: parsed.hostname,
|
|
1988
|
+
});
|
|
1989
|
+
return wrote > 0;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
async function importSessionFile(filePath, projectPath, projectEntry, options = {}) {
|
|
1482
1993
|
const parsed = parseSessionFile(filePath, projectPath, projectEntry);
|
|
1483
|
-
_ingestTranscriptStoreForParsedFile(filePath, parsed);
|
|
1994
|
+
_ingestTranscriptStoreForParsedFile(filePath, parsed, options);
|
|
1484
1995
|
if (parsed.agent === 'codex') {
|
|
1485
|
-
return _importCodexSessionFile(parsed, filePath);
|
|
1996
|
+
return _importCodexSessionFile(parsed, filePath, options);
|
|
1997
|
+
}
|
|
1998
|
+
if (parsed.agent === 'walle') {
|
|
1999
|
+
return _importWalleSessionFile(parsed, filePath);
|
|
1486
2000
|
}
|
|
1487
2001
|
if (parsed.agent === claudeDesktopSessions.DESKTOP_AGENT) {
|
|
1488
2002
|
const messages = claudeDesktopSessions.getMessages(parsed.sessionId) || [];
|
|
@@ -1510,8 +2024,9 @@ async function importSessionFile(filePath, projectPath, projectEntry) {
|
|
|
1510
2024
|
file_size: parsed.fileSize,
|
|
1511
2025
|
session_created_at: parsed.timestamp,
|
|
1512
2026
|
hostname: parsed.hostname,
|
|
1513
|
-
model_provider: parsed.modelProvider || (existing && existing.model_provider) ||
|
|
2027
|
+
model_provider: parsed.modelProvider || (existing && existing.model_provider) || CLAUDE_MODEL_PROVIDER,
|
|
1514
2028
|
model_id: parsed.modelId || (existing && existing.model_id) || '',
|
|
2029
|
+
import_parser_version: DEFAULT_CONVERSATION_IMPORT_PARSER_VERSION,
|
|
1515
2030
|
});
|
|
1516
2031
|
return true;
|
|
1517
2032
|
}
|
|
@@ -1555,8 +2070,16 @@ async function importSessionFile(filePath, projectPath, projectEntry) {
|
|
|
1555
2070
|
} finally {
|
|
1556
2071
|
await fh.close();
|
|
1557
2072
|
}
|
|
1558
|
-
// Carry forward existing parsed messages
|
|
1559
|
-
|
|
2073
|
+
// Carry forward existing parsed messages. Phase 6: load the base from the faithful
|
|
2074
|
+
// per-message rows when known-complete (column read, no multi-MB JSON.parse — and it
|
|
2075
|
+
// survives blob retirement in Phase 7); fall back to the blob when rows aren't ready.
|
|
2076
|
+
try {
|
|
2077
|
+
if (typeof db.sessionContentRowsAvailable === 'function' && db.sessionContentRowsAvailable(parsed.sessionId)) {
|
|
2078
|
+
baseMessages = db.getSessionMessagesArray(parsed.sessionId, { fallbackToBlob: false });
|
|
2079
|
+
} else {
|
|
2080
|
+
baseMessages = JSON.parse(existing.messages || '[]');
|
|
2081
|
+
}
|
|
2082
|
+
} catch {}
|
|
1560
2083
|
baseAssistantCount = existing.assistant_msg_count || 0;
|
|
1561
2084
|
} else {
|
|
1562
2085
|
// Full read for new files or when file shrank (truncated/rotated)
|
|
@@ -1568,7 +2091,11 @@ async function importSessionFile(filePath, projectPath, projectEntry) {
|
|
|
1568
2091
|
searchMessages: newSearchMessages,
|
|
1569
2092
|
firstUserContent: parsedFirstUser, lastUserContent: parsedLastUser,
|
|
1570
2093
|
firstAssistantText: parsedFirstAssistant, renameName: parsedRename,
|
|
1571
|
-
|
|
2094
|
+
summaryTitle: parsedSummaryTitle,
|
|
2095
|
+
} = await _parseConversationContent(content, {
|
|
2096
|
+
fileSessionId: claudeFileSessionId(jsonlPath),
|
|
2097
|
+
fileDir: path.dirname(jsonlPath),
|
|
2098
|
+
});
|
|
1572
2099
|
const allMessages = baseMessages.concat(newMessages);
|
|
1573
2100
|
const indexedMessages = prevFileSize > 0 && parsed.fileSize > prevFileSize
|
|
1574
2101
|
? _loadIndexedSessionMessages(parsed.sessionId)
|
|
@@ -1600,7 +2127,7 @@ async function importSessionFile(filePath, projectPath, projectEntry) {
|
|
|
1600
2127
|
search_messages: allSearchMessages,
|
|
1601
2128
|
user_msg_count: signals.userCount,
|
|
1602
2129
|
assistant_msg_count: signals.assistantCount || baseAssistantCount + newAssistants,
|
|
1603
|
-
title: parsed.title || (existing && existing.title) || '',
|
|
2130
|
+
title: parsed.title || (existing && existing.title) || parsedSummaryTitle || '',
|
|
1604
2131
|
first_message: mergedFirstUser,
|
|
1605
2132
|
last_user_content: mergedLastUser,
|
|
1606
2133
|
first_assistant_text: mergedFirstAssistant,
|
|
@@ -1609,8 +2136,9 @@ async function importSessionFile(filePath, projectPath, projectEntry) {
|
|
|
1609
2136
|
file_size: parsed.fileSize,
|
|
1610
2137
|
session_created_at: parsed.timestamp,
|
|
1611
2138
|
hostname: parsed.hostname,
|
|
1612
|
-
model_provider: parsed.modelProvider || (existing && existing.model_provider) ||
|
|
2139
|
+
model_provider: parsed.modelProvider || (existing && existing.model_provider) || CLAUDE_MODEL_PROVIDER,
|
|
1613
2140
|
model_id: parsed.modelId || (existing && existing.model_id) || '',
|
|
2141
|
+
import_parser_version: DEFAULT_CONVERSATION_IMPORT_PARSER_VERSION,
|
|
1614
2142
|
});
|
|
1615
2143
|
return true;
|
|
1616
2144
|
}
|
|
@@ -1666,7 +2194,10 @@ async function runIncrementalConversationImport() {
|
|
|
1666
2194
|
try {
|
|
1667
2195
|
if (error) throw error;
|
|
1668
2196
|
scanned++;
|
|
1669
|
-
if (await importSessionFile(filePath, projectPath, projectEntry
|
|
2197
|
+
if (await importSessionFile(filePath, projectPath, projectEntry, {
|
|
2198
|
+
cooperative: true,
|
|
2199
|
+
transcriptMaxBytes: _backgroundTranscriptImportMaxBytes(),
|
|
2200
|
+
})) imported++;
|
|
1670
2201
|
const importLimited = imported >= maxImportedPerRun;
|
|
1671
2202
|
const processedLimited = scanned >= maxProcessedPerRun;
|
|
1672
2203
|
if ((importLimited || processedLimited) && scanned < candidates.length) {
|
|
@@ -1708,6 +2239,108 @@ async function runIncrementalConversationImport() {
|
|
|
1708
2239
|
}
|
|
1709
2240
|
}
|
|
1710
2241
|
|
|
2242
|
+
// --- Cursor conversation import ----------------------------------------------
|
|
2243
|
+
// Cursor stores conversations as a content-addressed blob graph in its own SQLite
|
|
2244
|
+
// store (~/.cursor/chats/<ws>/<agent>/store.db), NOT a JSONL append log. Reconstructing
|
|
2245
|
+
// it is expensive (blob-graph BFS + per-blob JSON.parse), so — exactly like the JSONL
|
|
2246
|
+
// importer — import once into session_conversations and let the normal cache read path
|
|
2247
|
+
// serve it, instead of reconstructing on every UI poll (the old design; see the
|
|
2248
|
+
// _orderedBlobRefs CPU-profile finding). Signature-gated (store size:mtime) so an unchanged
|
|
2249
|
+
// store is skipped; the store path is resolved once per session and cached on a CONFIDENT
|
|
2250
|
+
// (agentId) match. Runs on the db-owner worker (off the main event loop) like the JSONL import.
|
|
2251
|
+
const CURSOR_IMPORT_PARSER_VERSION = 1; // bump to force a full cursor re-import on shape change
|
|
2252
|
+
const _cursorImportSignatures = new Map(); // ctm_session_id → last-imported store signature
|
|
2253
|
+
const _cursorResolvedStorePaths = new Map(); // ctm_session_id → resolved store.db path (confident match only)
|
|
2254
|
+
|
|
2255
|
+
async function runCursorConversationImport({ cursorHome } = {}) {
|
|
2256
|
+
let imported = 0;
|
|
2257
|
+
let scanned = 0;
|
|
2258
|
+
let skipped = 0;
|
|
2259
|
+
let failed = 0;
|
|
2260
|
+
let total = 0;
|
|
2261
|
+
let cursorStore;
|
|
2262
|
+
try {
|
|
2263
|
+
cursorStore = require('./lib/cursor-conversation-store');
|
|
2264
|
+
} catch (e) {
|
|
2265
|
+
return { imported, scanned, skipped, failed, total, error: `cursor store unavailable: ${e.message}` };
|
|
2266
|
+
}
|
|
2267
|
+
try {
|
|
2268
|
+
const sessions = db.getDb().prepare(
|
|
2269
|
+
"SELECT id, cwd, project_path, created_at, updated_at FROM ctm_sessions WHERE provider = 'cursor'"
|
|
2270
|
+
).all();
|
|
2271
|
+
total = sessions.length;
|
|
2272
|
+
for (const s of sessions) {
|
|
2273
|
+
try {
|
|
2274
|
+
scanned += 1;
|
|
2275
|
+
const ctmId = String(s.id);
|
|
2276
|
+
const cwd = String(s.cwd || s.project_path || '').trim();
|
|
2277
|
+
const agentRows = db.getDb().prepare(
|
|
2278
|
+
"SELECT agent_session_id FROM agent_sessions WHERE ctm_session_id = ? AND agent_session_id IS NOT NULL AND agent_session_id != ''"
|
|
2279
|
+
).all(ctmId);
|
|
2280
|
+
const agentSessionIds = agentRows.map((r) => String(r.agent_session_id)).filter(Boolean);
|
|
2281
|
+
|
|
2282
|
+
// Resolve the store. If we already have a confident path, gate on its cheap signature
|
|
2283
|
+
// BEFORE the expensive reconstruct; skip when unchanged since the last import.
|
|
2284
|
+
let storeDbPath = _cursorResolvedStorePaths.get(ctmId) || '';
|
|
2285
|
+
if (storeDbPath) {
|
|
2286
|
+
const sig = cursorStore.statSignature(storeDbPath);
|
|
2287
|
+
if (sig && _cursorImportSignatures.get(ctmId) === sig) { skipped += 1; continue; }
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
let store = null;
|
|
2291
|
+
if (storeDbPath) {
|
|
2292
|
+
try { store = cursorStore.loadCursorStore(storeDbPath); } catch { store = null; }
|
|
2293
|
+
}
|
|
2294
|
+
if (!store) {
|
|
2295
|
+
// No cached path (or its load failed): resolve via match. loadCursorStore is itself
|
|
2296
|
+
// size:mtime-cached, so the enumeration is cheap for idle stores.
|
|
2297
|
+
store = cursorStore.loadCursorConversationForSession({
|
|
2298
|
+
cwd, createdAt: s.created_at, updatedAt: s.updated_at, agentSessionIds, cursorHome,
|
|
2299
|
+
});
|
|
2300
|
+
if (store && store.storeDbPath) {
|
|
2301
|
+
storeDbPath = store.storeDbPath;
|
|
2302
|
+
// Persist the resolved path ONLY on a confident (agentId) match — cwd+time alone is
|
|
2303
|
+
// a heuristic that could pin the wrong store.
|
|
2304
|
+
if (store.agentId && agentSessionIds.includes(store.agentId)) {
|
|
2305
|
+
_cursorResolvedStorePaths.set(ctmId, storeDbPath);
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
if (!store || !Array.isArray(store.messages) || store.messages.length === 0) continue;
|
|
2310
|
+
|
|
2311
|
+
const sig = storeDbPath ? cursorStore.statSignature(storeDbPath) : '';
|
|
2312
|
+
if (sig && _cursorImportSignatures.get(ctmId) === sig) { skipped += 1; continue; }
|
|
2313
|
+
|
|
2314
|
+
const fields = cursorStore.cursorStoreToConversationFields(store);
|
|
2315
|
+
db.importSessionConversation({
|
|
2316
|
+
session_id: ctmId,
|
|
2317
|
+
...fields,
|
|
2318
|
+
title: '',
|
|
2319
|
+
git_branch: '',
|
|
2320
|
+
file_size: 0, // cursor has no single jsonl footprint; freshness is gated by `sig`
|
|
2321
|
+
session_created_at: fields.session_created_at || s.created_at || '',
|
|
2322
|
+
hostname: '',
|
|
2323
|
+
import_parser_version: CURSOR_IMPORT_PARSER_VERSION,
|
|
2324
|
+
rename_name: '',
|
|
2325
|
+
});
|
|
2326
|
+
if (sig) _cursorImportSignatures.set(ctmId, sig);
|
|
2327
|
+
imported += 1;
|
|
2328
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
2329
|
+
} catch (e) {
|
|
2330
|
+
failed += 1;
|
|
2331
|
+
console.error(`[cursor-import] ${String(s.id).slice(0, 8)}: ${e.message}`);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
if (imported || failed) {
|
|
2335
|
+
console.log(`[cursor-import] imported ${imported}, skipped ${skipped}, failed ${failed} (scanned ${scanned}/${total})`);
|
|
2336
|
+
}
|
|
2337
|
+
return { imported, scanned, skipped, failed, total };
|
|
2338
|
+
} catch (e) {
|
|
2339
|
+
console.error('[cursor-import] Top-level error:', e.message);
|
|
2340
|
+
return { imported, scanned, skipped, failed, total, error: e.message };
|
|
2341
|
+
}
|
|
2342
|
+
}
|
|
2343
|
+
|
|
1711
2344
|
function handleGetConversation(req, res, url) {
|
|
1712
2345
|
const sessionId = url.pathname.split('/').pop();
|
|
1713
2346
|
const conv = db.getSessionConversation(sessionId);
|
|
@@ -1754,7 +2387,7 @@ async function handleGetSettings(req, res, url) {
|
|
|
1754
2387
|
for (const r of rows) result[r.key] = r.value;
|
|
1755
2388
|
return result;
|
|
1756
2389
|
}
|
|
1757
|
-
const allKeys = ['db_path', 'images_dir', 'default_context_type', 'editor_theme', 'auto_version'];
|
|
2390
|
+
const allKeys = ['db_path', 'images_dir', 'backup_dir', 'default_context_type', 'editor_theme', 'auto_version'];
|
|
1758
2391
|
const result = {};
|
|
1759
2392
|
for (const k of allKeys) result[k] = db.getSetting(k);
|
|
1760
2393
|
return result;
|
|
@@ -1772,7 +2405,11 @@ async function handlePutSettings(req, res) {
|
|
|
1772
2405
|
const changedKeys = await withSqliteBusyRetry(() => {
|
|
1773
2406
|
const keys = [];
|
|
1774
2407
|
for (const [k, v] of Object.entries(data)) {
|
|
1775
|
-
|
|
2408
|
+
if (k === 'backup_dir' && typeof db.setBackupDir === 'function') {
|
|
2409
|
+
db.setBackupDir(v, { persist: true });
|
|
2410
|
+
} else {
|
|
2411
|
+
db.setSetting(k, v);
|
|
2412
|
+
}
|
|
1776
2413
|
keys.push(k);
|
|
1777
2414
|
}
|
|
1778
2415
|
return keys;
|
|
@@ -1909,29 +2546,50 @@ function handleHotkeyUninstall(req, res) {
|
|
|
1909
2546
|
|
|
1910
2547
|
// --- Screenshot (macOS) ---
|
|
1911
2548
|
async function handleScreenshot(req, res) {
|
|
2549
|
+
// Self-describing diagnostics for the "press hotkey / take screenshot → nothing
|
|
2550
|
+
// happens" reports. Logs one [screenshot] line per request with where the time
|
|
2551
|
+
// went (capture vs DB write), the live session count, and the write outcome —
|
|
2552
|
+
// so a stall can be attributed to a blocked loop, a busy lock, or a cancel.
|
|
2553
|
+
const t0 = Date.now();
|
|
2554
|
+
const { sessions, sessionEvents } = require('./server-state');
|
|
2555
|
+
const sessionCount = sessions ? sessions.size : -1;
|
|
2556
|
+
let captureMs = 0;
|
|
2557
|
+
let writeMs = 0;
|
|
1912
2558
|
try {
|
|
1913
2559
|
const { execFile } = require('child_process');
|
|
1914
2560
|
const tmpFile = path.join(db.DEFAULT_IMAGES_DIR, `screenshot-${Date.now()}.png`);
|
|
1915
2561
|
// Use async execFile so the event loop stays alive — this lets the browser
|
|
1916
2562
|
// remain responsive and allows screencapture to work across all monitors.
|
|
2563
|
+
const tCap = Date.now();
|
|
1917
2564
|
await new Promise((resolve, reject) => {
|
|
1918
2565
|
execFile('/usr/sbin/screencapture', ['-i', tmpFile], { timeout: 30000 }, (err) => {
|
|
1919
2566
|
if (err) reject(err); else resolve();
|
|
1920
2567
|
});
|
|
1921
2568
|
});
|
|
1922
|
-
|
|
2569
|
+
captureMs = Date.now() - tCap;
|
|
2570
|
+
try {
|
|
2571
|
+
await fs.promises.access(tmpFile, fs.constants.R_OK);
|
|
2572
|
+
} catch (err) {
|
|
2573
|
+
if (err && err.code !== 'ENOENT') throw err;
|
|
2574
|
+
console.warn(`[screenshot] cancelled — capture=${captureMs}ms sessions=${sessionCount}`);
|
|
1923
2575
|
return jsonResponse(res, 400, { error: 'Screenshot cancelled' });
|
|
1924
2576
|
}
|
|
1925
|
-
const
|
|
1926
|
-
const result = await db.
|
|
2577
|
+
const tWrite = Date.now();
|
|
2578
|
+
const result = await db.saveImageFromFile(0, tmpFile, path.basename(tmpFile), 'image/png', { deleteSource: true });
|
|
2579
|
+
writeMs = Date.now() - tWrite;
|
|
2580
|
+
if (result && result._timing) delete result._timing; // internal timing — don't emit/return it (this path has its own [screenshot] log)
|
|
1927
2581
|
// If triggered by hotkey daemon, notify browser clients to open the editor
|
|
1928
2582
|
const url = new URL(req.url, 'http://localhost');
|
|
1929
2583
|
if (!url.searchParams.get('token')) {
|
|
1930
|
-
const { sessionEvents } = require('./server-state');
|
|
1931
2584
|
sessionEvents.emit('screenshot-captured', result);
|
|
1932
2585
|
}
|
|
2586
|
+
console.warn(`[screenshot] ok id=${result.id} total=${Date.now() - t0}ms capture=${captureMs}ms write=${writeMs}ms sessions=${sessionCount}`);
|
|
1933
2587
|
jsonResponse(res, 201, result);
|
|
1934
|
-
} catch (e) {
|
|
2588
|
+
} catch (e) {
|
|
2589
|
+
const busy = /SQLITE_BUSY|write lock|database is locked/i.test(e && e.message || '');
|
|
2590
|
+
console.warn(`[screenshot] FAILED total=${Date.now() - t0}ms capture=${captureMs}ms write=${writeMs}ms sessions=${sessionCount} busy=${busy} err=${e && e.message}`);
|
|
2591
|
+
jsonResponse(res, 500, { error: e.message });
|
|
2592
|
+
}
|
|
1935
2593
|
}
|
|
1936
2594
|
|
|
1937
2595
|
// --- Backups ---
|
|
@@ -1940,7 +2598,19 @@ function handleListBackups(req, res) {
|
|
|
1940
2598
|
const dbPath = db.getDbPath();
|
|
1941
2599
|
let dbSize = 0;
|
|
1942
2600
|
try { dbSize = require('fs').statSync(dbPath).size; } catch {}
|
|
1943
|
-
|
|
2601
|
+
const backupInfo = typeof db.getBackupDirInfo === 'function'
|
|
2602
|
+
? db.getBackupDirInfo()
|
|
2603
|
+
: { backup_dir: db.getBackupDir() };
|
|
2604
|
+
jsonResponse(res, 200, {
|
|
2605
|
+
backups,
|
|
2606
|
+
db_path: dbPath,
|
|
2607
|
+
data_dir: dbPath ? path.dirname(dbPath) : '',
|
|
2608
|
+
backup_dir: backupInfo.backup_dir || db.getBackupDir(),
|
|
2609
|
+
default_backup_dir: backupInfo.default_backup_dir || '',
|
|
2610
|
+
configured_backup_dir: backupInfo.configured_backup_dir || '',
|
|
2611
|
+
backup_dir_source: backupInfo.source || 'default',
|
|
2612
|
+
db_size: dbSize,
|
|
2613
|
+
});
|
|
1944
2614
|
}
|
|
1945
2615
|
|
|
1946
2616
|
async function handleCreateBackup(req, res) {
|
|
@@ -1972,80 +2642,192 @@ function handleDeleteBackup(req, res, url) {
|
|
|
1972
2642
|
jsonResponse(res, 200, { ok: true });
|
|
1973
2643
|
}
|
|
1974
2644
|
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2645
|
+
function _ctmStorageInfo() {
|
|
2646
|
+
const dbPath = db.getDbPath();
|
|
2647
|
+
const backupInfo = typeof db.getBackupDirInfo === 'function'
|
|
2648
|
+
? db.getBackupDirInfo()
|
|
2649
|
+
: { backup_dir: db.getBackupDir() };
|
|
2650
|
+
let dbSize = 0;
|
|
2651
|
+
try { dbSize = require('fs').statSync(dbPath).size; } catch {}
|
|
2652
|
+
return {
|
|
2653
|
+
service: 'ctm',
|
|
2654
|
+
label: 'CTM',
|
|
2655
|
+
db_path: dbPath,
|
|
2656
|
+
data_dir: dbPath ? path.dirname(dbPath) : '',
|
|
2657
|
+
db_size: dbSize,
|
|
2658
|
+
backup_dir: backupInfo.backup_dir || db.getBackupDir(),
|
|
2659
|
+
default_backup_dir: backupInfo.default_backup_dir || '',
|
|
2660
|
+
configured_backup_dir: backupInfo.configured_backup_dir || '',
|
|
2661
|
+
backup_dir_source: backupInfo.source || 'default',
|
|
2662
|
+
};
|
|
2663
|
+
}
|
|
1982
2664
|
|
|
1983
|
-
|
|
1984
|
-
const
|
|
2665
|
+
async function _requestWalleData(pathname, opts = {}, timeoutMs = 5000) {
|
|
2666
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
2667
|
+
let timer = null;
|
|
2668
|
+
if (controller) timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
2669
|
+
try {
|
|
2670
|
+
const upstream = await walleClient.requestJson(pathname, { ...opts, signal: controller ? controller.signal : null });
|
|
2671
|
+
if (upstream.status < 200 || upstream.status >= 300 || upstream.json?.error) {
|
|
2672
|
+
const err = new Error(upstream.json?.error || upstream.body || `Wall-E returned ${upstream.status}`);
|
|
2673
|
+
err.status = upstream.status;
|
|
2674
|
+
throw err;
|
|
2675
|
+
}
|
|
2676
|
+
return upstream.json?.data || upstream.json || {};
|
|
2677
|
+
} finally {
|
|
2678
|
+
if (timer) clearTimeout(timer);
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
1985
2681
|
|
|
1986
|
-
function
|
|
1987
|
-
|
|
2682
|
+
async function _storageLocations() {
|
|
2683
|
+
const ctm = _ctmStorageInfo();
|
|
2684
|
+
let walle = null;
|
|
2685
|
+
let walleError = '';
|
|
2686
|
+
try {
|
|
2687
|
+
walle = await _requestWalleData('/api/wall-e/storage', {}, 5000);
|
|
2688
|
+
walle.service = 'walle';
|
|
2689
|
+
walle.label = 'Wall-E';
|
|
2690
|
+
} catch (e) {
|
|
2691
|
+
walleError = e.message || String(e);
|
|
2692
|
+
}
|
|
2693
|
+
return { ctm, walle, walle_error: walleError };
|
|
1988
2694
|
}
|
|
1989
2695
|
|
|
1990
|
-
function
|
|
1991
|
-
|
|
2696
|
+
async function handleGetStorageLocations(req, res) {
|
|
2697
|
+
try {
|
|
2698
|
+
const locations = await _storageLocations();
|
|
2699
|
+
jsonResponse(res, 200, { ok: true, ...locations, migration: storageMigration.readActiveMigration() });
|
|
2700
|
+
} catch (e) {
|
|
2701
|
+
jsonResponse(res, 500, { error: e.message });
|
|
2702
|
+
}
|
|
1992
2703
|
}
|
|
1993
2704
|
|
|
1994
|
-
function
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2705
|
+
async function handlePutBackupDirs(req, res) {
|
|
2706
|
+
try {
|
|
2707
|
+
const data = await readBody(req, 64 * 1024);
|
|
2708
|
+
const result = await _applyBackupDirChanges(data);
|
|
2709
|
+
jsonResponse(res, result.ok ? 200 : 207, result);
|
|
2710
|
+
} catch (e) {
|
|
2711
|
+
jsonResponse(res, 400, { error: e.message });
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
async function _applyBackupDirChanges(data = {}) {
|
|
2716
|
+
const moveExisting = data.move_existing !== false && data.moveExisting !== false;
|
|
2717
|
+
const result = { ok: true, ctm: null, walle: null, errors: [] };
|
|
2718
|
+
if (Object.prototype.hasOwnProperty.call(data, 'ctm_backup_dir')) {
|
|
2719
|
+
result.ctm = db.setBackupDir(data.ctm_backup_dir || '', { persist: true, moveExisting });
|
|
2720
|
+
}
|
|
2721
|
+
if (Object.prototype.hasOwnProperty.call(data, 'walle_backup_dir')) {
|
|
2722
|
+
try {
|
|
2723
|
+
result.walle = await _requestWalleData('/api/wall-e/storage/backup-dir', {
|
|
2724
|
+
method: 'PUT',
|
|
2725
|
+
body: { backup_dir: data.walle_backup_dir || '', move_existing: moveExisting },
|
|
2726
|
+
}, 10000);
|
|
2727
|
+
} catch (e) {
|
|
2728
|
+
result.ok = false;
|
|
2729
|
+
result.errors.push({ service: 'walle', error: e.message || String(e) });
|
|
2730
|
+
}
|
|
2731
|
+
}
|
|
2732
|
+
return result;
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2735
|
+
async function _buildPlanFromRequest(data) {
|
|
2736
|
+
const locations = await _storageLocations();
|
|
2737
|
+
return storageMigration.buildStorageMigrationPlan(data || {}, {
|
|
2738
|
+
dbModule: db,
|
|
2739
|
+
ctmCurrent: locations.ctm,
|
|
2740
|
+
walleCurrent: locations.walle || {
|
|
2741
|
+
data_dir: '',
|
|
2742
|
+
db_path: '',
|
|
2743
|
+
backup_dir: '',
|
|
2744
|
+
},
|
|
2001
2745
|
});
|
|
2002
|
-
jsonResponse(res, 200, result);
|
|
2003
2746
|
}
|
|
2004
2747
|
|
|
2005
|
-
function
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2748
|
+
async function handlePreviewStorageMigration(req, res) {
|
|
2749
|
+
try {
|
|
2750
|
+
const data = await readBody(req, 128 * 1024);
|
|
2751
|
+
const plan = await _buildPlanFromRequest(data);
|
|
2752
|
+
jsonResponse(res, 200, { ok: true, plan });
|
|
2753
|
+
} catch (e) {
|
|
2754
|
+
jsonResponse(res, 400, { error: e.message });
|
|
2009
2755
|
}
|
|
2010
|
-
|
|
2011
|
-
const allRules = db.listPermRules();
|
|
2756
|
+
}
|
|
2012
2757
|
|
|
2758
|
+
async function handleApplyStorageMigration(req, res) {
|
|
2759
|
+
try {
|
|
2760
|
+
const data = await readBody(req, 128 * 1024);
|
|
2761
|
+
if (data.confirm !== true) return jsonResponse(res, 400, { error: 'confirmation_required' });
|
|
2762
|
+
const plan = await _buildPlanFromRequest(data);
|
|
2763
|
+
if (!plan.requires_restart) {
|
|
2764
|
+
const services = data.services || {};
|
|
2765
|
+
const backupInput = {
|
|
2766
|
+
move_existing: services.ctm?.move_backups !== false && services.walle?.move_backups !== false,
|
|
2767
|
+
};
|
|
2768
|
+
if (services.ctm && (Object.prototype.hasOwnProperty.call(services.ctm, 'backup_dir') || Object.prototype.hasOwnProperty.call(services.ctm, 'backupDir'))) {
|
|
2769
|
+
backupInput.ctm_backup_dir = services.ctm.backup_dir || services.ctm.backupDir || '';
|
|
2770
|
+
}
|
|
2771
|
+
if (services.walle && (Object.prototype.hasOwnProperty.call(services.walle, 'backup_dir') || Object.prototype.hasOwnProperty.call(services.walle, 'backupDir'))) {
|
|
2772
|
+
backupInput.walle_backup_dir = services.walle.backup_dir || services.walle.backupDir || '';
|
|
2773
|
+
}
|
|
2774
|
+
const backup = await _applyBackupDirChanges(backupInput);
|
|
2775
|
+
return jsonResponse(res, backup.ok ? 200 : 207, { ok: backup.ok, plan, backup });
|
|
2776
|
+
}
|
|
2777
|
+
const started = storageMigration.spawnStorageMigrationSupervisor(plan);
|
|
2778
|
+
jsonResponse(res, 202, { ok: true, plan, migration: started });
|
|
2779
|
+
} catch (e) {
|
|
2780
|
+
jsonResponse(res, 400, { error: e.message });
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
|
|
2784
|
+
// ============================================================
|
|
2785
|
+
// Tool Permissions — CTM's standalone, global permission config.
|
|
2786
|
+
// ============================================================
|
|
2787
|
+
// CTM permissions live ONLY in the CTM DB (perm_rules table) and are global
|
|
2788
|
+
// (apply to all projects/sessions). They are intentionally decoupled from
|
|
2789
|
+
// Claude Code / Codex settings files — CTM no longer reads or writes
|
|
2790
|
+
// ~/.claude/settings*.json, project .claude/settings.json, or ~/.claude.json.
|
|
2791
|
+
// The shadow approver (approval-agent.js + lib/permission-match.js) enforces them.
|
|
2792
|
+
|
|
2793
|
+
function handleGetProjects(req, res) {
|
|
2794
|
+
// CTM permissions are global — no per-project scoping, no Claude config read.
|
|
2795
|
+
jsonResponse(res, 200, []);
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
async function handleGetToolPermRules(req, res) {
|
|
2799
|
+
// CTM permissions are a standalone GLOBAL config stored only in the CTM DB —
|
|
2800
|
+
// no reading from Claude/Codex settings files. Every rule is global.
|
|
2801
|
+
const allRules = db.listPermRules();
|
|
2013
2802
|
const rules = [];
|
|
2014
2803
|
const denyRules = [];
|
|
2015
|
-
const projectSet = new Set();
|
|
2016
|
-
|
|
2017
2804
|
for (const r of allRules) {
|
|
2018
|
-
const entry = { scope:
|
|
2019
|
-
if (r.list_type === '
|
|
2020
|
-
else
|
|
2021
|
-
if (r.project !== '__global__') projectSet.add(r.project);
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
// Also include projects from ~/.claude.json that might not have rules
|
|
2025
|
-
const claudeJson = readJsonFile(CLAUDE_JSON_PATH);
|
|
2026
|
-
for (const p of Object.keys(claudeJson.projects || {})) {
|
|
2027
|
-
projectSet.add(p);
|
|
2805
|
+
const entry = { scope: 'global', project: '__global__', rule: r.rule };
|
|
2806
|
+
if (r.list_type === 'deny') denyRules.push(entry);
|
|
2807
|
+
else rules.push(entry);
|
|
2028
2808
|
}
|
|
2029
|
-
|
|
2030
|
-
jsonResponse(res, 200, { rules, denyRules, projects: Array.from(projectSet) });
|
|
2809
|
+
jsonResponse(res, 200, { rules, denyRules, projects: [] });
|
|
2031
2810
|
}
|
|
2032
2811
|
|
|
2033
2812
|
async function handleSetToolPermRules(req, res) {
|
|
2034
2813
|
try {
|
|
2035
2814
|
const body = await readBody(req);
|
|
2036
|
-
const { action, rule,
|
|
2815
|
+
const { action, rule, listType } = body;
|
|
2037
2816
|
const lt = listType === 'deny' ? 'deny' : 'allow';
|
|
2038
|
-
const proj = project || '__global__';
|
|
2039
|
-
const scope = proj === '__global__' ? 'global' : 'project';
|
|
2040
2817
|
|
|
2041
2818
|
if (action === 'add') {
|
|
2042
|
-
|
|
2819
|
+
// Permission policy: rules apply globally to ALL projects. Every add is
|
|
2820
|
+
// stored as a global rule (~/.claude/settings.json) regardless of the UI
|
|
2821
|
+
// scope, so a permission granted once is honored everywhere.
|
|
2822
|
+
db.addPermRule({ rule, listType: lt, scope: 'global', project: '__global__' });
|
|
2043
2823
|
} else if (action === 'remove') {
|
|
2044
|
-
|
|
2824
|
+
// Rules are global; remove the global row (legacy per-project rows are
|
|
2825
|
+
// collapsed to global by the db migration).
|
|
2826
|
+
db.removePermRule({ rule, listType: lt, project: '__global__' });
|
|
2045
2827
|
}
|
|
2046
2828
|
|
|
2047
|
-
//
|
|
2048
|
-
|
|
2829
|
+
// CTM permissions are a standalone config in the CTM DB — intentionally NOT
|
|
2830
|
+
// synced to Claude/Codex settings files. The shadow approver enforces them.
|
|
2049
2831
|
|
|
2050
2832
|
jsonResponse(res, 200, { ok: true });
|
|
2051
2833
|
} catch (e) {
|
|
@@ -2582,20 +3364,21 @@ function handleListQueues(req, res) {
|
|
|
2582
3364
|
async function handleCreateQueue(req, res) {
|
|
2583
3365
|
try {
|
|
2584
3366
|
const body = await readBody(req);
|
|
2585
|
-
const { sessionId, mode, items, idleTimeoutMs, autoStart } = body;
|
|
3367
|
+
const { sessionId, mode, items, idleTimeoutMs, autoStart, append, strategy } = body;
|
|
2586
3368
|
if (!sessionId || !Array.isArray(items) || items.length === 0) {
|
|
2587
3369
|
return jsonResponse(res, 400, { error: 'sessionId and non-empty items[] required' });
|
|
2588
3370
|
}
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
3371
|
+
const appendMode = append === true || strategy === 'append';
|
|
3372
|
+
let state = appendMode
|
|
3373
|
+
? queueEngine.appendItems(sessionId, { mode, items, idleTimeoutMs, autoStart })
|
|
3374
|
+
: queueEngine.createQueue(sessionId, { mode, items, idleTimeoutMs });
|
|
3375
|
+
if (!appendMode && autoStart) state = queueEngine.start(sessionId) || state;
|
|
2593
3376
|
jsonResponse(res, 201, state);
|
|
2594
3377
|
} catch (e) { jsonResponse(res, 400, { error: e.message }); }
|
|
2595
3378
|
}
|
|
2596
3379
|
|
|
2597
3380
|
function handleGetQueue(req, res, sessionId) {
|
|
2598
|
-
const state = queueEngine.getState(sessionId);
|
|
3381
|
+
const state = queueEngine.getState(sessionId) || queueEngine.getPersistedState(sessionId);
|
|
2599
3382
|
if (!state) {
|
|
2600
3383
|
res.writeHead(204);
|
|
2601
3384
|
res.end();
|
|
@@ -2615,8 +3398,23 @@ async function handleQueueAction(req, res, sessionId, action) {
|
|
|
2615
3398
|
case 'start': state = queueEngine.start(sessionId); break;
|
|
2616
3399
|
case 'pause': state = queueEngine.pause(sessionId); break;
|
|
2617
3400
|
case 'resume': state = queueEngine.resume(sessionId); break;
|
|
2618
|
-
case 'next': state = queueEngine.
|
|
3401
|
+
case 'next': state = queueEngine.wake(sessionId, 'manual-next'); break;
|
|
2619
3402
|
case 'skip': state = queueEngine.skip(sessionId); break;
|
|
3403
|
+
case 'remove': {
|
|
3404
|
+
try {
|
|
3405
|
+
const body = await readBody(req);
|
|
3406
|
+
state = queueEngine.removeItem(sessionId, String(body.itemId || body.id || ''));
|
|
3407
|
+
} catch (e) { return jsonResponse(res, 400, { error: e.message }); }
|
|
3408
|
+
break;
|
|
3409
|
+
}
|
|
3410
|
+
case 'reorder': {
|
|
3411
|
+
try {
|
|
3412
|
+
const body = await readBody(req);
|
|
3413
|
+
const ids = Array.isArray(body.order) ? body.order : (Array.isArray(body.ids) ? body.ids : []);
|
|
3414
|
+
state = queueEngine.reorderItems(sessionId, ids.map(String));
|
|
3415
|
+
} catch (e) { return jsonResponse(res, 400, { error: e.message }); }
|
|
3416
|
+
break;
|
|
3417
|
+
}
|
|
2620
3418
|
case 'stop': state = queueEngine.stop(sessionId); break;
|
|
2621
3419
|
case 'mode': {
|
|
2622
3420
|
try {
|
|
@@ -2626,7 +3424,13 @@ async function handleQueueAction(req, res, sessionId, action) {
|
|
|
2626
3424
|
break;
|
|
2627
3425
|
}
|
|
2628
3426
|
}
|
|
2629
|
-
if (!state)
|
|
3427
|
+
if (!state) {
|
|
3428
|
+
const persisted = queueEngine.getPersistedState(sessionId);
|
|
3429
|
+
if (action === 'next' && persisted && persisted.status === 'done') {
|
|
3430
|
+
return jsonResponse(res, 200, persisted);
|
|
3431
|
+
}
|
|
3432
|
+
return jsonResponse(res, 404, { error: 'No queue for this session' });
|
|
3433
|
+
}
|
|
2630
3434
|
jsonResponse(res, 200, state);
|
|
2631
3435
|
}
|
|
2632
3436
|
|
|
@@ -2664,6 +3468,35 @@ function handleDeleteQueueLinkedItems(req, res, promptId) {
|
|
|
2664
3468
|
|
|
2665
3469
|
// --- Queue Draft (per-session builder state persisted to DB) ---
|
|
2666
3470
|
|
|
3471
|
+
function queueDraftStatusTimestamp(value) {
|
|
3472
|
+
const n = Number(value);
|
|
3473
|
+
return Number.isFinite(n) && n > 0 ? n : 0;
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
function mergeQueueDraftItemsByStatusTimestamp(existingItems, incomingItems) {
|
|
3477
|
+
const incoming = Array.isArray(incomingItems) ? incomingItems : [];
|
|
3478
|
+
const existing = Array.isArray(existingItems) ? existingItems : [];
|
|
3479
|
+
const existingById = new Map();
|
|
3480
|
+
for (const item of existing) {
|
|
3481
|
+
if (item && item.id) existingById.set(item.id, item);
|
|
3482
|
+
}
|
|
3483
|
+
return incoming.map((item) => {
|
|
3484
|
+
if (!item || typeof item !== 'object') return item;
|
|
3485
|
+
const previous = item.id ? existingById.get(item.id) : null;
|
|
3486
|
+
if (!previous) return item;
|
|
3487
|
+
const previousStatusAt = queueDraftStatusTimestamp(previous.statusUpdatedAt);
|
|
3488
|
+
const incomingStatusAt = queueDraftStatusTimestamp(item.statusUpdatedAt);
|
|
3489
|
+
if (previousStatusAt > incomingStatusAt && previous.status) {
|
|
3490
|
+
return {
|
|
3491
|
+
...item,
|
|
3492
|
+
status: previous.status,
|
|
3493
|
+
statusUpdatedAt: previous.statusUpdatedAt,
|
|
3494
|
+
};
|
|
3495
|
+
}
|
|
3496
|
+
return item;
|
|
3497
|
+
});
|
|
3498
|
+
}
|
|
3499
|
+
|
|
2667
3500
|
function handleGetQueueDraft(req, res, sessionId) {
|
|
2668
3501
|
const key = 'queue_draft_' + sessionId;
|
|
2669
3502
|
const draft = db.getSetting(key, { items: [], mode: 'manual' });
|
|
@@ -2675,7 +3508,10 @@ async function handleSaveQueueDraft(req, res, sessionId) {
|
|
|
2675
3508
|
const body = await readBody(req);
|
|
2676
3509
|
const key = 'queue_draft_' + sessionId;
|
|
2677
3510
|
const existing = db.getSetting(key, { items: [], mode: 'manual' });
|
|
2678
|
-
if (body.items !== undefined)
|
|
3511
|
+
if (body.items !== undefined) {
|
|
3512
|
+
existing.items = mergeQueueDraftItemsByStatusTimestamp(existing.items, body.items);
|
|
3513
|
+
if (body.savedAt !== undefined) existing.savedAt = body.savedAt;
|
|
3514
|
+
}
|
|
2679
3515
|
if (body.mode !== undefined) existing.mode = body.mode;
|
|
2680
3516
|
db.setSetting(key, existing);
|
|
2681
3517
|
jsonResponse(res, 200, { ok: true });
|
|
@@ -2697,6 +3533,94 @@ function handleListApprovalRules(req, res) {
|
|
|
2697
3533
|
});
|
|
2698
3534
|
}
|
|
2699
3535
|
|
|
3536
|
+
// Grouping key for an escalation row: prefer the recorded signature; for legacy
|
|
3537
|
+
// rows (recorded before signatures were stored) derive it from the captured
|
|
3538
|
+
// context via the same helper the approver uses, so old + new rows group together.
|
|
3539
|
+
function _escalationKeyFn(row) {
|
|
3540
|
+
if (row && row.command_signature) return row.command_signature;
|
|
3541
|
+
try {
|
|
3542
|
+
return approvalAgent.escalationCommandParts({
|
|
3543
|
+
toolName: row && row.tool_name,
|
|
3544
|
+
command: '',
|
|
3545
|
+
fullContext: row && row.full_context,
|
|
3546
|
+
}).signature;
|
|
3547
|
+
} catch { return ''; }
|
|
3548
|
+
}
|
|
3549
|
+
|
|
3550
|
+
// GET /api/approval-escalations — escalated commands grouped by TYPE for the
|
|
3551
|
+
// Permission "Needs Review" surface (collapses a huge pile into a few reviewable
|
|
3552
|
+
// types, secrets masked).
|
|
3553
|
+
function handleListEscalations(req, res) {
|
|
3554
|
+
try {
|
|
3555
|
+
const rows = db.getPendingEscalations() || [];
|
|
3556
|
+
const groups = escalationReview.groupEscalations(rows, _escalationKeyFn);
|
|
3557
|
+
jsonResponse(res, 200, { groups, total: rows.length, typeCount: groups.length });
|
|
3558
|
+
} catch (e) {
|
|
3559
|
+
console.error('[escalations] list error:', e.message);
|
|
3560
|
+
jsonResponse(res, 500, { error: e.message });
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
// POST /api/approval-escalations/resolve — resolve a whole TYPE at once.
|
|
3565
|
+
// body: { action: 'whitelist'|'block'|'dismiss'|'dismiss-all', ids:[...], rule? }
|
|
3566
|
+
// whitelist/block create an authoritative perm_rules allow/deny (honored by the
|
|
3567
|
+
// approver + shown in the rule list); all matching escalated rows are cleared.
|
|
3568
|
+
async function handleResolveEscalations(req, res) {
|
|
3569
|
+
try {
|
|
3570
|
+
const body = await readBody(req);
|
|
3571
|
+
const action = String(body.action || '').toLowerCase();
|
|
3572
|
+
const rule = String(body.rule || '').trim();
|
|
3573
|
+
if (!['whitelist', 'block', 'dismiss', 'dismiss-all', 'approve-all'].includes(action)) {
|
|
3574
|
+
return jsonResponse(res, 400, { error: 'invalid_action' });
|
|
3575
|
+
}
|
|
3576
|
+
// Bulk approve: whitelist EVERY pending type (one allow rule per group from its
|
|
3577
|
+
// suggested narrow pattern) and clear the queue. Re-group server-side so the
|
|
3578
|
+
// action is atomic and can't be skewed by a stale client view.
|
|
3579
|
+
if (action === 'approve-all') {
|
|
3580
|
+
const rows = db.getPendingEscalations() || [];
|
|
3581
|
+
const groups = escalationReview.groupEscalations(rows, _escalationKeyFn);
|
|
3582
|
+
let resolved = 0;
|
|
3583
|
+
let rulesCreated = 0;
|
|
3584
|
+
for (const g of groups) {
|
|
3585
|
+
const r = String(g.suggestedRule || '').trim();
|
|
3586
|
+
if (/^[A-Za-z]+\([^)]*\)$/.test(r)) {
|
|
3587
|
+
try {
|
|
3588
|
+
db.addPermRule({ rule: r, listType: 'allow', scope: 'global', project: '__global__' });
|
|
3589
|
+
rulesCreated += 1;
|
|
3590
|
+
} catch (e) { /* rule may already exist — still clear the rows below */ }
|
|
3591
|
+
}
|
|
3592
|
+
for (const id of (g.ids || [])) {
|
|
3593
|
+
try { db.resolveApprovalDecision(id, 'approved'); resolved += 1; } catch (e) { /* skip */ }
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
return jsonResponse(res, 200, { ok: true, action, resolved, rulesCreated, typeCount: groups.length });
|
|
3597
|
+
}
|
|
3598
|
+
let createdRule = null;
|
|
3599
|
+
if (action === 'whitelist' || action === 'block') {
|
|
3600
|
+
if (!/^[A-Za-z]+\([^)]*\)$/.test(rule)) {
|
|
3601
|
+
return jsonResponse(res, 400, { error: 'rule must look like Bash(cmd:*)' });
|
|
3602
|
+
}
|
|
3603
|
+
db.addPermRule({ rule, listType: action === 'whitelist' ? 'allow' : 'deny', scope: 'global', project: '__global__' });
|
|
3604
|
+
createdRule = rule;
|
|
3605
|
+
}
|
|
3606
|
+
let targetIds;
|
|
3607
|
+
if (action === 'dismiss-all') {
|
|
3608
|
+
targetIds = (db.getPendingEscalations() || []).map((r) => r.id);
|
|
3609
|
+
} else {
|
|
3610
|
+
targetIds = (Array.isArray(body.ids) ? body.ids : []).map(Number).filter(Number.isFinite);
|
|
3611
|
+
if (!targetIds.length) return jsonResponse(res, 400, { error: 'ids_required' });
|
|
3612
|
+
}
|
|
3613
|
+
const resolveDecision = action === 'block' ? 'denied' : action === 'whitelist' ? 'approved' : 'dismissed';
|
|
3614
|
+
let resolved = 0;
|
|
3615
|
+
for (const id of targetIds) {
|
|
3616
|
+
try { db.resolveApprovalDecision(id, resolveDecision); resolved += 1; } catch (e) { /* skip */ }
|
|
3617
|
+
}
|
|
3618
|
+
jsonResponse(res, 200, { ok: true, action, resolved, rule: createdRule });
|
|
3619
|
+
} catch (e) {
|
|
3620
|
+
jsonResponse(res, 400, { error: e.message });
|
|
3621
|
+
}
|
|
3622
|
+
}
|
|
3623
|
+
|
|
2700
3624
|
async function handleUpsertApprovalRule(req, res) {
|
|
2701
3625
|
try {
|
|
2702
3626
|
const body = await readBody(req);
|
|
@@ -2750,23 +3674,84 @@ async function handleResolveApprovalDecision(req, res, id) {
|
|
|
2750
3674
|
} catch (e) { jsonResponse(res, 400, { error: e.message }); }
|
|
2751
3675
|
}
|
|
2752
3676
|
|
|
2753
|
-
// --- Dangerous-command blocklist (
|
|
2754
|
-
// GET /api/approval/blocklist
|
|
2755
|
-
//
|
|
3677
|
+
// --- Dangerous-command blocklist (configurable; default ON) ---
|
|
3678
|
+
// GET /api/approval/blocklist
|
|
3679
|
+
// -> { enabled, patterns: [defaults], disabledIds: [int], custom: [{source,flags,reason,category,id}] }
|
|
3680
|
+
// POST /api/approval/blocklist { enabled?, disabledIds?, customPatterns? }
|
|
3681
|
+
// -> the same shape as GET (the merged view). Any field omitted is left as-is.
|
|
3682
|
+
// Custom patterns are validated server-side; an invalid one returns 400 and
|
|
3683
|
+
// nothing is persisted. The hard floor is editable but never silently broken.
|
|
3684
|
+
//
|
|
3685
|
+
// Persistence: `approval_blocklist_enabled` (bool) + `approval_blocklist_config`
|
|
3686
|
+
// ({ disabledIds:[int], customPatterns:[{source,flags,reason,category}] }).
|
|
3687
|
+
// The agent reads both on every check (approval-agent isBlocklistEnabled /
|
|
3688
|
+
// getBlocklistConfig), so edits take effect without a restart.
|
|
3689
|
+
function _blocklistView() {
|
|
3690
|
+
const { PATTERN_META } = require('./workers/approval-blocklist');
|
|
3691
|
+
const enabled = db.getSetting('approval_blocklist_enabled', true) !== false;
|
|
3692
|
+
const cfg = db.getSetting('approval_blocklist_config', null) || {};
|
|
3693
|
+
const disabledIds = Array.isArray(cfg.disabledIds)
|
|
3694
|
+
? cfg.disabledIds.filter((n) => Number.isInteger(n) && n >= 0 && n < PATTERN_META.length)
|
|
3695
|
+
: [];
|
|
3696
|
+
const custom = Array.isArray(cfg.customPatterns)
|
|
3697
|
+
? cfg.customPatterns.map((p, i) => ({
|
|
3698
|
+
id: (p && p.id != null) ? p.id : `c${i}`,
|
|
3699
|
+
source: String((p && p.source) || ''),
|
|
3700
|
+
flags: String((p && p.flags) || ''),
|
|
3701
|
+
reason: String((p && p.reason) || 'Custom blocklist pattern'),
|
|
3702
|
+
category: String((p && p.category) || 'custom'),
|
|
3703
|
+
}))
|
|
3704
|
+
: [];
|
|
3705
|
+
return { enabled, patterns: PATTERN_META, disabledIds, custom };
|
|
3706
|
+
}
|
|
2756
3707
|
function handleGetBlocklist(_req, res) {
|
|
2757
3708
|
try {
|
|
2758
|
-
|
|
2759
|
-
const enabled = !!db.getSetting('approval_blocklist_enabled', false);
|
|
2760
|
-
jsonResponse(res, 200, { enabled, patterns: PATTERN_META });
|
|
3709
|
+
jsonResponse(res, 200, _blocklistView());
|
|
2761
3710
|
} catch (e) { jsonResponse(res, 500, { error: e.message }); }
|
|
2762
3711
|
}
|
|
2763
3712
|
async function handleSetBlocklistEnabled(req, res) {
|
|
2764
3713
|
try {
|
|
2765
|
-
const
|
|
2766
|
-
const
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
3714
|
+
const { validateUserPattern, PATTERN_META } = require('./workers/approval-blocklist');
|
|
3715
|
+
const body = await readBody(req) || {};
|
|
3716
|
+
|
|
3717
|
+
if (Object.prototype.hasOwnProperty.call(body, 'enabled')) {
|
|
3718
|
+
db.setSetting('approval_blocklist_enabled', !!body.enabled);
|
|
3719
|
+
console.log(`[approval-blocklist] enabled=${!!body.enabled}`);
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
const touchesConfig = Object.prototype.hasOwnProperty.call(body, 'disabledIds')
|
|
3723
|
+
|| Object.prototype.hasOwnProperty.call(body, 'customPatterns');
|
|
3724
|
+
if (touchesConfig) {
|
|
3725
|
+
const cfg = db.getSetting('approval_blocklist_config', null) || {};
|
|
3726
|
+
|
|
3727
|
+
if (Object.prototype.hasOwnProperty.call(body, 'disabledIds')) {
|
|
3728
|
+
if (!Array.isArray(body.disabledIds)) return jsonResponse(res, 400, { error: 'disabledIds must be an array of pattern ids' });
|
|
3729
|
+
const ids = [];
|
|
3730
|
+
for (const raw of body.disabledIds) {
|
|
3731
|
+
const n = Number(raw);
|
|
3732
|
+
if (!Number.isInteger(n) || n < 0 || n >= PATTERN_META.length) return jsonResponse(res, 400, { error: `invalid pattern id: ${raw}` });
|
|
3733
|
+
if (!ids.includes(n)) ids.push(n);
|
|
3734
|
+
}
|
|
3735
|
+
cfg.disabledIds = ids;
|
|
3736
|
+
}
|
|
3737
|
+
|
|
3738
|
+
if (Object.prototype.hasOwnProperty.call(body, 'customPatterns')) {
|
|
3739
|
+
if (!Array.isArray(body.customPatterns)) return jsonResponse(res, 400, { error: 'customPatterns must be an array' });
|
|
3740
|
+
if (body.customPatterns.length > 200) return jsonResponse(res, 400, { error: 'too many custom patterns (max 200)' });
|
|
3741
|
+
const normalized = [];
|
|
3742
|
+
for (let i = 0; i < body.customPatterns.length; i++) {
|
|
3743
|
+
const v = validateUserPattern(body.customPatterns[i]);
|
|
3744
|
+
if (!v.ok) return jsonResponse(res, 400, { error: `pattern ${i + 1}: ${v.error}` });
|
|
3745
|
+
normalized.push(v.normalized); // { source, flags, reason, category }
|
|
3746
|
+
}
|
|
3747
|
+
cfg.customPatterns = normalized;
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
db.setSetting('approval_blocklist_config', cfg);
|
|
3751
|
+
console.log(`[approval-blocklist] config updated: ${(cfg.disabledIds || []).length} disabled, ${(cfg.customPatterns || []).length} custom`);
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
jsonResponse(res, 200, _blocklistView());
|
|
2770
3755
|
} catch (e) { jsonResponse(res, 400, { error: e.message }); }
|
|
2771
3756
|
}
|
|
2772
3757
|
|
|
@@ -3049,22 +4034,53 @@ function handlePromptQuality(req, res, url) {
|
|
|
3049
4034
|
jsonResponse(res, 200, harvest.assessPromptQuality(text));
|
|
3050
4035
|
}
|
|
3051
4036
|
|
|
3052
|
-
|
|
4037
|
+
// Injected by server.js: run the list query on the read-pool (off the main loop)
|
|
4038
|
+
// and resolve to the executions array, or null when the pool is unavailable so we
|
|
4039
|
+
// fall back to the main-thread query. SELECT * with a large LIMIT blocked the loop
|
|
4040
|
+
// ~3.9s on the request path per a CPU profile.
|
|
4041
|
+
let _promptExecutionsOffThread = null;
|
|
4042
|
+
function setPromptExecutionsOffThread(fn) { _promptExecutionsOffThread = typeof fn === 'function' ? fn : null; }
|
|
4043
|
+
|
|
4044
|
+
async function handleListExecutions(req, res, url) {
|
|
4045
|
+
const opts = {
|
|
4046
|
+
limit: url.searchParams.get('limit'),
|
|
4047
|
+
role: url.searchParams.get('role') || null,
|
|
4048
|
+
project: url.searchParams.get('project') || null,
|
|
4049
|
+
after: url.searchParams.get('after') || url.searchParams.get('since') || null,
|
|
4050
|
+
order: url.searchParams.get('order') || 'desc',
|
|
4051
|
+
};
|
|
3053
4052
|
try {
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
if (role) { sql += ' AND role = ?'; params.push(role); }
|
|
3061
|
-
if (project) { sql += ' AND project_path LIKE ?'; params.push('%' + project + '%'); }
|
|
3062
|
-
sql += ' ORDER BY executed_at DESC LIMIT ?';
|
|
3063
|
-
params.push(limit);
|
|
3064
|
-
jsonResponse(res, 200, { executions: d.prepare(sql).all(...params) });
|
|
4053
|
+
if (_promptExecutionsOffThread) {
|
|
4054
|
+
const executions = await _promptExecutionsOffThread(opts);
|
|
4055
|
+
if (executions) return jsonResponse(res, 200, { executions });
|
|
4056
|
+
// null → pool unavailable; fall through to the main-thread query.
|
|
4057
|
+
}
|
|
4058
|
+
jsonResponse(res, 200, { executions: queryPromptExecutions(db.getDb(), opts) });
|
|
3065
4059
|
} catch (e) { jsonResponse(res, 200, { executions: [] }); }
|
|
3066
4060
|
}
|
|
3067
4061
|
|
|
4062
|
+
function handlePromptExecutionStats(req, res) {
|
|
4063
|
+
try {
|
|
4064
|
+
const d = db.getDb();
|
|
4065
|
+
const totalSessions = d.prepare('SELECT COUNT(DISTINCT session_id) AS count FROM prompt_executions').get().count;
|
|
4066
|
+
const totalPairs = d.prepare(`
|
|
4067
|
+
SELECT COUNT(*) AS count FROM prompt_executions u
|
|
4068
|
+
JOIN prompt_executions a ON a.session_id = u.session_id AND a.message_index = u.message_index + 1
|
|
4069
|
+
WHERE u.role = 'user' AND a.role = 'assistant'
|
|
4070
|
+
AND length(u.message_text) >= 20 AND length(a.message_text) >= 20
|
|
4071
|
+
`).get().count;
|
|
4072
|
+
const multiTurnSessions = d.prepare(`
|
|
4073
|
+
SELECT COUNT(*) AS count FROM (
|
|
4074
|
+
SELECT session_id FROM prompt_executions WHERE role = 'user'
|
|
4075
|
+
GROUP BY session_id HAVING COUNT(*) >= 2
|
|
4076
|
+
)
|
|
4077
|
+
`).get().count;
|
|
4078
|
+
jsonResponse(res, 200, { totalSessions, totalPairs, multiTurnSessions });
|
|
4079
|
+
} catch (e) {
|
|
4080
|
+
jsonResponse(res, 200, { totalSessions: 0, totalPairs: 0, multiTurnSessions: 0, error: e.message });
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
|
|
3068
4084
|
function handleSessionExecutions(req, res, sessionId) {
|
|
3069
4085
|
try {
|
|
3070
4086
|
const result = harvest.getPromptsForSession(sessionId);
|
|
@@ -3203,30 +4219,28 @@ async function handleImprovePrompt(req, res) {
|
|
|
3203
4219
|
|
|
3204
4220
|
function handleSkillAutocomplete(req, res, url) {
|
|
3205
4221
|
try {
|
|
3206
|
-
const query =
|
|
4222
|
+
const query = url.searchParams.get('q') || '';
|
|
3207
4223
|
const agent = normalizeSkillAutocompleteAgent(url.searchParams.get('agent') || url.searchParams.get('agentType'));
|
|
3208
4224
|
const cwd = url.searchParams.get('cwd') || '';
|
|
3209
4225
|
const includeSessionSeen = !agent || agent === 'all' || url.searchParams.get('includeSession') === '1';
|
|
3210
4226
|
const skills = buildSkillAutocompleteItems({ cwd, agent, includeSessionSeen });
|
|
3211
4227
|
|
|
3212
|
-
|
|
3213
|
-
if (query) {
|
|
3214
|
-
filtered = skills.filter(s => fuzzySkillMatch(s.name, query));
|
|
3215
|
-
}
|
|
3216
|
-
|
|
3217
|
-
filtered.sort((a, b) => {
|
|
3218
|
-
if (b.frequency !== a.frequency) return b.frequency - a.frequency;
|
|
3219
|
-
if (agent) {
|
|
3220
|
-
const sourceDelta = skillAutocomplete.skillSourcePriorityForAgent(a.source, agent) - skillAutocomplete.skillSourcePriorityForAgent(b.source, agent);
|
|
3221
|
-
if (sourceDelta) return sourceDelta;
|
|
3222
|
-
}
|
|
3223
|
-
return a.name.localeCompare(b.name);
|
|
3224
|
-
});
|
|
4228
|
+
const filtered = skillAutocomplete.filterAndSortSkillAutocompleteItems(skills, query, agent || 'all');
|
|
3225
4229
|
|
|
3226
4230
|
jsonResponse(res, 200, filtered.slice(0, 20));
|
|
3227
4231
|
} catch (e) { jsonResponse(res, 500, { error: e.message }); }
|
|
3228
4232
|
}
|
|
3229
4233
|
|
|
4234
|
+
function handleSkillResolveIntent(req, res, url) {
|
|
4235
|
+
try {
|
|
4236
|
+
const intent = url.searchParams.get('intent') || '';
|
|
4237
|
+
const agent = normalizeSkillAutocompleteAgent(url.searchParams.get('agent') || url.searchParams.get('agentType'));
|
|
4238
|
+
const cwd = url.searchParams.get('cwd') || '';
|
|
4239
|
+
const result = skillIntentResolver.resolveSkillIntent({ intent, agent, cwd });
|
|
4240
|
+
jsonResponse(res, result.ok === false ? 400 : 200, result);
|
|
4241
|
+
} catch (e) { jsonResponse(res, 500, { ok: false, error: e.message }); }
|
|
4242
|
+
}
|
|
4243
|
+
|
|
3230
4244
|
function normalizeSkillAutocompleteAgent(value) {
|
|
3231
4245
|
return skillAutocomplete.normalizeSkillAgentType(value);
|
|
3232
4246
|
}
|
|
@@ -3244,6 +4258,8 @@ function buildSkillAutocompleteItems({ cwd, agent, includeSessionSeen }) {
|
|
|
3244
4258
|
description: skill.description || '',
|
|
3245
4259
|
source: skill.source || 'skill',
|
|
3246
4260
|
agents: Array.isArray(skill.agents) ? skill.agents : [],
|
|
4261
|
+
capabilities: Array.isArray(skill.capabilities) ? skill.capabilities : [],
|
|
4262
|
+
invocation: skill.invocation || '',
|
|
3247
4263
|
execution: skill.execution || '',
|
|
3248
4264
|
path: skill.filePath || skill.path || '',
|
|
3249
4265
|
frequency: 0,
|
|
@@ -3281,14 +4297,6 @@ function sanitizeSkillAutocompleteName(value) {
|
|
|
3281
4297
|
return /^[A-Za-z0-9_.:-]{1,80}$/.test(name) ? name : '';
|
|
3282
4298
|
}
|
|
3283
4299
|
|
|
3284
|
-
function fuzzySkillMatch(name, query) {
|
|
3285
|
-
let qi = 0;
|
|
3286
|
-
for (const ch of String(name || '').toLowerCase()) {
|
|
3287
|
-
if (qi < query.length && ch === query[qi]) qi++;
|
|
3288
|
-
}
|
|
3289
|
-
return qi === query.length;
|
|
3290
|
-
}
|
|
3291
|
-
|
|
3292
4300
|
function handleHarvestStats(req, res) {
|
|
3293
4301
|
try {
|
|
3294
4302
|
const state = harvest.getHarvestState();
|
|
@@ -3339,4 +4347,4 @@ function safeParse(json, fallback) {
|
|
|
3339
4347
|
try { return JSON.parse(json); } catch { return fallback; }
|
|
3340
4348
|
}
|
|
3341
4349
|
|
|
3342
|
-
module.exports = { handlePromptApi, queueEngine,
|
|
4350
|
+
module.exports = { handlePromptApi, queueEngine, runIncrementalConversationImport, runCursorConversationImport, importSessionFile, setUiPrefsBroadcaster, setPromptExecutionsOffThread, setDbMaintenanceRunner, setImageSaveRunner, _ingestPathFromInput, _ingestSourceAllowed, _conversationImportCandidates, _ingestTranscriptStoreForParsedFile };
|