@shadowob/connector 1.1.3-dev.271 → 1.1.4

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/index.cjs CHANGED
@@ -24,6 +24,14 @@ __export(index_exports, {
24
24
  createConnectorPlans: () => createConnectorPlans
25
25
  });
26
26
  module.exports = __toCommonJS(index_exports);
27
+
28
+ // src/cc-connect-fork.ts
29
+ var CC_CONNECT_FORK_REPO = "buggyblues/cc-connect";
30
+ var CC_CONNECT_FORK_REF = "63b5d59127b3004bc7002f2d51892b1f2a91ea83";
31
+ var CC_CONNECT_FORK_SHORT_REF = CC_CONNECT_FORK_REF.slice(0, 7);
32
+ var CC_CONNECT_FORK_DOCS_URL = `https://github.com/${CC_CONNECT_FORK_REPO}/blob/main/docs/shadowob.md`;
33
+
34
+ // src/index.ts
27
35
  var DEFAULT_SERVER_URL = "https://shadowob.com";
28
36
  var DEFAULT_WORK_DIR = ".";
29
37
  var DEFAULT_PROJECT_NAME = "shadow-buddy";
@@ -110,7 +118,14 @@ function buildOpenClawPlan(input) {
110
118
  "typing",
111
119
  "activityStatus",
112
120
  "reactions",
113
- "editDelete"
121
+ "editDelete",
122
+ "statusChecks",
123
+ "usageCosts",
124
+ "multiAgentBinding",
125
+ "shadowCliLogin",
126
+ "notifications",
127
+ "officialSkills",
128
+ "cronTasks"
114
129
  ]
115
130
  };
116
131
  }
@@ -196,7 +211,12 @@ function buildHermesPlan(input) {
196
211
  "onlineStatus",
197
212
  "typing",
198
213
  "activityStatus",
199
- "cronDelivery"
214
+ "cronDelivery",
215
+ "statusChecks",
216
+ "usageCosts",
217
+ "shadowCliLogin",
218
+ "notifications",
219
+ "officialSkills"
200
220
  ]
201
221
  };
202
222
  }
