acbridge 1.1.2 → 1.2.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/dist/acbridge.mjs +125 -113
- package/hub/dist/hub.cjs +53 -52
- package/package.json +1 -1
- package/plugin/README.md +6 -0
- package/plugin/plugins/alexa/dist/channel.mjs +33 -31
- package/plugin/plugins/alexa/scripts/codex-signal.mjs +128 -49
- package/plugin/plugins/alexa/scripts/signal.mjs +90 -20
- package/plugin/plugins/alexa/scripts/statusline-guard.mjs +6 -2
|
@@ -14,6 +14,9 @@
|
|
|
14
14
|
// SessionStart compact SILENT — Pre/PostCompact own compaction
|
|
15
15
|
// UserPromptSubmit — working
|
|
16
16
|
// Stop — finished
|
|
17
|
+
// SessionEnd — ended (upstream shipped SessionEnd mid-2026; the
|
|
18
|
+
// hub's 30-min idle close is now the BACKSTOP for
|
|
19
|
+
// abnormal exits / pre-SessionEnd Codex builds)
|
|
17
20
|
// PermissionRequest — needs_permission (observe-and-abstain: NEVER stdout —
|
|
18
21
|
// Codex's permission-shaped hooks read stdout as an
|
|
19
22
|
// approve/deny decision; this script never writes one)
|
|
@@ -24,6 +27,10 @@
|
|
|
24
27
|
// PostToolUse — a state-less context TICK (POST .../context)
|
|
25
28
|
// anything else — exit 0, nothing to report
|
|
26
29
|
//
|
|
30
|
+
// (synthesized) first event ever seen for a started, POSTed BEFORE the event's own signal —
|
|
31
|
+
// session is not a start/end the v0.146 TUI dispatches NO SessionStart hook
|
|
32
|
+
// (codex exec does); see TUI-SYNTHESIZED START below
|
|
33
|
+
//
|
|
27
34
|
// Unlike Claude Code, Codex's own hook payload carries `model` and `permission_mode` on EVERY event, so
|
|
28
35
|
// the mode/model FERRY (feeding the hub's modePlan/modeAuto/.../modelChanged synthesis, same as
|
|
29
36
|
// signal.mjs) is a straight field read here — no transcript walk needed for that part. The transcript
|
|
@@ -52,7 +59,7 @@
|
|
|
52
59
|
// only); stdout is NEVER written to (Codex hook stdout can influence approve/deny — this script is
|
|
53
60
|
// observe-only, on every path, always).
|
|
54
61
|
|
|
55
|
-
import { closeSync, fstatSync, openSync, readFileSync, readSync, writeFileSync } from "node:fs";
|
|
62
|
+
import { closeSync, existsSync, fstatSync, openSync, readFileSync, readSync, writeFileSync } from "node:fs";
|
|
56
63
|
import { request } from "node:http";
|
|
57
64
|
import os from "node:os";
|
|
58
65
|
import path from "node:path";
|
|
@@ -85,6 +92,23 @@ if (event === null || typeof event !== "object") done(); // non-object JSON (e.g
|
|
|
85
92
|
// Part-B: an adapter owns this session's states already — bail before touching the handshake or the hub.
|
|
86
93
|
if (process.env.ACB_CODEX_ADAPTER) done();
|
|
87
94
|
|
|
95
|
+
// ── The memories thread guard (2026-08, live-verified). Codex opens a SECOND session at startup with
|
|
96
|
+
// cwd inside its own home (`~/.codex/memories`, 2.3s after the real one). It is not a human session —
|
|
97
|
+
// signalling it self-derives a `memories@<machine>~codex` identity, arms the hub's duration watch, and
|
|
98
|
+
// pollutes the context tracker. Any event whose cwd sits under the codex home is dropped HERE, before
|
|
99
|
+
// handshake resolution, so it can neither POST nor write a handshake file. CODEX_HOME (upstream's home
|
|
100
|
+
// relocation) is honoured. Accepted cost: a user deliberately running codex with cwd inside ~/.codex
|
|
101
|
+
// gets no profile signals for that session.
|
|
102
|
+
{
|
|
103
|
+
const evCwd = typeof event.cwd === "string" ? event.cwd : "";
|
|
104
|
+
if (evCwd) {
|
|
105
|
+
const norm = (p) => String(p).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
106
|
+
const codexHome = norm(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"));
|
|
107
|
+
const c = norm(evCwd);
|
|
108
|
+
if (c === codexHome || c.startsWith(codexHome + "/")) done();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
88
112
|
// ── Dormancy gate: resolve THIS run's hub coordinates, mirroring signal.mjs's resolution order. ──────
|
|
89
113
|
const isPlaceholder = (v) => Boolean(v) && /^\$\{.*\}$/.test(v); // sanitized-env literal `${BRIDGE_*}`
|
|
90
114
|
const envVar = (v) => (v && !isPlaceholder(v) ? v : undefined);
|
|
@@ -183,8 +207,34 @@ function selfDeriveHandshake(ev, cfg) {
|
|
|
183
207
|
}
|
|
184
208
|
|
|
185
209
|
const cfg = readCliConfig();
|
|
186
|
-
|
|
187
|
-
|
|
210
|
+
const codexSid = typeof event.session_id === "string" ? event.session_id : undefined;
|
|
211
|
+
let handshake = resolveHandshake(codexSid);
|
|
212
|
+
|
|
213
|
+
// ── TUI-SYNTHESIZED START (2026-08-02, live-verified on Codex v0.146). The 0.146 interactive TUI
|
|
214
|
+
// dispatches NO SessionStart hook at open — the same binary's `codex exec` does, same hooks.json, same
|
|
215
|
+
// trust rows (a TUI session sat open with zero dispatches anywhere; every event after the first prompt
|
|
216
|
+
// then fired normally). No hook ⇒ no code of ours runs, so the closest honest approximation is: when the
|
|
217
|
+
// FIRST event we ever see for a session is not itself a start (SessionStart would double) or a close
|
|
218
|
+
// (started→ended in one breath is a pointless flash-and-revert), SessionStart never dispatched —
|
|
219
|
+
// synthesize `started` and POST it BEFORE the event's own signal. First-sighting detection is the
|
|
220
|
+
// self-derive handshake write itself: a lane where SessionStart DOES fire (exec, or a fixed future TUI)
|
|
221
|
+
// writes the file at start, so every later event finds it and never re-synthesizes. The explicit
|
|
222
|
+
// BRIDGE_SESSION_ID env path never synthesizes (a launcher that exports coords owns its own lifecycle),
|
|
223
|
+
// and a no-account run writes no file ⇒ no synthesis (the hub would refuse the unattributable POST anyway).
|
|
224
|
+
let synthesizeStart = false;
|
|
225
|
+
if (!handshake && autoProfilesEnabled(cfg)) {
|
|
226
|
+
const sessionsFile = codexSid ? path.join(SECRET_DIR, "sessions", `${codexSid}.json`) : null;
|
|
227
|
+
const hadFile = Boolean(sessionsFile) && existsSync(sessionsFile);
|
|
228
|
+
handshake = selfDeriveHandshake(event, cfg);
|
|
229
|
+
const name = String(event.hook_event_name ?? "");
|
|
230
|
+
synthesizeStart =
|
|
231
|
+
Boolean(handshake) &&
|
|
232
|
+
Boolean(sessionsFile) &&
|
|
233
|
+
!hadFile &&
|
|
234
|
+
existsSync(sessionsFile) && // the write really happened (needs a signed-in account)
|
|
235
|
+
name !== "SessionStart" &&
|
|
236
|
+
name !== "SessionEnd";
|
|
237
|
+
}
|
|
188
238
|
if (!handshake) done(); // no bridge session AND no hub configured (or auto-profiles off) — stay silent
|
|
189
239
|
|
|
190
240
|
// ── Classify table: hook_event_name (+ source) → one wire state, {tick:true}, or null. ────────────────
|
|
@@ -193,7 +243,8 @@ function classify(ev) {
|
|
|
193
243
|
switch (name) {
|
|
194
244
|
case "SessionStart": {
|
|
195
245
|
const source = String(ev.source ?? "");
|
|
196
|
-
// A compact "restart" is the SAME session continuing — PostCompact owns compaction.
|
|
246
|
+
// A compact "restart" is the SAME session continuing — PostCompact owns compaction. A "fork"
|
|
247
|
+
// deliberately falls through to `started` (a forked session IS a new session opening here).
|
|
197
248
|
if (source === "compact") return null;
|
|
198
249
|
return { state: source === "resume" ? "resumed" : "started" };
|
|
199
250
|
}
|
|
@@ -201,6 +252,10 @@ function classify(ev) {
|
|
|
201
252
|
return { state: "working" };
|
|
202
253
|
case "Stop":
|
|
203
254
|
return { state: "finished" };
|
|
255
|
+
case "SessionEnd":
|
|
256
|
+
// The real close (upstream hook, mid-2026): runs the hub's ONE ended path — arbiter stand-down +
|
|
257
|
+
// scene revert — immediately, instead of waiting out the 30-min SessionIdleWatch backstop.
|
|
258
|
+
return { state: "ended" };
|
|
204
259
|
case "PermissionRequest":
|
|
205
260
|
// Observe-and-abstain: classify it for the device signal, but NEVER emit stdout (below, always).
|
|
206
261
|
return { state: "needs_permission" };
|
|
@@ -213,11 +268,11 @@ function classify(ev) {
|
|
|
213
268
|
case "SubagentStop":
|
|
214
269
|
return { state: "subagentFinished" };
|
|
215
270
|
case "PostToolUse": {
|
|
216
|
-
// S2: legacy best-effort error detection — MIRRORS signal.mjs's exact
|
|
217
|
-
//
|
|
218
|
-
// failure, biased to UNDER-fire (an unflagged
|
|
219
|
-
// flashes the error scene.
|
|
220
|
-
const resp = ev.tool_response;
|
|
271
|
+
// S2: legacy best-effort error detection — MIRRORS signal.mjs's exact check (the Claude sibling,
|
|
272
|
+
// same field names: upstream's `tool_output` first, older `tool_response` as fallback): fire only
|
|
273
|
+
// when the tool response explicitly signals a hard failure, biased to UNDER-fire (an unflagged
|
|
274
|
+
// failure stays silent) so a normal session never flashes the error scene.
|
|
275
|
+
const resp = ev.tool_output ?? ev.tool_response;
|
|
221
276
|
const isError =
|
|
222
277
|
resp &&
|
|
223
278
|
typeof resp === "object" &&
|
|
@@ -234,7 +289,10 @@ function classify(ev) {
|
|
|
234
289
|
}
|
|
235
290
|
|
|
236
291
|
const mapped = classify(event);
|
|
237
|
-
|
|
292
|
+
// A synthesized start still POSTs even when the triggering event itself classifies to nothing (a future
|
|
293
|
+
// unregistered hook as the first sighting) — otherwise that sighting writes the handshake, exits, and the
|
|
294
|
+
// session's start is lost forever.
|
|
295
|
+
if (!mapped && !synthesizeStart) done(); // no state, no tick, nothing to synthesize → no-op
|
|
238
296
|
|
|
239
297
|
// ── readRolloutTail: the context ferry. Positioned read of the last 64KB only — rollouts reach tens of
|
|
240
298
|
// MB and this runs per tool call. Walk BACKWARD for the first (= latest) token_count row; cumulative
|
|
@@ -331,23 +389,37 @@ const ids = {
|
|
|
331
389
|
ts: Date.now(),
|
|
332
390
|
...(typeof event.cwd === "string" ? { cwd: event.cwd } : {}),
|
|
333
391
|
};
|
|
334
|
-
// A tick (PostToolUse) targets the state-less context route; everything else is a state signal.
|
|
335
|
-
const apiPath = mapped.tick ? "/local/session/context" : "/local/session/state";
|
|
336
392
|
// Straight from the payload — unlike Claude, Codex ships model + permission_mode on every hook event.
|
|
337
393
|
const mode = typeof event.permission_mode === "string" && event.permission_mode ? event.permission_mode : undefined;
|
|
338
394
|
const model = typeof event.model === "string" && event.model ? event.model : undefined;
|
|
339
395
|
const ferry = { agent: "codex", ...(mode ? { mode } : {}), ...(model ? { model } : {}) };
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
396
|
+
// This event's signal(s), POSTed strictly in order: a synthesized `started` must land BEFORE the event's
|
|
397
|
+
// own signal so the hub tracker sees start → activity exactly as a real dispatch order would. A tick
|
|
398
|
+
// (PostToolUse) targets the state-less context route; everything else is a state signal. The `event`
|
|
399
|
+
// audit string on the synthesized row says so — the state digest shows what really happened.
|
|
400
|
+
const posts = [];
|
|
401
|
+
if (synthesizeStart) {
|
|
402
|
+
posts.push({
|
|
403
|
+
apiPath: "/local/session/state",
|
|
404
|
+
payload: JSON.stringify({ ...ids, state: "started", event: "codex:SessionStart~synthesized", ...(context ? { context } : {}), ...ferry }),
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
if (mapped) {
|
|
408
|
+
posts.push({
|
|
409
|
+
apiPath: mapped.tick ? "/local/session/context" : "/local/session/state",
|
|
410
|
+
payload: JSON.stringify(
|
|
411
|
+
mapped.tick
|
|
412
|
+
? { ...ids, ...(context ? { context } : {}), ...ferry }
|
|
413
|
+
: {
|
|
414
|
+
...ids,
|
|
415
|
+
state: mapped.state,
|
|
416
|
+
event: `codex:${String(event.hook_event_name ?? "")}`,
|
|
417
|
+
...(context ? { context } : {}),
|
|
418
|
+
...ferry,
|
|
419
|
+
},
|
|
420
|
+
),
|
|
421
|
+
});
|
|
422
|
+
}
|
|
351
423
|
|
|
352
424
|
/** One bounded breadcrumb for a REJECTED signal — the twin of signal.mjs's, which this script lacked, so
|
|
353
425
|
* a dead cross-boundary Codex hook left no trace anywhere and `doctor` had nothing to report. Written to
|
|
@@ -377,30 +449,37 @@ function recordHookRejection(status) {
|
|
|
377
449
|
}
|
|
378
450
|
|
|
379
451
|
// node:http (not fetch) for zero deps + guaranteed NO Origin header (the guard 403s any Origin) + a hard
|
|
380
|
-
// timeout.
|
|
381
|
-
//
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
452
|
+
// timeout per request. Sequential: each POST goes out only after the previous one's response completes
|
|
453
|
+
// (order is the point of the chain); an unreachable/unresponsive hub aborts the remainder — bounded,
|
|
454
|
+
// never blocking Codex. Observe-and-abstain: responses are drained and discarded — NOTHING from them (or
|
|
455
|
+
// anywhere else in this script) is ever written to our own stdout.
|
|
456
|
+
function send(i) {
|
|
457
|
+
if (i >= posts.length) done();
|
|
458
|
+
const { apiPath, payload } = posts[i];
|
|
459
|
+
const req = request(
|
|
460
|
+
{
|
|
461
|
+
host: "127.0.0.1",
|
|
462
|
+
port: handshake.hubPort,
|
|
463
|
+
path: apiPath,
|
|
464
|
+
method: "POST",
|
|
465
|
+
headers: {
|
|
466
|
+
"content-type": "application/json",
|
|
467
|
+
"content-length": Buffer.byteLength(payload),
|
|
468
|
+
"x-bridge-hub-token": token,
|
|
469
|
+
},
|
|
470
|
+
timeout: 2000,
|
|
392
471
|
},
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
done
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
472
|
+
(res) => {
|
|
473
|
+
if (res.statusCode && res.statusCode >= 400) recordHookRejection(res.statusCode);
|
|
474
|
+
res.on("data", () => {}); // drain
|
|
475
|
+
res.on("end", () => send(i + 1));
|
|
476
|
+
},
|
|
477
|
+
);
|
|
478
|
+
req.on("timeout", () => {
|
|
479
|
+
req.destroy();
|
|
480
|
+
done(); // an unresponsive hub won't take the next one either
|
|
481
|
+
});
|
|
482
|
+
req.on("error", done); // hub down / connection refused → silent no-op
|
|
483
|
+
req.end(payload);
|
|
484
|
+
}
|
|
485
|
+
send(0);
|
|
@@ -170,25 +170,26 @@ function selfDeriveHandshake(ev, cfg) {
|
|
|
170
170
|
// SIGNED-IN account (sub === pinned scope). No account ⇒ nothing written ⇒ unattributable ⇒ still refused;
|
|
171
171
|
// an acbridge run / channel file is never overwritten; the selfWritten marker keeps gate.mjs dev-freedom.
|
|
172
172
|
//
|
|
173
|
-
//
|
|
174
|
-
// servers
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
173
|
+
// The write runs on EVERY hook, SessionStart INCLUDED (2026-08). It used to be suppressed at
|
|
174
|
+
// SessionStart (which fires BEFORE plugin MCP servers connect, code.claude.com/docs/en/hooks) so a
|
|
175
|
+
// bare `claude --channels` channel could never mistake a selfWritten race file for its own — but that
|
|
176
|
+
// suppression made `started`/`resumed` DEAD in every plain/app-launched session: no handshake at
|
|
177
|
+
// start ⇒ the hub drops the whole start payload ⇒ start scenes never fire. The race it guarded is
|
|
178
|
+
// CLOSED by AS-T9f2 (channel/src/handshake.ts): the channel, on connect, ADOPTS a pre-existing file
|
|
179
|
+
// and MERGE-asserts `voice: true` onto it (every field preserved, selfWritten included), before any
|
|
180
|
+
// reply egress or the first PreToolUse gate — so gate.mjs sees the voice marker either way. The lone
|
|
181
|
+
// residual is handshake.ts's documented Finding A (that atomic merge-rewrite failing), judged
|
|
182
|
+
// practically unreachable there: a same-user atomic rewrite of a 0600 file milliseconds old, inside
|
|
183
|
+
// the sub-second window before mcp.connect.
|
|
184
|
+
writePlainSessionHandshakeIfAbsent({
|
|
185
|
+
secretDir: SECRET_DIR,
|
|
186
|
+
id,
|
|
187
|
+
hubPort: cfg.hubPort,
|
|
188
|
+
hubTokenFile,
|
|
189
|
+
cwd,
|
|
190
|
+
claudeSessionId: typeof ev.session_id === "string" ? ev.session_id : undefined,
|
|
191
|
+
sub: readActiveUserSub(SECRET_DIR),
|
|
192
|
+
});
|
|
192
193
|
return { id, hubPort: cfg.hubPort, hubTokenFile };
|
|
193
194
|
}
|
|
194
195
|
|
|
@@ -197,6 +198,71 @@ let handshake = resolveHandshake(typeof event.session_id === "string" ? event.se
|
|
|
197
198
|
if (!handshake && autoProfilesEnabled(cfg)) handshake = selfDeriveHandshake(event, cfg);
|
|
198
199
|
if (!handshake) done(); // no bridge session AND no hub configured (or auto-profiles off) — stay silent
|
|
199
200
|
|
|
201
|
+
// ── Agent device-brief (2026-08-02, "Make agent aware of devices in chat"). At SessionStart, ask the
|
|
202
|
+
// hub whether this cwd's bound profile enables the brief and print it as additionalContext — the ONE
|
|
203
|
+
// stdout this script ever writes, and only on this event (every other path stays byte-silent). Emit
|
|
204
|
+
// on startup/clear/fork AND on compact (compaction is precisely when previously-injected context is
|
|
205
|
+
// lost); SKIP resume (the resumed transcript already contains the brief). Silent on any error,
|
|
206
|
+
// non-200, enabled:false, or a dead hub (ECONNREFUSED returns instantly; the timeout only guards a
|
|
207
|
+
// hung hub — a session must never start late because of us). The GET creates no files and completes
|
|
208
|
+
// BEFORE the classify/POST flow below, so ordering and the exit-0 contract are unchanged.
|
|
209
|
+
await new Promise((resolveBrief) => {
|
|
210
|
+
const name = String(event.hook_event_name ?? "");
|
|
211
|
+
const source = String(event.source ?? "");
|
|
212
|
+
if (name !== "SessionStart" || source === "resume") return resolveBrief();
|
|
213
|
+
let token = "";
|
|
214
|
+
try {
|
|
215
|
+
token = readFileSync(handshake.hubTokenFile, "utf8").trim();
|
|
216
|
+
} catch {
|
|
217
|
+
return resolveBrief();
|
|
218
|
+
}
|
|
219
|
+
if (!token) return resolveBrief();
|
|
220
|
+
const cwd = typeof event.cwd === "string" && event.cwd ? event.cwd : process.cwd();
|
|
221
|
+
let settled = false;
|
|
222
|
+
const settle = () => {
|
|
223
|
+
if (!settled) {
|
|
224
|
+
settled = true;
|
|
225
|
+
resolveBrief();
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
const req = request(
|
|
229
|
+
{
|
|
230
|
+
host: "127.0.0.1",
|
|
231
|
+
port: handshake.hubPort,
|
|
232
|
+
path: `/local/agent/brief?cwd=${encodeURIComponent(cwd)}&agent=claude`,
|
|
233
|
+
method: "GET",
|
|
234
|
+
headers: { "x-bridge-hub-token": token },
|
|
235
|
+
timeout: 600,
|
|
236
|
+
},
|
|
237
|
+
(res) => {
|
|
238
|
+
let body = "";
|
|
239
|
+
res.setEncoding("utf8");
|
|
240
|
+
res.on("data", (c) => (body += c));
|
|
241
|
+
res.on("end", () => {
|
|
242
|
+
try {
|
|
243
|
+
if (res.statusCode === 200) {
|
|
244
|
+
const parsed = JSON.parse(body || "{}");
|
|
245
|
+
if (parsed && parsed.enabled === true && typeof parsed.brief === "string" && parsed.brief) {
|
|
246
|
+
// Nested exactly as the hooks contract requires; sliced defensively under the upstream
|
|
247
|
+
// 10k additionalContext cap (the hub already composes ≤9,500 — this guards version skew).
|
|
248
|
+
process.stdout.write(
|
|
249
|
+
JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: parsed.brief.slice(0, 9800) } }) + "\n",
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} catch {
|
|
254
|
+
/* silent — the brief is an optimization, never a fault */
|
|
255
|
+
}
|
|
256
|
+
settle();
|
|
257
|
+
});
|
|
258
|
+
res.on("error", settle);
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
req.on("timeout", () => req.destroy());
|
|
262
|
+
req.on("error", settle);
|
|
263
|
+
req.end();
|
|
264
|
+
});
|
|
265
|
+
|
|
200
266
|
// ── Map the firing hook (+ payload) → one wire state, {tick:true} for a context tick, or null. ───────
|
|
201
267
|
const msgExtra = (ev) => (typeof ev.message === "string" && ev.message ? { message: ev.message.slice(0, 120) } : undefined);
|
|
202
268
|
|
|
@@ -207,6 +273,8 @@ function classify(ev) {
|
|
|
207
273
|
const source = String(ev.source ?? "");
|
|
208
274
|
// A compact "restart" is the SAME session continuing — PostCompact owns compaction (firing
|
|
209
275
|
// `started` here re-ran the start scene after every auto-compact, the pre-expansion bug).
|
|
276
|
+
// `source:"fork"` (2026 upstream matcher) deliberately falls through to `started`: a forked
|
|
277
|
+
// session IS a new session opening in this repo. Pinned by channel/test/signal.test.ts.
|
|
210
278
|
if (source === "compact") return null;
|
|
211
279
|
return { state: source === "resume" ? "resumed" : "started" };
|
|
212
280
|
}
|
|
@@ -265,7 +333,9 @@ function classify(ev) {
|
|
|
265
333
|
// Legacy best-effort error detection (older Claude Code without PostToolUseFailure): fire only when
|
|
266
334
|
// the tool response explicitly signals a hard failure — biased to UNDER-fire (a failed grep / non-
|
|
267
335
|
// zero exit the runtime doesn't flag stays silent) so a normal session never flashes the error scene.
|
|
268
|
-
|
|
336
|
+
// Upstream renamed the payload field to `tool_output` (2026); older builds still send
|
|
337
|
+
// `tool_response` — read both, upstream name first.
|
|
338
|
+
const resp = ev.tool_output ?? ev.tool_response;
|
|
269
339
|
const isError =
|
|
270
340
|
resp &&
|
|
271
341
|
typeof resp === "object" &&
|
|
@@ -23,8 +23,12 @@ export const CHAIN_DEPTH_ENV = "BRIDGE_STATUSLINE_DEPTH";
|
|
|
23
23
|
const MARKERS = ["plugins/alexa/scripts/statusline.mjs", "plugins\\alexa\\scripts\\statusline.mjs"];
|
|
24
24
|
|
|
25
25
|
/** Roots/launchers only the bridge installs under — used to attribute a RELOCATED or renamed copy of our
|
|
26
|
-
* script without also claiming a user's unrelated `statusline.mjs`.
|
|
27
|
-
|
|
26
|
+
* script without also claiming a user's unrelated `statusline.mjs`.
|
|
27
|
+
* `@bridgeapp` is the Windows install directory up to v1.3.1 (one-click ⇒ named after the package,
|
|
28
|
+
* `@bridge/app`); `ac bridge` is the one from v1.3.2 (assisted ⇒ named after the product). Both are
|
|
29
|
+
* live on real machines forever, because an in-place upgrade keeps the directory it was installed in.
|
|
30
|
+
* Matched against `norm()`ed text, so lower-case with `/` separators. */
|
|
31
|
+
const BRIDGE_OWNED = /\.claude-bridge|@bridgeapp|ac bridge|bridge-node|ac-bridge/;
|
|
28
32
|
|
|
29
33
|
const norm = (s) => s.replace(/\\/g, "/").toLowerCase();
|
|
30
34
|
|