@gigaflow/gmux 0.15.0
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/CHANGELOG.md +654 -0
- package/LICENSE +21 -0
- package/README.md +351 -0
- package/dist/adapters/claude-code.d.ts +52 -0
- package/dist/adapters/claude-code.js +315 -0
- package/dist/adapters/claude-code.js.map +1 -0
- package/dist/adapters/codex.d.ts +39 -0
- package/dist/adapters/codex.js +233 -0
- package/dist/adapters/codex.js.map +1 -0
- package/dist/adapters/jsonl.d.ts +40 -0
- package/dist/adapters/jsonl.js +92 -0
- package/dist/adapters/jsonl.js.map +1 -0
- package/dist/adapters/registry.d.ts +11 -0
- package/dist/adapters/registry.js +21 -0
- package/dist/adapters/registry.js.map +1 -0
- package/dist/adapters/types.d.ts +43 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/cli/border-client.d.ts +8 -0
- package/dist/cli/border-client.js +11 -0
- package/dist/cli/border-client.js.map +1 -0
- package/dist/cli/commands/__ask-cancel.d.ts +35 -0
- package/dist/cli/commands/__ask-cancel.js +91 -0
- package/dist/cli/commands/__ask-cancel.js.map +1 -0
- package/dist/cli/commands/__ask-refresh.d.ts +40 -0
- package/dist/cli/commands/__ask-refresh.js +54 -0
- package/dist/cli/commands/__ask-refresh.js.map +1 -0
- package/dist/cli/commands/__ask-run.d.ts +43 -0
- package/dist/cli/commands/__ask-run.js +126 -0
- package/dist/cli/commands/__ask-run.js.map +1 -0
- package/dist/cli/commands/__ask-send.d.ts +50 -0
- package/dist/cli/commands/__ask-send.js +138 -0
- package/dist/cli/commands/__ask-send.js.map +1 -0
- package/dist/cli/commands/__preview-card.d.ts +31 -0
- package/dist/cli/commands/__preview-card.js +72 -0
- package/dist/cli/commands/__preview-card.js.map +1 -0
- package/dist/cli/commands/ask.d.ts +50 -0
- package/dist/cli/commands/ask.js +240 -0
- package/dist/cli/commands/ask.js.map +1 -0
- package/dist/cli/commands/auto.d.ts +12 -0
- package/dist/cli/commands/auto.js +28 -0
- package/dist/cli/commands/auto.js.map +1 -0
- package/dist/cli/commands/cockpit.d.ts +14 -0
- package/dist/cli/commands/cockpit.js +52 -0
- package/dist/cli/commands/cockpit.js.map +1 -0
- package/dist/cli/commands/daemon.d.ts +75 -0
- package/dist/cli/commands/daemon.js +258 -0
- package/dist/cli/commands/daemon.js.map +1 -0
- package/dist/cli/commands/doctor.d.ts +6 -0
- package/dist/cli/commands/doctor.js +159 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/grep.d.ts +2 -0
- package/dist/cli/commands/grep.js +52 -0
- package/dist/cli/commands/grep.js.map +1 -0
- package/dist/cli/commands/index-cmd.d.ts +2 -0
- package/dist/cli/commands/index-cmd.js +29 -0
- package/dist/cli/commands/index-cmd.js.map +1 -0
- package/dist/cli/commands/ls.d.ts +30 -0
- package/dist/cli/commands/ls.js +95 -0
- package/dist/cli/commands/ls.js.map +1 -0
- package/dist/cli/commands/overlay.d.ts +26 -0
- package/dist/cli/commands/overlay.js +216 -0
- package/dist/cli/commands/overlay.js.map +1 -0
- package/dist/cli/commands/pick.d.ts +59 -0
- package/dist/cli/commands/pick.js +254 -0
- package/dist/cli/commands/pick.js.map +1 -0
- package/dist/cli/commands/picker-rows.d.ts +17 -0
- package/dist/cli/commands/picker-rows.js +47 -0
- package/dist/cli/commands/picker-rows.js.map +1 -0
- package/dist/cli/commands/resume.d.ts +25 -0
- package/dist/cli/commands/resume.js +88 -0
- package/dist/cli/commands/resume.js.map +1 -0
- package/dist/cli/commands/run.d.ts +13 -0
- package/dist/cli/commands/run.js +78 -0
- package/dist/cli/commands/run.js.map +1 -0
- package/dist/cli/commands/setup.d.ts +37 -0
- package/dist/cli/commands/setup.js +200 -0
- package/dist/cli/commands/setup.js.map +1 -0
- package/dist/cli/commands/show.d.ts +2 -0
- package/dist/cli/commands/show.js +24 -0
- package/dist/cli/commands/show.js.map +1 -0
- package/dist/cli/commands/summarize.d.ts +2 -0
- package/dist/cli/commands/summarize.js +59 -0
- package/dist/cli/commands/summarize.js.map +1 -0
- package/dist/cli/commands/tmux.d.ts +11 -0
- package/dist/cli/commands/tmux.js +90 -0
- package/dist/cli/commands/tmux.js.map +1 -0
- package/dist/cli/commands/watch.d.ts +2 -0
- package/dist/cli/commands/watch.js +76 -0
- package/dist/cli/commands/watch.js.map +1 -0
- package/dist/cli/format.d.ts +74 -0
- package/dist/cli/format.js +242 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/gmux-render.d.ts +11 -0
- package/dist/cli/gmux-render.js +60 -0
- package/dist/cli/gmux-render.js.map +1 -0
- package/dist/cli/main.d.ts +8 -0
- package/dist/cli/main.js +145 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/overlay-ask.d.ts +13 -0
- package/dist/cli/overlay-ask.js +32 -0
- package/dist/cli/overlay-ask.js.map +1 -0
- package/dist/cli/overlay.d.ts +26 -0
- package/dist/cli/overlay.js +149 -0
- package/dist/cli/overlay.js.map +1 -0
- package/dist/cli/picker.d.ts +308 -0
- package/dist/cli/picker.js +642 -0
- package/dist/cli/picker.js.map +1 -0
- package/dist/cli/preview.d.ts +87 -0
- package/dist/cli/preview.js +292 -0
- package/dist/cli/preview.js.map +1 -0
- package/dist/cli/tmux-label.d.ts +50 -0
- package/dist/cli/tmux-label.js +146 -0
- package/dist/cli/tmux-label.js.map +1 -0
- package/dist/core/errors.d.ts +54 -0
- package/dist/core/errors.js +91 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/fingerprint.d.ts +21 -0
- package/dist/core/fingerprint.js +59 -0
- package/dist/core/fingerprint.js.map +1 -0
- package/dist/core/gmux-types.d.ts +102 -0
- package/dist/core/gmux-types.js +13 -0
- package/dist/core/gmux-types.js.map +1 -0
- package/dist/core/pane-state.d.ts +5 -0
- package/dist/core/pane-state.js +15 -0
- package/dist/core/pane-state.js.map +1 -0
- package/dist/core/paths.d.ts +68 -0
- package/dist/core/paths.js +106 -0
- package/dist/core/paths.js.map +1 -0
- package/dist/core/proc-tree.d.ts +9 -0
- package/dist/core/proc-tree.js +33 -0
- package/dist/core/proc-tree.js.map +1 -0
- package/dist/core/text.d.ts +34 -0
- package/dist/core/text.js +126 -0
- package/dist/core/text.js.map +1 -0
- package/dist/core/types.d.ts +322 -0
- package/dist/core/types.js +10 -0
- package/dist/core/types.js.map +1 -0
- package/dist/services/ask-transcript.d.ts +256 -0
- package/dist/services/ask-transcript.js +544 -0
- package/dist/services/ask-transcript.js.map +1 -0
- package/dist/services/ask.d.ts +54 -0
- package/dist/services/ask.js +161 -0
- package/dist/services/ask.js.map +1 -0
- package/dist/services/auto-summarize.d.ts +188 -0
- package/dist/services/auto-summarize.js +415 -0
- package/dist/services/auto-summarize.js.map +1 -0
- package/dist/services/concurrency.d.ts +2 -0
- package/dist/services/concurrency.js +16 -0
- package/dist/services/concurrency.js.map +1 -0
- package/dist/services/config.d.ts +131 -0
- package/dist/services/config.js +325 -0
- package/dist/services/config.js.map +1 -0
- package/dist/services/daemon-client.d.ts +9 -0
- package/dist/services/daemon-client.js +34 -0
- package/dist/services/daemon-client.js.map +1 -0
- package/dist/services/daemon-socket.d.ts +30 -0
- package/dist/services/daemon-socket.js +76 -0
- package/dist/services/daemon-socket.js.map +1 -0
- package/dist/services/daemon.d.ts +43 -0
- package/dist/services/daemon.js +106 -0
- package/dist/services/daemon.js.map +1 -0
- package/dist/services/distill.d.ts +39 -0
- package/dist/services/distill.js +93 -0
- package/dist/services/distill.js.map +1 -0
- package/dist/services/guardian.d.ts +26 -0
- package/dist/services/guardian.js +71 -0
- package/dist/services/guardian.js.map +1 -0
- package/dist/services/index-store.d.ts +36 -0
- package/dist/services/index-store.js +125 -0
- package/dist/services/index-store.js.map +1 -0
- package/dist/services/log-rotation.d.ts +3 -0
- package/dist/services/log-rotation.js +28 -0
- package/dist/services/log-rotation.js.map +1 -0
- package/dist/services/pane-links.d.ts +10 -0
- package/dist/services/pane-links.js +47 -0
- package/dist/services/pane-links.js.map +1 -0
- package/dist/services/pane-process.d.ts +95 -0
- package/dist/services/pane-process.js +183 -0
- package/dist/services/pane-process.js.map +1 -0
- package/dist/services/pane-registry.d.ts +24 -0
- package/dist/services/pane-registry.js +40 -0
- package/dist/services/pane-registry.js.map +1 -0
- package/dist/services/provider-process.d.ts +37 -0
- package/dist/services/provider-process.js +105 -0
- package/dist/services/provider-process.js.map +1 -0
- package/dist/services/providers.d.ts +76 -0
- package/dist/services/providers.js +104 -0
- package/dist/services/providers.js.map +1 -0
- package/dist/services/resolve.d.ts +3 -0
- package/dist/services/resolve.js +18 -0
- package/dist/services/resolve.js.map +1 -0
- package/dist/services/resources.d.ts +17 -0
- package/dist/services/resources.js +113 -0
- package/dist/services/resources.js.map +1 -0
- package/dist/services/search.d.ts +29 -0
- package/dist/services/search.js +135 -0
- package/dist/services/search.js.map +1 -0
- package/dist/services/semantic-gate.d.ts +13 -0
- package/dist/services/semantic-gate.js +23 -0
- package/dist/services/semantic-gate.js.map +1 -0
- package/dist/services/semantic.d.ts +113 -0
- package/dist/services/semantic.js +216 -0
- package/dist/services/semantic.js.map +1 -0
- package/dist/services/sensors.d.ts +92 -0
- package/dist/services/sensors.js +246 -0
- package/dist/services/sensors.js.map +1 -0
- package/dist/services/summarize.d.ts +84 -0
- package/dist/services/summarize.js +230 -0
- package/dist/services/summarize.js.map +1 -0
- package/dist/services/tmux-gateway.d.ts +17 -0
- package/dist/services/tmux-gateway.js +10 -0
- package/dist/services/tmux-gateway.js.map +1 -0
- package/dist/services/tmux-resolve.d.ts +54 -0
- package/dist/services/tmux-resolve.js +0 -0
- package/dist/services/tmux-resolve.js.map +1 -0
- package/dist/services/tmux.d.ts +32 -0
- package/dist/services/tmux.js +107 -0
- package/dist/services/tmux.js.map +1 -0
- package/dist/services/views.d.ts +7 -0
- package/dist/services/views.js +18 -0
- package/dist/services/views.js.map +1 -0
- package/dist/services/watch.d.ts +36 -0
- package/dist/services/watch.js +98 -0
- package/dist/services/watch.js.map +1 -0
- package/dist/services/workspace.d.ts +19 -0
- package/dist/services/workspace.js +88 -0
- package/dist/services/workspace.js.map +1 -0
- package/package.json +64 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { parsePsOutput, subtreeRss } from "../core/proc-tree.js";
|
|
5
|
+
const run = promisify(execFile);
|
|
6
|
+
/**
|
|
7
|
+
* Cap every resource probe on the tick path. A wedged `ps`/`sysctl`/`vm_stat`
|
|
8
|
+
* would otherwise stall the tick forever; the timeout kills the child and
|
|
9
|
+
* rejects, which the defensive catches below already handle.
|
|
10
|
+
*/
|
|
11
|
+
const RESOURCE_TIMEOUT_MS = 5000;
|
|
12
|
+
export function defaultResourceDeps() {
|
|
13
|
+
return {
|
|
14
|
+
psSnapshot: async () => (await run("ps", ["-axo", "pid,ppid,rss,comm"], { timeout: RESOURCE_TIMEOUT_MS })).stdout,
|
|
15
|
+
hostMemory: async () => (process.platform === "darwin" ? readMacMemory() : readLinuxMemory()),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export class ResourceMonitor {
|
|
19
|
+
deps;
|
|
20
|
+
constructor(deps = {}) {
|
|
21
|
+
this.deps = { ...defaultResourceDeps(), ...deps };
|
|
22
|
+
}
|
|
23
|
+
async sample(panePids) {
|
|
24
|
+
const now = Date.now();
|
|
25
|
+
const rows = parsePsOutput(await this.deps.psSnapshot());
|
|
26
|
+
const perPane = new Map();
|
|
27
|
+
let attributed = 0;
|
|
28
|
+
for (const [paneId, pid] of panePids) {
|
|
29
|
+
const rss = subtreeRss(pid, rows);
|
|
30
|
+
attributed += rss;
|
|
31
|
+
perPane.set(paneId, { perPaneRss: rss, ts: now });
|
|
32
|
+
}
|
|
33
|
+
const mem = await this.deps.hostMemory();
|
|
34
|
+
const host = {
|
|
35
|
+
usedRatio: mem.usedRatio,
|
|
36
|
+
unattributed: Math.max(0, mem.usedBytes - attributed),
|
|
37
|
+
ts: now,
|
|
38
|
+
};
|
|
39
|
+
return { perPane, host };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Parse `vm_stat` + `sysctl -n hw.memsize` / `hw.pagesize` for macOS memory pressure.
|
|
44
|
+
* Used ≈ (active + wired + compressed) pages × page size. Defensive: returns a safe
|
|
45
|
+
* zero reading rather than throwing if parsing fails.
|
|
46
|
+
*/
|
|
47
|
+
async function readMacMemory() {
|
|
48
|
+
try {
|
|
49
|
+
const [{ stdout: memsizeOut }, { stdout: pagesizeOut }, { stdout: vmStatOut }] = await Promise.all([
|
|
50
|
+
run("sysctl", ["-n", "hw.memsize"], { timeout: RESOURCE_TIMEOUT_MS }),
|
|
51
|
+
run("sysctl", ["-n", "hw.pagesize"], { timeout: RESOURCE_TIMEOUT_MS }),
|
|
52
|
+
run("vm_stat", [], { timeout: RESOURCE_TIMEOUT_MS }),
|
|
53
|
+
]);
|
|
54
|
+
const total = Number(memsizeOut.trim());
|
|
55
|
+
const pageSize = Number(pagesizeOut.trim());
|
|
56
|
+
if (!Number.isFinite(total) || total <= 0 || !Number.isFinite(pageSize) || pageSize <= 0) {
|
|
57
|
+
return { usedRatio: 0, usedBytes: 0 };
|
|
58
|
+
}
|
|
59
|
+
const pages = parseVmStatPages(vmStatOut);
|
|
60
|
+
const active = pages.get("Pages active") ?? 0;
|
|
61
|
+
const wired = pages.get("Pages wired down") ?? 0;
|
|
62
|
+
const compressed = pages.get("Pages occupied by compressor") ?? 0;
|
|
63
|
+
const usedBytes = (active + wired + compressed) * pageSize;
|
|
64
|
+
if (!Number.isFinite(usedBytes) || usedBytes < 0) {
|
|
65
|
+
return { usedRatio: 0, usedBytes: 0 };
|
|
66
|
+
}
|
|
67
|
+
return { usedRatio: usedBytes / total, usedBytes };
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return { usedRatio: 0, usedBytes: 0 };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Parse `vm_stat` output lines like "Pages active: 123456." into a label→count map. */
|
|
74
|
+
function parseVmStatPages(raw) {
|
|
75
|
+
const pages = new Map();
|
|
76
|
+
for (const line of raw.split("\n")) {
|
|
77
|
+
const m = line.match(/^([A-Za-z][A-Za-z ]*?):\s*([\d,]+)\.?\s*$/);
|
|
78
|
+
if (!m)
|
|
79
|
+
continue;
|
|
80
|
+
const label = m[1].trim();
|
|
81
|
+
const count = Number(m[2].replace(/,/g, ""));
|
|
82
|
+
if (Number.isFinite(count))
|
|
83
|
+
pages.set(label, count);
|
|
84
|
+
}
|
|
85
|
+
return pages;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Parse `/proc/meminfo` for Linux memory pressure. Used = MemTotal − MemAvailable (KB→bytes).
|
|
89
|
+
* Defensive: returns a safe zero reading if the expected fields are missing.
|
|
90
|
+
*/
|
|
91
|
+
async function readLinuxMemory() {
|
|
92
|
+
try {
|
|
93
|
+
const raw = await readFile("/proc/meminfo", "utf8");
|
|
94
|
+
const fields = new Map();
|
|
95
|
+
for (const line of raw.split("\n")) {
|
|
96
|
+
const m = line.match(/^(\w+):\s*(\d+)\s*kB/);
|
|
97
|
+
if (!m)
|
|
98
|
+
continue;
|
|
99
|
+
fields.set(m[1], Number(m[2]));
|
|
100
|
+
}
|
|
101
|
+
const totalKb = fields.get("MemTotal");
|
|
102
|
+
const availKb = fields.get("MemAvailable");
|
|
103
|
+
if (totalKb === undefined || availKb === undefined || totalKb <= 0) {
|
|
104
|
+
return { usedRatio: 0, usedBytes: 0 };
|
|
105
|
+
}
|
|
106
|
+
const usedBytes = Math.max(0, totalKb - availKb) * 1024;
|
|
107
|
+
return { usedRatio: usedBytes / (totalKb * 1024), usedBytes };
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return { usedRatio: 0, usedBytes: 0 };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=resources.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resources.js","sourceRoot":"","sources":["../../src/services/resources.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-text search across every transcript.
|
|
3
|
+
*
|
|
4
|
+
* Half a gigabyte of JSONL is too much to scan in Node, so we shell out to
|
|
5
|
+
* ripgrep and map its hits back onto indexed sessions. `--json` gives us exact
|
|
6
|
+
* byte offsets and match counts without us parsing rg's human output.
|
|
7
|
+
*/
|
|
8
|
+
import type { SearchHit, SessionRecord } from "../core/types.js";
|
|
9
|
+
export interface SearchOptions {
|
|
10
|
+
/** Restrict to these sessions. Also supplies the file→session mapping. */
|
|
11
|
+
records: readonly SessionRecord[];
|
|
12
|
+
query: string;
|
|
13
|
+
/** Treat the query as a regex rather than a literal. */
|
|
14
|
+
regex?: boolean;
|
|
15
|
+
caseSensitive?: boolean;
|
|
16
|
+
maxSessions?: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function searchSessions(options: SearchOptions): Promise<SearchHit[]>;
|
|
19
|
+
/**
|
|
20
|
+
* A matching JSONL line is one enormous JSON record. Showing it raw would be
|
|
21
|
+
* unreadable, so we show a window of text around the match itself.
|
|
22
|
+
*/
|
|
23
|
+
export declare function snippetFrom(line: string, query: string, window?: number): string | null;
|
|
24
|
+
/**
|
|
25
|
+
* Split paths into groups that each fit comfortably inside argv.
|
|
26
|
+
* A single path longer than the cap still gets its own batch — dropping it
|
|
27
|
+
* silently would mean a session that can never be searched.
|
|
28
|
+
*/
|
|
29
|
+
export declare function batchPaths(paths: readonly string[], maxBytes: number): string[][];
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Full-text search across every transcript.
|
|
3
|
+
*
|
|
4
|
+
* Half a gigabyte of JSONL is too much to scan in Node, so we shell out to
|
|
5
|
+
* ripgrep and map its hits back onto indexed sessions. `--json` gives us exact
|
|
6
|
+
* byte offsets and match counts without us parsing rg's human output.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { MissingDependencyError } from "../core/errors.js";
|
|
10
|
+
import { truncate } from "../core/text.js";
|
|
11
|
+
const MAX_SNIPPETS_PER_SESSION = 3;
|
|
12
|
+
/**
|
|
13
|
+
* Cap on the bytes of file paths handed to one ripgrep invocation.
|
|
14
|
+
*
|
|
15
|
+
* We pass explicit paths so that `--project` and friends narrow the corpus
|
|
16
|
+
* exactly. But argv is finite: ARG_MAX is 1 MB on macOS and 128 KB per argument
|
|
17
|
+
* on Linux, and a heavy user's session paths add up fast (1,163 sessions is
|
|
18
|
+
* already ~230 KB). Past the limit the spawn fails with E2BIG — so we batch.
|
|
19
|
+
*/
|
|
20
|
+
const MAX_ARGV_BYTES = 60_000;
|
|
21
|
+
export async function searchSessions(options) {
|
|
22
|
+
const { records, query } = options;
|
|
23
|
+
if (records.length === 0 || query.trim() === "")
|
|
24
|
+
return [];
|
|
25
|
+
const byPath = new Map(records.map((r) => [r.filePath, r]));
|
|
26
|
+
const flags = [
|
|
27
|
+
"--json",
|
|
28
|
+
...(options.regex ? [] : ["--fixed-strings"]),
|
|
29
|
+
// Always pass the pattern via -e. As a bare positional, a query that starts
|
|
30
|
+
// with a dash (`gmux grep "-n"`) would be parsed by ripgrep as an option.
|
|
31
|
+
"-e",
|
|
32
|
+
query,
|
|
33
|
+
options.caseSensitive ? "--case-sensitive" : "--ignore-case",
|
|
34
|
+
"--",
|
|
35
|
+
];
|
|
36
|
+
const hits = new Map();
|
|
37
|
+
for (const batch of batchPaths([...byPath.keys()], MAX_ARGV_BYTES)) {
|
|
38
|
+
const lines = await runRipgrep([...flags, ...batch]);
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
let event;
|
|
41
|
+
try {
|
|
42
|
+
event = JSON.parse(line);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (event.type !== "match")
|
|
48
|
+
continue;
|
|
49
|
+
// rg reports a non-UTF8 path as `bytes` rather than `text`; skip those.
|
|
50
|
+
const path = event.data?.path?.text;
|
|
51
|
+
if (!path)
|
|
52
|
+
continue;
|
|
53
|
+
const record = byPath.get(path);
|
|
54
|
+
if (!record)
|
|
55
|
+
continue;
|
|
56
|
+
let hit = hits.get(path);
|
|
57
|
+
if (!hit) {
|
|
58
|
+
hit = { session: record, matchCount: 0, snippets: [] };
|
|
59
|
+
hits.set(path, hit);
|
|
60
|
+
}
|
|
61
|
+
hit.matchCount += 1;
|
|
62
|
+
if (hit.snippets.length < MAX_SNIPPETS_PER_SESSION) {
|
|
63
|
+
const snippet = snippetFrom(event.data?.lines?.text ?? "", query);
|
|
64
|
+
if (snippet)
|
|
65
|
+
hit.snippets.push(snippet);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const sorted = [...hits.values()].sort((a, b) => b.session.updatedAt.localeCompare(a.session.updatedAt));
|
|
70
|
+
return options.maxSessions ? sorted.slice(0, options.maxSessions) : sorted;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A matching JSONL line is one enormous JSON record. Showing it raw would be
|
|
74
|
+
* unreadable, so we show a window of text around the match itself.
|
|
75
|
+
*/
|
|
76
|
+
export function snippetFrom(line, query, window = 90) {
|
|
77
|
+
const at = line.toLowerCase().indexOf(query.toLowerCase());
|
|
78
|
+
const centre = at === -1 ? 0 : at;
|
|
79
|
+
const start = Math.max(0, centre - Math.floor(window / 3));
|
|
80
|
+
const slice = line.slice(start, start + window);
|
|
81
|
+
const cleaned = slice
|
|
82
|
+
.replace(/\\n/g, " ")
|
|
83
|
+
.replace(/\\"/g, '"')
|
|
84
|
+
.replace(/\s+/g, " ")
|
|
85
|
+
.trim();
|
|
86
|
+
return cleaned === "" ? null : truncate(cleaned, window);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Split paths into groups that each fit comfortably inside argv.
|
|
90
|
+
* A single path longer than the cap still gets its own batch — dropping it
|
|
91
|
+
* silently would mean a session that can never be searched.
|
|
92
|
+
*/
|
|
93
|
+
export function batchPaths(paths, maxBytes) {
|
|
94
|
+
const batches = [];
|
|
95
|
+
let current = [];
|
|
96
|
+
let bytes = 0;
|
|
97
|
+
for (const path of paths) {
|
|
98
|
+
const size = Buffer.byteLength(path) + 1;
|
|
99
|
+
if (current.length > 0 && bytes + size > maxBytes) {
|
|
100
|
+
batches.push(current);
|
|
101
|
+
current = [];
|
|
102
|
+
bytes = 0;
|
|
103
|
+
}
|
|
104
|
+
current.push(path);
|
|
105
|
+
bytes += size;
|
|
106
|
+
}
|
|
107
|
+
if (current.length > 0)
|
|
108
|
+
batches.push(current);
|
|
109
|
+
return batches;
|
|
110
|
+
}
|
|
111
|
+
function runRipgrep(args) {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
const child = spawn("rg", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
114
|
+
let stdout = "";
|
|
115
|
+
let stderr = "";
|
|
116
|
+
child.stdout.on("data", (chunk) => (stdout += String(chunk)));
|
|
117
|
+
child.stderr.on("data", (chunk) => (stderr += String(chunk)));
|
|
118
|
+
child.on("error", (error) => {
|
|
119
|
+
if (error.code === "ENOENT") {
|
|
120
|
+
reject(new MissingDependencyError("rg", "Install ripgrep: `brew install ripgrep` (macOS) or `apt install ripgrep` (Debian/Ubuntu)."));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
reject(error);
|
|
124
|
+
});
|
|
125
|
+
child.on("close", (code) => {
|
|
126
|
+
// rg exits 1 when there are simply no matches. That is not an error.
|
|
127
|
+
if (code === 0 || code === 1) {
|
|
128
|
+
resolve(stdout.split("\n").filter((l) => l !== ""));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
reject(new Error(`ripgrep exited with code ${code}: ${stderr.trim()}`));
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/services/search.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Observation } from "../core/gmux-types.js";
|
|
2
|
+
export declare class SemanticGate {
|
|
3
|
+
private last;
|
|
4
|
+
private readonly distance;
|
|
5
|
+
private readonly debounceMs;
|
|
6
|
+
constructor(opts?: {
|
|
7
|
+
distance?: number;
|
|
8
|
+
debounceMs?: number;
|
|
9
|
+
});
|
|
10
|
+
private fp;
|
|
11
|
+
shouldSummarize(paneId: string, obs: Observation, now: number): boolean;
|
|
12
|
+
noteQueued(paneId: string, obs: Observation, now: number): void;
|
|
13
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { hammingDistance, simhash64 } from "../core/fingerprint.js";
|
|
2
|
+
export class SemanticGate {
|
|
3
|
+
last = new Map();
|
|
4
|
+
distance;
|
|
5
|
+
debounceMs;
|
|
6
|
+
constructor(opts = {}) {
|
|
7
|
+
this.distance = opts.distance ?? 8;
|
|
8
|
+
this.debounceMs = opts.debounceMs ?? 4000;
|
|
9
|
+
}
|
|
10
|
+
fp(obs) { return simhash64(obs.tailLines.join("\n")); }
|
|
11
|
+
shouldSummarize(paneId, obs, now) {
|
|
12
|
+
const prev = this.last.get(paneId);
|
|
13
|
+
if (!prev)
|
|
14
|
+
return true;
|
|
15
|
+
if (now - prev.ts < this.debounceMs)
|
|
16
|
+
return false;
|
|
17
|
+
return hammingDistance(this.fp(obs), prev.fingerprint) > this.distance;
|
|
18
|
+
}
|
|
19
|
+
noteQueued(paneId, obs, now) {
|
|
20
|
+
this.last.set(paneId, { fingerprint: this.fp(obs), ts: now });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=semantic-gate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic-gate.js","sourceRoot":"","sources":["../../src/services/semantic-gate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAKpE,MAAM,OAAO,YAAY;IACf,IAAI,GAAG,IAAI,GAAG,EAAgB,CAAC;IACtB,QAAQ,CAAS;IACjB,UAAU,CAAS;IAEpC,YAAY,OAAmD,EAAE;QAC/D,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,CAAC,CAAC;QACnC,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC;IAC5C,CAAC;IAEO,EAAE,CAAC,GAAgB,IAAY,OAAO,SAAS,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAEpF,eAAe,CAAC,MAAc,EAAE,GAAgB,EAAE,GAAW;QAC3D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACnC,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC;QACvB,IAAI,GAAG,GAAG,IAAI,CAAC,EAAE,GAAG,IAAI,CAAC,UAAU;YAAE,OAAO,KAAK,CAAC;QAClD,OAAO,eAAe,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC;IACzE,CAAC;IAED,UAAU,CAAC,MAAc,EAAE,GAAgB,EAAE,GAAW;QACtD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,EAAE,EAAE,GAAG,EAAE,CAAC,CAAC;IAChE,CAAC;CACF"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The semantic worker: turns a gated pane observation into a `PaneSemantics`
|
|
3
|
+
* (one-line label + card) and writes it to the workspace model.
|
|
4
|
+
*
|
|
5
|
+
* THE DECOUPLING INVARIANT. An LLM call can hang, time out, or simply be slow
|
|
6
|
+
* — and gmux's fast path (sensors, gate, state transitions, the socket) must
|
|
7
|
+
* never notice. `SemanticWorker` is a bounded concurrent pool keyed by pane:
|
|
8
|
+
* `enqueue` never awaits the provider, coalesces a pane's pending job (newest
|
|
9
|
+
* observation wins), and lets `concurrency` jobs run side by side. One pane
|
|
10
|
+
* hanging never keeps another pane's label from landing.
|
|
11
|
+
*/
|
|
12
|
+
import type { Observation, PaneEntry } from "../core/gmux-types.js";
|
|
13
|
+
import type { WorkspaceModel } from "./workspace.js";
|
|
14
|
+
import type { SemanticGate } from "./semantic-gate.js";
|
|
15
|
+
/** What a label provider is asked for, and what it must answer with. */
|
|
16
|
+
export interface LabelProvider {
|
|
17
|
+
label(input: {
|
|
18
|
+
paneId: string;
|
|
19
|
+
project: string | null;
|
|
20
|
+
tailLines: string[];
|
|
21
|
+
}): Promise<{
|
|
22
|
+
label: string;
|
|
23
|
+
card: string;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Pull `{ label, card }` out of a model reply.
|
|
28
|
+
*
|
|
29
|
+
* Models fence their JSON or preface it more often than they should, so —
|
|
30
|
+
* matching `parseSummaryFields` — we take the outermost brace pair rather
|
|
31
|
+
* than trusting the whole reply to parse. A reply we can't make sense of
|
|
32
|
+
* still becomes *something*: the first line as the label, the raw text as
|
|
33
|
+
* the card, so a scrappy provider degrades gracefully instead of throwing.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseLabelFields(raw: string): {
|
|
36
|
+
label: string;
|
|
37
|
+
card: string;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The real `LabelProvider`: a thin wrapper over a provider CLI (the same
|
|
41
|
+
* argv `gmux`'s session summaries use), with its own compact prompt.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately NOT built on `SummaryProvider`/`SummaryInput` — that shape
|
|
44
|
+
* carries a whole session's prompt history, git branch, files touched, and a
|
|
45
|
+
* six-field reply, none of which a live pane observation (paneId, project,
|
|
46
|
+
* a tail of lines) can honestly fill in. Reusing it would mean fabricating
|
|
47
|
+
* those fields or leaving them empty, which is worse than a dedicated,
|
|
48
|
+
* shorter prompt asking for exactly the two fields we need. `argv` is
|
|
49
|
+
* resolved the same way summaries resolve theirs (`resolveSummaryCommand`),
|
|
50
|
+
* so the two features keep sharing one provider *choice* even though they
|
|
51
|
+
* don't share a prompt shape.
|
|
52
|
+
*
|
|
53
|
+
* `argv: null` means "no provider configured" (`gmux setup` never ran, or the
|
|
54
|
+
* user chose to make no model calls) — `label()` throws in that case, which
|
|
55
|
+
* `SemanticWorker`'s try/catch turns into a silent skip rather than a crash.
|
|
56
|
+
*/
|
|
57
|
+
export declare class CliLabelProvider implements LabelProvider {
|
|
58
|
+
private readonly argv;
|
|
59
|
+
constructor(argv: string[] | null);
|
|
60
|
+
label(input: {
|
|
61
|
+
paneId: string;
|
|
62
|
+
project: string | null;
|
|
63
|
+
tailLines: string[];
|
|
64
|
+
}): Promise<{
|
|
65
|
+
label: string;
|
|
66
|
+
card: string;
|
|
67
|
+
}>;
|
|
68
|
+
}
|
|
69
|
+
/** The `CliLabelProvider` for the user's current config (`gmux setup`). */
|
|
70
|
+
export declare function defaultLabelProvider(): Promise<CliLabelProvider>;
|
|
71
|
+
/**
|
|
72
|
+
* A bounded worker pool that labels gated observations without ever letting
|
|
73
|
+
* a slow or hung provider call block another pane, or the caller of
|
|
74
|
+
* `enqueue`.
|
|
75
|
+
*
|
|
76
|
+
* `enqueue` is synchronous from the caller's point of view: it consults the
|
|
77
|
+
* gate, coalesces this pane's pending job (a newer observation replaces an
|
|
78
|
+
* older, not-yet-started one), and kicks the pump — it never awaits the
|
|
79
|
+
* provider itself. Up to `concurrency` jobs run side by side; a pane whose
|
|
80
|
+
* `provider.label()` never resolves simply never frees its pool slot, while
|
|
81
|
+
* every other slot keeps turning over.
|
|
82
|
+
*/
|
|
83
|
+
export declare class SemanticWorker {
|
|
84
|
+
private readonly model;
|
|
85
|
+
private readonly provider;
|
|
86
|
+
private readonly gate;
|
|
87
|
+
private readonly concurrency;
|
|
88
|
+
private running;
|
|
89
|
+
private pending;
|
|
90
|
+
private waiters;
|
|
91
|
+
private pumpScheduled;
|
|
92
|
+
constructor(model: WorkspaceModel, provider: LabelProvider, gate: SemanticGate, concurrency?: number);
|
|
93
|
+
/** Gate, coalesce, and schedule — never awaits the provider. */
|
|
94
|
+
enqueue(entry: PaneEntry, obs: Observation, now: number): void;
|
|
95
|
+
/**
|
|
96
|
+
* Defer the next `pump()` to a microtask instead of dispatching inline.
|
|
97
|
+
*
|
|
98
|
+
* A caller commonly fires several synchronous `enqueue` calls back to back
|
|
99
|
+
* (one scan tick touching many panes). Dispatching inline would let the
|
|
100
|
+
* very first `enqueue` claim a free pool slot before its siblings even
|
|
101
|
+
* land in `pending`, making `nextJob()`'s priority pick blind to the rest
|
|
102
|
+
* of the burst. Coalescing all of them into `pending` before the first
|
|
103
|
+
* pick — via one microtask hop — is what makes prioritization observable;
|
|
104
|
+
* it changes nothing about concurrency or the fire-and-forget run below.
|
|
105
|
+
*/
|
|
106
|
+
private schedulePump;
|
|
107
|
+
/** Scan `pending` for the highest-`priorityOf` job; null when empty. */
|
|
108
|
+
private nextJob;
|
|
109
|
+
private pump;
|
|
110
|
+
private run;
|
|
111
|
+
/** Resolves once no job is running and none is pending. */
|
|
112
|
+
drain(): Promise<void>;
|
|
113
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The semantic worker: turns a gated pane observation into a `PaneSemantics`
|
|
3
|
+
* (one-line label + card) and writes it to the workspace model.
|
|
4
|
+
*
|
|
5
|
+
* THE DECOUPLING INVARIANT. An LLM call can hang, time out, or simply be slow
|
|
6
|
+
* — and gmux's fast path (sensors, gate, state transitions, the socket) must
|
|
7
|
+
* never notice. `SemanticWorker` is a bounded concurrent pool keyed by pane:
|
|
8
|
+
* `enqueue` never awaits the provider, coalesces a pane's pending job (newest
|
|
9
|
+
* observation wins), and lets `concurrency` jobs run side by side. One pane
|
|
10
|
+
* hanging never keeps another pane's label from landing.
|
|
11
|
+
*/
|
|
12
|
+
import { simhash64 } from "../core/fingerprint.js";
|
|
13
|
+
import { readConfig, resolveSummaryCommand } from "./config.js";
|
|
14
|
+
import { runProviderCommand } from "./provider-process.js";
|
|
15
|
+
const LABEL_TIMEOUT_MS = 120_000;
|
|
16
|
+
/** Cap the prompt so a runaway transcript tail doesn't balloon the call. */
|
|
17
|
+
const MAX_TAIL_LINES = 60;
|
|
18
|
+
function buildLabelPrompt(input) {
|
|
19
|
+
const project = input.project ?? "an unknown project";
|
|
20
|
+
const tail = input.tailLines.slice(-MAX_TAIL_LINES).join("\n");
|
|
21
|
+
return [
|
|
22
|
+
`You are labeling a live terminal pane working in "${project}".`,
|
|
23
|
+
"Here is the most recent output from that pane:",
|
|
24
|
+
"---",
|
|
25
|
+
tail,
|
|
26
|
+
"---",
|
|
27
|
+
"Reply with ONLY a single JSON object, no prose and no code fence:",
|
|
28
|
+
'{"label": "<=8 words, present tense, what it is doing right now>", "card": "2-4 sentences of context: what it is, what changed recently, what is next"}',
|
|
29
|
+
].join("\n");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Pull `{ label, card }` out of a model reply.
|
|
33
|
+
*
|
|
34
|
+
* Models fence their JSON or preface it more often than they should, so —
|
|
35
|
+
* matching `parseSummaryFields` — we take the outermost brace pair rather
|
|
36
|
+
* than trusting the whole reply to parse. A reply we can't make sense of
|
|
37
|
+
* still becomes *something*: the first line as the label, the raw text as
|
|
38
|
+
* the card, so a scrappy provider degrades gracefully instead of throwing.
|
|
39
|
+
*/
|
|
40
|
+
export function parseLabelFields(raw) {
|
|
41
|
+
const start = raw.indexOf("{");
|
|
42
|
+
const end = raw.lastIndexOf("}");
|
|
43
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(raw.slice(start, end + 1));
|
|
46
|
+
const label = typeof parsed["label"] === "string" ? parsed["label"].trim() : "";
|
|
47
|
+
const card = typeof parsed["card"] === "string" ? parsed["card"].trim() : "";
|
|
48
|
+
if (label)
|
|
49
|
+
return { label, card };
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Fall through to the defensive fallback below.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const firstLine = raw.trim().split("\n")[0]?.trim() ?? "";
|
|
56
|
+
return { label: firstLine || "(no label)", card: raw.trim() };
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The real `LabelProvider`: a thin wrapper over a provider CLI (the same
|
|
60
|
+
* argv `gmux`'s session summaries use), with its own compact prompt.
|
|
61
|
+
*
|
|
62
|
+
* Deliberately NOT built on `SummaryProvider`/`SummaryInput` — that shape
|
|
63
|
+
* carries a whole session's prompt history, git branch, files touched, and a
|
|
64
|
+
* six-field reply, none of which a live pane observation (paneId, project,
|
|
65
|
+
* a tail of lines) can honestly fill in. Reusing it would mean fabricating
|
|
66
|
+
* those fields or leaving them empty, which is worse than a dedicated,
|
|
67
|
+
* shorter prompt asking for exactly the two fields we need. `argv` is
|
|
68
|
+
* resolved the same way summaries resolve theirs (`resolveSummaryCommand`),
|
|
69
|
+
* so the two features keep sharing one provider *choice* even though they
|
|
70
|
+
* don't share a prompt shape.
|
|
71
|
+
*
|
|
72
|
+
* `argv: null` means "no provider configured" (`gmux setup` never ran, or the
|
|
73
|
+
* user chose to make no model calls) — `label()` throws in that case, which
|
|
74
|
+
* `SemanticWorker`'s try/catch turns into a silent skip rather than a crash.
|
|
75
|
+
*/
|
|
76
|
+
export class CliLabelProvider {
|
|
77
|
+
argv;
|
|
78
|
+
constructor(argv) {
|
|
79
|
+
this.argv = argv;
|
|
80
|
+
}
|
|
81
|
+
async label(input) {
|
|
82
|
+
if (!this.argv)
|
|
83
|
+
throw new Error("no summary provider configured");
|
|
84
|
+
const prompt = buildLabelPrompt(input);
|
|
85
|
+
const raw = await runProviderCommand(this.argv, prompt, { timeoutMs: LABEL_TIMEOUT_MS });
|
|
86
|
+
return parseLabelFields(raw);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** The `CliLabelProvider` for the user's current config (`gmux setup`). */
|
|
90
|
+
export async function defaultLabelProvider() {
|
|
91
|
+
const config = await readConfig();
|
|
92
|
+
return new CliLabelProvider(resolveSummaryCommand(config));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Priority for the pane a `PendingJob` labels: the active/visible pane
|
|
96
|
+
* outranks any background pane, and a background pane that's mid-`working`
|
|
97
|
+
* outranks an idle one. Ties (any two panes at the same priority) are broken
|
|
98
|
+
* by scan order — `nextJob()` keeps the first one it sees.
|
|
99
|
+
*/
|
|
100
|
+
function priorityOf(entry) {
|
|
101
|
+
if (entry.identity.active)
|
|
102
|
+
return 3;
|
|
103
|
+
if (entry.state === "working")
|
|
104
|
+
return 2;
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* A bounded worker pool that labels gated observations without ever letting
|
|
109
|
+
* a slow or hung provider call block another pane, or the caller of
|
|
110
|
+
* `enqueue`.
|
|
111
|
+
*
|
|
112
|
+
* `enqueue` is synchronous from the caller's point of view: it consults the
|
|
113
|
+
* gate, coalesces this pane's pending job (a newer observation replaces an
|
|
114
|
+
* older, not-yet-started one), and kicks the pump — it never awaits the
|
|
115
|
+
* provider itself. Up to `concurrency` jobs run side by side; a pane whose
|
|
116
|
+
* `provider.label()` never resolves simply never frees its pool slot, while
|
|
117
|
+
* every other slot keeps turning over.
|
|
118
|
+
*/
|
|
119
|
+
export class SemanticWorker {
|
|
120
|
+
model;
|
|
121
|
+
provider;
|
|
122
|
+
gate;
|
|
123
|
+
concurrency;
|
|
124
|
+
running = 0;
|
|
125
|
+
pending = new Map();
|
|
126
|
+
waiters = [];
|
|
127
|
+
pumpScheduled = false;
|
|
128
|
+
constructor(model, provider, gate, concurrency = 4) {
|
|
129
|
+
this.model = model;
|
|
130
|
+
this.provider = provider;
|
|
131
|
+
this.gate = gate;
|
|
132
|
+
this.concurrency = concurrency;
|
|
133
|
+
}
|
|
134
|
+
/** Gate, coalesce, and schedule — never awaits the provider. */
|
|
135
|
+
enqueue(entry, obs, now) {
|
|
136
|
+
if (!this.gate.shouldSummarize(entry.identity.paneId, obs, now))
|
|
137
|
+
return;
|
|
138
|
+
this.gate.noteQueued(entry.identity.paneId, obs, now);
|
|
139
|
+
this.pending.set(entry.identity.paneId, { entry, obs, now }); // coalesce: newest wins
|
|
140
|
+
this.schedulePump();
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Defer the next `pump()` to a microtask instead of dispatching inline.
|
|
144
|
+
*
|
|
145
|
+
* A caller commonly fires several synchronous `enqueue` calls back to back
|
|
146
|
+
* (one scan tick touching many panes). Dispatching inline would let the
|
|
147
|
+
* very first `enqueue` claim a free pool slot before its siblings even
|
|
148
|
+
* land in `pending`, making `nextJob()`'s priority pick blind to the rest
|
|
149
|
+
* of the burst. Coalescing all of them into `pending` before the first
|
|
150
|
+
* pick — via one microtask hop — is what makes prioritization observable;
|
|
151
|
+
* it changes nothing about concurrency or the fire-and-forget run below.
|
|
152
|
+
*/
|
|
153
|
+
schedulePump() {
|
|
154
|
+
if (this.pumpScheduled)
|
|
155
|
+
return;
|
|
156
|
+
this.pumpScheduled = true;
|
|
157
|
+
queueMicrotask(() => {
|
|
158
|
+
this.pumpScheduled = false;
|
|
159
|
+
this.pump();
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
/** Scan `pending` for the highest-`priorityOf` job; null when empty. */
|
|
163
|
+
nextJob() {
|
|
164
|
+
let best = null;
|
|
165
|
+
for (const kv of this.pending) {
|
|
166
|
+
if (!best || priorityOf(kv[1].entry) > priorityOf(best[1].entry))
|
|
167
|
+
best = kv;
|
|
168
|
+
}
|
|
169
|
+
return best;
|
|
170
|
+
}
|
|
171
|
+
pump() {
|
|
172
|
+
while (this.running < this.concurrency && this.pending.size > 0) {
|
|
173
|
+
const next = this.nextJob();
|
|
174
|
+
if (!next)
|
|
175
|
+
break; // Guards against a concurrent-mutation edge case under noUncheckedIndexedAccess.
|
|
176
|
+
const [paneId, job] = next;
|
|
177
|
+
this.pending.delete(paneId);
|
|
178
|
+
this.running += 1;
|
|
179
|
+
void this.run(paneId, job).finally(() => {
|
|
180
|
+
this.running -= 1;
|
|
181
|
+
this.pump();
|
|
182
|
+
if (this.running === 0 && this.pending.size === 0) {
|
|
183
|
+
const waiters = this.waiters;
|
|
184
|
+
this.waiters = [];
|
|
185
|
+
waiters.forEach((w) => w());
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async run(paneId, job) {
|
|
191
|
+
try {
|
|
192
|
+
const cwd = job.entry.identity.cwd;
|
|
193
|
+
const project = cwd ? (cwd.split("/").pop() ?? null) : null;
|
|
194
|
+
const { label, card } = await this.provider.label({ paneId, project, tailLines: job.obs.tailLines });
|
|
195
|
+
const semantics = {
|
|
196
|
+
label,
|
|
197
|
+
card,
|
|
198
|
+
fingerprint: simhash64(job.obs.tailLines.join("\n")),
|
|
199
|
+
updatedAt: job.now,
|
|
200
|
+
stale: false,
|
|
201
|
+
};
|
|
202
|
+
this.model.applySemantics(paneId, semantics);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// A provider failure marks nothing rather than crashing the worker;
|
|
206
|
+
// the next meaningful change re-triggers the gate and retries.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Resolves once no job is running and none is pending. */
|
|
210
|
+
drain() {
|
|
211
|
+
if (this.running === 0 && this.pending.size === 0)
|
|
212
|
+
return Promise.resolve();
|
|
213
|
+
return new Promise((r) => this.waiters.push(r));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=semantic.js.map
|