@schoolai/shipyard 3.2.1 → 3.2.2-nightly.20260422.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.
Files changed (32) hide show
  1. package/dist/{auth-W5HLP2KF.js → auth-LS3NBD42.js} +3 -3
  2. package/dist/{chunk-Q6ZPJJM4.js → chunk-67VJDX7G.js} +3 -3
  3. package/dist/{chunk-YOMKRHVO.js → chunk-CCW5QAUH.js} +2 -2
  4. package/dist/{chunk-3UYLOJ6E.js → chunk-DNIC3FOH.js} +3 -3
  5. package/dist/{chunk-3UYLOJ6E.js.map → chunk-DNIC3FOH.js.map} +1 -1
  6. package/dist/{chunk-IHSXN66C.js → chunk-GLH3V7NG.js} +2 -2
  7. package/dist/{chunk-JSUYB74F.js → chunk-JE3N5IWM.js} +5 -5
  8. package/dist/{chunk-CF336H24.js → chunk-M5M6VC5F.js} +2 -2
  9. package/dist/{chunk-CF336H24.js.map → chunk-M5M6VC5F.js.map} +1 -1
  10. package/dist/{chunk-FMMRZTOF.js → chunk-YZVF3NFD.js} +5 -2
  11. package/dist/chunk-YZVF3NFD.js.map +1 -0
  12. package/dist/index.js +8 -8
  13. package/dist/login-WPEVCQRE.js +19 -0
  14. package/dist/{logout-XYE7EJM7.js → logout-M7F7HXUU.js} +5 -5
  15. package/dist/{mcp-servers-XWOPKJ6F.js → mcp-servers-MUVTAMDT.js} +4 -4
  16. package/dist/{roi-YQ6OLVHX.js → roi-ZCVNBSTO.js} +3 -3
  17. package/dist/{serve-P5WC5JIT.js → serve-X4R5CDHD.js} +676 -288
  18. package/dist/{serve-P5WC5JIT.js.map → serve-X4R5CDHD.js.map} +1 -1
  19. package/dist/{start-2K7HFXHV.js → start-ARX3COMF.js} +8 -8
  20. package/package.json +2 -2
  21. package/dist/chunk-FMMRZTOF.js.map +0 -1
  22. package/dist/login-KSI4GTLM.js +0 -19
  23. /package/dist/{auth-W5HLP2KF.js.map → auth-LS3NBD42.js.map} +0 -0
  24. /package/dist/{chunk-Q6ZPJJM4.js.map → chunk-67VJDX7G.js.map} +0 -0
  25. /package/dist/{chunk-YOMKRHVO.js.map → chunk-CCW5QAUH.js.map} +0 -0
  26. /package/dist/{chunk-IHSXN66C.js.map → chunk-GLH3V7NG.js.map} +0 -0
  27. /package/dist/{chunk-JSUYB74F.js.map → chunk-JE3N5IWM.js.map} +0 -0
  28. /package/dist/{login-KSI4GTLM.js.map → login-WPEVCQRE.js.map} +0 -0
  29. /package/dist/{logout-XYE7EJM7.js.map → logout-M7F7HXUU.js.map} +0 -0
  30. /package/dist/{mcp-servers-XWOPKJ6F.js.map → mcp-servers-MUVTAMDT.js.map} +0 -0
  31. /package/dist/{roi-YQ6OLVHX.js.map → roi-ZCVNBSTO.js.map} +0 -0
  32. /package/dist/{start-2K7HFXHV.js.map → start-ARX3COMF.js.map} +0 -0
@@ -47,11 +47,11 @@ import {
47
47
  VaultKeyPutRequestSchema,
48
48
  VaultKeyPutResponseSchema,
49
49
  classifyClaudeCodeCompatibility
50
- } from "./chunk-FMMRZTOF.js";
50
+ } from "./chunk-YZVF3NFD.js";
51
51
  import "./chunk-EHQITHQX.js";
52
52
  import {
53
53
  loadAuthToken
54
- } from "./chunk-IHSXN66C.js";
54
+ } from "./chunk-GLH3V7NG.js";
55
55
  import {
56
56
  DiscoveryStateSchema,
57
57
  detectMCPServers,
@@ -62,19 +62,19 @@ import {
62
62
  redactEnv,
63
63
  resolveEnabledMcpServers,
64
64
  resolveStdioEnv
65
- } from "./chunk-Q6ZPJJM4.js";
65
+ } from "./chunk-67VJDX7G.js";
66
66
  import {
67
67
  createChildLogger,
68
68
  flushLogger,
69
69
  logger
70
- } from "./chunk-3UYLOJ6E.js";
70
+ } from "./chunk-DNIC3FOH.js";
71
71
  import {
72
72
  external_exports,
73
73
  getShipyardHome,
74
74
  isVanillaAgentMode,
75
75
  toJSONSchema,
76
76
  validateEnv
77
- } from "./chunk-CF336H24.js";
77
+ } from "./chunk-M5M6VC5F.js";
78
78
  import {
79
79
  detectSkills
80
80
  } from "./chunk-DPMRSLYJ.js";
@@ -87,7 +87,7 @@ import { mkdir as mkdir24, realpath as realpath2 } from "fs/promises";
87
87
  import { join as join55 } from "path";
88
88
  import { query as query2 } from "@anthropic-ai/claude-agent-sdk";
89
89
 
90
- // ../../node_modules/.pnpm/@loro-extended+change@6.0.0-beta.0_loro-crdt@1.10.6/node_modules/@loro-extended/change/dist/index.js
90
+ // ../../node_modules/.pnpm/@loro-extended+change@6.0.0-beta.0_loro-crdt@1.11.1/node_modules/@loro-extended/change/dist/index.js
91
91
  import { LoroDoc } from "loro-crdt";
92
92
  import { isContainer, isContainerId } from "loro-crdt";
