baychat 0.23.1 → 0.25.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.
Files changed (39) hide show
  1. package/README.md +120 -15
  2. package/dist/api.js +19 -2
  3. package/dist/claude-context-flush.js +219 -0
  4. package/dist/claude-context-state.js +191 -0
  5. package/dist/claude-context.js +311 -0
  6. package/dist/claude-settings.js +503 -0
  7. package/dist/codex-onboarding.js +33 -23
  8. package/dist/context-hooks-command.js +165 -0
  9. package/dist/context-report.js +307 -0
  10. package/dist/index.js +26 -497
  11. package/dist/main.js +511 -0
  12. package/dist/mcp-notes.js +110 -0
  13. package/dist/mcp.js +84 -5
  14. package/dist/protocol-content.js +1 -1
  15. package/dist/relay/acp/client.js +18 -0
  16. package/dist/relay/acp/prompt.js +54 -3
  17. package/dist/relay/acp/runner.js +100 -2
  18. package/dist/relay/acp/turn-queue.js +99 -3
  19. package/dist/relay/codex-context.js +242 -0
  20. package/dist/relay/compact-command.js +63 -0
  21. package/dist/relay/compact-focus.attacks.js +36 -0
  22. package/dist/relay/compact-focus.js +64 -0
  23. package/dist/relay/context-notice.js +144 -0
  24. package/dist/relay/daemon.js +429 -73
  25. package/dist/relay/fresh-start-verify.js +237 -0
  26. package/dist/relay/fresh-start.js +141 -0
  27. package/dist/relay/message-format.js +34 -15
  28. package/dist/relay/project-brief.js +195 -0
  29. package/dist/relay/project-notes.js +300 -0
  30. package/dist/relay/resume.js +25 -0
  31. package/dist/relay/server-registration.js +40 -0
  32. package/dist/relay/session-commands.js +2 -0
  33. package/dist/relay/terminal-text.js +13 -0
  34. package/dist/relay/transport.js +2 -1
  35. package/dist/relay/updates.js +46 -8
  36. package/dist/relay/ws.js +49 -6
  37. package/dist/runtimes.js +24 -6
  38. package/dist/tool-defs.js +93 -0
  39. package/package.json +1 -1
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ // `baychat context-hooks install | uninstall | status` — the person's switch for Claude Code context
3
+ // reporting. Nothing is ever installed without this command being run (see claude-settings.ts).
4
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
5
+ if (k2 === undefined) k2 = k;
6
+ var desc = Object.getOwnPropertyDescriptor(m, k);
7
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
8
+ desc = { enumerable: true, get: function() { return m[k]; } };
9
+ }
10
+ Object.defineProperty(o, k2, desc);
11
+ }) : (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ o[k2] = m[k];
14
+ }));
15
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
16
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
17
+ }) : function(o, v) {
18
+ o["default"] = v;
19
+ });
20
+ var __importStar = (this && this.__importStar) || (function () {
21
+ var ownKeys = function(o) {
22
+ ownKeys = Object.getOwnPropertyNames || function (o) {
23
+ var ar = [];
24
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
25
+ return ar;
26
+ };
27
+ return ownKeys(o);
28
+ };
29
+ return function (mod) {
30
+ if (mod && mod.__esModule) return mod;
31
+ var result = {};
32
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
33
+ __setModuleDefault(result, mod);
34
+ return result;
35
+ };
36
+ })();
37
+ Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.currentLaunch = currentLaunch;
39
+ exports.launchProblem = launchProblem;
40
+ exports.cmdContextHooks = cmdContextHooks;
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const claude_context_1 = require("./claude-context");
44
+ const claude_settings_1 = require("./claude-settings");
45
+ const config_1 = require("./config");
46
+ const USAGE = "Usage: baychat context-hooks <install|uninstall|status>";
47
+ /**
48
+ * This very CLI, by absolute path: the node running it and the entry file it was started from.
49
+ * Symlinks resolved, so an npm bin link or an nvm alias cannot move under it later.
50
+ */
51
+ function currentLaunch() {
52
+ return { node: fs.realpathSync(process.execPath), entry: fs.realpathSync(process.argv[1] ?? "") };
53
+ }
54
+ /** Why this launch cannot be written into settings.json, or undefined when it can. */
55
+ function launchProblem(launch) {
56
+ if (/[\\/]_npx[\\/]/.test(launch.entry)) {
57
+ return "This is a temporary `npx` copy of BayChat, which npm deletes on its own — your status line would lose BayChat without warning. Install it for good (`npm install --global baychat`), then run `baychat context-hooks install` from that.";
58
+ }
59
+ if (!fs.existsSync(launch.entry))
60
+ return `BayChat's own entry file (${launch.entry}) could not be found.`;
61
+ return undefined;
62
+ }
63
+ function describe(state) {
64
+ const line = state.statusLine === "ours"
65
+ ? state.wrapped !== undefined
66
+ ? ` status line: BayChat reader, running your own command: ${state.wrapped}`
67
+ : " status line: BayChat reader (you had no status line of your own)"
68
+ : state.statusLine === "theirs"
69
+ ? " status line: yours, not wrapped — context is not being read"
70
+ : " status line: none";
71
+ return [
72
+ line,
73
+ ` PreCompact hook: ${state.hooks.PreCompact ? "installed" : "not installed"}`,
74
+ ` PostCompact hook: ${state.hooks.PostCompact ? "installed" : "not installed"}`,
75
+ ];
76
+ }
77
+ async function cmdContextHooks(args, deps = {}) {
78
+ const out = deps.out ?? ((l) => console.log(l));
79
+ const err = deps.err ?? ((l) => console.error(l));
80
+ const file = deps.settingsFile ?? (0, claude_settings_1.claudeSettingsPath)();
81
+ const sub = args[0];
82
+ if (sub === "status") {
83
+ const state = (0, claude_settings_1.readClaudeSettingsState)(file);
84
+ if ("error" in state) {
85
+ err(state.error);
86
+ return 1;
87
+ }
88
+ out(`Claude Code context reporting (${file}):`);
89
+ for (const l of describe(state))
90
+ out(l);
91
+ out(` BayChat login: ${(0, config_1.loadDeviceCredentials)() ? "yes" : "no — run `baychat login`"}`);
92
+ const until = (0, claude_context_1.backoffUntil)();
93
+ if (until > Date.now())
94
+ out(` server: does not accept context reports yet — next try after ${new Date(until).toLocaleTimeString()}`);
95
+ out(` problems, if any, are logged in ${path.join((0, claude_context_1.contextDir)(), "errors.log")}`);
96
+ return 0;
97
+ }
98
+ if (sub === "install") {
99
+ const platform = deps.platform ?? process.platform;
100
+ if (platform === "win32") {
101
+ // Claude Code runs a status line through Git Bash or PowerShell on Windows, and the wrapper
102
+ // re-runs yours through Node's shell (cmd.exe). Until that is proven to give the same
103
+ // output, a broken status line is too high a price.
104
+ err("Not on Windows yet: BayChat could not promise your status line would look exactly the same. Nothing was changed.");
105
+ return 1;
106
+ }
107
+ const launch = (deps.launch ?? currentLaunch)();
108
+ const problem = launchProblem(launch);
109
+ if (problem) {
110
+ err(`${problem} Nothing was changed.`);
111
+ return 1;
112
+ }
113
+ let result;
114
+ try {
115
+ result = (0, claude_settings_1.editClaudeSettings)(file, (settings) => (0, claude_settings_1.installContextHooks)(settings, launch));
116
+ }
117
+ catch (e) {
118
+ err(e instanceof Error ? e.message : String(e));
119
+ return 1;
120
+ }
121
+ if (result.kind === "unchanged") {
122
+ out("Already installed — nothing changed.");
123
+ for (const l of describe(result.state))
124
+ out(l);
125
+ return 0;
126
+ }
127
+ out(`Installed in ${result.file}:`);
128
+ for (const l of describe(result.state))
129
+ out(l);
130
+ if (result.backup)
131
+ out(`Your previous settings are copied to ${result.backup}.`);
132
+ out("");
133
+ out("Your status line looks exactly as before: your own command still runs, in the same shell, and BayChat reads a copy of its input in the background.");
134
+ out(`BayChat is called by absolute path (${launch.node}, ${launch.entry}). If that ever goes away, your status line keeps working without it; run this again after moving Node or BayChat.`);
135
+ out("BayChat now sees how full each Claude Code session's context is, and when it tidies itself.");
136
+ out("A status line set in a project's .claude/settings.json takes precedence over this one there.");
137
+ if (!(0, config_1.loadDeviceCredentials)())
138
+ out("This computer is not logged in to BayChat yet — run `baychat login`, or nothing can be sent.");
139
+ out("To undo it: baychat context-hooks uninstall");
140
+ return 0;
141
+ }
142
+ if (sub === "uninstall") {
143
+ let result;
144
+ try {
145
+ result = (0, claude_settings_1.editClaudeSettings)(file, claude_settings_1.uninstallContextHooks, undefined, { restoreFromBackups: true });
146
+ }
147
+ catch (e) {
148
+ err(e instanceof Error ? e.message : String(e));
149
+ return 1;
150
+ }
151
+ if (result.kind === "unchanged") {
152
+ out("Not installed — nothing changed.");
153
+ return 0;
154
+ }
155
+ out(`Removed from ${result.file}. Your status line is your own command again${result.state.statusLine === "none" ? " (you had none)" : ""}.`);
156
+ out(result.restored === "exact"
157
+ ? "Restored byte-for-byte: the file is exactly as you had it before install, formatting included."
158
+ : "Restored semantically: the same settings you had (and any you changed since), but the file was re-formatted — your hand formatting is in the backups beside it.");
159
+ if (result.backup)
160
+ out(`The settings as they were a moment ago are copied to ${result.backup}. Backups are never overwritten; the first one (${(0, claude_settings_1.settingsBackupPath)(result.file)}) is your file from before BayChat ever touched it.`);
161
+ return 0;
162
+ }
163
+ err(USAGE);
164
+ return 1;
165
+ }
@@ -0,0 +1,307 @@
1
+ "use strict";
2
+ // How full is an attached agent's context — measured on this machine, reported to BayChat.
3
+ //
4
+ // Three sources feed this module, and each is the runtime's OWN number, never a guess:
5
+ //
6
+ // • Claude Code — the status-line JSON (`context_window.*`) and the PreCompact/PostCompact
7
+ // hooks, read by `claude-context.ts` in a process of its own.
8
+ // • Codex — the `token_count` events in the session's rollout JSONL, tailed by the relay
9
+ // (`relay/codex-context.ts`).
10
+ // • ACP agents (dsh, Cursor) — the standard `session/update` → `usage_update { used, size }`,
11
+ // which the relay used to throw away (`relay/acp/client.ts`).
12
+ //
13
+ // All three become ONE body (contract §7) and go out through ONE function. What this module
14
+ // guarantees, and what every caller relies on:
15
+ //
16
+ // 1. IT NEVER THROWS INTO A CALLER. A context report is a nicety riding beside the relay's
17
+ // real job (waking sessions); a failure here must never cost a delivery. Failures are
18
+ // LOGGED — never swallowed — and repeated failures are logged once, not once per sample.
19
+ // 2. IT IS CHEAP. "usage" is throttled: at most one every 15 s per session, sooner only when
20
+ // the fill moved by a whole percentage point. Compaction events are never throttled.
21
+ // 3. IT IS HARMLESS AGAINST A SERVER THAT DOES NOT HAVE THE ROUTE YET. A 404 with no error
22
+ // `code` is Express's "no such route" — phase 2 not deployed — and the reporter goes quiet
23
+ // for an hour instead of sending a doomed request every 15 seconds. A 404 WITH a code
24
+ // (`SESSION_NOT_FOUND`) is a real refusal about one session and backs off only that one.
25
+ //
26
+ // THE CREDENTIAL. The relay holds a DEVICE credential (`bay_u_…`); a session agent has no token
27
+ // of its own (REMOTE_MCP_SESSIONS.md: "An agent with a sessionKey has no apiToken"). So a device
28
+ // posts to the device surface and names the session, exactly as `/approvals` and the relay's
29
+ // command replies do; an agent token (a standing agent) posts to the agent surface as itself.
30
+ // Recorded in the contract's CHANGES, 2026-09-26.
31
+ Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.ContextReporter = exports.SESSION_REFUSED_BACKOFF_MS = exports.NOT_DEPLOYED_BACKOFF_MS = exports.USAGE_MIN_STEP_PERCENT = exports.USAGE_MIN_INTERVAL_MS = void 0;
33
+ exports.usageDue = usageDue;
34
+ exports.claudeUsageFromStatus = claudeUsageFromStatus;
35
+ exports.claudeAutoCompactAt = claudeAutoCompactAt;
36
+ exports.codexUsageFromTokenCount = codexUsageFromTokenCount;
37
+ exports.codexAutoCompactAt = codexAutoCompactAt;
38
+ exports.acpUsageFromUpdate = acpUsageFromUpdate;
39
+ exports.isDeviceCredential = isDeviceCredential;
40
+ exports.postContextEvent = postContextEvent;
41
+ const api_1 = require("./api");
42
+ // ─── Throttle ────────────────────────────────────────────────────────────────
43
+ exports.USAGE_MIN_INTERVAL_MS = 15_000;
44
+ exports.USAGE_MIN_STEP_PERCENT = 1;
45
+ /** How long a server without the route is left alone. */
46
+ exports.NOT_DEPLOYED_BACKOFF_MS = 60 * 60_000;
47
+ /** How long one session the server refused (ended, not live) is left alone. */
48
+ exports.SESSION_REFUSED_BACKOFF_MS = 5 * 60_000;
49
+ /**
50
+ * Is this usage sample worth a request?
51
+ *
52
+ * Yes when nothing was sent yet, when the window itself changed (a model switch), or when the
53
+ * fill moved by a whole percentage point. Otherwise yes only once 15 s have passed AND the
54
+ * number actually moved — a session sitting idle at 43 % sends nothing at all.
55
+ */
56
+ function usageDue(prev, next, now) {
57
+ if (!prev)
58
+ return true;
59
+ if (prev.size !== next.size || prev.autoCompactAt !== next.autoCompactAt)
60
+ return true;
61
+ const step = next.size > 0 ? (Math.abs(next.used - prev.used) / next.size) * 100 : 0;
62
+ if (step >= exports.USAGE_MIN_STEP_PERCENT)
63
+ return true;
64
+ return next.used !== prev.used && now - prev.at >= exports.USAGE_MIN_INTERVAL_MS;
65
+ }
66
+ // ─── Normalisers: one per source ─────────────────────────────────────────────
67
+ const isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
68
+ const count = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0 ? Math.round(v) : undefined;
69
+ /**
70
+ * Claude Code's status-line JSON → a sample, or null when it carries none yet.
71
+ *
72
+ * `current_usage` is the exact per-call count and is preferred; `used` is input + cache writes +
73
+ * cache reads — the same input-only formula Claude Code's own `used_percentage` uses (docs:
74
+ * "It does not include output_tokens"). It is null before the first API call and again right
75
+ * after `/compact`, and then `used_percentage` is the fallback. Neither → null, never a zero:
76
+ * "no number yet" and "empty" are different claims.
77
+ */
78
+ function claudeUsageFromStatus(status) {
79
+ if (!isRecord(status) || !isRecord(status.context_window))
80
+ return null;
81
+ const cw = status.context_window;
82
+ const size = count(cw.context_window_size);
83
+ if (!size)
84
+ return null;
85
+ const cu = cw.current_usage;
86
+ if (isRecord(cu)) {
87
+ const parts = [cu.input_tokens, cu.cache_creation_input_tokens, cu.cache_read_input_tokens].map(count);
88
+ if (parts.every((p) => p !== undefined))
89
+ return { used: parts.reduce((a, b) => a + b, 0), size };
90
+ }
91
+ const pct = typeof cw.used_percentage === "number" && Number.isFinite(cw.used_percentage) ? cw.used_percentage : undefined;
92
+ if (pct === undefined)
93
+ return null;
94
+ return { used: Math.round((Math.max(0, pct) / 100) * size), size };
95
+ }
96
+ /**
97
+ * Where Claude Code will compact on its own.
98
+ *
99
+ * Documented (model-config, "Default auto-compact thresholds"): an explicit window —
100
+ * `CLAUDE_CODE_AUTO_COMPACT_WINDOW`, else the `autoCompactWindow` setting — capped at the model's
101
+ * window; otherwise about 967K on a native 1M window and the window itself below that.
102
+ */
103
+ function claudeAutoCompactAt(size, override) {
104
+ if (override && override > 0)
105
+ return Math.min(Math.round(override), size);
106
+ return size >= 1_000_000 ? 967_000 : size;
107
+ }
108
+ /**
109
+ * A Codex rollout `event_msg` whose payload is `token_count` → a sample.
110
+ *
111
+ * `info.last_token_usage` is the LAST request — what is in the window now. `total_token_usage`
112
+ * is cumulative across the whole thread (13M on a real rollout against a 258K window) and must
113
+ * never be used; neither must `codex exec --json`'s totals, which are cumulative too.
114
+ */
115
+ function codexUsageFromTokenCount(payload) {
116
+ if (!isRecord(payload) || payload.type !== "token_count" || !isRecord(payload.info))
117
+ return null;
118
+ const last = payload.info.last_token_usage;
119
+ const size = count(payload.info.model_context_window);
120
+ if (!isRecord(last) || !size)
121
+ return null;
122
+ const total = count(last.total_tokens);
123
+ const used = total ?? (count(last.input_tokens) ?? 0) + (count(last.output_tokens) ?? 0);
124
+ return { used, size };
125
+ }
126
+ /** Codex compacts on its own at about 90 % of the window (contract §7). */
127
+ function codexAutoCompactAt(size) {
128
+ return Math.floor(size * 0.9);
129
+ }
130
+ /** ACP `usage_update { used, size }` → a sample. */
131
+ function acpUsageFromUpdate(update) {
132
+ if (!isRecord(update))
133
+ return null;
134
+ const used = count(update.used);
135
+ const size = count(update.size);
136
+ return used !== undefined && size ? { used, size } : null;
137
+ }
138
+ /** A device credential names the session; an agent token IS the agent. */
139
+ function isDeviceCredential(auth) {
140
+ return auth.token.startsWith("bay_u_");
141
+ }
142
+ /**
143
+ * POST one event. NEVER THROWS — every failure is an outcome.
144
+ *
145
+ * `session` is required on the device route (a device owns several sessions and the server must
146
+ * never pick one for us); on the agent route it is not sent at all.
147
+ */
148
+ async function postContextEvent(auth, session, event, request = api_1.apiRequest) {
149
+ try {
150
+ if (isDeviceCredential(auth)) {
151
+ if (!session)
152
+ return { kind: "refused", status: 0, message: "no session named for a device report" };
153
+ await request(auth, "POST", "/api/device-api/context", { session, ...event });
154
+ }
155
+ else {
156
+ await request(auth, "POST", "/api/agent-api/context", event);
157
+ }
158
+ return { kind: "sent" };
159
+ }
160
+ catch (err) {
161
+ if (err instanceof api_1.ApiError) {
162
+ // Express's own 404 carries no JSON envelope, so no code: the route does not exist here.
163
+ if (err.status === 404 && !err.code)
164
+ return { kind: "not-deployed" };
165
+ if (err.status >= 400 && err.status < 500 && err.status !== 429) {
166
+ return { kind: "refused", status: err.status, code: err.code, message: err.message };
167
+ }
168
+ }
169
+ return { kind: "failed", message: err instanceof Error ? err.message : String(err) };
170
+ }
171
+ }
172
+ /**
173
+ * Throttles, sends and backs off, per session, in the relay's process.
174
+ *
175
+ * `report` returns immediately and never throws; `idle()` resolves when everything queued has
176
+ * been attempted (tests, and a clean shutdown).
177
+ */
178
+ class ContextReporter {
179
+ deps;
180
+ sessions = new Map();
181
+ notDeployedUntil = 0;
182
+ closed = false;
183
+ constructor(deps) {
184
+ this.deps = deps;
185
+ }
186
+ now() {
187
+ return (this.deps.now ?? Date.now)();
188
+ }
189
+ state(session) {
190
+ let s = this.sessions.get(session);
191
+ if (!s) {
192
+ s = { chain: Promise.resolve() };
193
+ this.sessions.set(session, s);
194
+ }
195
+ return s;
196
+ }
197
+ report(session, event) {
198
+ try {
199
+ if (this.closed)
200
+ return;
201
+ const s = this.state(session);
202
+ if (event.event === "usage") {
203
+ if (event.used === undefined || event.size === undefined)
204
+ return;
205
+ const sample = { used: event.used, size: event.size, autoCompactAt: event.autoCompactAt };
206
+ if (!usageDue(s.sent, sample, this.now())) {
207
+ this.holdTrailing(session, s, event);
208
+ return;
209
+ }
210
+ s.timer?.cancel();
211
+ s.timer = undefined;
212
+ s.trailing = undefined;
213
+ // Counted as sent when it is QUEUED, so a burst of samples cannot all pass the throttle
214
+ // while the first request is still in flight.
215
+ s.sent = { ...sample, at: this.now() };
216
+ }
217
+ else {
218
+ // A compaction resets the baseline: the next usage is the "after" number and must go.
219
+ s.sent = undefined;
220
+ s.trailing = undefined;
221
+ s.timer?.cancel();
222
+ s.timer = undefined;
223
+ }
224
+ this.enqueue(session, s, event);
225
+ }
226
+ catch (err) {
227
+ this.deps.log(`context: could not queue a report for ${session}: ${errText(err)}`);
228
+ }
229
+ }
230
+ /** Resolves once every queued send has been attempted. */
231
+ async idle() {
232
+ await Promise.all([...this.sessions.values()].map((s) => s.chain));
233
+ }
234
+ close() {
235
+ this.closed = true;
236
+ for (const s of this.sessions.values())
237
+ s.timer?.cancel();
238
+ }
239
+ holdTrailing(session, s, event) {
240
+ s.trailing = event;
241
+ if (s.timer || !s.sent)
242
+ return;
243
+ const wait = Math.max(0, s.sent.at + exports.USAGE_MIN_INTERVAL_MS - this.now());
244
+ const setTimer = this.deps.setTimer ??
245
+ ((fn, ms) => {
246
+ const t = setTimeout(fn, ms);
247
+ t.unref?.();
248
+ return { cancel: () => clearTimeout(t) };
249
+ });
250
+ s.timer = setTimer(() => {
251
+ s.timer = undefined;
252
+ const held = s.trailing;
253
+ s.trailing = undefined;
254
+ // Re-run through `report` so the throttle is the one rule, not a second copy of it.
255
+ if (held)
256
+ this.report(session, held);
257
+ }, wait);
258
+ }
259
+ enqueue(session, s, event) {
260
+ s.chain = s.chain.then(() => this.send(session, s, event)).catch((err) => {
261
+ this.deps.log(`context: report for ${session} failed unexpectedly: ${errText(err)}`);
262
+ });
263
+ }
264
+ async send(session, s, event) {
265
+ const now = this.now();
266
+ if (now < this.notDeployedUntil)
267
+ return;
268
+ if (s.refusedUntil && now < s.refusedUntil)
269
+ return;
270
+ const auth = this.deps.auth();
271
+ if (!auth)
272
+ return;
273
+ const outcome = await postContextEvent(auth, session, event, this.deps.request ?? api_1.apiRequest);
274
+ switch (outcome.kind) {
275
+ case "sent":
276
+ if (s.lastFailure)
277
+ this.deps.log(`context: reports for ${session} are going through again`);
278
+ s.lastFailure = undefined;
279
+ return;
280
+ case "not-deployed":
281
+ this.notDeployedUntil = this.now() + exports.NOT_DEPLOYED_BACKOFF_MS;
282
+ this.deps.log("context: this BayChat server does not accept context reports yet — trying again in an hour");
283
+ return;
284
+ case "refused": {
285
+ s.refusedUntil = this.now() + exports.SESSION_REFUSED_BACKOFF_MS;
286
+ // The sample that was refused was never recorded by the server; the next one must go.
287
+ s.sent = undefined;
288
+ this.logOnce(s, `context: BayChat refused a report for ${session} (${outcome.message}${outcome.code ? `, ${outcome.code}` : ""}) — pausing that session's reports for 5 minutes`);
289
+ return;
290
+ }
291
+ case "failed":
292
+ s.sent = undefined;
293
+ this.logOnce(s, `context: could not report for ${session} (${outcome.message}) — will retry with the next change`);
294
+ return;
295
+ }
296
+ }
297
+ logOnce(s, line) {
298
+ if (s.lastFailure === line)
299
+ return;
300
+ s.lastFailure = line;
301
+ this.deps.log(line);
302
+ }
303
+ }
304
+ exports.ContextReporter = ContextReporter;
305
+ function errText(err) {
306
+ return err instanceof Error ? err.message : String(err);
307
+ }