@snowyroad/braid 0.92.3 → 0.93.1

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 CHANGED
@@ -29,7 +29,9 @@ import {
29
29
  classifyFatal,
30
30
  configDir,
31
31
  createAdapter,
32
+ createWorkspaceNoticeGate,
32
33
  decodeInvite,
34
+ distinctWorkspaceNames,
33
35
  effectiveAnthropicCredential,
34
36
  handoffCwdFor,
35
37
  hasSubscriptionLogin,
@@ -69,10 +71,12 @@ import {
69
71
  startingLines,
70
72
  toolModeLabel,
71
73
  toolStatusLine,
74
+ updateAgentWorkspace,
72
75
  validateRelayWsUrl,
73
76
  wsToHttp
74
- } from "./chunk-WJ2X7ILC.js";
77
+ } from "./chunk-KBWVFEO5.js";
75
78
  import {
79
+ clampUntrusted,
76
80
  externalSourceLabel,
77
81
  externalUntrustedPreamble,
78
82
  fence,
@@ -85,7 +89,7 @@ import {
85
89
  sameText,
86
90
  untrustedPreamble,
87
91
  utext
88
- } from "./chunk-BDOF72CL.js";
92
+ } from "./chunk-HQNAAXUM.js";
89
93
 
90
94
  // src/health.ts
91
95
  var CEILING_STRIKES_TO_DEAD = 3;
@@ -263,7 +267,7 @@ ${fence("flow discussion history", joinUntrusted(lines, "\n"))}
263
267
 
264
268
  `;
265
269
  }
266
- function buildFlowPrompt(signal, agentName, channelId, toolMode = "readonly", cause = "operator") {
270
+ function buildFlowPrompt(signal, agentName, channelId, toolMode = "readonly", cause = "operator", channelName) {
267
271
  if (signal.kind === "direction") {
268
272
  const candidates = joinUntrusted(signal.candidates ?? [], ", ");
269
273
  const history2 = renderFlowHistory(signal.history);
@@ -298,8 +302,9 @@ function buildFlowPrompt(signal, agentName, channelId, toolMode = "readonly", ca
298
302
  ${toolStatusLine(toolMode, cause)}
299
303
 
300
304
  ${header}
301
- CHANNEL: ${channelId}
302
- FLOW: ${signal.flowId}
305
+ ` + (channelName ? `CHANNEL:
306
+ ${fence("channel name", clampUntrusted(channelName, 60))}
307
+ ` : ``) + `FLOW: ${signal.flowId}
303
308
  TOPIC:
304
309
  ${fence("flow topic", signal.topic)}
305
310
  ` + role + ctx + synth + "\n" + history + closer;
@@ -357,7 +362,7 @@ function externalTaintOf(messages) {
357
362
  return { preambleLabel: "an external source", fenceLabel: "external message" };
358
363
  }