93
93
  import {
@@ -5226,7 +5226,7 @@ function getImplicitContext() {
5226
5226
  return result;
5227
5227
  }
5228
5228
 
5229
- // ../../node_modules/.pnpm/@loro-extended+repo@6.0.0-beta.0_loro-crdt@1.10.6/node_modules/@loro-extended/repo/dist/index.js
5229
+ // ../../node_modules/.pnpm/@loro-extended+repo@6.0.0-beta.0_loro-crdt@1.11.1/node_modules/@loro-extended/repo/dist/index.js
5230
5230
  import { LoroDoc as LoroDoc2 } from "loro-crdt";
5231
5231
  import { VersionVector } from "loro-crdt";
5232
5232
  import { VersionVector as VersionVector2 } from "loro-crdt";
@@ -6941,7 +6941,7 @@ var makeCreator = (arg) => {
6941
6941
  var create = makeCreator();
6942
6942
  var constructorString = Object.prototype.constructor.toString();
6943
6943
 
6944
- // ../../node_modules/.pnpm/@loro-extended+repo@6.0.0-beta.0_loro-crdt@1.10.6/node_modules/@loro-extended/repo/dist/index.js
6944
+ // ../../node_modules/.pnpm/@loro-extended+repo@6.0.0-beta.0_loro-crdt@1.11.1/node_modules/@loro-extended/repo/dist/index.js
6945
6945
  import { EphemeralStore } from "loro-crdt";
6946
6946
  import {
6947
6947
  LoroDoc as LoroDoc22
@@ -12461,7 +12461,7 @@ function encodeCBOR(data) {
12461
12461
  return output;
12462
12462
  }
12463
12463
 
12464
- // ../../node_modules/.pnpm/@loro-extended+wire-format@0.1.1-beta.0_loro-crdt@1.10.6/node_modules/@loro-extended/wire-format/dist/index.js
12464
+ // ../../node_modules/.pnpm/@loro-extended+wire-format@0.1.1-beta.0_loro-crdt@1.11.1/node_modules/@loro-extended/wire-format/dist/index.js
12465
12465
  import { VersionVector as VersionVector4 } from "loro-crdt";
12466
12466
  var WIRE_VERSION = 2;
12467
12467
  var HEADER_SIZE = 6;
@@ -13249,7 +13249,7 @@ var FragmentReassembler = class {
13249
13249
  }
13250
13250
  };
13251
13251
 
13252
- // ../../node_modules/.pnpm/@loro-extended+adapter-webrtc@6.0.0-beta.0_loro-crdt@1.10.6/node_modules/@loro-extended/adapter-webrtc/dist/index.js
13252
+ // ../../node_modules/.pnpm/@loro-extended+adapter-webrtc@6.0.0-beta.0_loro-crdt@1.11.1/node_modules/@loro-extended/adapter-webrtc/dist/index.js
13253
13253
  var DEFAULT_FRAGMENT_THRESHOLD = 200 * 1024;
13254
13254
  var WebRtcDataChannelAdapter = class extends Adapter {
13255
13255
  /**
@@ -29036,7 +29036,7 @@ function resolveTaskIdFromResult(resultText) {
29036
29036
  return match2?.[1] ?? null;
29037
29037
  }
29038
29038
  function ccFileToStructuredTask(file) {
29039
- const now = Date.now();
29039
+ const createdAt = file.createdAt ?? 0;
29040
29040
  return {
29041
29041
  id: file.id,
29042
29042
  subject: file.subject,
@@ -29046,8 +29046,8 @@ function ccFileToStructuredTask(file) {
29046
29046
  owner: file.owner,
29047
29047
  blocks: file.blocks,
29048
29048
  blockedBy: file.blockedBy,
29049
- createdAt: file.createdAt ?? now,
29050
- updatedAt: file.updatedAt ?? now
29049
+ createdAt,
29050
+ updatedAt: file.updatedAt ?? createdAt
29051
29051
  };
29052
29052
  }
29053
29053
  var CC_STATUS_MAP = {
@@ -29719,8 +29719,12 @@ async function detectModels() {
29719
29719
  try {
29720
29720
  await run("which", ["claude"]);
29721
29721
  const supported = getSupportedEfforts();
29722
- const filter = (efforts) => efforts.filter((e) => supported.has(e));
29723
- const pickDefault = (desired, efforts) => efforts.includes(desired) ? desired : efforts[efforts.length - 1] ?? "low";
29722
+ const filter = (efforts) => efforts.filter((e) => e === "auto" || supported.has(e));
29723
+ const pickDefault = (desired, efforts) => {
29724
+ if (efforts.includes(desired)) return desired;
29725
+ const concrete = efforts.filter((e) => e !== "auto");
29726
+ return concrete[concrete.length - 1] ?? "low";
29727
+ };
29724
29728
  const opusEfforts = filter(["low", "medium", "high", "xhigh", "max", "auto"]);
29725
29729
  const sonnetEfforts = filter(["low", "medium", "high", "max", "auto"]);
29726
29730
  models.push(
@@ -31450,7 +31454,7 @@ function nanoid(size2 = 21) {
31450
31454
  }
31451
31455
 
31452
31456
  // src/services/bootstrap/signaling.ts
31453
- var DAEMON_NPM_VERSION = true ? "3.2.1" : "unknown";
31457
+ var DAEMON_NPM_VERSION = true ? "3.2.2" : "unknown";
31454
31458
  function createDaemonSignaling(config2) {
31455
31459
  const agentId = config2.agentId ?? nanoid();
31456
31460
  function send(msg) {
@@ -37227,12 +37231,19 @@ var SANDBOX_COLOR_PALETTE = `## Color Palette
37227
37231
  - Group by **category**, not sequence. All items of one type share one ramp.
37228
37232
  - Gray for neutral/structural elements.
37229
37233
  - Reserve blue/green/red for semantic meaning (info, success, danger). Use teal, amber, coral for general categories.
37230
- - When placing text on a colored background (badges, pills, cards, tags), use the darkest stop from that same color ramp for the text \u2014 never plain black or generic gray.
37234
+ - When placing text on a colored background (badges, pills, cards, tags), use a stop from the far end of the same ramp: on a light fill (stop 50) use stop 800 for text; on a dark fill (stop 800) use stop 50 or 200 for text. Never plain black or generic gray.
37231
37235
 
37232
37236
  **Light/dark mode stop assignment** \u2014 use only stops from the table, never off-table hex values:
37233
37237
  - **Light mode**: 50 fill + 600 stroke + **800 title / 600 subtitle**
37234
- - **Dark mode**: 800 fill + 200 stroke + **200 title / 400 subtitle**
37238
+ - **Dark mode**: 800 fill + 200 stroke + **50 title / 200 subtitle**
37235
37239
  - Title and subtitle MUST use different stops \u2014 same stop reads flat even with different font weights. When a box has both a title and subtitle, the weight difference alone is not enough; you need two distinct color stops for visual hierarchy.
37240
+ - Never use stop 400 as body text in either mode \u2014 it's a mid-tone that fails WCAG AA (~3:1) on near-white or near-black page backgrounds. Stop 400 is fine as a fill or as text on a 50/200 background; just not as text on the page bg.
37241
+ - Stop 600 is tuned for light-mode subtitles on 50 fills \u2014 do not reach for it as text in dark mode, where it approaches the 800 fill and reads muddy.
37242
+ - For muted text, always use \`var(--text-muted)\` \u2014 never a hand-picked gray hex (e.g. gray-400/600) as a muted substitute.
37243
+
37244
+ **Container contrast on dark mode** \u2014 an 800-fill box on a near-black page bg nearly disappears (gray-800 on gray-950 reads at ~1.8:1 container-vs-bg). When placing 800-fill boxes in dark mode, do ONE of:
37245
+ - Add a stroke using stop 200 from the same ramp (recommended \u2014 the 200 stroke is luminous enough to define the container edge).
37246
+ - Place the box on \`var(--bg-surface)\` or \`var(--bg-overlay)\` instead of the raw page bg.
37236
37247
 
37237
37248
  **Dark mode is mandatory** \u2014 mental test: if the background were near-black, would every text element still be readable? Always use \`var(--text)\` or \`var(--text-muted)\` for text, never hardcoded dark colors.`;
37238
37249
  var SANDBOX_ANTI_SLOP = `## Design Quality
@@ -50290,7 +50301,7 @@ var PluginKey = class {
50290
50301
  // src/services/plan/plan-content-bridge.ts
50291
50302
  import { LoroMap as LoroMap4 } from "loro-crdt";
50292
50303
 
50293
- // ../../node_modules/.pnpm/loro-prosemirror@0.4.2_loro-crdt@1.10.6_prosemirror-model@1.25.4_prosemirror-state@1.4.4_prosemirror-view@1.41.6/node_modules/loro-prosemirror/dist/index.mjs
50304
+ // ../../node_modules/.pnpm/loro-prosemirror@0.4.2_loro-crdt@1.11.1_prosemirror-model@1.25.4_prosemirror-state@1.4.4_prosemirror-view@1.41.6/node_modules/loro-prosemirror/dist/index.mjs
50294
50305
  import { LoroMap as LoroMap3, LoroText as LoroText3, LoroList as LoroList3, isContainer as isContainer2, EphemeralStore as EphemeralStore2, Cursor, Awareness, UndoManager } from "loro-crdt";
50295
50306
 
50296
50307
  // ../../node_modules/.pnpm/prosemirror-view@1.41.6/node_modules/prosemirror-view/dist/index.js
@@ -55621,7 +55632,7 @@ var simpleDiffString = (a, b2) => {
55621
55632
  };
55622
55633
  var simpleDiff = simpleDiffString;
55623
55634
 
55624
- // ../../node_modules/.pnpm/loro-prosemirror@0.4.2_loro-crdt@1.10.6_prosemirror-model@1.25.4_prosemirror-state@1.4.4_prosemirror-view@1.41.6/node_modules/loro-prosemirror/dist/index.mjs
55635
+ // ../../node_modules/.pnpm/loro-prosemirror@0.4.2_loro-crdt@1.11.1_prosemirror-model@1.25.4_prosemirror-state@1.4.4_prosemirror-view@1.41.6/node_modules/loro-prosemirror/dist/index.mjs
55625
55636
  var ROOT_DOC_KEY = "doc";
55626
55637
  var ATTRIBUTES_KEY = "attributes";
55627
55638
  var CHILDREN_KEY = "children";
@@ -72907,10 +72918,27 @@ function hasErrorCode(err, code2) {
72907
72918
  const record = err;
72908
72919
  return record.code === code2;
72909
72920
  }
72921
+ function isCliFlagEffort(effort) {
72922
+ switch (effort) {
72923
+ case "low":
72924
+ case "medium":
72925
+ case "high":
72926
+ case "xhigh":
72927
+ case "max":
72928
+ return true;
72929
+ case "auto":
72930
+ return false;
72931
+ default: {
72932
+ const _exhaustive = effort;
72933
+ return _exhaustive;
72934
+ }
72935
+ }
72936
+ }
72910
72937
  var FALLBACK_PRIORITY = ["high", "max", "medium", "low"];
72911
72938
  var warnedEffortCoercions = /* @__PURE__ */ new Set();
72912
72939
  function resolveSupportedEffort(effort, log) {
72913
72940
  if (!effort) return void 0;
72941
+ if (!isCliFlagEffort(effort)) return void 0;
72914
72942
  const supported = getSupportedEfforts();
72915
72943
  if (supported.has(effort)) return effort;
72916
72944
  const fallback = FALLBACK_PRIORITY.find((e) => supported.has(e));
@@ -85417,7 +85445,7 @@ var PlanHandler = class {
85417
85445
  this.#deps.enqueueAsync(async () => {
85418
85446
  try {
85419
85447
  await this.#preparePlanForResolution(toolUseId, opts?.comments ?? []);
85420
- await this.#applyPermissionMode(opts?.permissionMode);
85448
+ this.#applyPermissionMode(opts?.permissionMode);
85421
85449
  } catch (err) {
85422
85450
  this.#deps.log({
85423
85451
  event: "plan_continue_preparation_failed",
@@ -85682,14 +85710,9 @@ var PlanHandler = class {
85682
85710
  });
85683
85711
  });
85684
85712
  }
85685
- async #applyPermissionMode(permissionMode) {
85713
+ #applyPermissionMode(permissionMode) {
85686
85714
  if (!permissionMode) return;
85687
- this.#deps.setLatestSettings({ permissionMode });
85688
- await this.#deps.getSubprocess()?.setPermissionMode(permissionMode);
85689
- this.#deps.getSendControlMessage()?.({
85690
- type: "settings_ack",
85691
- settings: { permissionMode }
85692
- });
85715
+ this.#deps.applyTaskSettings({ permissionMode });
85693
85716
  }
85694
85717
  async #injectDecisionMessage(decision, feedback) {
85695
85718
  const reviewer = this.#deps.getHumanParticipantName();
@@ -86274,6 +86297,22 @@ function planCompactionSnapshot(input) {
86274
86297
  }
86275
86298
 
86276
86299
  // src/services/task/resource-push-manager.ts
86300
+ async function writeResourceSynthetics(deps, messages) {
86301
+ const allContent = [];
86302
+ for (const msg of messages) {
86303
+ await deps.store.appendMessage({
86304
+ channelId: deps.channelId,
86305
+ messageId: crypto.randomUUID(),
86306
+ participantId: deps.humanParticipantId,
86307
+ senderKind: "human",
86308
+ content: msg.content,
86309
+ timestamp: Date.now(),
86310
+ isSynthetic: true
86311
+ });
86312
+ for (const block2 of msg.content) allContent.push(block2);
86313
+ }
86314
+ return allContent;
86315
+ }
86277
86316
  var MAX_PUSHES_PER_TURN = 3;
86278
86317
  var ResourcePushManager = class {
86279
86318
  #deps;
@@ -86424,20 +86463,14 @@ var ResourcePushManager = class {
86424
86463
  });
86425
86464
  }
86426
86465
  async #writeSynthetics(messages) {
86427
- const allContent = [];
86428
- for (const msg of messages) {
86429
- await this.#deps.store.appendMessage({
86466
+ return writeResourceSynthetics(
86467
+ {
86468
+ store: this.#deps.store,
86430
86469
  channelId: this.#deps.channelId,
86431
- messageId: crypto.randomUUID(),
86432
- participantId: this.#deps.humanParticipantId,
86433
- senderKind: "human",
86434
- content: msg.content,
86435
- timestamp: Date.now(),
86436
- isSynthetic: true
86437
- });
86438
- for (const block2 of msg.content) allContent.push(block2);
86439
- }
86440
- return allContent;
86470
+ humanParticipantId: this.#deps.humanParticipantId
86471
+ },
86472
+ messages
86473
+ );
86441
86474
  }
86442
86475
  #deliverToSubprocess(allContent, uriCount) {
