@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,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config: the one thing gmux remembers because a human said so.
|
|
3
|
+
*
|
|
4
|
+
* Everything else in `~/.cache/gmux` is derived and disposable. This
|
|
5
|
+
* isn't, which is why it lives under the config dir instead — see core/paths.ts.
|
|
6
|
+
*
|
|
7
|
+
* Two rules run through this file:
|
|
8
|
+
*
|
|
9
|
+
* 1. **A bad config is not an error.** Unreadable, malformed, or written by a
|
|
10
|
+
* future version — all are treated as *absent*. `gmux ls` must not die because
|
|
11
|
+
* a JSON file got truncated; it should fall back to the behavior of someone
|
|
12
|
+
* who never ran `gmux setup`, which is a behavior that works.
|
|
13
|
+
* 2. **The env var wins.** `GMUX_SUMMARY_CMD` sat on top before config
|
|
14
|
+
* existed, and every script, test and CI job that sets it must keep working
|
|
15
|
+
* without being told about a new file.
|
|
16
|
+
*/
|
|
17
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
18
|
+
import { dirname } from "node:path";
|
|
19
|
+
import { configPath } from "../core/paths.js";
|
|
20
|
+
import { DEFAULT_GMUX_CONFIG } from "../core/gmux-types.js";
|
|
21
|
+
import { CONFIG_VERSION } from "../core/types.js";
|
|
22
|
+
import { askArgvFor, firstDetected, summaryArgvFor } from "./providers.js";
|
|
23
|
+
/** The last-resort provider, unchanged from before config existed. */
|
|
24
|
+
export const FALLBACK_COMMAND = ["claude", "-p"];
|
|
25
|
+
/** The env var that outranks everything here. */
|
|
26
|
+
export const SUMMARY_CMD_ENV = "GMUX_SUMMARY_CMD";
|
|
27
|
+
/**
|
|
28
|
+
* Set on any `gmux` we spawn ourselves.
|
|
29
|
+
*
|
|
30
|
+
* THE RECURSION GUARD. `gmux ask` runs a provider that may run `gmux grep`, and
|
|
31
|
+
* `gmux grep` would otherwise fire the postAction hook and spawn a summarizer —
|
|
32
|
+
* from inside a process that a summarizer-adjacent call started. The lock would
|
|
33
|
+
* absorb most of it; this stops it being a question in the first place.
|
|
34
|
+
*/
|
|
35
|
+
export const CHILD_ENV = "GMUX_CHILD";
|
|
36
|
+
export function isChildProcess(env = process.env) {
|
|
37
|
+
const raw = env[CHILD_ENV];
|
|
38
|
+
return raw != null && raw.trim() !== "" && raw !== "0";
|
|
39
|
+
}
|
|
40
|
+
/** The env a spawned provider gets: ours, plus the marker. */
|
|
41
|
+
export function childEnv(env = process.env) {
|
|
42
|
+
return { ...env, [CHILD_ENV]: "1" };
|
|
43
|
+
}
|
|
44
|
+
export function defaultConfig() {
|
|
45
|
+
return { version: CONFIG_VERSION, provider: null, autoSummarize: false };
|
|
46
|
+
}
|
|
47
|
+
/** Parse a config blob. Returns null for anything we cannot trust. */
|
|
48
|
+
export function parseConfig(raw) {
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(raw);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
57
|
+
return null;
|
|
58
|
+
const object = parsed;
|
|
59
|
+
// A config from a future version may mean things we'd misread. Ignoring it is
|
|
60
|
+
// safe; guessing is not.
|
|
61
|
+
const version = object["version"];
|
|
62
|
+
if (typeof version !== "number" || version > CONFIG_VERSION)
|
|
63
|
+
return null;
|
|
64
|
+
// Every field must be present and well-typed, or the file is not one we
|
|
65
|
+
// wrote and we do not get to guess at what it meant.
|
|
66
|
+
if (typeof object["autoSummarize"] !== "boolean")
|
|
67
|
+
return null;
|
|
68
|
+
if (!("provider" in object))
|
|
69
|
+
return null;
|
|
70
|
+
/**
|
|
71
|
+
* THE DISTINCTION THIS FUNCTION EXISTS FOR.
|
|
72
|
+
*
|
|
73
|
+
* An explicit `"provider": null` is a *decision* — "make no model calls" —
|
|
74
|
+
* and must be honored. A provider that is present but malformed is
|
|
75
|
+
* *corruption*, and must make the whole config untrusted, which resolves to
|
|
76
|
+
* autodetect.
|
|
77
|
+
*
|
|
78
|
+
* Collapsing the two means a truncated write, or a hand-edited file with a
|
|
79
|
+
* typo, silently disables every model-backed feature — permanently, since the
|
|
80
|
+
* file still exists and so the wizard never runs again. The user would see
|
|
81
|
+
* summaries stop and have nothing to look at but a config that reads fine.
|
|
82
|
+
*/
|
|
83
|
+
const raw_provider = object["provider"];
|
|
84
|
+
let provider = null;
|
|
85
|
+
if (raw_provider !== null) {
|
|
86
|
+
provider = parseProvider(raw_provider);
|
|
87
|
+
if (!provider)
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
// gmux is newer than the rest of GmConfig, so its absence — or a partial
|
|
91
|
+
// block from a version that wrote fewer fields — must not untrust the whole
|
|
92
|
+
// file. resolveGmuxConfig fills in whatever this leaves out.
|
|
93
|
+
const gmux = parseGmuxBlock(object["gmux"]);
|
|
94
|
+
return { version, provider, autoSummarize: object["autoSummarize"], ...(gmux ? { gmux } : {}) };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Parse the gmux block leniently: field-by-field, keeping whatever is
|
|
98
|
+
* well-typed and dropping the rest — never the whole block, and never the
|
|
99
|
+
* config it lives in. `resolveGmuxConfig` is what turns this partial result
|
|
100
|
+
* into a complete `GmuxConfig`.
|
|
101
|
+
*/
|
|
102
|
+
function parseGmuxBlock(value) {
|
|
103
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
104
|
+
return undefined;
|
|
105
|
+
const object = value;
|
|
106
|
+
const result = {};
|
|
107
|
+
if (object["guardianPolicy"] === "off" || object["guardianPolicy"] === "notify" || object["guardianPolicy"] === "auto") {
|
|
108
|
+
result.guardianPolicy = object["guardianPolicy"];
|
|
109
|
+
}
|
|
110
|
+
if (typeof object["memoryThreshold"] === "number")
|
|
111
|
+
result.memoryThreshold = object["memoryThreshold"];
|
|
112
|
+
if (typeof object["cooldownSeconds"] === "number")
|
|
113
|
+
result.cooldownSeconds = object["cooldownSeconds"];
|
|
114
|
+
if (typeof object["tickMs"] === "number")
|
|
115
|
+
result.tickMs = object["tickMs"];
|
|
116
|
+
if (Object.keys(result).length === 0)
|
|
117
|
+
return undefined;
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Fill in whatever a stored `gmux` block leaves out, from `DEFAULT_GMUX_CONFIG`.
|
|
122
|
+
*
|
|
123
|
+
* Never fails on a missing or partial block — older configs have none at all,
|
|
124
|
+
* and `parseGmuxBlock` above only ever keeps well-typed fields, so what
|
|
125
|
+
* reaches here is trustworthy as far as it goes.
|
|
126
|
+
*/
|
|
127
|
+
export function resolveGmuxConfig(config) {
|
|
128
|
+
const g = config?.gmux;
|
|
129
|
+
if (!g)
|
|
130
|
+
return { ...DEFAULT_GMUX_CONFIG };
|
|
131
|
+
const policy = g.guardianPolicy === "off" || g.guardianPolicy === "notify" || g.guardianPolicy === "auto"
|
|
132
|
+
? g.guardianPolicy
|
|
133
|
+
: DEFAULT_GMUX_CONFIG.guardianPolicy;
|
|
134
|
+
return {
|
|
135
|
+
guardianPolicy: policy,
|
|
136
|
+
memoryThreshold: typeof g.memoryThreshold === "number" ? g.memoryThreshold : DEFAULT_GMUX_CONFIG.memoryThreshold,
|
|
137
|
+
cooldownSeconds: typeof g.cooldownSeconds === "number" ? g.cooldownSeconds : DEFAULT_GMUX_CONFIG.cooldownSeconds,
|
|
138
|
+
tickMs: typeof g.tickMs === "number" ? g.tickMs : DEFAULT_GMUX_CONFIG.tickMs,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Parse a provider. Null means malformed — NOT "no provider".
|
|
143
|
+
*
|
|
144
|
+
* The caller turns a null here into an untrusted config. A command containing
|
|
145
|
+
* anything that is not a string is rejected rather than filtered: dropping the
|
|
146
|
+
* bad part would spawn a *different* command than the one on disk, which is a
|
|
147
|
+
* worse outcome than declining to spawn anything.
|
|
148
|
+
*/
|
|
149
|
+
function parseProvider(value) {
|
|
150
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
151
|
+
return null;
|
|
152
|
+
const object = value;
|
|
153
|
+
const id = object["id"];
|
|
154
|
+
const command = object["command"];
|
|
155
|
+
if (typeof id !== "string" || id.trim() === "")
|
|
156
|
+
return null;
|
|
157
|
+
if (!Array.isArray(command) || command.length === 0)
|
|
158
|
+
return null;
|
|
159
|
+
if (!command.every((part) => typeof part === "string"))
|
|
160
|
+
return null;
|
|
161
|
+
return { id, command: [...command] };
|
|
162
|
+
}
|
|
163
|
+
/** The config on disk, or null when there isn't a usable one. */
|
|
164
|
+
export async function readConfig() {
|
|
165
|
+
try {
|
|
166
|
+
return parseConfig(await readFile(configPath(), "utf8"));
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return null; // Absent, unreadable — same answer either way.
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* True when a config file exists, however broken.
|
|
174
|
+
*
|
|
175
|
+
* Distinct from `readConfig() !== null` on purpose: the first-run wizard keys
|
|
176
|
+
* off *existence*, so a corrupt file doesn't re-launch the wizard on every
|
|
177
|
+
* single run. It falls back to defaults quietly instead, and `gmux doctor` says so.
|
|
178
|
+
*/
|
|
179
|
+
export async function configExists() {
|
|
180
|
+
try {
|
|
181
|
+
await readFile(configPath(), "utf8");
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/** Write config through a temp file and a rename, so a kill can't truncate it. */
|
|
189
|
+
export async function writeConfig(config) {
|
|
190
|
+
const path = configPath();
|
|
191
|
+
await mkdir(dirname(path), { recursive: true });
|
|
192
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
193
|
+
await writeFile(temp, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
194
|
+
await rename(temp, path);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Split a command string into argv, honoring quotes and backslash escapes.
|
|
198
|
+
*
|
|
199
|
+
* Splitting on whitespace alone was wrong in a way that fails quietly: a
|
|
200
|
+
* perfectly reasonable `llm -m "model name"` became four arguments with literal
|
|
201
|
+
* quote characters embedded — `["llm","-m","\"model","name\""]` — and gmux then
|
|
202
|
+
* spawned a command the user never typed. The wizard invites free-form input, so
|
|
203
|
+
* "don't type spaces in your arguments" is not a rule we get to have.
|
|
204
|
+
*
|
|
205
|
+
* This is not a shell. It handles the quoting a person actually uses in an
|
|
206
|
+
* answer to a prompt — single quotes, double quotes, backslash escapes — and
|
|
207
|
+
* deliberately does NOT do expansion, substitution, globbing or operators. The
|
|
208
|
+
* argv goes to `spawn` without a shell, so there is nothing for those to mean,
|
|
209
|
+
* and pretending otherwise would invite an injection surface where none exists.
|
|
210
|
+
*
|
|
211
|
+
* An unterminated quote closes at end of input rather than throwing: the user is
|
|
212
|
+
* standing at a prompt, and taking their obvious intent beats an error message
|
|
213
|
+
* about lexing.
|
|
214
|
+
*/
|
|
215
|
+
export function parseCommand(input) {
|
|
216
|
+
const argv = [];
|
|
217
|
+
let current = "";
|
|
218
|
+
let started = false; // Distinguishes a real empty argument ("") from no argument.
|
|
219
|
+
let quote = null;
|
|
220
|
+
for (let i = 0; i < input.length; i++) {
|
|
221
|
+
const char = input[i];
|
|
222
|
+
if (quote === null && (char === " " || char === "\t" || char === "\n")) {
|
|
223
|
+
if (started)
|
|
224
|
+
argv.push(current);
|
|
225
|
+
current = "";
|
|
226
|
+
started = false;
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
// Backslash escapes the next character, except inside single quotes — where
|
|
230
|
+
// a backslash is a literal backslash, as in every POSIX shell.
|
|
231
|
+
if (char === "\\" && quote !== "'" && i + 1 < input.length) {
|
|
232
|
+
current += input[++i];
|
|
233
|
+
started = true;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (quote === null && (char === '"' || char === "'")) {
|
|
237
|
+
quote = char;
|
|
238
|
+
started = true; // `gmux ask ""` is an empty argument, not an absent one.
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (char === quote) {
|
|
242
|
+
quote = null;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
current += char;
|
|
246
|
+
started = true;
|
|
247
|
+
}
|
|
248
|
+
if (started)
|
|
249
|
+
argv.push(current);
|
|
250
|
+
return argv;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The command to use for summaries, or null for "make no model calls".
|
|
254
|
+
*
|
|
255
|
+
* Pure, and takes `detected` rather than probing PATH itself, so the precedence
|
|
256
|
+
* rule can be tested without a machine that happens to have Claude installed.
|
|
257
|
+
*
|
|
258
|
+
* Highest wins:
|
|
259
|
+
* 1. GMUX_SUMMARY_CMD
|
|
260
|
+
* 2. config.provider.command
|
|
261
|
+
* 3. first detected provider
|
|
262
|
+
* 4. `claude -p`
|
|
263
|
+
*
|
|
264
|
+
* Rules 3 and 4 are the pre-config behavior, preserved exactly: someone with no
|
|
265
|
+
* config file sees no change at all.
|
|
266
|
+
*/
|
|
267
|
+
export function resolveSummaryCommand(config, env = process.env, detected = firstDetected()) {
|
|
268
|
+
const override = env[SUMMARY_CMD_ENV];
|
|
269
|
+
if (override && override.trim() !== "")
|
|
270
|
+
return parseCommand(override);
|
|
271
|
+
// Only an existing config can say "no": absent config means "not asked yet",
|
|
272
|
+
// which falls through to autodetect rather than to silence. A known provider
|
|
273
|
+
// re-derives from the catalog (summaryArgvFor), so a flag added after setup
|
|
274
|
+
// reaches it — the same self-healing resolveAskCommand already has.
|
|
275
|
+
if (config)
|
|
276
|
+
return config.provider ? summaryArgvFor(config.provider) : null;
|
|
277
|
+
if (detected)
|
|
278
|
+
return [...detected.summaryArgv];
|
|
279
|
+
return [...FALLBACK_COMMAND];
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* The command to use for `gmux ask`, or null for "make no model calls".
|
|
283
|
+
*
|
|
284
|
+
* Same precedence, but the catalog's `askArgv` is used where we recognise the
|
|
285
|
+
* provider — that's the variant permitted to run `gmux grep`.
|
|
286
|
+
*/
|
|
287
|
+
export function resolveAskCommand(config, env = process.env, detected = firstDetected()) {
|
|
288
|
+
const override = env[SUMMARY_CMD_ENV];
|
|
289
|
+
if (override && override.trim() !== "")
|
|
290
|
+
return parseCommand(override);
|
|
291
|
+
if (config)
|
|
292
|
+
return config.provider ? askArgvFor(config.provider) : null;
|
|
293
|
+
if (detected)
|
|
294
|
+
return [...detected.askArgv];
|
|
295
|
+
return [...FALLBACK_COMMAND];
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Whether background summaries are allowed.
|
|
299
|
+
*
|
|
300
|
+
* `GMUX_AUTO_SUMMARIZE=0` is checked by the caller and still wins. This
|
|
301
|
+
* adds the config answer: a user who said "no" in the wizard has said it once
|
|
302
|
+
* and for good, and must not be asked again by a token spend they declined.
|
|
303
|
+
*
|
|
304
|
+
* No config means yes — the pre-config default.
|
|
305
|
+
*/
|
|
306
|
+
export function autoSummarizeAllowed(config) {
|
|
307
|
+
return config ? config.autoSummarize : true;
|
|
308
|
+
}
|
|
309
|
+
export function shouldRunSetupWizard(gate) {
|
|
310
|
+
if (gate.hasConfig)
|
|
311
|
+
return false;
|
|
312
|
+
if (!gate.isTty)
|
|
313
|
+
return false;
|
|
314
|
+
if (gate.isJson)
|
|
315
|
+
return false;
|
|
316
|
+
// `__`-prefixed commands are the hidden internals we spawn at ourselves.
|
|
317
|
+
// Neither has a human at the other end, and `__picker-rows` runs while fzf
|
|
318
|
+
// owns the terminal — a prompt there would corrupt the display.
|
|
319
|
+
if (gate.commandName.startsWith("__"))
|
|
320
|
+
return false;
|
|
321
|
+
if (gate.commandName === "setup")
|
|
322
|
+
return false;
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/services/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { WorkspaceSnapshot } from "../core/gmux-types.js";
|
|
2
|
+
export declare function readSnapshotFile(path?: string): Promise<{
|
|
3
|
+
snapshot: WorkspaceSnapshot;
|
|
4
|
+
ageMs: number;
|
|
5
|
+
} | null>;
|
|
6
|
+
export declare function subscribe(onSnapshot: (s: WorkspaceSnapshot) => void, opts?: {
|
|
7
|
+
socketPath?: string;
|
|
8
|
+
onError?: (e: Error) => void;
|
|
9
|
+
}): () => void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { connect } from "node:net";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { gmuxSnapshotPath, gmuxSocketPath } from "../core/paths.js";
|
|
4
|
+
export async function readSnapshotFile(path = gmuxSnapshotPath()) {
|
|
5
|
+
try {
|
|
6
|
+
const [raw, st] = await Promise.all([readFile(path, "utf8"), stat(path)]);
|
|
7
|
+
const snapshot = JSON.parse(raw);
|
|
8
|
+
return { snapshot, ageMs: Math.max(0, Date.now() - st.mtimeMs) };
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function subscribe(onSnapshot, opts = {}) {
|
|
15
|
+
const sock = connect(opts.socketPath ?? gmuxSocketPath());
|
|
16
|
+
let buf = "";
|
|
17
|
+
sock.on("data", (d) => {
|
|
18
|
+
buf += d.toString();
|
|
19
|
+
let nl;
|
|
20
|
+
while ((nl = buf.indexOf("\n")) >= 0) {
|
|
21
|
+
const line = buf.slice(0, nl);
|
|
22
|
+
buf = buf.slice(nl + 1);
|
|
23
|
+
if (line.trim()) {
|
|
24
|
+
try {
|
|
25
|
+
onSnapshot(JSON.parse(line));
|
|
26
|
+
}
|
|
27
|
+
catch { /* skip */ }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
sock.on("error", (e) => opts.onError?.(e));
|
|
32
|
+
return () => sock.destroy();
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=daemon-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-client.js","sourceRoot":"","sources":["../../src/services/daemon-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAElD,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAEpE,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAe,gBAAgB,EAAE;IAEjC,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC1E,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAsB,CAAC;QACtD,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IACnE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS,CACvB,UAA0C,EAC1C,OAA8D,EAAE;IAEhE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,UAAU,IAAI,cAAc,EAAE,CAAC,CAAC;IAC1D,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,EAAE;QACpB,GAAG,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;QACpB,IAAI,EAAU,CAAC;QACf,OAAO,CAAC,EAAE,GAAG,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YACrC,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAC9B,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;YACxB,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;gBAAC,IAAI,CAAC;oBAAC,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAsB,CAAC,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,UAAU,CAAC,CAAC;YAAC,CAAC;QACtG,CAAC;IACH,CAAC,CAAC,CAAC;IACH,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3C,OAAO,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;AAC9B,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { WorkspaceModel } from "./workspace.js";
|
|
2
|
+
export declare class ModelServer {
|
|
3
|
+
private readonly model;
|
|
4
|
+
private readonly socketPath;
|
|
5
|
+
private readonly snapshotPath;
|
|
6
|
+
private server;
|
|
7
|
+
private clients;
|
|
8
|
+
private readonly onChange;
|
|
9
|
+
/** Serializes snapshot-file writes so overlapping "change" bursts never
|
|
10
|
+
* race on the same temp path (one write's `rename` could otherwise yank
|
|
11
|
+
* the temp file out from under another still-in-flight write). */
|
|
12
|
+
private writeChain;
|
|
13
|
+
private writeCounter;
|
|
14
|
+
constructor(model: WorkspaceModel, socketPath?: string, snapshotPath?: string);
|
|
15
|
+
start(): Promise<void>;
|
|
16
|
+
private line;
|
|
17
|
+
private broadcast;
|
|
18
|
+
/**
|
|
19
|
+
* Serialized through `writeChain` so a burst of near-simultaneous "change"
|
|
20
|
+
* events (e.g. N `upsertIdentity` calls during an initial scan) never has
|
|
21
|
+
* two writes in flight at once — an overlapping `rename()` could otherwise
|
|
22
|
+
* yank the shared temp file out from under a sibling write (ENOENT) and
|
|
23
|
+
* crash the process via an unhandled rejection. Each call also gets its
|
|
24
|
+
* own temp filename as belt-and-suspenders. Never rejects: a failed write
|
|
25
|
+
* is swallowed rather than surfaced, so it can't become an unhandled
|
|
26
|
+
* rejection further up the chain either.
|
|
27
|
+
*/
|
|
28
|
+
private writeSnapshot;
|
|
29
|
+
stop(): Promise<void>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// src/services/daemon-socket.ts
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
import { mkdir, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
import { gmuxSnapshotPath, gmuxSocketPath } from "../core/paths.js";
|
|
6
|
+
export class ModelServer {
|
|
7
|
+
model;
|
|
8
|
+
socketPath;
|
|
9
|
+
snapshotPath;
|
|
10
|
+
server;
|
|
11
|
+
clients = new Set();
|
|
12
|
+
onChange = () => { void this.broadcast().catch(() => { }); };
|
|
13
|
+
/** Serializes snapshot-file writes so overlapping "change" bursts never
|
|
14
|
+
* race on the same temp path (one write's `rename` could otherwise yank
|
|
15
|
+
* the temp file out from under another still-in-flight write). */
|
|
16
|
+
writeChain = Promise.resolve();
|
|
17
|
+
writeCounter = 0;
|
|
18
|
+
constructor(model, socketPath = gmuxSocketPath(), snapshotPath = gmuxSnapshotPath()) {
|
|
19
|
+
this.model = model;
|
|
20
|
+
this.socketPath = socketPath;
|
|
21
|
+
this.snapshotPath = snapshotPath;
|
|
22
|
+
}
|
|
23
|
+
async start() {
|
|
24
|
+
await mkdir(dirname(this.socketPath), { recursive: true });
|
|
25
|
+
await rm(this.socketPath, { force: true });
|
|
26
|
+
this.server = createServer((sock) => {
|
|
27
|
+
this.clients.add(sock);
|
|
28
|
+
sock.on("close", () => this.clients.delete(sock));
|
|
29
|
+
sock.on("error", () => this.clients.delete(sock));
|
|
30
|
+
sock.write(this.line()); // initial snapshot
|
|
31
|
+
});
|
|
32
|
+
await new Promise((resolve, reject) => {
|
|
33
|
+
this.server.once("error", reject);
|
|
34
|
+
this.server.listen(this.socketPath, resolve);
|
|
35
|
+
});
|
|
36
|
+
this.model.on("change", this.onChange);
|
|
37
|
+
await this.writeSnapshot(this.line());
|
|
38
|
+
}
|
|
39
|
+
line() { return JSON.stringify(this.model.snapshot()) + "\n"; }
|
|
40
|
+
async broadcast() {
|
|
41
|
+
const line = this.line();
|
|
42
|
+
// Write the snapshot file first so it never lags behind what clients see
|
|
43
|
+
// (also avoids a race where a socket read outpaces the async fs write).
|
|
44
|
+
await this.writeSnapshot(line);
|
|
45
|
+
for (const c of this.clients)
|
|
46
|
+
c.write(line);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Serialized through `writeChain` so a burst of near-simultaneous "change"
|
|
50
|
+
* events (e.g. N `upsertIdentity` calls during an initial scan) never has
|
|
51
|
+
* two writes in flight at once — an overlapping `rename()` could otherwise
|
|
52
|
+
* yank the shared temp file out from under a sibling write (ENOENT) and
|
|
53
|
+
* crash the process via an unhandled rejection. Each call also gets its
|
|
54
|
+
* own temp filename as belt-and-suspenders. Never rejects: a failed write
|
|
55
|
+
* is swallowed rather than surfaced, so it can't become an unhandled
|
|
56
|
+
* rejection further up the chain either.
|
|
57
|
+
*/
|
|
58
|
+
async writeSnapshot(content) {
|
|
59
|
+
const tmp = `${this.snapshotPath}.${process.pid}.${this.writeCounter++}.tmp`;
|
|
60
|
+
this.writeChain = this.writeChain.then(async () => {
|
|
61
|
+
await mkdir(dirname(this.snapshotPath), { recursive: true });
|
|
62
|
+
await writeFile(tmp, content, "utf8");
|
|
63
|
+
await rename(tmp, this.snapshotPath);
|
|
64
|
+
}).catch(() => { });
|
|
65
|
+
await this.writeChain;
|
|
66
|
+
}
|
|
67
|
+
async stop() {
|
|
68
|
+
this.model.off("change", this.onChange);
|
|
69
|
+
for (const c of this.clients)
|
|
70
|
+
c.destroy();
|
|
71
|
+
this.clients.clear();
|
|
72
|
+
await new Promise((resolve) => (this.server ? this.server.close(() => resolve()) : resolve()));
|
|
73
|
+
await rm(this.socketPath, { force: true });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=daemon-socket.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"daemon-socket.js","sourceRoot":"","sources":["../../src/services/daemon-socket.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The daemon tick loop: registry diff → attach/detach sensors → observe →
|
|
3
|
+
* classify → write state to the model → evict gone panes.
|
|
4
|
+
*
|
|
5
|
+
* No LLM here (Phase 0) — `classifyState` is a pure heuristic over the raw
|
|
6
|
+
* `Observation`. Sensors and the clock are injected so the whole loop can be
|
|
7
|
+
* driven headlessly in tests, without a real tmux server.
|
|
8
|
+
*/
|
|
9
|
+
import type { PaneIdentity } from "../core/gmux-types.js";
|
|
10
|
+
import type { TmuxGateway } from "./tmux-gateway.js";
|
|
11
|
+
import type { Guardian } from "./guardian.js";
|
|
12
|
+
import type { ResourceMonitor } from "./resources.js";
|
|
13
|
+
import { type Sensor } from "./sensors.js";
|
|
14
|
+
import type { SemanticWorker } from "./semantic.js";
|
|
15
|
+
import type { WorkspaceModel } from "./workspace.js";
|
|
16
|
+
export interface DaemonDeps {
|
|
17
|
+
gateway: TmuxGateway;
|
|
18
|
+
model: WorkspaceModel;
|
|
19
|
+
now: () => number;
|
|
20
|
+
makeSensor?: (id: PaneIdentity, gw: TmuxGateway) => Sensor;
|
|
21
|
+
semantic?: SemanticWorker;
|
|
22
|
+
resources?: ResourceMonitor;
|
|
23
|
+
guardian?: Guardian;
|
|
24
|
+
/**
|
|
25
|
+
* Overrides which present panes count as "agent panes" for guardian
|
|
26
|
+
* broadcast targeting. Defaults to panes the registry has resolved to a
|
|
27
|
+
* harness + session (`harness && sessionId`). This override exists only
|
|
28
|
+
* so tests can mark panes as agents without a full link fixture — real
|
|
29
|
+
* callers should leave it unset.
|
|
30
|
+
*/
|
|
31
|
+
resolveAgents?: (present: PaneIdentity[]) => string[];
|
|
32
|
+
}
|
|
33
|
+
export declare class Daemon {
|
|
34
|
+
private readonly deps;
|
|
35
|
+
private registry;
|
|
36
|
+
/** Long-lived per pane: created on appear, torn down on vanish. This is
|
|
37
|
+
* what lets AgentSensor's lazy transcript-file resolution pay off over
|
|
38
|
+
* the pane's whole lifetime instead of retrying from scratch each tick. */
|
|
39
|
+
private sensors;
|
|
40
|
+
private make;
|
|
41
|
+
constructor(deps: DaemonDeps);
|
|
42
|
+
tickOnce(): Promise<void>;
|
|
43
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The daemon tick loop: registry diff → attach/detach sensors → observe →
|
|
3
|
+
* classify → write state to the model → evict gone panes.
|
|
4
|
+
*
|
|
5
|
+
* No LLM here (Phase 0) — `classifyState` is a pure heuristic over the raw
|
|
6
|
+
* `Observation`. Sensors and the clock are injected so the whole loop can be
|
|
7
|
+
* driven headlessly in tests, without a real tmux server.
|
|
8
|
+
*/
|
|
9
|
+
import { classifyState } from "../core/pane-state.js";
|
|
10
|
+
import { PaneRegistry } from "./pane-registry.js";
|
|
11
|
+
import { makeSensor as defaultMakeSensor } from "./sensors.js";
|
|
12
|
+
export class Daemon {
|
|
13
|
+
deps;
|
|
14
|
+
registry;
|
|
15
|
+
/** Long-lived per pane: created on appear, torn down on vanish. This is
|
|
16
|
+
* what lets AgentSensor's lazy transcript-file resolution pay off over
|
|
17
|
+
* the pane's whole lifetime instead of retrying from scratch each tick. */
|
|
18
|
+
sensors = new Map();
|
|
19
|
+
make;
|
|
20
|
+
constructor(deps) {
|
|
21
|
+
this.deps = deps;
|
|
22
|
+
this.registry = new PaneRegistry(deps.gateway);
|
|
23
|
+
this.make = deps.makeSensor ?? defaultMakeSensor;
|
|
24
|
+
}
|
|
25
|
+
async tickOnce() {
|
|
26
|
+
const now = this.deps.now();
|
|
27
|
+
let diff;
|
|
28
|
+
try {
|
|
29
|
+
diff = await this.registry.diff();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// tmux hiccup (e.g. listPanes throws): idle this tick, loop survives.
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const { present, vanished } = diff;
|
|
36
|
+
for (const id of vanished) {
|
|
37
|
+
await this.sensors.get(id)?.teardown().catch(() => { });
|
|
38
|
+
this.sensors.delete(id);
|
|
39
|
+
this.deps.model.markGone(id);
|
|
40
|
+
}
|
|
41
|
+
for (const id of present) {
|
|
42
|
+
this.deps.model.upsertIdentity(id);
|
|
43
|
+
let sensor = this.sensors.get(id.paneId);
|
|
44
|
+
if (!sensor) {
|
|
45
|
+
sensor = this.make(id, this.deps.gateway);
|
|
46
|
+
this.sensors.set(id.paneId, sensor);
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
const obs = await sensor.observe(now);
|
|
50
|
+
this.deps.model.applyState(id.paneId, classifyState(obs, now), obs.lastActivityTs, now);
|
|
51
|
+
if (this.deps.semantic) {
|
|
52
|
+
const entry = this.deps.model.snapshot().panes.find((p) => p.identity.paneId === id.paneId);
|
|
53
|
+
if (entry)
|
|
54
|
+
this.deps.semantic.enqueue(entry, obs, now); // fire-and-forget, never awaited
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// A single sensor failure must never abort the whole tick.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (this.deps.resources) {
|
|
62
|
+
try {
|
|
63
|
+
const panePids = new Map(present.map((p) => [p.paneId, p.pid]));
|
|
64
|
+
const { perPane, host } = await this.deps.resources.sample(panePids);
|
|
65
|
+
for (const [paneId, res] of perPane)
|
|
66
|
+
this.deps.model.applyResources(paneId, res);
|
|
67
|
+
this.deps.model.setHostPressure(host);
|
|
68
|
+
if (this.deps.guardian) {
|
|
69
|
+
const agentIds = this.deps.resolveAgents?.(present)
|
|
70
|
+
?? present.filter((p) => p.harness && p.sessionId).map((p) => p.paneId);
|
|
71
|
+
const agentIdSet = new Set(agentIds);
|
|
72
|
+
// Guardian.decide treats a pane as an agent via identity.harness &&
|
|
73
|
+
// identity.sessionId. `resolveAgents` is a test-only override, so
|
|
74
|
+
// when it's set, patch those fields (decision purposes only, never
|
|
75
|
+
// persisted to the model) so the guardian agrees with the override.
|
|
76
|
+
const panes = this.deps.model.snapshot().panes.map((p) => {
|
|
77
|
+
if (!this.deps.resolveAgents || !agentIdSet.has(p.identity.paneId) || p.identity.harness)
|
|
78
|
+
return p;
|
|
79
|
+
return { ...p, identity: { ...p.identity, harness: "test-agent", sessionId: p.identity.paneId } };
|
|
80
|
+
});
|
|
81
|
+
const decision = this.deps.guardian.decide(host, panes, now);
|
|
82
|
+
if (decision.action !== "none") {
|
|
83
|
+
if (decision.action === "broadcast") {
|
|
84
|
+
for (const id of agentIds) {
|
|
85
|
+
await this.deps.gateway.send(id, decision.message + "\n").catch(() => { });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
this.deps.model.logGuardian({
|
|
89
|
+
ts: now,
|
|
90
|
+
pressure: host.usedRatio,
|
|
91
|
+
culpritPaneId: decision.culpritPaneId,
|
|
92
|
+
culpritLabel: decision.culpritLabel,
|
|
93
|
+
action: decision.action,
|
|
94
|
+
message: decision.message,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Resource sampling / guardian failures must never abort the tick.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
this.deps.model.evictGone();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=daemon.js.map
|