@schoolai/shipyard 3.0.0 → 3.1.0-rc.20260414.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.
@@ -37,7 +37,7 @@ import {
37
37
  VaultKeyPutRequestSchema,
38
38
  VaultKeyPutResponseSchema,
39
39
  classifyClaudeCodeCompatibility
40
- } from "./chunk-QMLEP5EE.js";
40
+ } from "./chunk-E747QA24.js";
41
41
  import {
42
42
  loadAuthToken
43
43
  } from "./chunk-IHSXN66C.js";
@@ -76,7 +76,7 @@ import { execSync } from "child_process";
76
76
  import { existsSync as existsSync7 } from "fs";
77
77
  import { mkdir as mkdir18 } from "fs/promises";
78
78
  import { createRequire as createRequire4 } from "module";
79
- import { dirname as dirname14, join as join42 } from "path";
79
+ import { dirname as dirname14, join as join43 } from "path";
80
80
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
81
81
 
82
82
  // ../../node_modules/.pnpm/@loro-extended+change@6.0.0-beta.0_loro-crdt@1.10.6/node_modules/@loro-extended/change/dist/index.js
@@ -7262,6 +7262,243 @@ var Adapter = class {
7262
7262
  this.#sendInterceptors = [];
7263
7263
  }
7264
7264
  };
7265
+ var Bridge = class {
7266
+ adapters = /* @__PURE__ */ new Map();
7267
+ logger;
7268
+ constructor({ logger: logger2 } = {}) {
7269
+ this.logger = logger2 ?? getLogger(["@loro-extended", "repo"]);
7270
+ }
7271
+ /**
7272
+ * Register an adapter with this bridge
7273
+ */
7274
+ addAdapter(adapter) {
7275
+ if (!adapter.adapterType)
7276
+ throw new Error("can't add adapter without adapter id");
7277
+ this.adapters.set(adapter.adapterType, adapter);
7278
+ }
7279
+ /**
7280
+ * Remove an adapter from this bridge
7281
+ */
7282
+ removeAdapter(adapterType) {
7283
+ this.adapters.delete(adapterType);
7284
+ }
7285
+ /**
7286
+ * Route a message from one adapter to another
7287
+ */
7288
+ routeMessage(fromAdapterType, toAdapterType, message) {
7289
+ this.logger.trace("routeMessage: {messageType} from {from} to {to}", {
7290
+ from: fromAdapterType,
7291
+ to: toAdapterType,
7292
+ messageType: message.type
7293
+ });
7294
+ const toAdapter = this.adapters.get(toAdapterType);
7295
+ if (toAdapter) {
7296
+ toAdapter.deliverMessage(fromAdapterType, message);
7297
+ } else {
7298
+ this.logger.warn(
7299
+ "routeMessage: target adapter {toAdapterType} not found",
7300
+ { toAdapterType }
7301
+ );
7302
+ }
7303
+ }
7304
+ /**
7305
+ * Get all adapter IDs currently in the bridge
7306
+ */
7307
+ get adapterTypes() {
7308
+ return new Set(this.adapters.keys());
7309
+ }
7310
+ };
7311
+ var BridgeAdapter = class extends Adapter {
7312
+ bridge;
7313
+ logger;
7314
+ // Track which remote adapter each channel connects to
7315
+ channelToAdapter = /* @__PURE__ */ new Map();
7316
+ adapterToChannel = /* @__PURE__ */ new Map();
7317
+ constructor({ adapterType, adapterId, bridge, logger: logger2 }) {
7318
+ super({ adapterType, adapterId: adapterId ?? adapterType });
7319
+ this.bridge = bridge;
7320
+ this.logger = (logger2 ?? getLogger(["@loro-extended", "repo"])).with({
7321
+ adapterType
7322
+ });
7323
+ this.logger.trace(`new BridgeAdapter`);
7324
+ }
7325
+ generate(context) {
7326
+ this.logger.debug("generate channel to {targetAdapterType}", {
7327
+ targetAdapterType: context.targetAdapterType
7328
+ });
7329
+ return {
7330
+ adapterType: this.adapterType,
7331
+ kind: "network",
7332
+ send: (msg) => {
7333
+ this.logger.debug("channel.send: {messageType} from {from} to {to}", {
7334
+ from: this.adapterType,
7335
+ to: context.targetAdapterType,
7336
+ messageType: msg.type
7337
+ });
7338
+ this.bridge.routeMessage(
7339
+ this.adapterType,
7340
+ context.targetAdapterType,
7341
+ msg
7342
+ );
7343
+ },
7344
+ stop: () => {
7345
+ }
7346
+ };
7347
+ }
7348
+ /**
7349
+ * Start participating in the in-process network.
7350
+ * Uses two-phase initialization:
7351
+ * 1. Create all channels (no messages sent)
7352
+ * 2. Establish channels (only the "newer" adapter initiates to avoid double-establishment)
7353
+ */
7354
+ async onStart() {
7355
+ this.logger.trace(`onStart - registering with bridge`);
7356
+ this.bridge.addAdapter(this);
7357
+ for (const [adapterType, adapter] of this.bridge.adapters) {
7358
+ if (adapterType !== this.adapterType) {
7359
+ this.logger.trace("telling {adapterType} to create channel to us", {
7360
+ adapterType
7361
+ });
7362
+ adapter.createChannelTo(this.adapterType);
7363
+ }
7364
+ }
7365
+ for (const adapterType of this.bridge.adapters.keys()) {
7366
+ if (adapterType !== this.adapterType) {
7367
+ this.logger.trace("creating our channel to {adapterType}", {
7368
+ adapterType
7369
+ });
7370
+ this.createChannelTo(adapterType);
7371
+ }
7372
+ }
7373
+ for (const channelId of this.adapterToChannel.values()) {
7374
+ this.logger.trace("establishing our channel {channelId}", { channelId });
7375
+ this.establishChannel(channelId);
7376
+ }
7377
+ this.logger.trace(`onStart complete`);
7378
+ }
7379
+ /**
7380
+ * Stop participating in the in-process network.
7381
+ * Cleans up all channels and removes from bridge.
7382
+ */
7383
+ async onStop() {
7384
+ this.logger.trace(`stop`);
7385
+ for (const [adapterType, adapter] of this.bridge.adapters) {
7386
+ if (adapterType !== this.adapterType) {
7387
+ adapter.removeChannelTo(this.adapterType);
7388
+ }
7389
+ }
7390
+ this.bridge.removeAdapter(this.adapterType);
7391
+ for (const channelId of this.channelToAdapter.keys()) {
7392
+ this.removeChannel(channelId);
7393
+ }
7394
+ this.channelToAdapter.clear();
7395
+ this.adapterToChannel.clear();
7396
+ }
7397
+ /**
7398
+ * Create a channel to a target adapter (Phase 1).
7399
+ * Does NOT trigger establishment - that happens in Phase 2.
7400
+ * Called by our own onStart() or by other adapters when they start.
7401
+ */
7402
+ createChannelTo(targetAdapterType) {
7403
+ if (this.adapterToChannel.has(targetAdapterType)) {
7404
+ this.logger.trace("channel already exists to {targetAdapterType}", {
7405
+ targetAdapterType
7406
+ });
7407
+ return;
7408
+ }
7409
+ const channel = this.addChannel({ targetAdapterType });
7410
+ this.channelToAdapter.set(channel.channelId, targetAdapterType);
7411
+ this.adapterToChannel.set(targetAdapterType, channel.channelId);
7412
+ this.logger.trace(
7413
+ "channel {channelId} created (not yet established) to {targetAdapterType}",
7414
+ {
7415
+ targetAdapterType,
7416
+ channelId: channel.channelId
7417
+ }
7418
+ );
7419
+ }
7420
+ /**
7421
+ * Establish a channel to a target adapter (Phase 2).
7422
+ * Triggers the establishment handshake.
7423
+ * Called by our own onStart() or by other adapters when they start.
7424
+ */
7425
+ establishChannelTo(targetAdapterType) {
7426
+ const channelId = this.adapterToChannel.get(targetAdapterType);
7427
+ if (!channelId) {
7428
+ this.logger.warn("no channel found to establish to {targetAdapterType}", {
7429
+ targetAdapterType
7430
+ });
7431
+ return;
7432
+ }
7433
+ this.logger.trace("establishing channel {channelId}", { channelId });
7434
+ this.establishChannel(channelId);
7435
+ }
7436
+ /**
7437
+ * Remove a channel to a target adapter.
7438
+ * Called by other adapters when they stop.
7439
+ */
7440
+ removeChannelTo(targetAdapterType) {
7441
+ const channelId = this.adapterToChannel.get(targetAdapterType);
7442
+ if (channelId) {
7443
+ this.logger.trace("removing channel to adapter {targetAdapterType}", {
7444
+ targetAdapterType
7445
+ });
7446
+ this.removeChannel(channelId);
7447
+ this.channelToAdapter.delete(channelId);
7448
+ this.adapterToChannel.delete(targetAdapterType);
7449
+ }
7450
+ }
7451
+ /**
7452
+ * Deliver a message from another adapter to the appropriate channel.
7453
+ * Called by Bridge.routeMessage().
7454
+ *
7455
+ * Delivers messages asynchronously via queueMicrotask() to simulate real
7456
+ * network adapter behavior. This ensures tests using BridgeAdapter exercise
7457
+ * the same async codepaths as production adapters (WebSocket, SSE, etc.).
7458
+ *
7459
+ * Tests should use `waitForSync()` or `waitUntilReady()` to await sync completion.
7460
+ */
7461
+ deliverMessage(fromAdapterType, message) {
7462
+ const channelId = this.adapterToChannel.get(fromAdapterType);
7463
+ if (channelId) {
7464
+ const channel = this.channels.get(channelId);
7465
+ if (channel) {
7466
+ this.logger.trace(
7467
+ "queueing message {messageType} to channel {channelId} from {from}",
7468
+ {
7469
+ from: fromAdapterType,
7470
+ messageType: message.type,
7471
+ channelId
7472
+ }
7473
+ );
7474
+ queueMicrotask(() => {
7475
+ this.logger.trace(
7476
+ "delivering message {messageType} to channel {channelId} from {from}",
7477
+ {
7478
+ from: fromAdapterType,
7479
+ messageType: message.type,
7480
+ channelId
7481
+ }
7482
+ );
7483
+ channel.onReceive(message);
7484
+ });
7485
+ } else {
7486
+ this.logger.warn(
7487
+ "channel {channelId} not found for message delivery from {fromAdapterType}",
7488
+ {
7489
+ fromAdapterType,
7490
+ channelId
7491
+ }
7492
+ );
7493
+ }
7494
+ } else {
7495
+ this.logger.warn("no channel found for adapter {fromAdapterType}", {
7496
+ fromAdapterType,
7497
+ availableChannels: Array.from(this.adapterToChannel.keys())
7498
+ });
7499
+ }
7500
+ }
7501
+ };
7265
7502
  function isEstablished(channel) {
7266
7503
  return channel.type === "established";
7267
7504
  }
@@ -24374,6 +24611,7 @@ function parseDocumentId(id) {
24374
24611
  return null;
24375
24612
  return { prefix, key, epoch };
24376
24613
  }
24614
+ var LORO_BACKPRESSURE_HIGH_WATER = 1024 * 1024;
24377
24615
  var HARNESS_SERVER_NAME = "shipyard-harness";
24378
24616
  var VISUALIZE_READ_ME_TOOL = "read_me";
24379
24617
  var VISUALIZE_TOOL = "visualize";
@@ -24828,6 +25066,47 @@ var PRStatePayloadSchema = external_exports.object({
24828
25066
  requiredChecks: external_exports.array(external_exports.string()).default([]),
24829
25067
  updatedAt: external_exports.number()
24830
25068
  });
25069
+ function applyPresenceUpdate(pool2, peerId, identity, state) {
25070
+ const existing = pool2[peerId];
25071
+ const mergedState = {
25072
+ ...existing?.state,
25073
+ ...state
25074
+ };
25075
+ if (state.typing === void 0 && "typing" in state) {
25076
+ delete mergedState.typing;
25077
+ }
25078
+ if (state.viewing === void 0 && "viewing" in state) {
25079
+ delete mergedState.viewing;
25080
+ }
25081
+ return {
25082
+ ...pool2,
25083
+ [peerId]: {
25084
+ userId: identity.userId,
25085
+ username: identity.username,
25086
+ avatarUrl: identity.avatarUrl,
25087
+ state: mergedState
25088
+ }
25089
+ };
25090
+ }
25091
+ function removePresence(pool2, peerId) {
25092
+ const { [peerId]: _2, ...rest } = pool2;
25093
+ return rest;
25094
+ }
25095
+ var PeerPresenceStateSchema = external_exports.object({
25096
+ typing: external_exports.object({
25097
+ taskId: external_exports.string(),
25098
+ context: external_exports.string()
25099
+ }).optional(),
25100
+ viewing: external_exports.object({
25101
+ taskId: external_exports.string()
25102
+ }).optional()
25103
+ });
25104
+ var PeerPresenceSlotSchema = external_exports.object({
25105
+ userId: external_exports.string(),
25106
+ username: external_exports.string(),
25107
+ avatarUrl: external_exports.string().nullable(),
25108
+ state: PeerPresenceStateSchema
25109
+ });
24831
25110
  var ASSET_CHUNK_SIZE = 16384;
24832
25111
  var ASSET_REQUEST_ID_BYTES = 36;
24833
25112
  var ASSET_TRANSFER_LABEL = "asset-transfer";
@@ -25768,6 +26047,10 @@ var BrowserToDaemonControlMessageSchema = external_exports.discriminatedUnion("t
25768
26047
  type: external_exports.literal("remove_worktree"),
25769
26048
  worktreePath: external_exports.string()
25770
26049
  }),