86443
86476
  const state = this.#deps.getState();
@@ -87284,6 +87317,9 @@ var SideThreadRegistry = class {
87284
87317
  );
87285
87318
  entry.thread = thread;
87286
87319
  this.#threads.set(params.threadId, entry);
87320
+ if (params.initialPermissionMode !== void 0) {
87321
+ thread.applyPermissionMode(params.initialPermissionMode);
87322
+ }
87287
87323
  this.#persist().catch((err) => {
87288
87324
  const errMsg = err instanceof Error ? err.message : String(err);
87289
87325
  this.#deps.log({
@@ -88002,7 +88038,7 @@ async function removeStaleFiles(dir, targetIds, hashes) {
88002
88038
  }
88003
88039
  for (const entry of entries) {
88004
88040
  if (!entry.endsWith(".json")) continue;
88005
- const id = entry.replace(".json", "");
88041
+ const id = entry.slice(0, -".json".length);
88006
88042
  if (!targetIds.has(id) && hashes.has(id)) {
88007
88043
  await unlink7(join41(dir, entry)).catch(() => {
88008
88044
  });
@@ -88010,14 +88046,15 @@ async function removeStaleFiles(dir, targetIds, hashes) {
88010
88046
  }
88011
88047
  }
88012
88048
  }
88013
- function createCCTaskFileWriter(dir, log) {
88049
+ function createCCTaskFileWriter(initialDir, log) {
88014
88050
  const lastWrittenHashes = /* @__PURE__ */ new Map();
88051
+ let currentDir = initialDir;
88015
88052
  let debounceTimer = null;
88016
88053
  let pendingTasks = null;
88017
88054
  let disposed = false;
88018
88055
  let flushInProgress = Promise.resolve();
88019
88056
  const DEBOUNCE_MS3 = 200;
88020
- async function flush(tasks) {
88057
+ async function flush(dir, tasks) {
88021
88058
  if (disposed) return;
88022
88059
  try {
88023
88060
  await mkdir17(dir, { recursive: true });
@@ -88033,7 +88070,7 @@ function createCCTaskFileWriter(dir, log) {
88033
88070
  });
88034
88071
  }
88035
88072
  }
88036
- function scheduleFlush(tasks) {
88073
+ function scheduleFlush(dir, tasks) {
88037
88074
  pendingTasks = tasks;
88038
88075
  if (debounceTimer) clearTimeout(debounceTimer);
88039
88076
  debounceTimer = setTimeout(() => {
@@ -88041,19 +88078,42 @@ function createCCTaskFileWriter(dir, log) {
88041
88078
  const snapshot = pendingTasks;
88042
88079
  pendingTasks = null;
88043
88080
  if (snapshot) {
88044
- flushInProgress = flushInProgress.then(() => flush(snapshot));
88081
+ flushInProgress = flushInProgress.then(() => flush(dir, snapshot));
88045
88082
  }
88046
88083
  }, DEBOUNCE_MS3);
88047
88084
  }
88048
88085
  return {
88049
- dir,
88086
+ get dir() {
88087
+ return currentDir;
88088
+ },
88050
88089
  writeMergedTasks(tasks) {
88051
88090
  if (disposed) return;
88052
- scheduleFlush(tasks);
88091
+ if (currentDir === null) {
88092
+ pendingTasks = tasks;
88093
+ return;
88094
+ }
88095
+ scheduleFlush(currentDir, tasks);
88096
+ },
88097
+ async setDir(dir) {
88098
+ if (disposed) return;
88099
+ if (currentDir !== null) return;
88100
+ currentDir = dir;
88101
+ if (debounceTimer) {
88102
+ clearTimeout(debounceTimer);
88103
+ debounceTimer = null;
88104
+ }
88105
+ const snapshot = pendingTasks;
88106
+ pendingTasks = null;
88107
+ if (snapshot) {
88108
+ const flushPromise = flush(dir, snapshot);
88109
+ flushInProgress = flushInProgress.then(() => flushPromise);
88110
+ await flushPromise;
88111
+ }
88053
88112
  },
88054
88113
  dispose() {
88055
88114
  disposed = true;
88056
88115
  if (debounceTimer) clearTimeout(debounceTimer);
88116
+ debounceTimer = null;
88057
88117
  pendingTasks = null;
88058
88118
  }
88059
88119
  };
@@ -88105,6 +88165,39 @@ function applyDepEdgeAdditions(merged, depEdges) {
88105
88165
  }
88106
88166
  }
88107
88167
  }
88168
+ function applyOverlayToMap(base3, overlay) {
88169
+ const merged = new Map(base3);
88170
+ for (const userTask of overlay.userTasks) {
88171
+ const baseTask = merged.get(userTask.id);
88172
+ if (!baseTask || baseTask.updatedAt <= userTask.updatedAt) {
88173
+ merged.set(userTask.id, userTask);
88174
+ }
88175
+ }
88176
+ for (const id of overlay.removedIds) merged.delete(id);
88177
+ for (const [id, status] of Object.entries(overlay.statusOverrides)) {
88178
+ const task = merged.get(id);
88179
+ if (task) merged.set(id, { ...task, status });
88180
+ }
88181
+ applyDepEdgeRemovals(merged, overlay.removedDepEdges);
88182
+ applyDepEdgeAdditions(merged, overlay.depEdges);
88183
+ return merged;
88184
+ }
88185
+ function computeTodoProgress(tasks) {
88186
+ if (tasks.size === 0) return void 0;
88187
+ let completed = 0;
88188
+ let currentActivity = null;
88189
+ for (const task of tasks.values()) {
88190
+ if (task.status === "completed" || task.status === "cancelled") completed++;
88191
+ if (task.status === "in_progress" && !currentActivity) {
88192
+ currentActivity = task.activeForm ?? task.subject;
88193
+ }
88194
+ }
88195
+ return {
88196
+ todoCompleted: completed,
88197
+ todoTotal: tasks.size,
88198
+ currentActivity
88199
+ };
88200
+ }
88108
88201
  var StructuredTaskTracker = class {
88109
88202
  #deps;
88110
88203
  #structuredTasks = /* @__PURE__ */ new Map();
@@ -88114,9 +88207,12 @@ var StructuredTaskTracker = class {
88114
88207
  #currentOverlay = null;
88115
88208
  #ccTaskWatcherDispose = null;
88116
88209
  #suppressWriteThrough = false;
88117
- #ccTaskFileWriter = null;
88210
+ #ccTaskFileWriter;
88211
+ #restoreInProgress = false;
88118
88212
  constructor(deps) {
88119
88213
  this.#deps = deps;
88214
+ this.#ccTaskFileWriter = createCCTaskFileWriter(null, deps.log);
88215
+ this.#restoreInProgress = deps.restoreInProgress ?? false;
88120
88216
  }
88121
88217
  get currentOverlay() {
88122
88218
  return this.#currentOverlay;
@@ -88124,8 +88220,7 @@ var StructuredTaskTracker = class {
88124
88220
  dispose() {
88125
88221
  this.#ccTaskWatcherDispose?.();
88126
88222
  this.#ccTaskWatcherDispose = null;
88127
- this.#ccTaskFileWriter?.dispose();
88128
- this.#ccTaskFileWriter = null;
88223
+ this.#ccTaskFileWriter.dispose();
88129
88224
  }
88130
88225
  clearInMemoryTasks() {
88131
88226
  this.#structuredTasks.clear();
@@ -88136,14 +88231,14 @@ var StructuredTaskTracker = class {
88136
88231
  detachFileWatcher() {
88137
88232
  this.#ccTaskWatcherDispose?.();
88138
88233
  this.#ccTaskWatcherDispose = null;
88139
- this.#ccTaskFileWriter?.dispose();
88140
- this.#ccTaskFileWriter = null;
88234
+ this.#ccTaskFileWriter.dispose();
88235
+ this.#ccTaskFileWriter = createCCTaskFileWriter(null, this.#deps.log);
88141
88236
  }
88142
88237
  /**
88143
88238
  * Attach a file watcher for CC task files. Called after init_received
88144
88239
  * when we know the CC session ID, or immediately for resumed sessions.
88145
88240
  */
88146
- attachFileWatcher(sessionId) {
88241
+ async attachFileWatcher(sessionId) {
88147
88242
  if (this.#ccTaskWatcherDispose) {
88148
88243
  this.#deps.log({
88149
88244
  event: "file_watcher_already_attached",
@@ -88153,7 +88248,7 @@ var StructuredTaskTracker = class {
88153
88248
  return;
88154
88249
  }
88155
88250
  const watcher = createCCTaskFileWatcher(sessionId, this.#deps.log);
88156
- this.#ccTaskFileWriter = createCCTaskFileWriter(watcher.dir, this.#deps.log);
88251
+ await this.#ccTaskFileWriter.setDir(watcher.dir);
88157
88252
  this.#initFileWatcher(watcher);
88158
88253
  if (this.#currentOverlay) {
88159
88254
  this.#flushStructuredTasks();
@@ -88161,9 +88256,24 @@ var StructuredTaskTracker = class {
88161
88256
  }
88162
88257
  /**
88163
88258
  * Apply an overlay on top of CC tasks. Stores the overlay and re-flushes.
88259
+ * Exits restore mode — applyOverlay is the terminal piece of restoration
88260
+ * (disk reconcile is idempotent with the seeded overlay, so we flush once
88261
+ * here with full inputs instead of producing a partial pre-overlay flush).
88164
88262
  */
88165
88263
  applyOverlay(overlay) {
88166
88264
  this.#currentOverlay = overlay;
88265
+ this.#restoreInProgress = false;
88266
+ this.#flushStructuredTasks();
88267
+ }
88268
+ /**
88269
+ * Signal that restoration has finished for tasks that have no persisted
88270
+ * overlay to apply. Unblocks #flushStructuredTasks and runs one flush with
88271
+ * the current state (disk-only, no overlay). Must be called by the
88272
+ * restoration caller whenever applyOverlay will not be invoked.
88273
+ */
88274
+ markRestoreComplete() {
88275
+ if (!this.#restoreInProgress) return;
88276
+ this.#restoreInProgress = false;
88167
88277
  this.#flushStructuredTasks();
88168
88278
  }
88169
88279
  processStructuredTaskEvents(content) {
@@ -88272,48 +88382,16 @@ var StructuredTaskTracker = class {
88272
88382
  * 6. Compute todo progress from the merged result
88273
88383
  * 7. Push to updateStructuredTasks
88274
88384
  */
88275
- #applyOverlayToMap(base3, overlay) {
88276
- const merged = new Map(base3);
88277
- for (const userTask of overlay.userTasks) {
88278
- const baseTask = merged.get(userTask.id);
88279
- if (!baseTask || baseTask.updatedAt <= userTask.updatedAt) {
88280
- merged.set(userTask.id, userTask);
88281
- }
88282
- }
88283
- for (const id of overlay.removedIds) merged.delete(id);
88284
- for (const [id, status] of Object.entries(overlay.statusOverrides)) {
88285
- const task = merged.get(id);
88286
- if (task) merged.set(id, { ...task, status, updatedAt: Date.now() });
88287
- }
88288
- applyDepEdgeRemovals(merged, overlay.removedDepEdges);
88289
- applyDepEdgeAdditions(merged, overlay.depEdges);
88290
- return merged;
88291
- }
88292
88385
  #flushStructuredTasks() {
88386
+ if (this.#restoreInProgress) return;
88293
88387
  const overlay = this.#currentOverlay ?? DEFAULT_TASK_OVERLAY;
88294
- const merged = this.#applyOverlayToMap(this.#structuredTasks, overlay);
88295
- const todoProgress = this.#computeTodoProgress(merged);
88388
+ const merged = applyOverlayToMap(this.#structuredTasks, overlay);
88389
+ const todoProgress = computeTodoProgress(merged);
88296
88390
  this.#deps.updateStructuredTasks(Object.fromEntries(merged), todoProgress);
88297
88391
  if (!this.#suppressWriteThrough) {
88298
- this.#ccTaskFileWriter?.writeMergedTasks(merged);
88392
+ this.#ccTaskFileWriter.writeMergedTasks(merged);
88299
88393
  }
88300
88394
  }
88301
- #computeTodoProgress(tasks) {
88302
- if (tasks.size === 0) return void 0;
88303
- let completed = 0;
88304
- let currentActivity = null;
88305
- for (const task of tasks.values()) {
88306
- if (task.status === "completed" || task.status === "cancelled") completed++;
88307
- if (task.status === "in_progress" && !currentActivity) {
88308
- currentActivity = task.activeForm ?? task.subject;
88309
- }
88310
- }
88311
- return {
88312
- todoCompleted: completed,
88313
- todoTotal: tasks.size,
88314
- currentActivity
88315
- };
88316
- }
88317
88395
  #initFileWatcher(watcher) {
88318
88396
  this.#ccTaskWatcherDispose = watcher.watch((tasks) => {
88319
88397
  this.#reconcileFromDisk(tasks);
@@ -89509,7 +89587,8 @@ var Task = class {
89509
89587
  this.#structuredTaskTracker = new StructuredTaskTracker({
89510
89588
  taskId: deps.taskId,
89511
89589
  log: deps.log,
89512
- updateStructuredTasks: deps.updateStructuredTasks
89590
+ updateStructuredTasks: deps.updateStructuredTasks,
89591
+ restoreInProgress: deps.restoreInProgress ?? false
89513
89592
  });
89514
89593
  this.#pushManager = new ResourcePushManager({
89515
89594
  taskId: deps.taskId,
@@ -89545,7 +89624,15 @@ var Task = class {
89545
89624
  queueKey: mainQueueKey(deps.taskId)
89546
89625
  });
89547
89626
  if (deps.existingSessionId) {
89548
- this.#structuredTaskTracker.attachFileWatcher(deps.existingSessionId);
89627
+ const sessionIdForLog = deps.existingSessionId;
89628
+ this.#structuredTaskTracker.attachFileWatcher(sessionIdForLog).catch((err) => {
89629
+ deps.log({
89630
+ event: "attach_file_watcher_failed",
89631
+ taskId: deps.taskId,
89632
+ sessionId: sessionIdForLog,
89633
+ error: err instanceof Error ? err.message : String(err)
89634
+ });
89635
+ });
89549
89636
  }
89550
89637
  this.#subagentManager = new SubagentManager({
89551
89638
  taskId: deps.taskId,
@@ -89578,10 +89665,6 @@ var Task = class {
89578
89665
  humanParticipantId: deps.humanParticipantId,
89579
89666
  planRepo: deps.planRepo,
89580
89667
  annotationStore: deps.annotationStore,
89581
- /** Delegate to Thread's encapsulated setPermissionMode */
89582
- getSubprocess: () => ({
89583
- setPermissionMode: (mode) => this.#mainThread?.setSubprocessPermissionMode(mode) ?? Promise.resolve()
89584
- }),
89585
89668
  getSendControlMessage: () => this.#broadcastToAllPeers,
89586
89669
  /**
89587
89670
  * Route PlanHandler messages through the permission queue so they
@@ -89599,12 +89682,7 @@ var Task = class {
89599
89682
  log: deps.log,
89600
89683
  metricsCollector: deps.metricsCollector,
89601
89684
  getLatestSettings: () => this.#latestSettings,
89602
- setLatestSettings: (patch) => {
89603
- this.#latestSettings = { ...this.#latestSettings, ...patch };
89604
- if (patch.permissionMode) {
89605
- this.#mainThread?.applyPermissionMode(patch.permissionMode);
89606
- }
89607
- },
89685
+ applyTaskSettings: (settings) => this.#deps.applyTaskSettings(settings),
89608
89686
  pushSyntheticMessage: (content) => {
89609
89687
  this.#mainThread?.pushSyntheticMessage(content);
89610
89688
  },
@@ -90039,19 +90117,35 @@ var Task = class {
90039
90117
  return count + collabCount;
90040
90118
  }
90041
90119
  applyPermissionMode(mode) {
90042
- this.#latestSettings = {
90043
- ...this.#latestSettings,
90044
- permissionMode: mode
90045
- };
90046
- this.#mainThread.applyPermissionMode(mode);
90120
+ this.applySettings({ permissionMode: mode });
90047
90121
  }
90048
90122
  applySettings(settings) {
90049
90123
  this.#latestSettings = { ...this.#latestSettings, ...settings };
90050
- if (settings.permissionMode) {
90051
- this.#mainThread.applyPermissionMode(settings.permissionMode);
90124
+ if (settings.permissionMode !== void 0) {
90125
+ const mode = settings.permissionMode;
90126
+ this.#mainThread.applyPermissionMode(mode);
90127
+ this.#propagateToSideThreads(
90128
+ (t) => t.applyPermissionMode(mode),
90129
+ "side_thread_permission_mode_failed"
90130
+ );
90131
+ }
90132
+ if (settings.model !== void 0) {
90133
+ const model = settings.model;
90134
+ this.#mainThread.setModel(model);
90135
+ this.#propagateToSideThreads((t) => t.setModel(model), "side_thread_model_failed");
90052
90136
  }
90053
- if (settings.model) {
90054
- this.#mainThread.setModel(settings.model);
90137
+ }
90138
+ #propagateToSideThreads(action, event) {
90139
+ for (const thread of this.#sideThreads.liveThreads()) {
90140
+ try {
90141
+ action(thread);
90142
+ } catch (err) {
90143
+ this.#deps.log({
90144
+ event,
90145
+ taskId: this.#deps.taskId,
90146
+ error: err instanceof Error ? err.message : String(err)
90147
+ });
90148
+ }
90055
90149
  }
90056
90150
  }
90057
90151
  async setMcpServers(servers) {
@@ -90505,7 +90599,10 @@ var Task = class {
90505
90599
  return this.#sideThreads.getMetadata(threadId);
90506
90600
  }
90507
90601
  createSideThread(params) {
90508
- return this.#sideThreads.create(params);
90602
+ return this.#sideThreads.create({
90603
+ ...params,
90604
+ initialPermissionMode: params.initialPermissionMode ?? this.#latestSettings.permissionMode
90605
+ });
90509
90606
  }
90510
90607
  async listSideThreads() {
90511
90608
  return this.#sideThreads.list();
@@ -90676,13 +90773,21 @@ Use this context to maintain continuity. You have already done this work \u2014
90676
90773
  try {
90677
90774
  const taskResource = await registry.resolve(taskUri);
90678
90775
  if ("text" in taskResource && !taskResource.text.includes('task-count="0"')) {
90679
- this.#pushManager.markPushed(taskUri);
90680
90776
  const rootMsg = buildRootMessage(
90681
90777
  taskUri,
90682
90778
  taskResource,
90683
90779
  "Task Plan",
90684
90780
  (/* @__PURE__ */ new Date()).toISOString()
90685
90781
  );
90782
+ await writeResourceSynthetics(
90783
+ {
90784
+ store: this.#deps.store,
90785
+ channelId: this.#deps.channelId,
90786
+ humanParticipantId: this.#deps.humanParticipantId
90787
+ },
90788
+ [rootMsg]
90789
+ );
90790
+ this.#pushManager.markPushed(taskUri);
90686
90791
  return rootMsg.content;
90687
90792
  }
90688
90793
  } catch {
@@ -90798,7 +90903,14 @@ Use this context to maintain continuity. You have already done this work \u2014
90798
90903
  {
90799
90904
  const sessionId = this.#mainThread.sessionId;
90800
90905
  if (sessionId) {
90801
- this.#structuredTaskTracker.attachFileWatcher(sessionId);
90906
+ this.#structuredTaskTracker.attachFileWatcher(sessionId).catch((err) => {
90907
+ this.#deps.log({
90908
+ event: "attach_file_watcher_failed",
90909
+ taskId: this.#deps.taskId,
90910
+ sessionId,
90911
+ error: err instanceof Error ? err.message : String(err)
90912
+ });
90913
+ });
90802
90914
  }
90803
90915
  }
90804
90916
  this.#pushManager.onTurnStart();
@@ -91162,6 +91274,10 @@ Use this context to maintain continuity. You have already done this work \u2014
91162
91274
  applyOverlay(overlay) {
91163
91275
  this.#structuredTaskTracker.applyOverlay(overlay);
91164
91276
  }
91277
+ /** See StructuredTaskTracker.markRestoreComplete. */
91278
+ markStructuredTaskRestoreComplete() {
91279
+ this.#structuredTaskTracker.markRestoreComplete();
91280
+ }
91165
91281
  /** ---------------------------------------------------------------- */
91166
91282
  /** Resource resolution */
91167
91283
  /** ---------------------------------------------------------------- */
@@ -91643,6 +91759,7 @@ function buildInitialOverlayFromTemplate(template, now) {
91643
91759
  subject: item2.content,
91644
91760
  description: item2.description,
91645
91761
  status: "pending",
91762
+ owner: "user",
91646
91763
  blocks: [],
91647
91764
  blockedBy: item2.deps,
91648
91765
  createdAt: now,
@@ -91650,18 +91767,21 @@ function buildInitialOverlayFromTemplate(template, now) {
91650
91767
  }));
91651
91768
  return { ...DEFAULT_TASK_OVERLAY, userTasks };
91652
91769
  }
91653
- function applyInitialOverlayToOrchestrator(args) {
91654
- const { taskId, initialOverlay, orchestratorRef, isTaskRegistered } = args;
91655
- if (orchestratorRef && isTaskRegistered(taskId)) {
91656
- orchestratorRef.applyOverlay(initialOverlay);
91657
- args.notifyTaskResourceChange(taskId);
91770
+ async function applyInitialOverlayToOrchestrator(args) {
91771
+ const { taskId, initialOverlay, orchestratorRef, isTaskRegistered, taskStateStore } = args;
91772
+ if (!orchestratorRef || !isTaskRegistered(taskId)) {
91773
+ args.log({
91774
+ event: "initial_overlay_skipped",
91775
+ taskId,
91776
+ reason: orchestratorRef ? "task_removed" : "orchestrator_not_registered"
91777
+ });
91658
91778
  return;
91659
91779
  }
91660
- args.log({
91661
- event: "initial_overlay_skipped",
91662
- taskId,
91663
- reason: orchestratorRef ? "task_removed" : "orchestrator_not_registered"
91664
- });
91780
+ orchestratorRef.applyOverlay(initialOverlay);
91781
+ const merged = applyOverlayToMap(/* @__PURE__ */ new Map(), initialOverlay);
91782
+ const todoProgress = computeTodoProgress(merged);
91783
+ await taskStateStore.updateStructuredTasks(taskId, Object.fromEntries(merged), todoProgress);
91784
+ args.notifyTaskResourceChange(taskId);
91665
91785
  }
91666
91786
 
91667
91787
  // src/services/task/manager/task-manager.ts
@@ -91931,6 +92051,7 @@ var TaskManager = class {
91931
92051
  task.orchestrator.applyPermissionMode(mode);
91932
92052
  }
91933
92053
  applyTaskSettings(taskId, settings) {
92054
+ if (Object.keys(settings).length === 0) return;
91934
92055
  const composerPatch = {};
91935
92056
  if (settings.model !== void 0) composerPatch.model = settings.model;
91936
92057
  if (settings.reasoningEffort !== void 0)
@@ -91948,8 +92069,12 @@ var TaskManager = class {
91948
92069
  });
91949
92070
  }
91950
92071
  const task = this.#tasks.get(taskId);
91951
- if (!task) return;
92072
+ if (!task) {
92073
+ this.#deps.log({ event: "task_settings_no_orchestrator", taskId });
92074
+ return;
92075
+ }
91952
92076
  task.orchestrator.applySettings(settings);
92077
+ this.broadcastControl({ type: "settings_ack", settings, taskId });
91953
92078
  }
91954
92079
  getPermissionMode(taskId) {
91955
92080
  const task = this.#tasks.get(taskId);
@@ -92021,12 +92146,13 @@ var TaskManager = class {
92021
92146
  initialOverlay
92022
92147
  });
92023
92148
  if (initialOverlay) {
92024
- applyInitialOverlayToOrchestrator({
92149
+ await applyInitialOverlayToOrchestrator({
92025
92150
  taskId: params.taskId,
92026
92151
  initialOverlay,
92027
92152
  orchestratorRef,
92028
92153
  isTaskRegistered: (id) => this.#tasks.has(id),
92029
92154
  notifyTaskResourceChange: this.#deps.notifyTaskResourceChange,
92155
+ taskStateStore: this.#deps.taskStateStore,
92030
92156
  log: this.#deps.log
92031
92157
  });
92032
92158
  }
@@ -92082,6 +92208,27 @@ var TaskManager = class {
92082
92208
  if (this.#tasks.has(taskId)) return;
92083
92209
  const cwd = opts.cwd;
92084
92210
  const mode = opts.mode ?? "task";
92211
+ let orchestratorRef = null;
92212
+ const restoreHydration = async () => {
92213
+ const record = await this.#deps.taskStateStore.getTask(taskId);
92214
+ if (!orchestratorRef || !this.#tasks.has(taskId)) return;
92215
+ if (record?.taskOverlay) {
92216
+ orchestratorRef.applyOverlay(record.taskOverlay);
92217
+ } else {
92218
+ orchestratorRef.markStructuredTaskRestoreComplete();
92219
+ }
92220
+ if (record?.composerSettings) {
92221
+ applyPersistedComposerSettings(orchestratorRef, record.composerSettings);
92222
+ }
92223
+ };
92224
+ const hydrationPromise = restoreHydration().catch((err) => {
92225
+ this.#deps.log({
92226
+ event: "overlay_restore_failed",
92227
+ taskId,
92228
+ error: err instanceof Error ? err.message : String(err)
92229
+ });
92230
+ orchestratorRef?.markStructuredTaskRestoreComplete();
92231
+ });
92085
92232
  const orchestrator = this.#createTask(taskId, channelId, {
92086
92233
  initialState: opts.initialState,
92087
92234
  existingSessionId: opts.existingSessionId,
@@ -92091,21 +92238,15 @@ var TaskManager = class {
92091
92238
  initialTokenCount: opts.initialTokenCount,
92092
92239
  initialPlanDetection: opts.initialPlanDetection,
92093
92240
  mode,
92241
+ hydrationPromise,
92094
92242
  initialRoiStartedEmitted: opts.initialRoiStartedEmitted,
92095
92243
  taskCreatedAt: opts.taskCreatedAt,
92096
- initialTurnCount: opts.initialTurnCount
92244
+ initialTurnCount: opts.initialTurnCount,
92245
+ restoreInProgress: true
92097
92246
  });
92247
+ orchestratorRef = orchestrator;
92098
92248
  this.#tasks.set(taskId, { taskId, channelId, cwd, mode, orchestrator });
92099
92249
  this.#flushPendingStreamSubs(taskId, orchestrator);
92100
- this.#deps.taskStateStore.getTask(taskId).then((record) => {
92101
- if (record?.taskOverlay) orchestrator.applyOverlay(record.taskOverlay);
92102
- }).catch((err) => {
92103
- this.#deps.log({
92104
- event: "overlay_restore_failed",
92105
- taskId,
92106
- error: err instanceof Error ? err.message : String(err)
92107
- });
92108
- });
92109
92250
  this.#deps.log({ event: "task_restored", taskId, channelId, initialState: opts.initialState });
92110
92251
  }
