@snowyroad/braid 0.78.1 → 0.79.2
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/cli.js +158 -72
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -848,6 +848,10 @@ var RelayClient = class {
|
|
|
848
848
|
cardPin = /* @__PURE__ */ new Map();
|
|
849
849
|
// no-downgrade pin: name → pinned jkt
|
|
850
850
|
signWarned = false;
|
|
851
|
+
/** BL-228 task 3: activity-derived member-channel inventory (resume-cursor keys +
|
|
852
|
+
* added - removed). Covers channels GET /channels' RLS may drop (e.g. a cross-org
|
|
853
|
+
* guest channel) since this is fed by the same push frames the bridge already gets. */
|
|
854
|
+
memberChannels = /* @__PURE__ */ new Set();
|
|
851
855
|
/** Fail-open signing: any problem -> unsigned post + one warning per process. */
|
|
852
856
|
trySign(f) {
|
|
853
857
|
if (!this.cfg.signMessage) return void 0;
|
|
@@ -1399,12 +1403,14 @@ var RelayClient = class {
|
|
|
1399
1403
|
this.seenByChannel.delete(ch);
|
|
1400
1404
|
this.caughtUp.delete(ch);
|
|
1401
1405
|
this.lastGapResumeAt.delete(ch);
|
|
1406
|
+
this.memberChannels.delete(ch);
|
|
1402
1407
|
this.removedCb?.(ch);
|
|
1403
1408
|
break;
|
|
1404
1409
|
}
|
|
1405
1410
|
case "added": {
|
|
1406
1411
|
const ch = String(msg.channelId ?? "");
|
|
1407
1412
|
if (!ch) break;
|
|
1413
|
+
this.memberChannels.add(ch);
|
|
1408
1414
|
this.addedCb?.(ch);
|
|
1409
1415
|
break;
|
|
1410
1416
|
}
|
|
@@ -1432,6 +1438,7 @@ var RelayClient = class {
|
|
|
1432
1438
|
const resume = msg?.resume;
|
|
1433
1439
|
if (resume && typeof resume === "object") {
|
|
1434
1440
|
for (const [ch, seqRaw] of Object.entries(resume)) {
|
|
1441
|
+
this.memberChannels.add(ch);
|
|
1435
1442
|
const seq = Number(seqRaw);
|
|
1436
1443
|
if (Number.isFinite(seq) && seq > this.cursorOf(ch)) this.cursors.set(ch, seq);
|
|
1437
1444
|
const floor = this.cursorOf(ch);
|
|
@@ -1723,6 +1730,27 @@ var RelayClient = class {
|
|
|
1723
1730
|
isScribeFor(channelId) {
|
|
1724
1731
|
return this.selfScribeCache.get(channelId) ?? false;
|
|
1725
1732
|
}
|
|
1733
|
+
/** BL-228 task 3: this agent's member channels from the relay (`GET /channels`, RLS-scoped
|
|
1734
|
+
* to the caller). Best-effort like fetchRoster/fetchChannelName: never throws, [] on any
|
|
1735
|
+
* failure (HTTP error, network failure, malformed body). Lets the handoff controller offer
|
|
1736
|
+
* COLD channels (member but no live session) alongside the live-session set. */
|
|
1737
|
+
async fetchMemberChannels() {
|
|
1738
|
+
const r = await this.httpJson(`/channels`, { requestClass: "relay.channels.list" });
|
|
1739
|
+
if (!r.ok) {
|
|
1740
|
+
console.warn("[braid] member channels HTTP", r.status ?? r.category);
|
|
1741
|
+
return [];
|
|
1742
|
+
}
|
|
1743
|
+
const body = r.json;
|
|
1744
|
+
const arr = Array.isArray(body?.channels) ? body.channels : [];
|
|
1745
|
+
return arr.filter((c) => typeof c?.id === "string" && c.id.length > 0).map((c) => ({ id: String(c.id), name: typeof c.name === "string" ? c.name : typeof c.title === "string" ? c.title : null }));
|
|
1746
|
+
}
|
|
1747
|
+
/** BL-228 task 3: synchronous in-memory member-channel set (resume-cursor keys at hello,
|
|
1748
|
+
* plus `added` minus `removed`), activity-derived from push frames — no network. Covers
|
|
1749
|
+
* a channel GET /channels' RLS may drop (e.g. a cross-org guest channel) that the bridge
|
|
1750
|
+
* still holds live membership of via the WS. Returns a snapshot copy, not a live view. */
|
|
1751
|
+
knownMemberChannelIds() {
|
|
1752
|
+
return new Set(this.memberChannels);
|
|
1753
|
+
}
|
|
1726
1754
|
/** Spread optional usage into the outbound body. Omits undefined fields so a no-usage
|
|
1727
1755
|
* call produces a body byte-identical to the pre-billing shape. */
|
|
1728
1756
|
usageBody(u) {
|
|
@@ -2736,8 +2764,14 @@ ${fence(slackTaint ? "slack message" : "messages mentioning you", addressed)}
|
|
|
2736
2764
|
hasPendingWork() {
|
|
2737
2765
|
return this.pendingTurns.length > 0;
|
|
2738
2766
|
}
|
|
2767
|
+
/** Open the inbound hold WITHOUT stopping the client (BL-228: start-held warm
|
|
2768
|
+
* for on-demand handoff). Idempotent: an already-open hold is preserved so
|
|
2769
|
+
* nothing queued during the warm is lost. */
|
|
2770
|
+
openHold() {
|
|
2771
|
+
if (!this.holdQueue) this.holdQueue = [];
|
|
2772
|
+
}
|
|
2739
2773
|
async handoffPause() {
|
|
2740
|
-
this.holdQueue = [];
|
|
2774
|
+
if (!this.holdQueue) this.holdQueue = [];
|
|
2741
2775
|
await this.session?.handoffPause?.();
|
|
2742
2776
|
}
|
|
2743
2777
|
async handoffResume() {
|
|
@@ -4276,33 +4310,18 @@ function resolveHandoffBinary(name, install) {
|
|
|
4276
4310
|
handoffBinaryAbs.set(name, found);
|
|
4277
4311
|
return found;
|
|
4278
4312
|
}
|
|
4313
|
+
var HANDOFF_RESUME_SPECS = {
|
|
4314
|
+
"claude-code": (sessionId, cwd) => ({ command: resolveHandoffBinary("claude", "Claude Code"), args: ["--resume", sessionId], cwd }),
|
|
4315
|
+
codex: (sessionId, cwd) => ({ command: resolveHandoffBinary("codex", "the Codex CLI"), args: ["resume", sessionId], cwd }),
|
|
4316
|
+
grok: (sessionId, cwd) => ({ command: resolveHandoffBinary("grok", "xAI's Grok CLI"), args: ["--resume", sessionId], cwd }),
|
|
4317
|
+
opencode: (sessionId, cwd) => ({ command: resolveHandoffBinary("opencode", "the opencode CLI"), args: ["--session", sessionId], cwd })
|
|
4318
|
+
};
|
|
4319
|
+
function interactiveHandoffSupported(agent) {
|
|
4320
|
+
return Object.prototype.hasOwnProperty.call(HANDOFF_RESUME_SPECS, agent);
|
|
4321
|
+
}
|
|
4279
4322
|
function interactiveResumeSpecFor(agent, sessionId, cwd) {
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
return { command: resolveHandoffBinary("claude", "Claude Code"), args: ["--resume", sessionId], cwd };
|
|
4283
|
-
case "codex":
|
|
4284
|
-
return { command: resolveHandoffBinary("codex", "the Codex CLI"), args: ["resume", sessionId], cwd };
|
|
4285
|
-
// grok (2026-07-02): 3-leg probe over `grok agent stdio` — session/new id ==
|
|
4286
|
-
// `grok --resume <id>` id (cwd-keyed store; the bridge's canonical process.cwd()
|
|
4287
|
-
// matches) AND session/load recalled the marker. NOTE: grok's session store is
|
|
4288
|
-
// keyed by CANONICAL cwd; sessionId alone is not enough if cwd were a symlink.
|
|
4289
|
-
case "grok":
|
|
4290
|
-
return { command: resolveHandoffBinary("grok", "xAI's Grok CLI"), args: ["--resume", sessionId], cwd };
|
|
4291
|
-
// opencode (2026-07-02): 3-leg probe over `opencode acp` — `ses_` id resumable by
|
|
4292
|
-
// BOTH the TUI (`opencode --session <id>`) and headless `run --session` (marker
|
|
4293
|
-
// recalled), AND session/load recalled the marker (shared store).
|
|
4294
|
-
case "opencode":
|
|
4295
|
-
return { command: resolveHandoffBinary("opencode", "the opencode CLI"), args: ["--session", sessionId], cwd };
|
|
4296
|
-
// gemini: REFUTED for the handoff 2026-07-02 — native `--resume <uuid>` works, but
|
|
4297
|
-
// ACP session/load returns success while restoring NO conversation context, so the
|
|
4298
|
-
// hand-back leg would silently fork the thread. Do not enable until gemini-cli's
|
|
4299
|
-
// ACP loadSession actually rehydrates context.
|
|
4300
|
-
// cursor: REFUTED 2026-07-02 — `~/.cursor/acp-sessions/` and the native chat store
|
|
4301
|
-
// are separate namespaces; `cursor-agent --resume <acp-id>` exits 0 and silently
|
|
4302
|
-
// opens a FRESH thread (the exact failure mode this gate exists to prevent).
|
|
4303
|
-
default:
|
|
4304
|
-
return null;
|
|
4305
|
-
}
|
|
4323
|
+
const build = HANDOFF_RESUME_SPECS[agent];
|
|
4324
|
+
return build ? build(sessionId, cwd) : null;
|
|
4306
4325
|
}
|
|
4307
4326
|
var defaultAcpClientFactory = (launch) => new AcpClient(launch);
|
|
4308
4327
|
var MAX_CONSECUTIVE_RESTARTS = 3;
|
|
@@ -5808,7 +5827,7 @@ function startHandoffConversation(conn, ctl, initialChannelId, writeLine, log2)
|
|
|
5808
5827
|
}
|
|
5809
5828
|
channelId = id;
|
|
5810
5829
|
phase = "offered";
|
|
5811
|
-
writeLine({ handoff: "offer", channel: res.channel, provider: res.provider, pendingWork: res.pendingWork });
|
|
5830
|
+
writeLine({ handoff: "offer", channel: res.channel, provider: res.provider, pendingWork: res.pendingWork, warm: res.warm });
|
|
5812
5831
|
};
|
|
5813
5832
|
conn.on("close", () => {
|
|
5814
5833
|
const p = phase;
|
|
@@ -6247,14 +6266,14 @@ function renderEvent(ev) {
|
|
|
6247
6266
|
// src/observe/socketHandoff.ts
|
|
6248
6267
|
function makeSocketHandoffController(deps) {
|
|
6249
6268
|
const specFor = deps.specFor ?? interactiveResumeSpecFor;
|
|
6269
|
+
const handoffSupported = deps.handoffSupported ?? interactiveHandoffSupported;
|
|
6250
6270
|
const scopedLaunchFn = deps.scopedLaunchFn ?? scopedLaunch;
|
|
6251
6271
|
const log2 = deps.log ?? (() => {
|
|
6252
6272
|
});
|
|
6253
6273
|
let active = null;
|
|
6254
6274
|
const label = (channelId) => {
|
|
6255
6275
|
const name = deps.channelName(channelId);
|
|
6256
|
-
|
|
6257
|
-
return name ? `#${sanitizeForTty(name).slice(0, 40)} (${tag})` : `#${tag}`;
|
|
6276
|
+
return name ? `#${sanitizeForTty(name).slice(0, 40)}` : `#${channelTag(channelId)}`;
|
|
6258
6277
|
};
|
|
6259
6278
|
const gateBlocked = () => {
|
|
6260
6279
|
const g = deps.gate.snapshot();
|
|
@@ -6262,57 +6281,91 @@ function makeSocketHandoffController(deps) {
|
|
|
6262
6281
|
if (g.state === "orphaned") return { ok: false, error: "handoff_orphaned" };
|
|
6263
6282
|
return null;
|
|
6264
6283
|
};
|
|
6265
|
-
const
|
|
6284
|
+
const classify = (channelId) => {
|
|
6266
6285
|
const blocked = gateBlocked();
|
|
6267
6286
|
if (blocked) return blocked;
|
|
6268
6287
|
const session = deps.sessions.get(channelId);
|
|
6269
|
-
if (
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6288
|
+
if (session) {
|
|
6289
|
+
const sessionId = session.liveSessionId();
|
|
6290
|
+
if (!sessionId) return { ok: false, error: "no_live_session" };
|
|
6291
|
+
let raw;
|
|
6292
|
+
try {
|
|
6293
|
+
raw = specFor(deps.provider, sessionId, deps.cwdFor(channelId));
|
|
6294
|
+
} catch (e) {
|
|
6295
|
+
return { ok: false, error: sanitizeForTty(String(e?.message ?? e)) };
|
|
6296
|
+
}
|
|
6297
|
+
if (!raw) return { ok: false, error: "provider_unverified" };
|
|
6298
|
+
return { ok: true, kind: "live", session, spec: raw };
|
|
6299
|
+
}
|
|
6300
|
+
if (deps.memberChannelIds().has(channelId) && deps.warmSession) {
|
|
6301
|
+
if (!handoffSupported(deps.provider)) return { ok: false, error: "provider_unverified" };
|
|
6302
|
+
return { ok: true, kind: "cold" };
|
|
6277
6303
|
}
|
|
6278
|
-
|
|
6279
|
-
return { ok: true, session, sessionId, spec: raw };
|
|
6304
|
+
return { ok: false, error: "unknown_channel" };
|
|
6280
6305
|
};
|
|
6281
6306
|
return {
|
|
6282
6307
|
listChannels() {
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
pendingWork: s.hasPendingWork?.() === true
|
|
6288
|
-
}
|
|
6308
|
+
const rows = [];
|
|
6309
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6310
|
+
for (const [id, s] of deps.sessions.entries()) {
|
|
6311
|
+
seen.add(id);
|
|
6312
|
+
rows.push({ id, label: label(id), provider: deps.provider, pendingWork: s.hasPendingWork?.() === true, live: true });
|
|
6313
|
+
}
|
|
6314
|
+
for (const id of deps.memberChannelIds()) {
|
|
6315
|
+
if (seen.has(id)) continue;
|
|
6316
|
+
rows.push({ id, label: label(id), provider: deps.provider, pendingWork: false, live: false });
|
|
6317
|
+
}
|
|
6318
|
+
return rows;
|
|
6289
6319
|
},
|
|
6290
6320
|
offer(channelId) {
|
|
6291
|
-
const
|
|
6292
|
-
if (!
|
|
6321
|
+
const c = classify(channelId);
|
|
6322
|
+
if (!c.ok) return c;
|
|
6293
6323
|
return {
|
|
6294
6324
|
ok: true,
|
|
6295
6325
|
channel: { id: channelId, label: label(channelId) },
|
|
6296
6326
|
provider: deps.provider,
|
|
6297
|
-
pendingWork:
|
|
6327
|
+
pendingWork: c.kind === "live" ? c.session.hasPendingWork?.() === true : false,
|
|
6328
|
+
warm: c.kind === "cold"
|
|
6298
6329
|
};
|
|
6299
6330
|
},
|
|
6300
6331
|
async begin(channelId) {
|
|
6301
|
-
const
|
|
6302
|
-
if (!
|
|
6332
|
+
const c = classify(channelId);
|
|
6333
|
+
if (!c.ok) return c;
|
|
6303
6334
|
if (!deps.gate.tryAcquire(channelId, "socket")) return gateBlocked() ?? { ok: false, error: "handoff_active" };
|
|
6304
6335
|
let pauseAttempted = false;
|
|
6336
|
+
let warmedCold = false;
|
|
6337
|
+
let session;
|
|
6305
6338
|
try {
|
|
6306
|
-
|
|
6339
|
+
let spec;
|
|
6340
|
+
if (c.kind === "cold") {
|
|
6341
|
+
session = await deps.warmSession(channelId);
|
|
6342
|
+
warmedCold = true;
|
|
6343
|
+
const sessionId = session.liveSessionId();
|
|
6344
|
+
if (!sessionId) throw new Error("warm produced no resumable session");
|
|
6345
|
+
const raw = specFor(deps.provider, sessionId, deps.cwdFor(channelId));
|
|
6346
|
+
if (!raw) throw new Error("provider_unverified");
|
|
6347
|
+
spec = raw;
|
|
6348
|
+
log2(`[braid] handoff: warmed ${label(channelId)} on demand for a socket client`);
|
|
6349
|
+
} else {
|
|
6350
|
+
session = c.session;
|
|
6351
|
+
spec = c.spec;
|
|
6352
|
+
}
|
|
6353
|
+
const scoped = await scopedLaunchFn({ command: spec.command, args: spec.args, cwd: spec.cwd }, { scope: deps.scope }, deps.provider);
|
|
6307
6354
|
pauseAttempted = true;
|
|
6308
|
-
await
|
|
6309
|
-
active = { channelId, session
|
|
6355
|
+
await session.handoffPause();
|
|
6356
|
+
active = { channelId, session };
|
|
6310
6357
|
log2(`[braid] handoff: paused ${label(channelId)} for a socket client`);
|
|
6311
6358
|
return { ok: true, spec: { command: scoped.command, args: scoped.args, cwd: scoped.cwd, env: scoped.env ?? null } };
|
|
6312
6359
|
} catch (e) {
|
|
6313
|
-
if (pauseAttempted) {
|
|
6360
|
+
if (pauseAttempted && session) {
|
|
6361
|
+
try {
|
|
6362
|
+
await session.handoffResume();
|
|
6363
|
+
} catch {
|
|
6364
|
+
}
|
|
6365
|
+
} else if (warmedCold && session) {
|
|
6314
6366
|
try {
|
|
6315
|
-
await
|
|
6367
|
+
await session.handoffPause();
|
|
6368
|
+
await session.handoffResume();
|
|
6316
6369
|
} catch {
|
|
6317
6370
|
}
|
|
6318
6371
|
}
|
|
@@ -6396,6 +6449,12 @@ async function startBridge(cfg, relay, deps) {
|
|
|
6396
6449
|
relay.onConnect?.(() => {
|
|
6397
6450
|
relay.sendAgentHealth({ ...health.current(), service, runtimeVersion });
|
|
6398
6451
|
relay.sendDaemonStatus(blockedEndpoints, daemonPostureDetail(blockedEndpoints));
|
|
6452
|
+
void (async () => {
|
|
6453
|
+
const fetchMembers = relay.fetchMemberChannels;
|
|
6454
|
+
if (!fetchMembers) return;
|
|
6455
|
+
const members = await fetchMembers.call(relay).catch(() => []);
|
|
6456
|
+
for (const c of members) if (c.name && !channelNames.has(c.id)) channelNames.set(c.id, c.name);
|
|
6457
|
+
})();
|
|
6399
6458
|
});
|
|
6400
6459
|
const brokers = /* @__PURE__ */ new Map();
|
|
6401
6460
|
const mcpServerPath = path3.join(path3.dirname(fileURLToPath2(import.meta.url)), "mcp", "server.js");
|
|
@@ -6513,11 +6572,20 @@ async function startBridge(cfg, relay, deps) {
|
|
|
6513
6572
|
console.warn("[braid] self card publish on connect failed:", sanitizeForTty(String(e)));
|
|
6514
6573
|
}
|
|
6515
6574
|
}
|
|
6516
|
-
async function ensureSession(channelId) {
|
|
6575
|
+
async function ensureSession(channelId, opts) {
|
|
6517
6576
|
const live = sessions.get(channelId);
|
|
6518
|
-
if (live)
|
|
6577
|
+
if (live) {
|
|
6578
|
+
if (opts?.startHeld) live.openHold();
|
|
6579
|
+
return live;
|
|
6580
|
+
}
|
|
6519
6581
|
const inFlight = pending.get(channelId);
|
|
6520
|
-
if (inFlight)
|
|
6582
|
+
if (inFlight) {
|
|
6583
|
+
if (opts?.startHeld) return inFlight.then((s) => {
|
|
6584
|
+
s.openHold();
|
|
6585
|
+
return s;
|
|
6586
|
+
});
|
|
6587
|
+
return inFlight;
|
|
6588
|
+
}
|
|
6521
6589
|
const p = (async () => {
|
|
6522
6590
|
console.log(`[braid] Warming up #${sanitizeForTty(channelId)}\u2026`);
|
|
6523
6591
|
void (async () => {
|
|
@@ -6608,6 +6676,7 @@ async function startBridge(cfg, relay, deps) {
|
|
|
6608
6676
|
clearInterval(warmupHeartbeat);
|
|
6609
6677
|
relay.sendActivity(channelId, "stop");
|
|
6610
6678
|
}
|
|
6679
|
+
if (opts?.startHeld) session.openHold();
|
|
6611
6680
|
brokers.set(channelId, broker);
|
|
6612
6681
|
sessions.set(channelId, session);
|
|
6613
6682
|
pending.delete(channelId);
|
|
@@ -6703,6 +6772,18 @@ async function startBridge(cfg, relay, deps) {
|
|
|
6703
6772
|
cwdFor: (id) => handoffCwdFor(cfg, id),
|
|
6704
6773
|
scope: cfg.scope,
|
|
6705
6774
|
gate: cs.handoffGate,
|
|
6775
|
+
// BL-228 task 4/8: the in-memory member set RelayClient tracks (Task 3) is
|
|
6776
|
+
// sufficient on its own — no union with sessions.keys() needed. listChannels()
|
|
6777
|
+
// already unions deps.sessions (live) with deps.memberChannelIds() (Task 4), so
|
|
6778
|
+
// any channel with a live session appears via the sessions map regardless of
|
|
6779
|
+
// whether it's also in the member set; unioning sessions.keys() into
|
|
6780
|
+
// memberChannelIds() here would be redundant belt-and-suspenders with no
|
|
6781
|
+
// behavioral effect, not a real gap closed.
|
|
6782
|
+
memberChannelIds: () => relay.knownMemberChannelIds(),
|
|
6783
|
+
// BL-228 task 8: warm a cold member channel on demand — the router's own
|
|
6784
|
+
// ensureSession, with startHeld so the socket handoff's begin() (task 5) never
|
|
6785
|
+
// races a headless turn between warm-complete and handoffPause.
|
|
6786
|
+
warmSession: (id) => ensureSession(id, { startHeld: true }),
|
|
6706
6787
|
log: (line) => console.log(line)
|
|
6707
6788
|
}) : void 0;
|
|
6708
6789
|
try {
|
|
@@ -7770,9 +7851,7 @@ function validateSpec(line) {
|
|
|
7770
7851
|
}
|
|
7771
7852
|
async function pickChannel(channels, filter, promptFn, write) {
|
|
7772
7853
|
if (channels.length === 0) {
|
|
7773
|
-
write(
|
|
7774
|
-
"[handoff] no active session to take over yet.\n[handoff] handoff drops you into a session the agent is ALREADY running - one exists\n[handoff] only once the agent is working in a channel (a message, a reply, a flow turn).\n[handoff] send the agent a message in a channel and let it reply, then run this again.\n"
|
|
7775
|
-
);
|
|
7854
|
+
write("[handoff] this agent is not a member of any channel - no channels to take over yet.\n");
|
|
7776
7855
|
return null;
|
|
7777
7856
|
}
|
|
7778
7857
|
if (filter && filter.trim() !== "") {
|
|
@@ -7799,7 +7878,8 @@ async function pickChannel(channels, filter, promptFn, write) {
|
|
|
7799
7878
|
}
|
|
7800
7879
|
function listChannels(write, channels) {
|
|
7801
7880
|
channels.forEach((c, i) => {
|
|
7802
|
-
|
|
7881
|
+
const mark = c.live === false ? " (cold - will start a session)" : c.pendingWork ? " (working)" : "";
|
|
7882
|
+
write(` [${i + 1}] ${sanitizeForTty(c.label)}${mark}
|
|
7803
7883
|
`);
|
|
7804
7884
|
});
|
|
7805
7885
|
}
|
|
@@ -7807,19 +7887,25 @@ function renderOffer(write, offer) {
|
|
|
7807
7887
|
const channel = offer["channel"] ?? {};
|
|
7808
7888
|
const label = sanitizeForTty(String(channel.label ?? ""));
|
|
7809
7889
|
const pendingWork = offer["pendingWork"] === true;
|
|
7890
|
+
const warm = offer["warm"] === true;
|
|
7810
7891
|
write(
|
|
7811
7892
|
banner("handoff", `TAKING CONTROL of ${label}`, [
|
|
7812
7893
|
"pausing the bridge and opening the agent's REAL interactive session",
|
|
7813
7894
|
"on this terminal. Quit it (Ctrl-C or /exit) to hand control back.",
|
|
7814
7895
|
"",
|
|
7815
|
-
|
|
7816
|
-
|
|
7817
|
-
|
|
7818
|
-
|
|
7819
|
-
"",
|
|
7820
|
-
"
|
|
7821
|
-
"
|
|
7822
|
-
|
|
7896
|
+
...warm ? [
|
|
7897
|
+
"NOTE: this channel has NO running session - taking control will START",
|
|
7898
|
+
"one now, then drop you into it."
|
|
7899
|
+
] : [
|
|
7900
|
+
"WARNING: changes you make inside PERSIST after you leave -",
|
|
7901
|
+
"switching model or mode (or compacting/clearing) carries over into",
|
|
7902
|
+
"how this agent serves the channel afterwards.",
|
|
7903
|
+
...pendingWork ? [
|
|
7904
|
+
"",
|
|
7905
|
+
"NOTE: a reply is being worked on right now; taking control interrupts",
|
|
7906
|
+
"it. The interrupted message will re-run when you hand back."
|
|
7907
|
+
] : []
|
|
7908
|
+
]
|
|
7823
7909
|
])
|
|
7824
7910
|
);
|
|
7825
7911
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snowyroad/braid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@snowyroad/braid",
|
|
9
|
-
"version": "0.
|
|
9
|
+
"version": "0.79.2",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@agentclientprotocol/sdk": "0.25.1",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snowyroad/braid",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.2",
|
|
4
4
|
"description": "Braid: connect your own coding agent (Claude Code, Codex, Gemini, Grok) to a Braid channel and collaborate with other agents and humans.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "SnowyRoad",
|