359
364
  var ChannelSession = class {
360
- constructor(adapter, onReply, agentName, channelId, flow, fetchContext, beacon, toolMode = "readonly", holdCatchUpMaxMentions = 3, fetchPrivateContext) {
365
+ constructor(adapter, onReply, agentName, channelId, flow, fetchContext, beacon, toolMode = "readonly", holdCatchUpMaxMentions = 3, fetchPrivateContext, channelName) {
361
366
  this.adapter = adapter;
362
367
  this.onReply = onReply;
363
368
  this.agentName = agentName;
@@ -368,6 +373,7 @@ var ChannelSession = class {
368
373
  this.toolMode = toolMode;
369
374
  this.holdCatchUpMaxMentions = holdCatchUpMaxMentions;
370
375
  this.fetchPrivateContext = fetchPrivateContext;
376
+ this.channelName = channelName;
371
377
  }
372
378
  adapter;
373
379
  onReply;
@@ -379,6 +385,7 @@ var ChannelSession = class {
379
385
  toolMode;
380
386
  holdCatchUpMaxMentions;
381
387
  fetchPrivateContext;
388
+ channelName;
382
389
  session = null;
383
390
  roster = [];
384
391
  /** Replace the known peer roster (situational facts surfaced per message). */
@@ -500,8 +507,11 @@ ${toolStatusLine(mode, cause)}
500
507
  ${fence("reply parent sender", msg.replyTo.senderName)}
501
508
  ${fence("reply parent content", msg.replyTo.content)}
502
509
  `;
503
- const head = this.promptHead(effectiveMode, externalLabel) + privateContext + channelContext + `You are ${this.agentName} observing a message in Braid channel ${this.channelId}.
504
- ` + // BL-057 D-b1: fixed wording only — senderName is UntrustedText and must never
510
+ const channelLabel = this.channelName?.(this.channelId);
511
+ const head = this.promptHead(effectiveMode, externalLabel) + privateContext + channelContext + (channelLabel ? `You are ${this.agentName} observing a message in a Braid channel named:
512
+ ${fence("channel name", clampUntrusted(channelLabel, 60))}
513
+ ` : `You are ${this.agentName} observing a message in a Braid channel.
514
+ `) + // BL-057 D-b1: fixed wording only — senderName is UntrustedText and must never
505
515
  // appear outside a fence (the fenced identity block above already carries it).
506
516
  (msg.channelKind === "dm" ? "This is a direct message between you and the sender identified above. Reply directly; no mention is needed.\n" : "") + `This message's id is ${msg.id} (use it as message_id if you decide to react to this message).
507
517
  FROM:
@@ -539,7 +549,14 @@ ${fence("reactions", formatReactions(msg.reactions))}
539
549
  if (history.length === 0) history = await this.flow.fetchHistory(signal.flowId);
540
550
  const effectiveMode = narrowestMode(this.lastBundleMode, this.toolMode);
541
551
  const cause = effectiveMode === "readonly" && this.toolMode !== "readonly" ? "channel" : "operator";
542
- const prompt = buildFlowPrompt({ ...signal, history }, this.agentName, this.channelId, effectiveMode, cause);
552
+ const prompt = buildFlowPrompt(
553
+ { ...signal, history },
554
+ this.agentName,
555
+ this.channelId,
556
+ effectiveMode,
557
+ cause,
558
+ this.channelName?.(this.channelId)
559
+ );
543
560
  const reply = await this.session.converseLocal(capPrompt(prompt), narrowestOverride(effectiveMode, this.toolMode));
544
561
  await this.flow.postReply(signal.flowId, reply.trim() || "(no response)", void 0, signal.dispatchId);
545
562
  } finally {
@@ -586,6 +603,10 @@ ${fence("reactions", formatReactions(msg.reactions))}
586
603
  const effectiveMode = narrowestMode(bundleMode, this.toolMode, externalMode);
587
604
  const overrideMode = narrowestOverride(effectiveMode, this.toolMode);
588
605
  const transcriptLabel = taint ? taint.fenceLabel : "missed channel messages";
606
+ const channelLabel = this.channelName?.(this.channelId);
607
+ const reconnectedClause = channelLabel ? `You just reconnected to a Braid channel named:
608
+ ${fence("channel name", clampUntrusted(channelLabel, 60))}
609
+ after being away. ` : `You just reconnected to this Braid channel after being away. `;
589
610
  const transcript = joinUntrusted(
590
611
  result.context.map((m) => utext`[${m.createdAt}] ${firstNonEmpty([m.senderName, m.senderId], "someone")}: ${m.content}${m.reactions && m.reactions.length ? utext` [reactions: ${formatReactions(m.reactions)}]` : utext``}`),
591
612
  "\n"
@@ -595,7 +616,7 @@ ${fence("reactions", formatReactions(msg.reactions))}
595
616
  this.beacon?.begin();
596
617
  try {
597
618
  await this.session.converseLocal(capPrompt(
598
- this.promptHead(effectiveMode, taint?.preambleLabel) + `You just reconnected to Braid channel ${this.channelId} after being away. Here is what you missed (context only, do NOT reply to it):
619
+ this.promptHead(effectiveMode, taint?.preambleLabel) + reconnectedClause + `Here is what you missed (context only, do NOT reply to it):
599
620
  ` + fence(transcriptLabel, transcript)
600
621
  ), overrideMode);
601
622
  } finally {
@@ -610,7 +631,9 @@ ${fence("reactions", formatReactions(msg.reactions))}
610
631
  result.mentions.map((m) => utext`[${m.createdAt}] ${firstNonEmpty([m.senderName, m.senderId], "someone")}: ${m.content}`),
611
632
  "\n"
612
633
  );
613
- const head = this.promptHead(effectiveMode, taint?.preambleLabel) + privateContext + channelContext + `You are ${this.agentName}. You just reconnected to Braid channel ${this.channelId} after being away. While you were gone, the channel said (context):
634
+ const head = this.promptHead(effectiveMode, taint?.preambleLabel) + privateContext + channelContext + `You are ${this.agentName}. ` + (channelLabel ? `You just reconnected to a Braid channel named:
635
+ ${fence("channel name", clampUntrusted(channelLabel, 60))}
636
+ after ` : `You just reconnected to this Braid channel after `) + `being away. While you were gone, the channel said (context):
614
637
  ${fence(transcriptLabel, transcript)}
615
638
 
616
639
  You were directly addressed (@mentioned) in:
@@ -641,6 +664,10 @@ ${fence(taint ? taint.fenceLabel : "messages mentioning you", addressed)}
641
664
  const effectiveMode = narrowestMode(bundleMode, this.toolMode, externalMode);
642
665
  const overrideMode = narrowestOverride(effectiveMode, this.toolMode);
643
666
  const transcriptLabel = taint ? taint.fenceLabel : "recent channel messages";
667
+ const channelLabel = this.channelName?.(this.channelId);
668
+ const joinedClause = channelLabel ? `You just joined a Braid channel named:
669
+ ${fence("channel name", clampUntrusted(channelLabel, 60))}
670
+ Here is the recent discussion so far ` : `You just joined this Braid channel. Here is the recent discussion so far `;
644
671
  const transcript = joinUntrusted(
645
672
  history.map((m) => utext`[${m.createdAt}] ${firstNonEmpty([m.senderName, m.senderId], "someone")}: ${m.content}`),
646
673
  "\n"
@@ -648,7 +675,7 @@ ${fence(taint ? taint.fenceLabel : "messages mentioning you", addressed)}
648
675
  this.beacon?.begin();
649
676
  try {
650
677
  await this.session.converseLocal(capPrompt(
651
- this.promptHead(effectiveMode, taint?.preambleLabel) + `You just joined Braid channel ${this.channelId}. Here is the recent discussion so far (context only, do NOT reply to it):
678
+ this.promptHead(effectiveMode, taint?.preambleLabel) + joinedClause + `(context only, do NOT reply to it):
652
679
  ` + fence(transcriptLabel, transcript)
653
680
  ), overrideMode);
654
681
  } finally {
@@ -2508,7 +2535,16 @@ async function startBridge(cfg, relay, deps) {
2508
2535
  cfg.catchUpMaxMentions,
2509
2536
  // BL-056 task 7: PHYSICALLY SEPARATE from fetchChannelContext above — this
2510
2537
  // agent's own private-memory lane, never the shared channel path.
2511
- (queryHint) => relay.fetchPrivateMemoryContext(queryHint)
2538
+ // D12 task 7.2: thread this session's own channelId (closure-bound above)
2539
+ // so the relay org-scopes recall to it, and the channelNames lookup (same
2540
+ // map/pattern as the handoff controller's channelName resolver, line ~647)
2541
+ // so a channel-scoped hit's attribution renders the channel NAME, never
2542
+ // its raw UUID (G7).
2543
+ (queryHint) => relay.fetchPrivateMemoryContext(channelId, queryHint, (id) => channelNames.get(id)),
2544
+ // D12 task 7.2 (review follow-up): the SAME channelNames resolver, threaded so
2545
+ // ChannelSession's own situational framing lines ("You are X observing a message
2546
+ // in Braid channel <NAME>") never fall back to the raw UUID either (G7).
2547
+ (id) => channelNames.get(id)
2512
2548
  );
2513
2549
  const broker = new SourceBroker(channelId, relay);
2514
2550
  const brokerInfo = await broker.start();
@@ -2654,7 +2690,12 @@ async function startBridge(cfg, relay, deps) {
2654
2690
  });
2655
2691
  const handoffCtl = cs.handoffGate ? makeSocketHandoffController({
2656
2692
  sessions,
2657
- channelName: (id) => channelNames.get(id),
2693
+ // Display path (handoff picker/banner): unwrap the branded name for the
2694
+ // TTY. String() is the sanctioned display unwrap (not a prompt sink).
2695
+ channelName: (id) => {
2696
+ const u = channelNames.get(id);
2697
+ return u === void 0 ? void 0 : String(u);
2698
+ },
2658
2699
  provider: cfg.agent,
2659
2700
  cwdFor: (id) => handoffCwdFor(cfg, id),
2660
2701
  scope: cfg.scope,
@@ -4547,6 +4588,10 @@ function renderStatusHuman(input) {
4547
4588
  );
4548
4589
  const ipcProfile = resolveIpcProfile(e.agent.scope, void 0);
4549
4590
  lines.push(` ipcprofile: ${ipcProfile} (${e.agent.scope?.ipcProfile !== void 0 ? "file" : "default"})`);
4591
+ if (e.agent.workspace) lines.push(` workspace: ${sanitizeForTty(e.agent.workspace)}`);
4592
+ if (e.agent.channelWorkspaces && e.agent.channelWorkspaces.length > 1) {
4593
+ lines.push(` channels in: ${e.agent.channelWorkspaces.map((w) => sanitizeForTty(w)).join(", ")}`);
4594
+ }
4550
4595
  const eps = endpointRows(e, k, input);
4551
4596
  for (const ep of eps) {
4552
4597
  const state = ep.kind === "env" ? "auto-allowed (env)" : ep.pinned ? `pinned ${ep.pinnedAt}` : "NOT PINNED \u2014 pending approval";
@@ -4579,7 +4624,11 @@ function renderStatusJson(input) {
4579
4624
  }
4580
4625
  },
4581
4626
  ipcProfile: { value: ipcProfile, origin: e.agent.scope?.ipcProfile !== void 0 ? "file" : "default" },
4582
- endpoints: endpointRows(e, k, input)
4627
+ endpoints: endpointRows(e, k, input),
4628
+ // Task 7.1 (D4): null (not "personal"/omitted key) when never cached, so machine
4629
+ // consumers can distinguish "no data yet" from an actual value.
4630
+ workspace: e.agent.workspace ?? null,
4631
+ channelWorkspaces: e.agent.channelWorkspaces ?? []
4583
4632
  };
4584
4633
  });
4585
4634
  return JSON.stringify({ agents }, null, 2);
@@ -6374,6 +6423,18 @@ async function main() {
6374
6423
  onRelayCreated: (relay) => {
6375
6424
  relayRef.current = relay;
6376
6425
  relay.onConnEvent((e) => connStatus.handle(e));
6426
+ const workspaceNoticeGate = createWorkspaceNoticeGate();
6427
+ relay.onHello(({ channelOrgs, agentWorkspace }) => {
6428
+ const notice = workspaceNoticeGate(channelOrgs);
6429
+ if (notice.length > 0) console.log(banner("braid", "WORKSPACES", notice));
6430
+ if (cfg.agentName) {
6431
+ const channelWorkspaces = distinctWorkspaceNames(channelOrgs);
6432
+ updateAgentWorkspace(configDir(process.env), cfg.agentName, {
6433
+ ...agentWorkspace ? { workspace: agentWorkspace } : {},
6434
+ ...channelWorkspaces.length > 0 ? { channelWorkspaces } : {}
6435
+ });
6436
+ }
6437
+ });
6377
6438
  }
6378
6439
  });
6379
6440
  if (cfg.runtimeVersion) {
@@ -6394,12 +6455,15 @@ async function main() {
6394
6455
  const channelLabel = (channelId) => {
6395
6456
  const name = bridge.channelNames.get(channelId);
6396
6457
  const tag = sanitizeForTty(channelId).slice(0, 8);
6397
- return name ? `#${sanitizeForTty(name).slice(0, 40)} (${tag})` : `#${tag}`;
6458
+ return name ? `#${sanitizeForTty(String(name)).slice(0, 40)} (${tag})` : `#${tag}`;
6398
6459
  };
6399
6460
  attach = startAttach({
6400
6461
  hub: bridge.hub,
6401
6462
  listChannels: () => [...bridge.sessions.keys()],
6402
- channelName: (channelId) => bridge.channelNames.get(channelId),
6463
+ channelName: (channelId) => {
6464
+ const u = bridge.channelNames.get(channelId);
6465
+ return u === void 0 ? void 0 : String(u);
6466
+ },
6403
6467
  stdin: process.stdin,
6404
6468
  stdout: stickyWriteThrough(sticky, process.stdout),
6405
6469
  onInterrupt: () => {
@@ -4,8 +4,8 @@ import {
4
4
  boundedJsonFetch,
5
5
  resetHttpMetricsForTest,
6
6
  snapshotHttpMetrics
7
- } from "./chunk-WJ2X7ILC.js";
8
- import "./chunk-BDOF72CL.js";
7
+ } from "./chunk-KBWVFEO5.js";
8
+ import "./chunk-HQNAAXUM.js";
9
9
  export {
10
10
  assertHttpEndpoint,
11
11
  boundedJsonFetch,
@@ -3,8 +3,9 @@ import {
3
3
  applyEnvAliases
4
4
  } from "../chunk-SHUJGYPW.js";
5
5
  import {
6
- neutralizeMarkers
7
- } from "../chunk-BDOF72CL.js";
6
+ neutralizeMarkers,
7
+ resolveChannelLabel
8
+ } from "../chunk-HQNAAXUM.js";
8
9
 
9
10
  // src/mcp/server.ts
10
11
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -296,7 +297,9 @@ ${lines.join("\n")}
296
297
  const results = out?.results ?? [];
297
298
  if (results.length === 0) return "No matching entries found in your private memory.";
298
299
  const lines = results.map((r) => {
299
- const channel = typeof r?.channelId === "string" && r.channelId ? ` (channel ${neutralizeMarkers(r.channelId)})` : "";
300
+ const channelId = typeof r?.channelId === "string" && r.channelId ? r.channelId : void 0;
301
+ const label = resolveChannelLabel(channelId);
302
+ const channel = label ? ` (channel ${neutralizeMarkers(String(label))})` : "";
300
303
  const snippet = neutralizeMarkers(String(r?.content ?? ""));
301
304
  return `- ${snippet}${channel}`;
302
305
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snowyroad/braid",
3
- "version": "0.92.3",
3
+ "version": "0.93.1",
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",
@@ -51,7 +51,7 @@
51
51
  "@anthropic-ai/sandbox-runtime": "0.0.63",
52
52
  "@anthropic-ai/sdk": "0.104.1",
53
53
  "@modelcontextprotocol/sdk": "1.29.0",
54
- "@snowyroad/braid-tui": "0.0.28",
54
+ "@snowyroad/braid-tui": "0.0.30",
55
55
  "canonicalize": "^3.0.0",
56
56
  "diff": "^9.0.0",
57
57
  "jose": "^6.2.3",