create-walle 0.9.21 → 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 +27 -5
- package/package.json +2 -2
- 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 +1203 -182
- package/template/claude-task-manager/api-reviews.js +109 -15
- 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 +4417 -295
- package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
- 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 +13 -0
- package/template/claude-task-manager/docs/ipad-web-preview.md +88 -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 +897 -27
- 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 +47 -4
- package/template/claude-task-manager/lib/auth-rules.js +29 -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 +141 -6
- 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 +750 -26
- package/template/claude-task-manager/lib/mobile-auth-api.js +274 -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 +109 -0
- 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 +123 -23
- package/template/claude-task-manager/lib/session-state-bus.js +14 -0
- package/template/claude-task-manager/lib/session-stream.js +64 -16
- 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 +84 -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 +52 -0
- package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
- 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 +551 -61
- package/template/claude-task-manager/public/css/setup.css +191 -0
- package/template/claude-task-manager/public/css/walle-session.css +865 -10
- package/template/claude-task-manager/public/css/walle.css +154 -0
- package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
- package/template/claude-task-manager/public/index.html +18516 -2058
- package/template/claude-task-manager/public/ipad.html +363 -0
- package/template/claude-task-manager/public/js/document-review-links.js +301 -0
- package/template/claude-task-manager/public/js/image-normalize.js +69 -36
- package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
- package/template/claude-task-manager/public/js/prompts.js +66 -29
- package/template/claude-task-manager/public/js/reviews.js +901 -133
- 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 +1273 -176
- package/template/claude-task-manager/public/js/stream-view.js +691 -73
- 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 +455 -28
- package/template/claude-task-manager/public/m/app.css +2909 -262
- package/template/claude-task-manager/public/m/app.js +6601 -398
- package/template/claude-task-manager/public/m/claim.html +224 -17
- package/template/claude-task-manager/public/m/index.html +117 -21
- 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 +14341 -2197
- package/template/claude-task-manager/session-integrity.js +160 -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 +129 -38
- 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 +524 -70
- 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 +1614 -141
- 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 +1002 -233
- 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 +1330 -221
- 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 +836 -34
- 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/lib/service-health.js +194 -0
- package/template/wall-e/llm/anthropic.js +130 -5
- package/template/wall-e/llm/client.js +266 -63
- 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 +26 -4
- package/template/wall-e/llm/portkey-sync.js +654 -0
- 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 +15 -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/website/index.html +10 -10
- 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,7 +5,9 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { StringDecoder } = require('string_decoder');
|
|
7
7
|
|
|
8
|
-
const { codexMessageFromEntry, codexRolloutIdFromPath,
|
|
8
|
+
const { codexMessageFromEntry, codexRolloutIdFromPath, createCodexUserDeduper } = require('./session-history');
|
|
9
|
+
const { isProviderGeneratedUserContextText } = require('./provider-user-context');
|
|
10
|
+
const { sortTimelineMessages } = require('./timeline-order');
|
|
9
11
|
|
|
10
12
|
const PARSER_VERSION = 1;
|
|
11
13
|
const DEFAULT_CHUNK_BYTES = 1024 * 1024;
|
|
@@ -16,6 +18,16 @@ function sha1(value) {
|
|
|
16
18
|
return crypto.createHash('sha1').update(String(value || '')).digest('hex');
|
|
17
19
|
}
|
|
18
20
|
|
|
21
|
+
function parseMetadataJson(value) {
|
|
22
|
+
if (!value) return {};
|
|
23
|
+
try {
|
|
24
|
+
const parsed = JSON.parse(String(value));
|
|
25
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
26
|
+
} catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
function normalizeProvider(provider, filePath = '') {
|
|
20
32
|
const raw = String(provider || '').trim().toLowerCase();
|
|
21
33
|
if (raw) return raw;
|
|
@@ -28,6 +40,48 @@ function sourceIdFromPath(filePath) {
|
|
|
28
40
|
|| path.basename(filePath || '').replace(/\.jsonl(\.bak)?$/, '');
|
|
29
41
|
}
|
|
30
42
|
|
|
43
|
+
function isSubagentTranscriptPath(filePath) {
|
|
44
|
+
return String(filePath || '').replace(/\\/g, '/').includes('/subagents/');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadedTranscriptMessageFromRow(row) {
|
|
48
|
+
const metadata = parseMetadataJson(row.metadata_json);
|
|
49
|
+
const text = String(row.text || '');
|
|
50
|
+
let role = row.role;
|
|
51
|
+
let agentLabel = metadata.agentLabel || metadata.roleLabel || '';
|
|
52
|
+
let roleLabel = metadata.roleLabel || metadata.agentLabel || '';
|
|
53
|
+
const sourceKind = metadata.sourceKind || (isSubagentTranscriptPath(row.jsonl_path) ? 'subagent' : '');
|
|
54
|
+
|
|
55
|
+
if (role === 'user' && isProviderGeneratedUserContextText(text)) return null;
|
|
56
|
+
|
|
57
|
+
if (sourceKind === 'subagent') {
|
|
58
|
+
metadata.sourceKind = 'subagent';
|
|
59
|
+
if (role === 'user') {
|
|
60
|
+
metadata.originalRole = metadata.originalRole || 'user';
|
|
61
|
+
metadata.promptKind = metadata.promptKind || 'subagent';
|
|
62
|
+
role = 'system';
|
|
63
|
+
agentLabel = agentLabel || 'Subagent prompt';
|
|
64
|
+
roleLabel = roleLabel || 'Subagent prompt';
|
|
65
|
+
} else if (!agentLabel && role === 'assistant') {
|
|
66
|
+
agentLabel = 'Subagent';
|
|
67
|
+
roleLabel = roleLabel || 'Subagent';
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
role,
|
|
73
|
+
text,
|
|
74
|
+
timestamp: row.timestamp || '',
|
|
75
|
+
metadata,
|
|
76
|
+
source: 'transcript-events',
|
|
77
|
+
provider: row.provider || '',
|
|
78
|
+
agentSessionId: row.agent_session_id,
|
|
79
|
+
ctmSessionId: row.ctm_session_id || '',
|
|
80
|
+
agentLabel: agentLabel || undefined,
|
|
81
|
+
roleLabel: roleLabel || undefined,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
31
85
|
function ensureTranscriptTables(db, options = {}) {
|
|
32
86
|
db.exec(`
|
|
33
87
|
CREATE TABLE IF NOT EXISTS transcript_files (
|
|
@@ -76,6 +130,33 @@ function ensureTranscriptTables(db, options = {}) {
|
|
|
76
130
|
ON transcript_events(timestamp);
|
|
77
131
|
`);
|
|
78
132
|
|
|
133
|
+
// Phase 1 of the blob→rows migration: enrich transcript_events so it can be the
|
|
134
|
+
// PRIMARY conversation store (not just a search overlay). Additive, idempotent —
|
|
135
|
+
// SQLite has no ADD COLUMN IF NOT EXISTS, so each ALTER is guarded individually.
|
|
136
|
+
// turn_ordinal – turn group (a user prompt + its answer/tool calls); assigned at
|
|
137
|
+
// ingest, drives turn-mode pagination + render grouping.
|
|
138
|
+
// token_count – cheap per-row token estimate; summed for the session total.
|
|
139
|
+
// uuid/parent_uuid – Claude message identity (promoted from metadata_json) for
|
|
140
|
+
// streamed-partial dedup and parentUuid collapse parity.
|
|
141
|
+
// model/usage_json – per-message model + provider usage when present.
|
|
142
|
+
// content_json – optional inline rich content for recent rows; empty rows
|
|
143
|
+
// hydrate on demand from (jsonl_path, byte_start, byte_end).
|
|
144
|
+
for (const ddl of [
|
|
145
|
+
'ALTER TABLE transcript_events ADD COLUMN turn_ordinal INTEGER DEFAULT 0',
|
|
146
|
+
'ALTER TABLE transcript_events ADD COLUMN token_count INTEGER DEFAULT 0',
|
|
147
|
+
"ALTER TABLE transcript_events ADD COLUMN uuid TEXT DEFAULT ''",
|
|
148
|
+
"ALTER TABLE transcript_events ADD COLUMN parent_uuid TEXT DEFAULT ''",
|
|
149
|
+
"ALTER TABLE transcript_events ADD COLUMN model TEXT DEFAULT ''",
|
|
150
|
+
"ALTER TABLE transcript_events ADD COLUMN usage_json TEXT DEFAULT ''",
|
|
151
|
+
"ALTER TABLE transcript_events ADD COLUMN content_json TEXT DEFAULT ''",
|
|
152
|
+
]) {
|
|
153
|
+
try { db.exec(ddl); } catch { /* column already exists */ }
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_transcript_events_ctm_turn
|
|
157
|
+
ON transcript_events(ctm_session_id, turn_ordinal, seq);`);
|
|
158
|
+
} catch { /* index already exists / column missing on an old build */ }
|
|
159
|
+
|
|
79
160
|
try {
|
|
80
161
|
db.exec(`
|
|
81
162
|
CREATE VIRTUAL TABLE IF NOT EXISTS transcript_events_fts USING fts5(
|
|
@@ -129,6 +210,13 @@ function truncateVisibleText(text) {
|
|
|
129
210
|
return `${value.slice(0, MAX_VISIBLE_TEXT)}...truncated (${value.length} chars)`;
|
|
130
211
|
}
|
|
131
212
|
|
|
213
|
+
// Cheap per-row token estimate (~4 chars/token). Feeds an incremental session
|
|
214
|
+
// total; exact provider-authoritative counts are recomputed on the live path.
|
|
215
|
+
function estimateRowTokens(text) {
|
|
216
|
+
const len = String(text || '').length;
|
|
217
|
+
return len ? Math.max(1, Math.ceil(len / 4)) : 0;
|
|
218
|
+
}
|
|
219
|
+
|
|
132
220
|
function messageFromEntry(entry, provider, state = {}) {
|
|
133
221
|
if (!entry || typeof entry !== 'object') return null;
|
|
134
222
|
|
|
@@ -145,9 +233,8 @@ function messageFromEntry(entry, provider, state = {}) {
|
|
|
145
233
|
const msg = codexMessageFromEntry(entry);
|
|
146
234
|
if (!msg) return null;
|
|
147
235
|
if (msg.role === 'user') {
|
|
148
|
-
|
|
149
|
-
if (!
|
|
150
|
-
if (state.codexSeenUsers) state.codexSeenUsers.add(key);
|
|
236
|
+
if (!state.codexUserDeduper) state.codexUserDeduper = createCodexUserDeduper();
|
|
237
|
+
if (!state.codexUserDeduper.remember(msg.text, msg.timestamp)) return null;
|
|
151
238
|
}
|
|
152
239
|
return { role: msg.role, text: truncateVisibleText(msg.text), timestamp: msg.timestamp || '', metadata: {} };
|
|
153
240
|
}
|
|
@@ -167,6 +254,8 @@ function messageFromEntry(entry, provider, state = {}) {
|
|
|
167
254
|
let text = '';
|
|
168
255
|
if (entry.partType === 'tool_call') text = `[Tool: ${data.name || 'tool'}]`;
|
|
169
256
|
else if (entry.partType === 'tool_result') text = `[Tool result: ${data.name || 'tool'}]`;
|
|
257
|
+
else if (entry.partType === 'artifact') text = artifactText(data);
|
|
258
|
+
else if (entry.partType === 'capability_routed') text = capabilityRouteText(data);
|
|
170
259
|
else if (entry.partType === 'error') text = `[Wall-E error] ${data.message || ''}`.trim();
|
|
171
260
|
else if (entry.partType === 'cancelled') text = '[Wall-E cancelled]';
|
|
172
261
|
return text ? { role: 'system', text, timestamp: entry.timestamp || '', metadata: { partType: entry.partType || '' } } : null;
|
|
@@ -179,11 +268,18 @@ function messageFromEntry(entry, provider, state = {}) {
|
|
|
179
268
|
text = isToolResult ? text : cleanClaudeUserText(text);
|
|
180
269
|
text = truncateVisibleText(text);
|
|
181
270
|
if (!text) return null;
|
|
271
|
+
if (!isToolResult && isProviderGeneratedUserContextText(text)) return null;
|
|
272
|
+
const isSubagentTask = !isToolResult && state.sourceKind === 'subagent';
|
|
182
273
|
return {
|
|
183
|
-
role: isToolResult ? 'system' : 'user',
|
|
274
|
+
role: isToolResult || isSubagentTask ? 'system' : 'user',
|
|
184
275
|
text,
|
|
185
276
|
timestamp: entry.timestamp || '',
|
|
186
|
-
metadata: {
|
|
277
|
+
metadata: {
|
|
278
|
+
uuid: entry.uuid || '',
|
|
279
|
+
cwd: entry.cwd || '',
|
|
280
|
+
gitBranch: entry.gitBranch || '',
|
|
281
|
+
...(isSubagentTask ? { sourceKind: 'subagent', originalRole: 'user', promptKind: 'subagent', roleLabel: 'Subagent prompt', agentLabel: 'Subagent prompt' } : {}),
|
|
282
|
+
},
|
|
187
283
|
};
|
|
188
284
|
}
|
|
189
285
|
|
|
@@ -201,6 +297,24 @@ function messageFromEntry(entry, provider, state = {}) {
|
|
|
201
297
|
return null;
|
|
202
298
|
}
|
|
203
299
|
|
|
300
|
+
function artifactText(data = {}) {
|
|
301
|
+
const artifact = data.artifact || {};
|
|
302
|
+
const kind = artifact.kind || data.type || 'artifact';
|
|
303
|
+
const label = artifact.path || artifact.originalPath || artifact.artifactId || '';
|
|
304
|
+
const bits = [`[Artifact: ${kind}]`];
|
|
305
|
+
if (artifact.bytes) bits.push(`${artifact.bytes} bytes`);
|
|
306
|
+
if (artifact.metadata?.page_count) bits.push(`${artifact.metadata.page_count} pages`);
|
|
307
|
+
if (label) bits.push(label);
|
|
308
|
+
return bits.join(' ');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function capabilityRouteText(data = {}) {
|
|
312
|
+
const capabilities = Array.isArray(data.capabilities) ? data.capabilities : [];
|
|
313
|
+
if (!capabilities.length) return '';
|
|
314
|
+
const names = capabilities.map((capability) => capability.id || capability.label).filter(Boolean);
|
|
315
|
+
return names.length ? `[Wall-E capability routes] ${names.join(', ')}` : '';
|
|
316
|
+
}
|
|
317
|
+
|
|
204
318
|
function readCompleteLines(filePath, { startOffset, maxBytes, partialLine, trimLeadingPartial }) {
|
|
205
319
|
const fd = fs.openSync(filePath, 'r');
|
|
206
320
|
const decoder = new StringDecoder('utf8');
|
|
@@ -349,7 +463,11 @@ function ingestJsonlFile(db, options = {}) {
|
|
|
349
463
|
trimLeadingPartial: cursor.trimLeadingPartial,
|
|
350
464
|
});
|
|
351
465
|
|
|
352
|
-
const state = {
|
|
466
|
+
const state = {
|
|
467
|
+
codexUserDeduper: createCodexUserDeduper(),
|
|
468
|
+
sourceFile: filePath,
|
|
469
|
+
sourceKind: isSubagentTranscriptPath(filePath) ? 'subagent' : '',
|
|
470
|
+
};
|
|
353
471
|
const messages = [];
|
|
354
472
|
for (const lineInfo of read.lines) {
|
|
355
473
|
let entry;
|
|
@@ -359,13 +477,17 @@ function ingestJsonlFile(db, options = {}) {
|
|
|
359
477
|
messages.push({ ...message, byteStart: lineInfo.byteStart, byteEnd: lineInfo.byteEnd });
|
|
360
478
|
}
|
|
361
479
|
|
|
362
|
-
|
|
480
|
+
// Seed seq AND turn_ordinal from the durable max so a restart mid-session never
|
|
481
|
+
// resets either (crash-safe resume — turns must stay monotonic across boots).
|
|
482
|
+
const nextSeqRow = db.prepare('SELECT COALESCE(MAX(seq) + 1, 0) AS nextSeq, COALESCE(MAX(turn_ordinal), 0) AS maxTurn FROM transcript_events WHERE agent_session_id = ?').get(agentSessionId);
|
|
363
483
|
let seq = Number(nextSeqRow?.nextSeq || 0);
|
|
484
|
+
let turn = Number(nextSeqRow?.maxTurn || 0);
|
|
364
485
|
let inserted = 0;
|
|
365
486
|
const insertEvent = db.prepare(`
|
|
366
487
|
INSERT OR IGNORE INTO transcript_events
|
|
367
|
-
(ctm_session_id, agent_session_id, provider, jsonl_path, seq, role, timestamp, content, byte_start, byte_end, event_hash, metadata_json
|
|
368
|
-
|
|
488
|
+
(ctm_session_id, agent_session_id, provider, jsonl_path, seq, role, timestamp, content, byte_start, byte_end, event_hash, metadata_json,
|
|
489
|
+
turn_ordinal, token_count, uuid, parent_uuid, model, usage_json, content_json)
|
|
490
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
369
491
|
`);
|
|
370
492
|
const insertEventFts = hasTranscriptEventsFts(db)
|
|
371
493
|
? db.prepare('INSERT INTO transcript_events_fts(rowid, content) VALUES (?, ?)')
|
|
@@ -390,8 +512,24 @@ function ingestJsonlFile(db, options = {}) {
|
|
|
390
512
|
updated_at=datetime('now')
|
|
391
513
|
`);
|
|
392
514
|
|
|
393
|
-
|
|
394
|
-
|
|
515
|
+
// Insert in bounded sub-transactions so a large catch-up ingest never holds the SQLite
|
|
516
|
+
// write lock for the whole file. One whole-file db.transaction() was observed holding the
|
|
517
|
+
// lock ~20s on the primary, bloating the WAL (no checkpoint can run mid-transaction) →
|
|
518
|
+
// reader/read-pool requests time out → callers fall back to the main thread (the bursty
|
|
519
|
+
// freezes). Each chunk is its own transaction = its own lock acquire/release, so
|
|
520
|
+
// checkpoints and readers interleave and the WAL stays bounded. INSERT OR IGNORE
|
|
521
|
+
// (event_hash) keeps re-runs idempotent, so the file cursor is advanced once at the end —
|
|
522
|
+
// a crash mid-ingest simply re-processes from the old offset without duplicating rows.
|
|
523
|
+
const CHUNK = Math.max(25, Math.min(5000, Number(options.ingestChunkSize || process.env.CTM_TRANSCRIPT_INGEST_CHUNK || 250)));
|
|
524
|
+
const insertChunk = db.transaction((slice) => {
|
|
525
|
+
for (const message of slice) {
|
|
526
|
+
// A turn = one real user prompt + the assistant answer(s)/tool calls that
|
|
527
|
+
// follow it. messageFromEntry already maps tool-results and subagent prompts
|
|
528
|
+
// to role 'system', so role==='user' is a genuine prompt → start a new turn.
|
|
529
|
+
// Assistant/system rows inherit the current turn. (Read-side turn grouping in
|
|
530
|
+
// Phase 3 keys on this; it mirrors the renderer's user-message turn boundary.)
|
|
531
|
+
if (message.role === 'user') turn++;
|
|
532
|
+
const meta = message.metadata || {};
|
|
395
533
|
const hash = eventHash(agentSessionId, message);
|
|
396
534
|
const info = insertEvent.run(
|
|
397
535
|
ctmSessionId,
|
|
@@ -405,7 +543,14 @@ function ingestJsonlFile(db, options = {}) {
|
|
|
405
543
|
message.byteStart || 0,
|
|
406
544
|
message.byteEnd || 0,
|
|
407
545
|
hash,
|
|
408
|
-
JSON.stringify(
|
|
546
|
+
JSON.stringify(meta),
|
|
547
|
+
turn,
|
|
548
|
+
estimateRowTokens(message.text),
|
|
549
|
+
String(meta.uuid || ''),
|
|
550
|
+
String(meta.parentUuid || ''),
|
|
551
|
+
String(meta.model || ''),
|
|
552
|
+
'', // usage_json: enriched later; empty keeps the row small
|
|
553
|
+
'' // content_json: rich content hydrated on demand from byte offsets
|
|
409
554
|
);
|
|
410
555
|
if (info.changes > 0) {
|
|
411
556
|
if (insertEventFts) {
|
|
@@ -415,21 +560,25 @@ function ingestJsonlFile(db, options = {}) {
|
|
|
415
560
|
seq++;
|
|
416
561
|
}
|
|
417
562
|
}
|
|
418
|
-
upsertFile.run(
|
|
419
|
-
filePath,
|
|
420
|
-
agentSessionId,
|
|
421
|
-
ctmSessionId,
|
|
422
|
-
provider,
|
|
423
|
-
String(stat.dev ?? ''),
|
|
424
|
-
String(stat.ino ?? ''),
|
|
425
|
-
stat.size,
|
|
426
|
-
stat.mtimeMs,
|
|
427
|
-
read.nextOffset,
|
|
428
|
-
read.partialLine || '',
|
|
429
|
-
PARSER_VERSION
|
|
430
|
-
);
|
|
431
563
|
});
|
|
432
|
-
|
|
564
|
+
for (let i = 0; i < messages.length; i += CHUNK) {
|
|
565
|
+
insertChunk(messages.slice(i, i + CHUNK));
|
|
566
|
+
}
|
|
567
|
+
// Advance the file cursor only after all events are committed. INSERT OR IGNORE makes a
|
|
568
|
+
// re-run from the old offset idempotent, so a crash between chunks loses no data.
|
|
569
|
+
upsertFile.run(
|
|
570
|
+
filePath,
|
|
571
|
+
agentSessionId,
|
|
572
|
+
ctmSessionId,
|
|
573
|
+
provider,
|
|
574
|
+
String(stat.dev ?? ''),
|
|
575
|
+
String(stat.ino ?? ''),
|
|
576
|
+
stat.size,
|
|
577
|
+
stat.mtimeMs,
|
|
578
|
+
read.nextOffset,
|
|
579
|
+
read.partialLine || '',
|
|
580
|
+
PARSER_VERSION
|
|
581
|
+
);
|
|
433
582
|
|
|
434
583
|
return {
|
|
435
584
|
inserted,
|
|
@@ -509,7 +658,7 @@ function loadTranscriptMessages(db, ids = []) {
|
|
|
509
658
|
if (!cleanIds.length) return [];
|
|
510
659
|
const placeholders = cleanIds.map(() => '?').join(',');
|
|
511
660
|
return db.prepare(`
|
|
512
|
-
SELECT ctm_session_id, agent_session_id, provider, role, content AS text, timestamp, seq, jsonl_path
|
|
661
|
+
SELECT ctm_session_id, agent_session_id, provider, role, content AS text, timestamp, seq, jsonl_path, metadata_json
|
|
513
662
|
FROM transcript_events
|
|
514
663
|
WHERE ctm_session_id IN (${placeholders})
|
|
515
664
|
OR agent_session_id IN (${placeholders})
|
|
@@ -518,14 +667,166 @@ function loadTranscriptMessages(db, ids = []) {
|
|
|
518
667
|
timestamp,
|
|
519
668
|
agent_session_id,
|
|
520
669
|
seq
|
|
521
|
-
`).all(...cleanIds, ...cleanIds)
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
670
|
+
`).all(...cleanIds, ...cleanIds)
|
|
671
|
+
.map(loadedTranscriptMessageFromRow)
|
|
672
|
+
.filter(Boolean);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// ---------------------------------------------------------------------------
|
|
676
|
+
// Phase 3: keyset/offset-paginated reads over transcript_events, so a session's
|
|
677
|
+
// conversation is served from per-message ROWS (≤ limit materialized) instead of
|
|
678
|
+
// JSON.parse-ing a multi-GB blob. Reproduces lib/message-pagination.js semantics
|
|
679
|
+
// (newest-first offset; chronological within a page) so the API/response shape and
|
|
680
|
+
// the renderer are unchanged. Page reads are O(limit) on the (ctm_session_id, seq)
|
|
681
|
+
// / (ctm_session_id, turn_ordinal) indexes — the 28-33s blob-parse freeze is gone.
|
|
682
|
+
// ---------------------------------------------------------------------------
|
|
683
|
+
|
|
684
|
+
function _resolveSessionRowIds(db, ids) {
|
|
685
|
+
const list = Array.isArray(ids) ? ids : [ids];
|
|
686
|
+
const seed = list.find(Boolean) || '';
|
|
687
|
+
return transcriptSourceIds(db, seed, list);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// True only when the row store can serve this session: the migrated columns exist
|
|
691
|
+
// AND at least one row is present for the id set. Callers fall back to the legacy
|
|
692
|
+
// blob/JSONL ladder otherwise, so a half-migrated session never breaks.
|
|
693
|
+
function sessionMessagesRowsAvailable(db, ids) {
|
|
694
|
+
try {
|
|
695
|
+
ensureTranscriptTables(db, { skipFtsBackfill: true });
|
|
696
|
+
const cols = db.prepare("PRAGMA table_info(transcript_events)").all().map(c => c.name);
|
|
697
|
+
if (!cols.includes('turn_ordinal')) return false;
|
|
698
|
+
const sids = _resolveSessionRowIds(db, ids);
|
|
699
|
+
if (!sids.length) return false;
|
|
700
|
+
const ph = sids.map(() => '?').join(',');
|
|
701
|
+
const row = db.prepare(`SELECT 1 FROM transcript_events WHERE ctm_session_id IN (${ph}) OR agent_session_id IN (${ph}) LIMIT 1`).get(...sids, ...sids);
|
|
702
|
+
if (!row) return false;
|
|
703
|
+
// FRESHNESS GATE: the rows must be CAUGHT UP to every source file. Stat the LIVE file
|
|
704
|
+
// (not transcript_files.size, which is itself only as fresh as the last ingest): if the
|
|
705
|
+
// file has grown past the durable ingest cursor (offset), the rows are missing its recent
|
|
706
|
+
// tail — serving them would show a stale conversation (caught a real case on a giant Codex
|
|
707
|
+
// session whose rows lagged the file by ~10h). When stale, return false so the caller uses
|
|
708
|
+
// the legacy blob/JSONL path, which reads the live file and is always complete. 64KB
|
|
709
|
+
// tolerates the trailing partial line; anything more is real lag.
|
|
710
|
+
const files = db.prepare(
|
|
711
|
+
`SELECT jsonl_path, offset FROM transcript_files WHERE ctm_session_id IN (${ph}) OR agent_session_id IN (${ph})`
|
|
712
|
+
).all(...sids, ...sids);
|
|
713
|
+
if (!files.length) return false;
|
|
714
|
+
for (const f of files) {
|
|
715
|
+
let size = 0;
|
|
716
|
+
try { size = fs.statSync(f.jsonl_path).size; } catch { return false; } // file gone → use legacy path
|
|
717
|
+
if (size - Number(f.offset || 0) > 65536) return false;
|
|
718
|
+
}
|
|
719
|
+
return true;
|
|
720
|
+
} catch { return false; }
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function _whereIds(sids) {
|
|
724
|
+
const ph = sids.map(() => '?').join(',');
|
|
725
|
+
return { clause: `(ctm_session_id IN (${ph}) OR agent_session_id IN (${ph}))`, args: [...sids, ...sids] };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const _ROW_ORDER = `CASE WHEN timestamp IS NULL OR timestamp = '' THEN 1 ELSE 0 END, timestamp, agent_session_id, seq`;
|
|
729
|
+
const _ROW_COLS = `ctm_session_id, agent_session_id, provider, role, content AS text, timestamp, seq, jsonl_path, byte_start, byte_end, metadata_json, turn_ordinal`;
|
|
730
|
+
|
|
731
|
+
function countSessionMessages(db, ids) {
|
|
732
|
+
const sids = _resolveSessionRowIds(db, ids);
|
|
733
|
+
if (!sids.length) return 0;
|
|
734
|
+
const w = _whereIds(sids);
|
|
735
|
+
return Number(db.prepare(`SELECT COUNT(*) AS c FROM transcript_events WHERE ${w.clause}`).get(...w.args)?.c || 0);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Newest-first offset (offset 0 = the most recent `limit`), chronological within the
|
|
739
|
+
// page — matches lib/message-pagination.js paginateMessages().
|
|
740
|
+
function getSessionMessagesPage(db, ids, { offset = 0, limit = 200 } = {}) {
|
|
741
|
+
ensureTranscriptTables(db, { skipFtsBackfill: true });
|
|
742
|
+
const sids = _resolveSessionRowIds(db, ids);
|
|
743
|
+
if (!sids.length) return { messages: [], total: 0, has_more: false, next_offset: Number(offset) || 0 };
|
|
744
|
+
const w = _whereIds(sids);
|
|
745
|
+
const total = Number(db.prepare(`SELECT COUNT(*) AS c FROM transcript_events WHERE ${w.clause}`).get(...w.args)?.c || 0);
|
|
746
|
+
const lim = Math.max(1, Math.min(1000, Number(limit) || 200));
|
|
747
|
+
const off = Math.max(0, Number(offset) || 0);
|
|
748
|
+
const start = Math.max(0, total - off - lim);
|
|
749
|
+
const take = Math.max(0, Math.min(lim, total - off - start));
|
|
750
|
+
const raw = take === 0 ? [] : db.prepare(
|
|
751
|
+
`SELECT ${_ROW_COLS} FROM transcript_events WHERE ${w.clause} ORDER BY ${_ROW_ORDER} LIMIT ? OFFSET ?`
|
|
752
|
+
).all(...w.args, take, start);
|
|
753
|
+
const messages = raw.map(loadedTranscriptMessageFromRow).filter(Boolean);
|
|
754
|
+
return { messages, total, has_more: start > 0, next_offset: off + take, _rows: raw };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Newest-first by turn (offset 0 = the most recent `limit` turns). Returns all rows
|
|
758
|
+
// belonging to the page's turns in chronological order — matches paginateMessageTurns().
|
|
759
|
+
function getSessionTurnsPage(db, ids, { offset = 0, limit = 50 } = {}) {
|
|
760
|
+
ensureTranscriptTables(db, { skipFtsBackfill: true });
|
|
761
|
+
const sids = _resolveSessionRowIds(db, ids);
|
|
762
|
+
const empty = { messages: [], total: 0, has_more: false, next_offset: Number(offset) || 0, page_kind: 'turns', turn_count: 0 };
|
|
763
|
+
if (!sids.length) return empty;
|
|
764
|
+
const w = _whereIds(sids);
|
|
765
|
+
const turns = db.prepare(`SELECT DISTINCT turn_ordinal FROM transcript_events WHERE ${w.clause} ORDER BY turn_ordinal`).all(...w.args).map(r => Number(r.turn_ordinal));
|
|
766
|
+
const totalTurns = turns.length;
|
|
767
|
+
const lim = Math.max(1, Math.min(1000, Number(limit) || 50));
|
|
768
|
+
const off = Math.max(0, Number(offset) || 0);
|
|
769
|
+
const startIdx = Math.max(0, totalTurns - off - lim);
|
|
770
|
+
const endIdx = Math.max(startIdx, totalTurns - off);
|
|
771
|
+
const pageTurns = turns.slice(startIdx, endIdx);
|
|
772
|
+
if (!pageTurns.length) return { ...empty, total: totalTurns, has_more: startIdx > 0 };
|
|
773
|
+
const minT = pageTurns[0], maxT = pageTurns[pageTurns.length - 1];
|
|
774
|
+
const raw = db.prepare(
|
|
775
|
+
`SELECT ${_ROW_COLS} FROM transcript_events WHERE ${w.clause} AND turn_ordinal BETWEEN ? AND ? ORDER BY ${_ROW_ORDER}`
|
|
776
|
+
).all(...w.args, minT, maxT);
|
|
777
|
+
const messages = raw.map(loadedTranscriptMessageFromRow).filter(Boolean);
|
|
778
|
+
return {
|
|
779
|
+
messages, total: totalTurns, has_more: startIdx > 0, next_offset: off + pageTurns.length,
|
|
780
|
+
page_kind: 'turns', turn_count: pageTurns.length, first_turn_index: minT, last_turn_index: maxT, _rows: raw,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Phase 4 (existing-user backfill): enrich rows that were ingested BEFORE Phase 2 and
|
|
785
|
+
// therefore have turn_ordinal=0 / token_count=0. Walks one agent session's rows in order
|
|
786
|
+
// and assigns turn_ordinal (++ on each real user prompt) + a token estimate, in bounded
|
|
787
|
+
// UPDATE transactions. Idempotent: only runs for a session that has user rows but no turns
|
|
788
|
+
// yet (after it runs, MAX(turn_ordinal) > 0, so it never repeats). Returns rows updated.
|
|
789
|
+
function enrichTranscriptRowsForSession(db, agentSessionId) {
|
|
790
|
+
ensureTranscriptTables(db, { skipFtsBackfill: true });
|
|
791
|
+
const aid = String(agentSessionId || '').trim();
|
|
792
|
+
if (!aid) return 0;
|
|
793
|
+
const stat = db.prepare(`SELECT MAX(turn_ordinal) AS maxTurn,
|
|
794
|
+
SUM(CASE WHEN role = 'user' THEN 1 ELSE 0 END) AS users
|
|
795
|
+
FROM transcript_events WHERE agent_session_id = ?`).get(aid);
|
|
796
|
+
if (!stat || Number(stat.maxTurn || 0) > 0 || Number(stat.users || 0) === 0) return 0;
|
|
797
|
+
const rows = db.prepare('SELECT id, role, content FROM transcript_events WHERE agent_session_id = ? ORDER BY seq').all(aid);
|
|
798
|
+
const upd = db.prepare('UPDATE transcript_events SET turn_ordinal = ?, token_count = ? WHERE id = ?');
|
|
799
|
+
let turn = 0, n = 0;
|
|
800
|
+
const tx = db.transaction((batch) => {
|
|
801
|
+
for (const r of batch) {
|
|
802
|
+
if (r.role === 'user') turn++;
|
|
803
|
+
upd.run(turn, estimateRowTokens(r.content), r.id);
|
|
804
|
+
n++;
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
const CHUNK = 500;
|
|
808
|
+
for (let i = 0; i < rows.length; i += CHUNK) tx(rows.slice(i, i + CHUNK));
|
|
809
|
+
return n;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Find agent sessions whose rows still need enrichment (have user rows, no turns yet),
|
|
813
|
+
// bounded — for a background sweep that migrates existing users without a big-bang.
|
|
814
|
+
function findUnenrichedSessions(db, limit = 25) {
|
|
815
|
+
ensureTranscriptTables(db, { skipFtsBackfill: true });
|
|
816
|
+
try {
|
|
817
|
+
return db.prepare(`SELECT agent_session_id FROM transcript_events
|
|
818
|
+
GROUP BY agent_session_id
|
|
819
|
+
HAVING MAX(turn_ordinal) = 0 AND SUM(CASE WHEN role = 'user' THEN 1 ELSE 0 END) > 0
|
|
820
|
+
LIMIT ?`).all(Math.max(1, Math.min(500, Number(limit) || 25))).map(r => r.agent_session_id);
|
|
821
|
+
} catch { return []; }
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Session token total from per-row estimates (no full-array rebuild).
|
|
825
|
+
function getSessionTokenSum(db, ids) {
|
|
826
|
+
const sids = _resolveSessionRowIds(db, ids);
|
|
827
|
+
if (!sids.length) return 0;
|
|
828
|
+
const w = _whereIds(sids);
|
|
829
|
+
return Number(db.prepare(`SELECT COALESCE(SUM(token_count), 0) AS t FROM transcript_events WHERE ${w.clause}`).get(...w.args)?.t || 0);
|
|
529
830
|
}
|
|
530
831
|
|
|
531
832
|
function mergeMessageLists(baseMessages, overlayMessages) {
|
|
@@ -543,13 +844,7 @@ function mergeMessageLists(baseMessages, overlayMessages) {
|
|
|
543
844
|
};
|
|
544
845
|
for (const message of Array.isArray(baseMessages) ? baseMessages : []) add(message);
|
|
545
846
|
for (const message of Array.isArray(overlayMessages) ? overlayMessages : []) add(message);
|
|
546
|
-
merged
|
|
547
|
-
const at = String(a.timestamp || '');
|
|
548
|
-
const bt = String(b.timestamp || '');
|
|
549
|
-
if (at !== bt) return at.localeCompare(bt);
|
|
550
|
-
return String(a.role || '').localeCompare(String(b.role || ''));
|
|
551
|
-
});
|
|
552
|
-
return merged;
|
|
847
|
+
return sortTimelineMessages(merged);
|
|
553
848
|
}
|
|
554
849
|
|
|
555
850
|
function catchUpTranscriptFiles(db, options = {}) {
|
|
@@ -641,7 +936,17 @@ module.exports = {
|
|
|
641
936
|
markTranscriptFileError,
|
|
642
937
|
mergeMessageLists,
|
|
643
938
|
messageFromEntry,
|
|
939
|
+
isSubagentTranscriptPath,
|
|
644
940
|
normalizeProvider,
|
|
645
941
|
sourceIdFromPath,
|
|
646
942
|
transcriptSourceIds,
|
|
943
|
+
// Phase 3 row-store readers
|
|
944
|
+
sessionMessagesRowsAvailable,
|
|
945
|
+
countSessionMessages,
|
|
946
|
+
getSessionMessagesPage,
|
|
947
|
+
getSessionTurnsPage,
|
|
948
|
+
getSessionTokenSum,
|
|
949
|
+
// Phase 4 existing-user enrichment backfill
|
|
950
|
+
enrichTranscriptRowsForSession,
|
|
951
|
+
findUnenrichedSessions,
|
|
647
952
|
};
|
|
@@ -94,11 +94,59 @@ function trustedForwardedHost(req) {
|
|
|
94
94
|
);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
function cleanIpHeaderValue(value) {
|
|
98
|
+
let raw = stripHeaderQuotes(firstHeaderValue(value));
|
|
99
|
+
if (!raw || raw.toLowerCase() === 'unknown') return '';
|
|
100
|
+
if (raw.startsWith('[')) {
|
|
101
|
+
const end = raw.indexOf(']');
|
|
102
|
+
raw = end >= 0 ? raw.slice(1, end) : stripIpv6Brackets(raw);
|
|
103
|
+
} else if (net.isIP(raw) === 0) {
|
|
104
|
+
const colonCount = (raw.match(/:/g) || []).length;
|
|
105
|
+
if (colonCount === 1 && /^\d+\.\d+\.\d+\.\d+:\d+$/.test(raw)) {
|
|
106
|
+
raw = raw.replace(/:\d+$/, '');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
raw = stripIpv6Brackets(raw);
|
|
110
|
+
return net.isIP(raw) ? raw : '';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function forwardedClientIp(req) {
|
|
114
|
+
const forwarded = parseForwardedHeader(req?.headers?.forwarded || '');
|
|
115
|
+
for (const value of [
|
|
116
|
+
req?.headers?.['x-forwarded-for'],
|
|
117
|
+
req?.headers?.['x-real-ip'],
|
|
118
|
+
req?.headers?.['cf-connecting-ip'],
|
|
119
|
+
forwarded.for,
|
|
120
|
+
]) {
|
|
121
|
+
const ip = cleanIpHeaderValue(value);
|
|
122
|
+
if (ip) return ip;
|
|
123
|
+
}
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function requestClientIp(req) {
|
|
128
|
+
const direct = stripIpv6Brackets(req?.socket?.remoteAddress || '');
|
|
129
|
+
if (!isLoopbackAddress(direct)) return direct;
|
|
130
|
+
const tunnelHost = trustedForwardedHost(req) || cleanHostHeaderValue(req?.headers?.host);
|
|
131
|
+
if (!isManagedHttpsTunnelHost(tunnelHost)) return direct;
|
|
132
|
+
const forwardedIp = forwardedClientIp(req);
|
|
133
|
+
if (forwardedIp && !isLoopbackAddress(forwardedIp)) return forwardedIp;
|
|
134
|
+
const tunnelName = hostHeaderName(tunnelHost);
|
|
135
|
+
return tunnelName ? `tunnel:${tunnelName}` : direct;
|
|
136
|
+
}
|
|
137
|
+
|
|
97
138
|
function isLoopbackAddress(address) {
|
|
98
139
|
const h = stripIpv6Brackets(address).toLowerCase();
|
|
99
140
|
return LOOPBACK_HOSTS.has(h) || h === '::ffff:127.0.0.1';
|
|
100
141
|
}
|
|
101
142
|
|
|
143
|
+
function primaryProcessIpLockoutExemptions(primaryHost) {
|
|
144
|
+
const out = new Set(['127.0.0.1', '::1']);
|
|
145
|
+
const host = stripIpv6Brackets(primaryHost).trim();
|
|
146
|
+
if (host && !isLoopbackHost(host)) out.add(host);
|
|
147
|
+
return Array.from(out);
|
|
148
|
+
}
|
|
149
|
+
|
|
102
150
|
function hasNonLoopbackBrowserOrigin(req) {
|
|
103
151
|
for (const value of [req?.headers?.origin, req?.headers?.referer, req?.headers?.referrer]) {
|
|
104
152
|
const raw = firstHeaderValue(value);
|
|
@@ -112,12 +160,45 @@ function hasNonLoopbackBrowserOrigin(req) {
|
|
|
112
160
|
return false;
|
|
113
161
|
}
|
|
114
162
|
|
|
163
|
+
// Proxy/tunnel forwarding headers a genuinely local browser or CLI never sends.
|
|
164
|
+
// A devtunnel (or any reverse proxy) forwards remote traffic to CTM over the
|
|
165
|
+
// loopback interface, so socket.remoteAddress alone cannot tell local apart from
|
|
166
|
+
// tunneled. Azure Dev Tunnels and every standard proxy stamp X-Forwarded-* /
|
|
167
|
+
// Forwarded on the request; their presence on a loopback peer means the request
|
|
168
|
+
// originated off-box and must NOT be trusted as loopback admin.
|
|
169
|
+
const PROXY_FORWARDING_HEADERS = Object.freeze([
|
|
170
|
+
'x-forwarded-for',
|
|
171
|
+
'x-forwarded-host',
|
|
172
|
+
'x-forwarded-proto',
|
|
173
|
+
'forwarded',
|
|
174
|
+
'x-real-ip',
|
|
175
|
+
'x-ms-original-host',
|
|
176
|
+
'x-original-host',
|
|
177
|
+
'cf-connecting-ip',
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
function hasProxyForwardingMarkers(req) {
|
|
181
|
+
const headers = req?.headers || {};
|
|
182
|
+
for (const name of PROXY_FORWARDING_HEADERS) {
|
|
183
|
+
const value = headers[name];
|
|
184
|
+
if (value !== undefined && value !== null && String(value).trim() !== '') return true;
|
|
185
|
+
}
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// SECURITY INVARIANT: loopback === fully-trusted local admin, but ONLY for
|
|
190
|
+
// requests that are provably on-box. Trust requires ALL of: (1) a loopback socket
|
|
191
|
+
// peer, (2) no proxy/tunnel forwarding markers, (3) no non-loopback browser
|
|
192
|
+
// Origin/Referer, and (4) a loopback (or absent) Host. A managed tunnel forwards
|
|
193
|
+
// remote clients to 127.0.0.1, so any signal that the request originated off-box
|
|
194
|
+
// means it must authenticate with a device token like any other remote client.
|
|
115
195
|
function isLoopbackRequest(req) {
|
|
116
196
|
if (!isLoopbackAddress(req?.socket?.remoteAddress || '')) return false;
|
|
197
|
+
if (hasProxyForwardingMarkers(req)) return false;
|
|
117
198
|
const forwardedHost = trustedForwardedHost(req);
|
|
118
199
|
if (forwardedHost && !isLoopbackHost(hostHeaderName(forwardedHost))) return false;
|
|
119
|
-
const host = hostHeaderName(req?.headers?.host || '');
|
|
120
200
|
if (hasNonLoopbackBrowserOrigin(req)) return false;
|
|
201
|
+
const host = hostHeaderName(req?.headers?.host || '');
|
|
121
202
|
return !host || isLoopbackHost(host);
|
|
122
203
|
}
|
|
123
204
|
|
|
@@ -296,6 +377,8 @@ module.exports = {
|
|
|
296
377
|
normalizeBindHost,
|
|
297
378
|
originAllowed,
|
|
298
379
|
parseAllowedOrigins,
|
|
380
|
+
primaryProcessIpLockoutExemptions,
|
|
381
|
+
requestClientIp,
|
|
299
382
|
requestBrowserOrigin,
|
|
300
383
|
requestOrigin,
|
|
301
384
|
requestProtocol,
|