@@ -211,8 +231,12 @@ function buildCcConnectPlan(input) {
211
231
  "",
212
232
  "[[projects]]",
213
233
  `name = "${projectName}"`,
234
+ "",
235
+ "[projects.agent]",
236
+ `type = "${agentType}"`,
237
+ "",
238
+ "[projects.agent.options]",
214
239
  `work_dir = "${workDir}"`,
215
- `agent_type = "${agentType}"`,
216
240
  "",
217
241
  "[[projects.platforms]]",
218
242
  'type = "shadowob"',
@@ -225,15 +249,6 @@ function buildCcConnectPlan(input) {
225
249
  "share_session_in_channel = false",
226
250
  'progress_style = "compact"'
227
251
  ].join("\n");
228
- const commands = [
229
- { label: "Install cc-connect", command: "npm install -g cc-connect" },
230
- { label: "Create config directory", command: "mkdir -p ~/.cc-connect" },
231
- {
232
- label: "Edit config",
233
- command: "$EDITOR ~/.cc-connect/config.toml"
234
- },
235
- { label: "Start cc-connect", command: "cc-connect" }
236
- ];
237
252
  const connectCommand = [
238
253
  "npx @shadowob/connector@latest connect",
239
254
  "--target cc-connect",
@@ -243,12 +258,26 @@ function buildCcConnectPlan(input) {
243
258
  `--project-name ${shellQuote(projectName)}`,
244
259
  `--agent-type ${shellQuote(agentType)}`
245
260
  ].join(" ");
261
+ const installCommand = `${connectCommand} --install`;
262
+ const startCommand = `${connectCommand} --install --start`;
263
+ const commands = [
264
+ {
265
+ label: "Install ShadowOB cc-connect fork",
266
+ command: installCommand
267
+ },
268
+ { label: "Create config directory", command: "mkdir -p ~/.cc-connect" },
269
+ {
270
+ label: "Edit config",
271
+ command: "$EDITOR ~/.cc-connect/config.toml"
272
+ },
273
+ { label: "Start ShadowOB cc-connect fork", command: startCommand }
274
+ ];
246
275
  return {
247
276
  target: "cc-connect",
248
277
  title: "cc-connect",
249
- summary: "Use cc-connect ShadowOB Socket.IO platform support with this Buddy token.",
250
- connectCommand,
251
- quickCommand: commands.map((item) => item.command).join(" && "),
278
+ summary: `Use ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF} with ShadowOB Socket.IO platform support for this Buddy token.`,
279
+ connectCommand: startCommand,
280
+ quickCommand: startCommand,
252
281
  commands,
253
282
  configBlocks: [{ label: "~/.cc-connect/config.toml", language: "toml", content: tomlConfig }],
254
283
  aiPrompt: [
@@ -259,9 +288,9 @@ function buildCcConnectPlan(input) {
259
288
  `Project work_dir: ${workDir}`,
260
289
  `Agent type: ${agentType}`,
261
290
  "",
262
- "Install cc-connect, add the TOML platform block, and start cc-connect."
291
+ `Install ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF}, add the TOML platform block, and start cc-connect.`
263
292
  ].join("\n"),
264
- docsUrl: "https://github.com/buggyblues/cc-connect/blob/main/docs/shadowob.md",
293
+ docsUrl: CC_CONNECT_FORK_DOCS_URL,
265
294
  capabilities: [
266
295
  "channelMessages",
267
296
  "dms",
@@ -271,7 +300,12 @@ function buildCcConnectPlan(input) {
271
300
  "slashCommands",
272
301
  "typing",
273
302
  "streamingPreviews",
274
- "forms"
303
+ "forms",
304
+ "statusChecks",
305
+ "usageCosts",
306
+ "multiAgentBinding",
307
+ "shadowCliLogin",
308
+ "notifications"
275
309
  ]
276
310
  };
277
311
  }
package/dist/index.js CHANGED
@@ -1,3 +1,9 @@
1
+ // src/cc-connect-fork.ts
2
+ var CC_CONNECT_FORK_REPO = "buggyblues/cc-connect";
3
+ var CC_CONNECT_FORK_REF = "63b5d59127b3004bc7002f2d51892b1f2a91ea83";
4
+ var CC_CONNECT_FORK_SHORT_REF = CC_CONNECT_FORK_REF.slice(0, 7);
5
+ var CC_CONNECT_FORK_DOCS_URL = `https://github.com/${CC_CONNECT_FORK_REPO}/blob/main/docs/shadowob.md`;
6
+
1
7
  // src/index.ts
2
8
  var DEFAULT_SERVER_URL = "https://shadowob.com";
3
9
  var DEFAULT_WORK_DIR = ".";
@@ -85,7 +91,14 @@ function buildOpenClawPlan(input) {
85
91
  "typing",
86
92
  "activityStatus",
87
93
  "reactions",
88
- "editDelete"
94
+ "editDelete",
95
+ "statusChecks",
96
+ "usageCosts",
97
+ "multiAgentBinding",
98
+ "shadowCliLogin",
99
+ "notifications",
100
+ "officialSkills",
101
+ "cronTasks"
89
102
  ]
90
103
  };
91
104
  }
@@ -171,7 +184,12 @@ function buildHermesPlan(input) {
171
184
  "onlineStatus",
172
185
  "typing",
173
186
  "activityStatus",
174
- "cronDelivery"
187
+ "cronDelivery",
188
+ "statusChecks",
189
+ "usageCosts",
190
+ "shadowCliLogin",
191
+ "notifications",
192
+ "officialSkills"
175
193
  ]
176
194
  };
177
195
  }
