create-walle 0.9.29 → 0.9.30
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/package.json +1 -1
- package/template/bin/ctm-launch.sh +66 -21
- package/template/bin/dev.sh +13 -0
- package/template/bin/ensure-stable-node.js +11 -0
- package/template/bin/node-bin.sh +9 -0
- package/template/claude-task-manager/api-prompts.js +182 -8
- package/template/claude-task-manager/db.js +168 -13
- package/template/claude-task-manager/docs/session-title-authority.md +8 -3
- package/template/claude-task-manager/lib/claude-desktop-sessions.js +63 -0
- package/template/claude-task-manager/lib/codex-config-guard.js +124 -0
- package/template/claude-task-manager/lib/codex-rollout-snapshot.js +42 -2
- package/template/claude-task-manager/lib/coding-agent-models.js +5 -4
- package/template/claude-task-manager/lib/db-owner-cooperative-scheduler.js +114 -0
- package/template/claude-task-manager/lib/db-owner-task-queue.js +67 -0
- package/template/claude-task-manager/lib/db-owner-worker-client.js +5 -1
- package/template/claude-task-manager/lib/desktop-fork.js +81 -0
- package/template/claude-task-manager/lib/headless-term-service.js +7 -2
- package/template/claude-task-manager/lib/mirror-feed-sanitize.js +45 -0
- package/template/claude-task-manager/lib/runtime-context-truth.js +16 -6
- package/template/claude-task-manager/lib/scrollback-snapshot-policy.js +37 -0
- package/template/claude-task-manager/lib/session-history.js +88 -4
- package/template/claude-task-manager/lib/session-messages-page.js +13 -0
- package/template/claude-task-manager/lib/session-messages-projection.js +11 -27
- package/template/claude-task-manager/lib/session-stream.js +61 -16
- package/template/claude-task-manager/lib/session-title-signals.js +54 -0
- package/template/claude-task-manager/lib/session-token-usage.js +13 -0
- package/template/claude-task-manager/lib/state-sync/frame-emitter.js +43 -2
- package/template/claude-task-manager/lib/transcript-ingest-chunker.js +41 -0
- package/template/claude-task-manager/lib/transcript-store.js +12 -1
- package/template/claude-task-manager/lib/walle-session-model-catalog.js +100 -9
- package/template/claude-task-manager/public/css/walle-session.css +4 -0
- package/template/claude-task-manager/public/css/walle.css +0 -66
- package/template/claude-task-manager/public/index.html +766 -89
- package/template/claude-task-manager/public/js/state-sync-client.js +40 -1
- package/template/claude-task-manager/public/js/walle-session.js +211 -19
- package/template/claude-task-manager/public/js/walle.js +6 -110
- package/template/claude-task-manager/server.js +564 -90
- package/template/claude-task-manager/workers/db-owner-worker.js +15 -6
- package/template/claude-task-manager/workers/read-pool-worker.js +37 -0
- package/template/claude-task-manager/workers/session-host-pool-process.js +6 -1
- package/template/claude-task-manager/workers/session-host-process.js +6 -1
- package/template/claude-task-manager/workers/state-detectors/codex.js +33 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +78 -16
- package/template/wall-e/api-walle.js +24 -43
- package/template/wall-e/bin/walle-mcp-stdio.js +138 -5
- package/template/wall-e/brain.js +122 -1
- package/template/wall-e/chat.js +46 -1
- package/template/wall-e/http/model-admin.js +22 -0
- package/template/wall-e/lib/brain-owner-worker-client.js +20 -0
- package/template/wall-e/lib/parent-brain-owner-client.js +109 -0
- package/template/wall-e/lib/runtime-worker-pool.js +15 -1
- package/template/wall-e/lib/scheduler-worker-jobs.js +30 -1
- package/template/wall-e/lib/scheduler.js +71 -2
- package/template/wall-e/lib/slack-identity.js +120 -0
- package/template/wall-e/lib/slack-permalink.js +107 -0
- package/template/wall-e/lib/slack-web.js +174 -0
- package/template/wall-e/lib/worker-thread-pool.js +49 -0
- package/template/wall-e/llm/cli-binary.js +17 -4
- package/template/wall-e/llm/codex-cli.js +105 -60
- package/template/wall-e/llm/model-catalog.js +129 -17
- package/template/wall-e/loops/backfill.js +32 -16
- package/template/wall-e/loops/ingest.js +50 -16
- package/template/wall-e/mcp-server.js +215 -6
- package/template/wall-e/skills/_bundled/gws-workspace/gws-router +61 -4
- package/template/wall-e/skills/_bundled/slack-mentions/run.js +167 -52
- package/template/wall-e/skills/skill-planner.js +5 -26
- package/template/wall-e/utils/dedup.js +165 -66
- package/template/wall-e/weather-runtime.js +12 -4
- package/template/wall-e/workers/brain-owner-worker.js +60 -0
- package/template/wall-e/workers/runtime-worker.js +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-walle",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.30",
|
|
4
4
|
"description": "CTM + Wall-E — AI coding dashboard and personal digital twin agent. Multi-agent terminal for Claude Code, Codex, Gemini, Aider, OpenCode, and more, plus prompt editor, task queue, remote phone and tablet access, code/doc review, and an agent that learns from Slack, email & calendar.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"create-walle": "bin/create-walle.js"
|
|
@@ -9,10 +9,35 @@
|
|
|
9
9
|
# LaunchAgent plist never needs to change.
|
|
10
10
|
set -euo pipefail
|
|
11
11
|
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
12
|
-
|
|
12
|
+
# CTM_LAUNCH_NODE_BIN lets a test inject the pinned node without running node-bin.sh.
|
|
13
|
+
NODE_BIN="${CTM_LAUNCH_NODE_BIN:-"$(bash "$ROOT/bin/node-bin.sh")"}"
|
|
13
14
|
SERVER="$ROOT/claude-task-manager/server.js"
|
|
14
15
|
PINNED_V="$("$NODE_BIN" -v 2>/dev/null)"
|
|
15
16
|
|
|
17
|
+
# Exec the chosen runtime — or, under CTM_LAUNCH_PRINT_CHOICE (tests), print the path it WOULD exec
|
|
18
|
+
# and exit 0 without launching the server. Keeps the candidate ladder below assertable end-to-end.
|
|
19
|
+
_exec_choice() {
|
|
20
|
+
local bin="$1"; shift
|
|
21
|
+
if [ -n "${CTM_LAUNCH_PRINT_CHOICE:-}" ]; then printf '%s\n' "$bin"; exit 0; fi
|
|
22
|
+
exec "$bin" "$SERVER" "$@"
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Self-heal the native SQLite driver BEFORE exec'ing the server. A hard power-off can leave
|
|
26
|
+
# better_sqlite3.node torn on disk, and a Homebrew node upgrade can leave it built for the wrong
|
|
27
|
+
# ABI. The server DETECTS this but cannot hot-reload a native module mid-process, so it wedges
|
|
28
|
+
# the boot: DB-owner worker dies -> "Database not initialized" everywhere -> no port bound ->
|
|
29
|
+
# launchd respawns into the SAME broken state every ~5s (a respawn spiral that never converges).
|
|
30
|
+
# Running the preflight here rebuilds the driver if needed, so the fresh `exec` below loads a good
|
|
31
|
+
# binary. It is cheap on the happy path (a require + smoke-test that early-returns when the ABI
|
|
32
|
+
# already matches) and only does the heavy `npm rebuild` when the driver is genuinely broken.
|
|
33
|
+
# Best-effort: a failure here never blocks the boot — the server's own in-process repair still runs.
|
|
34
|
+
# Skipped under CTM_LAUNCH_PRINT_CHOICE: it is irrelevant to runtime selection and needs a real DB.
|
|
35
|
+
CHECK_DRIVER="$ROOT/claude-task-manager/bin/check-sqlite-driver.js"
|
|
36
|
+
if [ -z "${CTM_LAUNCH_PRINT_CHOICE:-}" ] && [ -f "$CHECK_DRIVER" ]; then
|
|
37
|
+
( cd "$ROOT/claude-task-manager" && "$NODE_BIN" "$CHECK_DRIVER" ) >/tmp/ctm-boot-sqlite-check.log 2>&1 \
|
|
38
|
+
|| echo "[ctm-launch] sqlite driver preflight reported a problem; see /tmp/ctm-boot-sqlite-check.log (boot continues; server self-repair still applies)" >&2
|
|
39
|
+
fi
|
|
40
|
+
|
|
16
41
|
# Prefer a STABLE-IDENTITY node so macOS TCC grants (e.g. the "Coding Task Manager would like to
|
|
17
42
|
# access data from other apps" prompt) PERSIST across restarts. macOS keys a TCC grant to the
|
|
18
43
|
# binary's code-signing Designated Requirement: a notarized / Developer-ID-signed node keeps its
|
|
@@ -23,46 +48,66 @@ PINNED_V="$("$NODE_BIN" -v 2>/dev/null)"
|
|
|
23
48
|
# bump can never select a mismatched-ABI runtime (preserves the "upgrade = edit .node-version"
|
|
24
49
|
# guarantee, and the daemon's native modules stay ABI-correct).
|
|
25
50
|
_ver_match() { [ -x "$1" ] && [ "$("$1" -v 2>/dev/null)" = "$PINNED_V" ]; }
|
|
51
|
+
# Does this binary carry a stable code-signing Team Identifier (Developer-ID signed)? Signal that a
|
|
52
|
+
# TCC/Full-Disk-Access grant will PERSIST and that prompts show a branded name. A fast local read.
|
|
53
|
+
# NB this alone CANNOT distinguish "our branded CTM bundle" from the bare notarized node — the
|
|
54
|
+
# notarized node is also Developer-ID signed (Team HX7739G8FX, Node.js Foundation). The discriminator
|
|
55
|
+
# is the code-signing IDENTIFIER (see _is_ctm_bundle_identity).
|
|
56
|
+
# NB capture codesign's output FIRST, then match the string — do NOT pipe `codesign | grep -q`.
|
|
57
|
+
# `grep -q` exits on the first match and closes the pipe; codesign then dies with SIGPIPE (141), and
|
|
58
|
+
# under `set -o pipefail` (above) the pipeline is reported as FAILED even though the pattern matched.
|
|
59
|
+
# That false-negative is exactly what made the daemon keep running the wrong node. Capturing to a
|
|
60
|
+
# var runs codesign to completion (exit 0), then a here-string grep has no upstream to kill.
|
|
61
|
+
_has_team_id() { local o; o="$(codesign -dv "$1" 2>&1)" || true; grep -q '^TeamIdentifier=[A-Z0-9]' <<<"$o"; }
|
|
62
|
+
# Is this binary OUR branded CTM bundle (Identifier=com.walle.ctm)? That is the EXACT identity the
|
|
63
|
+
# Full Disk Access banner tells the user to grant ("Coding Task Manager.app"). The notarized node's
|
|
64
|
+
# Identifier is "node", so this cleanly separates the two.
|
|
65
|
+
_is_ctm_bundle_identity() { local o; o="$(codesign -dv "$1" 2>&1)" || true; grep -q '^Identifier=com\.walle\.ctm$' <<<"$o"; }
|
|
66
|
+
|
|
67
|
+
CTM_BUNDLE="$HOME/.walle/bundles/Coding Task Manager.app/Contents/MacOS/Coding Task Manager"
|
|
26
68
|
|
|
27
|
-
# 0) The
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
69
|
+
# 0) The branded CTM bundle (Identifier=com.walle.ctm) — the EXACT identity the FDA banner asks the
|
|
70
|
+
# user to grant Full Disk Access to. Prefer it ABOVE the stable-node marker. WHY first: the marker
|
|
71
|
+
# written by ensure-stable-node.js can point at the bare notarized node ("node", Team HX7739G8FX);
|
|
72
|
+
# because that node IS Developer-ID signed it would satisfy a naive "has a Team Identifier" check
|
|
73
|
+
# and win — the daemon then runs as "node", the user's grant to "Coding Task Manager.app" never
|
|
74
|
+
# applies, and the macOS "network volume"/FDA prompt recurs on EVERY restart. Checking THIS bundle
|
|
75
|
+
# path + its com.walle.ctm identity makes the grant-matching runtime authoritative. Gated on the
|
|
76
|
+
# version (ABI) match too. Costs one fast `codesign -dv` read per boot (boots are rare); on a
|
|
77
|
+
# no-Dev-ID machine the bundle is absent/self-signed so this falls through with no behavior change.
|
|
78
|
+
if _ver_match "$CTM_BUNDLE" && _is_ctm_bundle_identity "$CTM_BUNDLE" && _has_team_id "$CTM_BUNDLE"; then
|
|
79
|
+
_exec_choice "$CTM_BUNDLE" "$@"
|
|
80
|
+
fi
|
|
81
|
+
# 1) The stable daemon node chosen by bin/ensure-stable-node.js (run off the boot path from
|
|
82
|
+
# restart-ctm.sh). On a machine WITHOUT a Developer ID this is the bare notarized node — the best
|
|
83
|
+
# stable identity available there. Version-gated; the version check rejects a stale marker.
|
|
32
84
|
MARKER="$HOME/.walle/.stable-daemon-node"
|
|
33
85
|
if [ -f "$MARKER" ]; then
|
|
34
86
|
STABLE_NODE="$(head -n1 "$MARKER" 2>/dev/null)"
|
|
35
87
|
if [ -n "$STABLE_NODE" ] && _ver_match "$STABLE_NODE"; then
|
|
36
|
-
|
|
88
|
+
_exec_choice "$STABLE_NODE" "$@"
|
|
37
89
|
fi
|
|
38
90
|
fi
|
|
39
|
-
#
|
|
91
|
+
# 2) Notarized node handed in by the downloadable Developer-ID Wall-E.app.
|
|
40
92
|
if [ -n "${WALLE_NOTARIZED_NODE:-}" ] && _ver_match "$WALLE_NOTARIZED_NODE"; then
|
|
41
|
-
|
|
42
|
-
fi
|
|
43
|
-
# 2) The branded .app bundle node when it carries a stable Team Identifier (Developer-ID-signed by
|
|
44
|
-
# ensure-stable-node.js) — branded AND grant-persisting. `codesign -dv` is a fast local read; it
|
|
45
|
-
# runs only on a cold boot where the marker is absent/stale.
|
|
46
|
-
CTM_BUNDLE="$HOME/.walle/bundles/Coding Task Manager.app/Contents/MacOS/Coding Task Manager"
|
|
47
|
-
if _ver_match "$CTM_BUNDLE" && codesign -dv "$CTM_BUNDLE" 2>&1 | grep -q '^TeamIdentifier=[A-Z0-9]'; then
|
|
48
|
-
exec "$CTM_BUNDLE" "$SERVER" "$@"
|
|
93
|
+
_exec_choice "$WALLE_NOTARIZED_NODE" "$@"
|
|
49
94
|
fi
|
|
50
95
|
# 2.5) The adopted Developer-ID-notarized Wall-E.app's vendored node (create-walle
|
|
51
96
|
# ensureNotarizedBrandedApp) — branded "Wall-E" AND grant-persisting, for no-Dev-ID machines.
|
|
52
97
|
# Gated on the version match + a stable Team Identifier (skips a self-signed/wrong bundle).
|
|
53
98
|
BRANDED_APP_NODE="$HOME/.walle/notarized-app/Wall-E.app/Contents/Resources/node"
|
|
54
|
-
if _ver_match "$BRANDED_APP_NODE" &&
|
|
55
|
-
|
|
99
|
+
if _ver_match "$BRANDED_APP_NODE" && _has_team_id "$BRANDED_APP_NODE"; then
|
|
100
|
+
_exec_choice "$BRANDED_APP_NODE" "$@"
|
|
56
101
|
fi
|
|
57
102
|
# 3) Notarized node we provisioned ourselves (~/.walle/notarized-node) — stable but anonymous
|
|
58
103
|
# ("node"); the fallback for machines without a Developer ID.
|
|
59
104
|
NOTARIZED_NODE="$HOME/.walle/notarized-node/bin/node"
|
|
60
105
|
if _ver_match "$NOTARIZED_NODE"; then
|
|
61
|
-
|
|
106
|
+
_exec_choice "$NOTARIZED_NODE" "$@"
|
|
62
107
|
fi
|
|
63
108
|
# 4) The branded bundle node even if only self-signed (branded name, but may re-prompt).
|
|
64
109
|
if _ver_match "$CTM_BUNDLE"; then
|
|
65
|
-
|
|
110
|
+
_exec_choice "$CTM_BUNDLE" "$@"
|
|
66
111
|
fi
|
|
67
112
|
# 5) Last resort: the pinned node (ABI-correct, but no stable TCC identity → may re-prompt).
|
|
68
|
-
|
|
113
|
+
_exec_choice "$NODE_BIN" "$@"
|
package/template/bin/dev.sh
CHANGED
|
@@ -164,6 +164,19 @@ mkdir -p "$DEV_DIR"
|
|
|
164
164
|
# (corruption + contention). A throwaway CODEX_HOME keeps dev codex writes — and
|
|
165
165
|
# CTM's own rollout reads, which resolve via the same CODEX_HOME — inside DEV_DIR.
|
|
166
166
|
mkdir -p "$DEV_DIR/codex/sessions"
|
|
167
|
+
# Seed the throwaway CODEX_HOME with the user's existing Codex login. The isolation above
|
|
168
|
+
# relocates CODEX_HOME, which ALSO relocates ~/.codex/auth.json — so without this, dev codex
|
|
169
|
+
# sessions are logged out and prompt for `codex login` (the "codex login issue in staging").
|
|
170
|
+
# Copy only auth.json from the real ~/.codex so dev codex is authenticated while rollouts/sessions
|
|
171
|
+
# stay isolated in DEV_DIR. We deliberately do NOT copy config.toml — it can carry MCP entries that
|
|
172
|
+
# point at the user's primary (e.g. the node_repl missing-binary noise). Idempotent: never clobber
|
|
173
|
+
# an existing dev login.
|
|
174
|
+
_REAL_CODEX_HOME="$HOME/.codex"
|
|
175
|
+
if [[ -f "$_REAL_CODEX_HOME/auth.json" && ! -f "$DEV_DIR/codex/auth.json" ]]; then
|
|
176
|
+
cp "$_REAL_CODEX_HOME/auth.json" "$DEV_DIR/codex/auth.json" 2>/dev/null \
|
|
177
|
+
&& chmod 600 "$DEV_DIR/codex/auth.json" 2>/dev/null \
|
|
178
|
+
&& echo " seeded dev CODEX_HOME auth from $_REAL_CODEX_HOME/auth.json"
|
|
179
|
+
fi
|
|
167
180
|
|
|
168
181
|
cleanup_processes() {
|
|
169
182
|
local stale_args=()
|
|
@@ -65,6 +65,17 @@ function resolveStableNode(deps) {
|
|
|
65
65
|
if (t.exec === ctmExec && team) ctmTeam = team;
|
|
66
66
|
}
|
|
67
67
|
if (ctmTeam) return ctmExec;
|
|
68
|
+
// Re-signing did not land a Team Identifier THIS run (e.g. a transient codesign failure), but
|
|
69
|
+
// the CTM bundle may ALREADY be Developer-ID-signed on disk from a prior run — that signature
|
|
70
|
+
// is self-contained and stays valid regardless. Prefer it over the anonymous notarized node so
|
|
71
|
+
// the marker records the BRANDED identity and the daemon keeps matching the user's Full Disk
|
|
72
|
+
// Access grant to "Coding Task Manager.app" (otherwise it runs as "node" → the FDA banner
|
|
73
|
+
// re-prompts every restart). Gated on existing + version-matching the pin (ABI safety).
|
|
74
|
+
if (ctmExec && existsSync(ctmExec) && (!pin || cw.nodeReportsVersion(ctmExec, pin))
|
|
75
|
+
&& codesign.localCodeSignTeamId && codesign.localCodeSignTeamId(ctmExec)) {
|
|
76
|
+
log('CTM bundle already Developer-ID-signed on disk → using it (skip notarized fallback)');
|
|
77
|
+
return ctmExec;
|
|
78
|
+
}
|
|
68
79
|
} catch (e) {
|
|
69
80
|
log(`bundle signing failed: ${e && e.message ? e.message : e}`);
|
|
70
81
|
}
|
package/template/bin/node-bin.sh
CHANGED
|
@@ -44,6 +44,15 @@ if [[ -z "$PIN" ]]; then
|
|
|
44
44
|
fi
|
|
45
45
|
|
|
46
46
|
candidates=(
|
|
47
|
+
# Immutable, self-managed runtimes FIRST. A `brew upgrade node` (often dragged in by an
|
|
48
|
+
# unrelated `brew install`) repoints or deletes the Cellar dir below, which would otherwise
|
|
49
|
+
# leave this resolver unable to find the pin -> daemon won't start. These vendored nodes are
|
|
50
|
+
# provisioned by bin/ensure-stable-node.js / create-walle and are not touched by Homebrew, so
|
|
51
|
+
# resolution survives that churn. Every candidate is STILL gated on an exact version match with
|
|
52
|
+
# the pin in the loop below, so a stale vendored node is skipped (it can never select a wrong ABI).
|
|
53
|
+
"$HOME/.walle/notarized-node/bin/node"
|
|
54
|
+
"$HOME/.walle/notarized-app/Wall-E.app/Contents/Resources/node"
|
|
55
|
+
"$HOME/.walle/bundles/Coding Task Manager.app/Contents/MacOS/Coding Task Manager"
|
|
47
56
|
"/opt/homebrew/Cellar/node/$PIN/bin/node"
|
|
48
57
|
"/usr/local/Cellar/node/$PIN/bin/node"
|
|
49
58
|
"$HOME/.fnm/node-versions/v$PIN/installation/bin/node"
|
|
@@ -13,6 +13,7 @@ const permissionMatch = require('./lib/permission-match');
|
|
|
13
13
|
// CTM permissions are a standalone global config in the CTM DB.
|
|
14
14
|
const walleClient = require('./lib/walle-client');
|
|
15
15
|
const claudeDesktopSessions = require('./lib/claude-desktop-sessions');
|
|
16
|
+
const desktopFork = require('./lib/desktop-fork');
|
|
16
17
|
const skillAutocomplete = require('./lib/skill-autocomplete');
|
|
17
18
|
const skillIntentResolver = require('./lib/skill-intent-resolver');
|
|
18
19
|
const resourceLinks = require('./lib/resource-links');
|
|
@@ -48,6 +49,26 @@ const CONVERSATION_IMPORT_MIN_INTERVAL_MS = Math.max(0, Number(process.env.CTM_C
|
|
|
48
49
|
const CONVERSATION_IMPORT_FORCE_BYTES = Math.max(0, Number(process.env.CTM_CONVERSATION_IMPORT_FORCE_BYTES ?? 256 * 1024));
|
|
49
50
|
const _lastConversationImportAt = new Map(); // sessionId → Date.now() of last completed import
|
|
50
51
|
|
|
52
|
+
// Event-driven (near-real-time) durable import. The 8s floor above exists to stop a hot session's
|
|
53
|
+
// rapid small appends from re-processing the WHOLE conversation each tick (the giant-transcript
|
|
54
|
+
// write-lock thrash). With blob retirement the common case is now an O(Δ) append (a tiny write on
|
|
55
|
+
// the low-priority lane), so when this is on we track the live tail at a short coalescing window —
|
|
56
|
+
// but ONLY for the cheap append path. A session that recently fell back to a FULL rebuild keeps the
|
|
57
|
+
// long floor so a rewrite-in-progress session can't thrash. Off → exact 8s/batch behavior.
|
|
58
|
+
const CONVERSATION_IMPORT_EVENT_DRIVEN = process.env.CTM_CONVERSATION_IMPORT_EVENT_DRIVEN === '1';
|
|
59
|
+
const CONVERSATION_IMPORT_EVENT_DRIVEN_MIN_INTERVAL_MS = Math.max(0, Number(
|
|
60
|
+
process.env.CTM_CONVERSATION_IMPORT_EVENT_DRIVEN_MIN_INTERVAL_MS ?? 250));
|
|
61
|
+
const _lastConversationFullRebuildAt = new Map(); // sessionId → Date.now() of last FULL rebuild
|
|
62
|
+
|
|
63
|
+
// Pure decision: the debounce floor for a hot session's small-growth (append-eligible) re-import.
|
|
64
|
+
// Extracted so the rebuild-thrash guard is unit-testable without the import machinery.
|
|
65
|
+
function _conversationImportFloorMs({ eventDriven, baseFloorMs, eventFloorMs, lastRebuildAt, now }) {
|
|
66
|
+
if (!eventDriven) return baseFloorMs;
|
|
67
|
+
// Held back to the long floor only while a recent full rebuild is still "warm".
|
|
68
|
+
const rebuiltRecently = lastRebuildAt > 0 && (now - lastRebuildAt) < baseFloorMs;
|
|
69
|
+
return rebuiltRecently ? baseFloorMs : eventFloorMs;
|
|
70
|
+
}
|
|
71
|
+
|
|
51
72
|
function setDbMaintenanceRunner(fn) {
|
|
52
73
|
dbMaintenanceRunner = typeof fn === 'function' ? fn : null;
|
|
53
74
|
}
|
|
@@ -192,6 +213,10 @@ function handlePromptApi(req, res, url) {
|
|
|
192
213
|
const p = url.pathname;
|
|
193
214
|
const m = req.method;
|
|
194
215
|
|
|
216
|
+
// --- Claude Desktop → Code conversion (snapshot-and-fork) ---
|
|
217
|
+
if (p === '/api/claude-desktop/forks' && m === 'GET') return handleListDesktopForks(req, res);
|
|
218
|
+
if (p === '/api/claude-desktop/convert' && m === 'POST') return handleConvertDesktopSession(req, res);
|
|
219
|
+
|
|
195
220
|
// --- Prompts ---
|
|
196
221
|
if (p === '/api/prompts' && m === 'GET') return handleListPrompts(req, res, url);
|
|
197
222
|
if (p === '/api/prompts' && m === 'POST') return handleCreatePrompt(req, res);
|
|
@@ -392,6 +417,70 @@ function handleListPrompts(req, res, url) {
|
|
|
392
417
|
jsonResponse(res, 200, db.listPrompts(opts));
|
|
393
418
|
}
|
|
394
419
|
|
|
420
|
+
// Convert a read-only Claude Desktop conversation into a resumable Claude Code session.
|
|
421
|
+
// Thin HTTP wrapper around lib/desktop-fork.convertDesktopConversation; `deps` is injectable
|
|
422
|
+
// for unit tests (defaults to the real Desktop cache + fork orchestrator).
|
|
423
|
+
const DESKTOP_CACHE_ONLY_MESSAGE =
|
|
424
|
+
'Claude Desktop has only cached this conversation in the recent list — the full transcript ' +
|
|
425
|
+
'is not in the local cache yet. Open the conversation in Claude Desktop once, then rescan, ' +
|
|
426
|
+
'before converting it to a Claude Code session.';
|
|
427
|
+
|
|
428
|
+
// List Desktop conversations that already have a live Claude Code fork, so the client can
|
|
429
|
+
// flip "Convert" → "Resume fork" per conversation. Excludes forks whose transcript was
|
|
430
|
+
// deleted (those should re-offer conversion). `deps` injectable for tests.
|
|
431
|
+
function handleListDesktopForks(req, res, deps = {}) {
|
|
432
|
+
const dbm = deps.db || db;
|
|
433
|
+
const fsx = deps.fs || fs;
|
|
434
|
+
let rows;
|
|
435
|
+
try {
|
|
436
|
+
rows = dbm.listDesktopForks() || [];
|
|
437
|
+
} catch (e) {
|
|
438
|
+
return jsonResponse(res, 500, { error: 'list_failed', message: e && e.message || 'failed' });
|
|
439
|
+
}
|
|
440
|
+
const forks = rows
|
|
441
|
+
.filter((f) => f.jsonlPath && fsx.existsSync(f.jsonlPath))
|
|
442
|
+
.map((f) => ({ desktopUuid: f.desktopUuid, forkSessionId: f.forkSessionId, cwd: f.projectPath || '' }));
|
|
443
|
+
return jsonResponse(res, 200, { forks });
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function handleConvertDesktopSession(req, res, deps = {}) {
|
|
447
|
+
const desktop = deps.desktop || claudeDesktopSessions;
|
|
448
|
+
const fork = deps.fork || desktopFork;
|
|
449
|
+
|
|
450
|
+
let body;
|
|
451
|
+
try {
|
|
452
|
+
body = await readBody(req);
|
|
453
|
+
} catch {
|
|
454
|
+
return jsonResponse(res, 400, { error: 'invalid_body' });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const desktopUuid = String((body && body.desktopUuid) || '').trim();
|
|
458
|
+
const cwd = String((body && body.cwd) || '').trim();
|
|
459
|
+
if (!desktopUuid || !cwd) {
|
|
460
|
+
return jsonResponse(res, 400, { error: 'desktopUuid and cwd are required' });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const session = desktop.getSession(desktopUuid);
|
|
464
|
+
if (!session) {
|
|
465
|
+
return jsonResponse(res, 404, { error: 'Claude Desktop conversation not found' });
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const messages = desktop.getMessages(desktopUuid) || [];
|
|
469
|
+
if (messages.length === 0) {
|
|
470
|
+
return jsonResponse(res, 409, { error: 'cache_only', message: DESKTOP_CACHE_ONLY_MESSAGE });
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
try {
|
|
474
|
+
const result = fork.convertDesktopConversation({ session, cwd });
|
|
475
|
+
return jsonResponse(res, 200, { ok: true, ...result });
|
|
476
|
+
} catch (e) {
|
|
477
|
+
if (/no .*message/i.test(e && e.message || '')) {
|
|
478
|
+
return jsonResponse(res, 409, { error: 'cache_only', message: DESKTOP_CACHE_ONLY_MESSAGE });
|
|
479
|
+
}
|
|
480
|
+
return jsonResponse(res, 500, { error: 'convert_failed', message: e && e.message || 'conversion failed' });
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
395
484
|
async function handleCreatePrompt(req, res) {
|
|
396
485
|
try {
|
|
397
486
|
const data = await readBody(req);
|
|
@@ -1555,6 +1644,8 @@ async function _importCompactPair(parsed, jsonlPath, bakPath, jsonlSize, bakSize
|
|
|
1555
1644
|
model_provider: parsed.modelProvider || (existing && existing.model_provider) || CLAUDE_MODEL_PROVIDER,
|
|
1556
1645
|
model_id: parsed.modelId || (existing && existing.model_id) || '',
|
|
1557
1646
|
import_parser_version: DEFAULT_CONVERSATION_IMPORT_PARSER_VERSION,
|
|
1647
|
+
// Chunked-import route (cold/large): defer rows to content-rows-backfill, keep the real blob.
|
|
1648
|
+
..._chunkedImportFlags(parsed.sessionId, allMessages, totalSize, 'compact-pair'),
|
|
1558
1649
|
});
|
|
1559
1650
|
return true;
|
|
1560
1651
|
}
|
|
@@ -1683,7 +1774,15 @@ async function _conversationImportCandidates(allFiles, lastScanAt) {
|
|
|
1683
1774
|
const lastAt = _lastConversationImportAt.get(sessionId) || 0;
|
|
1684
1775
|
const sinceLast = Date.now() - lastAt;
|
|
1685
1776
|
const grewBytes = Math.max(0, effectiveSize - existingSize);
|
|
1686
|
-
|
|
1777
|
+
// Near-real-time floor for the cheap append path; long floor right after a full rebuild.
|
|
1778
|
+
const floorMs = _conversationImportFloorMs({
|
|
1779
|
+
eventDriven: CONVERSATION_IMPORT_EVENT_DRIVEN,
|
|
1780
|
+
baseFloorMs: CONVERSATION_IMPORT_MIN_INTERVAL_MS,
|
|
1781
|
+
eventFloorMs: CONVERSATION_IMPORT_EVENT_DRIVEN_MIN_INTERVAL_MS,
|
|
1782
|
+
lastRebuildAt: _lastConversationFullRebuildAt.get(sessionId) || 0,
|
|
1783
|
+
now: Date.now(),
|
|
1784
|
+
});
|
|
1785
|
+
if (lastAt > 0 && sinceLast < floorMs && grewBytes < CONVERSATION_IMPORT_FORCE_BYTES) {
|
|
1687
1786
|
continue; // imported very recently and only a little new data — let appends coalesce
|
|
1688
1787
|
}
|
|
1689
1788
|
}
|
|
@@ -1734,6 +1833,46 @@ function _backgroundTranscriptImportMaxBytes() {
|
|
|
1734
1833
|
return Math.max(256 * 1024, Number.isFinite(raw) ? raw : BACKGROUND_TRANSCRIPT_IMPORT_DEFAULT_BYTES);
|
|
1735
1834
|
}
|
|
1736
1835
|
|
|
1836
|
+
// Phase 1.5: a cold/large full-rebuild's inline session_message_rows write (delete-all + insert-all)
|
|
1837
|
+
// can block the db-owner worker for seconds — over the coop slice budget. Above a message-count
|
|
1838
|
+
// threshold, route the import through the CHUNKED path: importSessionConversation writes the blob +
|
|
1839
|
+
// stamps the source-len HWM now (forceBlobWrite + skipMessageRows), then content-rows-backfill fills
|
|
1840
|
+
// rows in windows across slices. Returns the two flags to spread into the importSessionConversation
|
|
1841
|
+
// options. Applied at every large cold-import call site (codex / claude-desktop / generic claude).
|
|
1842
|
+
// Over-cap sessions ignore forceBlobWrite downstream (empty blob, transcript-store source of truth).
|
|
1843
|
+
// Routing counters (per import run) — surfaced in the always-on [auto-import] summary so the
|
|
1844
|
+
// chunked-vs-inline distribution is visible WITHOUT depending on CTM_COOP_LOG reaching the worker
|
|
1845
|
+
// thread. Reset by runIncrementalConversationImport after it logs them.
|
|
1846
|
+
const _chunkedRouteStats = { chunked: 0, inline: 0 };
|
|
1847
|
+
function _readResetChunkedRouteStats() {
|
|
1848
|
+
const s = { ..._chunkedRouteStats };
|
|
1849
|
+
_chunkedRouteStats.chunked = 0; _chunkedRouteStats.inline = 0;
|
|
1850
|
+
return s;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function _chunkedImportFlags(sessionId, messages, fileSize, reason) {
|
|
1854
|
+
// Gated on the coop scheduler: deferring rows only helps when the cooperative scheduler is driving
|
|
1855
|
+
// bounded slices (and the content-rows-backfill coop/legacy job fills the deferred rows). With the
|
|
1856
|
+
// flag OFF the import behaves byte-for-byte as legacy (rows written inline) — so merging this branch
|
|
1857
|
+
// is a true no-op on the primary until CTM_COOP_SCHEDULER=1 is set.
|
|
1858
|
+
if (process.env.CTM_COOP_SCHEDULER !== '1') return { skipMessageRows: false, forceBlobWrite: false };
|
|
1859
|
+
const minMsgs = Math.max(1, Number(process.env.CTM_IMPORT_CHUNK_MIN_MSGS || 1000));
|
|
1860
|
+
// ALSO trigger on file size: a giant rollout is tail-bounded at parse time (transcriptMaxBytes),
|
|
1861
|
+
// so `messages.length` here can be small even though the session is huge and its inline row write
|
|
1862
|
+
// would block the worker. Keying on the JSONL byte high-water-mark catches those. Default 2MB.
|
|
1863
|
+
const minBytes = Math.max(1, Number(process.env.CTM_IMPORT_CHUNK_MIN_BYTES || 2 * 1024 * 1024));
|
|
1864
|
+
const n = Array.isArray(messages) ? messages.length : 0;
|
|
1865
|
+
const bytes = Number(fileSize || 0);
|
|
1866
|
+
const useChunked = n > minMsgs || bytes > minBytes;
|
|
1867
|
+
_chunkedRouteStats[useChunked ? 'chunked' : 'inline'] += 1;
|
|
1868
|
+
if (process.env.CTM_COOP_LOG === '1') {
|
|
1869
|
+
const fileMB = (bytes / (1024 * 1024)).toFixed(1);
|
|
1870
|
+
const trig = useChunked ? (n > minMsgs ? 'msgs' : 'bytes') : 'none';
|
|
1871
|
+
console.log(`[coop-import] sid=${String(sessionId || '').slice(0, 8)} msgs=${n} fileMB=${fileMB} path=${useChunked ? 'chunked' : 'inline'} trigger=${trig} reason=${reason}`);
|
|
1872
|
+
}
|
|
1873
|
+
return { skipMessageRows: useChunked, forceBlobWrite: useChunked };
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1737
1876
|
function _codexImportedFileSize(parsedFileSize, prevFileSize, parsedTail) {
|
|
1738
1877
|
const fileSize = Math.max(0, Number(parsedFileSize || 0));
|
|
1739
1878
|
const prev = Math.max(0, Number(prevFileSize || 0));
|
|
@@ -1852,6 +1991,9 @@ async function _importCodexSessionFile(parsed, filePath, options = {}) {
|
|
|
1852
1991
|
model_provider: modelProvider,
|
|
1853
1992
|
model_id: model || (existing && existing.model_id) || '',
|
|
1854
1993
|
import_parser_version: parserVersion,
|
|
1994
|
+
// Chunked-import route (cold/large codex rollout — the giant-session case): defer rows to
|
|
1995
|
+
// content-rows-backfill, keep the real blob. Over-cap rollouts ignore forceBlobWrite downstream.
|
|
1996
|
+
..._chunkedImportFlags(sessionId, allMessages, importedFileSize, 'codex'),
|
|
1855
1997
|
});
|
|
1856
1998
|
|
|
1857
1999
|
try {
|
|
@@ -2050,6 +2192,8 @@ async function importSessionFile(filePath, projectPath, projectEntry, options =
|
|
|
2050
2192
|
model_provider: parsed.modelProvider || (existing && existing.model_provider) || CLAUDE_MODEL_PROVIDER,
|
|
2051
2193
|
model_id: parsed.modelId || (existing && existing.model_id) || '',
|
|
2052
2194
|
import_parser_version: DEFAULT_CONVERSATION_IMPORT_PARSER_VERSION,
|
|
2195
|
+
// Chunked-import route (cold/large): defer rows to content-rows-backfill, keep the real blob.
|
|
2196
|
+
..._chunkedImportFlags(parsed.sessionId, messages, parsed.fileSize, 'claude-desktop'),
|
|
2053
2197
|
});
|
|
2054
2198
|
return true;
|
|
2055
2199
|
}
|
|
@@ -2131,7 +2275,12 @@ async function importSessionFile(filePath, projectPath, projectEntry, options =
|
|
|
2131
2275
|
rename_name: parsedRename || parsed.renameName || '',
|
|
2132
2276
|
import_parser_version: DEFAULT_CONVERSATION_IMPORT_PARSER_VERSION,
|
|
2133
2277
|
});
|
|
2134
|
-
if (appended && appended.ok)
|
|
2278
|
+
if (appended && appended.ok) {
|
|
2279
|
+
// Cheap O(Δ) append succeeded — clear any rebuild backoff so event-driven mode
|
|
2280
|
+
// tracks this session's live tail at the fast cadence.
|
|
2281
|
+
_lastConversationFullRebuildAt.delete(parsed.sessionId);
|
|
2282
|
+
return true;
|
|
2283
|
+
}
|
|
2135
2284
|
}
|
|
2136
2285
|
|
|
2137
2286
|
// Full rebuild: cold import, file shrank/rotated, or the append guard refused. Load the base
|
|
@@ -2173,6 +2322,11 @@ async function importSessionFile(filePath, projectPath, projectEntry, options =
|
|
|
2173
2322
|
// but an empty new parse must not wipe an existing rename.
|
|
2174
2323
|
const mergedRename = signals.renameName || parsedRename || parsed.renameName || (existing && existing.rename_name) || '';
|
|
2175
2324
|
|
|
2325
|
+
// This is the expensive full-rebuild path (cold import, file shrank/rotated, or the append
|
|
2326
|
+
// guard refused a non-clean-prefix). Stamp the rebuild clock so event-driven mode keeps the
|
|
2327
|
+
// long debounce floor for this session until a cheap append succeeds again (no rebuild thrash).
|
|
2328
|
+
try { _lastConversationFullRebuildAt.set(parsed.sessionId, Date.now()); } catch {}
|
|
2329
|
+
|
|
2176
2330
|
db.importSessionConversation({
|
|
2177
2331
|
session_id: parsed.sessionId,
|
|
2178
2332
|
project_path: parsed.cwd || parsed.project,
|
|
@@ -2192,6 +2346,8 @@ async function importSessionFile(filePath, projectPath, projectEntry, options =
|
|
|
2192
2346
|
model_provider: parsed.modelProvider || (existing && existing.model_provider) || CLAUDE_MODEL_PROVIDER,
|
|
2193
2347
|
model_id: parsed.modelId || (existing && existing.model_id) || '',
|
|
2194
2348
|
import_parser_version: DEFAULT_CONVERSATION_IMPORT_PARSER_VERSION,
|
|
2349
|
+
// Chunked-import route (cold/large): defer rows to content-rows-backfill, keep the real blob.
|
|
2350
|
+
..._chunkedImportFlags(parsed.sessionId, allMessages, parsed.fileSize, 'full-rebuild'),
|
|
2195
2351
|
});
|
|
2196
2352
|
return true;
|
|
2197
2353
|
}
|
|
@@ -2209,19 +2365,27 @@ async function handleImportConversations(req, res) {
|
|
|
2209
2365
|
// Incremental auto-import: only processes files modified since last scan
|
|
2210
2366
|
// Uses async filesystem operations to avoid blocking the event loop
|
|
2211
2367
|
// (critical when ~/.claude/projects is on Dropbox/network-synced FS)
|
|
2212
|
-
async function runIncrementalConversationImport() {
|
|
2368
|
+
async function runIncrementalConversationImport(opts = {}) {
|
|
2369
|
+
const _now = typeof opts.now === 'function' ? opts.now : () => Date.now();
|
|
2213
2370
|
// Capture scanStart BEFORE anything else — we advance lastScanAt even on
|
|
2214
2371
|
// partial failure so a single bad file doesn't stall the whole job forever.
|
|
2215
2372
|
// Previously, a top-level failure kept lastScanAt frozen, causing repeated
|
|
2216
2373
|
// full rescans + repeated failures (stall bug observed in production).
|
|
2217
|
-
const scanStart =
|
|
2374
|
+
const scanStart = _now();
|
|
2218
2375
|
let imported = 0;
|
|
2219
2376
|
let scanned = 0;
|
|
2220
2377
|
let total = 0;
|
|
2221
2378
|
let failed = 0;
|
|
2222
2379
|
let hitLimit = false;
|
|
2223
2380
|
let hitLimitLabel = '';
|
|
2224
|
-
|
|
2381
|
+
// Wall-clock slice budget: a single giant/cold full-rebuild can overrun the COUNT cap on TIME, so
|
|
2382
|
+
// also stop between files once the budget is spent. 0/absent = no time cap (legacy behavior).
|
|
2383
|
+
const budgetMs = Math.max(0, Number(opts.budgetMs ?? process.env.CTM_CONVERSATION_IMPORT_BUDGET_MS ?? 0));
|
|
2384
|
+
const deadline = budgetMs > 0 ? scanStart + budgetMs : Infinity;
|
|
2385
|
+
// Smaller cap (4) applies only when a slice budget is active (coop driver re-wakes immediately on
|
|
2386
|
+
// hasMore, so smaller slices drain just as fast while keeping each worker op short). Legacy path
|
|
2387
|
+
// (no budget) keeps the original default of 12 so flag-OFF import drain is unchanged.
|
|
2388
|
+
const maxImportedPerRun = Math.max(1, Number(process.env.CTM_CONVERSATION_IMPORT_MAX_PER_RUN || (budgetMs > 0 ? 4 : 12)));
|
|
2225
2389
|
const maxProcessedPerRun = Math.max(1, Number(process.env.CTM_CONVERSATION_IMPORT_MAX_PROCESSED_PER_RUN || maxImportedPerRun));
|
|
2226
2390
|
const retryAfterMsRaw = Number(process.env.CTM_CONVERSATION_IMPORT_RETRY_AFTER_MS || CONVERSATION_IMPORT_RETRY_AFTER_MS);
|
|
2227
2391
|
const retryAfterMs = Number.isFinite(retryAfterMsRaw) && retryAfterMsRaw >= 1000
|
|
@@ -2262,6 +2426,11 @@ async function runIncrementalConversationImport() {
|
|
|
2262
2426
|
hitLimitLabel = importLimited ? `${maxImportedPerRun} imports` : `${maxProcessedPerRun} files`;
|
|
2263
2427
|
break;
|
|
2264
2428
|
}
|
|
2429
|
+
if (_now() >= deadline && scanned < candidates.length) {
|
|
2430
|
+
hitLimit = true;
|
|
2431
|
+
hitLimitLabel = `${budgetMs}ms budget`;
|
|
2432
|
+
break;
|
|
2433
|
+
}
|
|
2265
2434
|
await new Promise((resolve) => setImmediate(resolve));
|
|
2266
2435
|
} catch (e) {
|
|
2267
2436
|
failed++;
|
|
@@ -2271,9 +2440,11 @@ async function runIncrementalConversationImport() {
|
|
|
2271
2440
|
}
|
|
2272
2441
|
}
|
|
2273
2442
|
|
|
2443
|
+
const _rt = _readResetChunkedRouteStats();
|
|
2274
2444
|
if (imported > 0 || failed > 0) {
|
|
2275
2445
|
const suffix = hitLimit ? `, paused after ${hitLimitLabel}` : '';
|
|
2276
|
-
|
|
2446
|
+
const route = (_rt.chunked || _rt.inline) ? ` route=chunked:${_rt.chunked}/inline:${_rt.inline}` : '';
|
|
2447
|
+
console.log(`[auto-import] Imported ${imported}, failed ${failed} (scanned ${scanned}/${total}${suffix})${route}`);
|
|
2277
2448
|
}
|
|
2278
2449
|
return {
|
|
2279
2450
|
imported,
|
|
@@ -2282,11 +2453,12 @@ async function runIncrementalConversationImport() {
|
|
|
2282
2453
|
candidates: candidates.length,
|
|
2283
2454
|
failed,
|
|
2284
2455
|
remaining: hitLimit,
|
|
2456
|
+
hasMore: hitLimit,
|
|
2285
2457
|
retry_after_ms: hitLimit ? retryAfterMs : undefined,
|
|
2286
2458
|
};
|
|
2287
2459
|
} catch (e) {
|
|
2288
2460
|
console.error('[auto-import] Top-level error:', e.message);
|
|
2289
|
-
return { imported, scanned, total, failed, error: e.message };
|
|
2461
|
+
return { imported, scanned, total, failed, error: e.message, hasMore: false };
|
|
2290
2462
|
} finally {
|
|
2291
2463
|
// Always advance the timestamp — even on error — so we don't re-scan the
|
|
2292
2464
|
// same problematic files forever on every tick.
|
|
@@ -2379,6 +2551,8 @@ async function runCursorConversationImport({ cursorHome } = {}) {
|
|
|
2379
2551
|
hostname: '',
|
|
2380
2552
|
import_parser_version: CURSOR_IMPORT_PARSER_VERSION,
|
|
2381
2553
|
rename_name: '',
|
|
2554
|
+
// Chunked-import route (cold/large cursor session): defer rows to content-rows-backfill.
|
|
2555
|
+
..._chunkedImportFlags(ctmId, fields.messages, 0, 'cursor'),
|
|
2382
2556
|
});
|
|
2383
2557
|
if (sig) _cursorImportSignatures.set(ctmId, sig);
|
|
2384
2558
|
imported += 1;
|
|
@@ -5149,4 +5323,4 @@ function safeParse(json, fallback) {
|
|
|
5149
5323
|
try { return JSON.parse(json); } catch { return fallback; }
|
|
5150
5324
|
}
|
|
5151
5325
|
|
|
5152
|
-
module.exports = { handlePromptApi, queueEngine, runIncrementalConversationImport, runCursorConversationImport, importSessionFile, setUiPrefsBroadcaster, setPromptExecutionsOffThread, setDbMaintenanceRunner, setImageSaveRunner, ensureHotkeyDaemon, hotkeyEnsureAction, screenshotResponsibleContext, screenshotNodeCandidates, probeScreenshotNodeGranted, _ingestPathFromInput, _ingestSourceAllowed, _conversationImportCandidates, _ingestTranscriptStoreForParsedFile, _learnSignatureRules, _appendBlocklistExceptions, _finalizePermIntent, _denyHinted, _lastConversationImportAt };
|
|
5326
|
+
module.exports = { handlePromptApi, handleConvertDesktopSession, handleListDesktopForks, queueEngine, runIncrementalConversationImport, runCursorConversationImport, importSessionFile, _chunkedImportFlags, setUiPrefsBroadcaster, setPromptExecutionsOffThread, setDbMaintenanceRunner, setImageSaveRunner, ensureHotkeyDaemon, hotkeyEnsureAction, screenshotResponsibleContext, screenshotNodeCandidates, probeScreenshotNodeGranted, _ingestPathFromInput, _ingestSourceAllowed, _conversationImportCandidates, _ingestTranscriptStoreForParsedFile, _learnSignatureRules, _appendBlocklistExceptions, _finalizePermIntent, _denyHinted, _lastConversationImportAt, _conversationImportFloorMs, _lastConversationFullRebuildAt };
|