@crouton-kit/crouter 0.3.70 → 0.3.79
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 +1 -1
- package/dist/build-root.d.ts +12 -4
- package/dist/build-root.js +25 -6
- package/dist/builtin-memory/00-runtime-base.md +3 -10
- package/dist/builtin-memory/04-base-worker.md +18 -0
- package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
- package/dist/builtin-memory/crouter-development/plugins.md +82 -5
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +13 -34
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/provider-rotation.test.ts +1115 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +234 -71
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/statusline.ts +2 -9
- package/dist/builtin-pi-packages/pi-crtr-extensions/lib/subscription-state.ts +2 -516
- package/dist/builtin-pi-packages/pi-crtr-extensions/package-lock.json +2 -2
- package/dist/builtin-pi-packages/pi-mode-switch/README.md +11 -39
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +20 -15
- package/dist/builtin-pi-packages/pi-mode-switch/package.json +1 -1
- package/dist/builtin-views/canvas/tui.mjs +8 -9
- package/dist/builtin-views/chat/tui.mjs +12 -12
- package/dist/builtin-views/git-pr/tui.mjs +7 -8
- package/dist/builtin-views/inbox/tui.mjs +27 -41
- package/dist/builtin-views/linkedin/tui.mjs +23 -37
- package/dist/builtin-views/prompt-review/tui.mjs +11 -11
- package/dist/builtin-views/settings/tui.mjs +11 -11
- package/dist/builtin-views/workspace-sidebar/tui.mjs +8 -9
- package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +11 -6
- package/dist/clients/attach/__tests__/attach-keybindings.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/attach-keybindings.test.js +113 -0
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +1 -1
- package/dist/clients/attach/__tests__/mermaid-render.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/mermaid-render.test.js +28 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.d.ts +1 -0
- package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +110 -0
- package/dist/clients/attach/attach-cmd.d.ts +9 -1
- package/dist/clients/attach/attach-cmd.js +841 -792
- package/dist/clients/attach/auth-pickers.d.ts +0 -12
- package/dist/clients/attach/auth-pickers.js +64 -15
- package/dist/clients/attach/canvas-panels.js +2 -3
- package/dist/clients/attach/chat-view.d.ts +7 -8
- package/dist/clients/attach/chat-view.js +119 -79
- package/dist/clients/attach/edit-diff-render.d.ts +6 -0
- package/dist/clients/attach/edit-diff-render.js +263 -0
- package/dist/clients/attach/graph-overlay.d.ts +12 -2
- package/dist/clients/attach/graph-overlay.js +83 -33
- package/dist/clients/attach/input-controller.d.ts +28 -2
- package/dist/clients/attach/input-controller.js +38 -5
- package/dist/clients/attach/mermaid-render.js +5 -2
- package/dist/clients/attach/pickers.d.ts +8 -7
- package/dist/clients/attach/pickers.js +11 -17
- package/dist/clients/attach/slash-commands.d.ts +9 -0
- package/dist/clients/attach/slash-commands.js +127 -7
- package/dist/clients/attach/titled-editor.d.ts +12 -1
- package/dist/clients/attach/titled-editor.js +103 -8
- package/dist/commands/canvas-browse.js +2 -2
- package/dist/commands/human/queue.js +3 -4
- package/dist/commands/memory/lint.js +39 -5
- package/dist/commands/memory/write.js +1 -0
- package/dist/commands/node.js +9 -2
- package/dist/commands/pkg/plugin-inspect.js +22 -1
- package/dist/commands/pkg/plugin-manage.js +31 -9
- package/dist/commands/surface-tmux-spread.js +1 -3
- package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-keybindings.test.js +55 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.d.ts +1 -0
- package/dist/commands/sys/__tests__/config-model-ladders.test.js +121 -0
- package/dist/commands/sys/__tests__/setup-core.test.js +158 -1
- package/dist/commands/sys/config.js +18 -21
- package/dist/commands/sys/doctor.js +42 -4
- package/dist/commands/sys/setup-core.d.ts +49 -1
- package/dist/commands/sys/setup-core.js +161 -4
- package/dist/commands/sys/setup.d.ts +88 -0
- package/dist/commands/sys/setup.js +943 -169
- package/dist/commands/view-pick.d.ts +4 -0
- package/dist/commands/view-pick.js +17 -7
- package/dist/core/__tests__/base-worker-prompt.test.d.ts +1 -0
- package/dist/core/__tests__/base-worker-prompt.test.js +24 -0
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +232 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +34 -9
- package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins-surfaces.test.js +298 -0
- package/dist/core/__tests__/command-plugins.test.d.ts +1 -0
- package/dist/core/__tests__/command-plugins.test.js +444 -0
- package/dist/core/__tests__/fault-classifier.test.js +15 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +6 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +9 -1
- package/dist/core/__tests__/full/broker-dialogs.test.js +7 -2
- package/dist/core/__tests__/host-teardown-process-group.test.js +15 -4
- package/dist/core/__tests__/preview-registry-sync.test.js +30 -1
- package/dist/core/__tests__/scope-crouter-home-fence.test.d.ts +1 -0
- package/dist/core/__tests__/scope-crouter-home-fence.test.js +55 -0
- package/dist/core/__tests__/stream-watchdog.test.d.ts +1 -0
- package/dist/core/__tests__/stream-watchdog.test.js +70 -0
- package/dist/core/__tests__/tmux-surface.test.js +72 -0
- package/dist/core/canvas/browse/__tests__/model.test.js +23 -5
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.d.ts +6 -0
- package/dist/core/canvas/browse/app.js +230 -41
- package/dist/core/canvas/browse/model.d.ts +10 -6
- package/dist/core/canvas/browse/model.js +20 -12
- package/dist/core/canvas/browse/pins.d.ts +4 -0
- package/dist/core/canvas/browse/pins.js +29 -0
- package/dist/core/canvas/browse/render.d.ts +13 -1
- package/dist/core/canvas/browse/render.js +72 -15
- package/dist/core/canvas/nav-model.js +5 -11
- package/dist/core/canvas/paths.d.ts +5 -0
- package/dist/core/canvas/paths.js +7 -0
- package/dist/core/command-plugins/adapter.d.ts +15 -0
- package/dist/core/command-plugins/adapter.js +145 -0
- package/dist/core/command-plugins/compose.d.ts +5 -0
- package/dist/core/command-plugins/compose.js +56 -0
- package/dist/core/command-plugins/discovery.d.ts +104 -0
- package/dist/core/command-plugins/discovery.js +565 -0
- package/dist/core/config.d.ts +48 -0
- package/dist/core/config.js +228 -110
- package/dist/core/fault-classifier.js +1 -1
- package/dist/core/fs-utils.d.ts +1 -0
- package/dist/core/fs-utils.js +15 -1
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/bespoke-consumers.test.js +40 -0
- package/dist/core/keybindings/__tests__/resolve.test.d.ts +1 -0
- package/dist/core/keybindings/__tests__/resolve.test.js +224 -0
- package/dist/core/keybindings/catalog.d.ts +14 -0
- package/dist/core/keybindings/catalog.js +257 -0
- package/dist/core/keybindings/index.d.ts +5 -0
- package/dist/core/keybindings/index.js +4 -0
- package/dist/core/keybindings/match.d.ts +29 -0
- package/dist/core/keybindings/match.js +99 -0
- package/dist/core/keybindings/persistence.d.ts +14 -0
- package/dist/core/keybindings/persistence.js +31 -0
- package/dist/core/keybindings/resolve.d.ts +14 -0
- package/dist/core/keybindings/resolve.js +305 -0
- package/dist/core/keybindings/types.d.ts +37 -0
- package/dist/core/keybindings/types.js +1 -0
- package/dist/core/memory-resolver.d.ts +1 -1
- package/dist/core/memory-resolver.js +10 -3
- package/dist/core/predicate.d.ts +5 -3
- package/dist/core/predicate.js +5 -3
- package/dist/core/profiles/select.d.ts +6 -0
- package/dist/core/profiles/select.js +86 -52
- package/dist/core/provider-management.d.ts +12 -0
- package/dist/core/provider-management.js +24 -0
- package/dist/core/runtime/broker-protocol.d.ts +33 -5
- package/dist/core/runtime/broker.js +164 -14
- package/dist/core/runtime/launch.d.ts +39 -6
- package/dist/core/runtime/launch.js +78 -20
- package/dist/core/runtime/naming.js +3 -3
- package/dist/core/runtime/pi-cli.d.ts +6 -0
- package/dist/core/runtime/pi-cli.js +16 -2
- package/dist/core/runtime/pi-vendored.d.ts +8 -0
- package/dist/core/runtime/pi-vendored.js +14 -0
- package/dist/core/runtime/placement.d.ts +2 -2
- package/dist/core/runtime/placement.js +4 -1
- package/dist/core/runtime/promote.js +4 -0
- package/dist/core/runtime/recap.d.ts +3 -3
- package/dist/core/runtime/recap.js +75 -47
- package/dist/core/runtime/recycle.js +6 -1
- package/dist/core/runtime/reset.js +5 -0
- package/dist/core/runtime/session-list-cache.d.ts +33 -0
- package/dist/core/runtime/session-list-cache.js +338 -0
- package/dist/core/runtime/spawn.js +52 -45
- package/dist/core/runtime/stream-watchdog.d.ts +26 -0
- package/dist/core/runtime/stream-watchdog.js +75 -0
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +2 -2
- package/dist/core/runtime/tmux.d.ts +18 -15
- package/dist/core/runtime/tmux.js +217 -110
- package/dist/core/scope.js +27 -4
- package/dist/core/subscription-state.d.ts +90 -0
- package/dist/core/subscription-state.js +762 -0
- package/dist/core/tui/__tests__/host-keybindings.test.d.ts +1 -0
- package/dist/core/tui/__tests__/host-keybindings.test.js +112 -0
- package/dist/core/tui/host.d.ts +30 -1
- package/dist/core/tui/host.js +148 -34
- package/dist/core/view/contract.d.ts +17 -3
- package/dist/daemon/crtrd.js +253 -12
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +41 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +121 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +1 -1
- package/dist/pi-extensions/canvas-inbox-watcher.js +203 -25
- package/dist/pi-extensions/canvas-recap.d.ts +4 -0
- package/dist/pi-extensions/canvas-recap.js +73 -46
- package/dist/pi-extensions/canvas-stophook.d.ts +11 -0
- package/dist/pi-extensions/canvas-stophook.js +33 -4
- package/dist/types.d.ts +9 -26
- package/dist/types.js +1 -28
- package/dist/web-client/assets/{index-DiFuLcp6.js → index--md2ylfi.js} +1 -1
- package/dist/web-client/index.html +1 -1
- package/dist/web-client/sw.js +1 -1
- package/package.json +8 -4
- package/dist/pi-extensions/canvas-nav.d.ts +0 -43
- package/dist/pi-extensions/canvas-nav.js +0 -640
- package/dist/pi-extensions/widget-order-bus.d.ts +0 -6
- package/dist/pi-extensions/widget-order-bus.js +0 -34
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// Broker-side cache for the `/resume` session picker's `list_sessions` reply.
|
|
2
|
+
//
|
|
3
|
+
// pi's `SessionManager.list()` reads EVERY `.jsonl` in a cwd's session dir in
|
|
4
|
+
// full, line-by-line, to build each session's `allMessagesText` search field.
|
|
5
|
+
// On a large corpus (3.3k files / 1.1 GiB here) that is ~3.6s of CPU-bound
|
|
6
|
+
// JSON.parse on EVERY picker open — pi exposes no per-file entry point and no
|
|
7
|
+
// cache. This module keeps a `(mtimeMs, size)`-keyed cache of the already-wire-
|
|
8
|
+
// shaped, search-text-bounded `WireSessionInfo`: on each open it stats every
|
|
9
|
+
// file (cheap — ~260ms for 3.3k) and rebuilds info ONLY for new/changed files,
|
|
10
|
+
// reusing cached entries for the rest and dropping deleted ones. The cache is
|
|
11
|
+
// persisted to one global JSON file so the first open after a broker restart is
|
|
12
|
+
// also fast.
|
|
13
|
+
//
|
|
14
|
+
// `buildWireSessionInfo` below is a faithful mirror of pi's private
|
|
15
|
+
// `buildSessionInfo` (node_modules/@earendil-works/pi-coding-agent/dist/core/
|
|
16
|
+
// session-manager.js) — it is not exported, so the only way to rebuild a single
|
|
17
|
+
// file's info without re-reading the whole corpus is to reproduce its parse.
|
|
18
|
+
// Keep it in sync with pi's session-file format (CURRENT_SESSION_VERSION).
|
|
19
|
+
import { createReadStream } from 'node:fs';
|
|
20
|
+
import { readFile, readdir, rename, stat, unlink, writeFile } from 'node:fs/promises';
|
|
21
|
+
import { createInterface } from 'node:readline';
|
|
22
|
+
import { dirname, join } from 'node:path';
|
|
23
|
+
/** Search-text bound: `allMessagesText` is searched on every query edit; pi's
|
|
24
|
+
* unbounded value retains gigabytes in the viewer and turns key-repeat into GC
|
|
25
|
+
* jitter. The opening and latest text keep search useful. */
|
|
26
|
+
const SESSION_SEARCH_TEXT_CHARS = 1024;
|
|
27
|
+
export function compactSessionSearchText(text) {
|
|
28
|
+
if (text.length <= SESSION_SEARCH_TEXT_CHARS)
|
|
29
|
+
return text;
|
|
30
|
+
const half = SESSION_SEARCH_TEXT_CHARS / 2;
|
|
31
|
+
return `${text.slice(0, half)} … ${text.slice(-half)}`;
|
|
32
|
+
}
|
|
33
|
+
const MAX_CONCURRENT_INFO_LOADS = 10;
|
|
34
|
+
/** Persisted-file schema version. Bump whenever `CacheEntry`'s shape (or
|
|
35
|
+
* `WireSessionInfo`'s) changes: on a version mismatch the whole file is
|
|
36
|
+
* discarded and rebuilt from disk — no per-entry migration, no lenient
|
|
37
|
+
* recovery (the cache is fully derivable). */
|
|
38
|
+
const CACHE_SCHEMA_VERSION = 1;
|
|
39
|
+
/** The full map is tens of MB (~15 MB for one cwd, ~46 MB for `all`); a
|
|
40
|
+
* synchronous `JSON.stringify` of it must never sit on the reply path. Persists
|
|
41
|
+
* are coalesced onto this trailing timer so rapid opens write at most once per
|
|
42
|
+
* window, off the hot path. */
|
|
43
|
+
const PERSIST_DEBOUNCE_MS = 2000;
|
|
44
|
+
/** Shape-check a loaded entry — a well-formed-JSON file with the right version
|
|
45
|
+
* but a stale entry shape (e.g. missing `info`) would otherwise push undefined
|
|
46
|
+
* into the sort and throw. Any failure discards the whole file (rebuild). */
|
|
47
|
+
function isValidEntry(e) {
|
|
48
|
+
if (typeof e !== 'object' || e === null)
|
|
49
|
+
return false;
|
|
50
|
+
const c = e;
|
|
51
|
+
return (typeof c['mtimeMs'] === 'number' &&
|
|
52
|
+
typeof c['size'] === 'number' &&
|
|
53
|
+
typeof c['info'] === 'object' &&
|
|
54
|
+
c['info'] !== null &&
|
|
55
|
+
typeof c['info']['modified'] === 'string');
|
|
56
|
+
}
|
|
57
|
+
// ---- faithful mirror of pi's private buildSessionInfo -----------------------
|
|
58
|
+
function parseLine(line) {
|
|
59
|
+
if (!line.trim())
|
|
60
|
+
return null;
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(line);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function extractTextContent(message) {
|
|
69
|
+
const content = message['content'];
|
|
70
|
+
if (typeof content === 'string')
|
|
71
|
+
return content;
|
|
72
|
+
if (!Array.isArray(content))
|
|
73
|
+
return '';
|
|
74
|
+
return content
|
|
75
|
+
.filter((b) => b.type === 'text')
|
|
76
|
+
.map((b) => b.text)
|
|
77
|
+
.join(' ');
|
|
78
|
+
}
|
|
79
|
+
function messageActivityTime(entry) {
|
|
80
|
+
const message = entry['message'];
|
|
81
|
+
if (!message || !('content' in message))
|
|
82
|
+
return undefined;
|
|
83
|
+
const role = message['role'];
|
|
84
|
+
if (role !== 'user' && role !== 'assistant')
|
|
85
|
+
return undefined;
|
|
86
|
+
const ts = message['timestamp'];
|
|
87
|
+
if (typeof ts === 'number')
|
|
88
|
+
return ts;
|
|
89
|
+
const t = new Date(entry['timestamp']).getTime();
|
|
90
|
+
return Number.isNaN(t) ? undefined : t;
|
|
91
|
+
}
|
|
92
|
+
/** Build one session's wire info by streaming the file. Returns null for a
|
|
93
|
+
* malformed/headerless file (matches pi, which drops those from the list). */
|
|
94
|
+
async function buildWireSessionInfo(filePath) {
|
|
95
|
+
try {
|
|
96
|
+
const stats = await stat(filePath);
|
|
97
|
+
let header = null;
|
|
98
|
+
let messageCount = 0;
|
|
99
|
+
let firstMessage = '';
|
|
100
|
+
const allMessages = [];
|
|
101
|
+
let name;
|
|
102
|
+
let lastActivityTime;
|
|
103
|
+
const rl = createInterface({
|
|
104
|
+
input: createReadStream(filePath, { encoding: 'utf8' }),
|
|
105
|
+
crlfDelay: Infinity,
|
|
106
|
+
});
|
|
107
|
+
for await (const line of rl) {
|
|
108
|
+
const entry = parseLine(line);
|
|
109
|
+
if (!entry)
|
|
110
|
+
continue;
|
|
111
|
+
if (!header) {
|
|
112
|
+
if (entry['type'] !== 'session')
|
|
113
|
+
return null;
|
|
114
|
+
header = entry;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (entry['type'] === 'session_info') {
|
|
118
|
+
name = entry['name']?.trim() || undefined;
|
|
119
|
+
}
|
|
120
|
+
if (entry['type'] !== 'message')
|
|
121
|
+
continue;
|
|
122
|
+
messageCount++;
|
|
123
|
+
const activityTime = messageActivityTime(entry);
|
|
124
|
+
if (typeof activityTime === 'number') {
|
|
125
|
+
lastActivityTime = Math.max(lastActivityTime ?? 0, activityTime);
|
|
126
|
+
}
|
|
127
|
+
const message = entry['message'];
|
|
128
|
+
if (!message || !('content' in message))
|
|
129
|
+
continue;
|
|
130
|
+
const role = message['role'];
|
|
131
|
+
if (role !== 'user' && role !== 'assistant')
|
|
132
|
+
continue;
|
|
133
|
+
const textContent = extractTextContent(message);
|
|
134
|
+
if (!textContent)
|
|
135
|
+
continue;
|
|
136
|
+
allMessages.push(textContent);
|
|
137
|
+
if (!firstMessage && role === 'user')
|
|
138
|
+
firstMessage = textContent;
|
|
139
|
+
}
|
|
140
|
+
if (!header)
|
|
141
|
+
return null;
|
|
142
|
+
const cwd = typeof header['cwd'] === 'string' ? header['cwd'] : '';
|
|
143
|
+
const headerTime = typeof header['timestamp'] === 'string' ? new Date(header['timestamp']).getTime() : NaN;
|
|
144
|
+
const modified = typeof lastActivityTime === 'number' && lastActivityTime > 0
|
|
145
|
+
? new Date(lastActivityTime)
|
|
146
|
+
: !Number.isNaN(headerTime)
|
|
147
|
+
? new Date(headerTime)
|
|
148
|
+
: stats.mtime;
|
|
149
|
+
return {
|
|
150
|
+
path: filePath,
|
|
151
|
+
id: header['id'],
|
|
152
|
+
cwd,
|
|
153
|
+
name,
|
|
154
|
+
parentSessionPath: header['parentSession'],
|
|
155
|
+
created: new Date(header['timestamp']).toISOString(),
|
|
156
|
+
modified: modified.toISOString(),
|
|
157
|
+
messageCount,
|
|
158
|
+
firstMessage: firstMessage || '(no messages)',
|
|
159
|
+
// Cache the already-bounded wire form (the selector searches this field).
|
|
160
|
+
allMessagesText: compactSessionSearchText(allMessages.join(' ')),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// ---- the cache ---------------------------------------------------------------
|
|
168
|
+
/** In-memory + file-backed cache keyed by absolute `.jsonl` path. One instance
|
|
169
|
+
* per broker; the file is shared across brokers (last-writer-wins via atomic
|
|
170
|
+
* rename — a lost update just costs one file's rebuild next open, never
|
|
171
|
+
* corruption). */
|
|
172
|
+
export class SessionListCache {
|
|
173
|
+
cacheFile;
|
|
174
|
+
cache = new Map();
|
|
175
|
+
loaded = false;
|
|
176
|
+
persisting = false;
|
|
177
|
+
persistAgain = false;
|
|
178
|
+
persistTimer = null;
|
|
179
|
+
constructor(cacheFile) {
|
|
180
|
+
this.cacheFile = cacheFile;
|
|
181
|
+
}
|
|
182
|
+
/** Fire-and-forget warm of the cwd scope at broker start, so the first
|
|
183
|
+
* `/resume` open after this broker launches is already warm. The cold scan
|
|
184
|
+
* streams each file via readline (yields per chunk), so it never blocks the
|
|
185
|
+
* broker loop — pre-warming beats worker threads here. Errors are swallowed
|
|
186
|
+
* by the same paths that guard a normal open. */
|
|
187
|
+
prewarm(sessionDir) {
|
|
188
|
+
void this.listDir(sessionDir);
|
|
189
|
+
}
|
|
190
|
+
/** Sessions for one cwd's session dir (the picker's default `cwd` scope). */
|
|
191
|
+
async listDir(sessionDir) {
|
|
192
|
+
return this.build(await jsonlFiles(sessionDir), new Set([sessionDir]));
|
|
193
|
+
}
|
|
194
|
+
/** Sessions across every cwd's dir under the sessions root (`all` scope). */
|
|
195
|
+
async listAll(sessionsRoot) {
|
|
196
|
+
let dirEntries;
|
|
197
|
+
try {
|
|
198
|
+
dirEntries = (await readdir(sessionsRoot, { withFileTypes: true }))
|
|
199
|
+
.filter((e) => e.isDirectory())
|
|
200
|
+
.map((e) => join(sessionsRoot, e.name));
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
const files = [];
|
|
206
|
+
for (const dir of dirEntries)
|
|
207
|
+
files.push(...(await jsonlFiles(dir)));
|
|
208
|
+
return this.build(files, new Set(dirEntries));
|
|
209
|
+
}
|
|
210
|
+
async build(files, scannedDirs) {
|
|
211
|
+
await this.ensureLoaded();
|
|
212
|
+
const present = new Set(files);
|
|
213
|
+
let changed = false;
|
|
214
|
+
// Prune ONLY keys under the dirs we actually scanned. A cwd-scope open must
|
|
215
|
+
// not evict other cwds' cached entries: they weren't rescanned, so their
|
|
216
|
+
// absence from `files` says nothing about whether they still exist on disk.
|
|
217
|
+
// (Pruning the whole map here would self-destruct the global/`all` cache on
|
|
218
|
+
// every cwd open.)
|
|
219
|
+
for (const key of this.cache.keys()) {
|
|
220
|
+
if (!present.has(key) && scannedDirs.has(dirname(key))) {
|
|
221
|
+
this.cache.delete(key);
|
|
222
|
+
changed = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Positioned by file index so equal-`modified` ties keep readdir order
|
|
226
|
+
// deterministically (workers finish out of order); a stable sort preserves it.
|
|
227
|
+
const infos = new Array(files.length);
|
|
228
|
+
let next = 0;
|
|
229
|
+
const worker = async () => {
|
|
230
|
+
for (;;) {
|
|
231
|
+
const i = next++;
|
|
232
|
+
if (i >= files.length)
|
|
233
|
+
return;
|
|
234
|
+
const file = files[i];
|
|
235
|
+
if (file === undefined)
|
|
236
|
+
return;
|
|
237
|
+
const st = await stat(file).catch(() => null);
|
|
238
|
+
if (!st) {
|
|
239
|
+
if (this.cache.delete(file))
|
|
240
|
+
changed = true;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const cached = this.cache.get(file);
|
|
244
|
+
if (cached && cached.mtimeMs === st.mtimeMs && cached.size === st.size) {
|
|
245
|
+
infos[i] = cached.info;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const info = await buildWireSessionInfo(file);
|
|
249
|
+
changed = true;
|
|
250
|
+
if (!info) {
|
|
251
|
+
this.cache.delete(file);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
this.cache.set(file, { mtimeMs: st.mtimeMs, size: st.size, info });
|
|
255
|
+
infos[i] = info;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
await Promise.all(Array.from({ length: Math.min(MAX_CONCURRENT_INFO_LOADS, files.length) }, () => worker()));
|
|
259
|
+
if (changed)
|
|
260
|
+
this.schedulePersist();
|
|
261
|
+
const result = infos.filter((x) => x !== undefined);
|
|
262
|
+
result.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
async ensureLoaded() {
|
|
266
|
+
if (this.loaded)
|
|
267
|
+
return;
|
|
268
|
+
this.loaded = true;
|
|
269
|
+
try {
|
|
270
|
+
const raw = JSON.parse(await readFile(this.cacheFile, 'utf8'));
|
|
271
|
+
// Version mismatch or shape drift → discard and rebuild. No lenient
|
|
272
|
+
// multi-path recovery: the cache is derivable, so a bad file is just
|
|
273
|
+
// overwritten on the next persist.
|
|
274
|
+
if (raw?.version !== CACHE_SCHEMA_VERSION || typeof raw.entries !== 'object')
|
|
275
|
+
return;
|
|
276
|
+
for (const [path, entry] of Object.entries(raw.entries)) {
|
|
277
|
+
if (!isValidEntry(entry)) {
|
|
278
|
+
this.cache.clear();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
this.cache.set(path, entry);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// Missing or corrupt cache file → start empty and rebuild.
|
|
286
|
+
this.cache.clear();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** Coalesce persists onto a single trailing timer so the tens-of-MB
|
|
290
|
+
* `JSON.stringify` never runs on the reply path and rapid opens write once. */
|
|
291
|
+
schedulePersist() {
|
|
292
|
+
if (this.persistTimer)
|
|
293
|
+
return;
|
|
294
|
+
this.persistTimer = setTimeout(() => {
|
|
295
|
+
this.persistTimer = null;
|
|
296
|
+
void this.persist();
|
|
297
|
+
}, PERSIST_DEBOUNCE_MS);
|
|
298
|
+
this.persistTimer.unref?.();
|
|
299
|
+
}
|
|
300
|
+
/** Atomic write (temp + rename) so a concurrent reader never sees a partial
|
|
301
|
+
* file. Coalesces overlapping writes into one trailing flush. */
|
|
302
|
+
async persist() {
|
|
303
|
+
if (this.persisting) {
|
|
304
|
+
this.persistAgain = true;
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
this.persisting = true;
|
|
308
|
+
const tmp = `${this.cacheFile}.${process.pid}.tmp`;
|
|
309
|
+
try {
|
|
310
|
+
const entries = {};
|
|
311
|
+
for (const [path, entry] of this.cache)
|
|
312
|
+
entries[path] = entry;
|
|
313
|
+
const payload = { version: CACHE_SCHEMA_VERSION, entries };
|
|
314
|
+
await writeFile(tmp, JSON.stringify(payload));
|
|
315
|
+
await rename(tmp, this.cacheFile);
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
// Best-effort: a failed persist only costs a rebuild next open. Clean up
|
|
319
|
+
// the temp file so a failed write never leaks it.
|
|
320
|
+
await unlink(tmp).catch(() => { });
|
|
321
|
+
}
|
|
322
|
+
finally {
|
|
323
|
+
this.persisting = false;
|
|
324
|
+
if (this.persistAgain) {
|
|
325
|
+
this.persistAgain = false;
|
|
326
|
+
void this.persist();
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async function jsonlFiles(dir) {
|
|
332
|
+
try {
|
|
333
|
+
return (await readdir(dir)).filter((f) => f.endsWith('.jsonl')).map((f) => join(dir, f));
|
|
334
|
+
}
|
|
335
|
+
catch {
|
|
336
|
+
return [];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
import { spawn } from 'node:child_process';
|
|
13
13
|
import { readdirSync, existsSync } from 'node:fs';
|
|
14
14
|
import { isAbsolute, resolve, join } from 'node:path';
|
|
15
|
-
import { homedir } from 'node:os';
|
|
16
15
|
import { spawnNode, currentNodeContext, rootOfSpine, newNodeId } from './nodes.js';
|
|
17
16
|
import { loadProfileManifest } from '../profiles/manifest.js';
|
|
18
17
|
import { selectProfileForCwd } from '../profiles/select.js';
|
|
@@ -24,10 +23,11 @@ import { writeGoal } from './kickoff.js';
|
|
|
24
23
|
import { appendSituationalContext } from './situational-context.js';
|
|
25
24
|
import { hasRoadmap, seedRoadmap } from './roadmap.js';
|
|
26
25
|
import { buildIdentityAssertion, buildWakeBearings } from './bearings.js';
|
|
27
|
-
import {
|
|
26
|
+
import { installTmuxBindings } from './tmux-chrome.js';
|
|
28
27
|
import { getNode, fullName, recordPid } from '../canvas/index.js';
|
|
29
28
|
import { registerViewerFocus, openViewerWindow, waitForBrokerViewSocket, viewerSplitEnv, windowOfPane, currentTmux, inTmux, focusWindow, } from './placement.js';
|
|
30
29
|
import { transition } from './lifecycle.js';
|
|
30
|
+
import { piSessionsRoot } from './pi-vendored.js';
|
|
31
31
|
import { headlessBrokerHost } from './host.js';
|
|
32
32
|
import { ensureDaemon } from '../../daemon/manage.js';
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
@@ -72,9 +72,13 @@ export async function bootRoot(opts) {
|
|
|
72
72
|
// covering cwd > a synchronous create-or-root-profile prompt (the stable root
|
|
73
73
|
// profile, headless).
|
|
74
74
|
const profileId = await selectProfileForCwd(opts.cwd, opts.profile, opts.pickProfile ?? false);
|
|
75
|
+
// Immediate feedback: the broker + viewer boot take ~1s and the profile
|
|
76
|
+
// selector has just released the terminal — a silent cursor reads as a hang.
|
|
77
|
+
if (process.stdout.isTTY)
|
|
78
|
+
process.stdout.write('\u001b[2m › starting…\u001b[0m\r\n');
|
|
75
79
|
// A born-resident root starts in base mode; it earns the orchestrator persona
|
|
76
80
|
// the first time it delegates (or on promotion). Resident lifecycle either way.
|
|
77
|
-
const { launch } = buildLaunchSpec(kind, 'base', { lifecycle: 'resident', hasManager: false });
|
|
81
|
+
const { launch } = buildLaunchSpec(kind, 'base', { lifecycle: 'resident', hasManager: false, cwd: opts.cwd, profileId });
|
|
78
82
|
// Born WITHOUT a name. Naming is async + event-driven: the canvas-goal-capture
|
|
79
83
|
// extension names the node from its FIRST real message (the kickoff prompt or
|
|
80
84
|
// a human's first line) inside its own pi process, via a headless `pi -p`.
|
|
@@ -93,18 +97,9 @@ export async function bootRoot(opts) {
|
|
|
93
97
|
// mandate (bare `crtr` has none — writeGoal no-ops on empty).
|
|
94
98
|
if (opts.prompt !== undefined)
|
|
95
99
|
writeGoal(meta.node_id, opts.prompt);
|
|
96
|
-
//
|
|
97
|
-
// view-nav chord live on this server (idempotent; guaranteed in tmux above).
|
|
100
|
+
// Reconcile the crouter-owned tmux bindings on this server.
|
|
98
101
|
try {
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
catch { /* best-effort */ }
|
|
102
|
-
try {
|
|
103
|
-
installNavBindings();
|
|
104
|
-
}
|
|
105
|
-
catch { /* best-effort */ }
|
|
106
|
-
try {
|
|
107
|
-
installViewNavBindings();
|
|
102
|
+
installTmuxBindings();
|
|
108
103
|
}
|
|
109
104
|
catch { /* best-effort */ }
|
|
110
105
|
// This terminal becomes the root's attach VIEWER in the user's CURRENT session,
|
|
@@ -140,19 +135,14 @@ export async function bootRoot(opts) {
|
|
|
140
135
|
throw new Error(`failed to launch the broker engine for the front-door root ${meta.node_id} — nothing to attach to.`);
|
|
141
136
|
}
|
|
142
137
|
recordPid(meta.node_id, placed.pid);
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
/* best-effort cleanup */
|
|
152
|
-
}
|
|
153
|
-
throw new Error(`the front-door root broker ${meta.node_id} never bound its view socket — cannot attach.`);
|
|
154
|
-
}
|
|
155
|
-
const attachEnv = { ...process.env, ...viewerSplitEnv() };
|
|
138
|
+
// Spawn the attach viewer NOW, in parallel with the broker's boot: the
|
|
139
|
+
// viewer's own `crtr` cold start (~0.4s of module load) then overlaps the
|
|
140
|
+
// broker's ~1s view-socket bind instead of serializing after it — this was
|
|
141
|
+
// the bulk of the post-profile-pick dead air. CRTR_ATTACH_WAIT_MS tells the
|
|
142
|
+
// viewer to poll view.sock until it accepts (preWaitForViewSocket in
|
|
143
|
+
// attach-cmd.ts) instead of failing fast on the not-yet-bound socket; the
|
|
144
|
+
// deadline mirrors placement's BROKER_FOCUS_SOCKET_WAIT_MS.
|
|
145
|
+
const attachEnv = { ...process.env, ...viewerSplitEnv(), CRTR_ATTACH_WAIT_MS: '30000' };
|
|
156
146
|
// Async, NOT spawnSync: a synchronous spawn would park THIS process's own
|
|
157
147
|
// event loop for the whole attach session (which legitimately runs for
|
|
158
148
|
// hours) — starving it of the SIGCHLD delivery it needs to reap its OWN
|
|
@@ -165,23 +155,36 @@ export async function bootRoot(opts) {
|
|
|
165
155
|
// reap it in real time; `stdio: 'inherit'` still hands the child the TTY
|
|
166
156
|
// directly, so this is a behavior-neutral swap for the foreground session.
|
|
167
157
|
const attachChild = spawn('crtr', ['surface', 'attach', 'to', meta.node_id], { stdio: 'inherit', env: attachEnv });
|
|
168
|
-
const
|
|
158
|
+
const attachExit = new Promise((resolveCode) => {
|
|
169
159
|
attachChild.once('exit', (exitCode) => resolveCode(exitCode ?? 0));
|
|
170
160
|
attachChild.once('error', () => resolveCode(1));
|
|
171
161
|
});
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
162
|
+
// The root is useless without its engine, so a socket timeout IS fatal here
|
|
163
|
+
// (unlike a background child's optional viewer, which is skipped on timeout):
|
|
164
|
+
// await readiness concurrently with the viewer's own pre-wait, mark the node
|
|
165
|
+
// crashed and reap the polling viewer if the broker never binds. Raced
|
|
166
|
+
// against the viewer exiting on its own first (e.g. the user quits during
|
|
167
|
+
// boot), which simply ends the session like any other detach.
|
|
168
|
+
const outcome = await Promise.race([
|
|
169
|
+
waitForBrokerViewSocket(meta.node_id, placed.exited).then((ok) => (ok ? 'ready' : 'never')),
|
|
170
|
+
attachExit.then(() => 'viewer-exit'),
|
|
171
|
+
]);
|
|
172
|
+
if (outcome === 'never') {
|
|
173
|
+
try {
|
|
174
|
+
attachChild.kill();
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
/* best-effort */
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
transition(meta.node_id, 'crash');
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
/* best-effort cleanup */
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`the front-door root broker ${meta.node_id} never bound its view socket — cannot attach.`);
|
|
186
|
+
}
|
|
187
|
+
process.exit(await attachExit);
|
|
185
188
|
}
|
|
186
189
|
/** Resolve a bare/partial pi session uuid to its ABSOLUTE `.jsonl` path by
|
|
187
190
|
* scanning pi's sessions store the way pi's own CLI does (exact id, else a
|
|
@@ -302,10 +305,6 @@ export async function spawnChild(opts) {
|
|
|
302
305
|
// reaps when done. A child born as an orchestrator is terminal/orchestrator
|
|
303
306
|
// (delegates + holds a roadmap, but still reports up), NOT resident.
|
|
304
307
|
const lifecycle = root ? 'resident' : 'terminal';
|
|
305
|
-
// Spine: a managed child reports up to its spawner (has a manager); an
|
|
306
|
-
// independent root sits top-of-spine with nobody to push to. Mirrors the
|
|
307
|
-
// `parent` set below (root ? null : spawner), so hasManager === parent!==null.
|
|
308
|
-
const { launch } = buildLaunchSpec(opts.kind, mode, { lifecycle, hasManager: !root, model: opts.model });
|
|
309
308
|
// Born WITHOUT a name — the canvas-goal-capture extension names it async from
|
|
310
309
|
// its first message (the kickoff task) inside its own pi process, so spawn
|
|
311
310
|
// never blocks on the LLM naming round-trip (the 2-3s freeze it used to cost).
|
|
@@ -326,6 +325,14 @@ export async function spawnChild(opts) {
|
|
|
326
325
|
managedWorktree = createManagedWorktree(opts.cwd, nodeId);
|
|
327
326
|
spawnCwd = managedWorktree.path;
|
|
328
327
|
}
|
|
328
|
+
// Spine: a managed child reports up to its spawner (has a manager); an
|
|
329
|
+
// independent root sits top-of-spine with nobody to push to. Mirrors the
|
|
330
|
+
// `parent` set below (root ? null : spawner), so hasManager === parent!==null.
|
|
331
|
+
// Built HERE — after profile + worktree cwd are resolved — so the kind's
|
|
332
|
+
// model/tools resolve against the CHILD's own profile + project stack, not
|
|
333
|
+
// this process's ambient scope (a front-door shell has no CRTR_PROFILE_ID,
|
|
334
|
+
// which silently hid every profile-scope kind override).
|
|
335
|
+
const { launch } = buildLaunchSpec(opts.kind, mode, { lifecycle, hasManager: !root, model: opts.model, cwd: spawnCwd, profileId });
|
|
329
336
|
const meta = spawnNode({
|
|
330
337
|
kind: opts.kind,
|
|
331
338
|
mode,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare function resolveStreamWatchdogMs(): number;
|
|
2
|
+
/**
|
|
3
|
+
* A single-timer dead-man's switch: `poke(token)` (re)arms it, `clear()`
|
|
4
|
+
* disarms it, and `onStall(token)` fires exactly once if `poke()` isn't called
|
|
5
|
+
* again within `ms`. The broker pokes it on relayed engine events while a turn
|
|
6
|
+
* is awaiting the provider and clears it at `agent_end` / tool execution — see
|
|
7
|
+
* broker.ts `relayEvent`.
|
|
8
|
+
*
|
|
9
|
+
* The `token` is an opaque generation stamp: each arm captures the token passed
|
|
10
|
+
* to its `poke()`, and `onStall` receives exactly that token. The broker bumps
|
|
11
|
+
* a session generation on every rebind, so a timer armed against a since-
|
|
12
|
+
* superseded session hands its stale generation to `onStall`, which no-ops it
|
|
13
|
+
* instead of aborting the live replacement session.
|
|
14
|
+
*/
|
|
15
|
+
export declare class StreamWatchdog {
|
|
16
|
+
private readonly onStall;
|
|
17
|
+
private timer;
|
|
18
|
+
private readonly ms;
|
|
19
|
+
constructor(onStall: (token: number) => void, ms?: number);
|
|
20
|
+
/** Proof of progress — (re)arm the timer for `token`, canceling any prior
|
|
21
|
+
* countdown. The fired `onStall` receives THIS arm's token. */
|
|
22
|
+
poke(token: number): void;
|
|
23
|
+
/** Turn over (or broker tearing down) — no more relay expected until the
|
|
24
|
+
* next `poke()`. Idempotent. */
|
|
25
|
+
clear(): void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// stream-watchdog.ts — broker-side dead-provider-stream detector.
|
|
2
|
+
//
|
|
3
|
+
// A laptop sleep or Wi-Fi loss can leave the provider's HTTP response stream
|
|
4
|
+
// half-open: the socket never errors and never closes, it just stops
|
|
5
|
+
// delivering bytes. pi's turn loop has no read-timeout on that socket, so the
|
|
6
|
+
// engine wedges in phase='turn' indefinitely — nothing drains (steer/followUp
|
|
7
|
+
// queues included) until the daemon's 20-minute wedge detector SIGTERMs the
|
|
8
|
+
// broker, which is the actual data-loss event a wedge this long causes (see
|
|
9
|
+
// queued-messages-never-delivered-diagnosis.md RC1/RC2). This watchdog aborts
|
|
10
|
+
// the stalled stream itself, well before that backstop: if no engine event has
|
|
11
|
+
// been relayed for STREAM_WATCHDOG_MS while a turn is active, the stream is
|
|
12
|
+
// presumed dead — abort it. The turn ends with stopReason 'aborted' (or the
|
|
13
|
+
// stophook's own error classification if the abort races a real error), the
|
|
14
|
+
// existing connection-fault retry machinery (which arms on agent_end) owns
|
|
15
|
+
// re-driving the turn, and the broker process — and its in-memory queues —
|
|
16
|
+
// survive instead of being killed.
|
|
17
|
+
const DEFAULT_STREAM_WATCHDOG_MS = 5 * 60_000; // 5 minutes.
|
|
18
|
+
// Comfortably under the daemon's 20-minute WEDGE_QUIET_MS hard kick (crtrd.ts),
|
|
19
|
+
// so this is the common-case recovery path and the daemon kick becomes the rare
|
|
20
|
+
// backstop. Generous enough that a live, healthy stream never trips it: a
|
|
21
|
+
// single LLM turn's own network round-trips (message_update deltas,
|
|
22
|
+
// tool_execution_start/update/end) land well inside 5 minutes. The watchdog
|
|
23
|
+
// covers ONLY time awaiting the provider: the broker disarms it while a tool is
|
|
24
|
+
// executing (a legitimately silent tool can run longer than the timeout — see
|
|
25
|
+
// broker.ts relayEvent) and rearms it once the last in-flight tool ends, so a
|
|
26
|
+
// slow-but-live turn is never mistaken for a dead one.
|
|
27
|
+
export function resolveStreamWatchdogMs() {
|
|
28
|
+
const raw = process.env['CRTR_STREAM_WATCHDOG_MS'];
|
|
29
|
+
if (raw === undefined)
|
|
30
|
+
return DEFAULT_STREAM_WATCHDOG_MS;
|
|
31
|
+
const n = Number(raw);
|
|
32
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_STREAM_WATCHDOG_MS;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A single-timer dead-man's switch: `poke(token)` (re)arms it, `clear()`
|
|
36
|
+
* disarms it, and `onStall(token)` fires exactly once if `poke()` isn't called
|
|
37
|
+
* again within `ms`. The broker pokes it on relayed engine events while a turn
|
|
38
|
+
* is awaiting the provider and clears it at `agent_end` / tool execution — see
|
|
39
|
+
* broker.ts `relayEvent`.
|
|
40
|
+
*
|
|
41
|
+
* The `token` is an opaque generation stamp: each arm captures the token passed
|
|
42
|
+
* to its `poke()`, and `onStall` receives exactly that token. The broker bumps
|
|
43
|
+
* a session generation on every rebind, so a timer armed against a since-
|
|
44
|
+
* superseded session hands its stale generation to `onStall`, which no-ops it
|
|
45
|
+
* instead of aborting the live replacement session.
|
|
46
|
+
*/
|
|
47
|
+
export class StreamWatchdog {
|
|
48
|
+
onStall;
|
|
49
|
+
timer;
|
|
50
|
+
ms;
|
|
51
|
+
constructor(onStall, ms) {
|
|
52
|
+
this.onStall = onStall;
|
|
53
|
+
this.ms = ms ?? resolveStreamWatchdogMs();
|
|
54
|
+
}
|
|
55
|
+
/** Proof of progress — (re)arm the timer for `token`, canceling any prior
|
|
56
|
+
* countdown. The fired `onStall` receives THIS arm's token. */
|
|
57
|
+
poke(token) {
|
|
58
|
+
this.clear();
|
|
59
|
+
this.timer = setTimeout(() => {
|
|
60
|
+
this.timer = undefined;
|
|
61
|
+
this.onStall(token);
|
|
62
|
+
}, this.ms);
|
|
63
|
+
// unref() so a live watchdog never keeps the broker process open on its own.
|
|
64
|
+
if (typeof this.timer.unref === 'function')
|
|
65
|
+
this.timer.unref();
|
|
66
|
+
}
|
|
67
|
+
/** Turn over (or broker tearing down) — no more relay expected until the
|
|
68
|
+
* next `poke()`. Idempotent. */
|
|
69
|
+
clear() {
|
|
70
|
+
if (this.timer === undefined)
|
|
71
|
+
return;
|
|
72
|
+
clearTimeout(this.timer);
|
|
73
|
+
this.timer = undefined;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { installTmuxBindings, sendKeysEnter, displayMessage, paneCurrentPath, type TmuxInstalledPair, type TmuxInstallDiagnostic, type TmuxInstallResult, } from './tmux.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// tmux-chrome.ts — chrome seam (§2.1): stateless keybind/input verbs.
|
|
2
2
|
// The ONLY non-placement module allowed to import the tmux driver, per the
|
|
3
|
-
// §5.1 lint. Re-exports the
|
|
4
|
-
export {
|
|
3
|
+
// §5.1 lint. Re-exports the server chrome callers need.
|
|
4
|
+
export { installTmuxBindings, sendKeysEnter, displayMessage, paneCurrentPath, } from './tmux.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type BindingId, type BindingResolution } from '../keybindings/index.js';
|
|
1
2
|
/** POSIX single-quote escaping for one shell word. */
|
|
2
3
|
export declare function shellQuote(s: string): string;
|
|
3
4
|
export declare function inTmux(): boolean;
|
|
@@ -202,18 +203,20 @@ export declare function switchClient(session: string): boolean;
|
|
|
202
203
|
* (`tmux send-keys -t <pane> '<text>' Enter`). Requires the pane's editor be
|
|
203
204
|
* empty, same limitation as the menu's `/promote` item. Best-effort. */
|
|
204
205
|
export declare function sendKeysEnter(pane: string, text: string): boolean;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
206
|
+
export interface TmuxInstalledPair {
|
|
207
|
+
readonly table: string;
|
|
208
|
+
readonly key: string;
|
|
209
|
+
}
|
|
210
|
+
export interface TmuxInstallDiagnostic {
|
|
211
|
+
readonly code: 'occupied' | 'tmux-error';
|
|
212
|
+
readonly bindingId?: BindingId;
|
|
213
|
+
readonly gesture?: string;
|
|
214
|
+
readonly message: string;
|
|
215
|
+
}
|
|
216
|
+
export interface TmuxInstallResult {
|
|
217
|
+
readonly ok: boolean;
|
|
218
|
+
readonly installed: readonly TmuxInstalledPair[];
|
|
219
|
+
readonly diagnostics: readonly TmuxInstallDiagnostic[];
|
|
220
|
+
}
|
|
221
|
+
/** Reconcile the tmux server with one immutable crouter binding snapshot. */
|
|
222
|
+
export declare function installTmuxBindings(bindings?: BindingResolution<BindingId>): TmuxInstallResult;
|