@@ -186,8 +204,12 @@ function buildCcConnectPlan(input) {
186
204
  "",
187
205
  "[[projects]]",
188
206
  `name = "${projectName}"`,
207
+ "",
208
+ "[projects.agent]",
209
+ `type = "${agentType}"`,
210
+ "",
211
+ "[projects.agent.options]",
189
212
  `work_dir = "${workDir}"`,
190
- `agent_type = "${agentType}"`,
191
213
  "",
192
214
  "[[projects.platforms]]",
193
215
  'type = "shadowob"',
@@ -200,15 +222,6 @@ function buildCcConnectPlan(input) {
200
222
  "share_session_in_channel = false",
201
223
  'progress_style = "compact"'
202
224
  ].join("\n");
203
- const commands = [
204
- { label: "Install cc-connect", command: "npm install -g cc-connect" },
205
- { label: "Create config directory", command: "mkdir -p ~/.cc-connect" },
206
- {
207
- label: "Edit config",
208
- command: "$EDITOR ~/.cc-connect/config.toml"
209
- },
210
- { label: "Start cc-connect", command: "cc-connect" }
211
- ];
212
225
  const connectCommand = [
213
226
  "npx @shadowob/connector@latest connect",
214
227
  "--target cc-connect",
@@ -218,12 +231,26 @@ function buildCcConnectPlan(input) {
218
231
  `--project-name ${shellQuote(projectName)}`,
219
232
  `--agent-type ${shellQuote(agentType)}`
220
233
  ].join(" ");
234
+ const installCommand = `${connectCommand} --install`;
235
+ const startCommand = `${connectCommand} --install --start`;
236
+ const commands = [
237
+ {
238
+ label: "Install ShadowOB cc-connect fork",
239
+ command: installCommand
240
+ },
241
+ { label: "Create config directory", command: "mkdir -p ~/.cc-connect" },
242
+ {
243
+ label: "Edit config",
244
+ command: "$EDITOR ~/.cc-connect/config.toml"
245
+ },
246
+ { label: "Start ShadowOB cc-connect fork", command: startCommand }
247
+ ];
221
248
  return {
222
249
  target: "cc-connect",
223
250
  title: "cc-connect",
224
- summary: "Use cc-connect ShadowOB Socket.IO platform support with this Buddy token.",
225
- connectCommand,
226
- quickCommand: commands.map((item) => item.command).join(" && "),
251
+ summary: `Use ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF} with ShadowOB Socket.IO platform support for this Buddy token.`,
252
+ connectCommand: startCommand,
253
+ quickCommand: startCommand,
227
254
  commands,
228
255
  configBlocks: [{ label: "~/.cc-connect/config.toml", language: "toml", content: tomlConfig }],
229
256
  aiPrompt: [
@@ -234,9 +261,9 @@ function buildCcConnectPlan(input) {
234
261
  `Project work_dir: ${workDir}`,
235
262
  `Agent type: ${agentType}`,
236
263
  "",
237
- "Install cc-connect, add the TOML platform block, and start cc-connect."
264
+ `Install ${CC_CONNECT_FORK_REPO}@${CC_CONNECT_FORK_SHORT_REF}, add the TOML platform block, and start cc-connect.`
238
265
  ].join("\n"),
239
- docsUrl: "https://github.com/buggyblues/cc-connect/blob/main/docs/shadowob.md",
266
+ docsUrl: CC_CONNECT_FORK_DOCS_URL,
240
267
  capabilities: [
241
268
  "channelMessages",
242
269
  "dms",
@@ -246,7 +273,12 @@ function buildCcConnectPlan(input) {
246
273
  "slashCommands",
247
274
  "typing",
248
275
  "streamingPreviews",
249
- "forms"
276
+ "forms",
277
+ "statusChecks",
278
+ "usageCosts",
279
+ "multiAgentBinding",
280
+ "shadowCliLogin",
281
+ "notifications"
250
282
  ]
251
283
  };
252
284
  }
@@ -150,8 +150,8 @@ This iteration fixes issues found during static review against the uploaded Shad
150
150
  - Fixed REST polling shutdown condition so the polling loop stops when the adapter is disconnected.
151
151
  - Fixed `env_enablement_fn` to return a flat seed dict, because Hermes merges all non-`home_channel` keys directly into `PlatformConfig.extra`.
152
152
  - Changed env auto-enable and connector setup to require only Shadow endpoint/token; Buddy id and channel policy are now resolved dynamically from Shadow.
153
- - Added dynamic handling for `channel:member-added` and `channel:member-removed` events.
154
- - Added dynamic handling for `server:joined` and `agent:policy-changed` events.
153
+ - Added OpenClaw-aligned dynamic handling for `channel:member-added`, `channel:member-removed`, `server:joined`, and `agent:policy-changed` events.
154
+ - Changed empty-channel startup from fatal to tolerant waiting. The adapter stays online, refreshes channel policy periodically, and uses the owner DM as the default home channel when available.
155
155
  - Updated standalone media delivery to support local paths, `MEDIA:` refs, relative paths, Shadow private URLs and remote URLs through the SDK helper.
156
156
 
157
157
  ## Known limits in this first version
@@ -465,6 +465,13 @@ def _remote_listen_channel_entries(
465
465
  return entries
466
466
 
467
467
 
468
+ def _owner_id_from_remote_config(remote_config: dict[str, Any] | None) -> str | None:
469
+ if not isinstance(remote_config, dict):
470
+ return None
471
+ owner_id = str(remote_config.get("ownerId") or remote_config.get("owner_id") or "").strip()
472
+ return owner_id or None
473
+
474
+
468
475
  class ShadowOBAdapter(BasePlatformAdapter):
469
476
  """Hermes ``BasePlatformAdapter`` implementation for Shadow."""
470
477
 
@@ -479,6 +486,7 @@ class ShadowOBAdapter(BasePlatformAdapter):
479
486
  self.socket: ShadowSocketClient | None = None
480
487
  self._poll_task: asyncio.Task | None = None
481
488
  self._heartbeat_task: asyncio.Task | None = None
489
+ self._channel_refresh_task: asyncio.Task | None = None
482
490
  self._channel_ids: list[str] = _channel_ids_from_config(config)
483
491
  self._configured_channel_ids: set[str] = set(self._channel_ids)
484
492
  self._remote_channel_ids: set[str] = set()
@@ -532,26 +540,26 @@ class ShadowOBAdapter(BasePlatformAdapter):
532
540
  await self.client.open()
533
541
  await self._load_identity()
534
542
  await self._register_slash_commands()
535
- await self._start_heartbeat()
536
543
  await self._resolve_channels()
537
544
  if not self._channel_ids:
538
- self._set_fatal_error(
539
- "config_missing",
540
- "No Shadow channels are available for this Buddy token. Add the Buddy to a server/channel, open a DM, or verify the remote agent policy.",
541
- retryable=False,
545
+ logger.warning(
546
+ "[Shadow] No channels are available yet for this Buddy token. "
547
+ "Waiting for the Buddy to be added to a channel or DM.",
542
548
  )
543
- return False
544
549
 
545
- if self._rest_only:
546
- await self._start_polling()
547
- else:
550
+ use_polling = self._rest_only
551
+ if not self._rest_only:
548
552
  try:
549
553
  await self._start_socket()
550
554
  except Exception as exc:
551
555
  logger.warning("[Shadow] Socket.IO connection failed, falling back to REST polling: %s", exc)
552
- await self._start_polling()
556
+ use_polling = True
553
557
 
554
558
  self._mark_connected()
559
+ if use_polling:
560
+ await self._start_polling()
561
+ await self._start_heartbeat()
562
+ await self._start_channel_refresh()
555
563
  logger.info("[Shadow] Connected to %s; channels=%s", self.base_url, ",".join(self._channel_ids))
556
564
  return True
557
565
  except Exception as exc:
@@ -573,6 +581,13 @@ class ShadowOBAdapter(BasePlatformAdapter):
573
581
  except asyncio.CancelledError:
574
582
  pass
575
583
  self._heartbeat_task = None
584
+ if self._channel_refresh_task is not None and not self._channel_refresh_task.done():
585
+ self._channel_refresh_task.cancel()
586
+ try:
587
+ await self._channel_refresh_task
588
+ except asyncio.CancelledError:
589
+ pass
590
+ self._channel_refresh_task = None
576
591
  if self._poll_task is not None and not self._poll_task.done():
577
592
  self._poll_task.cancel()
578
593
  try:
@@ -909,19 +924,47 @@ class ShadowOBAdapter(BasePlatformAdapter):
909
924
  self._remote_channel_ids = new_remote_ids
910
925
  logger.info("[Shadow] Refreshed remote config for agent %s; channels=%s", self._agent_id, len(new_remote_ids))
911
926
 
912
- if sync_socket and self.socket is not None:
913
- next_channel_ids = set(self._channel_ids)
914
- for channel_id in old_channel_ids - next_channel_ids:
915
- try:
916
- await self.socket.leave_channel(channel_id)
917
- except Exception:
918
- pass
919
- for channel_id in next_channel_ids - old_channel_ids:
920
- try:
921
- ack = await self.socket.join_channel(channel_id)
922
- logger.info("[Shadow] Joined channel %s after config refresh ack=%s", channel_id, ack)
923
- except Exception as exc:
924
- logger.warning("[Shadow] Failed to join refreshed channel %s: %s", channel_id, exc)
927
+ if sync_socket:
928
+ await self._sync_socket_channels(old_channel_ids)
929
+
930
+ async def _sync_socket_channels(self, old_channel_ids: set[str]) -> None:
931
+ if self.socket is None:
932
+ return
933
+ next_channel_ids = set(self._channel_ids)
934
+ for channel_id in old_channel_ids - next_channel_ids:
935
+ try:
936
+ await self.socket.leave_channel(channel_id)
937
+ except Exception:
938
+ pass
939
+ for channel_id in next_channel_ids - old_channel_ids:
940
+ try:
941
+ ack = await self.socket.join_channel(channel_id)
942
+ logger.info("[Shadow] Joined channel %s after config refresh ack=%s", channel_id, ack)
943
+ except Exception as exc:
944
+ logger.warning("[Shadow] Failed to join refreshed channel %s: %s", channel_id, exc)
945
+
946
+ async def _ensure_owner_dm_home_channel(self) -> None:
947
+ if self.client is None:
948
+ return
949
+ owner_id = _owner_id_from_remote_config(self._remote_config)
950
+ if not owner_id or owner_id == self._bot_user_id:
951
+ return
952
+ try:
953
+ channel = await self.client.create_direct_channel(owner_id)
954
+ except Exception as exc:
955
+ logger.debug("[Shadow] Owner DM home channel is not available yet: %s", exc)
956
+ return
957
+ channel_id = str(channel.get("id") or "").strip()
958
+ if not channel_id:
959
+ return
960
+ self._channel_cache[channel_id] = {
961
+ **channel,
962
+ "kind": channel.get("kind") or channel.get("type") or "dm",
963
+ }
964
+ self._channel_policies.setdefault(channel_id, _default_policy_from_remote_config(self._remote_config))
965
+ if channel_id not in self._channel_ids:
966
+ self._channel_ids.append(channel_id)
967
+ logger.info("[Shadow] Using owner DM %s as the default home channel", channel_id)
925
968
 
926
969
  async def _register_slash_commands(self) -> None:
927
970
  if self.client is None or not self._agent_id or not self._slash_commands:
@@ -952,6 +995,26 @@ class ShadowOBAdapter(BasePlatformAdapter):
952
995
  except Exception as exc:
953
996
  logger.debug("[Shadow] heartbeat failed for agent %s: %s", self._agent_id, exc)
954
997
 
998
+ async def _start_channel_refresh(self) -> None:
999
+ if self._channel_refresh_task is None or self._channel_refresh_task.done():
1000
+ self._channel_refresh_task = asyncio.create_task(
1001
+ self._channel_refresh_loop(),
1002
+ name="shadowob-channel-refresh",
1003
+ )
1004
+
1005
+ async def _channel_refresh_loop(self) -> None:
1006
+ interval = max(10.0, min(60.0, self._heartbeat_interval))
1007
+ while self._running and not self.has_fatal_error:
1008
+ await asyncio.sleep(interval)
1009
+ try:
1010
+ await self._resolve_channels(sync_socket=True)
1011
+ if not self._channel_ids:
1012
+ logger.debug("[Shadow] Still waiting for a channel or owner DM")
1013
+ except asyncio.CancelledError:
1014
+ raise
1015
+ except Exception as exc:
1016
+ logger.debug("[Shadow] Channel refresh failed: %s", exc)
1017
+
955
1018
  async def _send_slash_interactive_prompt(
956
1019
  self,
957
1020
  match: tuple[dict[str, Any], str, str],
@@ -986,9 +1049,10 @@ class ShadowOBAdapter(BasePlatformAdapter):
986
1049
  logger.info("[Shadow] Sent interactive prompt for slash command /%s", name)
987
1050
  return True
988
1051
 
989
- async def _resolve_channels(self) -> None:
1052
+ async def _resolve_channels(self, *, sync_socket: bool = False) -> None:
990
1053
  if self.client is None:
991
1054
  return
1055
+ old_channel_ids = set(self._channel_ids)
992
1056
  if self._agent_id:
993
1057
  try:
994
1058
  await self._refresh_remote_config()
@@ -1044,6 +1108,8 @@ class ShadowOBAdapter(BasePlatformAdapter):
1044
1108
  except Exception as exc:
1045
1109
  logger.debug("[Shadow] Failed to list direct channels: %s", exc)
1046
1110
 
1111
+ await self._ensure_owner_dm_home_channel()
1112
+
1047
1113
  # Best-effort metadata cache for explicitly configured channels.
1048
1114
  for channel_id in list(self._channel_ids):
1049
1115
  if channel_id in self._channel_cache:
@@ -1053,6 +1119,9 @@ class ShadowOBAdapter(BasePlatformAdapter):
1053
1119
  except Exception:
1054
1120
  self._channel_cache[channel_id] = {"id": channel_id, "name": channel_id, "kind": "channel"}
1055
1121
 
1122
+ if sync_socket:
1123
+ await self._sync_socket_channels(old_channel_ids)
1124
+
1056
1125
  async def _start_socket(self) -> None:
1057
1126
  self.socket = ShadowSocketClient(self.base_url, self.token, transports=self._transports, logger=logger)
1058
1127
  self.socket.on("connect", self._on_socket_connect)
@@ -1070,13 +1139,27 @@ class ShadowOBAdapter(BasePlatformAdapter):
1070
1139
  self.socket.on("server:joined", self._on_server_joined)
1071
1140
  self.socket.on("agent:policy-changed", self._on_agent_policy_changed)
1072
1141
  await self.socket.connect()
1073
- await self.socket.update_presence("online")
1074
- for channel_id in self._channel_ids:
1075
- ack = await self.socket.join_channel(channel_id)
1076
- logger.info("[Shadow] Joined channel %s ack=%s", channel_id, ack)
1142
+ await self._join_current_socket_channels()
1077
1143
  if self._catchup_minutes > 0:
1078
1144
  await self._catchup_recent_messages()
1079
1145
 
1146
+ async def _join_current_socket_channels(self) -> None:
1147
+ if self.socket is None:
1148
+ return
1149
+ try:
1150
+ await self.socket.update_presence("online")
1151
+ except Exception as exc:
1152
+ logger.debug("[Shadow] Failed to update socket presence: %s", exc)
1153
+ if not self._channel_ids:
1154
+ logger.info("[Shadow] Socket connected with no channels yet; waiting for channel membership events")
1155
+ return
1156
+ for channel_id in list(self._channel_ids):
1157
+ try:
1158
+ ack = await self.socket.join_channel(channel_id)
1159
+ logger.info("[Shadow] Joined channel %s ack=%s", channel_id, ack)
1160
+ except Exception as exc:
1161
+ logger.warning("[Shadow] Failed to join channel %s: %s", channel_id, exc)
1162
+
1080
1163
  async def _start_polling(self) -> None:
1081
1164
  if self._catchup_minutes > 0:
1082
1165
  await self._catchup_recent_messages()
@@ -1142,6 +1225,7 @@ class ShadowOBAdapter(BasePlatformAdapter):
1142
1225
 
1143
1226
  async def _on_socket_connect(self) -> None:
1144
1227
  logger.info("[Shadow] Socket connected")
1228
+ await self._join_current_socket_channels()
1145
1229
 
1146
1230
  async def _on_socket_disconnect(self, reason: str | None = None) -> None:
1147
1231
  logger.info("[Shadow] Socket disconnected: %s", reason)
@@ -1163,6 +1247,11 @@ class ShadowOBAdapter(BasePlatformAdapter):
1163
1247
  channel_id = str(payload.get("channelId") or payload.get("channel_id") or "").strip()
1164
1248
  if not channel_id:
1165
1249
  return
1250
+ old_channel_ids = set(self._channel_ids)
1251
+ try:
1252
+ await self._resolve_channels(sync_socket=False)
1253
+ except Exception as exc:
1254
+ logger.warning("[Shadow] Failed to refresh config after channel member add: %s", exc)
1166
1255
  if channel_id not in self._channel_ids:
1167
1256
  self._channel_ids.append(channel_id)
1168
1257
  if self.client is not None:
@@ -1170,10 +1259,13 @@ class ShadowOBAdapter(BasePlatformAdapter):
1170
1259
  self._channel_cache[channel_id] = await self.client.get_channel(channel_id)
1171
1260
  except Exception:
1172
1261
  self._channel_cache[channel_id] = {"id": channel_id, "name": channel_id, "kind": "channel"}
1262
+ self._channel_policies.setdefault(
1263
+ channel_id,
1264
+ _default_policy_from_remote_config(self._remote_config),
1265
+ )
1173
1266
  if self.socket is not None:
1174
1267
  try:
1175
- ack = await self.socket.join_channel(channel_id)
1176
- logger.info("[Shadow] Joined newly added channel %s ack=%s", channel_id, ack)
1268
+ await self._sync_socket_channels(old_channel_ids)
1177
1269
  except Exception as exc:
1178
1270
  logger.warning("[Shadow] Failed to join newly added channel %s: %s", channel_id, exc)
1179
1271
  if self._catchup_minutes > 0:
@@ -1200,7 +1292,7 @@ class ShadowOBAdapter(BasePlatformAdapter):
1200
1292
  if payload_agent_id and self._agent_id and payload_agent_id != self._agent_id:
1201
1293
  return
1202
1294
  try:
1203
- await self._refresh_remote_config(sync_socket=True)
1295
+ await self._resolve_channels(sync_socket=True)
1204
1296
  except Exception as exc:
1205
1297
  logger.warning("[Shadow] Failed to refresh remote config after server join: %s", exc)
1206
1298
 
@@ -1209,7 +1301,7 @@ class ShadowOBAdapter(BasePlatformAdapter):
1209
1301
  if payload_agent_id and self._agent_id and payload_agent_id != self._agent_id:
1210
1302
  return
1211
1303
  try:
1212
- await self._refresh_remote_config(sync_socket=True)
1304
+ await self._resolve_channels(sync_socket=True)
1213
1305
  except Exception as exc:
1214
1306
  logger.warning("[Shadow] Failed to refresh remote config after policy change: %s", exc)
1215
1307
 
@@ -231,9 +231,31 @@ class ShadowAsyncClient:
231
231
  async def list_direct_channels(self) -> list[JsonDict]:
232
232
  return await self.request("GET", "/api/channels/dm")
233
233
 
234
+ async def create_direct_channel(self, user_id: str) -> JsonDict:
235
+ return await self.request(
236
+ "POST",
237
+ "/api/channels/dm",
238
+ json_body={"userId": str(user_id)},
239
+ )
240
+
234
241
  async def get_channel(self, channel_id: str) -> JsonDict:
235
242
  return await self.request("GET", f"/api/channels/{quote(str(channel_id), safe='')}")
236
243
 
244
+ async def get_channel_bootstrap(
245
+ self,
246
+ channel_id: str,
247
+ *,
248
+ messages_limit: int | None = None,
249
+ ) -> JsonDict:
250
+ params: JsonDict = {}
251
+ if messages_limit is not None:
252
+ params["messagesLimit"] = int(messages_limit)
253
+ return await self.request(
254
+ "GET",
255
+ f"/api/channels/{quote(str(channel_id), safe='')}/bootstrap",
256
+ params=params or None,
257
+ )
258
+
237
259
  async def get_messages(
238
260
  self,
239
261
  channel_id: str,
@@ -256,11 +278,20 @@ class ShadowAsyncClient:
256
278
  async def get_message(self, message_id: str) -> JsonDict:
257
279
  return await self.request("GET", f"/api/messages/{quote(str(message_id), safe='')}")
258
280
 
259
- async def resolve_attachment_media_url(self, attachment_id: str, *, disposition: str = "inline") -> JsonDict:
281
+ async def resolve_attachment_media_url(
282
+ self,
283
+ attachment_id: str,
284
+ *,
285
+ disposition: str = "inline",
286
+ variant: str | None = None,
287
+ ) -> JsonDict:
288
+ params: JsonDict = {"disposition": disposition}
289
+ if variant:
290
+ params["variant"] = variant
260
291
  return await self.request(
261
292
  "GET",
262
293
  f"/api/attachments/{quote(str(attachment_id), safe='')}/media-url",
263
- params={"disposition": disposition},
294
+ params=params,
264
295
  )
265
296
 
266
297
  async def send_message(