92111
92252
  async removeTask(taskId) {
@@ -92550,6 +92691,7 @@ var TaskManager = class {
92550
92691
  onAuthNotLoggedIn: () => this.#onAuthNotLoggedIn?.(),
92551
92692
  onRateLimitEvent: (info) => this.#onRateLimitEvent?.(info),
92552
92693
  sendControlMessage: this.#controlChannels.size > 0 ? this.#buildBroadcastFn() : void 0,
92694
+ applyTaskSettings: (settings) => this.applyTaskSettings(taskId, settings),
92553
92695
  resourceRegistry: this.#deps.resourceRegistry,
92554
92696
  planRepo: this.#deps.planRepo,
92555
92697
  annotationStore: this.#deps.annotationStore,
@@ -92594,10 +92736,19 @@ var TaskManager = class {
92594
92736
  },
92595
92737
  onForwardedAck: (correlationId) => {
92596
92738
  this.#forwardedAckEmitters.get(taskId)?.(correlationId);
92597
- }
92739
+ },
92740
+ restoreInProgress: opts?.restoreInProgress ?? false
92598
92741
  });
92599
92742
  }
92600
92743
  };
92744
+ function applyPersistedComposerSettings(orchestrator, settings) {
92745
+ const patch = {};
92746
+ if (settings.model != null) patch.model = settings.model;
92747
+ if (settings.reasoningEffort != null) patch.reasoningEffort = settings.reasoningEffort;
92748
+ if (settings.permissionMode != null) patch.permissionMode = settings.permissionMode;
92749
+ if (settings.fastMode != null) patch.fastMode = settings.fastMode;
92750
+ if (Object.keys(patch).length > 0) orchestrator.applySettings(patch);
92751
+ }
92601
92752
 
