pubblue 0.6.9 → 0.7.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.
@@ -5,6 +5,7 @@ import {
5
5
  buildClaudeArgs,
6
6
  buildSessionBriefing,
7
7
  createClaudeCodeBridgeRunner,
8
+ createClaudeSdkBridgeRunner,
8
9
  createOpenClawBridgeRunner,
9
10
  decodeMessage,
10
11
  encodeMessage,
@@ -14,12 +15,20 @@ import {
14
15
  makeDeliveryReceiptMessage,
15
16
  makeEventMessage,
16
17
  parseAckMessage,
18
+ parseIpcRequest,
19
+ parseLiveInfo,
20
+ readFiniteNumber,
17
21
  readLatestCliVersion,
22
+ readNonEmptyString,
23
+ readRecord,
24
+ readString,
25
+ readStringArray,
26
+ readStringRecord,
18
27
  resolveClaudeCodePath,
19
28
  resolveOpenClawRuntime,
20
29
  shouldAcknowledgeMessage,
21
30
  writeLiveSessionContentFile
22
- } from "./chunk-JSX5KHV3.js";
31
+ } from "./chunk-5LI2HLKX.js";
23
32
 
24
33
  // src/lib/live-daemon.ts
25
34
  import { randomUUID } from "crypto";
@@ -33,51 +42,37 @@ function resolveAckChannel(input) {
33
42
  return null;
34
43
  }
35
44
 
45
+ // ../shared/webrtc-transport-core.ts
46
+ var WEBRTC_STUN_URLS = [
47
+ "stun:stun.l.google.com:19302",
48
+ "stun:stun1.l.google.com:19302"
49
+ ];
50
+ var WEBRTC_ICE_SERVER_CONFIG = [
51
+ { urls: WEBRTC_STUN_URLS[0] },
52
+ { urls: WEBRTC_STUN_URLS[1] }
53
+ ];
54
+ var ORDERED_DATA_CHANNEL_OPTIONS = {
55
+ ordered: true
56
+ };
57
+
36
58
  // src/lib/live-command-handler.ts
37
59
  import { spawn } from "child_process";
38
60
 
39
61
  // ../shared/command-protocol-core.ts
40
62
  var COMMAND_PROTOCOL_VERSION = 1;
41
63
  var COMMAND_MANIFEST_MAX_FUNCTIONS = 64;
42
- function makeCommandBindResultMessage(payload) {
43
- return makeEventMessage("command.bind.result", payload);
44
- }
64
+ var COMMAND_MANIFEST_MIME = "application/pubblue-command-manifest+json";
45
65
  function makeCommandResultMessage(payload) {
46
66
  return makeEventMessage("command.result", payload);
47
67
  }
48
- function readRecord(input) {
49
- return input && typeof input === "object" && !Array.isArray(input) ? input : null;
50
- }
51
- function readString(input) {
52
- return typeof input === "string" && input.trim().length > 0 ? input : void 0;
53
- }
54
68
  function readReturnType(input) {
55
69
  if (input === "void" || input === "text" || input === "json") return input;
56
70
  return void 0;
57
71
  }