26050
+ external_exports.object({
26051
+ type: external_exports.literal("list_directories"),
26052
+ basePath: external_exports.string()
26053
+ }),
25771
26054
  external_exports.object({
25772
26055
  type: external_exports.literal("create_task"),
25773
26056
  taskId: external_exports.string(),
@@ -25792,6 +26075,11 @@ var BrowserToDaemonControlMessageSchema = external_exports.discriminatedUnion("t
25792
26075
  value: external_exports.unknown()
25793
26076
  }),
25794
26077
  external_exports.object({ type: external_exports.literal("request_user_settings") }),
26078
+ external_exports.object({
26079
+ type: external_exports.literal("request_preview_target"),
26080
+ port: external_exports.number().int().positive().optional(),
26081
+ url: external_exports.string().url().optional()
26082
+ }),
25795
26083
  external_exports.object({
25796
26084
  type: external_exports.literal("add_annotation"),
25797
26085
  taskId: external_exports.string(),
@@ -25847,6 +26135,11 @@ var BrowserToDaemonControlMessageSchema = external_exports.discriminatedUnion("t
25847
26135
  external_exports.object({ type: external_exports.literal("delete_template"), templateId: external_exports.string() }),
25848
26136
  external_exports.object({ type: external_exports.literal("list_templates") }),
25849
26137
  external_exports.object({ type: external_exports.literal("stop_task"), taskId: external_exports.string() }),
26138
+ external_exports.object({
26139
+ type: external_exports.literal("stop_background_agent"),
26140
+ taskId: external_exports.string(),
26141
+ backgroundTaskId: external_exports.string()
26142
+ }),
25850
26143
  external_exports.object({
25851
26144
  type: external_exports.literal("todo_item_added"),
25852
26145
  taskId: external_exports.string(),
@@ -25881,6 +26174,14 @@ var BrowserToDaemonControlMessageSchema = external_exports.discriminatedUnion("t
25881
26174
  depId: external_exports.string(),
25882
26175
  depContent: external_exports.string(),
25883
26176
  action: external_exports.enum(["connected", "disconnected"])
26177
+ }),
26178
+ external_exports.object({
26179
+ type: external_exports.literal("request_recent_logs"),
26180
+ windowMinutes: external_exports.number().int().positive().max(120).default(30)
26181
+ }),
26182
+ external_exports.object({
26183
+ type: external_exports.literal("presence_update"),
26184
+ state: PeerPresenceStateSchema
25884
26185
  })
25885
26186
  ]);
25886
26187
  var DaemonToBrowserControlMessageSchema = external_exports.discriminatedUnion("type", [
@@ -25974,7 +26275,8 @@ var DaemonToBrowserControlMessageSchema = external_exports.discriminatedUnion("t
25974
26275
  }),
25975
26276
  external_exports.object({
25976
26277
  type: external_exports.literal("detected_ports"),
25977
- ports: external_exports.array(DetectedPortSchema)
26278
+ ports: external_exports.array(DetectedPortSchema),
26279
+ proxyPort: external_exports.number().optional()
25978
26280
  }),
25979
26281
  external_exports.object({
25980
26282
  type: external_exports.literal("set_preview_target"),
@@ -26056,6 +26358,11 @@ var DaemonToBrowserControlMessageSchema = external_exports.discriminatedUnion("t
26056
26358
  type: external_exports.literal("worktree_removed"),
26057
26359
  worktreePath: external_exports.string()
26058
26360
  }),
26361
+ external_exports.object({
26362
+ type: external_exports.literal("directory_list"),
26363
+ basePath: external_exports.string(),
26364
+ directories: external_exports.array(external_exports.string())
26365
+ }),
26059
26366
  external_exports.object({
26060
26367
  type: external_exports.literal("task_index_snapshot"),
26061
26368
  tasks: external_exports.record(external_exports.string(), TaskRecordSchema),
@@ -26247,6 +26554,15 @@ var DaemonToBrowserControlMessageSchema = external_exports.discriminatedUnion("t
26247
26554
  taskId: external_exports.string(),
26248
26555
  error: external_exports.string(),
26249
26556
  errorKind: TaskErrorKindSchema
26557
+ }),
26558
+ external_exports.object({
26559
+ type: external_exports.literal("recent_logs"),
26560
+ lines: external_exports.array(external_exports.string()),
26561
+ truncated: external_exports.boolean()
26562
+ }),
26563
+ external_exports.object({
26564
+ type: external_exports.literal("presence_state"),
26565
+ peers: external_exports.record(external_exports.string(), PeerPresenceSlotSchema)
26250
26566
  })
26251
26567
  ]);
26252
26568
  var TASK_MESSAGES_PREFIX = "task-messages:";
@@ -26645,14 +26961,6 @@ function structuredTaskToCCFile(task) {
26645
26961
  updatedAt: task.updatedAt
26646
26962
  };
26647
26963
  }
26648
- var TaskViewPresenceShape = Shape.plain.struct({
26649
- userId: Shape.plain.string(),
26650
- username: Shape.plain.string(),
26651
- avatarUrl: Shape.plain.string(),
26652
- taskId: Shape.plain.string(),
26653
- timestamp: Shape.plain.number()
26654
- });
26655
- var TaskViewDocSchema = Shape.doc({}, { mergeable: true });
26656
26964
  var TODO_ITEM_STATUSES = ["pending", "in_progress", "completed"];
26657
26965
  function generateTodoId(content) {
26658
26966
  let hash = 5381;
@@ -27504,14 +27812,25 @@ function runWithTimeout(command2, args, cwd, timeoutMs) {
27504
27812
  }
27505
27813
 
27506
27814
  // src/shared/capabilities/git-repo.ts
27815
+ var ghAvailableCache = false;
27507
27816
  async function isGhAvailable() {
27817
+ if (ghAvailableCache) return true;
27508
27818
  try {
27509
27819
  await run("which", ["gh"]);
27820
+ ghAvailableCache = true;
27510
27821
  return true;
27511
27822
  } catch {
27512
27823
  return false;
27513
27824
  }
27514
27825
  }
27826
+ var topLevelCache = /* @__PURE__ */ new Map();
27827
+ async function getGitTopLevel(cwd) {
27828
+ const cached = topLevelCache.get(cwd);
27829
+ if (cached !== void 0) return cached;
27830
+ const topLevel = (await runWithTimeout("git", ["rev-parse", "--show-toplevel"], cwd, TIMEOUT_MS)).trim();
27831
+ topLevelCache.set(cwd, topLevel);
27832
+ return topLevel;
27833
+ }
27515
27834
  function parseOwnerRepo(remoteUrl) {
27516
27835
  const match2 = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
27517
27836
  if (!match2?.[1] || !match2[2]) return null;
@@ -29186,7 +29505,7 @@ function nanoid(size2 = 21) {
29186
29505
  }
29187
29506
 
29188
29507
  // src/services/bootstrap/signaling.ts
29189
- var DAEMON_NPM_VERSION = true ? "3.0.0" : "unknown";
29508
+ var DAEMON_NPM_VERSION = true ? "3.1.0" : "unknown";
29190
29509
  function createDaemonSignaling(config2) {
29191
29510
  const agentId = config2.agentId ?? nanoid();
29192
29511
  function send(msg) {
@@ -29486,7 +29805,9 @@ function handleBrowserPreviewChannel(port, send, sendBinary, log, options) {
29486
29805
  function buildRequestHeaders(msgHeaders) {
29487
29806
  const headers = { host: `localhost:${port}` };
29488
29807
  for (const [k2, v2] of Object.entries(msgHeaders)) {
29489
- if (!HOP_BY_HOP_HEADERS.has(k2.toLowerCase())) headers[k2] = v2;
29808
+ const lower = k2.toLowerCase();
29809
+ if (HOP_BY_HOP_HEADERS.has(lower) || lower === "accept-encoding") continue;
29810
+ headers[k2] = v2;
29490
29811
  }
29491
29812
  const cookieHeader = buildCookieHeader();
29492
29813
  if (cookieHeader) {
@@ -29571,9 +29892,11 @@ var IFRAME_BLOCKING_HEADERS = /* @__PURE__ */ new Set([
29571
29892
  "content-security-policy-report-only"
29572
29893
  ]);
29573
29894
  var HISTORY_GUARD = `<script>(function(){var p=history.pushState,r=history.replaceState;history.pushState=function(){try{p.apply(this,arguments)}catch(e){if(!(e instanceof DOMException&&e.name==="SecurityError"))throw e}};history.replaceState=function(){try{r.apply(this,arguments)}catch(e){if(!(e instanceof DOMException&&e.name==="SecurityError"))throw e}};var la=Location.prototype.assign,lr=Location.prototype.replace;function patchLoc(orig,hFn){return function(url){if(typeof url==="string"&&url.charAt(0)==="#"){try{hFn.call(history,null,"",location.origin+location.pathname+location.search+url)}catch(e){location.hash=url}return}try{return orig.call(this,url)}catch(e){if(!(e instanceof DOMException&&e.name==="SecurityError"))throw e}}}try{Location.prototype.assign=patchLoc(la,p);Location.prototype.replace=patchLoc(lr,r)}catch(e){}})();</script>`;
29895
+ var COLLAB_REFRESH = `<script>(function(){if(!location.pathname.startsWith("/__preview/"))return;var R=WebSocket;WebSocket=function(u,p){var s={readyState:3,send:function(){},close:function(){},addEventListener:function(){},removeEventListener:function(){},onopen:null,onclose:null,onerror:null,onmessage:null,url:u,protocol:"",extensions:"",bufferedAmount:0,binaryType:"blob",CONNECTING:0,OPEN:1,CLOSING:2,CLOSED:3};setTimeout(function(){if(s.onclose)s.onclose({code:1006,reason:"",wasClean:false})},0);return s};WebSocket.CONNECTING=0;WebSocket.OPEN=1;WebSocket.CLOSING=2;WebSocket.CLOSED=3;var h="";function poll(){fetch(location.href,{cache:"no-store"}).then(function(r){return r.text()}).then(function(t){var n=0;for(var i=0;i<t.length;i++){n=((n<<5)-n+t.charCodeAt(i))|0}var s=String(n);if(h&&h!==s)location.reload();h=s}).catch(function(){});setTimeout(poll,3000)}setTimeout(poll,3000)})();</script>`;
29574
29896
  function injectBaseHref(html, origin) {
29575
- const baseTag = `<base href="${origin}/">`;
29576
- const injection = HISTORY_GUARD + baseTag + ANNOTATION_BRIDGE_TAG;
29897
+ const encodedOrigin = encodeURIComponent(origin);
29898
+ const baseTag = `<base href="/__preview/url/${encodedOrigin}/">`;
29899
+ const injection = HISTORY_GUARD + baseTag + COLLAB_REFRESH + ANNOTATION_BRIDGE_TAG;
29577
29900
  const headIdx = html.indexOf("<head");
29578
29901
  if (headIdx === -1) return injection + html;
29579
29902
  const closeIdx = html.indexOf(">", headIdx);
@@ -29630,7 +29953,9 @@ function handleBrowserPreviewUrlChannel(send, sendBinary, log, options) {
29630
29953
  function buildFetchHeaders(msgHeaders, host) {
29631
29954
  const headers = {};
29632
29955
  for (const [k2, v2] of Object.entries(msgHeaders)) {
29633
- if (!HOP_BY_HOP_HEADERS.has(k2.toLowerCase())) headers[k2] = v2;
29956
+ const lower = k2.toLowerCase();
29957
+ if (HOP_BY_HOP_HEADERS.has(lower) || lower === "accept-encoding") continue;
29958
+ headers[k2] = v2;
29634
29959
  }
29635
29960
  headers.host = host;
29636
29961
  return headers;
@@ -30802,7 +31127,7 @@ function handlePluginAuthRequest(pluginId, sendControl, deps, logAdapter) {
30802
31127
  }
30803
31128
 
30804
31129
  // src/services/pr-poller.ts
30805
- var PR_POLL_INTERVAL_MS = 6e4;
31130
+ var PR_POLL_INTERVAL_MS = 3e4;
30806
31131
  async function enrichClosedPR(pr, cwd) {
30807
31132
  if (pr.state !== "closed" || pr.mergeCommitSha) return pr;
30808
31133
  if (!pr.headRefSha) return pr;
@@ -30813,10 +31138,9 @@ async function enrichClosedPR(pr, cwd) {
30813
31138
  );
30814
31139
  return headInBase ? { ...pr, headCommitInBase: true } : pr;
30815
31140
  }
30816
- async function fetchCIData(pr, cwd) {
31141
+ async function fetchCIDataWithRemote(pr, cwd, remote) {
30817
31142
  const referenceCommit = pr?.mergeCommitSha ?? pr?.headRefSha ?? await run("git", ["rev-parse", "HEAD"], cwd).catch(() => "");
30818
31143
  if (!referenceCommit) return { deployments: [], workflowRuns: [], requiredChecks: [] };
30819
- const remote = await run("git", ["remote", "get-url", "origin"], cwd).catch(() => "");
30820
31144
  const parsed = parseOwnerRepo(remote);
30821
31145
  if (!parsed) return { deployments: [], workflowRuns: [], requiredChecks: [] };
30822
31146
  const branch = pr?.baseRef ?? "main";
@@ -30843,13 +31167,14 @@ async function fetchCIData(pr, cwd) {
30843
31167
  const allDeployments = [...deployments, ...placeholderDeployments];
30844
31168
  return { deployments: allDeployments, workflowRuns, requiredChecks };
30845
31169
  }
30846
- function createPRPoller(callbacks, log) {
30847
- const polls = /* @__PURE__ */ new Map();
30848
- async function fetchPRState(taskId, cwd) {
31170
+ function createPRPoller(callbacks, log, resolveTopLevel = getGitTopLevel) {
31171
+ const repos = /* @__PURE__ */ new Map();
31172
+ const taskToRepo = /* @__PURE__ */ new Map();
31173
+ let disposed = false;
31174
+ async function fetchPRState(cwd) {
30849
31175
  const available = await isGhAvailable();
30850
31176
  if (!available) {
30851
31177
  return {
30852
- taskId,
30853
31178
  prAvailable: false,
30854
31179
  currentBranchPR: null,
30855
31180
  assignedReviews: [],
@@ -30860,14 +31185,14 @@ function createPRPoller(callbacks, log) {
30860
31185
  updatedAt: Date.now()
30861
31186
  };
30862
31187
  }
30863
- const [currentPR, assigned, user] = await Promise.allSettled([
30864
- getPRForCurrentBranch(cwd),
30865
- getAssignedReviews(cwd),
30866
- getUserPRs(cwd)
31188
+ const [prResults, remote] = await Promise.all([
31189
+ Promise.allSettled([getPRForCurrentBranch(cwd), getAssignedReviews(cwd), getUserPRs(cwd)]),
31190
+ run("git", ["remote", "get-url", "origin"], cwd).catch(() => "")
30867
31191
  ]);
31192
+ const [currentPR, assigned, user] = prResults;
30868
31193
  const { allPRs: _2, currentBranchPR: rawPR } = deduplicatePRs(currentPR, assigned, user);
30869
31194
  const currentBranchPR = rawPR ? await enrichClosedPR(rawPR, cwd) : null;
30870
- const ciData = await fetchCIData(currentBranchPR, cwd).catch(
31195
+ const ciData = await fetchCIDataWithRemote(currentBranchPR, cwd, remote).catch(
30871
31196
  () => ({
30872
31197
  deployments: [],
30873
31198
  workflowRuns: [],
@@ -30875,7 +31200,6 @@ function createPRPoller(callbacks, log) {
30875
31200
  })
30876
31201
  );
30877
31202
  return {
30878
- taskId,
30879
31203
  prAvailable: true,
30880
31204
  currentBranchPR,
30881
31205
  assignedReviews: assigned.status === "fulfilled" ? assigned.value : [],
@@ -30886,81 +31210,135 @@ function createPRPoller(callbacks, log) {
30886
31210
  updatedAt: Date.now()
30887
31211
  };
30888
31212
  }
30889
- async function poll(taskId, entry) {
31213
+ function pushToSubscribers(entry, payload) {
31214
+ for (const taskId of entry.subscribers) {
31215
+ callbacks.onPRState({ ...payload, taskId });
31216
+ }
31217
+ }
31218
+ async function poll(topLevel, entry) {
31219
+ if (entry.isPolling) return;
31220
+ entry.isPolling = true;
30890
31221
  try {
30891
- const payload = await fetchPRState(taskId, entry.cwd);
31222
+ const currentBranch = await run("git", ["rev-parse", "--abbrev-ref", "HEAD"], entry.cwd).then((s2) => s2.trim()).catch(() => "");
31223
+ if (entry.lastBranch && currentBranch && currentBranch !== entry.lastBranch) {
31224
+ entry.lastContent = "";
31225
+ log.debug(
31226
+ { topLevel, oldBranch: entry.lastBranch, newBranch: currentBranch },
31227
+ "Branch changed, clearing dedup cache"
31228
+ );
31229
+ }
31230
+ if (currentBranch) entry.lastBranch = currentBranch;
31231
+ const payload = await fetchPRState(entry.cwd);
30892
31232
  if (!payload) return;
30893
31233
  const { updatedAt: _2, ...comparable } = payload;
30894
31234
  const contentKey = JSON.stringify(comparable);
30895
31235
  if (entry.lastContent === contentKey) {
30896
- log.debug({ taskId }, "PR state unchanged, skipping push");
31236
+ log.debug({ topLevel }, "PR state unchanged, skipping push");
30897
31237
  return;
30898
31238
  }
30899
31239
  entry.lastContent = contentKey;
30900
- callbacks.onPRState(payload);
30901
- log.debug({ taskId, prAvailable: payload.prAvailable }, "PR state pushed");
31240
+ entry.lastPayload = payload;
31241
+ pushToSubscribers(entry, payload);
31242
+ log.debug(
31243
+ { topLevel, prAvailable: payload.prAvailable, subscribers: entry.subscribers.size },
31244
+ "PR state pushed"
31245
+ );
30902
31246
  } catch (err) {
30903
- log.warn({ err, taskId }, "PR poll failed");
31247
+ log.warn({ err, topLevel }, "PR poll failed");
31248
+ } finally {
31249
+ entry.isPolling = false;
30904
31250
  }
30905
31251
  }
31252
+ function removeFromPreviousRepo(taskId) {
31253
+ const prev = taskToRepo.get(taskId);
31254
+ if (!prev) return;
31255
+ const prevEntry = repos.get(prev.topLevel);
31256
+ if (prevEntry) {
31257
+ prevEntry.subscribers.delete(taskId);
31258
+ if (prevEntry.subscribers.size === 0) {
31259
+ clearInterval(prevEntry.timer);
31260
+ for (const t of prevEntry.burstTimers) clearTimeout(t);
31261
+ repos.delete(prev.topLevel);
31262
+ log.info({ topLevel: prev.topLevel }, "PR polling stopped (no subscribers)");
31263
+ }
31264
+ }
31265
+ taskToRepo.delete(taskId);
31266
+ }
30906
31267
  function startPolling(taskId, cwd) {
30907
- stopPolling(taskId);
30908
- const entry = {
30909
- timer: null,
30910
- burstTimers: [],
30911
- lastContent: "",
30912
- cwd
30913
- };
30914
- poll(taskId, entry).catch((err) => {
30915
- log.warn({ err, taskId }, "Initial PR poll failed");
30916
- });
30917
- entry.timer = setInterval(() => {
30918
- poll(taskId, entry).catch((err) => {
30919
- log.warn({ err, taskId }, "PR poll tick failed");
31268
+ resolveTopLevel(cwd).then((topLevel) => {
31269
+ if (disposed) return;
31270
+ removeFromPreviousRepo(taskId);
31271
+ taskToRepo.set(taskId, { topLevel, cwd });
31272
+ const existing = repos.get(topLevel);
31273
+ if (existing) {
31274
+ existing.subscribers.add(taskId);
31275
+ if (existing.lastPayload) {
31276
+ callbacks.onPRState({ ...existing.lastPayload, taskId });
31277
+ }
31278
+ log.info({ taskId, topLevel, cwd }, "PR polling joined existing repo");
31279
+ return;
31280
+ }
31281
+ const entry = {
31282
+ timer: null,
31283
+ burstTimers: [],
31284
+ lastContent: "",
31285
+ lastBranch: "",
31286
+ lastPayload: null,
31287
+ cwd,
31288
+ subscribers: /* @__PURE__ */ new Set([taskId]),
31289
+ isPolling: false
31290
+ };
31291
+ poll(topLevel, entry).catch((err) => {
31292
+ log.warn({ err, topLevel }, "Initial PR poll failed");
30920
31293
  });
30921
- }, PR_POLL_INTERVAL_MS);
30922
- polls.set(taskId, entry);
30923
- log.info({ taskId, cwd }, "PR polling started");
31294
+ entry.timer = setInterval(() => {
31295
+ poll(topLevel, entry).catch((err) => {
31296
+ log.warn({ err, topLevel }, "PR poll tick failed");
31297
+ });
31298
+ }, PR_POLL_INTERVAL_MS);
31299
+ repos.set(topLevel, entry);
31300
+ log.info({ taskId, topLevel, cwd }, "PR polling started");
31301
+ }).catch((err) => {
31302
+ log.warn({ err, taskId, cwd }, "Failed to resolve git toplevel for PR polling");
31303
+ });
30924
31304
  }
30925
31305
  function getCwd(taskId) {
30926
- return polls.get(taskId)?.cwd ?? null;
31306
+ return taskToRepo.get(taskId)?.cwd ?? null;
30927
31307
  }
30928
31308
  function stopPolling(taskId) {
30929
- const existing = polls.get(taskId);
30930
- if (existing) {
30931
- clearInterval(existing.timer);
30932
- for (const t of existing.burstTimers) clearTimeout(t);
30933
- polls.delete(taskId);
30934
- log.info({ taskId }, "PR polling stopped");
30935
- }
31309
+ removeFromPreviousRepo(taskId);
30936
31310
  }
30937
31311
  function forceRefresh(taskId) {
30938
- const entry = polls.get(taskId);
31312
+ const info = taskToRepo.get(taskId);
31313
+ if (!info) return;
31314
+ const entry = repos.get(info.topLevel);
30939
31315
  if (!entry) return;
30940
31316
  for (const t of entry.burstTimers) clearTimeout(t);
30941
31317
  entry.burstTimers = [];
30942
31318
  entry.lastContent = "";
30943
- poll(taskId, entry).catch((err) => {
30944
- log.warn({ err, taskId }, "PR force refresh failed");
31319
+ poll(info.topLevel, entry).catch((err) => {
31320
+ log.warn({ err, topLevel: info.topLevel }, "PR force refresh failed");
30945
31321
  });
30946
31322
  for (const delayMs of [3e3, 8e3, 15e3]) {
30947
31323
  entry.burstTimers.push(
30948
31324
  setTimeout(() => {
30949
31325
  entry.lastContent = "";
30950
- poll(taskId, entry).catch((err) => {
30951
- log.warn({ err, taskId }, "PR burst refresh failed");
31326
+ poll(info.topLevel, entry).catch((err) => {
31327
+ log.warn({ err, topLevel: info.topLevel }, "PR burst refresh failed");
30952
31328
  });
30953
31329
  }, delayMs)
30954
31330
  );
30955
31331
  }
30956
31332
  }
30957
31333
  function dispose() {
30958
- for (const [taskId, entry] of polls) {
31334
+ disposed = true;
31335
+ for (const [topLevel, entry] of repos) {
30959
31336
  clearInterval(entry.timer);
30960
31337
  for (const t of entry.burstTimers) clearTimeout(t);
30961
- log.debug({ taskId }, "PR poll timer cleared on dispose");
31338
+ log.debug({ topLevel }, "PR poll timer cleared on dispose");
30962
31339
  }
30963
- polls.clear();
31340
+ repos.clear();
31341
+ taskToRepo.clear();
30964
31342
  }
30965
31343
  return { startPolling, stopPolling, forceRefresh, getCwd, dispose };
30966
31344
  }
@@ -31792,10 +32170,11 @@ async function rehydrateFromPersistence(persistence, taskManager, log, taskState
31792
32170
  async function sweepStaleTasks(taskStateStore, taskManager, log) {
31793
32171
  const tasks = await taskStateStore.listTasks();
31794
32172
  const actions = planStaleSweep(tasks, (id) => taskManager.isRunning(id));
32173
+ const noBroadcast = { broadcast: false };
31795
32174
  for (const action of actions) {
31796
32175
  if (action.kind === "mark_input_required") {
31797
- await taskStateStore.updateTaskStatus(action.taskId, "input_required");
31798
- await taskStateStore.acknowledge(action.taskId);
32176
+ await taskStateStore.updateTaskStatus(action.taskId, "input_required", noBroadcast);
32177
+ await taskStateStore.acknowledge(action.taskId, noBroadcast);
31799
32178
  log({
31800
32179
  event: "stale_task_swept",
31801
32180
  taskId: action.taskId,
@@ -33797,7 +34176,7 @@ var TOOL_DESCRIPTION2 = [
33797
34176
  "The preview panel opens in the Shipyard UI next to the conversation. The user can interact with it directly."
33798
34177
  ].join("\n");
33799
34178
  var SetPreviewTargetInput = {
33800
- port: external_exports.number().int().positive().optional().describe("Local port number of the running dev server (e.g. 3000, 5173, 8080)."),
34179
+ port: external_exports.number().int().min(1).max(65535).optional().describe("Local port number of the running dev server (e.g. 3000, 5173, 8080)."),
33801
34180
  url: external_exports.string().url().optional().describe('Fully qualified URL to preview (e.g. "http://localhost:3000/dashboard").')
33802
34181
  };
33803
34182
  function createPreviewTools(ctx) {
@@ -33818,9 +34197,14 @@ function createPreviewTools(ctx) {
33818
34197
  isError: true
33819
34198
  };
33820
34199
  }
33821
- await ctx.previewProxy.stop();
33822
- if (input.port != null && input.url == null) {
33823
- await ctx.previewProxy.start({ port: input.port });
34200
+ if (input.url != null) {
34201
+ await ctx.previewProxy.stop();
34202
+ } else if (input.port != null) {
34203
+ if (ctx.previewProxy.port) {
34204
+ ctx.previewProxy.retarget(input.port);
34205
+ } else {
34206
+ await ctx.previewProxy.start({ port: input.port });
34207
+ }
33824
34208
  }
33825
34209
  ctx.broadcastControl({
33826
34210
  type: "set_preview_target",
@@ -42788,6 +43172,16 @@ When you call ExitPlanMode, your plan is published to the Shipyard canvas for co
42788
43172
  **While in plan mode** \u2014 place supporting visualizations on the canvas using \`present(slug, "canvas")\`. Architecture diagrams, data models, and flow charts alongside the plan text help reviewers understand and evaluate your design.
42789
43173
 
42790
43174
  Do not revise the plan unless reviewers request changes. When you see \`<${XML_TAGS.PLAN_REVIEW} decision="approve">\`, call ExitPlanMode again to begin implementation.
43175
+
43176
+ ## Background Execution
43177
+
43178
+ Prefer background execution to keep your main thread responsive to collaborators.
43179
+
43180
+ **Agent tool** \u2014 Always use \`run_in_background: true\`. Foreground agents block your main thread \u2014 collaborators cannot interact with you, and there is no way to move a foreground operation to background after it starts. Background agents run in parallel and notify on completion.
43181
+
43182
+ **Bash tool** \u2014 Use \`run_in_background: true\` for commands that may take more than 10 seconds: builds, installs, test suites, large git operations, network requests. Use \`TaskOutput\` to read results when notified. Keep short commands (git status, ls, cat) in the foreground.
43183
+
43184
+ **Why this matters:** Foreground operations block everything. The only recovery from a stuck foreground operation is stopping the entire agent. Background-first execution avoids this and keeps you available for collaborator feedback.
42791
43185
  `.trim();
42792
43186
  function buildCollabSystemPrompt(participants) {
42793
43187
  const roster = participants.map((p2) => `${p2.name} (${p2.role})`).join(", ");
@@ -66428,11 +66822,13 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
66428
66822
  * Guards all SDK write operations to prevent "ProcessTransport not ready" errors.
66429
66823
  */
66430
66824
  #closed = false;
66431
- constructor(query3, controller, onEvent, spawnMcpServers) {
66825
+ #pidRef;
66826
+ constructor(query3, controller, onEvent, spawnMcpServers, pidRef) {
66432
66827
  this.#query = query3;
66433
66828
  this.#controller = controller;
66434
66829
  this.#onEvent = onEvent;
66435
66830
  this.#spawnMcpServers = spawnMcpServers;
66831
+ this.#pidRef = pidRef;
66436
66832
  }
66437
66833
  /** True once the subprocess has exited and SDK writes will throw. */
66438
66834
  get isClosed() {
@@ -66452,27 +66848,8 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
66452
66848
  const controller = new InputController();
66453
66849
  const { onChildSpawned, onChildExited } = options;
66454
66850
  const stderrCallback = options.stderr;
66455
- const spawnClaudeCodeProcess = onChildSpawned ? (spawnOpts) => {
66456
- const child = spawn4(spawnOpts.command, spawnOpts.args, {
66457
- cwd: spawnOpts.cwd,
66458
- stdio: ["pipe", "pipe", stderrCallback ? "pipe" : "ignore"],
66459
- signal: spawnOpts.signal,
66460
- env: { ...spawnOpts.env, CLAUDECODE: void 0 },
66461
- windowsHide: true
66462
- });
66463
- if (!child.stdin || !child.stdout) {
66464
- throw new Error("Failed to create stdio pipes for Claude subprocess");
66465
- }
66466
- if (stderrCallback && child.stderr) {
66467
- child.stderr.on("data", (chunk) => stderrCallback(chunk.toString()));
66468
- }
66469
- if (child.pid) {
66470
- const pid = child.pid;
66471
- onChildSpawned(pid);
66472
- child.on("exit", () => onChildExited?.(pid));
66473
- }
66474
- return Object.assign(child, { stdin: child.stdin, stdout: child.stdout });
66475
- } : void 0;
66851
+ const pidRef = { current: null };
66852
+ const spawnClaudeCodeProcess = onChildSpawned ? createChildSpawner(stderrCallback, pidRef, onChildSpawned, onChildExited) : void 0;
66476
66853
  const queryInstance = queryFn({
66477
66854
  prompt: controller.iterable(),
66478
66855
  options: {
@@ -66503,7 +66880,8 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
66503
66880
  queryInstance,
66504
66881
  controller,
66505
66882
  onEvent,
66506
- spawnMcpServers
66883
+ spawnMcpServers,
66884
+ pidRef
66507
66885
  );
66508
66886
  if (initialContent.length > 0) {
66509
66887
  controller.push(toSdkContent(initialContent));
@@ -66528,6 +66906,13 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
66528
66906
  this.#query.close();
66529
66907
  }
66530
66908
  forceKill() {
66909
+ if (this.#pidRef.current != null) {
66910
+ try {
66911
+ process.kill(this.#pidRef.current, "SIGKILL");
66912
+ } catch {
66913
+ }
66914
+ this.#pidRef.current = null;
66915
+ }
66531
66916
  this.#closed = true;
66532
66917
  this.#controller.end();
66533
66918
  this.#query.close();
@@ -66584,6 +66969,14 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
66584
66969
  await this.#query.reconnectMcpServer(serverName);
66585
66970
  }
66586
66971
  }
66972
+ async stopBackgroundTask(taskId) {
66973
+ if (this.#closed) return;
66974
+ try {
66975
+ await this.#query.stopTask(taskId);
66976
+ } catch (err) {
66977
+ if (!this.#closed) throw err;
66978
+ }
66979
+ }
66587
66980
  async #runMessageLoop(log) {
66588
66981
  try {
66589
66982
  for await (const message of this.#query) {
@@ -66592,9 +66985,11 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
66592
66985
  this.#onEvent(event);
66593
66986
  }
66594
66987
  }
66988
+ this.#pidRef.current = null;
66595
66989
  this.#closed = true;
66596
66990
  this.#onEvent({ type: "subprocess_died" });
66597
66991
  } catch (error2) {
66992
+ this.#pidRef.current = null;
66598
66993
  this.#closed = true;
66599
66994
  const errorMsg = error2 instanceof Error ? error2.message : String(error2);
66600
66995
  this.#onEvent({
@@ -66605,6 +67000,30 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
66605
67000
  }
66606
67001
  }
66607
67002
  };
67003
+ function createChildSpawner(stderrCallback, pidRef, onChildSpawned, onChildExited) {
67004
+ return (spawnOpts) => {
67005
+ const child = spawn4(spawnOpts.command, spawnOpts.args, {
67006
+ cwd: spawnOpts.cwd,
67007
+ stdio: ["pipe", "pipe", stderrCallback ? "pipe" : "ignore"],
67008
+ signal: spawnOpts.signal,
67009
+ env: { ...spawnOpts.env, CLAUDECODE: void 0 },
67010
+ windowsHide: true
67011
+ });
67012
+ if (!child.stdin || !child.stdout) {
67013
+ throw new Error("Failed to create stdio pipes for Claude subprocess");
67014
+ }
67015
+ if (stderrCallback && child.stderr) {
67016
+ child.stderr.on("data", (chunk) => stderrCallback(chunk.toString()));
67017
+ }
67018
+ if (child.pid) {
67019
+ const pid = child.pid;
67020
+ pidRef.current = pid;
67021
+ onChildSpawned(pid);
67022
+ child.on("exit", () => onChildExited?.(pid));
67023
+ }
67024
+ return Object.assign(child, { stdin: child.stdin, stdout: child.stdout });
67025
+ };
67026
+ }
66608
67027
  function toSdkPermissionMode(mode) {
66609
67028
  switch (mode) {
66610
67029
  case "default":
@@ -72958,6 +73377,9 @@ var DirectApiSubprocess = class _DirectApiSubprocess {
72958
73377
  /** No-op: no MCP in direct API mode. */
72959
73378
  async reconnectMcpServer(_serverName) {
72960
73379
  }
73380
+ /** No-op: direct API mode does not support background tasks. */
73381
+ async stopBackgroundTask(_taskId) {
73382
+ }
72961
73383
  /** No-op in Phase 1 -- harness task context not used by direct API mode. */
72962
73384
  setHarnessTaskId(_taskId) {
72963
73385
  }
@@ -75062,12 +75484,20 @@ function noop3(snapshot) {
75062
75484
  };
75063
75485
  }
75064
75486
  function handleColdIdle(snapshot, event) {
75065
- if (event.type !== "user_message") return noop3(snapshot);
75066
- const b2 = createEffectBuilder();
75067
- b2.log("cold_idle", "spawning", event.type);
75068
- b2.taskStatus("in_progress");
75069
- b2.effects.push({ type: "spawn", reason: { kind: "fresh" } });
75070
- return { state: "spawning", sessionId: null, rewindAtMessageId: null, effects: b2.effects };
75487
+ if (event.type === "user_message") {
75488
+ const b2 = createEffectBuilder();
75489
+ b2.log("cold_idle", "spawning", event.type);
75490
+ b2.taskStatus("in_progress");
75491
+ b2.effects.push({ type: "spawn", reason: { kind: "fresh" } });
75492
+ return { state: "spawning", sessionId: null, rewindAtMessageId: null, effects: b2.effects };
75493
+ }
75494
+ if (event.type === "stop_commanded") {
75495
+ const b2 = createEffectBuilder();
75496
+ b2.effects.push({ type: "clear_queue" });
75497
+ b2.taskStatus("input_required");
75498
+ return { state: "cold_idle", sessionId: null, rewindAtMessageId: null, effects: b2.effects };
75499
+ }
75500
+ return noop3(snapshot);
75071
75501
  }
75072
75502
  function handleResumableIdle(snapshot, event) {
75073
75503
  const { sessionId, rewindAtMessageId } = snapshot;
@@ -75086,6 +75516,12 @@ function handleResumableIdle(snapshot, event) {
75086
75516
  b2.effects.push({ type: "spawn", reason });
75087
75517
  return { state: "spawning", sessionId, rewindAtMessageId: null, effects: b2.effects };
75088
75518
  }
75519
+ if (event.type === "stop_commanded") {
75520
+ const b2 = createEffectBuilder();
75521
+ b2.effects.push({ type: "clear_queue" });
75522
+ b2.taskStatus("input_required");
75523
+ return { state: "resumable_idle", sessionId, rewindAtMessageId, effects: b2.effects };
75524
+ }
75089
75525
  if (event.type === "session_expired") {
75090
75526
  const b2 = createEffectBuilder();
75091
75527
  b2.log("resumable_idle", "cold_idle", event.type);
@@ -75245,12 +75681,7 @@ function exitStoppingAlive(snapshot, trigger) {
75245
75681
  const { sessionId, rewindAtMessageId } = snapshot;
75246
75682
  const b2 = createEffectBuilder();
75247
75683
  b2.effects.push({ type: "clear_pending_inputs" });
75248
- if (snapshot.queueDepth > 0) {
75249
- b2.log("stopping", "running", trigger);
75250
- b2.effects.push({ type: "push_message" });
75251
- b2.taskStatus("in_progress");
75252
- return { state: "running", sessionId, rewindAtMessageId, effects: b2.effects };
75253
- }
75684
+ b2.effects.push({ type: "clear_queue" });
75254
75685
  b2.log("stopping", "warm_idle", trigger);
75255
75686
  b2.taskStatus("input_required");
75256
75687
  return { state: "warm_idle", sessionId, rewindAtMessageId, effects: b2.effects };
@@ -75278,7 +75709,7 @@ function handleStopping(snapshot, event) {
75278
75709
  b2.taskStatus("input_required");
75279
75710
  return { state: "resumable_idle", sessionId, rewindAtMessageId, effects: b2.effects };
75280
75711
  }
75281
- if (event.type === "stop_timeout") {
75712
+ if (event.type === "stop_timeout" || event.type === "interrupt_failed") {
75282
75713
  b2.log("stopping", "resumable_idle", event.type);
75283
75714
  b2.effects.push({ type: "force_kill" });
75284
75715
  b2.effects.push({ type: "clear_pending_inputs" });
@@ -75367,6 +75798,9 @@ var AgentSessionManager = class {
75367
75798
  notifyInterruptAcknowledged() {
75368
75799
  this.#dispatch({ type: "interrupt_acknowledged" });
75369
75800
  }
75801
+ notifyInterruptFailed() {
75802
+ this.#dispatch({ type: "interrupt_failed" });
75803
+ }
75370
75804
  notifyCloseAcknowledged() {
75371
75805
  this.#dispatch({ type: "close_acknowledged" });
75372
75806
  }
@@ -75868,6 +76302,10 @@ var Thread = class {
75868
76302
  if (!this.#subprocess) return;
75869
76303
  return this.#subprocess.mcpSubmitOAuthCallbackUrl(serverName, callbackUrl);
75870
76304
  }
76305
+ async stopBackgroundTask(taskId) {
76306
+ if (!this.#subprocess) return;
76307
+ await this.#subprocess.stopBackgroundTask(taskId);
76308
+ }
75871
76309
  async dispose() {
75872
76310
  this.#disposed = true;
75873
76311
  this.#permissionHandler.denyAllPending("Thread disposed");
@@ -76158,13 +76596,18 @@ ${conversationReplay}` : conversationReplay;
76158
76596
  this.#subprocess?.pushMessage(content);
76159
76597
  }
76160
76598
  #handleInterrupt() {
76161
- this.#subprocess?.interrupt().catch((err) => {
76599
+ if (!this.#subprocess || this.#subprocess.isClosed) {
76600
+ this.#manager.notifySubprocessDied();
76601
+ return;
76602
+ }
76603
+ this.#subprocess.interrupt().catch((err) => {
76162
76604
  const msg = err instanceof Error ? err.message : String(err);
76163
76605
  this.#config.log({
76164
76606
  event: "thread_interrupt_failed",
76165
76607
  threadId: this.#config.threadId,
76166
76608
  error: msg
76167
76609
  });
76610
+ this.#manager.notifyInterruptFailed();
76168
76611
  });
76169
76612
  }
76170
76613
  #handleClose() {
@@ -81061,6 +81504,9 @@ var Task = class _Task {
81061
81504
  this.#explicitlyStopped = true;
81062
81505
  this.#mainThread.stop();
81063
81506
  }
81507
+ async stopBackgroundTask(backgroundTaskId) {
81508
+ await this.#mainThread.stopBackgroundTask(backgroundTaskId);
81509
+ }
81064
81510
  cancelQueued() {
81065
81511
  const count = this.#mainThread.cancelQueued();
81066
81512
  const collabCount = this.#collabQueue.cancelQueued();
@@ -82081,6 +82527,122 @@ Use this context to maintain continuity. You have already done this work \u2014
82081
82527
  }
82082
82528
  };
82083
82529
 
82530
+ // src/services/task/task-manager-mcp.ts
82531
+ function hasLiveSubprocess(state) {
82532
+ return state === "warm_idle" || state === "running" || state === "spawning";
82533
+ }
82534
+ function updateMcpServers(tasks, deps) {
82535
+ const resolved = deps.resolveMcpServers() ?? {};
82536
+ for (const task of tasks.values()) {
82537
+ if (!hasLiveSubprocess(task.orchestrator.state)) continue;
82538
+ task.orchestrator.setMcpServers(resolved).then((result) => {
82539
+ deps.log({
82540
+ event: "mcp_servers_updated",
82541
+ taskId: task.taskId,
82542
+ added: result?.added ?? [],
82543
+ removed: result?.removed ?? [],
82544
+ errors: result?.errors ?? {}
82545
+ });
82546
+ }).catch((err) => {
82547
+ deps.log({
82548
+ event: "mcp_servers_update_failed",
82549
+ taskId: task.taskId,
82550
+ error: err instanceof Error ? err.message : String(err)
82551
+ });
82552
+ });
82553
+ }
82554
+ }
82555
+ function triggerFastMcpPolling(tasks) {
82556
+ for (const task of tasks.values()) {
82557
+ if (!hasLiveSubprocess(task.orchestrator.state)) continue;
82558
+ task.orchestrator.triggerFastMcpPolling();
82559
+ }
82560
+ }
82561
+ function toggleMcpServer(tasks, serverName, enabled, log) {
82562
+ for (const task of tasks.values()) {
82563
+ if (!hasLiveSubprocess(task.orchestrator.state)) continue;
82564
+ task.orchestrator.toggleMcpServer(serverName, enabled).then(() => {
82565
+ log({
82566
+ event: "mcp_server_toggled",
82567
+ taskId: task.taskId,
82568
+ serverName,
82569
+ enabled
82570
+ });
82571
+ }).catch((err) => {
82572
+ log({
82573
+ event: "mcp_server_toggle_failed",
82574
+ taskId: task.taskId,
82575
+ serverName,
82576
+ enabled,
82577
+ error: err instanceof Error ? err.message : String(err)
82578
+ });
82579
+ });
82580
+ }
82581
+ }
82582
+ function reconnectMcpServer(tasks, serverName, deps) {
82583
+ const fullSet = deps.resolveMcpServers() ?? {};
82584
+ for (const task of tasks.values()) {
82585
+ if (!hasLiveSubprocess(task.orchestrator.state)) continue;
82586
+ const withoutTarget = { ...fullSet };
82587
+ delete withoutTarget[serverName];
82588
+ task.orchestrator.setMcpServers(withoutTarget).then(() => task.orchestrator.setMcpServers(fullSet)).then((result) => {
82589
+ deps.log({
82590
+ event: "mcp_server_reconnected",
82591
+ taskId: task.taskId,
82592
+ serverName,
82593
+ added: result?.added ?? [],
82594
+ removed: result?.removed ?? []
82595
+ });
82596
+ }).catch((err) => {
82597
+ deps.log({
82598
+ event: "mcp_server_reconnect_failed",
82599
+ taskId: task.taskId,
82600
+ serverName,
82601
+ error: err instanceof Error ? err.message : String(err)
82602
+ });
82603
+ });
82604
+ }
82605
+ }
82606
+ async function mcpAuthenticate(tasks, serverName) {
82607
+ for (const task of tasks.values()) {
82608
+ if (!hasLiveSubprocess(task.orchestrator.state)) continue;
82609
+ return task.orchestrator.mcpAuthenticate(serverName);
82610
+ }
82611
+ return null;
82612
+ }
82613
+ async function mcpSubmitOAuthCallbackUrl(tasks, serverName, callbackUrl) {
82614
+ for (const task of tasks.values()) {
82615
+ if (!hasLiveSubprocess(task.orchestrator.state)) continue;
82616
+ await task.orchestrator.mcpSubmitOAuthCallbackUrl(serverName, callbackUrl);
82617
+ return;
82618
+ }
82619
+ }
82620
+ function planGracefulShutdown(tasks, gracePeriodMs) {
82621
+ const immediate = [];
82622
+ const deferred = [];
82623
+ for (const task of tasks) {
82624
+ switch (task.state) {
82625
+ case "cold_idle":
82626
+ case "resumable_idle":
82627
+ case "warm_idle":
82628
+ immediate.push({ taskId: task.taskId, kind: "dispose_immediately" });
82629
+ break;
82630
+ case "spawning":
82631
+ case "running":
82632
+ deferred.push({ taskId: task.taskId, kind: "stop_then_wait" });
82633
+ break;
82634
+ case "stopping":
82635
+ deferred.push({ taskId: task.taskId, kind: "stop_then_wait" });
82636
+ break;
82637
+ default: {
82638
+ const _exhaustive = task.state;
82639
+ throw new Error(`Unhandled state: ${JSON.stringify(_exhaustive)}`);
82640
+ }
82641
+ }
82642
+ }
82643
+ return { immediate, deferred, gracePeriodMs };
82644
+ }
82645
+
82084
82646
  // src/services/task/task-manager.ts
82085
82647
  function cloneOverlay(overlay) {
82086
82648
  return {
@@ -82605,111 +83167,32 @@ var TaskManager = class {
82605
83167
  this.#deps.log({ event: "task_stop_requested", taskId });
82606
83168
  this.#tasks.get(taskId)?.orchestrator.stop();
82607
83169
  }
82608
- /**
82609
- * Hot-update MCP servers on all live subprocesses. Since user/plugin
82610
- * MCPs are added via the dynamic pool (setMcpServers after init),
82611
- * they can be freely added and removed without subprocess restart.
82612
- */
82613
- updateMcpServers() {
82614
- const resolved = this.#deps.resolveMcpServers() ?? {};
82615
- for (const task of this.#tasks.values()) {
82616
- if (!this.#hasLiveSubprocess(task.orchestrator.state)) continue;
82617
- task.orchestrator.setMcpServers(resolved).then((result) => {
82618
- this.#deps.log({
82619
- event: "mcp_servers_updated",
82620
- taskId: task.taskId,
82621
- added: result?.added ?? [],
82622
- removed: result?.removed ?? [],
82623
- errors: result?.errors ?? {}
82624
- });
82625
- }).catch((err) => {
82626
- this.#deps.log({
82627
- event: "mcp_servers_update_failed",
82628
- taskId: task.taskId,
82629
- error: err instanceof Error ? err.message : String(err)
82630
- });
82631
- });
83170
+ async stopBackgroundTask(taskId, backgroundTaskId) {
83171
+ this.#deps.log({ event: "background_task_stop_requested", taskId, backgroundTaskId });
83172
+ const task = this.#tasks.get(taskId);
83173
+ if (!task) {
83174
+ this.#deps.log({ event: "background_task_stop_unknown_task", taskId, backgroundTaskId });
83175
+ return;
82632
83176
  }
83177
+ await task.orchestrator.stopBackgroundTask(backgroundTaskId);
83178
+ }
83179
+ updateMcpServers() {
83180
+ updateMcpServers(this.#tasks, this.#deps);
82633
83181
  }
82634
83182
  triggerFastMcpPolling() {
82635
- for (const task of this.#tasks.values()) {
82636
- if (!this.#hasLiveSubprocess(task.orchestrator.state)) continue;
82637
- task.orchestrator.triggerFastMcpPolling();
82638
- }
83183
+ triggerFastMcpPolling(this.#tasks);
82639
83184
  }
82640
83185
  toggleMcpServer(serverName, enabled) {
82641
- for (const task of this.#tasks.values()) {
82642
- if (!this.#hasLiveSubprocess(task.orchestrator.state)) continue;
82643
- task.orchestrator.toggleMcpServer(serverName, enabled).then(() => {
82644
- this.#deps.log({
82645
- event: "mcp_server_toggled",
82646
- taskId: task.taskId,
82647
- serverName,
82648
- enabled
82649
- });
82650
- }).catch((err) => {
82651
- this.#deps.log({
82652
- event: "mcp_server_toggle_failed",
82653
- taskId: task.taskId,
82654
- serverName,
82655
- enabled,
82656
- error: err instanceof Error ? err.message : String(err)
82657
- });
82658
- });
82659
- }
83186
+ toggleMcpServer(this.#tasks, serverName, enabled, this.#deps.log);
82660
83187
  }
82661
- /**
82662
- * Force reconnect a specific MCP server after token refresh.
82663
- *
82664
- * The SDK subprocess diffs setMcpServers by name: if the server name
82665
- * already exists it is NOT reconnected even when headers change. To
82666
- * pick up fresh credentials we remove the server first, then re-add it.
82667
- */
82668
83188
  reconnectMcpServer(serverName) {
82669
- const fullSet = this.#deps.resolveMcpServers() ?? {};
82670
- for (const task of this.#tasks.values()) {
82671
- if (!this.#hasLiveSubprocess(task.orchestrator.state)) continue;
82672
- const withoutTarget = { ...fullSet };
82673
- delete withoutTarget[serverName];
82674
- task.orchestrator.setMcpServers(withoutTarget).then(() => task.orchestrator.setMcpServers(fullSet)).then((result) => {
82675
- this.#deps.log({
82676
- event: "mcp_server_reconnected",
82677
- taskId: task.taskId,
82678
- serverName,
82679
- added: result?.added ?? [],
82680
- removed: result?.removed ?? []
82681
- });
82682
- }).catch((err) => {
82683
- this.#deps.log({
82684
- event: "mcp_server_reconnect_failed",
82685
- taskId: task.taskId,
82686
- serverName,
82687
- error: err instanceof Error ? err.message : String(err)
82688
- });
82689
- });
82690
- }
83189
+ reconnectMcpServer(this.#tasks, serverName, this.#deps);
82691
83190
  }
82692
- /**
82693
- * Initiate SDK-level OAuth for an MCP server on any active task.
82694
- * Returns the authorize URL if the SDK supports it, null otherwise.
82695
- */
82696
83191
  async mcpAuthenticate(serverName) {
82697
- for (const task of this.#tasks.values()) {
82698
- if (!this.#hasLiveSubprocess(task.orchestrator.state)) continue;
82699
- return task.orchestrator.mcpAuthenticate(serverName);
82700
- }
82701
- return null;
83192
+ return mcpAuthenticate(this.#tasks, serverName);
82702
83193
  }
82703
83194
  async mcpSubmitOAuthCallbackUrl(serverName, callbackUrl) {
82704
- for (const task of this.#tasks.values()) {
82705
- if (!this.#hasLiveSubprocess(task.orchestrator.state)) continue;
82706
- await task.orchestrator.mcpSubmitOAuthCallbackUrl(serverName, callbackUrl);
82707
- return;
82708
- }
82709
- }
82710
- /** True when the state implies a subprocess is alive and can accept commands. */
82711
- #hasLiveSubprocess(state) {
82712
- return state === "warm_idle" || state === "running" || state === "spawning";
83195
+ return mcpSubmitOAuthCallbackUrl(this.#tasks, serverName, callbackUrl);
82713
83196
  }
82714
83197
  cancelQueued(taskId) {
82715
83198
  const task = this.#tasks.get(taskId);
@@ -82886,7 +83369,7 @@ var TaskManager = class {
82886
83369
  /** Whether any task has a live subprocess that can accept MCP commands. */
82887
83370
  get hasLiveSubprocesses() {
82888
83371
  for (const task of this.#tasks.values()) {
82889
- if (this.#hasLiveSubprocess(task.orchestrator.state)) return true;
83372
+ if (hasLiveSubprocess(task.orchestrator.state)) return true;
82890
83373
  }
82891
83374
  return false;
82892
83375
  }
@@ -83016,31 +83499,6 @@ var TaskManager = class {
83016
83499
  });
83017
83500
  }
83018
83501
  };
83019
- function planGracefulShutdown(tasks, gracePeriodMs) {
83020
- const immediate = [];
83021
- const deferred = [];
83022
- for (const task of tasks) {
83023
- switch (task.state) {
83024
- case "cold_idle":
83025
- case "resumable_idle":
83026
- case "warm_idle":
83027
- immediate.push({ taskId: task.taskId, kind: "dispose_immediately" });
83028
- break;
83029
- case "spawning":
83030
- case "running":
83031
- deferred.push({ taskId: task.taskId, kind: "stop_then_wait" });
83032
- break;
83033
- case "stopping":
83034
- deferred.push({ taskId: task.taskId, kind: "stop_then_wait" });
83035
- break;
83036
- default: {
83037
- const _exhaustive = task.state;
83038
- throw new Error(`Unhandled state: ${JSON.stringify(_exhaustive)}`);
83039
- }
83040
- }
83041
- }
83042
- return { immediate, deferred, gracePeriodMs };
83043
- }
83044
83502
 
83045
83503
  // src/services/task/task-state-store.ts
83046
83504
  import { join as join35 } from "path";
@@ -83072,12 +83530,43 @@ function buildTaskStateStore(dataDir) {
83072
83530
  const unsubVersion = store.subscribe(() => {
83073
83531
  _version++;
83074
83532
  });
83533
+ const _broadcastQueue = [];
83534
+ const taskListeners = /* @__PURE__ */ new Set();
83535
+ const unsubBroadcast = store.subscribe((event) => {
83536
+ const broadcast = _broadcastQueue.shift() ?? true;
83537
+ const augmented = { ...event, broadcast };
83538
+ for (const listener of taskListeners) {
83539
+ try {
83540
+ listener(augmented);
83541
+ } catch {
83542
+ }
83543
+ }
83544
+ });
83545
+ function pushBroadcast(options) {
83546
+ _broadcastQueue.push(options?.broadcast ?? true);
83547
+ }
83548
+ async function safeUpdate(taskId, fn, options) {
83549
+ pushBroadcast(options);
83550
+ const lenBefore = _broadcastQueue.length;
83551
+ try {
83552
+ await store.update(taskId, fn);
83553
+ } catch (err) {
83554
+ if (_broadcastQueue.length === lenBefore) {
83555
+ _broadcastQueue.pop();
83556
+ }
83557
+ throw err;
83558
+ }
83559
+ if (_broadcastQueue.length === lenBefore) {
83560
+ _broadcastQueue.pop();
83561
+ }
83562
+ }
83075
83563
  return {
83076
83564
  get version() {
83077
83565
  return _version;
83078
83566
  },
83079
- async createTask({ taskId, channelId, title, cwd, mode, scheduleId, scheduleName }) {
83567
+ async createTask({ taskId, channelId, title, cwd, mode, scheduleId, scheduleName }, options) {
83080
83568
  const now = Date.now();
83569
+ pushBroadcast(options);
83081
83570
  await store.set(taskId, {
83082
83571
  taskId,
83083
83572
  channelId,
@@ -83097,41 +83586,46 @@ function buildTaskStateStore(dataDir) {
83097
83586
  ...scheduleName ? { scheduleName } : {}
83098
83587
  });
83099
83588
  },
83100
- async updateTaskStatus(taskId, status) {
83589
+ async updateTaskStatus(taskId, status, options) {
83101
83590
  const task = await store.get(taskId);
83102
83591
  if (!task) return;
83592
+ pushBroadcast(options);
83103
83593
  await store.set(taskId, applyStatusTransition(task, status, Date.now()));
83104
83594
  },
83105
- async updateTitle(taskId, title) {
83595
+ async updateTitle(taskId, title, options) {
83106
83596
  const task = await store.get(taskId);
83107
83597
  if (!task) return;
83598
+ pushBroadcast(options);
83108
83599
  await store.set(taskId, {
83109
83600
  ...task,
83110
83601
  title,
83111
83602
  updatedAt: Date.now()
83112
83603
  });
83113
83604
  },
83114
- async updateCwd(taskId, cwd) {
83605
+ async updateCwd(taskId, cwd, options) {
83115
83606
  const task = await store.get(taskId);
83116
83607
  if (!task) return;
83608
+ pushBroadcast(options);
83117
83609
  await store.set(taskId, {
83118
83610
  ...task,
83119
83611
  cwd,
83120
83612
  updatedAt: Date.now()
83121
83613
  });
83122
83614
  },
83123
- async updateMode(taskId, mode) {
83615
+ async updateMode(taskId, mode, options) {
83124
83616
  const task = await store.get(taskId);
83125
83617
  if (!task) return;
83618
+ pushBroadcast(options);
83126
83619
  await store.set(taskId, {
83127
83620
  ...task,
83128
83621
  mode,
83129
83622
  updatedAt: Date.now()
83130
83623
  });
83131
83624
  },
83132
- async updateTodoProgress(taskId, progress) {
83625
+ async updateTodoProgress(taskId, progress, options) {
83133
83626
  const task = await store.get(taskId);
83134
83627
  if (!task) return;
83628
+ pushBroadcast(options);
83135
83629
  await store.set(taskId, {
83136
83630
  ...task,
83137
83631
  todoCompleted: progress.todoCompleted,
@@ -83140,15 +83634,16 @@ function buildTaskStateStore(dataDir) {
83140
83634
  updatedAt: Date.now()
83141
83635
  });
83142
83636
  },
83143
- async acknowledge(taskId) {
83144
- await store.update(taskId, (task) => ({ ...task, acknowledgedAt: Date.now() }));
83637
+ async acknowledge(taskId, options) {
83638
+ await safeUpdate(taskId, (task) => ({ ...task, acknowledgedAt: Date.now() }), options);
83145
83639
  },
83146
- async togglePin(taskId) {
83147
- await store.update(taskId, (task) => ({ ...task, pinned: !task.pinned }));
83640
+ async togglePin(taskId, options) {
83641
+ await safeUpdate(taskId, (task) => ({ ...task, pinned: !task.pinned }), options);
83148
83642
  },
83149
- async updateStructuredTasks(taskId, tasks, todoProgress) {
83643
+ async updateStructuredTasks(taskId, tasks, todoProgress, options) {
83150
83644
  const task = await store.get(taskId);
83151
83645
  if (!task) return;
83646
+ pushBroadcast(options);
83152
83647
  await store.set(taskId, {
83153
83648
  ...task,
83154
83649
  structuredTasks: tasks,
@@ -83160,39 +83655,49 @@ function buildTaskStateStore(dataDir) {
83160
83655
  updatedAt: Date.now()
83161
83656
  });
83162
83657
  },
83163
- async updateTaskOverlay(taskId, overlay) {
83658
+ async updateTaskOverlay(taskId, overlay, options) {
83164
83659
  const task = await store.get(taskId);
83165
83660
  if (!task) return;
83661
+ pushBroadcast(options);
83166
83662
  await store.set(taskId, {
83167
83663
  ...task,
83168
83664
  taskOverlay: overlay,
83169
83665
  updatedAt: Date.now()
83170
83666
  });
83171
83667
  },
83172
- async updateComposerSettings(taskId, settings) {
83668
+ async updateComposerSettings(taskId, settings, options) {
83173
83669
  const task = await store.get(taskId);
83174
83670
  if (!task) return;
83671
+ pushBroadcast(options);
83175
83672
  await store.set(taskId, {
83176
83673
  ...task,
83177
83674
  composerSettings: { ...task.composerSettings, ...settings },
83178
83675
  updatedAt: Date.now()
83179
83676
  });
83180
83677
  },
83181
- async updateCostStats(taskId, stats) {
83182
- await store.update(taskId, (task) => ({
83183
- ...task,
83184
- totalCostUsd: stats.totalCostUsd,
83185
- totalOutputTokens: stats.totalOutputTokens,
83186
- updatedAt: Date.now()
83187
- }));
83678
+ async updateCostStats(taskId, stats, options) {
83679
+ await safeUpdate(
83680
+ taskId,
83681
+ (task) => ({
83682
+ ...task,
83683
+ totalCostUsd: stats.totalCostUsd,
83684
+ totalOutputTokens: stats.totalOutputTokens,
83685
+ updatedAt: Date.now()
83686
+ }),
83687
+ options
83688
+ );
83188
83689
  },
83189
- async clearCostStats(taskId) {
83190
- await store.update(taskId, (task) => ({
83191
- ...task,
83192
- totalCostUsd: 0,
83193
- totalOutputTokens: 0,
83194
- updatedAt: Date.now()
83195
- }));
83690
+ async clearCostStats(taskId, options) {
83691
+ await safeUpdate(
83692
+ taskId,
83693
+ (task) => ({
83694
+ ...task,
83695
+ totalCostUsd: 0,
83696
+ totalOutputTokens: 0,
83697
+ updatedAt: Date.now()
83698
+ }),
83699
+ options
83700
+ );
83196
83701
  },
83197
83702
  async getTask(taskId) {
83198
83703
  return store.get(taskId);
@@ -83201,10 +83706,14 @@ function buildTaskStateStore(dataDir) {
83201
83706
  return store.list();
83202
83707
  },
83203
83708
  subscribe(listener) {
83204
- return store.subscribe(listener);
83709
+ taskListeners.add(listener);
83710
+ return () => {
83711
+ taskListeners.delete(listener);
83712
+ };
83205
83713
  },
83206
83714
  dispose() {
83207
83715
  unsubVersion();
83716
+ unsubBroadcast();
83208
83717
  }
83209
83718
  };
83210
83719
  }
@@ -83963,6 +84472,7 @@ async function createDaemon(deps) {
83963
84472
  await rehydrateFromPersistence(sessionPersistence, taskManager, deps.log, taskStateStore);
83964
84473
  await sweepStaleTasks(taskStateStore, taskManager, deps.log);
83965
84474
  const taskStoreUnsub = taskStateStore.subscribe((event) => {
84475
+ if (!event.broadcast) return;
83966
84476
  switch (event.kind) {
83967
84477
  case "set": {
83968
84478
  taskManager.broadcastControl({
@@ -84311,10 +84821,12 @@ var ROLE_PERMISSIONS = {
84311
84821
  "list_schedules",
84312
84822
  "run_schedule_now",
84313
84823
  "stop_task",
84824
+ "request_preview_target",
84314
84825
  "todo_item_added",
84315
84826
  "todo_item_removed",
84316
84827
  "todo_item_updated",
84317
- "todo_dep_changed"
84828
+ "todo_dep_changed",
84829
+ "presence_update"
84318
84830
  ]),
84319
84831
  "collaborator-full": /* @__PURE__ */ new Set([
84320
84832
  "send_message",
@@ -84341,10 +84853,12 @@ var ROLE_PERMISSIONS = {
84341
84853
  "request_annotation_snapshot",
84342
84854
  "create_thread",
84343
84855
  "list_threads",
84856
+ "request_preview_target",
84344
84857
  "todo_item_added",
84345
84858
  "todo_item_removed",
84346
84859
  "todo_item_updated",
84347
- "todo_dep_changed"
84860
+ "todo_dep_changed",
84861
+ "presence_update"
84348
84862
  ]),
84349
84863
  "collaborator-review": /* @__PURE__ */ new Set([
84350
84864
  "stop",
@@ -84355,7 +84869,8 @@ var ROLE_PERMISSIONS = {
84355
84869
  "join_collab_room",
84356
84870
  "leave_collab_room",
84357
84871
  "request_annotation_snapshot",
84358
- "list_threads"
84872
+ "list_threads",
84873
+ "presence_update"
84359
84874
  ]),
84360
84875
  viewer: /* @__PURE__ */ new Set([
84361
84876
  "subscribe",
@@ -84363,7 +84878,8 @@ var ROLE_PERMISSIONS = {
84363
84878
  "request_capabilities",
84364
84879
  "request_pr_data",
84365
84880
  "join_collab_room",
84366
- "leave_collab_room"
84881
+ "leave_collab_room",
84882
+ "presence_update"
84367
84883
  ])
84368
84884
  };
84369
84885
  function isCollabActionAllowed(role, action) {
@@ -84536,6 +85052,7 @@ function filterOutboundForCollab(msg, collabTaskId) {
84536
85052
  case "worktree_error":
84537
85053
  case "worktree_list":
84538
85054
  case "worktree_removed":
85055
+ case "directory_list":
84539
85056
  case "collab_room_joined":
84540
85057
  case "collab_room_left":
84541
85058
  case "schedule_fired":
@@ -84572,11 +85089,15 @@ function filterOutboundForCollab(msg, collabTaskId) {
84572
85089
  /** Collab-scoped: always pass through (keyed by machineId/roomId, not taskId) */
84573
85090
  case "collab_peer_role":
84574
85091
  case "collab_participants_update":
85092
+ case "presence_state":
84575
85093
  return msg;
84576
85094
  /** Pass through: collab peers need error feedback */
84577
85095
  case "promote_nudge":
84578
85096
  case "error":
84579
85097
  return msg;
85098
+ /** Log request is local-only, not relevant for collab peers */
85099
+ case "recent_logs":
85100
+ return null;
84580
85101
  /** Exhaustiveness check: compile error if a new type is unhandled */
84581
85102
  default: {
84582
85103
  const _exhaustive = msg;
@@ -84851,6 +85372,9 @@ function routeMessage(msg, callbacks, log) {
84851
85372
  case "request_user_settings":
84852
85373
  callbacks.onRequestUserSettings();
84853
85374
  break;
85375
+ case "request_preview_target":
85376
+ callbacks.onRequestPreviewTarget(msg.port, msg.url);
85377
+ break;
84854
85378
  /** Unified annotation CRUD */
84855
85379
  case "add_annotation":
84856
85380
  callbacks.onAddAnnotation(msg.taskId, msg.annotation);
@@ -84931,6 +85455,9 @@ function routeMessage(msg, callbacks, log) {
84931
85455
  case "stop_task":
84932
85456
  callbacks.onStopTask(msg.taskId);
84933
85457
  break;
85458
+ case "stop_background_agent":
85459
+ callbacks.onStopBackgroundAgent(msg.taskId, msg.backgroundTaskId);
85460
+ break;
84934
85461
  case "todo_item_added":
84935
85462
  callbacks.onTodoItemAdded(msg.taskId, msg.item);
84936
85463
  break;
@@ -84950,6 +85477,15 @@ function routeMessage(msg, callbacks, log) {
84950
85477
  msg.action
84951
85478
  );
84952
85479
  break;
85480
+ case "request_recent_logs":
85481
+ callbacks.onRequestRecentLogs(msg.windowMinutes);
85482
+ break;
85483
+ case "presence_update":
85484
+ callbacks.onPresenceUpdate(msg.state);
85485
+ break;
85486
+ case "list_directories":
85487
+ callbacks.onListDirectories(msg.basePath);
85488
+ break;
84953
85489
  default: {
84954
85490
  const _exhaustive = msg;
84955
85491
  throw new Error(`Unhandled control message type: ${JSON.stringify(_exhaustive)}`);
@@ -84991,6 +85527,7 @@ function injectThreadResourceLink(store, mainChannelId, taskId, threadId, title,
84991
85527
 
84992
85528
  // src/services/channels/control-channel-infra-handlers.ts
84993
85529
  import { spawn as spawn6 } from "child_process";
85530
+ import { readdir as readdir7 } from "fs/promises";
84994
85531
 
84995
85532
  // src/services/worktree-service.ts
84996
85533
  import { execFile as execFile7, spawn as spawn5 } from "child_process";
@@ -85482,6 +86019,24 @@ function buildAgentInstallHandlers(deps) {
85482
86019
  }
85483
86020
  };
85484
86021
  }
86022
+ var FILTERED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".turbo", "dist", ".next", "build"]);
86023
+ function buildDirectoryListHandler(deps) {
86024
+ return {
86025
+ onListDirectories: (basePath) => {
86026
+ readdir7(basePath, { withFileTypes: true }).then((entries) => {
86027
+ const directories = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !FILTERED_DIRS.has(e.name)).map((e) => e.name).sort();
86028
+ deps.sendControlMessage({ type: "directory_list", basePath, directories });
86029
+ }).catch((err) => {
86030
+ deps.logAdapter({
86031
+ event: "list_directories_failed",
86032
+ basePath,
86033
+ error: err instanceof Error ? err.message : String(err)
86034
+ });
86035
+ deps.sendControlMessage({ type: "directory_list", basePath, directories: [] });
86036
+ });
86037
+ }
86038
+ };
86039
+ }
85485
86040
  function buildEnvironmentChangedHandler(deps) {
85486
86041
  const { daemon } = deps;
85487
86042
  let debounceTimer = null;
@@ -85715,6 +86270,99 @@ function runPluginOp(pluginName, marketplace, action, ctx) {
85715
86270
  });
85716
86271
  }
85717
86272
 
86273
+ // src/services/channels/read-recent-logs.ts
86274
+ import { createReadStream, readdirSync as readdirSync2 } from "fs";
86275
+ import { join as join38 } from "path";
86276
+ import { createInterface } from "readline";
86277
+ var MAX_BYTES = 5e4;
86278
+ var LOG_BASE_NAME = "daemon.log";
86279
+ var SENSITIVE_PATTERNS = [
86280
+ [/eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/g, "[REDACTED_JWT]"],
86281
+ [/"[Aa]uthorization"\s*:\s*"[^"]+"/g, '"Authorization":"[REDACTED]"'],
86282
+ [/"token"\s*:\s*"[^"]+"/g, '"token":"[REDACTED]"'],
86283
+ [/"accessToken"\s*:\s*"[^"]+"/g, '"accessToken":"[REDACTED]"'],
86284
+ [/"refreshToken"\s*:\s*"[^"]+"/g, '"refreshToken":"[REDACTED]"'],
86285
+ [/sk-ant-[A-Za-z0-9_-]{20,}/g, "[REDACTED_API_KEY]"],
86286
+ [/sk-[a-zA-Z0-9]{20,}/g, "[REDACTED_SECRET_KEY]"],
86287
+ [/device_code=[A-Za-z0-9_-]+/g, "device_code=[REDACTED]"]
86288
+ ];
86289
+ function scrubSensitiveData(line) {
86290
+ let result = line;
86291
+ for (const [pattern, replacement] of SENSITIVE_PATTERNS) {
86292
+ result = result.replace(pattern, replacement);
86293
+ }
86294
+ return result;
86295
+ }
86296
+ async function collectFromFile(filePath, cutoff, collector, maxBytes) {
86297
+ const rl = createInterface({
86298
+ input: createReadStream(filePath, { encoding: "utf-8" }),
86299
+ crlfDelay: Number.POSITIVE_INFINITY
86300
+ });
86301
+ for await (const line of rl) {
86302
+ if (!line) continue;
86303
+ const timestamp = extractTimestamp(line);
86304
+ if (timestamp === null || timestamp < cutoff) continue;
86305
+ const scrubbed = scrubSensitiveData(line);
86306
+ const lineBytes = Buffer.byteLength(scrubbed, "utf-8");
86307
+ if (collector.totalBytes + lineBytes > maxBytes) {
86308
+ collector.truncated = true;
86309
+ rl.close();
86310
+ return true;
86311
+ }
86312
+ collector.lines.push(scrubbed);
86313
+ collector.totalBytes += lineBytes;
86314
+ }
86315
+ return false;
86316
+ }
86317
+ async function readRecentLogs(logDir, windowMinutes, maxBytes = MAX_BYTES) {
86318
+ const cutoff = Date.now() - windowMinutes * 6e4;
86319
+ const collector = { lines: [], totalBytes: 0, truncated: false };
86320
+ for (const filePath of discoverLogFiles(logDir)) {
86321
+ try {
86322
+ const exhausted = await collectFromFile(filePath, cutoff, collector, maxBytes);
86323
+ if (exhausted) break;
86324
+ } catch {
86325
+ }
86326
+ }
86327
+ return { lines: collector.lines, truncated: collector.truncated };
86328
+ }
86329
+ function discoverLogFiles(logDir) {
86330
+ let entries;
86331
+ try {
86332
+ entries = readdirSync2(logDir);
86333
+ } catch {
86334
+ return [];
86335
+ }
86336
+ const rotated = [];
86337
+ let currentFile = null;
86338
+ for (const entry of entries) {
86339
+ if (entry === LOG_BASE_NAME) {
86340
+ currentFile = join38(logDir, entry);
86341
+ } else if (entry.startsWith(`${LOG_BASE_NAME}.`)) {
86342
+ const suffix = entry.slice(LOG_BASE_NAME.length + 1);
86343
+ const index = Number.parseInt(suffix, 10);
86344
+ if (!Number.isNaN(index)) {
86345
+ rotated.push({ path: join38(logDir, entry), index });
86346
+ }
86347
+ }
86348
+ }
86349
+ rotated.sort((a, b2) => b2.index - a.index);
86350
+ const result = rotated.map((r) => r.path);
86351
+ if (currentFile) result.push(currentFile);
86352
+ return result;
86353
+ }
86354
+ function extractTimestamp(line) {
86355
+ const idx = line.indexOf('"time":');
86356
+ if (idx === -1) return null;
86357
+ const start = idx + 7;
86358
+ let end = start;
86359
+ while (end < line.length && line.charCodeAt(end) >= 48 && line.charCodeAt(end) <= 57) {
86360
+ end++;
86361
+ }
86362
+ if (end === start) return null;
86363
+ return Number(line.slice(start, end));
86364
+ }
86365
+
85718
86366
  // src/services/channels/schedule-channel-callbacks.ts
85719
86367
  function buildScheduleCallbacks(daemon, getControlHandler, log) {
85720
86368
  return {
@@ -85883,6 +86531,33 @@ function buildTemplateCallbacks(daemon, getControlHandler, log) {
85883
86531
  }
85884
86532
 
85885
86533
  // src/services/channels/control-channel-wiring.ts
86534
+ function handlePreviewTargetRequest(proxy, port, url, daemon, log) {
86535
+ if (!proxy) return;
86536
+ (async () => {
86537
+ try {
86538
+ if (url) await proxy.stop();
86539
+ else if (port) proxy.port ? proxy.retarget(port) : await proxy.start({ port });
86540
+ daemon.taskManager.broadcastControl({
86541
+ type: "set_preview_target",
86542
+ port,
86543
+ url,
86544
+ proxyPort: proxy.port ?? void 0
86545
+ });
86546
+ } catch (err) {
86547
+ log({
86548
+ event: "preview_target_request_failed",
86549
+ error: err instanceof Error ? err.message : String(err)
86550
+ });
86551
+ }
86552
+ })();
86553
+ }
86554
+ function resolveIdentityFromRegistry(deps) {
86555
+ if (!deps.peerRoleRegistry || !deps.peerMachineId) return void 0;
86556
+ const entry = deps.peerRoleRegistry.getEntry(deps.peerMachineId);
86557
+ if (!entry) return void 0;
86558
+ const userId = entry.participantId.replace(/^human:/, "");
86559
+ return { userId, username: entry.displayName, avatarUrl: null };
86560
+ }
85886
86561
  function applyMcpServerToggles(daemon, prevDisabled, nextDisabled, log) {
85887
86562
  for (const name of nextDisabled) {
85888
86563
  if (!prevDisabled.has(name)) {
@@ -85978,6 +86653,10 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
85978
86653
  const worktreeHandlers = buildWorktreeHandlers(lazyInfraDeps);
85979
86654
  const agentInstallHandlers = buildAgentInstallHandlers(lazyInfraDeps);
85980
86655
  const envChangedHandler = buildEnvironmentChangedHandler(lazyInfraDeps);
86656
+ const directoryListHandler = buildDirectoryListHandler({
86657
+ sendControlMessage: (msg) => controlHandler.sendControl(msg),
86658
+ logAdapter
86659
+ });
85981
86660
  const prPoller = createPRPoller(
85982
86661
  {
85983
86662
  onPRState: (payload) => {
@@ -86241,6 +86920,7 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
86241
86920
  },
86242
86921
  ...agentInstallHandlers,
86243
86922
  ...worktreeHandlers,
86923
+ ...directoryListHandler,
86244
86924
  onJoinCollabRoom: (() => {
86245
86925
  const mgr = deps?.collabRoomManager;
86246
86926
  if (!mgr) return void 0;
@@ -86545,6 +87225,9 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
86545
87225
  });
86546
87226
  });
86547
87227
  },
87228
+ onRequestPreviewTarget: (port, url) => {
87229
+ handlePreviewTargetRequest(deps?.previewProxy, port, url, daemon, logAdapter);
87230
+ },
86548
87231
  onAcknowledgeTask: (taskId) => {
86549
87232
  daemon.taskStateStore.acknowledge(taskId).catch((err) => {
86550
87233
  logAdapter({
@@ -86574,9 +87257,44 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
86574
87257
  });
86575
87258
  }
86576
87259
  },
87260
+ onStopBackgroundAgent: (taskId, backgroundTaskId) => {
87261
+ daemon.taskManager.stopBackgroundTask(taskId, backgroundTaskId).catch((err) => {
87262
+ logAdapter({
87263
+ event: "stop_background_agent_failed",
87264
+ taskId,
87265
+ backgroundTaskId,
87266
+ error: err instanceof Error ? err.message : String(err)
87267
+ });
87268
+ });
87269
+ },
86577
87270
  ...buildScheduleCallbacks(daemon, () => controlHandler, logAdapter),
86578
87271
  ...buildTemplateCallbacks(daemon, () => controlHandler, logAdapter),
86579
- ...todoHandlers
87272
+ ...todoHandlers,
87273
+ onRequestRecentLogs: (windowMinutes) => {
87274
+ const logDir = `${deps?.shipyardHome ?? getShipyardHome()}/logs`;
87275
+ readRecentLogs(logDir, windowMinutes).then((result) => {
87276
+ handler.sendControl({ type: "recent_logs", ...result });
87277
+ logAdapter({
87278
+ event: "recent_logs_sent",
87279
+ lineCount: result.lines.length,
87280
+ truncated: result.truncated
87281
+ });
87282
+ }).catch((err) => {
87283
+ handler.sendControl({ type: "recent_logs", lines: [], truncated: false });
87284
+ logAdapter({
87285
+ event: "recent_logs_failed",
87286
+ error: err instanceof Error ? err.message : String(err)
87287
+ });
87288
+ });
87289
+ },
87290
+ onPresenceUpdate: (state) => {
87291
+ if (!deps?.presencePool) return;
87292
+ const identity = deps.peerIdentity ?? resolveIdentityFromRegistry(deps);
87293
+ if (!identity) return;
87294
+ const ref = deps.presencePool;
87295
+ ref.pool = applyPresenceUpdate(ref.pool, controlPeerId, identity, state);
87296
+ daemon.taskManager.broadcastControl({ type: "presence_state", peers: ref.pool });
87297
+ }
86580
87298
  },
86581
87299
  logAdapter
86582
87300
  );
@@ -86699,6 +87417,9 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
86699
87417
  const userSettingsUnsub = daemon.userSettingsStore.subscribe((settings) => {
86700
87418
  controlHandler.sendControl({ type: "user_settings_updated", settings });
86701
87419
  });
87420
+ if (deps?.presencePool) {
87421
+ controlHandler.sendControl({ type: "presence_state", peers: deps.presencePool.pool });
87422
+ }
86702
87423
  const enforcedTaskId = deps?.collabTaskId;
86703
87424
  dc.onmessage = (ev) => {
86704
87425
  const raw = typeof ev.data === "string" ? ev.data : String(ev.data);
@@ -86725,6 +87446,11 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
86725
87446
  handler.dispose();
86726
87447
  daemon.taskManager.unregisterControlChannel(controlPeerId);
86727
87448
  daemon.taskManager.setOnAuthNotLoggedIn(null);
87449
+ if (deps?.presencePool) {
87450
+ const ref = deps.presencePool;
87451
+ ref.pool = removePresence(ref.pool, controlPeerId);
87452
+ daemon.taskManager.broadcastControl({ type: "presence_state", peers: ref.pool });
87453
+ }
86728
87454
  };
86729
87455
  logAdapter({
86730
87456
  event: "control_channel_wired",
@@ -86952,6 +87678,77 @@ function handleMessageChannel(opts) {
86952
87678
  };
86953
87679
  }
86954
87680
 
87681
+ // src/services/collab/collab-repo-manager.ts
87682
+ var DAEMON_IDENTITY2 = { name: "shipyard-daemon", type: "service" };
87683
+ var COLLAB_VISIBLE_PREFIXES = /* @__PURE__ */ new Set(["plan", "canvas"]);
87684
+ var COLLAB_MUTABLE_PREFIXES = {
87685
+ "collaborator-full": /* @__PURE__ */ new Set(["plan", "canvas"]),
87686
+ "collaborator-review": /* @__PURE__ */ new Set(["plan"])
87687
+ };
87688
+ function planCollabRoomResources(taskId, roomId, epochs) {
87689
+ return {
87690
+ docIds: [buildPlanDocId(taskId, epochs.plan), buildCanvasDocId(taskId, epochs.canvas)],
87691
+ personalBridgeType: `bridge-personal-${roomId}`,
87692
+ collabBridgeType: `bridge-collab-${roomId}`
87693
+ };
87694
+ }
87695
+ function buildCollabRepoPermissions(taskId, peerRoleRegistry) {
87696
+ return {
87697
+ visibility(doc3, peer) {
87698
+ if (peer.channelKind === "storage") return true;
87699
+ if (peer.peerType === "service") return true;
87700
+ const parsed = parseDocumentId(doc3.id);
87701
+ if (!parsed) return false;
87702
+ if (parsed.key !== taskId) return false;
87703
+ return COLLAB_VISIBLE_PREFIXES.has(parsed.prefix);
87704
+ },
87705
+ mutability(doc3, peer) {
87706
+ if (peer.channelKind === "storage") return true;
87707
+ if (peer.peerType === "service") return true;
87708
+ const parsed = parseDocumentId(doc3.id);
87709
+ if (!parsed) return false;
87710
+ if (parsed.key !== taskId) return false;
87711
+ const entry = peerRoleRegistry.getEntry(peer.peerId);
87712
+ if (!entry) return false;
87713
+ const allowed = COLLAB_MUTABLE_PREFIXES[entry.role];
87714
+ return allowed ? allowed.has(parsed.prefix) : false;
87715
+ },
87716
+ creation(_docId, _peer) {
87717
+ return false;
87718
+ },
87719
+ deletion(_doc, _peer) {
87720
+ return false;
87721
+ }
87722
+ };
87723
+ }
87724
+ function createCollabRepo(personalRepo, taskId, roomId, epochs, peerRoleRegistry) {
87725
+ const resources = planCollabRoomResources(taskId, roomId, epochs);
87726
+ const bridge = new Bridge();
87727
+ const personalBridgeAdapter = new BridgeAdapter({
87728
+ adapterType: resources.personalBridgeType,
87729
+ bridge
87730
+ });
87731
+ const collabBridgeAdapter = new BridgeAdapter({
87732
+ adapterType: resources.collabBridgeType,
87733
+ bridge
87734
+ });
87735
+ const collabWebrtcAdapter = new WebRtcDataChannelAdapter();
87736
+ const collabRepo = new Repo({
87737
+ identity: { ...DAEMON_IDENTITY2, name: `collab-room-${roomId}` },
87738
+ adapters: [collabBridgeAdapter, collabWebrtcAdapter],
87739
+ permissions: buildCollabRepoPermissions(taskId, peerRoleRegistry)
87740
+ });
87741
+ personalRepo.addAdapter(personalBridgeAdapter);
87742
+ return {
87743
+ repo: collabRepo,
87744
+ webrtcAdapter: collabWebrtcAdapter,
87745
+ async destroy() {
87746
+ personalRepo.removeAdapter(personalBridgeAdapter.adapterId);
87747
+ await collabRepo.shutdown();
87748
+ }
87749
+ };
87750
+ }
87751
+
86955
87752
  // src/services/collab/collab-room-manager.ts
86956
87753
  function assertNever2(x2) {
86957
87754
  throw new Error(`Unhandled message type: ${JSON.stringify(x2)}`);
@@ -87084,6 +87881,7 @@ function createCollabRoomManager(deps) {
87084
87881
  for (const userId of room.knownPeers) {
87085
87882
  deps.peerRoleRegistry.unregisterPeer(namespacePeerId(roomId, userId));
87086
87883
  }
87884
+ room.collabRepoHandle?.destroy();
87087
87885
  room.peerManager.destroy();
87088
87886
  room.connection.disconnect();
87089
87887
  rooms.delete(roomId);
@@ -87121,7 +87919,19 @@ function createCollabRoomManager(deps) {
87121
87919
  maxDelayMs: 3e4,
87122
87920
  backoffMultiplier: 2
87123
87921
  });
87124
- const peerManager = deps.createPeerManagerForRoom(roomId, connection, taskId);
87922
+ const collabRepoHandle = createCollabRepo(
87923
+ deps.personalRepo,
87924
+ taskId,
87925
+ roomId,
87926
+ { plan: deps.planEpoch, canvas: deps.canvasEpoch },
87927
+ deps.peerRoleRegistry
87928
+ );
87929
+ const peerManager = deps.createPeerManagerForRoom(
87930
+ roomId,
87931
+ connection,
87932
+ taskId,
87933
+ collabRepoHandle.webrtcAdapter
87934
+ );
87125
87935
  const handle = {
87126
87936
  roomId,
87127
87937
  taskId,
@@ -87132,6 +87942,7 @@ function createCollabRoomManager(deps) {
87132
87942
  const delayMs = expiresAt - Date.now();
87133
87943
  if (delayMs <= 0 || !Number.isFinite(delayMs)) {
87134
87944
  deps.log({ event: "collab_expired", roomId, expiresAt });
87945
+ collabRepoHandle.destroy();
87135
87946
  return handle;
87136
87947
  }
87137
87948
  const safeDelayMs = Math.min(delayMs, 2147483647);
@@ -87140,6 +87951,7 @@ function createCollabRoomManager(deps) {
87140
87951
  taskId,
87141
87952
  connection,
87142
87953
  peerManager,
87954
+ collabRepoHandle,
87143
87955
  myUserId: null,
87144
87956
  knownPeers: /* @__PURE__ */ new Set(),
87145
87957
  participants: [],
@@ -87163,7 +87975,12 @@ function createCollabRoomManager(deps) {
87163
87975
  deps.peerRoleRegistry.unregisterPeer(namespacePeerId(roomId, userId));
87164
87976
  }
87165
87977
  room.peerManager.destroy();
87166
- room.peerManager = deps.createPeerManagerForRoom(roomId, connection, taskId);
87978
+ room.peerManager = deps.createPeerManagerForRoom(
87979
+ roomId,
87980
+ connection,
87981
+ taskId,
87982
+ room.collabRepoHandle?.webrtcAdapter ?? collabRepoHandle.webrtcAdapter
87983
+ );
87167
87984
  room.myUserId = null;
87168
87985
  room.knownPeers.clear();
87169
87986
  }
@@ -87204,41 +88021,20 @@ function findEntry(registry, peer) {
87204
88021
  }
87205
88022
  return void 0;
87206
88023
  }
87207
- var COLLAB_VISIBLE_PREFIXES = {
87208
- "collaborator-full": /* @__PURE__ */ new Set(["plan", "canvas"]),
87209
- "collaborator-review": /* @__PURE__ */ new Set(["plan", "canvas"]),
87210
- viewer: /* @__PURE__ */ new Set([])
87211
- };
87212
- var COLLAB_MUTABLE_PREFIXES = {
87213
- "collaborator-full": /* @__PURE__ */ new Set(["plan", "canvas"]),
87214
- "collaborator-review": /* @__PURE__ */ new Set(["plan"])
87215
- };
87216
88024
  function buildCollabPermissions(registry) {
88025
+ function isAllowed(_doc, peer) {
88026
+ if (peer.channelKind === "storage") return true;
88027
+ if (peer.peerType === "service") return true;
88028
+ const entry = findEntry(registry, peer);
88029
+ if (!entry) return false;
88030
+ return isPersonalPeer(entry);
88031
+ }
87217
88032
  return {
87218
- visibility(doc3, peer) {
87219
- if (peer.channelKind === "storage") return true;
87220
- const entry = findEntry(registry, peer);
87221
- if (!entry) return false;
87222
- if (isPersonalPeer(entry)) return true;
87223
- const parsed = parseDocumentId(doc3.id);
87224
- if (!parsed) return false;
87225
- if (parsed.key !== entry.taskId) return false;
87226
- const allowed = COLLAB_VISIBLE_PREFIXES[entry.role];
87227
- return allowed ? allowed.has(parsed.prefix) : false;
87228
- },
87229
- mutability(doc3, peer) {
87230
- if (peer.channelKind === "storage") return true;
87231
- const entry = findEntry(registry, peer);
87232
- if (!entry) return false;
87233
- if (isPersonalPeer(entry)) return true;
87234
- const parsed = parseDocumentId(doc3.id);
87235
- if (!parsed) return false;
87236
- if (parsed.key !== entry.taskId) return false;
87237
- const allowed = COLLAB_MUTABLE_PREFIXES[entry.role];
87238
- return allowed ? allowed.has(parsed.prefix) : false;
87239
- },
88033
+ visibility: isAllowed,
88034
+ mutability: isAllowed,
87240
88035
  creation(_docId, peer) {
87241
88036
  if (peer.channelKind === "storage") return true;
88037
+ if (peer.peerType === "service") return true;
87242
88038
  const entry = findEntry(registry, peer);
87243
88039
  if (!entry) return false;
87244
88040
  return isPersonalPeer(entry);
@@ -87298,8 +88094,8 @@ function createPeerRoleRegistry() {
87298
88094
  }
87299
88095
 
87300
88096
  // src/services/epoch-pruning.ts
87301
- import { readdir as readdir7, rm as rm3 } from "fs/promises";
87302
- import { join as join38 } from "path";
88097
+ import { readdir as readdir8, rm as rm3 } from "fs/promises";
88098
+ import { join as join39 } from "path";
87303
88099
  var LEGACY_PREFIXES = [
87304
88100
  "task-meta",
87305
88101
  "task-conv",
@@ -87316,7 +88112,7 @@ function isLegacyDocId(decoded) {
87316
88112
  async function pruneOldEpochData(dataDir, currentEpoch, log) {
87317
88113
  let entries;
87318
88114
  try {
87319
- entries = await readdir7(dataDir, { withFileTypes: true });
88115
+ entries = await readdir8(dataDir, { withFileTypes: true });
87320
88116
  } catch {
87321
88117
  return;
87322
88118
  }
@@ -87334,7 +88130,7 @@ async function pruneOldEpochData(dataDir, currentEpoch, log) {
87334
88130
  if (!parsed) {
87335
88131
  if (isLegacyDocId(decoded)) {
87336
88132
  removals.push(
87337
- rm3(join38(dataDir, entry.name), { recursive: true }).then(() => {
88133
+ rm3(join39(dataDir, entry.name), { recursive: true }).then(() => {
87338
88134
  pruned++;
87339
88135
  }).catch((err) => {
87340
88136
  log({
@@ -87349,7 +88145,7 @@ async function pruneOldEpochData(dataDir, currentEpoch, log) {
87349
88145
  }
87350
88146
  if (parsed.epoch >= currentEpoch) continue;
87351
88147
  removals.push(
87352
- rm3(join38(dataDir, entry.name), { recursive: true }).then(() => {
88148
+ rm3(join39(dataDir, entry.name), { recursive: true }).then(() => {
87353
88149
  pruned++;
87354
88150
  }).catch((err) => {
87355
88151
  log({
@@ -87368,7 +88164,7 @@ async function pruneOldEpochData(dataDir, currentEpoch, log) {
87368
88164
 
87369
88165
  // src/services/file-io-handler.ts
87370
88166
  import { execFile as execFile9, spawn as spawn7 } from "child_process";
87371
- import { readdir as readdir8, readFile as readFile25, stat as stat5, unlink as unlink6, writeFile as writeFile22 } from "fs/promises";
88167
+ import { readdir as readdir9, readFile as readFile25, stat as stat5, unlink as unlink6, writeFile as writeFile22 } from "fs/promises";
87372
88168
  import { normalize as normalize5, relative as relative2, resolve } from "path";
87373
88169
  import { promisify as promisify6 } from "util";
87374
88170
  function handleFileIOChannel(initialCwd, send, log, deps) {
@@ -87532,7 +88328,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
87532
88328
  }
87533
88329
  async function handleReaddir(requestId, absPath) {
87534
88330
  try {
87535
- const entries = await readdir8(absPath, { withFileTypes: true });
88331
+ const entries = await readdir9(absPath, { withFileTypes: true });
87536
88332
  const result = entries.filter((e) => !e.name.startsWith(".") && e.name !== "node_modules").map((e) => ({
87537
88333
  name: e.name,
87538
88334
  type: e.isDirectory() ? "directory" : "file"
@@ -88271,17 +89067,25 @@ function guardLoroChannelSend(dc, log) {
88271
89067
  const originalSend = dc.send.bind(dc);
88272
89068
  let dropCount = 0;
88273
89069
  let lastLogAt = 0;
89070
+ const logDrop = (reason) => {
89071
+ dropCount++;
89072
+ const now = Date.now();
89073
+ if (now - lastLogAt >= 1e3) {
89074
+ log.warn({ droppedCount: dropCount, reason }, "Loro sync send dropped");
89075
+ dropCount = 0;
89076
+ lastLogAt = now;
89077
+ }
89078
+ };
88274
89079
  dc.send = (data) => {
89080
+ if (dc.readyState !== void 0 && dc.readyState !== "open") return;
89081
+ if (typeof dc.bufferedAmount === "number" && dc.bufferedAmount > LORO_BACKPRESSURE_HIGH_WATER) {
89082
+ logDrop("backpressure");
89083
+ return;
89084
+ }
88275
89085
  try {
88276
89086
  originalSend(data);
88277
89087
  } catch {
88278
- dropCount++;
88279
- const now = Date.now();
88280
- if (now - lastLogAt >= 1e3) {
88281
- log.warn({ droppedCount: dropCount }, "Loro sync send failed (SCTP overflow)");
88282
- dropCount = 0;
88283
- lastLogAt = now;
88284
- }
89088
+ logDrop("send_error");
88285
89089
  }
88286
89090
  };
88287
89091
  }
@@ -88526,8 +89330,8 @@ function createPeerManager(config2) {
88526
89330
 
88527
89331
  // src/services/plugins/plugin-file-watcher.ts
88528
89332
  import { existsSync as existsSync6, watch as watch4 } from "fs";
88529
- import { readdir as readdir9, readFile as readFile26, stat as stat6 } from "fs/promises";
88530
- import { join as join39 } from "path";
89333
+ import { readdir as readdir10, readFile as readFile26, stat as stat6 } from "fs/promises";
89334
+ import { join as join40 } from "path";
88531
89335
  import { pathToFileURL } from "url";
88532
89336
  var DEBOUNCE_MS2 = 500;
88533
89337
  var PLUGIN_ID_PATTERN2 = /^[a-z0-9][a-z0-9-]*$/;
@@ -88578,14 +89382,14 @@ var PluginFileWatcher = class {
88578
89382
  }
88579
89383
  let entries;
88580
89384
  try {
88581
- entries = await readdir9(dir);
89385
+ entries = await readdir10(dir);
88582
89386
  } catch {
88583
89387
  this.#reconcile([]);
88584
89388
  return;
88585
89389
  }
88586
89390
  const loaded = [];
88587
89391
  for (const entry of entries) {
88588
- const pluginDir = join39(dir, entry);
89392
+ const pluginDir = join40(dir, entry);
88589
89393
  let stats;
88590
89394
  try {
88591
89395
  stats = await stat6(pluginDir);
@@ -88600,7 +89404,7 @@ var PluginFileWatcher = class {
88600
89404
  this.#reconcile(loaded);
88601
89405
  }
88602
89406
  async #loadPlugin(id, pluginDir) {
88603
- const manifestPath = join39(pluginDir, "plugin.json");
89407
+ const manifestPath = join40(pluginDir, "plugin.json");
88604
89408
  let manifestRaw;
88605
89409
  try {
88606
89410
  manifestRaw = await readFile26(manifestPath, "utf-8");
@@ -88627,7 +89431,7 @@ var PluginFileWatcher = class {
88627
89431
  }
88628
89432
  const manifest = parsed.data;
88629
89433
  let template = "";
88630
- const templatePath = join39(pluginDir, "template.html");
89434
+ const templatePath = join40(pluginDir, "template.html");
88631
89435
  try {
88632
89436
  template = await readFile26(templatePath, "utf-8");
88633
89437
  } catch {
@@ -88642,7 +89446,7 @@ var PluginFileWatcher = class {
88642
89446
  };
88643
89447
  }
88644
89448
  async #loadAndRegisterHandler(id, pluginDir, title) {
88645
- const handlerPath = join39(pluginDir, "handler.mjs");
89449
+ const handlerPath = join40(pluginDir, "handler.mjs");
88646
89450
  let handlerFn;
88647
89451
  try {
88648
89452
  const handlerStat = await stat6(handlerPath);
@@ -88903,28 +89707,14 @@ var HOP_BY_HOP_HEADERS2 = /* @__PURE__ */ new Set([
88903
89707
  "proxy-authorization",
88904
89708
  "proxy-connection",
88905
89709
  "te",
88906
- "host"
89710
+ "host",
89711
+ "accept-encoding"
88907
89712
  ]);
88908
89713
  var IFRAME_BLOCKING_HEADERS2 = /* @__PURE__ */ new Set([
88909
89714
  "x-frame-options",
88910
89715
  "content-security-policy",
88911
89716
  "content-security-policy-report-only"
88912
89717
  ]);
88913
- function findAvailablePort2() {
88914
- return new Promise((resolve3, reject) => {
88915
- const srv = http2.createServer();
88916
- srv.listen(0, "127.0.0.1", () => {
88917
- const addr = srv.address();
88918
- if (typeof addr === "object" && addr) {
88919
- const port = addr.port;
88920
- srv.close(() => resolve3(port));
88921
- } else {
88922
- reject(new Error("Failed to allocate port"));
88923
- }
88924
- });
88925
- srv.on("error", reject);
88926
- });
88927
- }
88928
89718
  function injectAnnotationBridge(html) {
88929
89719
  const headIdx = html.indexOf("<head");
88930
89720
  if (headIdx === -1) return ANNOTATION_BRIDGE_TAG + html;
@@ -88952,10 +89742,13 @@ function collectResponseHeaders(rawHeaders) {
88952
89742
  }
88953
89743
  return result;
88954
89744
  }
89745
+ var BLOCKED_PORTS = /* @__PURE__ */ new Set([22, 25, 3306, 5432, 6379, 27017]);
88955
89746
  function createPreviewProxy(config2) {
88956
89747
  const { log } = config2;
88957
89748
  let server = null;
88958
89749
  let allocatedPort = null;
89750
+ let currentTargetPort = null;
89751
+ let startPromise = null;
88959
89752
  const activeRequests = /* @__PURE__ */ new Set();
88960
89753
  const activeSockets = /* @__PURE__ */ new Set();
88961
89754
  function proxyRequest(clientReq, clientRes, targetPort) {
@@ -88963,7 +89756,7 @@ function createPreviewProxy(config2) {
88963
89756
  headers.host = `localhost:${targetPort}`;
88964
89757
  const proxyReq = http2.request(
88965
89758
  {
88966
- hostname: "127.0.0.1",
89759
+ hostname: "localhost",
88967
89760
  port: targetPort,
88968
89761
  path: clientReq.url ?? "/",
88969
89762
  method: clientReq.method ?? "GET",
@@ -89041,7 +89834,7 @@ function createPreviewProxy(config2) {
89041
89834
  headers.connection = "Upgrade";
89042
89835
  headers.upgrade = clientReq.headers.upgrade ?? "websocket";
89043
89836
  const proxyReq = http2.request({
89044
- hostname: "127.0.0.1",
89837
+ hostname: "localhost",
89045
89838
  port: targetPort,
89046
89839
  path: clientReq.url ?? "/",
89047
89840
  method: "GET",
@@ -89100,21 +89893,45 @@ function createPreviewProxy(config2) {
89100
89893
  get port() {
89101
89894
  return allocatedPort;
89102
89895
  },
89896
+ get targetPort() {
89897
+ return currentTargetPort;
89898
+ },
89899
+ retarget(port) {
89900
+ if (port < 1 || port > 65535 || BLOCKED_PORTS.has(port)) {
89901
+ throw new Error(`Port ${port} is not allowed for preview proxy`);
89902
+ }
89903
+ currentTargetPort = port;
89904
+ log({ event: "preview_proxy_retargeted", proxyPort: allocatedPort, targetPort: port });
89905
+ },
89103
89906
  async start(target) {
89907
+ if (startPromise) await startPromise;
89104
89908
  if (server) {
89105
89909
  throw new Error("Preview proxy already started");
89106
89910
  }
89107
- allocatedPort = await findAvailablePort2();
89911
+ if (target.port < 1 || target.port > 65535 || BLOCKED_PORTS.has(target.port)) {
89912
+ throw new Error(`Port ${target.port} is not allowed for preview proxy`);
89913
+ }
89914
+ currentTargetPort = target.port;
89108
89915
  const srv = http2.createServer((req, res) => {
89109
- proxyRequest(req, res, target.port);
89916
+ if (!currentTargetPort) {
89917
+ res.writeHead(502, { "content-type": "text/plain" });
89918
+ res.end("No preview target configured");
89919
+ return;
89920
+ }
89921
+ proxyRequest(req, res, currentTargetPort);
89110
89922
  });
89111
89923
  server = srv;
89112
89924
  srv.on("upgrade", (req, socket, head) => {
89113
- proxyWebSocketUpgrade(req, socket, head, target.port);
89925
+ if (!currentTargetPort) {
89926
+ socket.destroy();
89927
+ return;
89928
+ }
89929
+ proxyWebSocketUpgrade(req, socket, head, currentTargetPort);
89114
89930
  });
89115
- const listenPort = allocatedPort;
89116
- await new Promise((resolve3, reject) => {
89117
- srv.listen(listenPort, "127.0.0.1", () => {
89931
+ startPromise = new Promise((resolve3, reject) => {
89932
+ srv.listen(0, "localhost", () => {
89933
+ const addr = srv.address();
89934
+ allocatedPort = typeof addr === "object" && addr ? addr.port : null;
89118
89935
  log({
89119
89936
  event: "preview_proxy_started",
89120
89937
  proxyPort: allocatedPort,
@@ -89124,8 +89941,11 @@ function createPreviewProxy(config2) {
89124
89941
  });
89125
89942
  srv.on("error", reject);
89126
89943
  });
89944
+ await startPromise;
89945
+ startPromise = null;
89127
89946
  },
89128
89947
  async stop() {
89948
+ if (startPromise) await startPromise;
89129
89949
  for (const req of activeRequests) {
89130
89950
  req.destroy();
89131
89951
  }
@@ -89138,6 +89958,7 @@ function createPreviewProxy(config2) {
89138
89958
  const srv = server;
89139
89959
  server = null;
89140
89960
  allocatedPort = null;
89961
+ currentTargetPort = null;
89141
89962
  await new Promise((resolve3, reject) => {
89142
89963
  srv.close((err) => err ? reject(err) : resolve3());
89143
89964
  });
@@ -89150,7 +89971,7 @@ function createPreviewProxy(config2) {
89150
89971
  // src/services/storage/daemon-settings-store.ts
89151
89972
  import { createHash as createHash5 } from "crypto";
89152
89973
  import { mkdir as mkdir16, readFile as readFile27, rename as rename15, writeFile as writeFile23 } from "fs/promises";
89153
- import { join as join40 } from "path";
89974
+ import { join as join41 } from "path";
89154
89975
  var ProjectSettingsSchema = external_exports.object({
89155
89976
  disabledMcpServers: external_exports.array(external_exports.string()).optional()
89156
89977
  });
@@ -89158,9 +89979,9 @@ function hashProjectPath(projectPath) {
89158
89979
  return createHash5("sha256").update(projectPath).digest("hex").slice(0, 16);
89159
89980
  }
89160
89981
  function buildDaemonSettingsStore(dataDir) {
89161
- const settingsDir = join40(dataDir, "settings");
89982
+ const settingsDir = join41(dataDir, "settings");
89162
89983
  function settingsPath(projectPath) {
89163
- return join40(settingsDir, `${hashProjectPath(projectPath)}.json`);
89984
+ return join41(settingsDir, `${hashProjectPath(projectPath)}.json`);
89164
89985
  }
89165
89986
  async function ensureDir() {
89166
89987
  await mkdir16(settingsDir, { recursive: true });
@@ -89187,12 +90008,12 @@ function buildDaemonSettingsStore(dataDir) {
89187
90008
 
89188
90009
  // src/services/storage/plugin-config-store.ts
89189
90010
  import { mkdir as mkdir17, readFile as readFile28, rename as rename16, writeFile as writeFile24 } from "fs/promises";
89190
- import { join as join41 } from "path";
90011
+ import { join as join42 } from "path";
89191
90012
  function buildPluginConfigStore(pluginsDir) {
89192
90013
  const cache2 = /* @__PURE__ */ new Map();
89193
90014
  const writeQueues = /* @__PURE__ */ new Map();
89194
90015
  function configPath(pluginId) {
89195
- return join41(pluginsDir, pluginId, "config.json");
90016
+ return join42(pluginsDir, pluginId, "config.json");
89196
90017
  }
89197
90018
  async function ensureLoaded(pluginId) {
89198
90019
  const cached = cache2.get(pluginId);
@@ -89223,7 +90044,7 @@ function buildPluginConfigStore(pluginsDir) {
89223
90044
  const next = prev.then(async () => {
89224
90045
  const filePath = configPath(pluginId);
89225
90046
  const tmpPath = `${filePath}.tmp`;
89226
- await mkdir17(join41(pluginsDir, pluginId), { recursive: true });
90047
+ await mkdir17(join42(pluginsDir, pluginId), { recursive: true });
89227
90048
  await writeFile24(tmpPath, JSON.stringify(config2, null, 2), "utf-8");
89228
90049
  await rename16(tmpPath, filePath);
89229
90050
  });
@@ -89631,7 +90452,7 @@ function resolveClaudeCodePath(log) {
89631
90452
  try {
89632
90453
  const req = createRequire4(import.meta.url);
89633
90454
  const sdkMain = req.resolve("@anthropic-ai/claude-agent-sdk");
89634
- const p2 = join42(dirname14(sdkMain), "cli.js");
90455
+ const p2 = join43(dirname14(sdkMain), "cli.js");
89635
90456
  if (existsSync7(p2)) return ok("sdk_bundled", p2);
89636
90457
  } catch {
89637
90458
  }
@@ -89641,7 +90462,7 @@ function resolveClaudeCodePath(log) {
89641
90462
  } catch {
89642
90463
  }
89643
90464
  for (const c of [
89644
- join42(process.env.HOME ?? "", ".local", "bin", "claude"),
90465
+ join43(process.env.HOME ?? "", ".local", "bin", "claude"),
89645
90466
  "/usr/local/bin/claude"
89646
90467
  ])
89647
90468
  if (existsSync7(c)) return ok("well_known", c);
@@ -89649,11 +90470,11 @@ function resolveClaudeCodePath(log) {
89649
90470
  }
89650
90471
  async function serve(options = {}) {
89651
90472
  const shipyardHome = options.shipyardHome ?? getShipyardHome();
89652
- const dataDir = join42(shipyardHome, options.isDev ? "data-dev" : "data");
90473
+ const dataDir = join43(shipyardHome, options.isDev ? "data-dev" : "data");
89653
90474
  const log = createChildLogger({ mode: "serve" });
89654
90475
  const workspaceRoot = findProjectRoot(process.cwd());
89655
90476
  registerBuiltinPlugins();
89656
- const pluginConfigStore = buildPluginConfigStore(join42(dataDir, "plugins"));
90477
+ const pluginConfigStore = buildPluginConfigStore(join43(dataDir, "plugins"));
89657
90478
  await mkdir18(dataDir, { recursive: true });
89658
90479
  log.info({ shipyardHome, dataDir, workspaceRoot }, "Starting daemon");
89659
90480
  function logAdapter(entry) {
@@ -89671,13 +90492,14 @@ async function serve(options = {}) {
89671
90492
  await lifecycle.acquirePidFile(shipyardHome);
89672
90493
  const pidTracker = buildPidTracker(shipyardHome);
89673
90494
  await pidTracker.sweepOrphans(logAdapter);
89674
- await pruneOldEpochData(join42(dataDir, "loro"), LEGACY_EPOCH, logAdapter);
89675
- const storage = new FileStorageAdapter(join42(dataDir, "loro"));
89676
- const webrtcAdapter = new WebRtcDataChannelAdapter();
90495
+ await pruneOldEpochData(join43(dataDir, "loro"), LEGACY_EPOCH, logAdapter);
90496
+ const storage = new FileStorageAdapter(join43(dataDir, "loro"));
90497
+ const personalWebrtcAdapter = new WebRtcDataChannelAdapter();
89677
90498
  const peerRoleRegistry = createPeerRoleRegistry();
90499
+ const presencePoolRef = { pool: {} };
89678
90500
  const repo = new Repo({
89679
90501
  identity: DAEMON_IDENTITY,
89680
- adapters: [storage, webrtcAdapter],
90502
+ adapters: [storage, personalWebrtcAdapter],
89681
90503
  permissions: buildCollabPermissions(peerRoleRegistry)
89682
90504
  });
89683
90505
  const resolvedSignalingUrl = env.SHIPYARD_SIGNALING_URL ?? auth3.signalingUrl;
@@ -89726,7 +90548,7 @@ async function serve(options = {}) {
89726
90548
  previewProxy
89727
90549
  });
89728
90550
  daemon.healthMetrics.start();
89729
- const pluginsDir = join42(shipyardHome, "plugins");
90551
+ const pluginsDir = join43(shipyardHome, "plugins");
89730
90552
  await mkdir18(pluginsDir, { recursive: true });
89731
90553
  let loadedPlugins = [];
89732
90554
  const pluginFileWatcher = new PluginFileWatcher({
@@ -89806,8 +90628,11 @@ async function serve(options = {}) {
89806
90628
  const fileIOHandlers = /* @__PURE__ */ new Set();
89807
90629
  const collabRoomManager = signalingHandle ? createCollabRoomManager({
89808
90630
  peerRoleRegistry,
89809
- createPeerManagerForRoom: (roomId, connection, collabTaskId) => createPeerManager({
89810
- webrtcAdapter,
90631
+ personalRepo: repo,
90632
+ planEpoch: PLAN_EPOCH,
90633
+ canvasEpoch: CANVAS_EPOCH,
90634
+ createPeerManagerForRoom: (roomId, connection, collabTaskId, collabWebrtcAdapter) => createPeerManager({
90635
+ webrtcAdapter: collabWebrtcAdapter,
89811
90636
  log: createChildLogger({ mode: `collab-peer:${roomId}` }),
89812
90637
  onAnswer(namespacedId, answer) {
89813
90638
  const prefix = `collab:${roomId}:`;
@@ -89847,7 +90672,9 @@ async function serve(options = {}) {
89847
90672
  peerRoleRegistry,
89848
90673
  peerMachineId: machineId,
89849
90674
  collabTaskId,
89850
- configStore: pluginConfigStore
90675
+ configStore: pluginConfigStore,
90676
+ previewProxy,
90677
+ presencePool: presencePoolRef
89851
90678
  });
89852
90679
  if (loadedPlugins.length > 0) {
89853
90680
  daemon.taskManager.broadcastControl({
@@ -90086,7 +90913,7 @@ async function serve(options = {}) {
90086
90913
  );
90087
90914
  }
90088
90915
  const peerManager = signalingHandle ? createPeerManager({
90089
- webrtcAdapter,
90916
+ webrtcAdapter: personalWebrtcAdapter,
90090
90917
  onAnswer: (targetMachineId, answer) => {
90091
90918
  signalingHandle.connection.send({
90092
90919
  type: "webrtc-answer",
@@ -90114,7 +90941,10 @@ async function serve(options = {}) {
90114
90941
  machineId: signalingHandle.machineId,
90115
90942
  signalingBaseUrl: auth3.signalingUrl,
90116
90943
  userToken: auth3.token,
90117
- configStore: pluginConfigStore
90944
+ configStore: pluginConfigStore,
90945
+ previewProxy,
90946
+ presencePool: presencePoolRef,
90947
+ peerIdentity: { userId: auth3.userId, username: auth3.displayName, avatarUrl: null }
90118
90948
  });
90119
90949
  portDetector.resend();
90120
90950
  if (loadedPlugins.length > 0) {
@@ -90347,8 +91177,24 @@ async function serve(options = {}) {
90347
91177
  }
90348
91178
  portDetector = createPortDetector({
90349
91179
  workspaceRoot,
90350
- onPorts: (ports) => {
90351
- daemon.taskManager.broadcastControl({ type: "detected_ports", ports });
91180
+ onPorts: async (ports) => {
91181
+ const firstPort = ports[0]?.port;
91182
+ if (firstPort && !previewProxy.port) {
91183
+ try {
91184
+ await previewProxy.start({ port: firstPort });
91185
+ } catch (err) {
91186
+ logAdapter({
91187
+ event: "preview_proxy_auto_start_failed",
91188
+ port: firstPort,
91189
+ error: err instanceof Error ? err.message : String(err)
91190
+ });
91191
+ }
91192
+ }
91193
+ daemon.taskManager.broadcastControl({
91194
+ type: "detected_ports",
91195
+ ports,
91196
+ proxyPort: previewProxy.port ?? void 0
91197
+ });
90352
91198
  },
90353
91199
  log: logAdapter
90354
91200
  });
@@ -90702,4 +91548,4 @@ export {
90702
91548
  classifyLogLevel,
90703
91549
  serve
90704
91550
  };
90705
- //# sourceMappingURL=serve-LL6LUMZI.js.map
91551
+ //# sourceMappingURL=serve-XSXFIZDE.js.map