92602
92753
  // src/services/task/manager/task-state-store.ts
92603
92754
  import { join as join45 } from "path";
@@ -95464,6 +95615,13 @@ function filterOutboundForCollab(msg, collabTaskId) {
95464
95615
  /** Explicitly filtered: pr_state nests taskId inside `data` */
95465
95616
  case "pr_state":
95466
95617
  return msg.data.taskId === collabTaskId ? msg : null;
95618
+ /**
95619
+ * settings_ack: task-scoped variant reaches collab peers if the task
95620
+ * matches; daemon-wide variant (no taskId) is host-only.
95621
+ */
95622
+ case "settings_ack":
95623
+ if (msg.taskId === void 0) return null;
95624
+ return msg.taskId === collabTaskId ? msg : null;
95467
95625
  /** Suppressed: host-only settings and schedules */
95468
95626
  case "user_settings_snapshot":
95469
95627
  case "user_settings_updated":
@@ -95485,7 +95643,6 @@ function filterOutboundForCollab(msg, collabTaskId) {
95485
95643
  /** Suppressed: daemon-scoped messages with no taskId */
95486
95644
  case "capabilities":
95487
95645
  case "rate_limit_info":
95488
- case "settings_ack":
95489
95646
  case "enhance_prompt_chunk":
95490
95647
  case "mcp_server_auth_url":
95491
95648
  case "mcp_server_status":
@@ -96706,7 +96863,7 @@ function friendlyExecError(err) {
96706
96863
  async function refreshPluginCapabilities(daemon, tokenStore) {
96707
96864
  const [updatedMarketplace, updatedMcp, updatedSkills] = await Promise.all([
96708
96865
  detectMarketplacePlugins(),
96709
- import("./mcp-servers-XWOPKJ6F.js").then(
96866
+ import("./mcp-servers-MUVTAMDT.js").then(
96710
96867
  (m2) => m2.detectMCPServers(daemon.capabilities.environments, tokenStore)
96711
96868
  ),
96712
96869
  import("./skills-NCKYNLUS.js").then(
@@ -97706,7 +97863,6 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
97706
97863
  }
97707
97864
  if (Object.keys(applied).length === 0) return;
97708
97865
  daemon.taskManager.applyTaskSettings(taskId, applied);
97709
- handler.sendControl({ type: "settings_ack", settings: applied, taskId });
97710
97866
  logAdapter({ event: "task_settings_updated", taskId, settings: applied });
97711
97867
  },
97712
97868
  onRequestCapabilities: () => {
@@ -98660,13 +98816,21 @@ function handleParticipantLeft(room, userId, deps) {
98660
98816
  }
98661
98817
  function handleWebrtcSignaling(room, type, targetUserId, payload, deps) {
98662
98818
  const peerId = namespacePeerId(room.roomId, targetUserId);
98819
+ const generationId = `collab:${peerId}`;
98820
+ deps.log({
98821
+ event: "collab_generationid_placeholder",
98822
+ roomId: room.roomId,
98823
+ targetUserId,
98824
+ type,
98825
+ generationId
98826
+ });
98663
98827
  const actions = {
98664
98828
  // eslint-disable-next-line no-restricted-syntax -- SDP is opaque z.unknown() from signaling
98665
- offer: () => room.peerManager.handleOffer(peerId, payload),
98829
+ offer: () => room.peerManager.handleOffer(peerId, payload, generationId),
98666
98830
  // eslint-disable-next-line no-restricted-syntax -- SDP is opaque z.unknown() from signaling
98667
- answer: () => room.peerManager.handleAnswer(peerId, payload),
98831
+ answer: () => room.peerManager.handleAnswer(peerId, payload, generationId),
98668
98832
  // eslint-disable-next-line no-restricted-syntax -- ICE candidate is opaque z.unknown() from signaling
98669
- ice: () => room.peerManager.handleIce(peerId, payload)
98833
+ ice: () => room.peerManager.handleIce(peerId, payload, generationId)
98670
98834
  };
98671
98835
  actions[type]().catch((err) => {
98672
98836
  deps.log({ event: `collab_${type}_failed`, roomId: room.roomId, error: String(err) });
@@ -98810,6 +98974,7 @@ function createCollabRoomManager(deps) {
98810
98974
  const delayMs = expiresAt - Date.now();
98811
98975
  if (delayMs <= 0 || !Number.isFinite(delayMs)) {
98812
98976
  deps.log({ event: "collab_expired", roomId, expiresAt });
98977
+ peerManager.destroy();
98813
98978
  collabRepoHandle.destroy().catch((err) => {
98814
98979
  deps.log({
98815
98980
  event: "collab_repo_destroy_failed",
@@ -100432,14 +100597,132 @@ function createPeerManager(config2) {
100432
100597
  }
100433
100598
  return factoryPromise;
100434
100599
  }
100435
- function tearDownPeer(machineId) {
100600
+ async function resolveLiveEntry(fromMachineId, messageType, generationId) {
100601
+ const direct = peers.get(fromMachineId);
100602
+ if (direct) return direct;
100603
+ const pending = pendingCreates.get(fromMachineId);
100604
+ if (!pending) {
100605
+ if (messageType === "answer") {
100606
+ config2.log.warn({ fromMachineId, generationId }, "Received answer for unknown peer");
100607
+ } else {
100608
+ config2.log.debug(
100609
+ { fromMachineId, generationId },
100610
+ "Received ICE candidate for unknown peer"
100611
+ );
100612
+ }
100613
+ return null;
100614
+ }
100615
+ let resolved;
100616
+ try {
100617
+ resolved = await pending;
100618
+ } catch {
100619
+ return null;
100620
+ }
100621
+ if (peers.get(fromMachineId) !== resolved) return null;
100622
+ return resolved;
100623
+ }
100624
+ function discardSupersededPc(pc, event, fields) {
100625
+ pc.onconnectionstatechange = null;
100626
+ pc.onicecandidate = null;
100627
+ pc.ondatachannel = null;
100628
+ pc.close();
100629
+ config2.logAdapter?.({ event, ...fields });
100630
+ }
100631
+ function abortInitiatorCreate(pc, targetMachineId) {
100632
+ pendingCreates.delete(targetMachineId);
100633
+ peers.delete(targetMachineId);
100634
+ pc.onconnectionstatechange = null;
100635
+ pc.onicecandidate = null;
100636
+ pc.ondatachannel = null;
100637
+ pc.close();
100638
+ }
100639
+ function setupInitiatorDataChannel(pc, targetMachineId, myGenerationId) {
100640
+ if (!pc.createDataChannel) {
100641
+ throw new Error("PeerConnection does not support createDataChannel");
100642
+ }
100643
+ let channel;
100644
+ try {
100645
+ channel = pc.createDataChannel(LORO_SYNC_LABEL, { ordered: true });
100646
+ } catch (err) {
100647
+ config2.log.warn(
100648
+ { targetMachineId, err: String(err) },
100649
+ "createDataChannel failed (connection likely closing)"
100650
+ );
100651
+ abortInitiatorCreate(pc, targetMachineId);
100652
+ throw err;
100653
+ }
100654
+ const rawChannel = channel;
100655
+ const peerId = machineIdToPeerId(targetMachineId);
100656
+ const logAdapter = config2.logAdapter ?? noopLogAdapter;
100657
+ const guarded = installLoroGuard(
100658
+ rawChannel,
100659
+ config2.webrtcAdapter,
100660
+ peerId,
100661
+ config2.log,
100662
+ logAdapter
100663
+ );
100664
+ if (guarded) {
100665
+ disposeLoroGuard(peerId);
100666
+ loroGuards.set(peerId, guarded);
100667
+ }
100668
+ installInitiatorOnOpen(
100669
+ rawChannel,
100670
+ channel,
100671
+ targetMachineId,
100672
+ peerId,
100673
+ myGenerationId,
100674
+ logAdapter
100675
+ );
100676
+ }
100677
+ function installInitiatorOnOpen(rawChannel, channel, targetMachineId, peerId, myGenerationId, logAdapter) {
100678
+ rawChannel.onopen = () => {
100679
+ const attachResult = safeAttachDataChannel(
100680
+ () => {
100681
+ config2.webrtcAdapter.attachDataChannel(
100682
+ peerId,
100683
+ // eslint-disable-next-line no-restricted-syntax -- RTCDataChannel from node-datachannel satisfies the adapter interface
100684
+ channel
100685
+ );
100686
+ },
100687
+ {
100688
+ log: (entry) => logAdapter({ ...entry, peerId: String(peerId) }),
100689
+ onStopped: () => {
100690
+ const currentEntry = peers.get(targetMachineId);
100691
+ if (currentEntry?.generationId !== myGenerationId) return;
100692
+ config2.log.warn(
100693
+ { targetMachineId, peerId: String(peerId) },
100694
+ "Loro adapter in stopped state (initiator onopen); tearing down peer"
100695
+ );
100696
+ tearDownPeer(targetMachineId, "loro_adapter_stopped");
100697
+ }
100698
+ }
100699
+ );
100700
+ if (!attachResult.ok && attachResult.reason === "other") {
100701
+ config2.log.warn(
100702
+ { targetMachineId, err: String(attachResult.error) },
100703
+ "attachDataChannel failed with non-stopped error"
100704
+ );
100705
+ }
100706
+ };
100707
+ }
100708
+ function tearDownPeer(machineId, reason) {
100436
100709
  const peerId = machineIdToPeerId(machineId);
100437
100710
  disposeLoroGuard(peerId);
100438
100711
  config2.webrtcAdapter.detachDataChannel(peerId);
100439
- const pc = peers.get(machineId);
100440
- if (pc) {
100441
- pc.onconnectionstatechange = null;
100442
- pc.close();
100712
+ const entry = peers.get(machineId);
100713
+ if (entry) {
100714
+ config2.logAdapter?.({
100715
+ event: "peer_tear_down",
100716
+ machineId,
100717
+ generationId: entry.generationId,
100718
+ ageMs: Date.now() - entry.createdAt,
100719
+ lastState: entry.lastState,
100720
+ reason: reason ?? "explicit"
100721
+ });
100722
+ entry.pc.onconnectionstatechange = null;
100723
+ entry.pc.onicecandidate = null;
100724
+ entry.pc.ondatachannel = null;
100725
+ entry.pc.close();
100443
100726
  peers.delete(machineId);
100444
100727
  }
100445
100728
  }
@@ -100455,6 +100738,7 @@ function createPeerManager(config2) {
100455
100738
  logAdapter
100456
100739
  );
100457
100740
  if (guarded) {
100741
+ disposeLoroGuard(peerId);
100458
100742
  loroGuards.set(peerId, guarded);
100459
100743
  }
100460
100744
  const attachResult = safeAttachDataChannel(
@@ -100472,7 +100756,7 @@ function createPeerManager(config2) {
100472
100756
  { machineId, peerId: String(peerId) },
100473
100757
  "Loro adapter in stopped state; tearing down peer"
100474
100758
  );
100475
- tearDownPeer(machineId);
100759
+ tearDownPeer(machineId, "loro_adapter_stopped");
100476
100760
  }
100477
100761
  }
100478
100762
  );
@@ -100481,7 +100765,7 @@ function createPeerManager(config2) {
100481
100765
  { machineId, peerId: String(peerId), err: String(attachResult.error) },
100482
100766
  "attachDataChannel failed with non-stopped error; tearing down peer"
100483
100767
  );
100484
- tearDownPeer(machineId);
100768
+ tearDownPeer(machineId, "attach_failed");
100485
100769
  }
100486
100770
  }
100487
100771
  function handleDataChannel(machineId, event) {
@@ -100578,181 +100862,277 @@ function createPeerManager(config2) {
100578
100862
  );
100579
100863
  }
100580
100864
  }
100581
- function setupPeerHandlers(machineId, pc) {
100865
+ function setupPeerHandlers(machineId, pc, generationId) {
100582
100866
  pc.onicecandidate = (event) => {
100583
100867
  if (event.candidate) {
100584
- config2.onIceCandidate(machineId, {
100585
- candidate: event.candidate.candidate,
100586
- sdpMid: event.candidate.sdpMid,
100587
- sdpMLineIndex: event.candidate.sdpMLineIndex
100588
- });
100868
+ config2.onIceCandidate(
100869
+ machineId,
100870
+ {
100871
+ candidate: event.candidate.candidate,
100872
+ sdpMid: event.candidate.sdpMid,
100873
+ sdpMLineIndex: event.candidate.sdpMLineIndex
100874
+ },
100875
+ generationId
100876
+ );
100589
100877
  }
100590
100878
  };
100591
100879
  pc.ondatachannel = (event) => {
100592
100880
  handleDataChannel(machineId, event);
100593
100881
  };
100594
100882
  pc.onconnectionstatechange = () => {
100595
- const state = pc.connectionState;
100596
- config2.log.info({ machineId, state }, "Peer connection state changed");
100597
- if (state === "failed" || state === "closed") {
100598
- tearDownPeer(machineId);
100883
+ const currentEntry = peers.get(machineId);
100884
+ if (currentEntry?.generationId !== generationId) {
100885
+ config2.logAdapter?.({
100886
+ event: "stale_state_change_ignored",
100887
+ machineId,
100888
+ staleGenerationId: generationId,
100889
+ currentGenerationId: currentEntry?.generationId
100890
+ });
100891
+ return;
100892
+ }
100893
+ const newState = pc.connectionState;
100894
+ const prevState = currentEntry.lastState;
100895
+ const durationMs = Date.now() - currentEntry.lastStateAt;
100896
+ config2.logAdapter?.({
100897
+ event: "webrtc_state_transition",
100898
+ machineId,
100899
+ generationId,
100900
+ prevState,
100901
+ newState,
100902
+ durationMs
100903
+ });
100904
+ config2.log.info(
100905
+ { machineId, generationId, prevState, newState },
100906
+ "Peer connection state changed"
100907
+ );
100908
+ currentEntry.lastState = newState;
100909
+ currentEntry.lastStateAt = Date.now();
100910
+ if (newState === "failed" || newState === "closed") {
100911
+ tearDownPeer(machineId, newState);
100599
100912
  }
100600
100913
  };
100601
100914
  }
100602
100915
  return {
100603
- async handleOffer(fromMachineId, offer) {
100604
- config2.log.info({ fromMachineId }, "Handling WebRTC offer");
100916
+ async handleOffer(fromMachineId, offer, generationId) {
100605
100917
  const existing = peers.get(fromMachineId);
100606
- if (existing) {
100607
- config2.log.debug({ fromMachineId }, "Closing existing peer connection");
100608
- tearDownPeer(fromMachineId);
100609
- }
100610
- const promise = (async () => {
100611
- const factory = await getFactory();
100612
- const pc = factory();
100613
- setupPeerHandlers(fromMachineId, pc);
100614
- await pc.setRemoteDescription(offer);
100615
- const answer = await pc.createAnswer();
100616
- await pc.setLocalDescription(answer);
100617
- peers.set(fromMachineId, pc);
100618
- pendingCreates.delete(fromMachineId);
100619
- config2.onAnswer(fromMachineId, { type: "answer", sdp: answer.sdp });
100620
- return pc;
100918
+ config2.log.info(
100919
+ {
100920
+ fromMachineId,
100921
+ generationId,
100922
+ hadExistingPeer: !!existing,
100923
+ priorGenerationId: existing?.generationId
100924
+ },
100925
+ "Handling WebRTC offer"
100926
+ );
100927
+ config2.logAdapter?.({
100928
+ event: "webrtc_offer_received",
100929
+ generationId,
100930
+ fromMachineId,
100931
+ hadExistingPeer: !!existing,
100932
+ priorGenerationId: existing?.generationId
100933
+ });
100934
+ const inflight = pendingCreates.get(fromMachineId);
100935
+ if (inflight) {
100936
+ await Promise.race([
100937
+ inflight.catch(() => void 0),
100938
+ new Promise((resolve4) => setTimeout(resolve4, 500))
100939
+ ]);
100940
+ }
100941
+ if (existing || peers.has(fromMachineId)) {
100942
+ config2.log.debug({ fromMachineId, generationId }, "Closing existing peer connection");
100943
+ tearDownPeer(fromMachineId, "replaced_by_offer");
100944
+ }
100945
+ const now = Date.now();
100946
+ let promise;
100947
+ promise = (async () => {
100948
+ try {
100949
+ const factory = await getFactory();
100950
+ const pc = factory();
100951
+ setupPeerHandlers(fromMachineId, pc, generationId);
100952
+ await pc.setRemoteDescription(offer);
100953
+ const answer = await pc.createAnswer();
100954
+ await pc.setLocalDescription(answer);
100955
+ const entry = {
100956
+ pc,
100957
+ generationId,
100958
+ createdAt: now,
100959
+ lastState: "new",
100960
+ lastStateAt: now
100961
+ };
100962
+ if (pendingCreates.get(fromMachineId) !== promise) {
100963
+ discardSupersededPc(pc, "webrtc_offer_superseded", { fromMachineId, generationId });
100964
+ return entry;
100965
+ }
100966
+ peers.set(fromMachineId, entry);
100967
+ pendingCreates.delete(fromMachineId);
100968
+ config2.onAnswer(fromMachineId, { type: "answer", sdp: answer.sdp }, generationId);
100969
+ return entry;
100970
+ } catch (err) {
100971
+ if (pendingCreates.get(fromMachineId) === promise) {
100972
+ pendingCreates.delete(fromMachineId);
100973
+ }
100974
+ config2.logAdapter?.({
100975
+ event: "webrtc_offer_handshake_failed",
100976
+ fromMachineId,
100977
+ generationId,
100978
+ err: String(err)
100979
+ });
100980
+ throw err;
100981
+ }
100621
100982
  })();
100622
100983
  pendingCreates.set(fromMachineId, promise);
100623
100984
  setTimeout(() => {
100624
100985
  if (pendingCreates.get(fromMachineId) === promise) {
100625
100986
  pendingCreates.delete(fromMachineId);
100626
- config2.log.warn({ fromMachineId }, "WebRTC handshake timed out");
100987
+ config2.logAdapter?.({
100988
+ event: "webrtc_handshake_timeout",
100989
+ machineId: fromMachineId,
100990
+ generationId
100991
+ });
100992
+ config2.log.warn({ fromMachineId, generationId }, "WebRTC handshake timed out");
100627
100993
  }
100628
100994
  }, HANDSHAKE_TIMEOUT_MS);
100629
100995
  await promise;
100630
100996
  },
100631
100997
  async initiateOffer(targetMachineId) {
100632
- config2.log.info({ targetMachineId }, "Initiating WebRTC offer");
100998
+ const generationId = crypto.randomUUID();
100999
+ config2.log.info({ targetMachineId, generationId }, "Initiating WebRTC offer");
100633
101000
  const existing = peers.get(targetMachineId);
100634
101001
  if (existing) {
100635
- tearDownPeer(targetMachineId);
101002
+ tearDownPeer(targetMachineId, "replaced_by_initiate");
100636
101003
  }
100637
- const promise = (async () => {
100638
- const factory = await getFactory();
100639
- const pc = factory();
100640
- setupPeerHandlers(targetMachineId, pc);
100641
- if (!pc.createDataChannel) {
100642
- throw new Error("PeerConnection does not support createDataChannel");
100643
- }
100644
- if (!pc.createOffer) {
100645
- throw new Error("PeerConnection does not support createOffer");
100646
- }
100647
- let channel;
101004
+ const now = Date.now();
101005
+ let promise;
101006
+ promise = (async () => {
100648
101007
  try {
100649
- channel = pc.createDataChannel(LORO_SYNC_LABEL, { ordered: true });
100650
- } catch (err) {
100651
- config2.log.warn(
100652
- { targetMachineId, err: String(err) },
100653
- "createDataChannel failed (connection likely closing)"
100654
- );
101008
+ const factory = await getFactory();
101009
+ const pc = factory();
101010
+ setupPeerHandlers(targetMachineId, pc, generationId);
101011
+ if (!pc.createOffer) {
101012
+ throw new Error("PeerConnection does not support createOffer");
101013
+ }
101014
+ setupInitiatorDataChannel(pc, targetMachineId, generationId);
101015
+ const offer = await pc.createOffer();
101016
+ await pc.setLocalDescription(offer);
101017
+ const entry = {
101018
+ pc,
101019
+ generationId,
101020
+ createdAt: now,
101021
+ lastState: "new",
101022
+ lastStateAt: now
101023
+ };
101024
+ if (pendingCreates.get(targetMachineId) !== promise) {
101025
+ disposeLoroGuard(machineIdToPeerId(targetMachineId));
101026
+ discardSupersededPc(pc, "webrtc_initiate_superseded", {
101027
+ targetMachineId,
101028
+ generationId
101029
+ });
101030
+ return entry;
101031
+ }
101032
+ peers.set(targetMachineId, entry);
100655
101033
  pendingCreates.delete(targetMachineId);
100656
- peers.delete(targetMachineId);
100657
- pc.onconnectionstatechange = null;
100658
- pc.close();
101034
+ config2.onOffer?.(targetMachineId, { type: "offer", sdp: offer.sdp }, generationId);
101035
+ return entry;
101036
+ } catch (err) {
101037
+ if (pendingCreates.get(targetMachineId) === promise) {
101038
+ pendingCreates.delete(targetMachineId);
101039
+ }
101040
+ config2.logAdapter?.({
101041
+ event: "webrtc_initiate_handshake_failed",
101042
+ targetMachineId,
101043
+ generationId,
101044
+ err: String(err)
101045
+ });
100659
101046
  throw err;
100660
101047
  }
100661
- const rawChannel = channel;
100662
- const peerId = machineIdToPeerId(targetMachineId);
100663
- const logAdapter = config2.logAdapter ?? noopLogAdapter;
100664
- const guarded = installLoroGuard(
100665
- rawChannel,
100666
- config2.webrtcAdapter,
100667
- peerId,
100668
- config2.log,
100669
- logAdapter
100670
- );
100671
- if (guarded) {
100672
- loroGuards.set(peerId, guarded);
100673
- }
100674
- const myPc = pc;
100675
- rawChannel.onopen = () => {
100676
- const attachResult = safeAttachDataChannel(
100677
- () => {
100678
- config2.webrtcAdapter.attachDataChannel(
100679
- peerId,
100680
- // eslint-disable-next-line no-restricted-syntax -- RTCDataChannel from node-datachannel satisfies the adapter interface
100681
- channel
100682
- );
100683
- },
100684
- {
100685
- log: (entry) => logAdapter({ ...entry, peerId: String(peerId) }),
100686
- onStopped: () => {
100687
- const existingPc = peers.get(targetMachineId);
100688
- if (existingPc !== myPc) return;
100689
- config2.log.warn(
100690
- { targetMachineId, peerId: String(peerId) },
100691
- "Loro adapter in stopped state (initiator onopen); tearing down peer"
100692
- );
100693
- tearDownPeer(targetMachineId);
100694
- }
100695
- }
100696
- );
100697
- if (!attachResult.ok && attachResult.reason === "other") {
100698
- config2.log.warn(
100699
- { targetMachineId, err: String(attachResult.error) },
100700
- "attachDataChannel failed with non-stopped error"
100701
- );
100702
- }
100703
- };
100704
- const offer = await pc.createOffer();
100705
- await pc.setLocalDescription(offer);
100706
- peers.set(targetMachineId, pc);
100707
- pendingCreates.delete(targetMachineId);
100708
- config2.onOffer?.(targetMachineId, { type: "offer", sdp: offer.sdp });
100709
- return pc;
100710
101048
  })();
100711
101049
  pendingCreates.set(targetMachineId, promise);
100712
101050
  setTimeout(() => {
100713
101051
  if (pendingCreates.get(targetMachineId) === promise) {
100714
101052
  pendingCreates.delete(targetMachineId);
100715
- config2.log.warn({ targetMachineId }, "WebRTC handshake timed out (initiator)");
101053
+ config2.logAdapter?.({
101054
+ event: "webrtc_handshake_timeout",
101055
+ machineId: targetMachineId,
101056
+ generationId
101057
+ });
101058
+ config2.log.warn(
101059
+ { targetMachineId, generationId },
101060
+ "WebRTC handshake timed out (initiator)"
101061
+ );
100716
101062
  }
100717
101063
  }, HANDSHAKE_TIMEOUT_MS);
100718
101064
  await promise;
100719
101065
  },
100720
- async handleAnswer(fromMachineId, answer) {
100721
- let pc = peers.get(fromMachineId);
100722
- if (!pc) {
100723
- const pending = pendingCreates.get(fromMachineId);
100724
- if (pending) {
100725
- pc = await pending;
100726
- } else {
100727
- config2.log.warn({ fromMachineId }, "Received answer for unknown peer");
100728
- return;
100729
- }
101066
+ async handleAnswer(fromMachineId, answer, generationId) {
101067
+ const entry = await resolveLiveEntry(fromMachineId, "answer", generationId);
101068
+ if (!entry) return;
101069
+ if (entry.generationId !== generationId) {
101070
+ config2.logAdapter?.({
101071
+ event: "webrtc_answer_stale",
101072
+ fromMachineId,
101073
+ currentGenerationId: entry.generationId,
101074
+ staleGenerationId: generationId
101075
+ });
101076
+ config2.log.debug(
101077
+ {
101078
+ fromMachineId,
101079
+ currentGenerationId: entry.generationId,
101080
+ staleGenerationId: generationId
101081
+ },
101082
+ "Dropping stale WebRTC answer"
101083
+ );
101084
+ return;
100730
101085
  }
100731
- await pc.setRemoteDescription(answer);
101086
+ await entry.pc.setRemoteDescription(answer);
100732
101087
  },
100733
- async handleIce(fromMachineId, candidate) {
100734
- let pc = peers.get(fromMachineId);
100735
- if (!pc) {
100736
- const pending = pendingCreates.get(fromMachineId);
100737
- if (pending) {
100738
- pc = await pending;
100739
- } else {
100740
- config2.log.debug({ fromMachineId }, "Received ICE candidate for unknown peer");
100741
- return;
100742
- }
101088
+ async handleIce(fromMachineId, candidate, generationId) {
101089
+ const entry = await resolveLiveEntry(fromMachineId, "ice", generationId);
101090
+ if (!entry) return;
101091
+ if (entry.generationId !== generationId) {
101092
+ config2.logAdapter?.({
101093
+ event: "webrtc_ice_candidate_stale",
101094
+ fromMachineId,
101095
+ currentGenerationId: entry.generationId,
101096
+ staleGenerationId: generationId
101097
+ });
101098
+ config2.log.debug(
101099
+ {
101100
+ fromMachineId,
101101
+ currentGenerationId: entry.generationId,
101102
+ staleGenerationId: generationId
101103
+ },
101104
+ "Dropping stale ICE candidate"
101105
+ );
101106
+ return;
101107
+ }
101108
+ try {
101109
+ await entry.pc.addIceCandidate(candidate);
101110
+ } catch (err) {
101111
+ config2.logAdapter?.({
101112
+ event: "webrtc_ice_apply_failed",
101113
+ fromMachineId,
101114
+ generationId,
101115
+ err: String(err)
101116
+ });
101117
+ config2.log.warn(
101118
+ { fromMachineId, generationId, err: String(err) },
101119
+ "addIceCandidate failed"
101120
+ );
100743
101121
  }
100744
- await pc.addIceCandidate(candidate);
100745
101122
  },
100746
101123
  updateIceServers(servers) {
100747
101124
  config2.iceServers = servers;
100748
101125
  factoryPromise = null;
100749
101126
  },
100750
101127
  closePeer(targetId) {
100751
- tearDownPeer(targetId);
101128
+ tearDownPeer(targetId, "close_peer");
100752
101129
  },
100753
101130
  destroy() {
100754
101131
  for (const machineId of [...peers.keys()]) {
100755
- tearDownPeer(machineId);
101132
+ tearDownPeer(machineId, "destroy");
101133
+ }
101134
+ for (const guard of loroGuards.values()) {
101135
+ guard.dispose();
100756
101136
  }
100757
101137
  peers.clear();
100758
101138
  pendingCreates.clear();
@@ -102423,7 +102803,15 @@ function wireAutoOpenBrowser(connection, signalingClient, authToken, signalingUr
102423
102803
  var WARN_EVENTS = /* @__PURE__ */ new Set([
102424
102804
  "subprocess_force_killed",
102425
102805
  "file_watcher_events_dropped",
102426
- "mcp_server_toggle_failed"
102806
+ "mcp_server_toggle_failed",
102807
+ /**
102808
+ * WebRTC transient failures that match the `_failed` heuristic but are
102809
+ * expected during normal reconnect scenarios (stale/closed PCs, network
102810
+ * flap). Classify as warn to avoid flooding error-level telemetry.
102811
+ */
102812
+ "webrtc_ice_apply_failed",
102813
+ "webrtc_offer_handshake_failed",
102814
+ "webrtc_initiate_handshake_failed"
102427
102815
  ]);
102428
102816
  var DEBUG_EVENTS = /* @__PURE__ */ new Set([
102429
102817
  "port_detection_poll",
@@ -102444,19 +102832,19 @@ function routeSignalingMessage(peerManager, signalingHandle, log) {
102444
102832
  switch (msg.type) {
102445
102833
  case "webrtc-offer":
102446
102834
  if (msg.fromMachineId)
102447
- peerManager?.handleOffer(msg.fromMachineId, toSDPDescription(msg.offer)).catch(
102835
+ peerManager?.handleOffer(msg.fromMachineId, toSDPDescription(msg.offer), msg.generationId).catch(
102448
102836
  (err) => log.error({ event: "webrtc_offer_failed", error: String(err) })
102449
102837
  );
102450
102838
  break;
102451
102839
  case "webrtc-answer":
102452
102840
  if (msg.fromMachineId)
102453
- peerManager?.handleAnswer(msg.fromMachineId, toSDPDescription(msg.answer)).catch(
102841
+ peerManager?.handleAnswer(msg.fromMachineId, toSDPDescription(msg.answer), msg.generationId).catch(
102454
102842
  (err) => log.error({ event: "webrtc_answer_failed", error: String(err) })
102455
102843
  );
102456
102844
  break;
102457
102845
  case "webrtc-ice":
102458
102846
  if (msg.fromMachineId)
102459
- peerManager?.handleIce(msg.fromMachineId, toICECandidate(msg.candidate)).catch((err) => log.error({ event: "webrtc_ice_failed", error: String(err) }));
102847
+ peerManager?.handleIce(msg.fromMachineId, toICECandidate(msg.candidate), msg.generationId).catch((err) => log.error({ event: "webrtc_ice_failed", error: String(err) }));
102460
102848
  break;
102461
102849
  case "ice-servers":
102462
102850
  peerManager?.updateIceServers(msg.iceServers);
@@ -102478,10 +102866,8 @@ function routeSignalingMessage(peerManager, signalingHandle, log) {
102478
102866
  case "error":
102479
102867
  case "authenticated":
102480
102868
  break;
102481
- default: {
102482
- const _exhaustive = msg;
102483
- void _exhaustive;
102484
- }
102869
+ default:
102870
+ assertNever(msg);
102485
102871
  }
102486
102872
  };
102487
102873
  }
@@ -102633,18 +103019,20 @@ function buildPeerManager(deps) {
102633
103019
  return createPeerManager({
102634
103020
  webrtcAdapter: personalWebrtcAdapter,
102635
103021
  logAdapter,
102636
- onAnswer: (targetMachineId, answer) => {
103022
+ onAnswer: (targetMachineId, answer, generationId) => {
102637
103023
  signalingHandle.connection.send({
102638
103024
  type: "webrtc-answer",
102639
103025
  targetMachineId,
102640
- answer
103026
+ answer,
103027
+ generationId
102641
103028
  });
102642
103029
  },
102643
- onIceCandidate: (targetMachineId, candidate) => {
103030
+ onIceCandidate: (targetMachineId, candidate, generationId) => {
102644
103031
  signalingHandle.connection.send({
102645
103032
  type: "webrtc-ice",
102646
103033
  targetMachineId,
102647
- candidate
103034
+ candidate,
103035
+ generationId
102648
103036
  });
102649
103037
  },
102650
103038
  onPeerDataChannel: (machineId) => {
@@ -103304,4 +103692,4 @@ export {
103304
103692
  _testing,
103305
103693
  serve
103306
103694
  };
103307
- //# sourceMappingURL=serve-P5WC5JIT.js.map
103695
+ //# sourceMappingURL=serve-X4R5CDHD.js.map