58
- function readFiniteNumber(input) {
59
- if (typeof input !== "number" || !Number.isFinite(input)) return void 0;
60
- return input;
61
- }
62
- function readStringArray(input) {
63
- if (!Array.isArray(input)) return void 0;
64
- const values = input.filter((entry) => typeof entry === "string");
65
- return values.length === input.length ? values : void 0;
66
- }
67
- function readStringRecord(input) {
68
- const record = readRecord(input);
69
- if (!record) return void 0;
70
- const values = Object.entries(record).filter((entry) => {
71
- const [_key, value] = entry;
72
- return typeof value === "string";
73
- });
74
- if (values.length !== Object.keys(record).length) return void 0;
75
- return Object.fromEntries(values);
76
- }
77
72
  function parseExecutor(input) {
78
73
  const record = readRecord(input);
79
74
  if (!record) return void 0;
80
- const kind = readString(record.kind);
75
+ const kind = readNonEmptyString(record.kind);
81
76
  if (!kind) return void 0;
82
77
  if (kind === "exec") {
83
78
  const command = readString(record.command);
@@ -122,13 +117,13 @@ function parseExecutor(input) {
122
117
  function parseFunctionSpec(input, fallbackName) {
123
118
  const record = readRecord(input);
124
119
  if (!record) return null;
125
- const name = readString(record.name) ?? fallbackName;
120
+ const name = readNonEmptyString(record.name) ?? fallbackName;
126
121
  if (!name) return null;
127
122
  return {
128
123
  name,
129
124
  returns: readReturnType(record.returns),
130
125
  timeoutMs: readFiniteNumber(record.timeoutMs),
131
- description: readString(record.description),
126
+ description: readNonEmptyString(record.description),
132
127
  executor: parseExecutor(record.executor)
133
128
  };
134
129
  }
@@ -143,43 +138,62 @@ function parseFunctionList(input) {
143
138
  function parseMetaRecord(msg) {
144
139
  return msg.type === "event" && msg.meta ? readRecord(msg.meta) : null;
145
140
  }
146
- function parseCommandBindMessage(msg) {
147
- if (msg.type !== "event" || msg.data !== "command.bind") return null;
148
- const meta = parseMetaRecord(msg);
141
+ function parseCommandInvokePayload(input) {
142
+ const meta = readRecord(input);
149
143
  if (!meta) return null;
150
- const manifestId = readString(meta.manifestId);
151
- if (!manifestId) return null;
144
+ const callId = readNonEmptyString(meta.callId);
145
+ const name = readNonEmptyString(meta.name);
146
+ if (!callId || !name) return null;
152
147
  return {
153
148
  v: readFiniteNumber(meta.v) ?? COMMAND_PROTOCOL_VERSION,
154
- manifestId,
155
- functions: parseFunctionList(meta.functions)
149
+ callId,
150
+ name,
151
+ args: readRecord(meta.args) ?? void 0,
152
+ timeoutMs: readFiniteNumber(meta.timeoutMs)
156
153
  };
157
154
  }
158
155
  function parseCommandInvokeMessage(msg) {
159
156
  if (msg.type !== "event" || msg.data !== "command.invoke") return null;
160
- const meta = parseMetaRecord(msg);
157
+ return parseCommandInvokePayload(parseMetaRecord(msg));
158
+ }
159
+ function parseCommandCancelPayload(input) {
160
+ const meta = readRecord(input);
161
161
  if (!meta) return null;
162
- const callId = readString(meta.callId);
163
- const name = readString(meta.name);
164
- if (!callId || !name) return null;
162
+ const callId = readNonEmptyString(meta.callId);
163
+ if (!callId) return null;
165
164
  return {
166
165
  v: readFiniteNumber(meta.v) ?? COMMAND_PROTOCOL_VERSION,
167
166
  callId,
168
- name,
169
- args: readRecord(meta.args) ?? void 0,
170
- timeoutMs: readFiniteNumber(meta.timeoutMs)
167
+ reason: readNonEmptyString(meta.reason)
171
168
  };
172
169
  }
173
170
  function parseCommandCancelMessage(msg) {
174
171
  if (msg.type !== "event" || msg.data !== "command.cancel") return null;
175
- const meta = parseMetaRecord(msg);
176
- if (!meta) return null;
177
- const callId = readString(meta.callId);
178
- if (!callId) return null;
172
+ return parseCommandCancelPayload(parseMetaRecord(msg));
173
+ }
174
+ var MANIFEST_SCRIPT_RE = new RegExp(
175
+ `<script\\s[^>]*type\\s*=\\s*["']${COMMAND_MANIFEST_MIME.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}["'][^>]*>([\\s\\S]*?)<\\/script>`,
176
+ "i"
177
+ );
178
+ function extractManifestFromHtml(html) {
179
+ const match = MANIFEST_SCRIPT_RE.exec(html);
180
+ if (!match?.[1]) return null;
181
+ const raw = match[1].trim();
182
+ if (raw.length === 0) return null;
183
+ let parsed;
184
+ try {
185
+ parsed = JSON.parse(raw);
186
+ } catch {
187
+ return null;
188
+ }
189
+ if (!parsed || typeof parsed !== "object") return null;
190
+ const record = parsed;
191
+ const manifestId = typeof record.manifestId === "string" && record.manifestId.length > 0 ? record.manifestId : `manifest-${Date.now().toString(36)}`;
192
+ const functions = parseFunctionList(record.functions);
179
193
  return {
180
- v: readFiniteNumber(meta.v) ?? COMMAND_PROTOCOL_VERSION,
181
- callId,
182
- reason: readString(meta.reason)
194
+ v: typeof record.version === "number" ? record.version : 1,
195
+ manifestId,
196
+ functions
183
197
  };
184
198
  }
185
199
 
@@ -414,6 +428,10 @@ function createLiveCommandHandler(params) {
414
428
  const boundFunctions = /* @__PURE__ */ new Map();
415
429
  const running = /* @__PURE__ */ new Map();
416
430
  const recentResults = /* @__PURE__ */ new Map();
431
+ function clearBindings() {
432
+ boundFunctions.clear();
433
+ params.debugLog("commands cleared bindings");
434
+ }
417
435
  function buildCancelledResult(callId, startedAt) {
418
436
  return {
419
437
  v: COMMAND_PROTOCOL_VERSION,
@@ -433,9 +451,6 @@ function createLiveCommandHandler(params) {
433
451
  });
434
452
  await params.sendCommandMessage(makeCommandResultMessage(payload));
435
453
  }
436
- async function sendBindResult(payload) {
437
- await params.sendCommandMessage(makeCommandBindResultMessage(payload));
438
- }
439
454
  async function executeFunction(spec, args, abortSignal) {
440
455
  const executor = spec.executor;
441
456
  if (!executor) {
@@ -498,39 +513,27 @@ function createLiveCommandHandler(params) {
498
513
  signal: abortSignal
499
514
  });
500
515
  }
501
- async function handleBind(message) {
502
- params.debugLog(
503
- `command:bind manifestId=${message.manifestId} functions=[${message.functions.map((f) => f.name).join(", ")}]`
504
- );
505
- const accepted = [];
506
- const rejected = [];
507
- boundFunctions.clear();
508
- for (const entry of message.functions) {
516
+ function bindFunctions(functions) {
517
+ clearBindings();
518
+ for (const entry of functions) {
509
519
  const normalized = normalizeFunctionSpec(entry);
510
520
  if (!normalized.executor) {
511
- params.debugLog(`command:bind rejected "${normalized.name}" \u2014 missing executor`);
512
- rejected.push({
513
- name: normalized.name,
514
- code: "INVALID_FUNCTION",
515
- message: `Function "${normalized.name}" is missing executor definition.`
516
- });
521
+ params.debugLog(`commands skipped "${normalized.name}" \u2014 missing executor`);
517
522
  continue;
518
523
  }
519
524
  boundFunctions.set(normalized.name, normalized);
520
- accepted.push({
521
- name: normalized.name,
522
- returns: normalized.returns ?? "void"
523
- });
524
525
  }
525
- params.debugLog(
526
- `command:bind result accepted=[${accepted.map((a) => a.name).join(", ")}] rejected=[${rejected.map((r) => r.name).join(", ")}]`
527
- );
528
- await sendBindResult({
529
- v: COMMAND_PROTOCOL_VERSION,
530
- manifestId: message.manifestId,
531
- accepted,
532
- rejected
533
- });
526
+ params.debugLog(`commands bound=[${[...boundFunctions.keys()].join(", ")}]`);
527
+ }
528
+ function bindFromHtml(html) {
529
+ const manifest = extractManifestFromHtml(html);
530
+ if (!manifest) {
531
+ clearBindings();
532
+ params.debugLog("commands no manifest found in HTML");
533
+ return;
534
+ }
535
+ params.debugLog(`commands manifestId=${manifest.manifestId}`);
536
+ bindFunctions(manifest.functions);
534
537
  }
535
538
  async function handleInvoke(message) {
536
539
  if (!message) return;
@@ -552,7 +555,7 @@ function createLiveCommandHandler(params) {
552
555
  }
553
556
  const spec = getSpec(message.name);
554
557
  if (!spec) {
555
- params.debugLog(`command:invoke COMMAND_NOT_FOUND "${message.name}"`);
558
+ params.debugLog(`commands invoke COMMAND_NOT_FOUND "${message.name}"`);
556
559
  await sendResult({
557
560
  v: COMMAND_PROTOCOL_VERSION,
558
561
  callId: message.callId,
@@ -566,7 +569,7 @@ function createLiveCommandHandler(params) {
566
569
  return;
567
570
  }
568
571
  params.debugLog(
569
- `command:invoke "${message.name}" callId=${message.callId} args=${JSON.stringify(message.args ?? {}).slice(0, 200)}`
572
+ `commands invoke "${message.name}" callId=${message.callId} args=${JSON.stringify(message.args ?? {}).slice(0, 200)}`
570
573
  );
571
574
  const abort = new AbortController();
572
575
  const startedAt = Date.now();
@@ -576,14 +579,14 @@ function createLiveCommandHandler(params) {
576
579
  const active = running.get(message.callId);
577
580
  if (abort.signal.aborted || active?.cancelled) {
578
581
  params.debugLog(
579
- `command:invoke "${message.name}" cancelled after ${Date.now() - startedAt}ms`
582
+ `commands invoke "${message.name}" cancelled after ${Date.now() - startedAt}ms`
580
583
  );
581
584
  await sendResult(buildCancelledResult(message.callId, startedAt));
582
585
  return;
583
586
  }
584
587
  const durationMs = Date.now() - startedAt;
585
588
  params.debugLog(
586
- `command:invoke "${message.name}" ok=${true} duration=${durationMs}ms value=${JSON.stringify(value).slice(0, 200)}`
589
+ `commands invoke "${message.name}" ok=${true} duration=${durationMs}ms value=${JSON.stringify(value).slice(0, 200)}`
587
590
  );
588
591
  await sendResult({
589
592
  v: COMMAND_PROTOCOL_VERSION,
@@ -600,7 +603,7 @@ function createLiveCommandHandler(params) {
600
603
  }
601
604
  const durationMs = Date.now() - startedAt;
602
605
  params.debugLog(
603
- `command:invoke "${message.name}" FAILED duration=${durationMs}ms error=${detail.slice(0, 300)}`
606
+ `commands invoke "${message.name}" FAILED duration=${durationMs}ms error=${detail.slice(0, 300)}`
604
607
  );
605
608
  await sendResult({
606
609
  v: COMMAND_PROTOCOL_VERSION,
@@ -623,18 +626,13 @@ function createLiveCommandHandler(params) {
623
626
  async function handleBridgeMessage(message) {
624
627
  if (message.type !== "event") return;
625
628
  params.debugLog(
626
- `command:message type=${message.type} data=${typeof message.data === "string" ? message.data.slice(0, 120) : "?"}`
629
+ `commands message type=${message.type} data=${typeof message.data === "string" ? message.data.slice(0, 120) : "?"}`
627
630
  );
628
631
  for (const [callId, result] of recentResults) {
629
632
  if (result.expiresAt <= Date.now()) {
630
633
  recentResults.delete(callId);
631
634
  }
632
635
  }
633
- const bind = parseCommandBindMessage(message);
634
- if (bind) {
635
- await handleBind(bind);
636
- return;
637
- }
638
636
  const invoke = parseCommandInvokeMessage(message);
639
637
  if (invoke) {
640
638
  await handleInvoke(invoke);
@@ -646,11 +644,18 @@ function createLiveCommandHandler(params) {
646
644
  }
647
645
  }
648
646
  return {
647
+ bindFromHtml(html) {
648
+ bindFromHtml(html);
649
+ },
650
+ clearBindings() {
651
+ clearBindings();
652
+ },
649
653
  stop() {
650
654
  for (const [callId, active] of running) {
651
655
  active.abort.abort();
652
656
  running.delete(callId);
653
657
  }
658
+ clearBindings();
654
659
  },
655
660
  async onMessage(message) {
656
661
  await handleBridgeMessage(message).catch((error) => {
@@ -752,14 +757,33 @@ function createAnswer(peer, browserOffer, timeoutMs) {
752
757
  }
753
758
 
754
759
  // src/lib/live-daemon-ipc-handler.ts
760
+ function unreachableIpcRequest(request) {
761
+ throw new Error(`Unsupported IPC request: ${JSON.stringify(request)}`);
762
+ }
755
763
  function createDaemonIpcHandler(params) {
756
764
  return async function handleIpcRequest(req) {
757
765
  switch (req.method) {
758
766
  case "write": {
759
767
  const channel = req.params.channel || "chat";
768
+ const msg = req.params.msg;
769
+ if (channel === "canvas" && msg.type === "html" && typeof msg.data === "string") {
770
+ const slug = params.getActiveSlug();
771
+ if (!slug) return { ok: false, error: "No active live session." };
772
+ try {
773
+ await params.apiClient.update({
774
+ slug,
775
+ content: msg.data
776
+ });
777
+ params.bindCanvasCommands(msg.data);
778
+ return { ok: true, delivered: true };
779
+ } catch (error) {
780
+ const errMsg = error instanceof Error ? error.message : String(error);
781
+ params.markError(`failed to persist canvas HTML for "${slug}"`, error);
782
+ return { ok: false, error: `Canvas update failed: ${errMsg}` };
783
+ }
784
+ }
760
785
  const readinessError = params.getWriteReadinessError();
761
786
  if (readinessError) return { ok: false, error: readinessError };
762
- const msg = req.params.msg;
763
787
  const binaryBase64 = typeof req.params.binaryBase64 === "string" ? req.params.binaryBase64 : void 0;
764
788
  const binaryPayload = msg.type === "binary" && binaryBase64 ? Buffer.from(binaryBase64, "base64") : void 0;
765
789
  const maxAttempts = Math.max(1, params.writeAckMaxAttempts);
@@ -809,7 +833,6 @@ function createDaemonIpcHandler(params) {
809
833
  continue;
810
834
  }
811
835
  }
812
- params.trackOutboundMessage(channel, msg);
813
836
  return { ok: true, delivered: true };
814
837
  }
815
838
  return {
@@ -855,8 +878,9 @@ function createDaemonIpcHandler(params) {
855
878
  params.shutdown();
856
879
  return { ok: true };
857
880
  }
858
- default:
859
- return { ok: false, error: `Unknown method: ${req.method}` };
881
+ default: {
882
+ return unreachableIpcRequest(req);
883
+ }
860
884
  }
861
885
  };
862
886
  }
@@ -872,17 +896,22 @@ function createDaemonIpcServer(handler) {
872
896
  if (newlineIdx === -1) return;
873
897
  const line = data.slice(0, newlineIdx);
874
898
  data = data.slice(newlineIdx + 1);
875
- let request;
876
899
  try {
877
- request = JSON.parse(line);
900
+ const request = parseIpcRequest(JSON.parse(line));
901
+ if (!request) {
902
+ conn.write(`${JSON.stringify({ ok: false, error: "Invalid request" })}
903
+ `);
904
+ return;
905
+ }
906
+ handler(request).then((response) => conn.write(`${JSON.stringify(response)}
907
+ `)).catch(
908
+ (err) => conn.write(`${JSON.stringify({ ok: false, error: errorMessage(err) })}
909
+ `)
910
+ );
878
911
  } catch {
879
912
  conn.write(`${JSON.stringify({ ok: false, error: "Invalid JSON" })}
880
913
  `);
881
- return;
882
914
  }
883
- handler(request).then((response) => conn.write(`${JSON.stringify(response)}
884
- `)).catch((err) => conn.write(`${JSON.stringify({ ok: false, error: errorMessage(err) })}
885
- `));
886
915
  });
887
916
  });
888
917
  }
@@ -937,7 +966,7 @@ var CANVAS_COMMAND_PROTOCOL_GUIDE_MARKDOWN = [
937
966
 
938
967
  // src/lib/live-daemon-shared.ts
939
968
  function buildBridgeInstructions(mode) {
940
- if (mode === "claude-code") {
969
+ if (mode === "claude-code" || mode === "claude-sdk") {
941
970
  return {
942
971
  replyHint: 'Reply command: pubblue write "<your reply>"',
943
972
  canvasHint: "Canvas command: pubblue write -c canvas -f /path/to/file.html",
@@ -974,13 +1003,6 @@ function shouldRecoverForBrowserOfferChange(params) {
974
1003
  if (!lastAppliedBrowserOffer) return false;
975
1004
  return incomingBrowserOffer !== lastAppliedBrowserOffer;
976
1005
  }
977
- function readCanvasHtmlFromOutbound(params) {
978
- if (params.channel !== CHANNELS.CANVAS) return null;
979
- if (params.msg.type !== "html") return null;
980
- if (typeof params.msg.data !== "string") return null;
981
- if (params.msg.data.length === 0) return null;
982
- return params.msg.data;
983
- }
984
1006
 
985
1007
  // src/lib/live-daemon-signaling.ts
986
1008
  import { ConvexClient } from "convex/browser";
@@ -990,6 +1012,9 @@ import { makeFunctionReference } from "convex/server";
990
1012
  function decideSignalingUpdate(params) {
991
1013
  const { live, activeSlug, lastAppliedBrowserOffer, lastBrowserCandidateCount } = params;
992
1014
  if (!live) {
1015
+ if (activeSlug !== null || lastAppliedBrowserOffer !== null || lastBrowserCandidateCount > 0) {
1016
+ return { type: "clear-live", nextBrowserCandidateCount: 0 };
1017
+ }
993
1018
  return { type: "noop", nextBrowserCandidateCount: lastBrowserCandidateCount };
994
1019
  }
995
1020
  if (live.browserOffer && !live.agentAnswer) {
@@ -1020,33 +1045,13 @@ function decideSignalingUpdate(params) {
1020
1045
  }
1021
1046
 
1022
1047
  // src/lib/live-daemon-signaling.ts
1023
- var LIVE_SIGNAL_QUERY = makeFunctionReference("pubs:getLiveForAgentByApiKey");
1048
+ var LIVE_SIGNAL_QUERY = makeFunctionReference("pubs:getLive");
1024
1049
  function parseLiveSnapshot(result) {
1025
- if (result === null || result === void 0) return null;
1026
- if (typeof result !== "object") {
1050
+ const live = parseLiveInfo(result);
1051
+ if (result !== null && result !== void 0 && live === null) {
1027
1052
  throw new Error("Invalid signaling snapshot: expected object or null");
1028
1053
  }
1029
- const live = result;
1030
- if (typeof live.slug !== "string") throw new Error("Invalid signaling snapshot: missing slug");
1031
- if (!Array.isArray(live.browserCandidates)) {
1032
- throw new Error("Invalid signaling snapshot: missing browserCandidates");
1033
- }
1034
- if (!Array.isArray(live.agentCandidates)) {
1035
- throw new Error("Invalid signaling snapshot: missing agentCandidates");
1036
- }
1037
- if (typeof live.createdAt !== "number" || typeof live.expiresAt !== "number") {
1038
- throw new Error("Invalid signaling snapshot: missing timestamps");
1039
- }
1040
- return {
1041
- slug: live.slug,
1042
- status: live.status,
1043
- browserOffer: live.browserOffer,
1044
- agentAnswer: live.agentAnswer,
1045
- browserCandidates: live.browserCandidates,
1046
- agentCandidates: live.agentCandidates,
1047
- createdAt: live.createdAt,
1048
- expiresAt: live.expiresAt
1049
- };
1054
+ return live;
1050
1055
  }
1051
1056
  function createSignalingController(params) {
1052
1057
  const {
@@ -1060,7 +1065,8 @@ function createSignalingController(params) {
1060
1065
  getLastBrowserCandidateCount,
1061
1066
  setLastBrowserCandidateCount,
1062
1067
  onRecover,
1063
- onApplyBrowserCandidates
1068
+ onApplyBrowserCandidates,
1069
+ onClearLive
1064
1070
  } = params;
1065
1071
  let signalingClient = null;
1066
1072
  let signalingUnsubscribe = null;
@@ -1103,6 +1109,10 @@ function createSignalingController(params) {
1103
1109
  await onRecover(decision.slug, decision.browserOffer);
1104
1110
  return;
1105
1111
  }
1112
+ if (decision.type === "clear-live") {
1113
+ await onClearLive();
1114
+ return;
1115
+ }
1106
1116
  if (decision.type === "apply-browser-candidates") {
1107
1117
  await onApplyBrowserCandidates(decision.candidatePayloads);
1108
1118
  }
@@ -1203,9 +1213,6 @@ async function startDaemon(config) {
1203
1213
  let pendingInboundBinaryMeta = /* @__PURE__ */ new Map();
1204
1214
  let inboundStreams = /* @__PURE__ */ new Map();
1205
1215
  let seenInboundMessageKeys = /* @__PURE__ */ new Set();
1206
- let lastCanvasSnapshot = null;
1207
- let lastPersistedCanvasSnapshot = null;
1208
- let persistCanvasQueue = Promise.resolve();
1209
1216
  let heartbeatTimer = null;
1210
1217
  let localCandidateInterval = null;
1211
1218
  let localCandidateStopTimer = null;
@@ -1222,14 +1229,10 @@ async function startDaemon(config) {
1222
1229
  markError,
1223
1230
  sendCommandMessage: async (msg) => {
1224
1231
  if (!isLiveConnected()) return false;
1225
- const sent = await sendOutboundMessageWithAck(CHANNELS.COMMAND, msg, {
1232
+ return sendOutboundMessageWithAck(CHANNELS.COMMAND, msg, {
1226
1233
  context: 'command outbound on "command"',
1227
1234
  maxAttempts: OUTBOUND_SEND_MAX_ATTEMPTS
1228
1235
  });
1229
- if (sent) {
1230
- trackOutboundMessage(CHANNELS.COMMAND, msg);
1231
- }
1232
- return sent;
1233
1236
  }
1234
1237
  });
1235
1238
  function debugLog(message, error) {
@@ -1318,64 +1321,23 @@ async function startDaemon(config) {
1318
1321
  runHealthCheck();
1319
1322
  }
1320
1323
  function appendBufferedMessage(entry) {
1321
- if (entry.channel === CHANNELS.CANVAS || entry.channel === CHANNELS.COMMAND) return;
1324
+ if (entry.channel === CHANNELS.COMMAND) return;
1322
1325
  buffer.messages.push(entry);
1323
1326
  if (buffer.messages.length > MAX_BUFFERED_MESSAGES) {
1324
1327
  buffer.messages.splice(0, buffer.messages.length - MAX_BUFFERED_MESSAGES);
1325
1328
  }
1326
1329
  }
1327
- function getActiveCanvasSnapshot() {
1328
- if (!activeSlug || !lastCanvasSnapshot) return null;
1329
- if (lastCanvasSnapshot.slug !== activeSlug) return null;
1330
- return lastCanvasSnapshot;
1331
- }
1332
- function isSameCanvasSnapshot(a, b) {
1333
- if (!a || !b) return false;
1334
- return a.slug === b.slug && a.html === b.html;
1335
- }
1336
- function queuePersistCanvasSnapshot(snapshot, reason) {
1337
- if (!snapshot) return Promise.resolve();
1338
- if (isSameCanvasSnapshot(lastPersistedCanvasSnapshot, snapshot)) return Promise.resolve();
1339
- persistCanvasQueue = persistCanvasQueue.then(async () => {
1340
- if (isSameCanvasSnapshot(lastPersistedCanvasSnapshot, snapshot)) return;
1341
- try {
1342
- await apiClient2.update({
1343
- slug: snapshot.slug,
1344
- content: snapshot.html,
1345
- filename: "live-canvas.html"
1346
- });
1347
- lastPersistedCanvasSnapshot = snapshot;
1348
- debugLog(`persisted latest canvas for "${snapshot.slug}" (${reason})`);
1349
- } catch (error) {
1350
- markError(`failed to persist latest canvas for "${snapshot.slug}" (${reason})`, error);
1351
- if (!debugEnabled) {
1352
- console.error(
1353
- `[pubblue-agent] failed to persist latest canvas for "${snapshot.slug}" (${reason}): ${errorMessage(error)}`
1354
- );
1355
- }
1356
- }
1357
- });
1358
- return persistCanvasQueue;
1359
- }
1360
- function trackOutboundMessage(channel, msg) {
1361
- const html = readCanvasHtmlFromOutbound({ channel, msg });
1362
- if (!html || !activeSlug) return;
1363
- lastCanvasSnapshot = { slug: activeSlug, html };
1364
- }
1365
1330
  function handleConnectionClosed(reason) {
1366
- void queuePersistCanvasSnapshot(getActiveCanvasSnapshot(), reason);
1367
- const hadConnection = browserConnected || bridgePrimed;
1368
- browserConnected = false;
1369
- bridgePrimed = false;
1370
- bridgePriming = null;
1371
- if (bridgeAbort) {
1372
- bridgeAbort.abort();
1373
- bridgeAbort = null;
1374
- }
1375
- if (!hadConnection) return;
1376
- buffer.messages = [];
1377
- failPendingAcks();
1378
- stopPingPong();
1331
+ debugLog(`connection closed: ${reason}`);
1332
+ const hadSession = browserConnected || bridgePrimed || activeSlug !== null;
1333
+ if (!hadSession) return;
1334
+ activeSlug = null;
1335
+ commandHandler.stop();
1336
+ resetNegotiationState();
1337
+ closeCurrentPeer();
1338
+ void stopBridge().catch((error) => {
1339
+ markError("failed to stop bridge after connection closed", error);
1340
+ });
1379
1341
  }
1380
1342
  function emitDeliveryStatus(params) {
1381
1343
  if (!params.messageId || params.channel === CONTROL_CHANNEL) return;
@@ -1599,7 +1561,7 @@ async function startDaemon(config) {
1599
1561
  if (!peer) throw new Error("PeerConnection not initialized");
1600
1562
  const existing = channels.get(name);
1601
1563
  if (existing) return existing;
1602
- const dc = peer.createDataChannel(name, { ordered: true });
1564
+ const dc = peer.createDataChannel(name, ORDERED_DATA_CHANNEL_OPTIONS);
1603
1565
  setupChannel(name, dc);
1604
1566
  return dc;
1605
1567
  }
@@ -1692,7 +1654,7 @@ async function startDaemon(config) {
1692
1654
  }
1693
1655
  function createPeer() {
1694
1656
  const nextPeer = new ndc.PeerConnection("agent", {
1695
- iceServers: ["stun:stun.l.google.com:19302", "stun:stun1.l.google.com:19302"]
1657
+ iceServers: [...WEBRTC_STUN_URLS]
1696
1658
  });
1697
1659
  peer = nextPeer;
1698
1660
  channels = /* @__PURE__ */ new Map();
@@ -1738,6 +1700,15 @@ async function startDaemon(config) {
1738
1700
  inboundStreams.clear();
1739
1701
  seenInboundMessageKeys.clear();
1740
1702
  }
1703
+ async function clearActiveLiveSession(reason) {
1704
+ const slug = activeSlug;
1705
+ debugLog(`clearing active live session: ${reason}${slug ? ` (${slug})` : ""}`);
1706
+ activeSlug = null;
1707
+ await stopBridge();
1708
+ commandHandler.stop();
1709
+ closeCurrentPeer();
1710
+ resetNegotiationState();
1711
+ }
1741
1712
  function startLocalCandidateFlush(slug) {
1742
1713
  clearLocalCandidateTimers();
1743
1714
  localCandidateInterval = setInterval(async () => {
@@ -1756,14 +1727,8 @@ async function startDaemon(config) {
1756
1727
  if (recovering) return;
1757
1728
  recovering = true;
1758
1729
  try {
1759
- const previousCanvasSnapshot = getActiveCanvasSnapshot();
1760
- if (previousCanvasSnapshot && previousCanvasSnapshot.slug !== slug) {
1761
- void queuePersistCanvasSnapshot(previousCanvasSnapshot, `session-switch:${slug}`);
1762
- }
1763
- await stopBridge();
1764
- closeCurrentPeer();
1730
+ await clearActiveLiveSession("incoming-live-recovery");
1765
1731
  createPeer();
1766
- resetNegotiationState();
1767
1732
  if (!peer) throw new Error("PeerConnection not initialized");
1768
1733
  const answer = await createAnswer(peer, browserOffer, OFFER_TIMEOUT_MS);
1769
1734
  lastAppliedBrowserOffer = browserOffer;
@@ -1802,7 +1767,10 @@ async function startDaemon(config) {
1802
1767
  lastBrowserCandidateCount = count;
1803
1768
  },
1804
1769
  onRecover: handleIncomingLive,
1805
- onApplyBrowserCandidates: applyBrowserCandidates
1770
+ onApplyBrowserCandidates: applyBrowserCandidates,
1771
+ onClearLive: async () => {
1772
+ await clearActiveLiveSession("signaling-cleared");
1773
+ }
1806
1774
  });
1807
1775
  if (fs.existsSync(socketPath2)) {
1808
1776
  let stale = true;
@@ -1834,6 +1802,8 @@ async function startDaemon(config) {
1834
1802
  }
1835
1803
  }, HEARTBEAT_INTERVAL_MS);
1836
1804
  const handleIpcRequest = createDaemonIpcHandler({
1805
+ apiClient: apiClient2,
1806
+ bindCanvasCommands: (html) => commandHandler.bindFromHtml(html),
1837
1807
  getConnected: () => isLiveConnected(),
1838
1808
  getSignalingConnected: () => {
1839
1809
  const state = signaling.status();
@@ -1854,7 +1824,6 @@ async function startDaemon(config) {
1854
1824
  waitForChannelOpen,
1855
1825
  waitForDeliveryAck,
1856
1826
  settlePendingAck,
1857
- trackOutboundMessage,
1858
1827
  markError,
1859
1828
  shutdown: () => {
1860
1829
  void shutdown();
@@ -1874,28 +1843,24 @@ async function startDaemon(config) {
1874
1843
  signaling.start();
1875
1844
  async function sendOnChannel(channel, msg) {
1876
1845
  if (stopped || !isLiveConnected()) return false;
1877
- const sent = await sendOutboundMessageWithAck(channel, msg, {
1846
+ return sendOutboundMessageWithAck(channel, msg, {
1878
1847
  context: `bridge outbound on "${channel}"`,
1879
1848
  maxAttempts: OUTBOUND_SEND_MAX_ATTEMPTS
1880
1849
  });
1881
- if (sent) {
1882
- trackOutboundMessage(channel, msg);
1883
- }
1884
- return sent;
1885
1850
  }
1886
1851
  async function buildInitialSessionBriefing(params) {
1887
1852
  const pub = await apiClient2.get(params.slug);
1888
1853
  const content = typeof pub.content === "string" ? pub.content : "";
1854
+ if (content.length > 0) commandHandler.bindFromHtml(content);
1855
+ else commandHandler.clearBindings();
1889
1856
  const canvasContentFilePath = content.length > 0 ? writeLiveSessionContentFile({
1890
1857
  slug: params.slug,
1891
- contentType: pub.contentType,
1892
1858
  content
1893
1859
  }) : void 0;
1894
1860
  return buildSessionBriefing(
1895
1861
  params.slug,
1896
1862
  {
1897
1863
  title: pub.title,
1898
- contentType: pub.contentType,
1899
1864
  isPublic: pub.isPublic,
1900
1865
  canvasContentFilePath
1901
1866
  },
@@ -1928,7 +1893,7 @@ async function startDaemon(config) {
1928
1893
  debugLog,
1929
1894
  instructions
1930
1895
  };
1931
- const runner = config.bridgeMode === "claude-code" ? await createClaudeCodeBridgeRunner(bridgeConfig, abort.signal) : await createOpenClawBridgeRunner(bridgeConfig);
1896
+ const runner = config.bridgeMode === "claude-sdk" ? await createClaudeSdkBridgeRunner(bridgeConfig, abort.signal) : config.bridgeMode === "claude-code" ? await createClaudeCodeBridgeRunner(bridgeConfig, abort.signal) : await createOpenClawBridgeRunner(bridgeConfig);
1932
1897
  if (stopped || activeSlug !== slug || abort.signal.aborted) {
1933
1898
  await runner.stop();
1934
1899
  return;
@@ -1974,7 +1939,6 @@ async function startDaemon(config) {
1974
1939
  clearHeartbeatTimer();
1975
1940
  stopPingPong();
1976
1941
  await signaling.stop();
1977
- await queuePersistCanvasSnapshot(getActiveCanvasSnapshot(), "daemon-shutdown");
1978
1942
  try {
1979
1943
  await apiClient2.goOffline({ daemonSessionId });
1980
1944
  } catch (error) {