create-walle 0.10.2 → 0.10.3
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 +3 -1
- package/lib/release-schema.json +2 -2
- package/lib/runtime-closure-policy.json +8 -7
- package/package.json +1 -1
- package/template/coding-task-manager/api-prompts.js +45 -7
- package/template/coding-task-manager/bin/claude-hook.js +55 -1
- package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
- package/template/coding-task-manager/bin/land.js +2069 -0
- package/template/coding-task-manager/bin/memory-census.js +4 -2
- package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
- package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
- package/template/coding-task-manager/db.js +156 -32
- package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
- package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
- package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
- package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
- package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
- package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
- package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
- package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
- package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
- package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
- package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
- package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
- package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
- package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
- package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
- package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
- package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
- package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
- package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
- package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
- package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
- package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
- package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
- package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
- package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
- package/template/coding-task-manager/lib/agent-definitions.js +19 -0
- package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
- package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
- package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
- package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
- package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
- package/template/coding-task-manager/lib/agent-presets.js +84 -1
- package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
- package/template/coding-task-manager/lib/agent-version-service.js +74 -11
- package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
- package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
- package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
- package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
- package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
- package/template/coding-task-manager/lib/api/fleet.js +118 -0
- package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
- package/template/coding-task-manager/lib/api/ingress.js +9 -1
- package/template/coding-task-manager/lib/api/landing.js +403 -0
- package/template/coding-task-manager/lib/api/machines.js +691 -2
- package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
- package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
- package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
- package/template/coding-task-manager/lib/api/session-attach.js +623 -0
- package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
- package/template/coding-task-manager/lib/api/sessions.js +77 -51
- package/template/coding-task-manager/lib/api/setup-network.js +544 -16
- package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
- package/template/coding-task-manager/lib/api/setup.js +829 -10
- package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
- package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
- package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
- package/template/coding-task-manager/lib/auth-rules.js +292 -21
- package/template/coding-task-manager/lib/background-stop.js +369 -0
- package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
- package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
- package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
- package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
- package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
- package/template/coding-task-manager/lib/build-info.js +4 -2
- package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
- package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
- package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
- package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
- package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
- package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
- package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
- package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
- package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
- package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
- package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
- package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
- package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
- package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
- package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
- package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
- package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
- package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
- package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
- package/template/coding-task-manager/lib/conversation-sync.js +355 -0
- package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
- package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
- package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
- package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
- package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
- package/template/coding-task-manager/lib/db-domains.js +1 -0
- package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
- package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
- package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
- package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
- package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
- package/template/coding-task-manager/lib/feature-manifest.js +4 -0
- package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
- package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
- package/template/coding-task-manager/lib/fleet-hub.js +762 -0
- package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
- package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
- package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
- package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
- package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
- package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
- package/template/coding-task-manager/lib/fleet-update.js +798 -0
- package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
- package/template/coding-task-manager/lib/fs-watcher.js +58 -42
- package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
- package/template/coding-task-manager/lib/headless-term-service.js +84 -8
- package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
- package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
- package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
- package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
- package/template/coding-task-manager/lib/host-handoff.js +16 -19
- package/template/coding-task-manager/lib/host-stats.js +150 -3
- package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
- package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
- package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
- package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
- package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
- package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
- package/template/coding-task-manager/lib/hub-bridge.js +398 -0
- package/template/coding-task-manager/lib/idle-notify.js +491 -0
- package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
- package/template/coding-task-manager/lib/instance-manifest.js +5 -1
- package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
- package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
- package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
- package/template/coding-task-manager/lib/landing/config.js +536 -0
- package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
- package/template/coding-task-manager/lib/landing/gate.js +360 -0
- package/template/coding-task-manager/lib/landing/git.js +344 -0
- package/template/coding-task-manager/lib/landing/lease.js +247 -0
- package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
- package/template/coding-task-manager/lib/landing/proof.js +223 -0
- package/template/coding-task-manager/lib/landing/queue.js +998 -0
- package/template/coding-task-manager/lib/landing/radar.js +489 -0
- package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
- package/template/coding-task-manager/lib/landing/state.js +263 -0
- package/template/coding-task-manager/lib/landing/stop.js +49 -0
- package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
- package/template/coding-task-manager/lib/landing/sync.js +456 -0
- package/template/coding-task-manager/lib/landing/train.js +885 -0
- package/template/coding-task-manager/lib/landing/window.js +57 -0
- package/template/coding-task-manager/lib/launch-environment.js +121 -0
- package/template/coding-task-manager/lib/launch-model-override.js +36 -10
- package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
- package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
- package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
- package/template/coding-task-manager/lib/lead-model-options.js +160 -3
- package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
- package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
- package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
- package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
- package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
- package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
- package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
- package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
- package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
- package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
- package/template/coding-task-manager/lib/machine-registry.js +750 -5
- package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
- package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
- package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
- package/template/coding-task-manager/lib/memory-join.js +337 -0
- package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
- package/template/coding-task-manager/lib/memory-sync.js +565 -0
- package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
- package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
- package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
- package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
- package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
- package/template/coding-task-manager/lib/model-availability.js +727 -0
- package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
- package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
- package/template/coding-task-manager/lib/model-id-rule.js +19 -0
- package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
- package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
- package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
- package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
- package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
- package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
- package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
- package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
- package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
- package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
- package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
- package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
- package/template/coding-task-manager/lib/native-history-control.js +6 -4
- package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
- package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
- package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
- package/template/coding-task-manager/lib/opencode-cli.js +480 -0
- package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
- package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
- package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
- package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
- package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
- package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
- package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
- package/template/coding-task-manager/lib/perf-summary.js +7 -0
- package/template/coding-task-manager/lib/perf-tracker.js +63 -2
- package/template/coding-task-manager/lib/project-config.js +7 -0
- package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
- package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
- package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
- package/template/coding-task-manager/lib/read-pool-client.js +89 -2
- package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
- package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
- package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
- package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
- package/template/coding-task-manager/lib/relay-client.js +57 -3
- package/template/coding-task-manager/lib/relink-audit.js +34 -5
- package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
- package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
- package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
- package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
- package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
- package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
- package/template/coding-task-manager/lib/restart-guard.js +18 -6
- package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
- package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
- package/template/coding-task-manager/lib/resume-admission.js +17 -3
- package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
- package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
- package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
- package/template/coding-task-manager/lib/runtime-contract.js +3 -1
- package/template/coding-task-manager/lib/schema-indexes.js +1 -0
- package/template/coding-task-manager/lib/session-context-locator.js +742 -0
- package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
- package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
- package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
- package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
- package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
- package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
- package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
- package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
- package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
- package/template/coding-task-manager/lib/session-create/response.js +7 -7
- package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
- package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
- package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
- package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
- package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
- package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
- package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
- package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
- package/template/coding-task-manager/lib/session-empty.js +13 -10
- package/template/coding-task-manager/lib/session-host-manager.js +60 -0
- package/template/coding-task-manager/lib/session-jobs.js +3 -1
- package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
- package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
- package/template/coding-task-manager/lib/session-park.js +738 -0
- package/template/coding-task-manager/lib/session-path-grant.js +13 -4
- package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
- package/template/coding-task-manager/lib/session-transport.js +17 -0
- package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
- package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
- package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
- package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
- package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
- package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
- package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
- package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
- package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
- package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
- package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
- package/template/coding-task-manager/lib/submission-ack.js +90 -2
- package/template/coding-task-manager/lib/systemd-scope.js +115 -0
- package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
- package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
- package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
- package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
- package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
- package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
- package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
- package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
- package/template/coding-task-manager/lib/transcript-source.js +36 -4
- package/template/coding-task-manager/lib/transcript-store.js +9 -0
- package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
- package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
- package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
- package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
- package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
- package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
- package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
- package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
- package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
- package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
- package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
- package/template/coding-task-manager/lib/walle/turn.js +1 -1
- package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
- package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
- package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
- package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
- package/template/coding-task-manager/lib/walle-row-history.js +495 -0
- package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
- package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
- package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
- package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
- package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
- package/template/coding-task-manager/lib/ws/misc.js +1 -1
- package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
- package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
- package/template/coding-task-manager/oauth-proxy.js +112 -115
- package/template/coding-task-manager/package-lock.json +10 -94
- package/template/coding-task-manager/package.json +4 -3
- package/template/coding-task-manager/protocol/message-table.json +10 -0
- package/template/coding-task-manager/protocol/messages.js +10 -0
- package/template/coding-task-manager/protocol/ws-schema.json +20 -0
- package/template/coding-task-manager/protocol/ws-schema.md +2 -0
- package/template/coding-task-manager/public/css/app.css +170 -0
- package/template/coding-task-manager/public/css/walle-session.css +4 -0
- package/template/coding-task-manager/public/index.html +473 -243
- package/template/coding-task-manager/public/js/agent-command.js +86 -0
- package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
- package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
- package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
- package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
- package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
- package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
- package/template/coding-task-manager/public/js/background-work.js +196 -27
- package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
- package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
- package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
- package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
- package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
- package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
- package/template/coding-task-manager/public/js/document-review-links.js +9 -1
- package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
- package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
- package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
- package/template/coding-task-manager/public/js/message-renderer.js +4 -3
- package/template/coding-task-manager/public/js/model-picker.js +3 -1
- package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
- package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
- package/template/coding-task-manager/public/js/pasted-content.js +236 -0
- package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
- package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
- package/template/coding-task-manager/public/js/prompts.js +11 -10
- package/template/coding-task-manager/public/js/protocol.js +10 -0
- package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
- package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
- package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
- package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
- package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
- package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
- package/template/coding-task-manager/public/js/stream-view.js +6 -5
- package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
- package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
- package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
- package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
- package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
- package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
- package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
- package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
- package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
- package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
- package/template/coding-task-manager/public/js/walle.js +6 -0
- package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
- package/template/coding-task-manager/public/m/app.css +140 -0
- package/template/coding-task-manager/public/m/app.js +507 -68
- package/template/coding-task-manager/public/m/index.html +17 -0
- package/template/coding-task-manager/public/m/machines.js +838 -4
- package/template/coding-task-manager/public/m/private-api.js +31 -1
- package/template/coding-task-manager/public/usage.html +1 -0
- package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
- package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
- package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
- package/template/coding-task-manager/server.js +1950 -11679
- package/template/coding-task-manager/session-integrity.js +64 -4
- package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
- package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
- package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
- package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
- package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
- package/template/coding-task-manager/workers/session-host-process.js +5 -5
- package/template/coding-task-manager/workers/watch-worker.js +105 -0
- package/template/coding-task-manager/workers/worktree-worker.js +168 -33
- package/template/ctm-relay/lib/protocol.js +14 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/shared/backup-retention.js +82 -0
- package/template/shared/conversation-admission.js +2 -0
- package/template/shared/conversation-provenance.js +3 -7
- package/template/shared/conversation-storage-v2-contract.js +5 -0
- package/template/shared/linked-worktree.js +51 -0
- package/template/shared/macos-fda-target.js +57 -0
- package/template/shared/orphan-parents.js +55 -0
- package/template/shared/release-schema.json +2 -2
- package/template/shared/storage/contract.js +3 -0
- package/template/wall-e/adapters/imessage.js +12 -0
- package/template/wall-e/agent.js +71 -4
- package/template/wall-e/api-walle.js +21 -1
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
- package/template/wall-e/brain/brain-join.js +859 -0
- package/template/wall-e/brain/brain-sync-membership.js +20 -0
- package/template/wall-e/brain/brain-sync-store.js +131 -77
- package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
- package/template/wall-e/brain/brain-sync-v2.js +578 -0
- package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
- package/template/wall-e/brain/deterministic-id.js +51 -0
- package/template/wall-e/brain/memories.js +2 -2
- package/template/wall-e/brain/migrations.js +40 -6
- package/template/wall-e/brain/model-identity-migration.js +1 -1
- package/template/wall-e/brain/owner-call-whitelist.js +23 -7
- package/template/wall-e/brain/schema.js +45 -85
- package/template/wall-e/brain.js +171 -35
- package/template/wall-e/chat.js +3 -3
- package/template/wall-e/coding/agent-shell-env.js +7 -2
- package/template/wall-e/coding/prompt-bundle.js +66 -21
- package/template/wall-e/config/main-loop-exemptions.json +2 -2
- package/template/wall-e/http/admin-api.js +37 -0
- package/template/wall-e/http/gws-api.js +4 -0
- package/template/wall-e/http/helper-rules-api.js +7 -1
- package/template/wall-e/http/model-admin.js +8 -1
- package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
- package/template/wall-e/http/model-proxy-providers.js +174 -1
- package/template/wall-e/http/model-route-health.js +157 -0
- package/template/wall-e/http/sync-api.js +278 -464
- package/template/wall-e/jobs/job-runner.js +28 -1
- package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
- package/template/wall-e/lib/ctm-data-dir.js +26 -0
- package/template/wall-e/lib/derivation-home.js +147 -0
- package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
- package/template/wall-e/lib/helper-rules/grade.js +51 -8
- package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
- package/template/wall-e/lib/helper-rules/store.js +4 -2
- package/template/wall-e/lib/landing-mcp-tools.js +170 -0
- package/template/wall-e/lib/mcp-client-config.js +133 -14
- package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
- package/template/wall-e/lib/mcp-integration.js +58 -13
- package/template/wall-e/lib/parent-death.js +7 -1
- package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
- package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
- package/template/wall-e/llm/delegate-vendor-default.js +12 -5
- package/template/wall-e/llm/model-roles.js +1 -1
- package/template/wall-e/llm/native-delegate-selection.js +56 -8
- package/template/wall-e/llm/worker-delegate-args.js +72 -7
- package/template/wall-e/llm/worker-mcp-routing.js +66 -17
- package/template/wall-e/loops/brain-custom-writes.js +19 -0
- package/template/wall-e/loops/ingest.js +17 -1
- package/template/wall-e/loops/think.js +19 -4
- package/template/wall-e/mcp-contract.js +222 -6
- package/template/wall-e/mcp-protocol-contract.js +40 -5
- package/template/wall-e/mcp-server.js +128 -29
- package/template/wall-e/mcp-worker-schemas.js +5 -5
- package/template/wall-e/memory/ctm-context-client.js +85 -0
- package/template/wall-e/memory/ctm-session-context.js +9 -3
- package/template/wall-e/memory/hub-bridge-client.js +388 -0
- package/template/wall-e/memory/replica-origin.js +56 -0
- package/template/wall-e/memory/session-pool.js +135 -0
- package/template/wall-e/runtime/live-capabilities.js +8 -3
- package/template/wall-e/server.js +4 -2
- package/template/wall-e/shared/release-schema.json +2 -2
- package/template/wall-e/shared/sqlite-file-set.js +39 -0
- package/template/wall-e/shared/storage/contract.js +3 -0
- package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
- package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
- package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
- package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
- package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
- package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
- package/template/wall-e/tools/chrome-automation.js +6 -0
- package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
- package/template/wall-e/workers/brain-owner-worker.js +18 -4
- package/template/wall-e/workers/mcp-gateway-process.js +5 -5
|
@@ -0,0 +1,1384 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Landing-throughput WP-5 shard S3 (design doc §5 D-10 "move-only", §6 "Shard",
|
|
4
|
+
// module seam", §8 WP-5, §9 S-21..S-27): the session-messages API cluster moves out
|
|
5
|
+
// of server.js VERBATIM into this module behind the same configure(deps) seam as
|
|
6
|
+
// lib/api/setup-network.js. It registers NO routes -- the routes stay in
|
|
7
|
+
// lib/api/sessions.js, whose configure() call in server.js now receives
|
|
8
|
+
// sessionMessagesApi.apiSessionMessages / sessionMessagesApi.apiGenerateTitles.
|
|
9
|
+
// Each moved body is identical to server.js's declaration except for the `_deps.`
|
|
10
|
+
// prefix on every identifier that used to resolve to a server.js top-level binding
|
|
11
|
+
// (D-10 rule 1), proven by `node bin/move-only-check.js`.
|
|
12
|
+
//
|
|
13
|
+
// Injected deps, per function:
|
|
14
|
+
// apiSessionMessages: SESSION_ID_RE, _RESPONSE_CONTRACT_RANGE_METRICS, _RESPONSE_CONTRACT_STATUS, _RESPONSE_HISTORY_TIMING, _buildConversationPageV4OffThread, _buildSessionMessagesPageResponseFromRowsOffThread, _convFailSoftEnabled, _focusConversationRecovery, _kickUiRowMissImport, _loadSessionMessageExclusionsOffThread, _peekSessionMessageExclusionsMemo, _queueConversationCatchUpIfNeeded, _resolveCanonicalRowSources, _sessionHasCodexMessageSource, _sessionMessageRowsHwmOffThread, _sessionMessagesForegroundBreaker, _sessionRowsFreshnessForSource, _standupStatusText, _walleHistoryHttpCorrelationFromUrl, dbModule, getSession
|
|
15
|
+
// _computePromptMessagesFromFreshestSource: CTM_PROMPT_INDEX_RESOLVE_IN_WORKER, LARGE_SESSION_TAIL_READ_BYTES, _applySessionMessageExclusionsFromRows, _catchUpTranscriptStoreForSession, _computeUserPromptIndexOffThread, _loadSessionMessageExclusionsOffThread, _mergeSessionMessagesWithStreamTail, _messageIndexPromptSourceIds, _perfTracker, _promptRowsFreshness, _promptSummaryFromMessages, _resolveAllSessionFilesOffThread, _sessionHasCodexMessageSource, _sourceWithStreamTail, promptIdentityKey, streamEventToMessage
|
|
16
|
+
// _apiRecentSessionsLegacy: CTM_RECENT_SERIALIZE_CACHE_ENABLED, HOSTNAME, _RECENT_SERIALIZE_CACHE_TTL_MS, _RECENT_SESSIONS_CACHE_TTL, _apiAgentSessionsFromDbRow, _controlSnapshot, _dedupeRecentSessionsByCanonicalId, _getCachedStarredSessionIds, _inRestoreStorm, _isProviderChildAgentRow, _isProviderChildSessionRow, _loadPersistedRecentSessionsCache, _mainTranscriptReads, _perfTracker, _recentSessionActivityMs, _recentSessionFromDbRow, _recentSessionRowsMapCache, _recentSessionsCache, _recentSessionsCacheTime, _recentSessionsPersistLoaded, _recentSessionsSerializedCache, _recentTranscriptPathsForRows, _scheduleRecentSessionsFullRescan, _scheduleRecentSessionsRebuild, _shouldIncludeDbOnlyRecentSession, _starredIdsCacheAt, dbModule, dbRowAgentSessionIds, maxUserMsgCount, remoteMachineCatalog, sessionEmpty
|
|
17
|
+
// buildSessionSearchResponse: CTM_SEARCH_SEMANTIC_MIN_LEXICAL_RESULTS, SessionSearchUtils, _analysisFtsSearchRows, _apiAgentSessionsFromDbRow, _buildSearchSessionMap, _canonicalSearchSessionId, _dedupeSearchRows, _emptySessionSearchResponse, _groupMessageResults, _likeSearchMetadata, _loadSearchCorpusSessions, _mergeSearchSources, _primaryApiAgentSession, _scoreTranscriptEventFtsRows, _searchResultTitleFields, _searchSessionFtsCorpusOffThread, _semanticSessionSearch, _sessionCatalogForSearch, _sessionReferenceSearch, _sessionSearchPlan, _skippedSessionSearchSemanticStatus, dbRowAgentSessionIds, maxUserMsgCount, requestConversationImport
|
|
18
|
+
// apiGenerateTitles: _readSessionCatalogOffThread, callClaudeAsync, cleanFirstMessage, dbModule, isCtmInternalSession, isHumanPromptCandidate, persistTitleStatus, titleGenerationFailureInfo
|
|
19
|
+
//
|
|
20
|
+
// Mutable module state is injected as GETTERS (D-10 rule 2) -- _recentSessionRowsMapCache, _recentSessionsCache, _recentSessionsCacheTime, _recentSessionsPersistLoaded, _recentSessionsSerializedCache, _starredIdsCacheAt.
|
|
21
|
+
// The two the moved bodies also ASSIGN to are injected as getter/setter pairs
|
|
22
|
+
// (_recentSessionsPersistLoaded, _recentSessionsSerializedCache), so the write reaches the same server.js
|
|
23
|
+
// binding instead of a copy frozen at configure() time.
|
|
24
|
+
//
|
|
25
|
+
// apiSessionMessages' four inline `require('./lib/...')` calls are rebased to the
|
|
26
|
+
// module's own static require('../...') (S1 precedent: injecting server.js's
|
|
27
|
+
// `require` is a loader-value transfer the public runtime closure guard refuses).
|
|
28
|
+
// That is the ONE deviation from a pure move-only diff; the checker reports it.
|
|
29
|
+
|
|
30
|
+
let _deps = null;
|
|
31
|
+
|
|
32
|
+
function configure(deps = {}) {
|
|
33
|
+
_deps = deps;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function apiSessionMessages(req, res, url) {
|
|
37
|
+
void req;
|
|
38
|
+
const Protocol = require('../conversation-read-protocol');
|
|
39
|
+
const Cursor = require('../conversation-page-cursor');
|
|
40
|
+
const sendJson = (statusCode, body, headers = {}) => {
|
|
41
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json', ...headers });
|
|
42
|
+
res.end(JSON.stringify(body));
|
|
43
|
+
};
|
|
44
|
+
const sendBuffer = (statusCode, buffer, headers = {}) => {
|
|
45
|
+
const timing = buffer?.[_deps._RESPONSE_HISTORY_TIMING];
|
|
46
|
+
const boundedDuration = (value) => Math.max(0,
|
|
47
|
+
Math.round((Number(value) || 0) * 10) / 10);
|
|
48
|
+
const timingHeaders = timing ? {
|
|
49
|
+
'Server-Timing': [
|
|
50
|
+
`ctm_history;dur=${boundedDuration(timing.totalMs)}`,
|
|
51
|
+
`ctm_queue;dur=${boundedDuration(timing.queueMs)}`,
|
|
52
|
+
`ctm_query;dur=${boundedDuration(timing.queryMs)}`,
|
|
53
|
+
`ctm_render;dur=${boundedDuration(timing.renderMs)}`,
|
|
54
|
+
].join(', '),
|
|
55
|
+
'X-CTM-History-Total-Ms': String(boundedDuration(timing.totalMs)),
|
|
56
|
+
} : {};
|
|
57
|
+
res.writeHead(statusCode, {
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
...headers,
|
|
60
|
+
...timingHeaders,
|
|
61
|
+
});
|
|
62
|
+
res.end(buffer);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const sessionId = String(url.searchParams.get('id') || '').trim();
|
|
66
|
+
const historyHttpCorrelation = _deps._walleHistoryHttpCorrelationFromUrl(url);
|
|
67
|
+
if (historyHttpCorrelation) {
|
|
68
|
+
const correlation = historyHttpCorrelation.fields;
|
|
69
|
+
res.setHeader('X-CTM-History-Request-Id', String(correlation.historyRequestId || 0));
|
|
70
|
+
res.setHeader('X-CTM-History-Contract-Version', String(correlation.historyContractVersion || 0));
|
|
71
|
+
res.setHeader('X-CTM-History-Cycle-Id', String(correlation.historyCycleId || 0));
|
|
72
|
+
res.setHeader('X-CTM-History-Mode', correlation.historyMode);
|
|
73
|
+
res.setHeader('X-CTM-History-Cursor', encodeURIComponent(correlation.requestedCursor || ''));
|
|
74
|
+
res.setHeader('X-CTM-History-Requested-Generation', String(correlation.requestedGeneration));
|
|
75
|
+
res.setHeader(
|
|
76
|
+
'X-CTM-History-Requested-Ordering-Generation',
|
|
77
|
+
String(correlation.requestedOrderingGeneration),
|
|
78
|
+
);
|
|
79
|
+
// H2 controller incarnation echo (same-realm correlation, never an auth/proof claim):
|
|
80
|
+
// emitted ONLY when the request carried the marker own. A valid marker echoes its canonical
|
|
81
|
+
// decimal text; the explicit null sentinel is labeled 'invalid' -- never '0', never empty,
|
|
82
|
+
// never undefined and never an omission. A truly markerless request adds no header at all.
|
|
83
|
+
if (Object.prototype.hasOwnProperty.call(correlation, 'historyControllerInstanceId')) {
|
|
84
|
+
res.setHeader(
|
|
85
|
+
'X-CTM-History-Controller-Instance-Id',
|
|
86
|
+
correlation.historyControllerInstanceId === null
|
|
87
|
+
? 'invalid'
|
|
88
|
+
: String(correlation.historyControllerInstanceId),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
if (historyHttpCorrelation.error) {
|
|
92
|
+
sendJson(400, {
|
|
93
|
+
...Protocol.conversationErrorEnvelope({
|
|
94
|
+
status: 'invalid_request', reason: historyHttpCorrelation.error,
|
|
95
|
+
}),
|
|
96
|
+
error: historyHttpCorrelation.error,
|
|
97
|
+
...correlation,
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (!sessionId) {
|
|
103
|
+
sendJson(400, Protocol.conversationErrorEnvelope({
|
|
104
|
+
status: 'invalid_request', reason: 'missing_session_id',
|
|
105
|
+
}));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!_deps.SESSION_ID_RE.test(sessionId)) {
|
|
109
|
+
sendJson(400, Protocol.conversationErrorEnvelope({
|
|
110
|
+
status: 'invalid_request', reason: 'invalid_session_id',
|
|
111
|
+
}));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const beforeCursor = String(url.searchParams.get('before') || '').trim();
|
|
116
|
+
const afterCursor = String(url.searchParams.get('afterCursor') || '').trim();
|
|
117
|
+
const rangeCursor = String(url.searchParams.get('rangeCursor') || '').trim();
|
|
118
|
+
const cursorInputs = [beforeCursor, afterCursor, rangeCursor].filter(Boolean);
|
|
119
|
+
if (cursorInputs.length > 1) {
|
|
120
|
+
sendJson(400, Protocol.conversationErrorEnvelope({
|
|
121
|
+
status: 'invalid_cursor', reason: 'multiple_cursor_parameters',
|
|
122
|
+
}));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const cursorToken = cursorInputs[0] || '';
|
|
126
|
+
const cursorDirection = beforeCursor ? 'older' : (afterCursor ? 'newer' : 'range');
|
|
127
|
+
const paginationMode = url.searchParams.get('mode') === 'turns' ? 'turns' : 'messages';
|
|
128
|
+
let offset = Math.max(0, parseInt(url.searchParams.get('offset') || '0', 10) || 0);
|
|
129
|
+
let exactRange = false;
|
|
130
|
+
let limit = Math.max(1, Math.min(
|
|
131
|
+
Cursor.MAX_PAGE_SIZE,
|
|
132
|
+
parseInt(url.searchParams.get('limit') || '200', 10) || 200,
|
|
133
|
+
));
|
|
134
|
+
const compactConversation = paginationMode === 'turns'
|
|
135
|
+
&& (url.searchParams.get('compact') === 'conversation'
|
|
136
|
+
|| url.searchParams.get('conversationCompact') === '1');
|
|
137
|
+
const requestedContract = Number(url.searchParams.get('conversationContract'));
|
|
138
|
+
const conversationContract = requestedContract === 4 ? 4
|
|
139
|
+
: (requestedContract === 3 && process.env.CTM_CONVERSATION_V3 !== '0' ? 3 : 2);
|
|
140
|
+
|
|
141
|
+
// A contract-4 client never receives the contract-2 pending envelope: it gets the typed v4 503.
|
|
142
|
+
const sendReaderUnavailable = () => {
|
|
143
|
+
Protocol.logConversationServeRefusal(sessionId, 'canonical_reader_unavailable');
|
|
144
|
+
if (conversationContract === 4) {
|
|
145
|
+
sendJson(503, {
|
|
146
|
+
contractVersion: 4, status: 'error', transient: true, retryAfterMs: 750, reason: 'canonical_reader_unavailable',
|
|
147
|
+
}, { 'Retry-After': '1', 'Cache-Control': 'no-store' });
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
sendJson(200, Protocol.pendingConversationEnvelope({ reason: 'canonical_reader_unavailable' }), { 'Retry-After': '1' });
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
_deps._focusConversationRecovery(sessionId);
|
|
154
|
+
const resolution = _deps._resolveCanonicalRowSources(sessionId);
|
|
155
|
+
const canonicalSessionId = String(resolution?.canonicalSessionId || sessionId).trim();
|
|
156
|
+
const rowSources = Array.isArray(resolution?.sources) && resolution.sources.length > 0
|
|
157
|
+
? resolution.sources
|
|
158
|
+
: [{ id: canonicalSessionId, reason: 'canonical-fallback', rank: 1 }];
|
|
159
|
+
// D-3 (WP-2): a foreground viewer must never trade a real page for a fabricated pending
|
|
160
|
+
// envelope just because an auxiliary read (exclusions) or one row source's HWM failed.
|
|
161
|
+
// CTM_CONV_FAILSOFT=0 restores today's fail-hard-to-pending semantics byte-for-byte.
|
|
162
|
+
const failSoftEnabled = _deps._convFailSoftEnabled();
|
|
163
|
+
let exclusionRows;
|
|
164
|
+
let exclusionsDegraded = false;
|
|
165
|
+
try {
|
|
166
|
+
exclusionRows = await _deps._loadSessionMessageExclusionsOffThread(sessionId);
|
|
167
|
+
} catch {
|
|
168
|
+
if (!failSoftEnabled) {
|
|
169
|
+
sendReaderUnavailable();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
exclusionRows = _deps._peekSessionMessageExclusionsMemo(sessionId) || [];
|
|
173
|
+
exclusionsDegraded = true;
|
|
174
|
+
Protocol.logConversationServeRefusal(sessionId, 'exclusions_degraded');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let selectedRowSource = null;
|
|
178
|
+
let canonicalSourceKnown = false;
|
|
179
|
+
let hwmAttempted = 0;
|
|
180
|
+
let hwmFailed = 0;
|
|
181
|
+
for (const rowSource of rowSources) {
|
|
182
|
+
const rowSourceId = String(rowSource?.id || '').trim();
|
|
183
|
+
if (!rowSourceId) continue;
|
|
184
|
+
hwmAttempted += 1;
|
|
185
|
+
let rowHwm;
|
|
186
|
+
try {
|
|
187
|
+
rowHwm = await _deps._sessionMessageRowsHwmOffThread(rowSourceId, sessionId, {
|
|
188
|
+
preferRichProjection: true,
|
|
189
|
+
breaker: _deps._sessionMessagesForegroundBreaker,
|
|
190
|
+
});
|
|
191
|
+
} catch {
|
|
192
|
+
if (!failSoftEnabled) {
|
|
193
|
+
sendReaderUnavailable();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
hwmFailed += 1;
|
|
197
|
+
Protocol.logConversationServeRefusal(sessionId, 'row_hwm_degraded');
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const freshness = _deps._sessionRowsFreshnessForSource(sessionId, rowSourceId, {
|
|
201
|
+
knownRowCount: Number(rowHwm?.rowCount || 0),
|
|
202
|
+
});
|
|
203
|
+
const sourceKnown = rowSource?.reason !== 'fallback'
|
|
204
|
+
|| Number(freshness?.expectedSize || 0) > 0
|
|
205
|
+
|| Number(rowHwm?.rowCount || 0) > 0;
|
|
206
|
+
canonicalSourceKnown = canonicalSourceKnown || sourceKnown;
|
|
207
|
+
if (!sourceKnown) continue;
|
|
208
|
+
const isCanonical = rowSource?.isCanonical === true || rowSourceId === canonicalSessionId;
|
|
209
|
+
if (!isCanonical) continue;
|
|
210
|
+
if (!(Number(rowHwm?.rowCount) > 0) && freshness?.fresh !== true) continue;
|
|
211
|
+
selectedRowSource = { rowSource, rowSourceId, rowHwm, freshness };
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
// Only when EVERY attempted source failed does this degrade to the same pending envelope
|
|
215
|
+
// the old unconditional-return-on-first-failure code produced.
|
|
216
|
+
if (!selectedRowSource && failSoftEnabled && hwmAttempted > 0 && hwmFailed === hwmAttempted) {
|
|
217
|
+
sendReaderUnavailable();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (conversationContract === 4 && !selectedRowSource) {
|
|
222
|
+
// Same targeted import the v2/v3 tail triggers, so a source that has no rows yet gets some.
|
|
223
|
+
_deps._kickUiRowMissImport(sessionId, rowSources, canonicalSourceKnown);
|
|
224
|
+
sendJson(503, {
|
|
225
|
+
contractVersion: 4, status: 'error', transient: true, retryAfterMs: 750,
|
|
226
|
+
reason: 'no_canonical_source',
|
|
227
|
+
}, { 'Retry-After': '1', 'Cache-Control': 'no-store' });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (selectedRowSource) {
|
|
232
|
+
const { rowSourceId, rowHwm, freshness } = selectedRowSource;
|
|
233
|
+
if (freshness?.fresh !== true) {
|
|
234
|
+
const recoverySourceIds = [...new Set(rowSources
|
|
235
|
+
.map((source) => String(source?.id || '').trim())
|
|
236
|
+
.filter(Boolean))].slice(0, 16);
|
|
237
|
+
_deps._queueConversationCatchUpIfNeeded(
|
|
238
|
+
sessionId,
|
|
239
|
+
freshness,
|
|
240
|
+
'canonical-row-page-stale',
|
|
241
|
+
{ sourceIds: recoverySourceIds },
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (conversationContract === 4) {
|
|
245
|
+
const param = (name) => String(url.searchParams.get(name) || '').trim() || undefined;
|
|
246
|
+
let sessionGenerating = false;
|
|
247
|
+
try {
|
|
248
|
+
sessionGenerating = ['busy', 'running', 'working'].includes(_deps._standupStatusText(_deps.getSession(sessionId)));
|
|
249
|
+
} catch {
|
|
250
|
+
/* expected: an unloaded/unknown session is simply not generating */
|
|
251
|
+
}
|
|
252
|
+
const v4Payload = {
|
|
253
|
+
dbPath: _deps.dbModule.getDbPath(),
|
|
254
|
+
sessionId: rowSourceId,
|
|
255
|
+
before: param('before'),
|
|
256
|
+
after: param('after'),
|
|
257
|
+
around: param('around'),
|
|
258
|
+
entries: param('entries'),
|
|
259
|
+
limitTurns: Number(url.searchParams.get('limitTurns')) || undefined,
|
|
260
|
+
maxBytes: Number(url.searchParams.get('maxBytes')) || undefined,
|
|
261
|
+
outline: url.searchParams.get('outline') === '1',
|
|
262
|
+
resident: param('resident'),
|
|
263
|
+
sessionGenerating,
|
|
264
|
+
freshness: { fresh: freshness?.fresh === true },
|
|
265
|
+
};
|
|
266
|
+
let result = await _deps._buildConversationPageV4OffThread(v4Payload);
|
|
267
|
+
if (result?.error?.code === 'turn_index_missing') {
|
|
268
|
+
// The read-only worker cannot rebuild a missing index: heal on the writer DB, retry once.
|
|
269
|
+
try {
|
|
270
|
+
const healStartedAt = Date.now();
|
|
271
|
+
require('../conversation-turn-index').ensureTurnIndex(_deps.dbModule.getDb(), rowSourceId);
|
|
272
|
+
console.warn(`[conversation-v4] main-thread turn-index heal session=${rowSourceId} ms=${Date.now() - healStartedAt}`);
|
|
273
|
+
result = await _deps._buildConversationPageV4OffThread(v4Payload);
|
|
274
|
+
} catch (healError) {
|
|
275
|
+
result = { error: { code: 'turn_index_missing', message: String(healError?.message || healError).slice(0, 300) } };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (Buffer.isBuffer(result)) {
|
|
279
|
+
const status = String(result[_deps._RESPONSE_CONTRACT_STATUS] || 'ready');
|
|
280
|
+
const statusCode = status === 'range-invalidated' ? 409 : (status === 'invalid_request' ? 400 : 200);
|
|
281
|
+
sendBuffer(statusCode, result, {
|
|
282
|
+
'Cache-Control': 'no-store',
|
|
283
|
+
...(status === 'syncing' ? { 'Retry-After': '1' } : {}),
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
sendJson(503, {
|
|
288
|
+
contractVersion: 4, status: 'error', transient: true, retryAfterMs: 750,
|
|
289
|
+
reason: String(result?.error?.code || 'canonical_reader_unavailable'),
|
|
290
|
+
}, { 'Retry-After': '1', 'Cache-Control': 'no-store' });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (conversationContract === 3) {
|
|
294
|
+
const result = await _deps._buildSessionMessagesPageResponseFromRowsOffThread({
|
|
295
|
+
contractVersion: 3,
|
|
296
|
+
dbPath: _deps.dbModule.getDbPath(),
|
|
297
|
+
sessionId,
|
|
298
|
+
sessionImagesDir: _deps.dbModule.SESSION_IMAGES_DIR,
|
|
299
|
+
rowSourceId,
|
|
300
|
+
exclusionRows,
|
|
301
|
+
filterCodexSyntheticBase: _deps._sessionHasCodexMessageSource(sessionId),
|
|
302
|
+
mode: paginationMode,
|
|
303
|
+
direction: cursorDirection,
|
|
304
|
+
cursor: cursorToken || null,
|
|
305
|
+
limit,
|
|
306
|
+
maxSourceRows: 200,
|
|
307
|
+
compactConversation,
|
|
308
|
+
syncing: freshness?.fresh !== true,
|
|
309
|
+
telemetryReason: 'canonical-row-page-v3',
|
|
310
|
+
});
|
|
311
|
+
if (Buffer.isBuffer(result)) {
|
|
312
|
+
const status = String(result[_deps._RESPONSE_CONTRACT_STATUS] || 'error');
|
|
313
|
+
require('../conversation-range-metrics').recordAuthoritativePage(
|
|
314
|
+
{
|
|
315
|
+
...(result[_deps._RESPONSE_CONTRACT_RANGE_METRICS] || { sessionId: rowSourceId, status }),
|
|
316
|
+
...(result[_deps._RESPONSE_HISTORY_TIMING] || {}),
|
|
317
|
+
requestId: Number(historyHttpCorrelation?.fields?.historyRequestId) || 0,
|
|
318
|
+
},
|
|
319
|
+
);
|
|
320
|
+
const statusCode = status === 'range-invalidated' ? 409 : (status === 'error' ? 503 : 200);
|
|
321
|
+
const headers = { 'Cache-Control': 'no-store' };
|
|
322
|
+
if (status === 'upgrading' || status === 'syncing' || status === 'error') headers['Retry-After'] = '1';
|
|
323
|
+
sendBuffer(statusCode, result, headers);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
Protocol.logConversationServeRefusal(sessionId, 'canonical_reader_unavailable');
|
|
327
|
+
sendJson(503, Protocol.pendingConversationEnvelope({
|
|
328
|
+
reason: 'canonical_reader_unavailable',
|
|
329
|
+
}), { 'Retry-After': '1' });
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const generation = Math.max(0, Number(rowHwm?.projectionEpoch) || 0);
|
|
333
|
+
const identity = Protocol.canonicalTimelineIdentity(canonicalSessionId, generation);
|
|
334
|
+
if (cursorToken) {
|
|
335
|
+
const validation = Cursor.validateConversationPageCursor(cursorToken, {
|
|
336
|
+
sessionId,
|
|
337
|
+
identity,
|
|
338
|
+
generation,
|
|
339
|
+
mode: paginationMode,
|
|
340
|
+
direction: cursorDirection,
|
|
341
|
+
// Message counts cannot validate turn coordinates. Exact upper-bound
|
|
342
|
+
// validation is repeated by the worker against the requested domain.
|
|
343
|
+
maxOffset: paginationMode === 'messages'
|
|
344
|
+
? Math.max(0, Number(rowHwm?.rowCount) || 0)
|
|
345
|
+
: undefined,
|
|
346
|
+
allowLegacy: true,
|
|
347
|
+
});
|
|
348
|
+
if (!validation.ok) {
|
|
349
|
+
if (validation.error === 'stale_cursor') {
|
|
350
|
+
sendJson(409, Protocol.staleCursorEnvelope({
|
|
351
|
+
identity,
|
|
352
|
+
generation,
|
|
353
|
+
reason: validation.field || 'timeline',
|
|
354
|
+
}));
|
|
355
|
+
} else {
|
|
356
|
+
sendJson(400, Protocol.conversationErrorEnvelope({
|
|
357
|
+
status: 'invalid_cursor', reason: validation.field || 'invalid_cursor',
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
offset = validation.cursor.offset;
|
|
363
|
+
if (validation.cursor.version === Cursor.CURSOR_VERSION) {
|
|
364
|
+
limit = validation.cursor.size;
|
|
365
|
+
exactRange = true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const observedAt = new Date().toISOString();
|
|
369
|
+
const settled = freshness?.fresh === true;
|
|
370
|
+
const extra = {
|
|
371
|
+
settled,
|
|
372
|
+
catchingUp: !settled,
|
|
373
|
+
cacheFresh: settled,
|
|
374
|
+
readiness: {
|
|
375
|
+
reason: settled ? 'complete' : 'catching_up',
|
|
376
|
+
rowCount: Number(rowHwm.rowCount),
|
|
377
|
+
lagBytes: Math.max(0, Number(freshness?.lagBytes || 0)),
|
|
378
|
+
},
|
|
379
|
+
total_messages: Number(rowHwm.rowCount),
|
|
380
|
+
rowHwm,
|
|
381
|
+
rowSourceId,
|
|
382
|
+
requestedId: sessionId,
|
|
383
|
+
canonicalSessionId,
|
|
384
|
+
observedAt,
|
|
385
|
+
skipStreamTail: true,
|
|
386
|
+
// D-4-server additive field (WP-2 item 6): the page still served, but an auxiliary
|
|
387
|
+
// read degraded to last-known-good — never fabricated rows, just a badge hook for
|
|
388
|
+
// the client (WP-1 D-4). Omitted entirely (not `false`) on the healthy path so the
|
|
389
|
+
// shape is byte-identical to before this WP for the common case.
|
|
390
|
+
...(exclusionsDegraded ? { exclusionsDegraded: true } : {}),
|
|
391
|
+
};
|
|
392
|
+
const result = await _deps._buildSessionMessagesPageResponseFromRowsOffThread({
|
|
393
|
+
dbPath: _deps.dbModule.getDbPath(),
|
|
394
|
+
sessionId,
|
|
395
|
+
sessionImagesDir: _deps.dbModule.SESSION_IMAGES_DIR,
|
|
396
|
+
rowSourceId,
|
|
397
|
+
streamEvents: [],
|
|
398
|
+
skipStreamTail: true,
|
|
399
|
+
// Store selection follows the exact HWM snapshot above. Freshness controls
|
|
400
|
+
// settled/catching-up truth, not which store owns that snapshot: choosing v2
|
|
401
|
+
// after a rich HWM was published creates a split 6-row count / 4-row payload.
|
|
402
|
+
// The worker still requires active Wall-E cutover + v2 coverage + exact count.
|
|
403
|
+
preferRichProjection: true,
|
|
404
|
+
expectedRowCount: Number(rowHwm.rowCount),
|
|
405
|
+
exclusionRows,
|
|
406
|
+
filterCodexSyntheticBase: _deps._sessionHasCodexMessageSource(sessionId),
|
|
407
|
+
extra,
|
|
408
|
+
publicIdentity: identity,
|
|
409
|
+
cursorContext: {
|
|
410
|
+
sessionId,
|
|
411
|
+
canonicalSessionId,
|
|
412
|
+
identity,
|
|
413
|
+
generation,
|
|
414
|
+
},
|
|
415
|
+
offset,
|
|
416
|
+
limit,
|
|
417
|
+
exactRange,
|
|
418
|
+
allowAuthoritativeEmpty: settled && Number(rowHwm.rowCount) === 0,
|
|
419
|
+
mode: paginationMode,
|
|
420
|
+
compactConversation,
|
|
421
|
+
telemetryReason: 'canonical-row-page',
|
|
422
|
+
});
|
|
423
|
+
if (result === 'empty') selectedRowSource = null;
|
|
424
|
+
if (result?.staleCursor === true) {
|
|
425
|
+
sendJson(409, Protocol.staleCursorEnvelope({
|
|
426
|
+
identity,
|
|
427
|
+
generation,
|
|
428
|
+
reason: result.field || 'offset',
|
|
429
|
+
...Protocol.resumeHistoryObservationFields(result),
|
|
430
|
+
}));
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
if (Buffer.isBuffer(result)) {
|
|
434
|
+
sendBuffer(200, result);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (result !== 'empty') {
|
|
438
|
+
Protocol.logConversationServeRefusal(sessionId, 'canonical_reader_unavailable');
|
|
439
|
+
sendJson(200, Protocol.pendingConversationEnvelope({
|
|
440
|
+
reason: 'canonical_reader_unavailable',
|
|
441
|
+
}), { 'Retry-After': '1' });
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (cursorToken) {
|
|
447
|
+
sendJson(409, Protocol.staleCursorEnvelope({ reason: 'canonical_rows_not_ready' }));
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const sourceIds = _deps._kickUiRowMissImport(sessionId, rowSources, canonicalSourceKnown);
|
|
452
|
+
const _finalPendingReason = sourceIds.length > 0 ? 'canonical_rows_not_ready' : 'canonical_source_unavailable';
|
|
453
|
+
Protocol.logConversationServeRefusal(sessionId, _finalPendingReason);
|
|
454
|
+
sendJson(200, Protocol.pendingConversationEnvelope({
|
|
455
|
+
source: 'row_import_pending',
|
|
456
|
+
reason: _finalPendingReason,
|
|
457
|
+
}), { 'Retry-After': '1' });
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function _computePromptMessagesFromFreshestSource(sessionId) {
|
|
461
|
+
// The Queue Builder only needs the USER-PROMPT list, not the full conversation
|
|
462
|
+
// timeline. Building the full conversation projection on every prompt fetch
|
|
463
|
+
// does a SQLite read + JSON.parse of the entire cached
|
|
464
|
+
// conversation + transcript-store read + a merge/exclusions pass over the whole
|
|
465
|
+
// message array and was observed blocking the event loop up to 10s during a
|
|
466
|
+
// Codex "running" burst. The row-level message index
|
|
467
|
+
// already holds user prompts as a bounded, indexed query and is kept fresh by
|
|
468
|
+
// the same importer that powers Conversation/Review. So serve from the index
|
|
469
|
+
// FIRST + the live stream tail. An empty index degrades to that bounded tail
|
|
470
|
+
// while a background import converges; it never reconstructs history in-request.
|
|
471
|
+
// Phase 1: the heavy part (SELECT all user rows + sort + exclusions + sha1-per-prompt
|
|
472
|
+
// summary) runs on the read-pool worker (read-only connection), off the main loop. The 3
|
|
473
|
+
// small INPUTS (sourceIds/exclusions/codex-source) are resolved on the worker too by default
|
|
474
|
+
// (CTM_PROMPT_INDEX_RESOLVE_IN_WORKER) — that cold per-session resolution, stacked across a
|
|
475
|
+
// restore storm, is the apiSessionPrompts freeze. When the flag is off we resolve them here.
|
|
476
|
+
let indexSourceIds = _deps.CTM_PROMPT_INDEX_RESOLVE_IN_WORKER ? null : _deps._messageIndexPromptSourceIds(sessionId);
|
|
477
|
+
let indexExclusionRows = _deps.CTM_PROMPT_INDEX_RESOLVE_IN_WORKER
|
|
478
|
+
? null
|
|
479
|
+
: await _deps._loadSessionMessageExclusionsOffThread(sessionId);
|
|
480
|
+
const indexResult = await _deps._computeUserPromptIndexOffThread(
|
|
481
|
+
sessionId,
|
|
482
|
+
_deps.CTM_PROMPT_INDEX_RESOLVE_IN_WORKER ? null : {
|
|
483
|
+
sourceIds: indexSourceIds,
|
|
484
|
+
exclusionRows: indexExclusionRows,
|
|
485
|
+
filterCodexSynthetic: _deps._sessionHasCodexMessageSource(sessionId),
|
|
486
|
+
}
|
|
487
|
+
);
|
|
488
|
+
// The worker echoes the resolved sourceIds back (main still needs them below for the
|
|
489
|
+
// stream-tail merge + deferred placeholder). On a breaker short-circuit / fallback the worker
|
|
490
|
+
// didn't resolve → [] (the deferred list is empty and sourceIds is only a downstream hint there).
|
|
491
|
+
if (indexSourceIds == null) {
|
|
492
|
+
indexSourceIds = Array.isArray(indexResult._resolvedSourceIds) ? indexResult._resolvedSourceIds : [];
|
|
493
|
+
}
|
|
494
|
+
if (indexExclusionRows == null) {
|
|
495
|
+
indexExclusionRows = Array.isArray(indexResult._resolvedExclusionRows)
|
|
496
|
+
? indexResult._resolvedExclusionRows
|
|
497
|
+
: [];
|
|
498
|
+
}
|
|
499
|
+
const indexMessages = indexResult.messages || [];
|
|
500
|
+
if (indexResult.capped) {
|
|
501
|
+
console.warn(`[prompt-index] sid=${String(sessionId).slice(0, 8)} hit the per-session row ceiling (rowCount=${indexResult.rowCount}) — prompt nav shows the most-recent prompts only (defensive cap, not a data loss).`);
|
|
502
|
+
}
|
|
503
|
+
const indexRowsFreshness = _deps._promptRowsFreshness(sessionId, indexSourceIds || []);
|
|
504
|
+
|
|
505
|
+
if (indexResult._deferred) {
|
|
506
|
+
// Restore storm: the off-thread prompt-index compute was deferred to avoid a main-thread
|
|
507
|
+
// freeze and there's nothing cached to serve. Return an empty, explicitly-deferred result
|
|
508
|
+
// instead of running the heavy timeline projection (multi-MB JSON.parse) on the loop here.
|
|
509
|
+
// Not cached as final (see _loadPromptMessagesFromFreshestSource) → the next poll re-fetches.
|
|
510
|
+
return {
|
|
511
|
+
source: 'deferred',
|
|
512
|
+
promptMessages: [],
|
|
513
|
+
responsePrompts: [],
|
|
514
|
+
promptFingerprint: '',
|
|
515
|
+
lastPromptPreview: '',
|
|
516
|
+
userPromptCount: 0,
|
|
517
|
+
sourceIds: indexSourceIds || [],
|
|
518
|
+
streamTailAdded: 0,
|
|
519
|
+
cacheDetails: {},
|
|
520
|
+
cacheAvailable: false,
|
|
521
|
+
cacheFresh: false,
|
|
522
|
+
indexPromptCount: 0,
|
|
523
|
+
cachePromptCount: 0,
|
|
524
|
+
_deferred: true,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
if (indexMessages.length > 0) {
|
|
529
|
+
if (indexRowsFreshness.fresh !== true) {
|
|
530
|
+
// Fire-and-forget freshness nudge: the index is stale, so schedule the background worker
|
|
531
|
+
// catch-up that makes the NEXT poll fresh. We serve THIS response from the index we already
|
|
532
|
+
// have — so this nudge must not block the response. Previously it ran inline on the request
|
|
533
|
+
// path and did synchronous cold fs (`_resolveAllSessionFiles`) + a throwaway
|
|
534
|
+
// durable transcript-store read whose result we discard, which is the
|
|
535
|
+
// low-CPU / high-latency half of the apiSessionPrompts freeze (I/O-wait, up to 3.4s). Defer it
|
|
536
|
+
// to a later tick, off the response's critical path, and probe it so [perf-lag] attributes its
|
|
537
|
+
// residual cost by name instead of as `apiSessionPrompts (last completed)`.
|
|
538
|
+
setImmediate(() => {
|
|
539
|
+
_deps._perfTracker.runSyncProbed('prompts:stale-catchup-schedule', () => {
|
|
540
|
+
_deps._resolveAllSessionFilesOffThread(sessionId, '', { origin: 'prompt-nudge:stale-index' }).then((filePaths) => {
|
|
541
|
+
if (filePaths.length > 0) {
|
|
542
|
+
return _deps._catchUpTranscriptStoreForSession(sessionId, filePaths, {
|
|
543
|
+
reason: 'prompt-index-source-stale',
|
|
544
|
+
scheduleOnly: true,
|
|
545
|
+
maxBytesPerFile: _deps.LARGE_SESSION_TAIL_READ_BYTES,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
return null;
|
|
549
|
+
}).catch(() => {});
|
|
550
|
+
}, { context: () => ({ sid: String(sessionId).slice(0, 8) }) });
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
// Post-await sync chunk — the perf-tracker tag set by the parent
|
|
554
|
+
// apiSessionPrompts wrap was cleared at the await above, so without
|
|
555
|
+
// this probe these N-message merge/filter/summary passes log as
|
|
556
|
+
// active op:(unknown). Named here so [perf-lag] and the topOps trail
|
|
557
|
+
// in /api/diagnostics/event-loop can attribute the block.
|
|
558
|
+
return _deps._perfTracker.runSyncProbed(
|
|
559
|
+
'prompts:index-merge-respond',
|
|
560
|
+
() => {
|
|
561
|
+
// Index hit — the common, hot path. Apply the live stream tail (recent prompts
|
|
562
|
+
// not yet flushed to the index) without touching the conversation cache.
|
|
563
|
+
const durableUserTailKeys = new Set(
|
|
564
|
+
Array.isArray(indexResult.userPromptTailIdentityKeys)
|
|
565
|
+
? indexResult.userPromptTailIdentityKeys
|
|
566
|
+
: [],
|
|
567
|
+
);
|
|
568
|
+
const merge = _deps._mergeSessionMessagesWithStreamTail(sessionId, indexMessages, {
|
|
569
|
+
allowEmptyBase: true,
|
|
570
|
+
limit: 200,
|
|
571
|
+
eventFilter: (event) => {
|
|
572
|
+
const message = _deps.streamEventToMessage(event);
|
|
573
|
+
if (!message || String(message.role || '').trim() !== 'user') return false;
|
|
574
|
+
return !durableUserTailKeys.has(_deps.promptIdentityKey(message));
|
|
575
|
+
},
|
|
576
|
+
});
|
|
577
|
+
const mergedTailAdded = Math.max(0, (merge.messages || []).length - indexMessages.length);
|
|
578
|
+
const sourceIds = Array.from(new Set([
|
|
579
|
+
...(indexSourceIds || []),
|
|
580
|
+
...((merge.identity && merge.identity.conversationSourceIds) || []),
|
|
581
|
+
]));
|
|
582
|
+
let withTail;
|
|
583
|
+
let sourceSummary;
|
|
584
|
+
let responsePrompts;
|
|
585
|
+
if (mergedTailAdded === 0) {
|
|
586
|
+
// No live prompt beyond the index — reuse the worker's filtered list + summary as-is
|
|
587
|
+
// (zero O(N) work on the main thread). This is the hot path for an active session.
|
|
588
|
+
withTail = indexMessages;
|
|
589
|
+
sourceSummary = { ...indexResult.summary, sourceIds };
|
|
590
|
+
responsePrompts = Array.isArray(indexResult.responsePrompts)
|
|
591
|
+
? indexResult.responsePrompts
|
|
592
|
+
: [];
|
|
593
|
+
} else {
|
|
594
|
+
// Live prompts pending in the stream tail — fold them in on main (bounded: tail<=200).
|
|
595
|
+
withTail = (_deps._applySessionMessageExclusionsFromRows(
|
|
596
|
+
sessionId,
|
|
597
|
+
merge.messages,
|
|
598
|
+
indexExclusionRows,
|
|
599
|
+
{ codexSynthetic: true },
|
|
600
|
+
) || [])
|
|
601
|
+
.filter(m => String(m?.role || '').trim() === 'user');
|
|
602
|
+
sourceSummary = _deps._promptSummaryFromMessages(withTail, sourceIds);
|
|
603
|
+
responsePrompts = sourceSummary.responsePrompts;
|
|
604
|
+
}
|
|
605
|
+
const streamTailAdded = Math.max(0, withTail.length - indexMessages.length);
|
|
606
|
+
// The read-pool summary already counted filtered user prompts. Reuse it
|
|
607
|
+
// instead of reducing the full prompt array again on the main loop.
|
|
608
|
+
const indexPromptCount = Number(indexResult.summary?.userPromptCount || 0);
|
|
609
|
+
const indexFresh = indexRowsFreshness.fresh === true;
|
|
610
|
+
return {
|
|
611
|
+
source: streamTailAdded > 0
|
|
612
|
+
? _deps._sourceWithStreamTail(indexFresh ? 'message-index' : 'message-index-stale', streamTailAdded)
|
|
613
|
+
: (indexFresh ? 'message-index' : 'message-index-stale'),
|
|
614
|
+
promptMessages: withTail,
|
|
615
|
+
responsePrompts,
|
|
616
|
+
promptFingerprint: sourceSummary.promptFingerprint,
|
|
617
|
+
lastPromptPreview: sourceSummary.lastPromptPreview,
|
|
618
|
+
userPromptCount: sourceSummary.userPromptCount,
|
|
619
|
+
sourceIds: sourceSummary.sourceIds,
|
|
620
|
+
streamTailAdded,
|
|
621
|
+
cacheDetails: {
|
|
622
|
+
cacheAvailable: true,
|
|
623
|
+
cacheFresh: indexFresh,
|
|
624
|
+
staleSourceIds: indexRowsFreshness.staleSourceIds || [],
|
|
625
|
+
missingSourceIds: indexRowsFreshness.presentSourceIds?.length ? [] : (indexRowsFreshness.sourceIds || []),
|
|
626
|
+
},
|
|
627
|
+
cacheAvailable: true,
|
|
628
|
+
cacheFresh: indexFresh,
|
|
629
|
+
freshness: indexRowsFreshness.state || (indexFresh ? 'fresh' : 'catching_up'),
|
|
630
|
+
indexPromptCount,
|
|
631
|
+
cachePromptCount: 0,
|
|
632
|
+
};
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
context: () => ({
|
|
636
|
+
sid: String(sessionId).slice(0, 8),
|
|
637
|
+
indexMessages: indexMessages.length,
|
|
638
|
+
}),
|
|
639
|
+
}
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
// Index empty: keep the request bounded to the in-memory stream tail and nudge
|
|
644
|
+
// the importer asynchronously. This is intentionally incomplete for one poll;
|
|
645
|
+
// synchronous discovery or full-history reconstruction would stall every tab.
|
|
646
|
+
setImmediate(() => {
|
|
647
|
+
_deps._resolveAllSessionFilesOffThread(sessionId, '', { origin: 'prompt-nudge:empty-index' }).then((filePaths) => {
|
|
648
|
+
if (filePaths.length === 0) return null;
|
|
649
|
+
return _deps._catchUpTranscriptStoreForSession(sessionId, filePaths, {
|
|
650
|
+
reason: 'prompt-index-empty',
|
|
651
|
+
scheduleOnly: true,
|
|
652
|
+
maxBytesPerFile: _deps.LARGE_SESSION_TAIL_READ_BYTES,
|
|
653
|
+
});
|
|
654
|
+
}).catch(() => {});
|
|
655
|
+
});
|
|
656
|
+
return _deps._perfTracker.runSyncProbed(
|
|
657
|
+
'prompts:stream-tail-degraded-respond',
|
|
658
|
+
() => {
|
|
659
|
+
const merge = _deps._mergeSessionMessagesWithStreamTail(sessionId, [], {
|
|
660
|
+
allowEmptyBase: true,
|
|
661
|
+
limit: 200,
|
|
662
|
+
eventFilter: (event) => String(_deps.streamEventToMessage(event)?.role || '').trim() === 'user',
|
|
663
|
+
});
|
|
664
|
+
const tailMessages = _deps._applySessionMessageExclusionsFromRows(
|
|
665
|
+
sessionId,
|
|
666
|
+
merge.messages || [],
|
|
667
|
+
indexExclusionRows,
|
|
668
|
+
{ codexSynthetic: true },
|
|
669
|
+
).filter((message) => String(message?.role || '').trim() === 'user');
|
|
670
|
+
const sourceIds = Array.from(new Set([
|
|
671
|
+
...(indexSourceIds || []),
|
|
672
|
+
...((merge.identity && merge.identity.conversationSourceIds) || []),
|
|
673
|
+
]));
|
|
674
|
+
const sourceSummary = _deps._promptSummaryFromMessages(tailMessages, sourceIds);
|
|
675
|
+
const streamTailAdded = tailMessages.length;
|
|
676
|
+
|
|
677
|
+
return {
|
|
678
|
+
source: streamTailAdded > 0 ? 'stream-tail-degraded' : 'deferred',
|
|
679
|
+
promptMessages: tailMessages,
|
|
680
|
+
responsePrompts: sourceSummary.responsePrompts,
|
|
681
|
+
promptFingerprint: sourceSummary.promptFingerprint,
|
|
682
|
+
lastPromptPreview: sourceSummary.lastPromptPreview,
|
|
683
|
+
userPromptCount: sourceSummary.userPromptCount,
|
|
684
|
+
sourceIds: sourceSummary.sourceIds,
|
|
685
|
+
streamTailAdded,
|
|
686
|
+
cacheDetails: { cacheAvailable: false, cacheFresh: false, sourceIds },
|
|
687
|
+
cacheAvailable: false,
|
|
688
|
+
cacheFresh: false,
|
|
689
|
+
indexPromptCount: 0,
|
|
690
|
+
cachePromptCount: 0,
|
|
691
|
+
_deferred: streamTailAdded === 0,
|
|
692
|
+
};
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
context: () => ({
|
|
696
|
+
sid: String(sessionId).slice(0, 8),
|
|
697
|
+
streamTailOnly: true,
|
|
698
|
+
}),
|
|
699
|
+
}
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async function _apiRecentSessionsLegacy(req, res, url, options = {}) {
|
|
704
|
+
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
|
705
|
+
const forceRescan = url.searchParams.get('rescan') === '1';
|
|
706
|
+
// WP-3 (D-10): main's cheap registry snapshot for the DB-only projection below — the same input
|
|
707
|
+
// the read-pool op carries, so the legacy and published projections stamp identically. Deliberately
|
|
708
|
+
// read HERE, never in apiRecentSessions (tests/r4-9-recent-sessions-caps.test.js:144 pins that
|
|
709
|
+
// handler body to a pure published-buffer lookup).
|
|
710
|
+
const machineTags = _deps.remoteMachineCatalog.machineTagInputs();
|
|
711
|
+
|
|
712
|
+
// First access after (a cold) boot: seed the in-memory cache from the persisted snapshot so this
|
|
713
|
+
// request serves the last good list immediately instead of an empty one. It's marked stale, so
|
|
714
|
+
// the SWR path below still kicks an off-thread rebuild to refresh it.
|
|
715
|
+
if (!_deps._recentSessionsPersistLoaded) {
|
|
716
|
+
_deps._recentSessionsPersistLoaded = true;
|
|
717
|
+
if (!_deps._recentSessionsCache) _deps._loadPersistedRecentSessionsCache();
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const now = Date.now();
|
|
721
|
+
let recentSessions;
|
|
722
|
+
let recentSessionsStale = false;
|
|
723
|
+
if (_deps._recentSessionsCache && (now - _deps._recentSessionsCacheTime) < _deps._RECENT_SESSIONS_CACHE_TTL && !forceRescan) {
|
|
724
|
+
recentSessions = _deps._recentSessionsCache;
|
|
725
|
+
} else if (!forceRescan) {
|
|
726
|
+
recentSessionsStale = true;
|
|
727
|
+
if (_deps._recentSessionsCache && _deps._recentSessionsCache.length > 0) {
|
|
728
|
+
// Stale-while-revalidate: hand back the last good value now (no multi-second
|
|
729
|
+
// getAllSessionsData scan on the request path) and refresh in the background.
|
|
730
|
+
recentSessions = _deps._recentSessionsCache;
|
|
731
|
+
_deps._scheduleRecentSessionsRebuild();
|
|
732
|
+
} else if (_deps._inRestoreStorm()) {
|
|
733
|
+
// Restore storm + cold cache: do NOT run the getAllSessionsData build on the request
|
|
734
|
+
// (main) thread — that's the freeze we're eliminating. Serve an empty list now and
|
|
735
|
+
// kick the off-thread rebuild; the UI re-polls and fills within a couple seconds.
|
|
736
|
+
recentSessions = [];
|
|
737
|
+
_deps._scheduleRecentSessionsRebuild();
|
|
738
|
+
} else {
|
|
739
|
+
// Cold/empty cache outside the restore-storm window. The synchronous getAllSessionsData
|
|
740
|
+
// build is ~700ms warm but 30s+ on cold Dropbox; running it on the request path froze the
|
|
741
|
+
// event loop (measured 31s), which both truncated this response ("No sessions found") AND
|
|
742
|
+
// tripped the restart watchdog — turning a slow boot into a freeze→restart→cold loop. NEVER
|
|
743
|
+
// build on the request path: serve whatever we have (stale or empty) and FORCE an off-thread
|
|
744
|
+
// rebuild (bypassing the breaker, since an empty cache must keep trying). The UI re-polls and
|
|
745
|
+
// the list fills once the read-pool finishes — off the main thread, no freeze.
|
|
746
|
+
recentSessions = (Array.isArray(_deps._recentSessionsCache) && _deps._recentSessionsCache.length > 0)
|
|
747
|
+
? _deps._recentSessionsCache
|
|
748
|
+
: [];
|
|
749
|
+
_deps._scheduleRecentSessionsRebuild({ force: true });
|
|
750
|
+
}
|
|
751
|
+
} else {
|
|
752
|
+
recentSessionsStale = true;
|
|
753
|
+
recentSessions = Array.isArray(_deps._recentSessionsCache) ? _deps._recentSessionsCache : [];
|
|
754
|
+
_deps._scheduleRecentSessionsFullRescan(limit);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// R4-9: serve the cached serialized body when every stamped input is unchanged (see the cache
|
|
758
|
+
// definition for the key contract). Placed AFTER the SWR branch above so its rebuild-scheduling
|
|
759
|
+
// side effects still fire on every poll.
|
|
760
|
+
const _serializeCacheOn = _deps.CTM_RECENT_SERIALIZE_CACHE_ENABLED && !forceRescan && !options.catalogDegraded;
|
|
761
|
+
const _serializeKey = _serializeCacheOn
|
|
762
|
+
? `${limit}|${_deps._recentSessionsCacheTime}|${_deps._recentSessionRowsMapCache.at}|${_deps._starredIdsCacheAt}|${recentSessionsStale ? 1 : 0}`
|
|
763
|
+
: '';
|
|
764
|
+
if (_serializeCacheOn && _deps._recentSessionsSerializedCache
|
|
765
|
+
&& _deps._recentSessionsSerializedCache.key === _serializeKey
|
|
766
|
+
&& (now - _deps._recentSessionsSerializedCache.at) < _deps._RECENT_SERIALIZE_CACHE_TTL_MS) {
|
|
767
|
+
res.writeHead(200, { 'Content-Type': 'application/json', 'X-Recent-Cache': 'hit' });
|
|
768
|
+
res.end(_deps._recentSessionsSerializedCache.buffer);
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Work on a shallow copy so title/mapping enrichment does not mutate the cache.
|
|
773
|
+
let result = recentSessions.map(s => ({ ...s }));
|
|
774
|
+
|
|
775
|
+
// Merge titles, stars, and CTM -> agent mappings from the canonical
|
|
776
|
+
// ctm_sessions + agent_sessions catalog.
|
|
777
|
+
let sessionsMap = {};
|
|
778
|
+
let allSessionRows = [];
|
|
779
|
+
// Reuse the by-id lookup cached from the SAME getAllSessionsData rows the list build/rebuild
|
|
780
|
+
// already fetched (avoids a second main-thread catalog query on every poll — the per-poll
|
|
781
|
+
// double-query that showed as the residual getAllSessionsData main runs during the storm).
|
|
782
|
+
const _mapFresh = _deps._recentSessionRowsMapCache.map && (now - _deps._recentSessionRowsMapCache.at) < _deps._RECENT_SESSIONS_CACHE_TTL;
|
|
783
|
+
if (result.length === 0) {
|
|
784
|
+
// Nothing to enrich (e.g. the storm served an empty list while the off-thread rebuild
|
|
785
|
+
// runs) — skip the catalog query entirely rather than build a lookup for zero sessions.
|
|
786
|
+
sessionsMap = _mapFresh ? _deps._recentSessionRowsMapCache.map : {};
|
|
787
|
+
allSessionRows = _mapFresh ? (_deps._recentSessionRowsMapCache.rows || []) : [];
|
|
788
|
+
} else if (_mapFresh) {
|
|
789
|
+
sessionsMap = _deps._recentSessionRowsMapCache.map;
|
|
790
|
+
allSessionRows = _deps._recentSessionRowsMapCache.rows || [];
|
|
791
|
+
} else {
|
|
792
|
+
// Enrichment is optional presentation data. Keep the last published lookup and refresh it
|
|
793
|
+
// off-thread; a cold exact catalog query here blocked the request loop for up to 31 seconds.
|
|
794
|
+
sessionsMap = _deps._recentSessionRowsMapCache.map || {};
|
|
795
|
+
allSessionRows = _deps._recentSessionRowsMapCache.rows || [];
|
|
796
|
+
_deps._scheduleRecentSessionsRebuild({ force: true });
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Filter: remove scanned sessions whose DB row was deleted by ghost prevention.
|
|
800
|
+
// Small/empty files without a DB entry are ghosts or abandoned stubs.
|
|
801
|
+
result = result.filter(s => {
|
|
802
|
+
if (_deps._isProviderChildAgentRow(s)) return false;
|
|
803
|
+
const dbRow = sessionsMap[s.sessionId];
|
|
804
|
+
if (_deps._isProviderChildSessionRow(dbRow)) return false;
|
|
805
|
+
if (dbRow) return true; // Has a DB entry → keep
|
|
806
|
+
// No DB entry. Check if this file was intentionally cleaned up (not just missing).
|
|
807
|
+
// A session without a DB row but WITH a JSONL file on disk was cleaned up by ghost prevention.
|
|
808
|
+
// Keep it only if it's a substantial session (>2KB, has user messages) — small/empty files
|
|
809
|
+
// without DB entries are ghosts or abandoned stubs.
|
|
810
|
+
if (s.fileSize > 2048 || s.userMsgCount > 0) return true;
|
|
811
|
+
return false;
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
const existingSessionIds = new Set(result.map(s => s.sessionId));
|
|
815
|
+
const dbOnlyRows = allSessionRows.filter((row) => !_deps._isProviderChildSessionRow(row)
|
|
816
|
+
&& !existingSessionIds.has(row.id)
|
|
817
|
+
&& !_deps.dbRowAgentSessionIds(row).some((id) => existingSessionIds.has(id)));
|
|
818
|
+
const fileStats = await _deps._mainTranscriptReads.statPaths(_deps._recentTranscriptPathsForRows(dbOnlyRows));
|
|
819
|
+
if (!fileStats || [...fileStats.values()].some((st) => st.state === 'error')) recentSessionsStale = true;
|
|
820
|
+
|
|
821
|
+
for (const s of result) {
|
|
822
|
+
const dbRow = sessionsMap[s.sessionId];
|
|
823
|
+
if (dbRow && dbRow.title) {
|
|
824
|
+
s.aiTitle = dbRow.title;
|
|
825
|
+
s.userRenamed = !!dbRow.user_renamed;
|
|
826
|
+
if (s.userRenamed) s.title = dbRow.title;
|
|
827
|
+
}
|
|
828
|
+
if (dbRow) {
|
|
829
|
+
s.userMsgCount = _deps.maxUserMsgCount(s.userMsgCount, dbRow.user_msg_count);
|
|
830
|
+
s.lastMessageAt = dbRow.last_message_at || s.lastMessageAt || '';
|
|
831
|
+
s.lastActivityAt = s.lastMessageAt || s.lastActivityAt || '';
|
|
832
|
+
s.titleSource = dbRow.title_source || '';
|
|
833
|
+
s.titleStatus = dbRow.title_status || '';
|
|
834
|
+
s.titleStatusReason = dbRow.title_status_reason || '';
|
|
835
|
+
s.titleUpdatedAt = dbRow.title_updated_at || '';
|
|
836
|
+
}
|
|
837
|
+
// Carry starred flag from sessions table
|
|
838
|
+
if (dbRow) s.starred = !!dbRow.starred;
|
|
839
|
+
// Carry dual-ID mapping so frontend can resolve in both directions
|
|
840
|
+
if (dbRow) {
|
|
841
|
+
const agentSessions = _deps._apiAgentSessionsFromDbRow(dbRow, { useFileStats: false });
|
|
842
|
+
if (agentSessions.length > 0) s.agentSessions = agentSessions;
|
|
843
|
+
if (dbRow.agent_session_id && dbRow.agent_session_id !== dbRow.id) {
|
|
844
|
+
s.agentSessionId = dbRow.agent_session_id;
|
|
845
|
+
}
|
|
846
|
+
const agentSessionIds = _deps.dbRowAgentSessionIds(dbRow);
|
|
847
|
+
if (agentSessionIds.length > 0) s.agentSessionIds = agentSessionIds;
|
|
848
|
+
if (dbRow.id !== s.sessionId) {
|
|
849
|
+
// s.sessionId is the JSONL file ID; dbRow.id is the CTM tab ID
|
|
850
|
+
s.provisionalId = dbRow.id;
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
// Surface the multi-tier title fallback (ported from upstream
|
|
854
|
+
// claude-code-history-viewer — gap-reports/viewer-2026-04-23.md items
|
|
855
|
+
// I + J). pickDisplayTitle blends ctm_sessions.title (auto or user-
|
|
856
|
+
// renamed), rename_name from a /rename command, and the file-scan's
|
|
857
|
+
// firstMessage + the new last_user_content / first_assistant_text
|
|
858
|
+
// columns so post-compact continuations inherit a useful title.
|
|
859
|
+
const rowForTitle = {
|
|
860
|
+
user_renamed: s.userRenamed ? 1 : 0,
|
|
861
|
+
title: s.aiTitle || s.title || '',
|
|
862
|
+
rename_name: dbRow ? dbRow.rename_name : '',
|
|
863
|
+
first_message: s.firstMessage || '',
|
|
864
|
+
last_user_content: dbRow ? dbRow.last_user_content : '',
|
|
865
|
+
first_assistant_text: dbRow ? dbRow.first_assistant_text : '',
|
|
866
|
+
};
|
|
867
|
+
s.displayTitle = _deps.dbModule.pickDisplayTitle(rowForTitle);
|
|
868
|
+
s.renameName = (dbRow && dbRow.rename_name) || '';
|
|
869
|
+
_deps.sessionEmpty.applyRecentSessionEmptyState(s, dbRow);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Include DB-only sessions only when they are backed by canonical session
|
|
873
|
+
// identity, or explicitly preserved by the user. Auto-title/import caches are
|
|
874
|
+
// not session identity and must not float into the recent timeline.
|
|
875
|
+
for (const row of allSessionRows) {
|
|
876
|
+
if (_deps._isProviderChildSessionRow(row)) continue;
|
|
877
|
+
if (existingSessionIds.has(row.id)) continue;
|
|
878
|
+
if (_deps.dbRowAgentSessionIds(row).some(agentId => existingSessionIds.has(agentId))) continue;
|
|
879
|
+
const session = _deps._recentSessionFromDbRow(row, { fileStats, machineTags });
|
|
880
|
+
if (!_deps._shouldIncludeDbOnlyRecentSession(row, session, fileStats)) continue;
|
|
881
|
+
result.push(session);
|
|
882
|
+
existingSessionIds.add(row.id);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
result = _deps._dedupeRecentSessionsByCanonicalId(result);
|
|
886
|
+
|
|
887
|
+
// Tag all sessions with current hostname
|
|
888
|
+
for (const s of result) s.hostname = _deps.HOSTNAME;
|
|
889
|
+
|
|
890
|
+
result.sort((a, b) => _deps._recentSessionActivityMs(b) - _deps._recentSessionActivityMs(a));
|
|
891
|
+
|
|
892
|
+
// Slice to limit, but ensure starred/pinned sessions are always included
|
|
893
|
+
let sliced = result.slice(0, limit);
|
|
894
|
+
try {
|
|
895
|
+
const slicedIds = new Set(sliced.map(s => s.sessionId));
|
|
896
|
+
// Starred sessions from sessions table
|
|
897
|
+
const starredRows = _deps._getCachedStarredSessionIds();
|
|
898
|
+
const starredSet = new Set();
|
|
899
|
+
for (const r of starredRows) {
|
|
900
|
+
starredSet.add(r.id);
|
|
901
|
+
if (r.agent_session_id) starredSet.add(r.agent_session_id);
|
|
902
|
+
}
|
|
903
|
+
// Legacy: also check ui_pinned_sessions setting
|
|
904
|
+
const pinnedIds = _deps._controlSnapshot.getSetting('ui_pinned_sessions', []);
|
|
905
|
+
if (Array.isArray(pinnedIds)) {
|
|
906
|
+
for (const pid of pinnedIds) starredSet.add(pid);
|
|
907
|
+
}
|
|
908
|
+
for (const s of result) {
|
|
909
|
+
if (!slicedIds.has(s.sessionId) && (starredSet.has(s.sessionId) || s.starred)) {
|
|
910
|
+
sliced.push(s);
|
|
911
|
+
slicedIds.add(s.sessionId);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
} catch (e) { console.error('[recent-sessions] starred filter error:', e.message); }
|
|
915
|
+
|
|
916
|
+
// Include the last project where the user intentionally created a session
|
|
917
|
+
// so the frontend can auto-select it as the default project filter
|
|
918
|
+
let lastActiveProject = null;
|
|
919
|
+
try { lastActiveProject = _deps._controlSnapshot.getSetting('last_active_project', null); } catch (e) { console.error('[recent-sessions] getSetting error:', e.message); }
|
|
920
|
+
|
|
921
|
+
// R4-9: probed serialize (was an unprobed multi-hundred-KB stringify → `(unknown)` blocks)
|
|
922
|
+
// + populate the serialized-response cache for the next poll.
|
|
923
|
+
const _serializedBody = _deps._perfTracker.runSyncProbed('apiRecentSessions:serialize',
|
|
924
|
+
() => Buffer.from(JSON.stringify({
|
|
925
|
+
sessions: sliced,
|
|
926
|
+
lastActiveProject,
|
|
927
|
+
stale: recentSessionsStale || !!options.catalogDegraded,
|
|
928
|
+
catalogDegraded: !!options.catalogDegraded,
|
|
929
|
+
}), 'utf8'),
|
|
930
|
+
{ context: () => ({ sessions: sliced.length }) });
|
|
931
|
+
if (_serializeCacheOn) {
|
|
932
|
+
_deps._recentSessionsSerializedCache = { key: _serializeKey, at: Date.now(), buffer: _serializedBody };
|
|
933
|
+
}
|
|
934
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
935
|
+
res.end(_serializedBody);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// WP-0 (S-3/S-4, fleet-session-lists-hub-search-2026-11-01.html §5 D-3, §6.1): the owner contract.
|
|
939
|
+
// `owner` is either absent (today's answer) or `self` (only rows THIS host owns). Anything else is
|
|
940
|
+
// a client bug, never a query — both routes answer 400 { error: 'owner_invalid' }.
|
|
941
|
+
const OWNER_FILTER_VALUES = Object.freeze(['', 'self']);
|
|
942
|
+
|
|
943
|
+
function normalizeOwnerFilter(value) {
|
|
944
|
+
const owner = String(value == null ? '' : value).trim();
|
|
945
|
+
return { ok: OWNER_FILTER_VALUES.includes(owner), owner };
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// The owner a catalog row names — the same projection mapRecentSessionRow adds
|
|
949
|
+
// (lib/recent-catalog-query.js, S-1). A row that never went through that projection still answers
|
|
950
|
+
// from the raw columns, so the search path never depends on which of the two produced it.
|
|
951
|
+
function catalogRowOwner(row) {
|
|
952
|
+
if (!row) return '';
|
|
953
|
+
if (row.ownerInstanceId !== undefined && row.ownerInstanceId !== null) {
|
|
954
|
+
return String(row.ownerInstanceId).trim();
|
|
955
|
+
}
|
|
956
|
+
return String(row.execution_host_instance_id || row.current_owner_instance_id || '').trim();
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* WP-0 (S-4): project `ownerInstanceId` onto every search result and, when `ownerLocalIds` is
|
|
961
|
+
* given, drop the rows it proves foreign — an UNKNOWN owner (`''`) is always kept, so a row that
|
|
962
|
+
* predates ownership stamping can never vanish from a box's answer.
|
|
963
|
+
* @param {object[]} rows fused search results
|
|
964
|
+
* @param {{ownerLocalIds?:Iterable<string>|null, ownerFor?:(row)=>string}} opts
|
|
965
|
+
*/
|
|
966
|
+
function applyOwnerFilterToSearchResults(rows, { ownerLocalIds = null, ownerFor = null } = {}) {
|
|
967
|
+
const localIds = ownerLocalIds ? new Set(ownerLocalIds) : null;
|
|
968
|
+
const resolve = typeof ownerFor === 'function' ? ownerFor : () => '';
|
|
969
|
+
const projected = (Array.isArray(rows) ? rows : []).map((row) => ({
|
|
970
|
+
...row,
|
|
971
|
+
ownerInstanceId: String(resolve(row) || '').trim(),
|
|
972
|
+
}));
|
|
973
|
+
return localIds
|
|
974
|
+
? projected.filter((row) => !row.ownerInstanceId || localIds.has(row.ownerInstanceId))
|
|
975
|
+
: projected;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// The ids that count as THIS host's: the accessor the server's recent-catalog call site uses, so
|
|
979
|
+
// both surfaces filter against one truth. `_deps.getInstanceIdentity` wins when the seam injects it
|
|
980
|
+
// (that is how this module gets server state, and it is what lets a unit test stub it).
|
|
981
|
+
function _localOwnerInstanceIds() {
|
|
982
|
+
const accessor = typeof _deps?.getInstanceIdentity === 'function'
|
|
983
|
+
? _deps.getInstanceIdentity
|
|
984
|
+
: require('../instance-identity-access').getInstanceIdentity;
|
|
985
|
+
try {
|
|
986
|
+
const lineage = accessor({ required: false })?.lineage;
|
|
987
|
+
// Fail OPEN: with no identity yet this box cannot tell its rows from copies, so it serves them
|
|
988
|
+
// unfiltered rather than as "owns nothing" (which would silently hide every owned hit). The
|
|
989
|
+
// hub's merge still drops rows owned by its own lineage (fleet-session-lists D-4).
|
|
990
|
+
return lineage && lineage.size ? new Set(lineage) : null;
|
|
991
|
+
} catch (e) {
|
|
992
|
+
// HANDLE: an identity that cannot be derived serves the search unfiltered, never fails it.
|
|
993
|
+
console.error('[search] owner filter: instance identity unavailable, serving unfiltered:', e?.message || e);
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
async function buildSessionSearchResponse(query, options = {}) {
|
|
999
|
+
const q = String(query || '').trim();
|
|
1000
|
+
const qLookup = _deps.SessionSearchUtils.normalizeSearchValue(q);
|
|
1001
|
+
const limit = Math.min(Math.max(parseInt(options.limit || '50', 10) || 50, 1), 100);
|
|
1002
|
+
if (!qLookup || qLookup.length < 2) return _deps._emptySessionSearchResponse(qLookup, limit);
|
|
1003
|
+
|
|
1004
|
+
const plan = _deps._sessionSearchPlan(qLookup, q);
|
|
1005
|
+
if (plan.terms.length === 0) return _deps._emptySessionSearchResponse(qLookup, limit);
|
|
1006
|
+
|
|
1007
|
+
try {
|
|
1008
|
+
_deps.requestConversationImport('session-search');
|
|
1009
|
+
|
|
1010
|
+
// Search is DB-only on the request path. Provider JSONL files are source
|
|
1011
|
+
// logs consumed by import/watch/repair jobs; this route must not discover or
|
|
1012
|
+
// parse them per keystroke.
|
|
1013
|
+
const ftsCorpusPromise = _deps._searchSessionFtsCorpusOffThread(qLookup, plan, limit);
|
|
1014
|
+
const catalog = await _deps._sessionCatalogForSearch();
|
|
1015
|
+
const ftsCorpus = await ftsCorpusPromise;
|
|
1016
|
+
const allSessionRows = catalog.rows;
|
|
1017
|
+
const _searchSessionsMap = catalog.map;
|
|
1018
|
+
|
|
1019
|
+
// Search uses the durable DB corpus. The request path should not parse the
|
|
1020
|
+
// JSONL filesystem on each keystroke; the background importer keeps these
|
|
1021
|
+
// rows fresh and is woken above when search runs.
|
|
1022
|
+
const { allSessions } = _deps._loadSearchCorpusSessions(
|
|
1023
|
+
allSessionRows, _searchSessionsMap, ftsCorpus.conversationRows,
|
|
1024
|
+
);
|
|
1025
|
+
const sessionMap = _deps._buildSearchSessionMap(allSessions);
|
|
1026
|
+
|
|
1027
|
+
const stripTags = (s) => s.replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
1028
|
+
|
|
1029
|
+
// Build analysis lookup for enriching results
|
|
1030
|
+
const analysisMap = new Map();
|
|
1031
|
+
for (const a of ftsCorpus.analysisRows) {
|
|
1032
|
+
const rowId = a.ctm_session_id || a.session_id;
|
|
1033
|
+
if (!rowId) continue;
|
|
1034
|
+
analysisMap.set(rowId, a);
|
|
1035
|
+
const canonicalId = _deps._canonicalSearchSessionId(rowId, _searchSessionsMap);
|
|
1036
|
+
if (canonicalId && !analysisMap.has(canonicalId)) analysisMap.set(canonicalId, a);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// --- Source 1: FTS5 on session_analyses ---
|
|
1040
|
+
const ftsResults = _deps._analysisFtsSearchRows(ftsCorpus.analysisFtsRows);
|
|
1041
|
+
const ftsScored = [];
|
|
1042
|
+
for (let i = 0; i < ftsResults.length; i++) {
|
|
1043
|
+
const r = ftsResults[i];
|
|
1044
|
+
const sourceSessionId = r.session_id;
|
|
1045
|
+
const dbRow = _searchSessionsMap ? _searchSessionsMap[sourceSessionId] : null;
|
|
1046
|
+
const sessionId = _deps._canonicalSearchSessionId(sourceSessionId, _searchSessionsMap);
|
|
1047
|
+
const a = analysisMap.get(sourceSessionId) || analysisMap.get(sessionId);
|
|
1048
|
+
if (!a) continue;
|
|
1049
|
+
|
|
1050
|
+
// Find best highlighted field for display
|
|
1051
|
+
let matchField = '';
|
|
1052
|
+
let matchSnippet = '';
|
|
1053
|
+
const highlights = [
|
|
1054
|
+
['title', r.h_title], ['summary', r.h_summary], ['topics', r.h_topics],
|
|
1055
|
+
['category', r.h_category], ['skills_used', r.h_skills],
|
|
1056
|
+
['first_message', r.h_first_message], ['project', r.h_project],
|
|
1057
|
+
];
|
|
1058
|
+
for (const [field, hl] of highlights) {
|
|
1059
|
+
if (hl && hl.includes('<mark>')) {
|
|
1060
|
+
matchField = field;
|
|
1061
|
+
// Extract snippet around first highlight
|
|
1062
|
+
const markIdx = hl.indexOf('<mark>');
|
|
1063
|
+
const start = Math.max(0, markIdx - 40);
|
|
1064
|
+
const end = Math.min(hl.length, markIdx + 100);
|
|
1065
|
+
matchSnippet = stripTags((start > 0 ? '...' : '') + hl.slice(start, end) + (end < hl.length ? '...' : ''));
|
|
1066
|
+
break;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
// session_analyses_fts is contentless, so SQLite can identify a matching
|
|
1070
|
+
// row without returning highlight text. Recover field attribution from
|
|
1071
|
+
// the durable analysis row using the exact terms FTS expanded.
|
|
1072
|
+
if (!matchField) {
|
|
1073
|
+
const expandedTerms = (r._fuzzyExpansions || []).flatMap(expansion => expansion?.variants || []);
|
|
1074
|
+
const matchTerms = [...new Set([...(plan.terms || []), ...expandedTerms])]
|
|
1075
|
+
.map(term => String(term || '').toLowerCase())
|
|
1076
|
+
.filter(Boolean);
|
|
1077
|
+
const analysisFields = [
|
|
1078
|
+
['title', a.title], ['summary', a.summary], ['topics', a.topics],
|
|
1079
|
+
['category', a.category], ['skills_used', a.skills_used],
|
|
1080
|
+
['first_message', a.first_message], ['project', a.project],
|
|
1081
|
+
];
|
|
1082
|
+
for (const [field, value] of analysisFields) {
|
|
1083
|
+
const text = String(value || '');
|
|
1084
|
+
const lower = text.toLowerCase();
|
|
1085
|
+
const term = matchTerms.find(candidate => lower.includes(candidate));
|
|
1086
|
+
if (!term) continue;
|
|
1087
|
+
const index = lower.indexOf(term);
|
|
1088
|
+
const start = Math.max(0, index - 40);
|
|
1089
|
+
const end = Math.min(text.length, index + term.length + 60);
|
|
1090
|
+
matchField = field;
|
|
1091
|
+
matchSnippet = stripTags((start > 0 ? '...' : '') + text.slice(start, end) + (end < text.length ? '...' : ''));
|
|
1092
|
+
break;
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const raw = sessionMap.get(sessionId) || sessionMap.get(sourceSessionId);
|
|
1097
|
+
const titleFields = _deps._searchResultTitleFields({ analysis: a, raw, dbRow, stripTags });
|
|
1098
|
+
const agentSessions = dbRow ? _deps._apiAgentSessionsFromDbRow(dbRow, { useFileStats: false }) : (raw?.agentSessions || []);
|
|
1099
|
+
const primaryAgent = dbRow ? _deps._primaryApiAgentSession(dbRow, agentSessions) : null;
|
|
1100
|
+
ftsScored.push({
|
|
1101
|
+
id: sessionId,
|
|
1102
|
+
sessionId,
|
|
1103
|
+
source: 'analysisFts',
|
|
1104
|
+
project: a.project || raw?.project || '',
|
|
1105
|
+
projectEntry: raw?.projectEntry || '',
|
|
1106
|
+
cwd: raw?.cwd || a.project || '',
|
|
1107
|
+
firstMessage: a.first_message || raw?.firstMessage || '',
|
|
1108
|
+
title: titleFields.title,
|
|
1109
|
+
aiTitle: titleFields.aiTitle,
|
|
1110
|
+
displayTitle: titleFields.displayTitle,
|
|
1111
|
+
analysisTitle: titleFields.analysisTitle,
|
|
1112
|
+
userRenamed: titleFields.userRenamed,
|
|
1113
|
+
isEmpty: false,
|
|
1114
|
+
userMsgCount: _deps.maxUserMsgCount(a.message_count, raw?.userMsgCount),
|
|
1115
|
+
modifiedAt: raw?.modifiedAt || a.session_modified_at || '',
|
|
1116
|
+
timestamp: raw?.modifiedAt || a.session_modified_at || '',
|
|
1117
|
+
version: raw?.version || '',
|
|
1118
|
+
gitBranch: raw?.gitBranch || '',
|
|
1119
|
+
fileSize: raw?.fileSize || 0,
|
|
1120
|
+
agent: dbRow?.provider || primaryAgent?.provider || raw?.agent || undefined,
|
|
1121
|
+
provisionalId: dbRow?.id && dbRow.id !== sourceSessionId ? dbRow.id : undefined,
|
|
1122
|
+
agentSessionIds: _deps.dbRowAgentSessionIds(dbRow),
|
|
1123
|
+
agentSessionId: primaryAgent && primaryAgent.agentSessionId !== dbRow?.id ? primaryAgent.agentSessionId : undefined,
|
|
1124
|
+
agentSessions,
|
|
1125
|
+
_matchField: matchField,
|
|
1126
|
+
_matchSnippet: matchSnippet,
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// --- Source 3: FTS5 on session_messages ---
|
|
1131
|
+
const msgResults = _deps._dedupeSearchRows(ftsCorpus.messageRows, 'session_id');
|
|
1132
|
+
const msgBySession = _deps._groupMessageResults(msgResults, _searchSessionsMap, plan);
|
|
1133
|
+
|
|
1134
|
+
// --- Source 4: FTS5 on transcript_events ---
|
|
1135
|
+
// This covers provider append-tail rows, including Codex rollout events, from
|
|
1136
|
+
// SQLite. It must not fall back to scanning provider JSONL files.
|
|
1137
|
+
const eventScored = _deps._scoreTranscriptEventFtsRows(ftsCorpus.eventRows, plan, limit, _searchSessionsMap, stripTags);
|
|
1138
|
+
|
|
1139
|
+
// --- Source 6: direct id matches across CTM ids + agent ids ---
|
|
1140
|
+
const idScored = _deps.SessionSearchUtils.buildIdMatchResults(allSessions, qLookup);
|
|
1141
|
+
|
|
1142
|
+
// --- Source 5: exact session-id/URL references in prompts and transcripts ---
|
|
1143
|
+
const referenceScored = _deps._sessionReferenceSearch(
|
|
1144
|
+
q, limit, _searchSessionsMap, sessionMap, stripTags, ftsCorpus.referenceRows,
|
|
1145
|
+
);
|
|
1146
|
+
|
|
1147
|
+
// --- Metadata LIKE search for exact title/path recovery ---
|
|
1148
|
+
const likeScored = _deps._likeSearchMetadata(allSessions, plan.terms, stripTags, q, plan.phrases);
|
|
1149
|
+
|
|
1150
|
+
// --- Source 2: vector search over session analysis rows when cheaper
|
|
1151
|
+
// sources do not already provide enough candidates. Semantic search is
|
|
1152
|
+
// helpful for true "describe the thing I remember" queries, but local
|
|
1153
|
+
// embedding initialization can be expensive; keyword/id/message hits should
|
|
1154
|
+
// stay fast and local.
|
|
1155
|
+
const cheapCandidateCount = ftsScored.length + msgResults.length + eventScored.length + referenceScored.length + idScored.length + likeScored.length;
|
|
1156
|
+
const semanticResult = options.includeSemantic === false || !plan.descriptive || cheapCandidateCount >= _deps.CTM_SEARCH_SEMANTIC_MIN_LEXICAL_RESULTS
|
|
1157
|
+
? { vectorScored: [], semantic: _deps._skippedSessionSearchSemanticStatus() }
|
|
1158
|
+
: await _deps._semanticSessionSearch(qLookup, limit, analysisMap, sessionMap, _searchSessionsMap, stripTags);
|
|
1159
|
+
const vectorScored = semanticResult.vectorScored;
|
|
1160
|
+
|
|
1161
|
+
// --- Merge all sources using RRF ---
|
|
1162
|
+
// WP-0 (S-4): with owner=self the owner filter runs BEFORE the final slice, so the fusion is
|
|
1163
|
+
// asked for every ranked candidate it has (the sources are limit-bounded upstream) and the
|
|
1164
|
+
// caller's limit is enforced on the FILTERED set — a foreign-owned hit can never starve an
|
|
1165
|
+
// owned one out of the list. Without owner the merge is called exactly as before.
|
|
1166
|
+
const ownerLocalIds = options.owner === 'self' ? _localOwnerInstanceIds() : null;
|
|
1167
|
+
const merged = _deps._mergeSearchSources(
|
|
1168
|
+
ftsScored, vectorScored, msgBySession, eventScored, referenceScored, idScored, likeScored,
|
|
1169
|
+
ownerLocalIds ? Number.MAX_SAFE_INTEGER : limit,
|
|
1170
|
+
analysisMap, sessionMap, stripTags, plan,
|
|
1171
|
+
);
|
|
1172
|
+
const ownerFor = (row) => {
|
|
1173
|
+
const id = String(row?.sessionId || row?.id || '');
|
|
1174
|
+
if (!id || !_searchSessionsMap || !Object.prototype.hasOwnProperty.call(_searchSessionsMap, id)) return '';
|
|
1175
|
+
return catalogRowOwner(_searchSessionsMap[id]);
|
|
1176
|
+
};
|
|
1177
|
+
const projected = applyOwnerFilterToSearchResults(merged, { ownerLocalIds, ownerFor });
|
|
1178
|
+
const results = ownerLocalIds ? projected.slice(0, limit) : projected;
|
|
1179
|
+
|
|
1180
|
+
return {
|
|
1181
|
+
results,
|
|
1182
|
+
total: results.length,
|
|
1183
|
+
mode: 'smart',
|
|
1184
|
+
query: { normalized: qLookup, terms: plan.terms, descriptive: plan.descriptive },
|
|
1185
|
+
sources: {
|
|
1186
|
+
analysisFts: ftsScored.length,
|
|
1187
|
+
messageFts: msgResults.length,
|
|
1188
|
+
transcriptEvents: eventScored.length,
|
|
1189
|
+
references: referenceScored.length,
|
|
1190
|
+
ids: idScored.length,
|
|
1191
|
+
metadata: likeScored.length,
|
|
1192
|
+
codex: 0,
|
|
1193
|
+
semantic: vectorScored.length,
|
|
1194
|
+
},
|
|
1195
|
+
semantic: semanticResult.semantic,
|
|
1196
|
+
degraded: ftsCorpus.degraded || { ftsCorpus: 'ok', message: null },
|
|
1197
|
+
ftsLagRows: Math.max(0, Number(ftsCorpus.ftsLagRows) || 0),
|
|
1198
|
+
ftsLagAlert: Number(ftsCorpus.ftsLagRows || 0)
|
|
1199
|
+
>= (Number(process.env.CTM_FTS_LAG_ALERT_ROWS) || 50_000),
|
|
1200
|
+
limit,
|
|
1201
|
+
};
|
|
1202
|
+
} catch (e) {
|
|
1203
|
+
console.error('[search] Error:', e.message);
|
|
1204
|
+
throw e;
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function apiGenerateTitles(req, res) {
|
|
1209
|
+
let body = '';
|
|
1210
|
+
req.on('data', chunk => { body += chunk; if (body.length > 1024 * 1024) { req.destroy(); return; } });
|
|
1211
|
+
req.on('end', async () => {
|
|
1212
|
+
try {
|
|
1213
|
+
const { sessions } = JSON.parse(body);
|
|
1214
|
+
if (!Array.isArray(sessions) || sessions.length === 0) {
|
|
1215
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1216
|
+
res.end(JSON.stringify({ error: 'sessions array required' }));
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Filter to sessions that don't already have titles (or user-renamed), skip CTM-internal sessions
|
|
1221
|
+
// Build title lookup from sessions table (single source of truth)
|
|
1222
|
+
const { rows: _allRows } = await _deps._readSessionCatalogOffThread('generate-titles');
|
|
1223
|
+
const existingTitles = {};
|
|
1224
|
+
for (const r of _allRows) {
|
|
1225
|
+
if (r.title) existingTitles[r.id] = { title: r.title, userRenamed: !!r.user_renamed, titleSource: r.title_source || '' };
|
|
1226
|
+
if (r.agent_session_id && r.title) existingTitles[r.agent_session_id] = { title: r.title, userRenamed: !!r.user_renamed, titleSource: r.title_source || '' };
|
|
1227
|
+
}
|
|
1228
|
+
const flattenExistingTitles = () => {
|
|
1229
|
+
const flatTitles = {};
|
|
1230
|
+
for (const [id, v] of Object.entries(existingTitles)) flatTitles[id] = v.title;
|
|
1231
|
+
return flatTitles;
|
|
1232
|
+
};
|
|
1233
|
+
// AC-3g (session-rename-mirror WP-3, §5 D-1): a row at the 'provider_title' tier (the
|
|
1234
|
+
// coding agent's own session name) is never overwritten by this tier-3 AI-title generator
|
|
1235
|
+
// — same "lower tier never overwrites a higher one" rule as the `userRenamed` check right
|
|
1236
|
+
// beside it. `existing.title` truthy already implies one of these two guards fired for
|
|
1237
|
+
// every populated row (existingTitles is only ever populated when r.title is set), so this
|
|
1238
|
+
// is belt-and-suspenders for the case a future caller narrows that population condition.
|
|
1239
|
+
const candidates = sessions.filter(s => {
|
|
1240
|
+
const existing = existingTitles[s.sessionId];
|
|
1241
|
+
if (existing && (existing.userRenamed || existing.titleSource === 'provider_title' || existing.title)) return false;
|
|
1242
|
+
return !!s.firstMessage;
|
|
1243
|
+
});
|
|
1244
|
+
// D-7 (WP-3): harness/provider boilerplate (<local-command-caveat>, <task-notification>,
|
|
1245
|
+
// "# AGENTS.md instructions for", a compaction summary) is not a human prompt — the same
|
|
1246
|
+
// predicate server/boot.js's background auto-title job gates its candidates on.
|
|
1247
|
+
const needTitles = [];
|
|
1248
|
+
for (const s of candidates) {
|
|
1249
|
+
if (_deps.isCtmInternalSession(s.firstMessage)) {
|
|
1250
|
+
_deps.persistTitleStatus(s.sessionId, 'skipped', 'ctm_internal');
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
if (!_deps.isHumanPromptCandidate(s.provider, s.firstMessage)) {
|
|
1254
|
+
_deps.persistTitleStatus(s.sessionId, 'skipped', 'not_human_prompt');
|
|
1255
|
+
continue;
|
|
1256
|
+
}
|
|
1257
|
+
needTitles.push(s);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (needTitles.length === 0) {
|
|
1261
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1262
|
+
res.end(JSON.stringify({ titles: flattenExistingTitles(), generated: 0 }));
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// Batch up to 20 sessions at a time
|
|
1267
|
+
const batch = needTitles.slice(0, 20);
|
|
1268
|
+
|
|
1269
|
+
// Title generation is a background enhancement served entirely through
|
|
1270
|
+
// Wall-E's routing authority now (WP-2, D-1) — CTM no longer preflight-checks
|
|
1271
|
+
// provider credentials itself; a credential-less setup surfaces as a normal
|
|
1272
|
+
// callClaudeAsync failure below, classified by titleGenerationFailureInfo().
|
|
1273
|
+
|
|
1274
|
+
// Resume/continuation boilerplate is stripped by server/boot.js's cleanFirstMessage, shared
|
|
1275
|
+
// with the background auto-title job (its `This session is being continued…` line is gone:
|
|
1276
|
+
// isHumanPromptCandidate already rejects that compaction row).
|
|
1277
|
+
const prompt = `Generate short, descriptive titles (max 60 chars) for these Claude Code sessions.
|
|
1278
|
+
Each session has an ID and the user's first message. Create a title that summarizes what the user was working on.
|
|
1279
|
+
|
|
1280
|
+
IMPORTANT: Do NOT prefix titles with "Resume:", "Continue:", "Fix:", or similar action verbs. Just describe the topic/task directly.
|
|
1281
|
+
Good: "Shopping cart checkout flow redesign"
|
|
1282
|
+
Bad: "Resume: Update checkout page logic"
|
|
1283
|
+
|
|
1284
|
+
Sessions:
|
|
1285
|
+
${batch.map((s, i) => `${i + 1}. [${s.sessionId}] ${_deps.cleanFirstMessage(s.firstMessage).slice(0, 300)}`).join('\n')}
|
|
1286
|
+
|
|
1287
|
+
Return ONLY a JSON array of objects with "id" and "title" fields. No markdown fences.`;
|
|
1288
|
+
|
|
1289
|
+
let result = '';
|
|
1290
|
+
try {
|
|
1291
|
+
result = await _deps.callClaudeAsync(prompt, {
|
|
1292
|
+
modelTier: 'fast',
|
|
1293
|
+
maxTokens: 1200,
|
|
1294
|
+
timeoutMs: 30000,
|
|
1295
|
+
task: 'session-title',
|
|
1296
|
+
});
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
const failure = _deps.titleGenerationFailureInfo(err);
|
|
1299
|
+
for (const s of batch) {
|
|
1300
|
+
_deps.persistTitleStatus(s.sessionId, 'failed', failure.reason);
|
|
1301
|
+
}
|
|
1302
|
+
console.warn('[titles] Skipping background title generation:', failure.detail || failure.reason);
|
|
1303
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1304
|
+
res.end(JSON.stringify({
|
|
1305
|
+
titles: flattenExistingTitles(),
|
|
1306
|
+
generated: 0,
|
|
1307
|
+
skipped: failure.reason,
|
|
1308
|
+
titleStatus: {
|
|
1309
|
+
status: 'failed',
|
|
1310
|
+
reason: failure.reason,
|
|
1311
|
+
provider: failure.provider,
|
|
1312
|
+
model: failure.model,
|
|
1313
|
+
userMessage: failure.userMessage,
|
|
1314
|
+
},
|
|
1315
|
+
}));
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
const match = result.match(/\[[\s\S]*\]/);
|
|
1319
|
+
if (match) {
|
|
1320
|
+
let titles = [];
|
|
1321
|
+
try {
|
|
1322
|
+
titles = JSON.parse(match[0]);
|
|
1323
|
+
} catch (err) {
|
|
1324
|
+
for (const s of batch) {
|
|
1325
|
+
_deps.persistTitleStatus(s.sessionId, 'failed', 'provider_response_invalid');
|
|
1326
|
+
}
|
|
1327
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1328
|
+
res.end(JSON.stringify({
|
|
1329
|
+
titles: flattenExistingTitles(),
|
|
1330
|
+
generated: 0,
|
|
1331
|
+
skipped: 'provider_response_invalid',
|
|
1332
|
+
titleStatus: {
|
|
1333
|
+
status: 'failed',
|
|
1334
|
+
reason: 'provider_response_invalid',
|
|
1335
|
+
userMessage: 'AI title generation returned an unreadable response. Existing session names are still available.',
|
|
1336
|
+
},
|
|
1337
|
+
}));
|
|
1338
|
+
return;
|
|
1339
|
+
}
|
|
1340
|
+
for (const t of titles) {
|
|
1341
|
+
if (t.id && t.title) {
|
|
1342
|
+
await _deps.dbModule.setSessionTitleNew(t.id, t.title, false, { source: 'ai_title', status: 'generated' });
|
|
1343
|
+
existingTitles[t.id] = { title: t.title, userRenamed: false };
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
} else {
|
|
1347
|
+
for (const s of batch) {
|
|
1348
|
+
_deps.persistTitleStatus(s.sessionId, 'failed', 'provider_response_invalid');
|
|
1349
|
+
}
|
|
1350
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1351
|
+
res.end(JSON.stringify({
|
|
1352
|
+
titles: flattenExistingTitles(),
|
|
1353
|
+
generated: 0,
|
|
1354
|
+
skipped: 'provider_response_invalid',
|
|
1355
|
+
titleStatus: {
|
|
1356
|
+
status: 'failed',
|
|
1357
|
+
reason: 'provider_response_invalid',
|
|
1358
|
+
userMessage: 'AI title generation returned an unreadable response. Existing session names are still available.',
|
|
1359
|
+
},
|
|
1360
|
+
}));
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Flatten to simple {id: title} for response
|
|
1365
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1366
|
+
res.end(JSON.stringify({ titles: flattenExistingTitles(), generated: batch.length }));
|
|
1367
|
+
} catch (err) {
|
|
1368
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1369
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
module.exports = {
|
|
1375
|
+
configure,
|
|
1376
|
+
apiSessionMessages,
|
|
1377
|
+
_computePromptMessagesFromFreshestSource,
|
|
1378
|
+
_apiRecentSessionsLegacy,
|
|
1379
|
+
applyOwnerFilterToSearchResults,
|
|
1380
|
+
buildSessionSearchResponse,
|
|
1381
|
+
catalogRowOwner,
|
|
1382
|
+
normalizeOwnerFilter,
|
|
1383
|
+
apiGenerateTitles,
|
|
1384
|
+
};
|