@schoolai/shipyard 3.0.0 → 3.1.0-rc.20260414.0

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) {
@@ -30802,7 +31121,7 @@ function handlePluginAuthRequest(pluginId, sendControl, deps, logAdapter) {
30802
31121
  }
30803
31122
 
30804
31123
  // src/services/pr-poller.ts
30805
- var PR_POLL_INTERVAL_MS = 6e4;
31124
+ var PR_POLL_INTERVAL_MS = 3e4;
30806
31125
  async function enrichClosedPR(pr, cwd) {
30807
31126
  if (pr.state !== "closed" || pr.mergeCommitSha) return pr;
30808
31127
  if (!pr.headRefSha) return pr;
@@ -30813,10 +31132,9 @@ async function enrichClosedPR(pr, cwd) {
30813
31132
  );
30814
31133
  return headInBase ? { ...pr, headCommitInBase: true } : pr;
30815
31134
  }
30816
- async function fetchCIData(pr, cwd) {
31135
+ async function fetchCIDataWithRemote(pr, cwd, remote) {
30817
31136
  const referenceCommit = pr?.mergeCommitSha ?? pr?.headRefSha ?? await run("git", ["rev-parse", "HEAD"], cwd).catch(() => "");
30818
31137
  if (!referenceCommit) return { deployments: [], workflowRuns: [], requiredChecks: [] };
30819
- const remote = await run("git", ["remote", "get-url", "origin"], cwd).catch(() => "");
30820
31138
  const parsed = parseOwnerRepo(remote);
30821
31139
  if (!parsed) return { deployments: [], workflowRuns: [], requiredChecks: [] };
30822
31140
  const branch = pr?.baseRef ?? "main";
@@ -30843,13 +31161,14 @@ async function fetchCIData(pr, cwd) {
30843
31161
  const allDeployments = [...deployments, ...placeholderDeployments];
30844
31162
  return { deployments: allDeployments, workflowRuns, requiredChecks };
30845
31163
  }
30846
- function createPRPoller(callbacks, log) {
30847
- const polls = /* @__PURE__ */ new Map();
30848
- async function fetchPRState(taskId, cwd) {
31164
+ function createPRPoller(callbacks, log, resolveTopLevel = getGitTopLevel) {
31165
+ const repos = /* @__PURE__ */ new Map();
31166
+ const taskToRepo = /* @__PURE__ */ new Map();
31167
+ let disposed = false;
31168
+ async function fetchPRState(cwd) {
30849
31169
  const available = await isGhAvailable();
30850
31170
  if (!available) {
30851
31171
  return {
30852
- taskId,
30853
31172
  prAvailable: false,
30854
31173
  currentBranchPR: null,
30855
31174
  assignedReviews: [],
@@ -30860,14 +31179,14 @@ function createPRPoller(callbacks, log) {
30860
31179
  updatedAt: Date.now()
30861
31180
  };
30862
31181
  }
30863
- const [currentPR, assigned, user] = await Promise.allSettled([
30864
- getPRForCurrentBranch(cwd),
30865
- getAssignedReviews(cwd),
30866
- getUserPRs(cwd)
31182
+ const [prResults, remote] = await Promise.all([
31183
+ Promise.allSettled([getPRForCurrentBranch(cwd), getAssignedReviews(cwd), getUserPRs(cwd)]),
31184
+ run("git", ["remote", "get-url", "origin"], cwd).catch(() => "")
30867
31185
  ]);
31186
+ const [currentPR, assigned, user] = prResults;
30868
31187
  const { allPRs: _2, currentBranchPR: rawPR } = deduplicatePRs(currentPR, assigned, user);
30869
31188
  const currentBranchPR = rawPR ? await enrichClosedPR(rawPR, cwd) : null;
30870
- const ciData = await fetchCIData(currentBranchPR, cwd).catch(
31189
+ const ciData = await fetchCIDataWithRemote(currentBranchPR, cwd, remote).catch(
30871
31190
  () => ({
30872
31191
  deployments: [],
30873
31192
  workflowRuns: [],
@@ -30875,7 +31194,6 @@ function createPRPoller(callbacks, log) {
30875
31194
  })
30876
31195
  );
30877
31196
  return {
30878
- taskId,
30879
31197
  prAvailable: true,
30880
31198
  currentBranchPR,
30881
31199
  assignedReviews: assigned.status === "fulfilled" ? assigned.value : [],
@@ -30886,81 +31204,135 @@ function createPRPoller(callbacks, log) {
30886
31204
  updatedAt: Date.now()
30887
31205
  };
30888
31206
  }
30889
- async function poll(taskId, entry) {
31207
+ function pushToSubscribers(entry, payload) {
31208
+ for (const taskId of entry.subscribers) {
31209
+ callbacks.onPRState({ ...payload, taskId });
31210
+ }
31211
+ }
31212
+ async function poll(topLevel, entry) {
31213
+ if (entry.isPolling) return;
31214
+ entry.isPolling = true;
30890
31215
  try {
30891
- const payload = await fetchPRState(taskId, entry.cwd);
31216
+ const currentBranch = await run("git", ["rev-parse", "--abbrev-ref", "HEAD"], entry.cwd).then((s2) => s2.trim()).catch(() => "");
31217
+ if (entry.lastBranch && currentBranch && currentBranch !== entry.lastBranch) {
31218
+ entry.lastContent = "";
31219
+ log.debug(
31220
+ { topLevel, oldBranch: entry.lastBranch, newBranch: currentBranch },
31221
+ "Branch changed, clearing dedup cache"
31222
+ );
31223
+ }
31224
+ if (currentBranch) entry.lastBranch = currentBranch;
31225
+ const payload = await fetchPRState(entry.cwd);
30892
31226
  if (!payload) return;
30893
31227
  const { updatedAt: _2, ...comparable } = payload;
30894
31228
  const contentKey = JSON.stringify(comparable);
30895
31229
  if (entry.lastContent === contentKey) {
30896
- log.debug({ taskId }, "PR state unchanged, skipping push");
31230
+ log.debug({ topLevel }, "PR state unchanged, skipping push");
30897
31231
  return;
30898
31232
  }
30899
31233
  entry.lastContent = contentKey;
30900
- callbacks.onPRState(payload);
30901
- log.debug({ taskId, prAvailable: payload.prAvailable }, "PR state pushed");
31234
+ entry.lastPayload = payload;
31235
+ pushToSubscribers(entry, payload);
31236
+ log.debug(
31237
+ { topLevel, prAvailable: payload.prAvailable, subscribers: entry.subscribers.size },
31238
+ "PR state pushed"
31239
+ );
30902
31240
  } catch (err) {
30903
- log.warn({ err, taskId }, "PR poll failed");
31241
+ log.warn({ err, topLevel }, "PR poll failed");
31242
+ } finally {
31243
+ entry.isPolling = false;
30904
31244
  }
30905
31245
  }
31246
+ function removeFromPreviousRepo(taskId) {
31247
+ const prev = taskToRepo.get(taskId);
31248
+ if (!prev) return;
31249
+ const prevEntry = repos.get(prev.topLevel);
31250
+ if (prevEntry) {
31251
+ prevEntry.subscribers.delete(taskId);
31252
+ if (prevEntry.subscribers.size === 0) {
31253
+ clearInterval(prevEntry.timer);
31254
+ for (const t of prevEntry.burstTimers) clearTimeout(t);
31255
+ repos.delete(prev.topLevel);
31256
+ log.info({ topLevel: prev.topLevel }, "PR polling stopped (no subscribers)");
31257
+ }
31258
+ }
31259
+ taskToRepo.delete(taskId);
31260
+ }
30906
31261
  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");
31262
+ resolveTopLevel(cwd).then((topLevel) => {
31263
+ if (disposed) return;
31264
+ removeFromPreviousRepo(taskId);
31265
+ taskToRepo.set(taskId, { topLevel, cwd });
31266
+ const existing = repos.get(topLevel);
31267
+ if (existing) {
31268
+ existing.subscribers.add(taskId);
31269
+ if (existing.lastPayload) {
31270
+ callbacks.onPRState({ ...existing.lastPayload, taskId });
31271
+ }
31272
+ log.info({ taskId, topLevel, cwd }, "PR polling joined existing repo");
31273
+ return;
31274
+ }
31275
+ const entry = {
31276
+ timer: null,
31277
+ burstTimers: [],
31278
+ lastContent: "",
31279
+ lastBranch: "",
31280
+ lastPayload: null,
31281
+ cwd,
31282
+ subscribers: /* @__PURE__ */ new Set([taskId]),
31283
+ isPolling: false
31284
+ };
31285
+ poll(topLevel, entry).catch((err) => {
31286
+ log.warn({ err, topLevel }, "Initial PR poll failed");
30920
31287
  });
30921
- }, PR_POLL_INTERVAL_MS);
30922
- polls.set(taskId, entry);
30923
- log.info({ taskId, cwd }, "PR polling started");
31288
+ entry.timer = setInterval(() => {
31289
+ poll(topLevel, entry).catch((err) => {
31290
+ log.warn({ err, topLevel }, "PR poll tick failed");
31291
+ });
31292
+ }, PR_POLL_INTERVAL_MS);
31293
+ repos.set(topLevel, entry);
31294
+ log.info({ taskId, topLevel, cwd }, "PR polling started");
31295
+ }).catch((err) => {
31296
+ log.warn({ err, taskId, cwd }, "Failed to resolve git toplevel for PR polling");
31297
+ });
30924
31298
  }
30925
31299
  function getCwd(taskId) {
30926
- return polls.get(taskId)?.cwd ?? null;
31300
+ return taskToRepo.get(taskId)?.cwd ?? null;
30927
31301
  }
30928
31302
  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
- }
31303
+ removeFromPreviousRepo(taskId);
30936
31304
  }
30937
31305
  function forceRefresh(taskId) {
30938
- const entry = polls.get(taskId);
31306
+ const info = taskToRepo.get(taskId);
31307
+ if (!info) return;
31308
+ const entry = repos.get(info.topLevel);
30939
31309
  if (!entry) return;
30940
31310
  for (const t of entry.burstTimers) clearTimeout(t);
30941
31311
  entry.burstTimers = [];
30942
31312
  entry.lastContent = "";
30943
- poll(taskId, entry).catch((err) => {
30944
- log.warn({ err, taskId }, "PR force refresh failed");
31313
+ poll(info.topLevel, entry).catch((err) => {
31314
+ log.warn({ err, topLevel: info.topLevel }, "PR force refresh failed");
30945
31315
  });
30946
31316
  for (const delayMs of [3e3, 8e3, 15e3]) {
30947
31317
  entry.burstTimers.push(
30948
31318
  setTimeout(() => {
30949
31319
  entry.lastContent = "";
30950
- poll(taskId, entry).catch((err) => {
30951
- log.warn({ err, taskId }, "PR burst refresh failed");
31320
+ poll(info.topLevel, entry).catch((err) => {
31321
+ log.warn({ err, topLevel: info.topLevel }, "PR burst refresh failed");
30952
31322
  });
30953
31323
  }, delayMs)
30954
31324
  );
30955
31325
  }
30956
31326
  }
30957
31327
  function dispose() {
30958
- for (const [taskId, entry] of polls) {
31328
+ disposed = true;
31329
+ for (const [topLevel, entry] of repos) {
30959
31330
  clearInterval(entry.timer);
30960
31331
  for (const t of entry.burstTimers) clearTimeout(t);
30961
- log.debug({ taskId }, "PR poll timer cleared on dispose");
31332
+ log.debug({ topLevel }, "PR poll timer cleared on dispose");
30962
31333
  }
30963
- polls.clear();
31334
+ repos.clear();
31335
+ taskToRepo.clear();
30964
31336
  }
30965
31337
  return { startPolling, stopPolling, forceRefresh, getCwd, dispose };
30966
31338
  }
@@ -31792,10 +32164,11 @@ async function rehydrateFromPersistence(persistence, taskManager, log, taskState
31792
32164
  async function sweepStaleTasks(taskStateStore, taskManager, log) {
31793
32165
  const tasks = await taskStateStore.listTasks();
31794
32166
  const actions = planStaleSweep(tasks, (id) => taskManager.isRunning(id));
32167
+ const noBroadcast = { broadcast: false };
31795
32168
  for (const action of actions) {
31796
32169
  if (action.kind === "mark_input_required") {
31797
- await taskStateStore.updateTaskStatus(action.taskId, "input_required");
31798
- await taskStateStore.acknowledge(action.taskId);
32170
+ await taskStateStore.updateTaskStatus(action.taskId, "input_required", noBroadcast);
32171
+ await taskStateStore.acknowledge(action.taskId, noBroadcast);
31799
32172
  log({
31800
32173
  event: "stale_task_swept",
31801
32174
  taskId: action.taskId,
@@ -33797,7 +34170,7 @@ var TOOL_DESCRIPTION2 = [
33797
34170
  "The preview panel opens in the Shipyard UI next to the conversation. The user can interact with it directly."
33798
34171
  ].join("\n");
33799
34172
  var SetPreviewTargetInput = {
33800
- port: external_exports.number().int().positive().optional().describe("Local port number of the running dev server (e.g. 3000, 5173, 8080)."),
34173
+ 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
34174
  url: external_exports.string().url().optional().describe('Fully qualified URL to preview (e.g. "http://localhost:3000/dashboard").')
33802
34175
  };
33803
34176
  function createPreviewTools(ctx) {
@@ -33818,9 +34191,14 @@ function createPreviewTools(ctx) {
33818
34191
  isError: true
33819
34192
  };
33820
34193
  }
33821
- await ctx.previewProxy.stop();
33822
- if (input.port != null && input.url == null) {
33823
- await ctx.previewProxy.start({ port: input.port });
34194
+ if (input.url != null) {
34195
+ await ctx.previewProxy.stop();
34196
+ } else if (input.port != null) {
34197
+ if (ctx.previewProxy.port) {
34198
+ ctx.previewProxy.retarget(input.port);
34199
+ } else {
34200
+ await ctx.previewProxy.start({ port: input.port });
34201
+ }
33824
34202
  }
33825
34203
  ctx.broadcastControl({
33826
34204
  type: "set_preview_target",
@@ -42788,6 +43166,16 @@ When you call ExitPlanMode, your plan is published to the Shipyard canvas for co
42788
43166
  **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
43167
 
42790
43168
  Do not revise the plan unless reviewers request changes. When you see \`<${XML_TAGS.PLAN_REVIEW} decision="approve">\`, call ExitPlanMode again to begin implementation.
43169
+
43170
+ ## Background Execution
43171
+
43172
+ Prefer background execution to keep your main thread responsive to collaborators.
43173
+
43174
+ **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.
43175
+
43176
+ **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.
43177
+
43178
+ **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
43179
  `.trim();
42792
43180
  function buildCollabSystemPrompt(participants) {
42793
43181
  const roster = participants.map((p2) => `${p2.name} (${p2.role})`).join(", ");
@@ -66428,11 +66816,13 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
66428
66816
  * Guards all SDK write operations to prevent "ProcessTransport not ready" errors.
66429
66817
  */
66430
66818
  #closed = false;
66431
- constructor(query3, controller, onEvent, spawnMcpServers) {
66819
+ #pidRef;
66820
+ constructor(query3, controller, onEvent, spawnMcpServers, pidRef) {
66432
66821
  this.#query = query3;
66433
66822
  this.#controller = controller;
66434
66823
  this.#onEvent = onEvent;
66435
66824
  this.#spawnMcpServers = spawnMcpServers;
66825
+ this.#pidRef = pidRef;
66436
66826
  }
66437
66827
  /** True once the subprocess has exited and SDK writes will throw. */
66438
66828
  get isClosed() {
@@ -66452,27 +66842,8 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
66452
66842
  const controller = new InputController();
66453
66843
  const { onChildSpawned, onChildExited } = options;
66454
66844
  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;
66845
+ const pidRef = { current: null };
66846
+ const spawnClaudeCodeProcess = onChildSpawned ? createChildSpawner(stderrCallback, pidRef, onChildSpawned, onChildExited) : void 0;
66476
66847
  const queryInstance = queryFn({
66477
66848
  prompt: controller.iterable(),
66478
66849
  options: {
@@ -66503,7 +66874,8 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
66503
66874
  queryInstance,
66504
66875
  controller,
66505
66876
  onEvent,
66506
- spawnMcpServers
66877
+ spawnMcpServers,
66878
+ pidRef
66507
66879
  );
66508
66880
  if (initialContent.length > 0) {
66509
66881
  controller.push(toSdkContent(initialContent));
@@ -66528,6 +66900,13 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
66528
66900
  this.#query.close();
66529
66901
  }
66530
66902
  forceKill() {
66903
+ if (this.#pidRef.current != null) {
66904
+ try {
66905
+ process.kill(this.#pidRef.current, "SIGKILL");
66906
+ } catch {
66907
+ }
66908
+ this.#pidRef.current = null;
66909
+ }
66531
66910
  this.#closed = true;
66532
66911
  this.#controller.end();
66533
66912
  this.#query.close();
@@ -66584,6 +66963,14 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
66584
66963
  await this.#query.reconnectMcpServer(serverName);
66585
66964
  }
66586
66965
  }
66966
+ async stopBackgroundTask(taskId) {
66967
+ if (this.#closed) return;
66968
+ try {
66969
+ await this.#query.stopTask(taskId);
66970
+ } catch (err) {
66971
+ if (!this.#closed) throw err;
66972
+ }
66973
+ }
66587
66974
  async #runMessageLoop(log) {
66588
66975
  try {
66589
66976
  for await (const message of this.#query) {
@@ -66592,9 +66979,11 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
66592
66979
  this.#onEvent(event);
66593
66980
  }
66594
66981
  }
66982
+ this.#pidRef.current = null;
66595
66983
  this.#closed = true;
66596
66984
  this.#onEvent({ type: "subprocess_died" });
66597
66985
  } catch (error2) {
66986
+ this.#pidRef.current = null;
66598
66987
  this.#closed = true;
66599
66988
  const errorMsg = error2 instanceof Error ? error2.message : String(error2);
66600
66989
  this.#onEvent({
@@ -66605,6 +66994,30 @@ var AnthropicAgentSubprocess = class _AnthropicAgentSubprocess {
66605
66994
  }
66606
66995
  }
66607
66996
  };
66997
+ function createChildSpawner(stderrCallback, pidRef, onChildSpawned, onChildExited) {
66998
+ return (spawnOpts) => {
66999
+ const child = spawn4(spawnOpts.command, spawnOpts.args, {
67000
+ cwd: spawnOpts.cwd,
67001
+ stdio: ["pipe", "pipe", stderrCallback ? "pipe" : "ignore"],
67002
+ signal: spawnOpts.signal,
67003
+ env: { ...spawnOpts.env, CLAUDECODE: void 0 },
67004
+ windowsHide: true
67005
+ });
67006
+ if (!child.stdin || !child.stdout) {
67007
+ throw new Error("Failed to create stdio pipes for Claude subprocess");
67008
+ }
67009
+ if (stderrCallback && child.stderr) {
67010
+ child.stderr.on("data", (chunk) => stderrCallback(chunk.toString()));
67011
+ }
67012
+ if (child.pid) {
67013
+ const pid = child.pid;
67014
+ pidRef.current = pid;
67015
+ onChildSpawned(pid);
67016
+ child.on("exit", () => onChildExited?.(pid));
67017
+ }
67018
+ return Object.assign(child, { stdin: child.stdin, stdout: child.stdout });
67019
+ };
67020
+ }
66608
67021
  function toSdkPermissionMode(mode) {
66609
67022
  switch (mode) {
66610
67023
  case "default":
@@ -72958,6 +73371,9 @@ var DirectApiSubprocess = class _DirectApiSubprocess {
72958
73371
  /** No-op: no MCP in direct API mode. */
72959
73372
  async reconnectMcpServer(_serverName) {
72960
73373
  }
73374
+ /** No-op: direct API mode does not support background tasks. */
73375
+ async stopBackgroundTask(_taskId) {
73376
+ }
72961
73377
  /** No-op in Phase 1 -- harness task context not used by direct API mode. */
72962
73378
  setHarnessTaskId(_taskId) {
72963
73379
  }
@@ -75062,12 +75478,20 @@ function noop3(snapshot) {
75062
75478
  };
75063
75479
  }
75064
75480
  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 };
75481
+ if (event.type === "user_message") {
75482
+ const b2 = createEffectBuilder();
75483
+ b2.log("cold_idle", "spawning", event.type);
75484
+ b2.taskStatus("in_progress");
75485
+ b2.effects.push({ type: "spawn", reason: { kind: "fresh" } });
75486
+ return { state: "spawning", sessionId: null, rewindAtMessageId: null, effects: b2.effects };
75487
+ }
75488
+ if (event.type === "stop_commanded") {
75489
+ const b2 = createEffectBuilder();
75490
+ b2.effects.push({ type: "clear_queue" });
75491
+ b2.taskStatus("input_required");
75492
+ return { state: "cold_idle", sessionId: null, rewindAtMessageId: null, effects: b2.effects };
75493
+ }
75494
+ return noop3(snapshot);
75071
75495
  }
75072
75496
  function handleResumableIdle(snapshot, event) {
75073
75497
  const { sessionId, rewindAtMessageId } = snapshot;
@@ -75086,6 +75510,12 @@ function handleResumableIdle(snapshot, event) {
75086
75510
  b2.effects.push({ type: "spawn", reason });
75087
75511
  return { state: "spawning", sessionId, rewindAtMessageId: null, effects: b2.effects };
75088
75512
  }
75513
+ if (event.type === "stop_commanded") {
75514
+ const b2 = createEffectBuilder();
75515
+ b2.effects.push({ type: "clear_queue" });
75516
+ b2.taskStatus("input_required");
75517
+ return { state: "resumable_idle", sessionId, rewindAtMessageId, effects: b2.effects };
75518
+ }
75089
75519
  if (event.type === "session_expired") {
75090
75520
  const b2 = createEffectBuilder();
75091
75521
  b2.log("resumable_idle", "cold_idle", event.type);
@@ -75245,12 +75675,7 @@ function exitStoppingAlive(snapshot, trigger) {
75245
75675
  const { sessionId, rewindAtMessageId } = snapshot;
75246
75676
  const b2 = createEffectBuilder();
75247
75677
  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
- }
75678
+ b2.effects.push({ type: "clear_queue" });
75254
75679
  b2.log("stopping", "warm_idle", trigger);
75255
75680
  b2.taskStatus("input_required");
75256
75681
  return { state: "warm_idle", sessionId, rewindAtMessageId, effects: b2.effects };
@@ -75278,7 +75703,7 @@ function handleStopping(snapshot, event) {
75278
75703
  b2.taskStatus("input_required");
75279
75704
  return { state: "resumable_idle", sessionId, rewindAtMessageId, effects: b2.effects };
75280
75705
  }
75281
- if (event.type === "stop_timeout") {
75706
+ if (event.type === "stop_timeout" || event.type === "interrupt_failed") {
75282
75707
  b2.log("stopping", "resumable_idle", event.type);
75283
75708
  b2.effects.push({ type: "force_kill" });
75284
75709
  b2.effects.push({ type: "clear_pending_inputs" });
@@ -75367,6 +75792,9 @@ var AgentSessionManager = class {
75367
75792
  notifyInterruptAcknowledged() {
75368
75793
  this.#dispatch({ type: "interrupt_acknowledged" });
75369
75794
  }
75795
+ notifyInterruptFailed() {
75796
+ this.#dispatch({ type: "interrupt_failed" });
75797
+ }
75370
75798
  notifyCloseAcknowledged() {
75371
75799
  this.#dispatch({ type: "close_acknowledged" });
75372
75800
  }
@@ -75868,6 +76296,10 @@ var Thread = class {
75868
76296
  if (!this.#subprocess) return;
75869
76297
  return this.#subprocess.mcpSubmitOAuthCallbackUrl(serverName, callbackUrl);
75870
76298
  }
76299
+ async stopBackgroundTask(taskId) {
76300
+ if (!this.#subprocess) return;
76301
+ await this.#subprocess.stopBackgroundTask(taskId);
76302
+ }
75871
76303
  async dispose() {
75872
76304
  this.#disposed = true;
75873
76305
  this.#permissionHandler.denyAllPending("Thread disposed");
@@ -76158,13 +76590,18 @@ ${conversationReplay}` : conversationReplay;
76158
76590
  this.#subprocess?.pushMessage(content);
76159
76591
  }
76160
76592
  #handleInterrupt() {
76161
- this.#subprocess?.interrupt().catch((err) => {
76593
+ if (!this.#subprocess || this.#subprocess.isClosed) {
76594
+ this.#manager.notifySubprocessDied();
76595
+ return;
76596
+ }
76597
+ this.#subprocess.interrupt().catch((err) => {
76162
76598
  const msg = err instanceof Error ? err.message : String(err);
76163
76599
  this.#config.log({
76164
76600
  event: "thread_interrupt_failed",
76165
76601
  threadId: this.#config.threadId,
76166
76602
  error: msg
76167
76603
  });
76604
+ this.#manager.notifyInterruptFailed();
76168
76605
  });
76169
76606
  }
76170
76607
  #handleClose() {
@@ -81061,6 +81498,9 @@ var Task = class _Task {
81061
81498
  this.#explicitlyStopped = true;
81062
81499
  this.#mainThread.stop();
81063
81500
  }
81501
+ async stopBackgroundTask(backgroundTaskId) {
81502
+ await this.#mainThread.stopBackgroundTask(backgroundTaskId);
81503
+ }
81064
81504
  cancelQueued() {
81065
81505
  const count = this.#mainThread.cancelQueued();
81066
81506
  const collabCount = this.#collabQueue.cancelQueued();
@@ -82081,6 +82521,122 @@ Use this context to maintain continuity. You have already done this work \u2014
82081
82521
  }
82082
82522
  };
82083
82523
 
82524
+ // src/services/task/task-manager-mcp.ts
82525
+ function hasLiveSubprocess(state) {
82526
+ return state === "warm_idle" || state === "running" || state === "spawning";
82527
+ }
82528
+ function updateMcpServers(tasks, deps) {
82529
+ const resolved = deps.resolveMcpServers() ?? {};
82530
+ for (const task of tasks.values()) {
82531
+ if (!hasLiveSubprocess(task.orchestrator.state)) continue;
82532
+ task.orchestrator.setMcpServers(resolved).then((result) => {
82533
+ deps.log({
82534
+ event: "mcp_servers_updated",
82535
+ taskId: task.taskId,
82536
+ added: result?.added ?? [],
82537
+ removed: result?.removed ?? [],
82538
+ errors: result?.errors ?? {}
82539
+ });
82540
+ }).catch((err) => {
82541
+ deps.log({
82542
+ event: "mcp_servers_update_failed",
82543
+ taskId: task.taskId,
82544
+ error: err instanceof Error ? err.message : String(err)
82545
+ });
82546
+ });
82547
+ }
82548
+ }
82549
+ function triggerFastMcpPolling(tasks) {
82550
+ for (const task of tasks.values()) {
82551
+ if (!hasLiveSubprocess(task.orchestrator.state)) continue;
82552
+ task.orchestrator.triggerFastMcpPolling();
82553
+ }
82554
+ }
82555
+ function toggleMcpServer(tasks, serverName, enabled, log) {
82556
+ for (const task of tasks.values()) {
82557
+ if (!hasLiveSubprocess(task.orchestrator.state)) continue;
82558
+ task.orchestrator.toggleMcpServer(serverName, enabled).then(() => {
82559
+ log({
82560
+ event: "mcp_server_toggled",
82561
+ taskId: task.taskId,
82562
+ serverName,
82563
+ enabled
82564
+ });
82565
+ }).catch((err) => {
82566
+ log({
82567
+ event: "mcp_server_toggle_failed",
82568
+ taskId: task.taskId,
82569
+ serverName,
82570
+ enabled,
82571
+ error: err instanceof Error ? err.message : String(err)
82572
+ });
82573
+ });
82574
+ }
82575
+ }
82576
+ function reconnectMcpServer(tasks, serverName, deps) {
82577
+ const fullSet = deps.resolveMcpServers() ?? {};
82578
+ for (const task of tasks.values()) {
82579
+ if (!hasLiveSubprocess(task.orchestrator.state)) continue;
82580
+ const withoutTarget = { ...fullSet };
82581
+ delete withoutTarget[serverName];
82582
+ task.orchestrator.setMcpServers(withoutTarget).then(() => task.orchestrator.setMcpServers(fullSet)).then((result) => {
82583
+ deps.log({
82584
+ event: "mcp_server_reconnected",
82585
+ taskId: task.taskId,
82586
+ serverName,
82587
+ added: result?.added ?? [],
82588
+ removed: result?.removed ?? []
82589
+ });
82590
+ }).catch((err) => {
82591
+ deps.log({
82592
+ event: "mcp_server_reconnect_failed",
82593
+ taskId: task.taskId,
82594
+ serverName,
82595
+ error: err instanceof Error ? err.message : String(err)
82596
+ });
82597
+ });
82598
+ }
82599
+ }
82600
+ async function mcpAuthenticate(tasks, serverName) {
82601
+ for (const task of tasks.values()) {
82602
+ if (!hasLiveSubprocess(task.orchestrator.state)) continue;
82603
+ return task.orchestrator.mcpAuthenticate(serverName);
82604
+ }
82605
+ return null;
82606
+ }
82607
+ async function mcpSubmitOAuthCallbackUrl(tasks, serverName, callbackUrl) {
82608
+ for (const task of tasks.values()) {
82609
+ if (!hasLiveSubprocess(task.orchestrator.state)) continue;
82610
+ await task.orchestrator.mcpSubmitOAuthCallbackUrl(serverName, callbackUrl);
82611
+ return;
82612
+ }
82613
+ }
82614
+ function planGracefulShutdown(tasks, gracePeriodMs) {
82615
+ const immediate = [];
82616
+ const deferred = [];
82617
+ for (const task of tasks) {
82618
+ switch (task.state) {
82619
+ case "cold_idle":
82620
+ case "resumable_idle":
82621
+ case "warm_idle":
82622
+ immediate.push({ taskId: task.taskId, kind: "dispose_immediately" });
82623
+ break;
82624
+ case "spawning":
82625
+ case "running":
82626
+ deferred.push({ taskId: task.taskId, kind: "stop_then_wait" });
82627
+ break;
82628
+ case "stopping":
82629
+ deferred.push({ taskId: task.taskId, kind: "stop_then_wait" });
82630
+ break;
82631
+ default: {
82632
+ const _exhaustive = task.state;
82633
+ throw new Error(`Unhandled state: ${JSON.stringify(_exhaustive)}`);
82634
+ }
82635
+ }
82636
+ }
82637
+ return { immediate, deferred, gracePeriodMs };
82638
+ }
82639
+
82084
82640
  // src/services/task/task-manager.ts
82085
82641
  function cloneOverlay(overlay) {
82086
82642
  return {
@@ -82605,111 +83161,32 @@ var TaskManager = class {
82605
83161
  this.#deps.log({ event: "task_stop_requested", taskId });
82606
83162
  this.#tasks.get(taskId)?.orchestrator.stop();
82607
83163
  }
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
- });
83164
+ async stopBackgroundTask(taskId, backgroundTaskId) {
83165
+ this.#deps.log({ event: "background_task_stop_requested", taskId, backgroundTaskId });
83166
+ const task = this.#tasks.get(taskId);
83167
+ if (!task) {
83168
+ this.#deps.log({ event: "background_task_stop_unknown_task", taskId, backgroundTaskId });
83169
+ return;
82632
83170
  }
83171
+ await task.orchestrator.stopBackgroundTask(backgroundTaskId);
83172
+ }
83173
+ updateMcpServers() {
83174
+ updateMcpServers(this.#tasks, this.#deps);
82633
83175
  }
82634
83176
  triggerFastMcpPolling() {
82635
- for (const task of this.#tasks.values()) {
82636
- if (!this.#hasLiveSubprocess(task.orchestrator.state)) continue;
82637
- task.orchestrator.triggerFastMcpPolling();
82638
- }
83177
+ triggerFastMcpPolling(this.#tasks);
82639
83178
  }
82640
83179
  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
- }
83180
+ toggleMcpServer(this.#tasks, serverName, enabled, this.#deps.log);
82660
83181
  }
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
83182
  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
- }
83183
+ reconnectMcpServer(this.#tasks, serverName, this.#deps);
82691
83184
  }
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
83185
  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;
83186
+ return mcpAuthenticate(this.#tasks, serverName);
82702
83187
  }
82703
83188
  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";
83189
+ return mcpSubmitOAuthCallbackUrl(this.#tasks, serverName, callbackUrl);
82713
83190
  }
82714
83191
  cancelQueued(taskId) {
82715
83192
  const task = this.#tasks.get(taskId);
@@ -82886,7 +83363,7 @@ var TaskManager = class {
82886
83363
  /** Whether any task has a live subprocess that can accept MCP commands. */
82887
83364
  get hasLiveSubprocesses() {
82888
83365
  for (const task of this.#tasks.values()) {
82889
- if (this.#hasLiveSubprocess(task.orchestrator.state)) return true;
83366
+ if (hasLiveSubprocess(task.orchestrator.state)) return true;
82890
83367
  }
82891
83368
  return false;
82892
83369
  }
@@ -83016,31 +83493,6 @@ var TaskManager = class {
83016
83493
  });
83017
83494
  }
83018
83495
  };
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
83496
 
83045
83497
  // src/services/task/task-state-store.ts
83046
83498
  import { join as join35 } from "path";
@@ -83072,12 +83524,43 @@ function buildTaskStateStore(dataDir) {
83072
83524
  const unsubVersion = store.subscribe(() => {
83073
83525
  _version++;
83074
83526
  });
83527
+ const _broadcastQueue = [];
83528
+ const taskListeners = /* @__PURE__ */ new Set();
83529
+ const unsubBroadcast = store.subscribe((event) => {
83530
+ const broadcast = _broadcastQueue.shift() ?? true;
83531
+ const augmented = { ...event, broadcast };
83532
+ for (const listener of taskListeners) {
83533
+ try {
83534
+ listener(augmented);
83535
+ } catch {
83536
+ }
83537
+ }
83538
+ });
83539
+ function pushBroadcast(options) {
83540
+ _broadcastQueue.push(options?.broadcast ?? true);
83541
+ }
83542
+ async function safeUpdate(taskId, fn, options) {
83543
+ pushBroadcast(options);
83544
+ const lenBefore = _broadcastQueue.length;
83545
+ try {
83546
+ await store.update(taskId, fn);
83547
+ } catch (err) {
83548
+ if (_broadcastQueue.length === lenBefore) {
83549
+ _broadcastQueue.pop();
83550
+ }
83551
+ throw err;
83552
+ }
83553
+ if (_broadcastQueue.length === lenBefore) {
83554
+ _broadcastQueue.pop();
83555
+ }
83556
+ }
83075
83557
  return {
83076
83558
  get version() {
83077
83559
  return _version;
83078
83560
  },
83079
- async createTask({ taskId, channelId, title, cwd, mode, scheduleId, scheduleName }) {
83561
+ async createTask({ taskId, channelId, title, cwd, mode, scheduleId, scheduleName }, options) {
83080
83562
  const now = Date.now();
83563
+ pushBroadcast(options);
83081
83564
  await store.set(taskId, {
83082
83565
  taskId,
83083
83566
  channelId,
@@ -83097,41 +83580,46 @@ function buildTaskStateStore(dataDir) {
83097
83580
  ...scheduleName ? { scheduleName } : {}
83098
83581
  });
83099
83582
  },
83100
- async updateTaskStatus(taskId, status) {
83583
+ async updateTaskStatus(taskId, status, options) {
83101
83584
  const task = await store.get(taskId);
83102
83585
  if (!task) return;
83586
+ pushBroadcast(options);
83103
83587
  await store.set(taskId, applyStatusTransition(task, status, Date.now()));
83104
83588
  },
83105
- async updateTitle(taskId, title) {
83589
+ async updateTitle(taskId, title, options) {
83106
83590
  const task = await store.get(taskId);
83107
83591
  if (!task) return;
83592
+ pushBroadcast(options);
83108
83593
  await store.set(taskId, {
83109
83594
  ...task,
83110
83595
  title,
83111
83596
  updatedAt: Date.now()
83112
83597
  });
83113
83598
  },
83114
- async updateCwd(taskId, cwd) {
83599
+ async updateCwd(taskId, cwd, options) {
83115
83600
  const task = await store.get(taskId);
83116
83601
  if (!task) return;
83602
+ pushBroadcast(options);
83117
83603
  await store.set(taskId, {
83118
83604
  ...task,
83119
83605
  cwd,
83120
83606
  updatedAt: Date.now()
83121
83607
  });
83122
83608
  },
83123
- async updateMode(taskId, mode) {
83609
+ async updateMode(taskId, mode, options) {
83124
83610
  const task = await store.get(taskId);
83125
83611
  if (!task) return;
83612
+ pushBroadcast(options);
83126
83613
  await store.set(taskId, {
83127
83614
  ...task,
83128
83615
  mode,
83129
83616
  updatedAt: Date.now()
83130
83617
  });
83131
83618
  },
83132
- async updateTodoProgress(taskId, progress) {
83619
+ async updateTodoProgress(taskId, progress, options) {
83133
83620
  const task = await store.get(taskId);
83134
83621
  if (!task) return;
83622
+ pushBroadcast(options);
83135
83623
  await store.set(taskId, {
83136
83624
  ...task,
83137
83625
  todoCompleted: progress.todoCompleted,
@@ -83140,15 +83628,16 @@ function buildTaskStateStore(dataDir) {
83140
83628
  updatedAt: Date.now()
83141
83629
  });
83142
83630
  },
83143
- async acknowledge(taskId) {
83144
- await store.update(taskId, (task) => ({ ...task, acknowledgedAt: Date.now() }));
83631
+ async acknowledge(taskId, options) {
83632
+ await safeUpdate(taskId, (task) => ({ ...task, acknowledgedAt: Date.now() }), options);
83145
83633
  },
83146
- async togglePin(taskId) {
83147
- await store.update(taskId, (task) => ({ ...task, pinned: !task.pinned }));
83634
+ async togglePin(taskId, options) {
83635
+ await safeUpdate(taskId, (task) => ({ ...task, pinned: !task.pinned }), options);
83148
83636
  },
83149
- async updateStructuredTasks(taskId, tasks, todoProgress) {
83637
+ async updateStructuredTasks(taskId, tasks, todoProgress, options) {
83150
83638
  const task = await store.get(taskId);
83151
83639
  if (!task) return;
83640
+ pushBroadcast(options);
83152
83641
  await store.set(taskId, {
83153
83642
  ...task,
83154
83643
  structuredTasks: tasks,
@@ -83160,39 +83649,49 @@ function buildTaskStateStore(dataDir) {
83160
83649
  updatedAt: Date.now()
83161
83650
  });
83162
83651
  },
83163
- async updateTaskOverlay(taskId, overlay) {
83652
+ async updateTaskOverlay(taskId, overlay, options) {
83164
83653
  const task = await store.get(taskId);
83165
83654
  if (!task) return;
83655
+ pushBroadcast(options);
83166
83656
  await store.set(taskId, {
83167
83657
  ...task,
83168
83658
  taskOverlay: overlay,
83169
83659
  updatedAt: Date.now()
83170
83660
  });
83171
83661
  },
83172
- async updateComposerSettings(taskId, settings) {
83662
+ async updateComposerSettings(taskId, settings, options) {
83173
83663
  const task = await store.get(taskId);
83174
83664
  if (!task) return;
83665
+ pushBroadcast(options);
83175
83666
  await store.set(taskId, {
83176
83667
  ...task,
83177
83668
  composerSettings: { ...task.composerSettings, ...settings },
83178
83669
  updatedAt: Date.now()
83179
83670
  });
83180
83671
  },
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
- }));
83672
+ async updateCostStats(taskId, stats, options) {
83673
+ await safeUpdate(
83674
+ taskId,
83675
+ (task) => ({
83676
+ ...task,
83677
+ totalCostUsd: stats.totalCostUsd,
83678
+ totalOutputTokens: stats.totalOutputTokens,
83679
+ updatedAt: Date.now()
83680
+ }),
83681
+ options
83682
+ );
83188
83683
  },
83189
- async clearCostStats(taskId) {
83190
- await store.update(taskId, (task) => ({
83191
- ...task,
83192
- totalCostUsd: 0,
83193
- totalOutputTokens: 0,
83194
- updatedAt: Date.now()
83195
- }));
83684
+ async clearCostStats(taskId, options) {
83685
+ await safeUpdate(
83686
+ taskId,
83687
+ (task) => ({
83688
+ ...task,
83689
+ totalCostUsd: 0,
83690
+ totalOutputTokens: 0,
83691
+ updatedAt: Date.now()
83692
+ }),
83693
+ options
83694
+ );
83196
83695
  },
83197
83696
  async getTask(taskId) {
83198
83697
  return store.get(taskId);
@@ -83201,10 +83700,14 @@ function buildTaskStateStore(dataDir) {
83201
83700
  return store.list();
83202
83701
  },
83203
83702
  subscribe(listener) {
83204
- return store.subscribe(listener);
83703
+ taskListeners.add(listener);
83704
+ return () => {
83705
+ taskListeners.delete(listener);
83706
+ };
83205
83707
  },
83206
83708
  dispose() {
83207
83709
  unsubVersion();
83710
+ unsubBroadcast();
83208
83711
  }
83209
83712
  };
83210
83713
  }
@@ -83963,6 +84466,7 @@ async function createDaemon(deps) {
83963
84466
  await rehydrateFromPersistence(sessionPersistence, taskManager, deps.log, taskStateStore);
83964
84467
  await sweepStaleTasks(taskStateStore, taskManager, deps.log);
83965
84468
  const taskStoreUnsub = taskStateStore.subscribe((event) => {
84469
+ if (!event.broadcast) return;
83966
84470
  switch (event.kind) {
83967
84471
  case "set": {
83968
84472
  taskManager.broadcastControl({
@@ -84311,10 +84815,12 @@ var ROLE_PERMISSIONS = {
84311
84815
  "list_schedules",
84312
84816
  "run_schedule_now",
84313
84817
  "stop_task",
84818
+ "request_preview_target",
84314
84819
  "todo_item_added",
84315
84820
  "todo_item_removed",
84316
84821
  "todo_item_updated",
84317
- "todo_dep_changed"
84822
+ "todo_dep_changed",
84823
+ "presence_update"
84318
84824
  ]),
84319
84825
  "collaborator-full": /* @__PURE__ */ new Set([
84320
84826
  "send_message",
@@ -84341,10 +84847,12 @@ var ROLE_PERMISSIONS = {
84341
84847
  "request_annotation_snapshot",
84342
84848
  "create_thread",
84343
84849
  "list_threads",
84850
+ "request_preview_target",
84344
84851
  "todo_item_added",
84345
84852
  "todo_item_removed",
84346
84853
  "todo_item_updated",
84347
- "todo_dep_changed"
84854
+ "todo_dep_changed",
84855
+ "presence_update"
84348
84856
  ]),
84349
84857
  "collaborator-review": /* @__PURE__ */ new Set([
84350
84858
  "stop",
@@ -84355,7 +84863,8 @@ var ROLE_PERMISSIONS = {
84355
84863
  "join_collab_room",
84356
84864
  "leave_collab_room",
84357
84865
  "request_annotation_snapshot",
84358
- "list_threads"
84866
+ "list_threads",
84867
+ "presence_update"
84359
84868
  ]),
84360
84869
  viewer: /* @__PURE__ */ new Set([
84361
84870
  "subscribe",
@@ -84363,7 +84872,8 @@ var ROLE_PERMISSIONS = {
84363
84872
  "request_capabilities",
84364
84873
  "request_pr_data",
84365
84874
  "join_collab_room",
84366
- "leave_collab_room"
84875
+ "leave_collab_room",
84876
+ "presence_update"
84367
84877
  ])
84368
84878
  };
84369
84879
  function isCollabActionAllowed(role, action) {
@@ -84536,6 +85046,7 @@ function filterOutboundForCollab(msg, collabTaskId) {
84536
85046
  case "worktree_error":
84537
85047
  case "worktree_list":
84538
85048
  case "worktree_removed":
85049
+ case "directory_list":
84539
85050
  case "collab_room_joined":
84540
85051
  case "collab_room_left":
84541
85052
  case "schedule_fired":
@@ -84572,11 +85083,15 @@ function filterOutboundForCollab(msg, collabTaskId) {
84572
85083
  /** Collab-scoped: always pass through (keyed by machineId/roomId, not taskId) */
84573
85084
  case "collab_peer_role":
84574
85085
  case "collab_participants_update":
85086
+ case "presence_state":
84575
85087
  return msg;
84576
85088
  /** Pass through: collab peers need error feedback */
84577
85089
  case "promote_nudge":
84578
85090
  case "error":
84579
85091
  return msg;
85092
+ /** Log request is local-only, not relevant for collab peers */
85093
+ case "recent_logs":
85094
+ return null;
84580
85095
  /** Exhaustiveness check: compile error if a new type is unhandled */
84581
85096
  default: {
84582
85097
  const _exhaustive = msg;
@@ -84851,6 +85366,9 @@ function routeMessage(msg, callbacks, log) {
84851
85366
  case "request_user_settings":
84852
85367
  callbacks.onRequestUserSettings();
84853
85368
  break;
85369
+ case "request_preview_target":
85370
+ callbacks.onRequestPreviewTarget(msg.port, msg.url);
85371
+ break;
84854
85372
  /** Unified annotation CRUD */
84855
85373
  case "add_annotation":
84856
85374
  callbacks.onAddAnnotation(msg.taskId, msg.annotation);
@@ -84931,6 +85449,9 @@ function routeMessage(msg, callbacks, log) {
84931
85449
  case "stop_task":
84932
85450
  callbacks.onStopTask(msg.taskId);
84933
85451
  break;
85452
+ case "stop_background_agent":
85453
+ callbacks.onStopBackgroundAgent(msg.taskId, msg.backgroundTaskId);
85454
+ break;
84934
85455
  case "todo_item_added":
84935
85456
  callbacks.onTodoItemAdded(msg.taskId, msg.item);
84936
85457
  break;
@@ -84950,6 +85471,15 @@ function routeMessage(msg, callbacks, log) {
84950
85471
  msg.action
84951
85472
  );
84952
85473
  break;
85474
+ case "request_recent_logs":
85475
+ callbacks.onRequestRecentLogs(msg.windowMinutes);
85476
+ break;
85477
+ case "presence_update":
85478
+ callbacks.onPresenceUpdate(msg.state);
85479
+ break;
85480
+ case "list_directories":
85481
+ callbacks.onListDirectories(msg.basePath);
85482
+ break;
84953
85483
  default: {
84954
85484
  const _exhaustive = msg;
84955
85485
  throw new Error(`Unhandled control message type: ${JSON.stringify(_exhaustive)}`);
@@ -84991,6 +85521,7 @@ function injectThreadResourceLink(store, mainChannelId, taskId, threadId, title,
84991
85521
 
84992
85522
  // src/services/channels/control-channel-infra-handlers.ts
84993
85523
  import { spawn as spawn6 } from "child_process";
85524
+ import { readdir as readdir7 } from "fs/promises";
84994
85525
 
84995
85526
  // src/services/worktree-service.ts
84996
85527
  import { execFile as execFile7, spawn as spawn5 } from "child_process";
@@ -85482,6 +86013,24 @@ function buildAgentInstallHandlers(deps) {
85482
86013
  }
85483
86014
  };
85484
86015
  }
86016
+ var FILTERED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".turbo", "dist", ".next", "build"]);
86017
+ function buildDirectoryListHandler(deps) {
86018
+ return {
86019
+ onListDirectories: (basePath) => {
86020
+ readdir7(basePath, { withFileTypes: true }).then((entries) => {
86021
+ const directories = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".") && !FILTERED_DIRS.has(e.name)).map((e) => e.name).sort();
86022
+ deps.sendControlMessage({ type: "directory_list", basePath, directories });
86023
+ }).catch((err) => {
86024
+ deps.logAdapter({
86025
+ event: "list_directories_failed",
86026
+ basePath,
86027
+ error: err instanceof Error ? err.message : String(err)
86028
+ });
86029
+ deps.sendControlMessage({ type: "directory_list", basePath, directories: [] });
86030
+ });
86031
+ }
86032
+ };
86033
+ }
85485
86034
  function buildEnvironmentChangedHandler(deps) {
85486
86035
  const { daemon } = deps;
85487
86036
  let debounceTimer = null;
@@ -85715,6 +86264,99 @@ function runPluginOp(pluginName, marketplace, action, ctx) {
85715
86264
  });
85716
86265
  }
85717
86266
 
86267
+ // src/services/channels/read-recent-logs.ts
86268
+ import { createReadStream, readdirSync as readdirSync2 } from "fs";
86269
+ import { join as join38 } from "path";
86270
+ import { createInterface } from "readline";
86271
+ var MAX_BYTES = 5e4;
86272
+ var LOG_BASE_NAME = "daemon.log";
86273
+ var SENSITIVE_PATTERNS = [
86274
+ [/eyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*/g, "[REDACTED_JWT]"],
86275
+ [/"[Aa]uthorization"\s*:\s*"[^"]+"/g, '"Authorization":"[REDACTED]"'],
86276
+ [/"token"\s*:\s*"[^"]+"/g, '"token":"[REDACTED]"'],
86277
+ [/"accessToken"\s*:\s*"[^"]+"/g, '"accessToken":"[REDACTED]"'],
86278
+ [/"refreshToken"\s*:\s*"[^"]+"/g, '"refreshToken":"[REDACTED]"'],
86279
+ [/sk-ant-[A-Za-z0-9_-]{20,}/g, "[REDACTED_API_KEY]"],
86280
+ [/sk-[a-zA-Z0-9]{20,}/g, "[REDACTED_SECRET_KEY]"],
86281
+ [/device_code=[A-Za-z0-9_-]+/g, "device_code=[REDACTED]"]
86282
+ ];
86283
+ function scrubSensitiveData(line) {
86284
+ let result = line;
86285
+ for (const [pattern, replacement] of SENSITIVE_PATTERNS) {
86286
+ result = result.replace(pattern, replacement);
86287
+ }
86288
+ return result;
86289
+ }
86290
+ async function collectFromFile(filePath, cutoff, collector, maxBytes) {
86291
+ const rl = createInterface({
86292
+ input: createReadStream(filePath, { encoding: "utf-8" }),
86293
+ crlfDelay: Number.POSITIVE_INFINITY
86294
+ });
86295
+ for await (const line of rl) {
86296
+ if (!line) continue;
86297
+ const timestamp = extractTimestamp(line);
86298
+ if (timestamp === null || timestamp < cutoff) continue;
86299
+ const scrubbed = scrubSensitiveData(line);
86300
+ const lineBytes = Buffer.byteLength(scrubbed, "utf-8");
86301
+ if (collector.totalBytes + lineBytes > maxBytes) {
86302
+ collector.truncated = true;
86303
+ rl.close();
86304
+ return true;
86305
+ }
86306
+ collector.lines.push(scrubbed);
86307
+ collector.totalBytes += lineBytes;
86308
+ }
86309
+ return false;
86310
+ }
86311
+ async function readRecentLogs(logDir, windowMinutes, maxBytes = MAX_BYTES) {
86312
+ const cutoff = Date.now() - windowMinutes * 6e4;
86313
+ const collector = { lines: [], totalBytes: 0, truncated: false };
86314
+ for (const filePath of discoverLogFiles(logDir)) {
86315
+ try {
86316
+ const exhausted = await collectFromFile(filePath, cutoff, collector, maxBytes);
86317
+ if (exhausted) break;
86318
+ } catch {
86319
+ }
86320
+ }
86321
+ return { lines: collector.lines, truncated: collector.truncated };
86322
+ }
86323
+ function discoverLogFiles(logDir) {
86324
+ let entries;
86325
+ try {
86326
+ entries = readdirSync2(logDir);
86327
+ } catch {
86328
+ return [];
86329
+ }
86330
+ const rotated = [];
86331
+ let currentFile = null;
86332
+ for (const entry of entries) {
86333
+ if (entry === LOG_BASE_NAME) {
86334
+ currentFile = join38(logDir, entry);
86335
+ } else if (entry.startsWith(`${LOG_BASE_NAME}.`)) {
86336
+ const suffix = entry.slice(LOG_BASE_NAME.length + 1);
86337
+ const index = Number.parseInt(suffix, 10);
86338
+ if (!Number.isNaN(index)) {
86339
+ rotated.push({ path: join38(logDir, entry), index });
86340
+ }
86341
+ }
86342
+ }
86343
+ rotated.sort((a, b2) => b2.index - a.index);
86344
+ const result = rotated.map((r) => r.path);
86345
+ if (currentFile) result.push(currentFile);
86346
+ return result;
86347
+ }
86348
+ function extractTimestamp(line) {
86349
+ const idx = line.indexOf('"time":');
86350
+ if (idx === -1) return null;
86351
+ const start = idx + 7;
86352
+ let end = start;
86353
+ while (end < line.length && line.charCodeAt(end) >= 48 && line.charCodeAt(end) <= 57) {
86354
+ end++;
86355
+ }
86356
+ if (end === start) return null;
86357
+ return Number(line.slice(start, end));
86358
+ }
86359
+
85718
86360
  // src/services/channels/schedule-channel-callbacks.ts
85719
86361
  function buildScheduleCallbacks(daemon, getControlHandler, log) {
85720
86362
  return {
@@ -85883,6 +86525,33 @@ function buildTemplateCallbacks(daemon, getControlHandler, log) {
85883
86525
  }
85884
86526
 
85885
86527
  // src/services/channels/control-channel-wiring.ts
86528
+ function handlePreviewTargetRequest(proxy, port, url, daemon, log) {
86529
+ if (!proxy) return;
86530
+ (async () => {
86531
+ try {
86532
+ if (url) await proxy.stop();
86533
+ else if (port) proxy.port ? proxy.retarget(port) : await proxy.start({ port });
86534
+ daemon.taskManager.broadcastControl({
86535
+ type: "set_preview_target",
86536
+ port,
86537
+ url,
86538
+ proxyPort: proxy.port ?? void 0
86539
+ });
86540
+ } catch (err) {
86541
+ log({
86542
+ event: "preview_target_request_failed",
86543
+ error: err instanceof Error ? err.message : String(err)
86544
+ });
86545
+ }
86546
+ })();
86547
+ }
86548
+ function resolveIdentityFromRegistry(deps) {
86549
+ if (!deps.peerRoleRegistry || !deps.peerMachineId) return void 0;
86550
+ const entry = deps.peerRoleRegistry.getEntry(deps.peerMachineId);
86551
+ if (!entry) return void 0;
86552
+ const userId = entry.participantId.replace(/^human:/, "");
86553
+ return { userId, username: entry.displayName, avatarUrl: null };
86554
+ }
85886
86555
  function applyMcpServerToggles(daemon, prevDisabled, nextDisabled, log) {
85887
86556
  for (const name of nextDisabled) {
85888
86557
  if (!prevDisabled.has(name)) {
@@ -85978,6 +86647,10 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
85978
86647
  const worktreeHandlers = buildWorktreeHandlers(lazyInfraDeps);
85979
86648
  const agentInstallHandlers = buildAgentInstallHandlers(lazyInfraDeps);
85980
86649
  const envChangedHandler = buildEnvironmentChangedHandler(lazyInfraDeps);
86650
+ const directoryListHandler = buildDirectoryListHandler({
86651
+ sendControlMessage: (msg) => controlHandler.sendControl(msg),
86652
+ logAdapter
86653
+ });
85981
86654
  const prPoller = createPRPoller(
85982
86655
  {
85983
86656
  onPRState: (payload) => {
@@ -86241,6 +86914,7 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
86241
86914
  },
86242
86915
  ...agentInstallHandlers,
86243
86916
  ...worktreeHandlers,
86917
+ ...directoryListHandler,
86244
86918
  onJoinCollabRoom: (() => {
86245
86919
  const mgr = deps?.collabRoomManager;
86246
86920
  if (!mgr) return void 0;
@@ -86545,6 +87219,9 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
86545
87219
  });
86546
87220
  });
86547
87221
  },
87222
+ onRequestPreviewTarget: (port, url) => {
87223
+ handlePreviewTargetRequest(deps?.previewProxy, port, url, daemon, logAdapter);
87224
+ },
86548
87225
  onAcknowledgeTask: (taskId) => {
86549
87226
  daemon.taskStateStore.acknowledge(taskId).catch((err) => {
86550
87227
  logAdapter({
@@ -86574,9 +87251,44 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
86574
87251
  });
86575
87252
  }
86576
87253
  },
87254
+ onStopBackgroundAgent: (taskId, backgroundTaskId) => {
87255
+ daemon.taskManager.stopBackgroundTask(taskId, backgroundTaskId).catch((err) => {
87256
+ logAdapter({
87257
+ event: "stop_background_agent_failed",
87258
+ taskId,
87259
+ backgroundTaskId,
87260
+ error: err instanceof Error ? err.message : String(err)
87261
+ });
87262
+ });
87263
+ },
86577
87264
  ...buildScheduleCallbacks(daemon, () => controlHandler, logAdapter),
86578
87265
  ...buildTemplateCallbacks(daemon, () => controlHandler, logAdapter),
86579
- ...todoHandlers
87266
+ ...todoHandlers,
87267
+ onRequestRecentLogs: (windowMinutes) => {
87268
+ const logDir = `${deps?.shipyardHome ?? getShipyardHome()}/logs`;
87269
+ readRecentLogs(logDir, windowMinutes).then((result) => {
87270
+ handler.sendControl({ type: "recent_logs", ...result });
87271
+ logAdapter({
87272
+ event: "recent_logs_sent",
87273
+ lineCount: result.lines.length,
87274
+ truncated: result.truncated
87275
+ });
87276
+ }).catch((err) => {
87277
+ handler.sendControl({ type: "recent_logs", lines: [], truncated: false });
87278
+ logAdapter({
87279
+ event: "recent_logs_failed",
87280
+ error: err instanceof Error ? err.message : String(err)
87281
+ });
87282
+ });
87283
+ },
87284
+ onPresenceUpdate: (state) => {
87285
+ if (!deps?.presencePool) return;
87286
+ const identity = deps.peerIdentity ?? resolveIdentityFromRegistry(deps);
87287
+ if (!identity) return;
87288
+ const ref = deps.presencePool;
87289
+ ref.pool = applyPresenceUpdate(ref.pool, controlPeerId, identity, state);
87290
+ daemon.taskManager.broadcastControl({ type: "presence_state", peers: ref.pool });
87291
+ }
86580
87292
  },
86581
87293
  logAdapter
86582
87294
  );
@@ -86699,6 +87411,9 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
86699
87411
  const userSettingsUnsub = daemon.userSettingsStore.subscribe((settings) => {
86700
87412
  controlHandler.sendControl({ type: "user_settings_updated", settings });
86701
87413
  });
87414
+ if (deps?.presencePool) {
87415
+ controlHandler.sendControl({ type: "presence_state", peers: deps.presencePool.pool });
87416
+ }
86702
87417
  const enforcedTaskId = deps?.collabTaskId;
86703
87418
  dc.onmessage = (ev) => {
86704
87419
  const raw = typeof ev.data === "string" ? ev.data : String(ev.data);
@@ -86725,6 +87440,11 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
86725
87440
  handler.dispose();
86726
87441
  daemon.taskManager.unregisterControlChannel(controlPeerId);
86727
87442
  daemon.taskManager.setOnAuthNotLoggedIn(null);
87443
+ if (deps?.presencePool) {
87444
+ const ref = deps.presencePool;
87445
+ ref.pool = removePresence(ref.pool, controlPeerId);
87446
+ daemon.taskManager.broadcastControl({ type: "presence_state", peers: ref.pool });
87447
+ }
86728
87448
  };
86729
87449
  logAdapter({
86730
87450
  event: "control_channel_wired",
@@ -86952,6 +87672,77 @@ function handleMessageChannel(opts) {
86952
87672
  };
86953
87673
  }
86954
87674
 
87675
+ // src/services/collab/collab-repo-manager.ts
87676
+ var DAEMON_IDENTITY2 = { name: "shipyard-daemon", type: "service" };
87677
+ var COLLAB_VISIBLE_PREFIXES = /* @__PURE__ */ new Set(["plan", "canvas"]);
87678
+ var COLLAB_MUTABLE_PREFIXES = {
87679
+ "collaborator-full": /* @__PURE__ */ new Set(["plan", "canvas"]),
87680
+ "collaborator-review": /* @__PURE__ */ new Set(["plan"])
87681
+ };
87682
+ function planCollabRoomResources(taskId, roomId, epochs) {
87683
+ return {
87684
+ docIds: [buildPlanDocId(taskId, epochs.plan), buildCanvasDocId(taskId, epochs.canvas)],
87685
+ personalBridgeType: `bridge-personal-${roomId}`,
87686
+ collabBridgeType: `bridge-collab-${roomId}`
87687
+ };
87688
+ }
87689
+ function buildCollabRepoPermissions(taskId, peerRoleRegistry) {
87690
+ return {
87691
+ visibility(doc3, peer) {
87692
+ if (peer.channelKind === "storage") return true;
87693
+ if (peer.peerType === "service") return true;
87694
+ const parsed = parseDocumentId(doc3.id);
87695
+ if (!parsed) return false;
87696
+ if (parsed.key !== taskId) return false;
87697
+ return COLLAB_VISIBLE_PREFIXES.has(parsed.prefix);
87698
+ },
87699
+ mutability(doc3, peer) {
87700
+ if (peer.channelKind === "storage") return true;
87701
+ if (peer.peerType === "service") return true;
87702
+ const parsed = parseDocumentId(doc3.id);
87703
+ if (!parsed) return false;
87704
+ if (parsed.key !== taskId) return false;
87705
+ const entry = peerRoleRegistry.getEntry(peer.peerId);
87706
+ if (!entry) return false;
87707
+ const allowed = COLLAB_MUTABLE_PREFIXES[entry.role];
87708
+ return allowed ? allowed.has(parsed.prefix) : false;
87709
+ },
87710
+ creation(_docId, _peer) {
87711
+ return false;
87712
+ },
87713
+ deletion(_doc, _peer) {
87714
+ return false;
87715
+ }
87716
+ };
87717
+ }
87718
+ function createCollabRepo(personalRepo, taskId, roomId, epochs, peerRoleRegistry) {
87719
+ const resources = planCollabRoomResources(taskId, roomId, epochs);
87720
+ const bridge = new Bridge();
87721
+ const personalBridgeAdapter = new BridgeAdapter({
87722
+ adapterType: resources.personalBridgeType,
87723
+ bridge
87724
+ });
87725
+ const collabBridgeAdapter = new BridgeAdapter({
87726
+ adapterType: resources.collabBridgeType,
87727
+ bridge
87728
+ });
87729
+ const collabWebrtcAdapter = new WebRtcDataChannelAdapter();
87730
+ const collabRepo = new Repo({
87731
+ identity: { ...DAEMON_IDENTITY2, name: `collab-room-${roomId}` },
87732
+ adapters: [collabBridgeAdapter, collabWebrtcAdapter],
87733
+ permissions: buildCollabRepoPermissions(taskId, peerRoleRegistry)
87734
+ });
87735
+ personalRepo.addAdapter(personalBridgeAdapter);
87736
+ return {
87737
+ repo: collabRepo,
87738
+ webrtcAdapter: collabWebrtcAdapter,
87739
+ async destroy() {
87740
+ personalRepo.removeAdapter(personalBridgeAdapter.adapterId);
87741
+ await collabRepo.shutdown();
87742
+ }
87743
+ };
87744
+ }
87745
+
86955
87746
  // src/services/collab/collab-room-manager.ts
86956
87747
  function assertNever2(x2) {
86957
87748
  throw new Error(`Unhandled message type: ${JSON.stringify(x2)}`);
@@ -87084,6 +87875,7 @@ function createCollabRoomManager(deps) {
87084
87875
  for (const userId of room.knownPeers) {
87085
87876
  deps.peerRoleRegistry.unregisterPeer(namespacePeerId(roomId, userId));
87086
87877
  }
87878
+ room.collabRepoHandle?.destroy();
87087
87879
  room.peerManager.destroy();
87088
87880
  room.connection.disconnect();
87089
87881
  rooms.delete(roomId);
@@ -87121,7 +87913,19 @@ function createCollabRoomManager(deps) {
87121
87913
  maxDelayMs: 3e4,
87122
87914
  backoffMultiplier: 2
87123
87915
  });
87124
- const peerManager = deps.createPeerManagerForRoom(roomId, connection, taskId);
87916
+ const collabRepoHandle = createCollabRepo(
87917
+ deps.personalRepo,
87918
+ taskId,
87919
+ roomId,
87920
+ { plan: deps.planEpoch, canvas: deps.canvasEpoch },
87921
+ deps.peerRoleRegistry
87922
+ );
87923
+ const peerManager = deps.createPeerManagerForRoom(
87924
+ roomId,
87925
+ connection,
87926
+ taskId,
87927
+ collabRepoHandle.webrtcAdapter
87928
+ );
87125
87929
  const handle = {
87126
87930
  roomId,
87127
87931
  taskId,
@@ -87132,6 +87936,7 @@ function createCollabRoomManager(deps) {
87132
87936
  const delayMs = expiresAt - Date.now();
87133
87937
  if (delayMs <= 0 || !Number.isFinite(delayMs)) {
87134
87938
  deps.log({ event: "collab_expired", roomId, expiresAt });
87939
+ collabRepoHandle.destroy();
87135
87940
  return handle;
87136
87941
  }
87137
87942
  const safeDelayMs = Math.min(delayMs, 2147483647);
@@ -87140,6 +87945,7 @@ function createCollabRoomManager(deps) {
87140
87945
  taskId,
87141
87946
  connection,
87142
87947
  peerManager,
87948
+ collabRepoHandle,
87143
87949
  myUserId: null,
87144
87950
  knownPeers: /* @__PURE__ */ new Set(),
87145
87951
  participants: [],
@@ -87163,7 +87969,12 @@ function createCollabRoomManager(deps) {
87163
87969
  deps.peerRoleRegistry.unregisterPeer(namespacePeerId(roomId, userId));
87164
87970
  }
87165
87971
  room.peerManager.destroy();
87166
- room.peerManager = deps.createPeerManagerForRoom(roomId, connection, taskId);
87972
+ room.peerManager = deps.createPeerManagerForRoom(
87973
+ roomId,
87974
+ connection,
87975
+ taskId,
87976
+ room.collabRepoHandle?.webrtcAdapter ?? collabRepoHandle.webrtcAdapter
87977
+ );
87167
87978
  room.myUserId = null;
87168
87979
  room.knownPeers.clear();
87169
87980
  }
@@ -87204,41 +88015,20 @@ function findEntry(registry, peer) {
87204
88015
  }
87205
88016
  return void 0;
87206
88017
  }
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
88018
  function buildCollabPermissions(registry) {
88019
+ function isAllowed(_doc, peer) {
88020
+ if (peer.channelKind === "storage") return true;
88021
+ if (peer.peerType === "service") return true;
88022
+ const entry = findEntry(registry, peer);
88023
+ if (!entry) return false;
88024
+ return isPersonalPeer(entry);
88025
+ }
87217
88026
  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
- },
88027
+ visibility: isAllowed,
88028
+ mutability: isAllowed,
87240
88029
  creation(_docId, peer) {
87241
88030
  if (peer.channelKind === "storage") return true;
88031
+ if (peer.peerType === "service") return true;
87242
88032
  const entry = findEntry(registry, peer);
87243
88033
  if (!entry) return false;
87244
88034
  return isPersonalPeer(entry);
@@ -87298,8 +88088,8 @@ function createPeerRoleRegistry() {
87298
88088
  }
87299
88089
 
87300
88090
  // src/services/epoch-pruning.ts
87301
- import { readdir as readdir7, rm as rm3 } from "fs/promises";
87302
- import { join as join38 } from "path";
88091
+ import { readdir as readdir8, rm as rm3 } from "fs/promises";
88092
+ import { join as join39 } from "path";
87303
88093
  var LEGACY_PREFIXES = [
87304
88094
  "task-meta",
87305
88095
  "task-conv",
@@ -87316,7 +88106,7 @@ function isLegacyDocId(decoded) {
87316
88106
  async function pruneOldEpochData(dataDir, currentEpoch, log) {
87317
88107
  let entries;
87318
88108
  try {
87319
- entries = await readdir7(dataDir, { withFileTypes: true });
88109
+ entries = await readdir8(dataDir, { withFileTypes: true });
87320
88110
  } catch {
87321
88111
  return;
87322
88112
  }
@@ -87334,7 +88124,7 @@ async function pruneOldEpochData(dataDir, currentEpoch, log) {
87334
88124
  if (!parsed) {
87335
88125
  if (isLegacyDocId(decoded)) {
87336
88126
  removals.push(
87337
- rm3(join38(dataDir, entry.name), { recursive: true }).then(() => {
88127
+ rm3(join39(dataDir, entry.name), { recursive: true }).then(() => {
87338
88128
  pruned++;
87339
88129
  }).catch((err) => {
87340
88130
  log({
@@ -87349,7 +88139,7 @@ async function pruneOldEpochData(dataDir, currentEpoch, log) {
87349
88139
  }
87350
88140
  if (parsed.epoch >= currentEpoch) continue;
87351
88141
  removals.push(
87352
- rm3(join38(dataDir, entry.name), { recursive: true }).then(() => {
88142
+ rm3(join39(dataDir, entry.name), { recursive: true }).then(() => {
87353
88143
  pruned++;
87354
88144
  }).catch((err) => {
87355
88145
  log({
@@ -87368,7 +88158,7 @@ async function pruneOldEpochData(dataDir, currentEpoch, log) {
87368
88158
 
87369
88159
  // src/services/file-io-handler.ts
87370
88160
  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";
88161
+ import { readdir as readdir9, readFile as readFile25, stat as stat5, unlink as unlink6, writeFile as writeFile22 } from "fs/promises";
87372
88162
  import { normalize as normalize5, relative as relative2, resolve } from "path";
87373
88163
  import { promisify as promisify6 } from "util";
87374
88164
  function handleFileIOChannel(initialCwd, send, log, deps) {
@@ -87532,7 +88322,7 @@ function handleFileIOChannel(initialCwd, send, log, deps) {
87532
88322
  }
87533
88323
  async function handleReaddir(requestId, absPath) {
87534
88324
  try {
87535
- const entries = await readdir8(absPath, { withFileTypes: true });
88325
+ const entries = await readdir9(absPath, { withFileTypes: true });
87536
88326
  const result = entries.filter((e) => !e.name.startsWith(".") && e.name !== "node_modules").map((e) => ({
87537
88327
  name: e.name,
87538
88328
  type: e.isDirectory() ? "directory" : "file"
@@ -88271,17 +89061,25 @@ function guardLoroChannelSend(dc, log) {
88271
89061
  const originalSend = dc.send.bind(dc);
88272
89062
  let dropCount = 0;
88273
89063
  let lastLogAt = 0;
89064
+ const logDrop = (reason) => {
89065
+ dropCount++;
89066
+ const now = Date.now();
89067
+ if (now - lastLogAt >= 1e3) {
89068
+ log.warn({ droppedCount: dropCount, reason }, "Loro sync send dropped");
89069
+ dropCount = 0;
89070
+ lastLogAt = now;
89071
+ }
89072
+ };
88274
89073
  dc.send = (data) => {
89074
+ if (dc.readyState !== void 0 && dc.readyState !== "open") return;
89075
+ if (typeof dc.bufferedAmount === "number" && dc.bufferedAmount > LORO_BACKPRESSURE_HIGH_WATER) {
89076
+ logDrop("backpressure");
89077
+ return;
89078
+ }
88275
89079
  try {
88276
89080
  originalSend(data);
88277
89081
  } 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
- }
89082
+ logDrop("send_error");
88285
89083
  }
88286
89084
  };
88287
89085
  }
@@ -88526,8 +89324,8 @@ function createPeerManager(config2) {
88526
89324
 
88527
89325
  // src/services/plugins/plugin-file-watcher.ts
88528
89326
  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";
89327
+ import { readdir as readdir10, readFile as readFile26, stat as stat6 } from "fs/promises";
89328
+ import { join as join40 } from "path";
88531
89329
  import { pathToFileURL } from "url";
88532
89330
  var DEBOUNCE_MS2 = 500;
88533
89331
  var PLUGIN_ID_PATTERN2 = /^[a-z0-9][a-z0-9-]*$/;
@@ -88578,14 +89376,14 @@ var PluginFileWatcher = class {
88578
89376
  }
88579
89377
  let entries;
88580
89378
  try {
88581
- entries = await readdir9(dir);
89379
+ entries = await readdir10(dir);
88582
89380
  } catch {
88583
89381
  this.#reconcile([]);
88584
89382
  return;
88585
89383
  }
88586
89384
  const loaded = [];
88587
89385
  for (const entry of entries) {
88588
- const pluginDir = join39(dir, entry);
89386
+ const pluginDir = join40(dir, entry);
88589
89387
  let stats;
88590
89388
  try {
88591
89389
  stats = await stat6(pluginDir);
@@ -88600,7 +89398,7 @@ var PluginFileWatcher = class {
88600
89398
  this.#reconcile(loaded);
88601
89399
  }
88602
89400
  async #loadPlugin(id, pluginDir) {
88603
- const manifestPath = join39(pluginDir, "plugin.json");
89401
+ const manifestPath = join40(pluginDir, "plugin.json");
88604
89402
  let manifestRaw;
88605
89403
  try {
88606
89404
  manifestRaw = await readFile26(manifestPath, "utf-8");
@@ -88627,7 +89425,7 @@ var PluginFileWatcher = class {
88627
89425
  }
88628
89426
  const manifest = parsed.data;
88629
89427
  let template = "";
88630
- const templatePath = join39(pluginDir, "template.html");
89428
+ const templatePath = join40(pluginDir, "template.html");
88631
89429
  try {
88632
89430
  template = await readFile26(templatePath, "utf-8");
88633
89431
  } catch {
@@ -88642,7 +89440,7 @@ var PluginFileWatcher = class {
88642
89440
  };
88643
89441
  }
88644
89442
  async #loadAndRegisterHandler(id, pluginDir, title) {
88645
- const handlerPath = join39(pluginDir, "handler.mjs");
89443
+ const handlerPath = join40(pluginDir, "handler.mjs");
88646
89444
  let handlerFn;
88647
89445
  try {
88648
89446
  const handlerStat = await stat6(handlerPath);
@@ -88903,28 +89701,14 @@ var HOP_BY_HOP_HEADERS2 = /* @__PURE__ */ new Set([
88903
89701
  "proxy-authorization",
88904
89702
  "proxy-connection",
88905
89703
  "te",
88906
- "host"
89704
+ "host",
89705
+ "accept-encoding"
88907
89706
  ]);
88908
89707
  var IFRAME_BLOCKING_HEADERS2 = /* @__PURE__ */ new Set([
88909
89708
  "x-frame-options",
88910
89709
  "content-security-policy",
88911
89710
  "content-security-policy-report-only"
88912
89711
  ]);
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
89712
  function injectAnnotationBridge(html) {
88929
89713
  const headIdx = html.indexOf("<head");
88930
89714
  if (headIdx === -1) return ANNOTATION_BRIDGE_TAG + html;
@@ -88952,10 +89736,13 @@ function collectResponseHeaders(rawHeaders) {
88952
89736
  }
88953
89737
  return result;
88954
89738
  }
89739
+ var BLOCKED_PORTS = /* @__PURE__ */ new Set([22, 25, 3306, 5432, 6379, 27017]);
88955
89740
  function createPreviewProxy(config2) {
88956
89741
  const { log } = config2;
88957
89742
  let server = null;
88958
89743
  let allocatedPort = null;
89744
+ let currentTargetPort = null;
89745
+ let startPromise = null;
88959
89746
  const activeRequests = /* @__PURE__ */ new Set();
88960
89747
  const activeSockets = /* @__PURE__ */ new Set();
88961
89748
  function proxyRequest(clientReq, clientRes, targetPort) {
@@ -88963,7 +89750,7 @@ function createPreviewProxy(config2) {
88963
89750
  headers.host = `localhost:${targetPort}`;
88964
89751
  const proxyReq = http2.request(
88965
89752
  {
88966
- hostname: "127.0.0.1",
89753
+ hostname: "localhost",
88967
89754
  port: targetPort,
88968
89755
  path: clientReq.url ?? "/",
88969
89756
  method: clientReq.method ?? "GET",
@@ -89041,7 +89828,7 @@ function createPreviewProxy(config2) {
89041
89828
  headers.connection = "Upgrade";
89042
89829
  headers.upgrade = clientReq.headers.upgrade ?? "websocket";
89043
89830
  const proxyReq = http2.request({
89044
- hostname: "127.0.0.1",
89831
+ hostname: "localhost",
89045
89832
  port: targetPort,
89046
89833
  path: clientReq.url ?? "/",
89047
89834
  method: "GET",
@@ -89100,21 +89887,45 @@ function createPreviewProxy(config2) {
89100
89887
  get port() {
89101
89888
  return allocatedPort;
89102
89889
  },
89890
+ get targetPort() {
89891
+ return currentTargetPort;
89892
+ },
89893
+ retarget(port) {
89894
+ if (port < 1 || port > 65535 || BLOCKED_PORTS.has(port)) {
89895
+ throw new Error(`Port ${port} is not allowed for preview proxy`);
89896
+ }
89897
+ currentTargetPort = port;
89898
+ log({ event: "preview_proxy_retargeted", proxyPort: allocatedPort, targetPort: port });
89899
+ },
89103
89900
  async start(target) {
89901
+ if (startPromise) await startPromise;
89104
89902
  if (server) {
89105
89903
  throw new Error("Preview proxy already started");
89106
89904
  }
89107
- allocatedPort = await findAvailablePort2();
89905
+ if (target.port < 1 || target.port > 65535 || BLOCKED_PORTS.has(target.port)) {
89906
+ throw new Error(`Port ${target.port} is not allowed for preview proxy`);
89907
+ }
89908
+ currentTargetPort = target.port;
89108
89909
  const srv = http2.createServer((req, res) => {
89109
- proxyRequest(req, res, target.port);
89910
+ if (!currentTargetPort) {
89911
+ res.writeHead(502, { "content-type": "text/plain" });
89912
+ res.end("No preview target configured");
89913
+ return;
89914
+ }
89915
+ proxyRequest(req, res, currentTargetPort);
89110
89916
  });
89111
89917
  server = srv;
89112
89918
  srv.on("upgrade", (req, socket, head) => {
89113
- proxyWebSocketUpgrade(req, socket, head, target.port);
89919
+ if (!currentTargetPort) {
89920
+ socket.destroy();
89921
+ return;
89922
+ }
89923
+ proxyWebSocketUpgrade(req, socket, head, currentTargetPort);
89114
89924
  });
89115
- const listenPort = allocatedPort;
89116
- await new Promise((resolve3, reject) => {
89117
- srv.listen(listenPort, "127.0.0.1", () => {
89925
+ startPromise = new Promise((resolve3, reject) => {
89926
+ srv.listen(0, "localhost", () => {
89927
+ const addr = srv.address();
89928
+ allocatedPort = typeof addr === "object" && addr ? addr.port : null;
89118
89929
  log({
89119
89930
  event: "preview_proxy_started",
89120
89931
  proxyPort: allocatedPort,
@@ -89124,8 +89935,11 @@ function createPreviewProxy(config2) {
89124
89935
  });
89125
89936
  srv.on("error", reject);
89126
89937
  });
89938
+ await startPromise;
89939
+ startPromise = null;
89127
89940
  },
89128
89941
  async stop() {
89942
+ if (startPromise) await startPromise;
89129
89943
  for (const req of activeRequests) {
89130
89944
  req.destroy();
89131
89945
  }
@@ -89138,6 +89952,7 @@ function createPreviewProxy(config2) {
89138
89952
  const srv = server;
89139
89953
  server = null;
89140
89954
  allocatedPort = null;
89955
+ currentTargetPort = null;
89141
89956
  await new Promise((resolve3, reject) => {
89142
89957
  srv.close((err) => err ? reject(err) : resolve3());
89143
89958
  });
@@ -89150,7 +89965,7 @@ function createPreviewProxy(config2) {
89150
89965
  // src/services/storage/daemon-settings-store.ts
89151
89966
  import { createHash as createHash5 } from "crypto";
89152
89967
  import { mkdir as mkdir16, readFile as readFile27, rename as rename15, writeFile as writeFile23 } from "fs/promises";
89153
- import { join as join40 } from "path";
89968
+ import { join as join41 } from "path";
89154
89969
  var ProjectSettingsSchema = external_exports.object({
89155
89970
  disabledMcpServers: external_exports.array(external_exports.string()).optional()
89156
89971
  });
@@ -89158,9 +89973,9 @@ function hashProjectPath(projectPath) {
89158
89973
  return createHash5("sha256").update(projectPath).digest("hex").slice(0, 16);
89159
89974
  }
89160
89975
  function buildDaemonSettingsStore(dataDir) {
89161
- const settingsDir = join40(dataDir, "settings");
89976
+ const settingsDir = join41(dataDir, "settings");
89162
89977
  function settingsPath(projectPath) {
89163
- return join40(settingsDir, `${hashProjectPath(projectPath)}.json`);
89978
+ return join41(settingsDir, `${hashProjectPath(projectPath)}.json`);
89164
89979
  }
89165
89980
  async function ensureDir() {
89166
89981
  await mkdir16(settingsDir, { recursive: true });
@@ -89187,12 +90002,12 @@ function buildDaemonSettingsStore(dataDir) {
89187
90002
 
89188
90003
  // src/services/storage/plugin-config-store.ts
89189
90004
  import { mkdir as mkdir17, readFile as readFile28, rename as rename16, writeFile as writeFile24 } from "fs/promises";
89190
- import { join as join41 } from "path";
90005
+ import { join as join42 } from "path";
89191
90006
  function buildPluginConfigStore(pluginsDir) {
89192
90007
  const cache2 = /* @__PURE__ */ new Map();
89193
90008
  const writeQueues = /* @__PURE__ */ new Map();
89194
90009
  function configPath(pluginId) {
89195
- return join41(pluginsDir, pluginId, "config.json");
90010
+ return join42(pluginsDir, pluginId, "config.json");
89196
90011
  }
89197
90012
  async function ensureLoaded(pluginId) {
89198
90013
  const cached = cache2.get(pluginId);
@@ -89223,7 +90038,7 @@ function buildPluginConfigStore(pluginsDir) {
89223
90038
  const next = prev.then(async () => {
89224
90039
  const filePath = configPath(pluginId);
89225
90040
  const tmpPath = `${filePath}.tmp`;
89226
- await mkdir17(join41(pluginsDir, pluginId), { recursive: true });
90041
+ await mkdir17(join42(pluginsDir, pluginId), { recursive: true });
89227
90042
  await writeFile24(tmpPath, JSON.stringify(config2, null, 2), "utf-8");
89228
90043
  await rename16(tmpPath, filePath);
89229
90044
  });
@@ -89631,7 +90446,7 @@ function resolveClaudeCodePath(log) {
89631
90446
  try {
89632
90447
  const req = createRequire4(import.meta.url);
89633
90448
  const sdkMain = req.resolve("@anthropic-ai/claude-agent-sdk");
89634
- const p2 = join42(dirname14(sdkMain), "cli.js");
90449
+ const p2 = join43(dirname14(sdkMain), "cli.js");
89635
90450
  if (existsSync7(p2)) return ok("sdk_bundled", p2);
89636
90451
  } catch {
89637
90452
  }
@@ -89641,7 +90456,7 @@ function resolveClaudeCodePath(log) {
89641
90456
  } catch {
89642
90457
  }
89643
90458
  for (const c of [
89644
- join42(process.env.HOME ?? "", ".local", "bin", "claude"),
90459
+ join43(process.env.HOME ?? "", ".local", "bin", "claude"),
89645
90460
  "/usr/local/bin/claude"
89646
90461
  ])
89647
90462
  if (existsSync7(c)) return ok("well_known", c);
@@ -89649,11 +90464,11 @@ function resolveClaudeCodePath(log) {
89649
90464
  }
89650
90465
  async function serve(options = {}) {
89651
90466
  const shipyardHome = options.shipyardHome ?? getShipyardHome();
89652
- const dataDir = join42(shipyardHome, options.isDev ? "data-dev" : "data");
90467
+ const dataDir = join43(shipyardHome, options.isDev ? "data-dev" : "data");
89653
90468
  const log = createChildLogger({ mode: "serve" });
89654
90469
  const workspaceRoot = findProjectRoot(process.cwd());
89655
90470
  registerBuiltinPlugins();
89656
- const pluginConfigStore = buildPluginConfigStore(join42(dataDir, "plugins"));
90471
+ const pluginConfigStore = buildPluginConfigStore(join43(dataDir, "plugins"));
89657
90472
  await mkdir18(dataDir, { recursive: true });
89658
90473
  log.info({ shipyardHome, dataDir, workspaceRoot }, "Starting daemon");
89659
90474
  function logAdapter(entry) {
@@ -89671,13 +90486,14 @@ async function serve(options = {}) {
89671
90486
  await lifecycle.acquirePidFile(shipyardHome);
89672
90487
  const pidTracker = buildPidTracker(shipyardHome);
89673
90488
  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();
90489
+ await pruneOldEpochData(join43(dataDir, "loro"), LEGACY_EPOCH, logAdapter);
90490
+ const storage = new FileStorageAdapter(join43(dataDir, "loro"));
90491
+ const personalWebrtcAdapter = new WebRtcDataChannelAdapter();
89677
90492
  const peerRoleRegistry = createPeerRoleRegistry();
90493
+ const presencePoolRef = { pool: {} };
89678
90494
  const repo = new Repo({
89679
90495
  identity: DAEMON_IDENTITY,
89680
- adapters: [storage, webrtcAdapter],
90496
+ adapters: [storage, personalWebrtcAdapter],
89681
90497
  permissions: buildCollabPermissions(peerRoleRegistry)
89682
90498
  });
89683
90499
  const resolvedSignalingUrl = env.SHIPYARD_SIGNALING_URL ?? auth3.signalingUrl;
@@ -89726,7 +90542,7 @@ async function serve(options = {}) {
89726
90542
  previewProxy
89727
90543
  });
89728
90544
  daemon.healthMetrics.start();
89729
- const pluginsDir = join42(shipyardHome, "plugins");
90545
+ const pluginsDir = join43(shipyardHome, "plugins");
89730
90546
  await mkdir18(pluginsDir, { recursive: true });
89731
90547
  let loadedPlugins = [];
89732
90548
  const pluginFileWatcher = new PluginFileWatcher({
@@ -89806,8 +90622,11 @@ async function serve(options = {}) {
89806
90622
  const fileIOHandlers = /* @__PURE__ */ new Set();
89807
90623
  const collabRoomManager = signalingHandle ? createCollabRoomManager({
89808
90624
  peerRoleRegistry,
89809
- createPeerManagerForRoom: (roomId, connection, collabTaskId) => createPeerManager({
89810
- webrtcAdapter,
90625
+ personalRepo: repo,
90626
+ planEpoch: PLAN_EPOCH,
90627
+ canvasEpoch: CANVAS_EPOCH,
90628
+ createPeerManagerForRoom: (roomId, connection, collabTaskId, collabWebrtcAdapter) => createPeerManager({
90629
+ webrtcAdapter: collabWebrtcAdapter,
89811
90630
  log: createChildLogger({ mode: `collab-peer:${roomId}` }),
89812
90631
  onAnswer(namespacedId, answer) {
89813
90632
  const prefix = `collab:${roomId}:`;
@@ -89847,7 +90666,9 @@ async function serve(options = {}) {
89847
90666
  peerRoleRegistry,
89848
90667
  peerMachineId: machineId,
89849
90668
  collabTaskId,
89850
- configStore: pluginConfigStore
90669
+ configStore: pluginConfigStore,
90670
+ previewProxy,
90671
+ presencePool: presencePoolRef
89851
90672
  });
89852
90673
  if (loadedPlugins.length > 0) {
89853
90674
  daemon.taskManager.broadcastControl({
@@ -90086,7 +90907,7 @@ async function serve(options = {}) {
90086
90907
  );
90087
90908
  }
90088
90909
  const peerManager = signalingHandle ? createPeerManager({
90089
- webrtcAdapter,
90910
+ webrtcAdapter: personalWebrtcAdapter,
90090
90911
  onAnswer: (targetMachineId, answer) => {
90091
90912
  signalingHandle.connection.send({
90092
90913
  type: "webrtc-answer",
@@ -90114,7 +90935,10 @@ async function serve(options = {}) {
90114
90935
  machineId: signalingHandle.machineId,
90115
90936
  signalingBaseUrl: auth3.signalingUrl,
90116
90937
  userToken: auth3.token,
90117
- configStore: pluginConfigStore
90938
+ configStore: pluginConfigStore,
90939
+ previewProxy,
90940
+ presencePool: presencePoolRef,
90941
+ peerIdentity: { userId: auth3.userId, username: auth3.displayName, avatarUrl: null }
90118
90942
  });
90119
90943
  portDetector.resend();
90120
90944
  if (loadedPlugins.length > 0) {
@@ -90347,8 +91171,24 @@ async function serve(options = {}) {
90347
91171
  }
90348
91172
  portDetector = createPortDetector({
90349
91173
  workspaceRoot,
90350
- onPorts: (ports) => {
90351
- daemon.taskManager.broadcastControl({ type: "detected_ports", ports });
91174
+ onPorts: async (ports) => {
91175
+ const firstPort = ports[0]?.port;
91176
+ if (firstPort && !previewProxy.port) {
91177
+ try {
91178
+ await previewProxy.start({ port: firstPort });
91179
+ } catch (err) {
91180
+ logAdapter({
91181
+ event: "preview_proxy_auto_start_failed",
91182
+ port: firstPort,
91183
+ error: err instanceof Error ? err.message : String(err)
91184
+ });
91185
+ }
91186
+ }
91187
+ daemon.taskManager.broadcastControl({
91188
+ type: "detected_ports",
91189
+ ports,
91190
+ proxyPort: previewProxy.port ?? void 0
91191
+ });
90352
91192
  },
90353
91193
  log: logAdapter
90354
91194
  });
@@ -90702,4 +91542,4 @@ export {
90702
91542
  classifyLogLevel,
90703
91543
  serve
90704
91544
  };
90705
- //# sourceMappingURL=serve-LL6LUMZI.js.map
91545
+ //# sourceMappingURL=serve-KBBJR56G.js.map