@schoolai/shipyard 3.3.0 → 3.3.1-rc.20260425.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.
@@ -53,7 +53,7 @@ import {
53
53
  VaultKeyPutRequestSchema,
54
54
  VaultKeyPutResponseSchema,
55
55
  classifyClaudeCodeCompatibility
56
- } from "./chunk-UWBX6UGC.js";
56
+ } from "./chunk-QUDDKCMA.js";
57
57
  import "./chunk-EHQITHQX.js";
58
58
  import {
59
59
  loadAuthToken
@@ -14592,7 +14592,7 @@ function parseWorktreeListOutput(output, repoPath) {
14592
14592
  // src/shared/capabilities/index.ts
14593
14593
  import { readFile as readFile3 } from "fs/promises";
14594
14594
  import { homedir as homedir2 } from "os";
14595
- import { join as join7 } from "path";
14595
+ import { join as join6 } from "path";
14596
14596
 
14597
14597
  // src/shared/capabilities/agents.ts
14598
14598
  async function detectAgentProviders() {
@@ -14850,7 +14850,6 @@ async function detectMarketplacePlugins() {
14850
14850
  // src/shared/capabilities/effort-probe.ts
14851
14851
  import { existsSync as existsSync2, readFileSync } from "fs";
14852
14852
  import { createRequire } from "module";
14853
- import { dirname as dirname3, join as join6 } from "path";
14854
14853
 
14855
14854
  // ../../packages/loro-schema/dist/index.js
14856
14855
  import { LoroDoc as LoroDoc4 } from "loro-crdt";
@@ -27700,6 +27699,58 @@ var TaskColorSchema = external_exports.enum(TASK_COLORS);
27700
27699
  var WorktreeScriptSchema = external_exports.object({
27701
27700
  script: external_exports.string()
27702
27701
  });
27702
+ var LinearSortFieldSchema = external_exports.enum([
27703
+ "priority",
27704
+ "updated",
27705
+ "created",
27706
+ "status",
27707
+ "assignee",
27708
+ "estimate",
27709
+ "title"
27710
+ ]);
27711
+ var LinearSortDirectionSchema = external_exports.enum(["asc", "desc"]);
27712
+ var LinearGroupBySchema = external_exports.enum([
27713
+ "none",
27714
+ "status",
27715
+ "priority",
27716
+ "assignee",
27717
+ "team",
27718
+ "label",
27719
+ "parent",
27720
+ "project"
27721
+ ]);
27722
+ var LinearViewPreferencesSchema = external_exports.object({
27723
+ filters: external_exports.object({
27724
+ status: external_exports.array(external_exports.string()),
27725
+ priority: external_exports.array(external_exports.number()),
27726
+ labels: external_exports.array(external_exports.string()),
27727
+ assignee: external_exports.array(external_exports.string()),
27728
+ team: external_exports.array(external_exports.string()),
27729
+ project: external_exports.array(external_exports.string()),
27730
+ hasEstimate: external_exports.boolean().nullable(),
27731
+ createdAfter: external_exports.string().nullable(),
27732
+ createdBefore: external_exports.string().nullable()
27733
+ }),
27734
+ sortBy: LinearSortFieldSchema,
27735
+ sortDirection: LinearSortDirectionSchema,
27736
+ displayProperties: external_exports.object({
27737
+ identifier: external_exports.boolean(),
27738
+ status: external_exports.boolean(),
27739
+ assignee: external_exports.boolean(),
27740
+ priority: external_exports.boolean(),
27741
+ estimate: external_exports.boolean(),
27742
+ labels: external_exports.boolean(),
27743
+ created: external_exports.boolean(),
27744
+ subIssues: external_exports.boolean(),
27745
+ team: external_exports.boolean(),
27746
+ updated: external_exports.boolean()
27747
+ }),
27748
+ groupBy: LinearGroupBySchema,
27749
+ subGroupBy: LinearGroupBySchema,
27750
+ showSubIssues: external_exports.boolean(),
27751
+ showEmptyGroups: external_exports.boolean(),
27752
+ searchQuery: external_exports.string()
27753
+ });
27703
27754
  var UserSettingsRecordSchema = external_exports.object({
27704
27755
  composerModel: external_exports.string().nullable(),
27705
27756
  composerReasoning: ReasoningEffortSchema.nullable(),
@@ -27713,9 +27764,12 @@ var UserSettingsRecordSchema = external_exports.object({
27713
27764
  builtInTemplateOrder: external_exports.array(external_exports.string()).default([]),
27714
27765
  taskColors: external_exports.record(external_exports.string(), TaskColorSchema).default({}),
27715
27766
  favoriteTasks: external_exports.array(external_exports.string()).default([]),
27716
- collapsedProjects: external_exports.array(external_exports.string()).default([])
27767
+ collapsedProjects: external_exports.array(external_exports.string()).default([]),
27768
+ linearViewPreferences: LinearViewPreferencesSchema.nullable().default(null),
27769
+ linearScope: external_exports.enum(["my-issues", "team-issues"]).nullable().default(null),
27770
+ linearSelectedTeamId: external_exports.string().nullable().default(null)
27717
27771
  });
27718
- var USER_SETTINGS_STORE_VERSION = 3;
27772
+ var USER_SETTINGS_STORE_VERSION = 4;
27719
27773
  var UserSettingsStoreSchema = external_exports.object({
27720
27774
  schemaVersion: external_exports.number(),
27721
27775
  settings: UserSettingsRecordSchema
@@ -27746,7 +27800,10 @@ var DEFAULT_USER_SETTINGS = {
27746
27800
  builtInTemplateOrder: [],
27747
27801
  taskColors: {},
27748
27802
  favoriteTasks: [],
27749
- collapsedProjects: []
27803
+ collapsedProjects: [],
27804
+ linearViewPreferences: null,
27805
+ linearScope: null,
27806
+ linearSelectedTeamId: null
27750
27807
  };
27751
27808
  function migrateUserSettingsStore(raw) {
27752
27809
  if (typeof raw !== "object" || raw === null) {
@@ -28056,8 +28113,7 @@ var RequestPublishShape = external_exports.object({
28056
28113
  external_exports.object({
28057
28114
  kind: external_exports.literal("previewPort"),
28058
28115
  previewPort: external_exports.number().int().positive().max(65535),
28059
- canvasElementId: external_exports.string().optional(),
28060
- projectRoot: external_exports.string().optional()
28116
+ canvasElementId: external_exports.string().optional()
28061
28117
  })
28062
28118
  ]),
28063
28119
  ttl: external_exports.enum(["24h", "7d", "30d"]),
@@ -28106,6 +28162,20 @@ var PublishedArtifactsStateShape = external_exports.object({
28106
28162
  taskId: external_exports.string(),
28107
28163
  entries: external_exports.array(PublishedArtifactRecordShape)
28108
28164
  });
28165
+ var PreviewElementStateShape = external_exports.object({
28166
+ elementId: external_exports.string().min(1),
28167
+ ownerUserId: external_exports.string(),
28168
+ port: external_exports.number().int().positive().optional(),
28169
+ url: external_exports.string().optional(),
28170
+ proxyPort: external_exports.number().int().positive().optional(),
28171
+ initialPath: external_exports.string().optional(),
28172
+ projectRoot: external_exports.string().optional()
28173
+ });
28174
+ var PreviewElementsStateShape = external_exports.object({
28175
+ type: external_exports.literal("preview_elements_state"),
28176
+ taskId: external_exports.string(),
28177
+ entries: external_exports.array(PreviewElementStateShape)
28178
+ });
28109
28179
  var BrowserToDaemonControlMessageSchema = external_exports.discriminatedUnion("type", [
28110
28180
  external_exports.object({
28111
28181
  type: external_exports.literal("permission_response"),
@@ -28451,6 +28521,7 @@ var RateLimitInfoSchema = external_exports.object({
28451
28521
  "org_service_level_disabled",
28452
28522
  "org_service_zero_credit_limit",
28453
28523
  "no_limits_configured",
28524
+ "fetch_error",
28454
28525
  "unknown"
28455
28526
  ]).optional(),
28456
28527
  isUsingOverage: external_exports.boolean().optional(),
@@ -28938,7 +29009,8 @@ var DaemonToBrowserControlMessageSchema = external_exports.discriminatedUnion("t
28938
29009
  }),
28939
29010
  PublishProgressShape,
28940
29011
  PublishResultShape,
28941
- PublishedArtifactsStateShape
29012
+ PublishedArtifactsStateShape,
29013
+ PreviewElementsStateShape
28942
29014
  ]);
28943
29015
  var TASK_MESSAGES_PREFIX = "task-messages:";
28944
29016
  var DAEMON_CONTROL_LABEL = "daemon-control";
@@ -29319,8 +29391,7 @@ var PublishTargetSchema = external_exports.discriminatedUnion("kind", [
29319
29391
  external_exports.object({
29320
29392
  kind: external_exports.literal("previewPort"),
29321
29393
  previewPort: external_exports.number().int().positive().max(65535),
29322
- canvasElementId: external_exports.string().optional(),
29323
- projectRoot: external_exports.string().optional()
29394
+ canvasElementId: external_exports.string().optional()
29324
29395
  })
29325
29396
  ]);
29326
29397
  var PublishInputSchema = external_exports.object({
@@ -30319,21 +30390,27 @@ function isReasoningEffort(value) {
30319
30390
  }
30320
30391
  function probeCli() {
30321
30392
  try {
30322
- const req = createRequire(import.meta.url);
30323
- const sdkMain = req.resolve("@anthropic-ai/claude-agent-sdk");
30324
- const cliPath = join6(dirname3(sdkMain), "cli.js");
30325
- if (!existsSync2(cliPath)) {
30393
+ const binaryPath = resolveBundledClaudeBinary();
30394
+ if (!binaryPath || !existsSync2(binaryPath)) {
30326
30395
  logger.warn(
30327
- { cliPath },
30328
- "bundled Claude Code cli.js not found; using conservative effort set"
30396
+ { binaryPath },
30397
+ "bundled Claude Code binary not found; using conservative effort set"
30329
30398
  );
30330
30399
  return { supported: CONSERVATIVE_FALLBACK, source: "fallback" };
30331
30400
  }
30332
- const parsed = parseEffortsFromCliText(readFileSync(cliPath, "utf-8"));
30401
+ const slice2 = readEffortDescriptionFromBinary(binaryPath);
30402
+ if (slice2 === null) {
30403
+ logger.warn(
30404
+ { binaryPath },
30405
+ "effort description not found in bundled binary; using conservative effort set"
30406
+ );
30407
+ return { supported: CONSERVATIVE_FALLBACK, source: "fallback" };
30408
+ }
30409
+ const parsed = parseEffortsFromCliText(slice2);
30333
30410
  if (!parsed) {
30334
30411
  logger.warn(
30335
- { cliPath },
30336
- "could not parse effort tokens from bundled cli.js; using conservative effort set"
30412
+ { binaryPath },
30413
+ "could not parse effort tokens from bundled binary; using conservative effort set"
30337
30414
  );
30338
30415
  return { supported: CONSERVATIVE_FALLBACK, source: "fallback" };
30339
30416
  }
@@ -30343,6 +30420,32 @@ function probeCli() {
30343
30420
  return { supported: CONSERVATIVE_FALLBACK, source: "fallback" };
30344
30421
  }
30345
30422
  }
30423
+ function resolveBundledClaudeBinary() {
30424
+ const req = createRequire(import.meta.url);
30425
+ const sdkMain = req.resolve("@anthropic-ai/claude-agent-sdk");
30426
+ const sdkReq = createRequire(sdkMain);
30427
+ const ext2 = process.platform === "win32" ? ".exe" : "";
30428
+ const candidates = process.platform === "linux" ? [
30429
+ `@anthropic-ai/claude-agent-sdk-linux-${process.arch}-musl`,
30430
+ `@anthropic-ai/claude-agent-sdk-linux-${process.arch}`
30431
+ ] : [`@anthropic-ai/claude-agent-sdk-${process.platform}-${process.arch}`];
30432
+ for (const pkg of candidates) {
30433
+ try {
30434
+ return sdkReq.resolve(`${pkg}/claude${ext2}`);
30435
+ } catch (err) {
30436
+ logger.debug({ pkg, err }, "platform claude binary candidate not resolvable");
30437
+ }
30438
+ }
30439
+ return null;
30440
+ }
30441
+ function readEffortDescriptionFromBinary(binaryPath) {
30442
+ const buf = readFileSync(binaryPath);
30443
+ const needle = Buffer.from("Effort level for the current session");
30444
+ const idx = buf.indexOf(needle);
30445
+ if (idx === -1) return null;
30446
+ const end = Math.min(idx + 200, buf.length);
30447
+ return buf.toString("utf-8", idx, end);
30448
+ }
30346
30449
 
30347
30450
  // src/shared/capabilities/models.ts
30348
30451
  async function detectModels() {
@@ -31139,7 +31242,7 @@ var AutoModeConfigSchema = external_exports.object({
31139
31242
  }).passthrough();
31140
31243
  async function detectAutoModeEnabled() {
31141
31244
  try {
31142
- const claudeConfig = await readFile3(join7(homedir2(), ".claude.json"), "utf-8");
31245
+ const claudeConfig = await readFile3(join6(homedir2(), ".claude.json"), "utf-8");
31143
31246
  const result = AutoModeConfigSchema.safeParse(JSON.parse(claudeConfig));
31144
31247
  return result.success && result.data.cachedGrowthBookFeatures.tengu_auto_mode_config.enabled === "enabled";
31145
31248
  } catch {
@@ -31208,7 +31311,7 @@ async function detectCapabilitiesWithInitialRetry(tokenStore, methodHint, lastKn
31208
31311
 
31209
31312
  // src/shared/file-storage-adapter.ts
31210
31313
  import { access, mkdir as mkdir2, readdir as readdir3, readFile as readFile4, rename as rename2, unlink as unlink2, writeFile as writeFile2 } from "fs/promises";
31211
- import { dirname as dirname4, join as join8, sep } from "path";
31314
+ import { dirname as dirname3, join as join7, sep } from "path";
31212
31315
  var FileStorageAdapter = class _FileStorageAdapter extends StorageAdapter {
31213
31316
  #dataDir;
31214
31317
  constructor(dataDir) {
@@ -31226,7 +31329,7 @@ var FileStorageAdapter = class _FileStorageAdapter extends StorageAdapter {
31226
31329
  }
31227
31330
  async save(key, data) {
31228
31331
  const filePath = this.#keyToPath(key);
31229
- const dir = dirname4(filePath);
31332
+ const dir = dirname3(filePath);
31230
31333
  await mkdir2(dir, { recursive: true, mode: 448 });
31231
31334
  const tmpPath = `${filePath}.tmp`;
31232
31335
  await writeFile2(tmpPath, data, { mode: 384 });
@@ -31268,7 +31371,7 @@ var FileStorageAdapter = class _FileStorageAdapter extends StorageAdapter {
31268
31371
  * docId is the first key segment (e.g. "canvas:<taskId>:<epoch>").
31269
31372
  */
31270
31373
  async hasDoc(docId) {
31271
- const dirPath = join8(this.#dataDir, encodeURIComponent(docId));
31374
+ const dirPath = join7(this.#dataDir, encodeURIComponent(docId));
31272
31375
  try {
31273
31376
  await access(dirPath);
31274
31377
  return true;
@@ -31282,7 +31385,7 @@ var FileStorageAdapter = class _FileStorageAdapter extends StorageAdapter {
31282
31385
  }
31283
31386
  #keyToPath(key) {
31284
31387
  const sanitized = key.map((part) => encodeURIComponent(part));
31285
- return join8(this.#dataDir, ...sanitized);
31388
+ return join7(this.#dataDir, ...sanitized);
31286
31389
  }
31287
31390
  #pathToKey(filePath) {
31288
31391
  const relative5 = filePath.slice(this.#dataDir.length + 1);
@@ -31300,7 +31403,7 @@ var FileStorageAdapter = class _FileStorageAdapter extends StorageAdapter {
31300
31403
  return;
31301
31404
  }
31302
31405
  for (const entry of entries) {
31303
- const fullPath = join8(dir, entry.name);
31406
+ const fullPath = join7(dir, entry.name);
31304
31407
  if (entry.isDirectory()) {
31305
31408
  await this.#walkDir(fullPath, keyPrefix, results);
31306
31409
  } else if (entry.isFile() && !entry.name.endsWith(".tmp")) {
@@ -31478,7 +31581,7 @@ var KeepAwakeManager = class {
31478
31581
  // src/shared/mcp/token-store.ts
31479
31582
  import { readFileSync as readFileSync2, statSync } from "fs";
31480
31583
  import { readFile as readFile5, rename as rename3, stat as stat3, writeFile as writeFile3 } from "fs/promises";
31481
- import { join as join9 } from "path";
31584
+ import { join as join8 } from "path";
31482
31585
  var TOKEN_FILE = "mcp-tokens.json";
31483
31586
  var REFRESH_THRESHOLD_MS = 5 * 60 * 1e3;
31484
31587
  var MCPTokenStore = class {
@@ -31496,7 +31599,7 @@ var MCPTokenStore = class {
31496
31599
  this.#onChange = cb;
31497
31600
  }
31498
31601
  #filePath() {
31499
- return join9(this.#shipyardHome, TOKEN_FILE);
31602
+ return join8(this.#shipyardHome, TOKEN_FILE);
31500
31603
  }
31501
31604
  /** Read mtime from disk. Returns -1 if file does not exist. */
31502
31605
  #readMtimeMs() {
@@ -31608,7 +31711,7 @@ var MCPTokenStore = class {
31608
31711
  // src/services/bootstrap/lifecycle.ts
31609
31712
  import { unlinkSync } from "fs";
31610
31713
  import { readFile as readFile6, unlink as unlink3, writeFile as writeFile4 } from "fs/promises";
31611
- import { join as join10 } from "path";
31714
+ import { join as join9 } from "path";
31612
31715
 
31613
31716
  // src/services/bootstrap/classify-uncaught-error.ts
31614
31717
  function classifyUncaughtError(error2) {
@@ -31659,6 +31762,15 @@ function getLoroCrdtVersion() {
31659
31762
  }
31660
31763
 
31661
31764
  // src/services/bootstrap/lifecycle.ts
31765
+ function extractWasmPanicContext(error2) {
31766
+ const stack = error2 instanceof Error && typeof error2.stack === "string" ? error2.stack : "";
31767
+ const frames = stack.split("\n").map((f2) => f2.trim());
31768
+ const wasmHashMatch = stack.match(/wasm:\/\/wasm\/([0-9a-f]+):/);
31769
+ const wasmModuleHash = wasmHashMatch?.[1] ?? null;
31770
+ const jsFrames = frames.filter((f2) => f2.startsWith("at ") && !f2.includes("wasm://wasm/"));
31771
+ const jsLeafFrame = jsFrames[0]?.replace(/^at /, "") ?? null;
31772
+ return { jsLeafFrame, jsStack: jsFrames.slice(0, 8), wasmModuleHash };
31773
+ }
31662
31774
  function isProcessAlive(pid) {
31663
31775
  try {
31664
31776
  process.kill(pid, 0);
@@ -31742,7 +31854,7 @@ var LifecycleManager = class {
31742
31854
  this.#removePidFileSync();
31743
31855
  }
31744
31856
  async acquirePidFile(shipyardHome) {
31745
- const pidFilePath = join10(shipyardHome, "daemon.pid");
31857
+ const pidFilePath = join9(shipyardHome, "daemon.pid");
31746
31858
  try {
31747
31859
  const existing = await readFile6(pidFilePath, "utf-8");
31748
31860
  const raw = existing.trim();
@@ -31820,6 +31932,7 @@ var LifecycleManager = class {
31820
31932
  } catch (diagErr) {
31821
31933
  this.#safeLog("warn", { err: diagErr, origin }, "WASM panic diagnostics collection failed");
31822
31934
  }
31935
+ const { jsLeafFrame, jsStack, wasmModuleHash } = extractWasmPanicContext(error2);
31823
31936
  this.#safeLog(
31824
31937
  "warn",
31825
31938
  {
@@ -31827,7 +31940,23 @@ var LifecycleManager = class {
31827
31940
  origin,
31828
31941
  loroCrdtVersion: getLoroCrdtVersion(),
31829
31942
  docs: diagnostics,
31830
- docCount: diagnostics.length
31943
+ docCount: diagnostics.length,
31944
+ /**
31945
+ * JS-side context for panic class disambiguation (issue #2354).
31946
+ * jsLeafFrame: first non-wasm JS frame — identifies the code path
31947
+ * (e.g. LoroMapFinalization → finalization class,
31948
+ * subscribeLocalUpdates → reentry class, etc.)
31949
+ * jsStack: compact non-wasm call stack for triage without a full trace.
31950
+ * wasmModuleHash: discriminates Loro version (00c34872 = 1.11.1 stock,
31951
+ * 00bf5d86 = Shipyard fork 7dfda879, 00c501de = 1.10.8).
31952
+ *
31953
+ * TODO (#2354): add std::panic::set_hook in the vendored Loro wasm
31954
+ * build so this log also carries the Rust-level panic message and
31955
+ * file:line, making every panic class unambiguous.
31956
+ */
31957
+ jsLeafFrame,
31958
+ jsStack,
31959
+ wasmModuleHash
31831
31960
  },
31832
31961
  "WASM runtime panic \u2014 absorbed"
31833
31962
  );
@@ -31939,10 +32068,10 @@ var LifecycleManager = class {
31939
32068
  // src/services/bootstrap/load-or-create-peer-id.ts
31940
32069
  import { randomUUID } from "crypto";
31941
32070
  import { mkdir as mkdir3, readFile as readFile7, rename as rename4, writeFile as writeFile5 } from "fs/promises";
31942
- import { dirname as dirname5, join as join11 } from "path";
32071
+ import { dirname as dirname4, join as join10 } from "path";
31943
32072
  var PEER_ID_FILE = "daemon-peer-id";
31944
32073
  async function loadOrCreateDaemonPeerId(shipyardDataDir) {
31945
- const target = join11(shipyardDataDir, PEER_ID_FILE);
32074
+ const target = join10(shipyardDataDir, PEER_ID_FILE);
31946
32075
  const existing = await readExistingPeerId(target);
31947
32076
  if (existing !== null) return existing;
31948
32077
  const peerId = generatePeerId();
@@ -31967,7 +32096,7 @@ async function readExistingPeerId(target) {
31967
32096
  return candidate;
31968
32097
  }
31969
32098
  async function writePeerIdAtomic(target, peerId) {
31970
- await mkdir3(dirname5(target), { recursive: true });
32099
+ await mkdir3(dirname4(target), { recursive: true });
31971
32100
  const tmp = `${target}.${randomUUID()}.tmp`;
31972
32101
  await writeFile5(tmp, peerId, "utf-8");
31973
32102
  await rename4(tmp, target);
@@ -31975,10 +32104,10 @@ async function writePeerIdAtomic(target, peerId) {
31975
32104
 
31976
32105
  // src/services/bootstrap/pid-tracking.ts
31977
32106
  import { readFile as readFile8, writeFile as writeFile6 } from "fs/promises";
31978
- import { join as join12 } from "path";
32107
+ import { join as join11 } from "path";
31979
32108
  var PID_FILE = "children.pid";
31980
32109
  function buildPidTracker(shipyardHome) {
31981
- const filePath = join12(shipyardHome, PID_FILE);
32110
+ const filePath = join11(shipyardHome, PID_FILE);
31982
32111
  let queue = Promise.resolve();
31983
32112
  function enqueue(fn) {
31984
32113
  const next = queue.then(fn);
@@ -32061,7 +32190,7 @@ function buildReloadDocsFromStorage(repoRef, log) {
32061
32190
  // src/services/bootstrap/signaling.ts
32062
32191
  import { mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
32063
32192
  import { hostname } from "os";
32064
- import { dirname as dirname6, join as join13 } from "path";
32193
+ import { dirname as dirname5, join as join12 } from "path";
32065
32194
 
32066
32195
  // ../../node_modules/.pnpm/nanoid@5.1.6/node_modules/nanoid/index.js
32067
32196
  import { webcrypto as crypto2 } from "crypto";
@@ -32094,7 +32223,7 @@ function nanoid(size2 = 21) {
32094
32223
  }
32095
32224
 
32096
32225
  // src/services/bootstrap/signaling.ts
32097
- var DAEMON_NPM_VERSION = true ? "3.3.0" : "unknown";
32226
+ var DAEMON_NPM_VERSION = true ? "3.3.1" : "unknown";
32098
32227
  function createDaemonSignaling(config2) {
32099
32228
  const agentId = config2.agentId ?? nanoid();
32100
32229
  function send(msg) {
@@ -32123,7 +32252,7 @@ function createDaemonSignaling(config2) {
32123
32252
  };
32124
32253
  }
32125
32254
  function getOrCreateAgentId(shipyardHome) {
32126
- const filePath = join13(shipyardHome, "agent-id");
32255
+ const filePath = join12(shipyardHome, "agent-id");
32127
32256
  try {
32128
32257
  const existing = readFileSync3(filePath, "utf-8").trim();
32129
32258
  if (existing) return existing;
@@ -32131,7 +32260,7 @@ function getOrCreateAgentId(shipyardHome) {
32131
32260
  }
32132
32261
  const id = crypto.randomUUID();
32133
32262
  try {
32134
- mkdirSync(dirname6(filePath), { recursive: true });
32263
+ mkdirSync(dirname5(filePath), { recursive: true });
32135
32264
  writeFileSync(filePath, id, { mode: 384 });
32136
32265
  } catch {
32137
32266
  }
@@ -32339,7 +32468,7 @@ function createPeerRoleRegistry() {
32339
32468
 
32340
32469
  // src/services/epoch-pruning.ts
32341
32470
  import { readdir as readdir4, rm } from "fs/promises";
32342
- import { join as join14 } from "path";
32471
+ import { join as join13 } from "path";
32343
32472
  var LEGACY_PREFIXES = [
32344
32473
  "task-meta",
32345
32474
  "task-conv",
@@ -32379,7 +32508,7 @@ async function pruneOldEpochData(dataDir, currentEpoch, log) {
32379
32508
  }
32380
32509
  if (!shouldPrune(decoded, currentEpoch)) continue;
32381
32510
  removals.push(
32382
- rm(join14(dataDir, entry.name), { recursive: true }).then(() => {
32511
+ rm(join13(dataDir, entry.name), { recursive: true }).then(() => {
32383
32512
  pruned++;
32384
32513
  }).catch((err) => {
32385
32514
  log({
@@ -34003,6 +34132,7 @@ function createLocalDirectPeer(config2) {
34003
34132
  },
34004
34133
  "local-direct attachDataChannel failed"
34005
34134
  );
34135
+ config2.onAttachDataChannelFailed?.();
34006
34136
  channel.close();
34007
34137
  }
34008
34138
  return;
@@ -34385,7 +34515,7 @@ function onConnection(ws, deps, peers) {
34385
34515
 
34386
34516
  // src/services/local-direct/local-direct-token.ts
34387
34517
  import { chmod, mkdir as mkdir4, rename as rename5, rm as rm2, writeFile as writeFile7 } from "fs/promises";
34388
- import { dirname as dirname7, join as join15 } from "path";
34518
+ import { dirname as dirname6, join as join14 } from "path";
34389
34519
  var ADVERTISEMENT_FILE = "local-direct.json";
34390
34520
  var ADVERTISEMENT_MODE = 384;
34391
34521
  var ADVERTISEMENT_DIR_MODE = 448;
@@ -34394,11 +34524,11 @@ function generateLocalDirectToken() {
34394
34524
  return nanoid(TOKEN_LENGTH);
34395
34525
  }
34396
34526
  function advertisementPath(shipyardHome) {
34397
- return join15(shipyardHome, "data", ADVERTISEMENT_FILE);
34527
+ return join14(shipyardHome, "data", ADVERTISEMENT_FILE);
34398
34528
  }
34399
34529
  async function writeAdvertisement(shipyardHome, ad) {
34400
34530
  const target = advertisementPath(shipyardHome);
34401
- const dir = dirname7(target);
34531
+ const dir = dirname6(target);
34402
34532
  await mkdir4(dir, { recursive: true, mode: ADVERTISEMENT_DIR_MODE });
34403
34533
  try {
34404
34534
  await chmod(dir, ADVERTISEMENT_DIR_MODE);
@@ -34426,14 +34556,39 @@ async function deleteAdvertisement(shipyardHome) {
34426
34556
  }
34427
34557
 
34428
34558
  // src/services/local-direct/local-direct-wiring.ts
34559
+ var CANARY_WINDOW_MS = 3e4;
34560
+ var CANARY_THRESHOLD = 5;
34561
+ var CANARY_EMIT_INTERVAL_MS = 3e4;
34562
+ function createAttachFailureCanary(log, logAdapter) {
34563
+ const failureTimes = [];
34564
+ let lastCanaryAt = 0;
34565
+ return function onAttachDataChannelFailed() {
34566
+ const now = Date.now();
34567
+ failureTimes.push(now);
34568
+ const windowStart = now - CANARY_WINDOW_MS;
34569
+ while (failureTimes.length > 0 && (failureTimes[0] ?? 0) < windowStart) {
34570
+ failureTimes.shift();
34571
+ }
34572
+ if (failureTimes.length >= CANARY_THRESHOLD && now - lastCanaryAt >= CANARY_EMIT_INTERVAL_MS) {
34573
+ lastCanaryAt = now;
34574
+ const rate = failureTimes.length;
34575
+ log.warn(
34576
+ { event: "pre_storm_canary", failuresInWindow: rate, windowMs: CANARY_WINDOW_MS },
34577
+ "pre_storm_canary: local-direct attach failure rate elevated \u2014 potential storm incoming"
34578
+ );
34579
+ logAdapter({ event: "pre_storm_canary", failuresInWindow: rate, windowMs: CANARY_WINDOW_MS });
34580
+ }
34581
+ };
34582
+ }
34429
34583
  async function setupLocalDirect(deps) {
34430
34584
  const token = generateLocalDirectToken();
34585
+ const onAttachDataChannelFailed = createAttachFailureCanary(deps.log, deps.logAdapter);
34431
34586
  const server = await createLocalDirectServer({
34432
34587
  token,
34433
34588
  webrtcAdapter: deps.webrtcAdapter,
34434
34589
  log: deps.log,
34435
34590
  logAdapter: deps.logAdapter,
34436
- channelCallbacks: deps.channelCallbacks
34591
+ channelCallbacks: { ...deps.channelCallbacks, onAttachDataChannelFailed }
34437
34592
  });
34438
34593
  await writeAdvertisement(deps.shipyardHome, {
34439
34594
  port: server.port,
@@ -34523,7 +34678,7 @@ function createMetricsCollector(workerUrl, authToken, telemetryEnabled) {
34523
34678
  // src/services/plugins/plugin-file-watcher.ts
34524
34679
  import { existsSync as existsSync3, watch } from "fs";
34525
34680
  import { readdir as readdir5, readFile as readFile9, stat as stat4 } from "fs/promises";
34526
- import { join as join16 } from "path";
34681
+ import { join as join15 } from "path";
34527
34682
  import { pathToFileURL } from "url";
34528
34683
 
34529
34684
  // src/shared/plugins/plugin-registry.ts
@@ -34624,7 +34779,7 @@ var PluginFileWatcher = class {
34624
34779
  }
34625
34780
  const loaded = [];
34626
34781
  for (const entry of entries) {
34627
- const pluginDir = join16(dir, entry);
34782
+ const pluginDir = join15(dir, entry);
34628
34783
  let stats;
34629
34784
  try {
34630
34785
  stats = await stat4(pluginDir);
@@ -34639,7 +34794,7 @@ var PluginFileWatcher = class {
34639
34794
  this.#reconcile(loaded);
34640
34795
  }
34641
34796
  async #loadPlugin(id, pluginDir) {
34642
- const manifestPath = join16(pluginDir, "plugin.json");
34797
+ const manifestPath = join15(pluginDir, "plugin.json");
34643
34798
  let manifestRaw;
34644
34799
  try {
34645
34800
  manifestRaw = await readFile9(manifestPath, "utf-8");
@@ -34666,7 +34821,7 @@ var PluginFileWatcher = class {
34666
34821
  }
34667
34822
  const manifest = parsed.data;
34668
34823
  let template = "";
34669
- const templatePath = join16(pluginDir, "template.html");
34824
+ const templatePath = join15(pluginDir, "template.html");
34670
34825
  try {
34671
34826
  template = await readFile9(templatePath, "utf-8");
34672
34827
  } catch {
@@ -34681,7 +34836,7 @@ var PluginFileWatcher = class {
34681
34836
  };
34682
34837
  }
34683
34838
  async #loadAndRegisterHandler(id, pluginDir, title) {
34684
- const handlerPath = join16(pluginDir, "handler.mjs");
34839
+ const handlerPath = join15(pluginDir, "handler.mjs");
34685
34840
  let handlerFn;
34686
34841
  try {
34687
34842
  const handlerStat = await stat4(handlerPath);
@@ -34856,7 +35011,7 @@ var LIST_MY_TEAMS_QUERY = `
34856
35011
  }
34857
35012
  `;
34858
35013
  var LIST_TEAM_ISSUES_QUERY = `
34859
- query ListTeamIssues($teamId: ID!, $first: Int, $after: String) {
35014
+ query ListTeamIssues($teamId: String!, $first: Int, $after: String) {
34860
35015
  team(id: $teamId) {
34861
35016
  issues(
34862
35017
  first: $first
@@ -35348,7 +35503,7 @@ function handlePluginAuthRequest(pluginId, sendControl, deps, logAdapter) {
35348
35503
  // src/services/port-detection.ts
35349
35504
  import { execFile as execFile3 } from "child_process";
35350
35505
  import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
35351
- import { dirname as dirname8, join as join17 } from "path";
35506
+ import { dirname as dirname7, join as join16 } from "path";
35352
35507
  import { promisify as promisify2 } from "util";
35353
35508
  var execFileAsync = promisify2(execFile3);
35354
35509
  var EXEC_TIMEOUT_MS2 = 5e3;
@@ -35463,7 +35618,7 @@ function hasStringName(val) {
35463
35618
  function resolveProjectRoot(cwd) {
35464
35619
  let dir = cwd;
35465
35620
  while (true) {
35466
- const candidate = join17(dir, "package.json");
35621
+ const candidate = join16(dir, "package.json");
35467
35622
  if (existsSync4(candidate)) {
35468
35623
  let packageName;
35469
35624
  try {
@@ -35476,7 +35631,7 @@ function resolveProjectRoot(cwd) {
35476
35631
  }
35477
35632
  return { projectRoot: dir, packageName };
35478
35633
  }
35479
- const parent = dirname8(dir);
35634
+ const parent = dirname7(dir);
35480
35635
  if (parent === dir) {
35481
35636
  return {};
35482
35637
  }
@@ -35573,6 +35728,175 @@ async function getCwdByPid(pids) {
35573
35728
  }
35574
35729
  }
35575
35730
 
35731
+ // src/services/preview/preview-state-store.ts
35732
+ import { randomUUID as randomUUID2 } from "crypto";
35733
+ import {
35734
+ mkdirSync as mkdirSync2,
35735
+ readdirSync,
35736
+ readFileSync as readFileSync5,
35737
+ renameSync,
35738
+ unlinkSync as unlinkSync2,
35739
+ writeFileSync as writeFileSync2
35740
+ } from "fs";
35741
+ import { join as join17 } from "path";
35742
+ function isEnoent2(err) {
35743
+ return err instanceof Error && Reflect.get(err, "code") === "ENOENT";
35744
+ }
35745
+ function atomicWriteSync(filePath, content) {
35746
+ const tmpPath = `${filePath}.${randomUUID2()}.tmp`;
35747
+ writeFileSync2(tmpPath, content, "utf-8");
35748
+ renameSync(tmpPath, filePath);
35749
+ }
35750
+ function recordFilePath(rootDir, taskId, elementId) {
35751
+ return join17(rootDir, taskId, `${elementId}.json`);
35752
+ }
35753
+ function parseRecord(filePath, logger2) {
35754
+ let raw;
35755
+ try {
35756
+ raw = readFileSync5(filePath, "utf-8");
35757
+ } catch {
35758
+ return null;
35759
+ }
35760
+ let obj;
35761
+ try {
35762
+ obj = JSON.parse(raw);
35763
+ } catch {
35764
+ logger2.warn({ filePath }, "preview_state_store: invalid JSON, skipping");
35765
+ return null;
35766
+ }
35767
+ const result = PreviewElementStateShape.safeParse(obj);
35768
+ if (!result.success) {
35769
+ logger2.warn(
35770
+ { filePath, issues: result.error.issues },
35771
+ "preview_state_store: schema validation failed, skipping"
35772
+ );
35773
+ return null;
35774
+ }
35775
+ return result.data;
35776
+ }
35777
+ function createPreviewStateStore(opts) {
35778
+ const { rootDir, logger: logger2 } = opts;
35779
+ const registry = /* @__PURE__ */ new Map();
35780
+ let sendFn = null;
35781
+ mkdirSync2(rootDir, { recursive: true });
35782
+ function getTaskMap(taskId) {
35783
+ let taskMap = registry.get(taskId);
35784
+ if (!taskMap) {
35785
+ taskMap = /* @__PURE__ */ new Map();
35786
+ registry.set(taskId, taskMap);
35787
+ }
35788
+ return taskMap;
35789
+ }
35790
+ function scanTaskDir(taskId) {
35791
+ const taskPath = join17(rootDir, taskId);
35792
+ let files;
35793
+ try {
35794
+ files = readdirSync(taskPath);
35795
+ } catch {
35796
+ return;
35797
+ }
35798
+ for (const file of files) {
35799
+ if (!file.endsWith(".json")) continue;
35800
+ const record = parseRecord(join17(taskPath, file), logger2);
35801
+ if (record) getTaskMap(taskId).set(record.elementId, record);
35802
+ }
35803
+ }
35804
+ function scanRootDir() {
35805
+ let taskDirs;
35806
+ try {
35807
+ taskDirs = readdirSync(rootDir);
35808
+ } catch {
35809
+ return;
35810
+ }
35811
+ for (const taskId of taskDirs) {
35812
+ scanTaskDir(taskId);
35813
+ }
35814
+ }
35815
+ scanRootDir();
35816
+ function broadcastTask(taskId) {
35817
+ if (!sendFn) return;
35818
+ const taskMap = registry.get(taskId);
35819
+ const entries = taskMap ? [...taskMap.values()] : [];
35820
+ entries.sort((a, b2) => a.elementId < b2.elementId ? -1 : a.elementId > b2.elementId ? 1 : 0);
35821
+ sendFn({ type: "preview_elements_state", taskId, entries });
35822
+ }
35823
+ function writeToDisk(taskId, state) {
35824
+ const filePath = recordFilePath(rootDir, taskId, state.elementId);
35825
+ try {
35826
+ mkdirSync2(join17(rootDir, taskId), { recursive: true });
35827
+ atomicWriteSync(filePath, JSON.stringify(state, null, 2));
35828
+ } catch (err) {
35829
+ logger2.warn(
35830
+ {
35831
+ taskId,
35832
+ elementId: state.elementId,
35833
+ err: err instanceof Error ? err.message : String(err)
35834
+ },
35835
+ "preview_state_store: putState disk write failed"
35836
+ );
35837
+ }
35838
+ }
35839
+ function removeFromDisk(taskId, elementId) {
35840
+ const filePath = recordFilePath(rootDir, taskId, elementId);
35841
+ try {
35842
+ unlinkSync2(filePath);
35843
+ } catch (err) {
35844
+ if (!isEnoent2(err)) {
35845
+ logger2.warn(
35846
+ { taskId, elementId, err: err instanceof Error ? err.message : String(err) },
35847
+ "preview_state_store: deleteByElementId disk unlink failed"
35848
+ );
35849
+ }
35850
+ }
35851
+ }
35852
+ return {
35853
+ getByElementId(taskId, elementId) {
35854
+ return registry.get(taskId)?.get(elementId);
35855
+ },
35856
+ findByProjectRoot(taskId, projectRoot) {
35857
+ const taskMap = registry.get(taskId);
35858
+ if (!taskMap) return void 0;
35859
+ for (const state of taskMap.values()) {
35860
+ if (state.projectRoot === projectRoot) return state;
35861
+ }
35862
+ return void 0;
35863
+ },
35864
+ putState(taskId, state) {
35865
+ writeToDisk(taskId, state);
35866
+ const taskMap = getTaskMap(taskId);
35867
+ taskMap.set(state.elementId, state);
35868
+ broadcastTask(taskId);
35869
+ },
35870
+ deleteByElementId(taskId, elementId) {
35871
+ const taskMap = registry.get(taskId);
35872
+ if (!taskMap) return;
35873
+ const existed = taskMap.has(elementId);
35874
+ if (!existed) return;
35875
+ removeFromDisk(taskId, elementId);
35876
+ taskMap.delete(elementId);
35877
+ if (taskMap.size === 0) registry.delete(taskId);
35878
+ broadcastTask(taskId);
35879
+ },
35880
+ listByTask(taskId) {
35881
+ const taskMap = registry.get(taskId);
35882
+ if (!taskMap) return [];
35883
+ const entries = [...taskMap.values()];
35884
+ entries.sort((a, b2) => a.elementId < b2.elementId ? -1 : a.elementId > b2.elementId ? 1 : 0);
35885
+ return entries;
35886
+ },
35887
+ setSendControlMessage(fn) {
35888
+ sendFn = fn;
35889
+ },
35890
+ broadcast(taskId) {
35891
+ broadcastTask(taskId);
35892
+ },
35893
+ dispose() {
35894
+ registry.clear();
35895
+ sendFn = null;
35896
+ }
35897
+ };
35898
+ }
35899
+
35576
35900
  // src/services/preview-proxy.ts
35577
35901
  import http2 from "http";
35578
35902
 
@@ -36449,30 +36773,31 @@ function createPreviewProxy(config2) {
36449
36773
  }
36450
36774
 
36451
36775
  // src/services/publish/published-artifact-store.ts
36452
- import { createHash, randomUUID as randomUUID2 } from "crypto";
36453
- import { mkdirSync as mkdirSync2, readdirSync, readFileSync as readFileSync5, watch as watch2 } from "fs";
36776
+ import { createHash, randomUUID as randomUUID3 } from "crypto";
36777
+ import { mkdirSync as mkdirSync3, readdirSync as readdirSync2, readFileSync as readFileSync6, watch as watch2 } from "fs";
36454
36778
  import { mkdir as mkdir5, readFile as readFile10, rename as rename6, unlink as unlink4, writeFile as writeFile8 } from "fs/promises";
36455
- import { dirname as dirname9, join as join18, relative as relative2 } from "path";
36779
+ import { dirname as dirname8, join as join18, relative as relative2 } from "path";
36780
+ import { isDeepStrictEqual } from "util";
36456
36781
  var DEBOUNCE_MS2 = 200;
36457
36782
  function bindMapKey(bindKind, bindKey) {
36458
36783
  return `${bindKind}:${bindKey}`;
36459
36784
  }
36460
- function recordFilePath(rootDir, taskId, bindKind, bindKey) {
36785
+ function recordFilePath2(rootDir, taskId, bindKind, bindKey) {
36461
36786
  return join18(rootDir, taskId, bindKind, `${bindKey}.json`);
36462
36787
  }
36463
36788
  function sha256Hex(input) {
36464
36789
  return createHash("sha256").update(input, "utf-8").digest("hex");
36465
36790
  }
36466
36791
  async function atomicWrite(filePath, content) {
36467
- await mkdir5(dirname9(filePath), { recursive: true });
36468
- const tmpPath = `${filePath}.${randomUUID2()}.tmp`;
36792
+ await mkdir5(dirname8(filePath), { recursive: true });
36793
+ const tmpPath = `${filePath}.${randomUUID3()}.tmp`;
36469
36794
  await writeFile8(tmpPath, content, "utf-8");
36470
36795
  await rename6(tmpPath, filePath);
36471
36796
  }
36472
- function parseRecord(filePath, logger2) {
36797
+ function parseRecord2(filePath, logger2) {
36473
36798
  let raw;
36474
36799
  try {
36475
- raw = readFileSync5(filePath, "utf-8");
36800
+ raw = readFileSync6(filePath, "utf-8");
36476
36801
  } catch {
36477
36802
  return null;
36478
36803
  }
@@ -36493,7 +36818,7 @@ function parseRecord(filePath, logger2) {
36493
36818
  }
36494
36819
  return result.data;
36495
36820
  }
36496
- function isEnoent2(err) {
36821
+ function isEnoent3(err) {
36497
36822
  return err instanceof Error && Reflect.get(err, "code") === "ENOENT";
36498
36823
  }
36499
36824
  function createPublishedArtifactStore(opts) {
@@ -36501,20 +36826,19 @@ function createPublishedArtifactStore(opts) {
36501
36826
  const registry = /* @__PURE__ */ new Map();
36502
36827
  let sendFn = null;
36503
36828
  const debounceTimers = /* @__PURE__ */ new Map();
36504
- const ownWrites = /* @__PURE__ */ new Set();
36505
36829
  let watcher = null;
36506
36830
  let disposed = false;
36507
- mkdirSync2(rootDir, { recursive: true });
36831
+ mkdirSync3(rootDir, { recursive: true });
36508
36832
  function scanKindDir(taskId, kindPath) {
36509
36833
  let files;
36510
36834
  try {
36511
- files = readdirSync(kindPath);
36835
+ files = readdirSync2(kindPath);
36512
36836
  } catch {
36513
36837
  return;
36514
36838
  }
36515
36839
  for (const file of files) {
36516
36840
  if (!file.endsWith(".json")) continue;
36517
- const record = parseRecord(join18(kindPath, file), logger2);
36841
+ const record = parseRecord2(join18(kindPath, file), logger2);
36518
36842
  if (record) inMemorySet(taskId, record);
36519
36843
  }
36520
36844
  }
@@ -36522,7 +36846,7 @@ function createPublishedArtifactStore(opts) {
36522
36846
  const taskPath = join18(rootDir, taskId);
36523
36847
  let kindDirs;
36524
36848
  try {
36525
- kindDirs = readdirSync(taskPath);
36849
+ kindDirs = readdirSync2(taskPath);
36526
36850
  } catch {
36527
36851
  return;
36528
36852
  }
@@ -36534,7 +36858,7 @@ function createPublishedArtifactStore(opts) {
36534
36858
  function scanRootDir() {
36535
36859
  let taskDirs;
36536
36860
  try {
36537
- taskDirs = readdirSync(rootDir);
36861
+ taskDirs = readdirSync2(rootDir);
36538
36862
  } catch {
36539
36863
  return;
36540
36864
  }
@@ -36580,10 +36904,6 @@ function createPublishedArtifactStore(opts) {
36580
36904
  return { taskId, bindKind: kindStr, bindKey };
36581
36905
  }
36582
36906
  function scheduleFileEvent(filePath) {
36583
- if (ownWrites.has(filePath)) {
36584
- ownWrites.delete(filePath);
36585
- return;
36586
- }
36587
36907
  const prev = debounceTimers.get(filePath);
36588
36908
  if (prev) clearTimeout(prev);
36589
36909
  debounceTimers.set(
@@ -36607,6 +36927,8 @@ function createPublishedArtifactStore(opts) {
36607
36927
  try {
36608
36928
  raw = await readFile10(filePath, "utf-8");
36609
36929
  } catch {
36930
+ const existing2 = registry.get(taskId)?.get(bindMapKey(bindKind, bindKey));
36931
+ if (!existing2) return;
36610
36932
  inMemoryDelete(taskId, bindKind, bindKey);
36611
36933
  broadcastTask(taskId);
36612
36934
  return;
@@ -36626,6 +36948,8 @@ function createPublishedArtifactStore(opts) {
36626
36948
  );
36627
36949
  return;
36628
36950
  }
36951
+ const existing = registry.get(taskId)?.get(bindMapKey(bindKind, bindKey));
36952
+ if (existing && isDeepStrictEqual(existing, result.data)) return;
36629
36953
  inMemorySet(taskId, result.data);
36630
36954
  broadcastTask(taskId);
36631
36955
  }
@@ -36658,27 +36982,17 @@ function createPublishedArtifactStore(opts) {
36658
36982
  return entries;
36659
36983
  },
36660
36984
  async putBinding(taskId, record) {
36661
- const filePath = recordFilePath(rootDir, taskId, record.bindKind, record.bindKey);
36662
- ownWrites.add(filePath);
36663
- try {
36664
- await atomicWrite(filePath, JSON.stringify(record, null, 2));
36665
- } catch (err) {
36666
- ownWrites.delete(filePath);
36667
- throw err;
36668
- }
36985
+ const filePath = recordFilePath2(rootDir, taskId, record.bindKind, record.bindKey);
36986
+ await atomicWrite(filePath, JSON.stringify(record, null, 2));
36669
36987
  inMemorySet(taskId, record);
36670
36988
  broadcastTask(taskId);
36671
36989
  },
36672
36990
  async deleteBinding(taskId, bindKind, bindKey) {
36673
- const filePath = recordFilePath(rootDir, taskId, bindKind, bindKey);
36674
- ownWrites.add(filePath);
36991
+ const filePath = recordFilePath2(rootDir, taskId, bindKind, bindKey);
36675
36992
  try {
36676
36993
  await unlink4(filePath);
36677
36994
  } catch (err) {
36678
- if (!isEnoent2(err)) {
36679
- ownWrites.delete(filePath);
36680
- throw err;
36681
- }
36995
+ if (!isEnoent3(err)) throw err;
36682
36996
  }
36683
36997
  inMemoryDelete(taskId, bindKind, bindKey);
36684
36998
  broadcastTask(taskId);
@@ -37878,44 +38192,51 @@ async function storePreviewArtifact(input, bindKey, resolvedProjectRoot, existin
37878
38192
  expiresAt: result.expiresAt,
37879
38193
  ttl: input.ttl,
37880
38194
  lastSize: files.reduce((sum, f2) => sum + f2.content.length, 0),
37881
- projectRoot: resolvedProjectRoot ?? void 0,
38195
+ projectRoot: resolvedProjectRoot,
37882
38196
  canvasElementId: input.target.kind === "previewPort" ? input.target.canvasElementId : void 0
37883
38197
  };
37884
38198
  await input.publishedArtifactStore.putBinding(input.taskId, newRecord);
37885
38199
  }
38200
+ function resolvePreviewProjectRoot(input, target) {
38201
+ const canvasElementId = target.canvasElementId;
38202
+ const storeEntry = canvasElementId && input.previewStateStore ? input.previewStateStore.getByElementId(input.taskId, canvasElementId) ?? null : null;
38203
+ const storeProjectRoot = storeEntry?.projectRoot ?? null;
38204
+ const detectedPortMatch = input.getDetectedPorts().find((p2) => p2.port === target.previewPort);
38205
+ const detectedProjectRoot = detectedPortMatch?.projectRoot ?? null;
38206
+ const resolvedProjectRoot = storeProjectRoot ?? detectedProjectRoot ?? null;
38207
+ const resolvedFrom = storeProjectRoot ? "store" : detectedProjectRoot ? "detected_ports" : "none";
38208
+ return { resolvedProjectRoot, resolvedFrom, storeProjectRoot, detectedProjectRoot };
38209
+ }
37886
38210
  async function runPublishPreviewPort(input, ctx) {
37887
38211
  if (input.target.kind !== "previewPort") {
37888
38212
  return { ok: false, phase: "failed", error: "Internal error: wrong target kind" };
37889
38213
  }
37890
- const agentHint = input.target.projectRoot;
37891
- const previewPortNumber = input.target.previewPort;
37892
- const detectedPortMatch = input.getDetectedPorts().find((p2) => p2.port === previewPortNumber);
37893
- const resolvedProjectRoot = agentHint ?? detectedPortMatch?.projectRoot ?? null;
37894
- const bindKey = resolvedProjectRoot !== null ? sha256Hex(resolvedProjectRoot) : null;
37895
- const existing = bindKey !== null ? input.publishedArtifactStore?.getBinding(input.taskId, "preview", bindKey) ?? null : null;
37896
- const buildRoot = resolvedProjectRoot ?? input.projectRoot;
38214
+ const target = input.target;
38215
+ const canvasElementId = target.canvasElementId;
38216
+ const resolution = resolvePreviewProjectRoot(input, target);
38217
+ const { resolvedProjectRoot } = resolution;
37897
38218
  ctx.log?.({
37898
38219
  event: "publish_preview_resolved",
37899
- agentHint: agentHint ?? null,
37900
- detectedPortMatch: detectedPortMatch?.projectRoot ?? null,
38220
+ canvasElementId: canvasElementId ?? null,
38221
+ previewPort: target.previewPort,
38222
+ storeProjectRoot: resolution.storeProjectRoot,
38223
+ detectedPortProjectRoot: resolution.detectedProjectRoot,
37901
38224
  resolvedProjectRoot,
37902
- buildRoot
38225
+ resolvedFrom: resolution.resolvedFrom
37903
38226
  });
37904
- const built = await detectAndBuild(buildRoot, ctx);
38227
+ if (!resolvedProjectRoot) {
38228
+ const error2 = canvasElementId ? `Preview element has no resolvable projectRoot \u2014 was \`present({projectRoot})\` called? (canvasElementId: ${canvasElementId})` : "Preview publish requires a canvasElementId. The browser must send the stable canvas element id so the daemon can resolve projectRoot from the preview state store.";
38229
+ ctx.onProgress("failed", error2);
38230
+ return { ok: false, phase: "detecting_framework", error: error2 };
38231
+ }
38232
+ const bindKey = sha256Hex(resolvedProjectRoot);
38233
+ const existing = input.publishedArtifactStore?.getBinding(input.taskId, "preview", bindKey) ?? null;
38234
+ const built = await detectAndBuild(resolvedProjectRoot, ctx);
37905
38235
  if (!built.ok) return built.outcome;
37906
38236
  ctx.onProgress("uploading");
37907
38237
  try {
37908
38238
  const result = await uploadPreviewFiles(input, built.framework, built.files, existing);
37909
- if (bindKey !== null) {
37910
- await storePreviewArtifact(
37911
- input,
37912
- bindKey,
37913
- resolvedProjectRoot,
37914
- existing,
37915
- result,
37916
- built.files
37917
- );
37918
- }
38239
+ await storePreviewArtifact(input, bindKey, resolvedProjectRoot, existing, result, built.files);
37919
38240
  ctx.onProgress("completed");
37920
38241
  return { ok: true, id: result.id, url: result.url, expiresAt: result.expiresAt };
37921
38242
  } catch (err) {
@@ -37966,6 +38287,7 @@ function createPublishTools(ctx) {
37966
38287
  taskId: ctx.taskId.current,
37967
38288
  vizWatcher: ctx.vizWatcher,
37968
38289
  publishedArtifactStore: ctx.publishedArtifactStore,
38290
+ previewStateStore: ctx.previewStateStore,
37969
38291
  getDetectedPorts: ctx.getDetectedPorts
37970
38292
  },
37971
38293
  { onProgress: () => {
@@ -38068,6 +38390,7 @@ async function handlePublishRequest(params) {
38068
38390
  taskId: params.taskId,
38069
38391
  vizWatcher: params.vizWatcher,
38070
38392
  publishedArtifactStore: params.publishedArtifactStore,
38393
+ previewStateStore: params.previewStateStore,
38071
38394
  getDetectedPorts: params.getDetectedPorts
38072
38395
  },
38073
38396
  { onProgress: emitProgress, ...params.log !== void 0 && { log: params.log } }
@@ -39178,38 +39501,22 @@ async function rehydrateCollabQueues(persistence, taskManager, log) {
39178
39501
  })
39179
39502
  );
39180
39503
  }
39181
- function isPreviewData(raw) {
39182
- return typeof raw === "object" && raw !== null && !Array.isArray(raw);
39183
- }
39184
- function collectOwnedPreviews(canvasRepo, taskId, selfUserId) {
39185
- try {
39186
- const elements = canvasRepo.getElements(taskId);
39187
- const ports = [];
39188
- for (const el of elements) {
39189
- if (el.data.type !== "preview") continue;
39190
- const data = isPreviewData(el.data.data) ? el.data.data : null;
39191
- if (!data || data.ownerUserId !== selfUserId) continue;
39192
- if (typeof data.port === "number") ports.push(data.port);
39193
- }
39194
- return ports;
39195
- } catch {
39196
- return [];
39197
- }
39504
+ function collectOwnedPreviews(_canvasRepo, _taskId, _selfUserId) {
39505
+ return [];
39198
39506
  }
39199
39507
  async function rehydrateCanvasPreviews(canvasRepo, previewProxy, selfUserId, taskStateStore, log) {
39200
39508
  const tasks = await taskStateStore.listTasks();
39201
39509
  const refs = [];
39202
39510
  for (const taskId of Object.keys(tasks)) {
39203
39511
  if (!await canvasRepo.hasLocalDoc(taskId)) continue;
39204
- for (const port of collectOwnedPreviews(canvasRepo, taskId, selfUserId)) {
39205
- refs.push({ taskId, port });
39512
+ for (const ref of collectOwnedPreviews(canvasRepo, taskId, selfUserId)) {
39513
+ refs.push(ref);
39206
39514
  }
39207
39515
  }
39208
39516
  const warmedTaskIds = /* @__PURE__ */ new Set();
39209
39517
  for (const { taskId, port } of refs) {
39210
39518
  try {
39211
- const newProxyPort = await previewProxy.acquire(port);
39212
- await reconcileProxyPortInCanvas(canvasRepo, taskId, port, newProxyPort);
39519
+ await previewProxy.acquire(port);
39213
39520
  warmedTaskIds.add(taskId);
39214
39521
  log({ event: "canvas_preview_warmed", taskId, port });
39215
39522
  } catch (err) {
@@ -39223,19 +39530,6 @@ async function rehydrateCanvasPreviews(canvasRepo, previewProxy, selfUserId, tas
39223
39530
  }
39224
39531
  return Array.from(warmedTaskIds);
39225
39532
  }
39226
- async function reconcileProxyPortInCanvas(canvasRepo, taskId, upstreamPort, newProxyPort) {
39227
- if (!await canvasRepo.hasLocalDoc(taskId)) return;
39228
- for (const el of canvasRepo.getElements(taskId)) {
39229
- if (el.data.type !== "preview") continue;
39230
- const rec = el.data.data;
39231
- if (!isPreviewData(rec)) continue;
39232
- if (rec.port !== upstreamPort) continue;
39233
- if (rec.proxyPort === newProxyPort) continue;
39234
- canvasRepo.updateElement(taskId, el.id, {
39235
- data: { ...rec, proxyPort: newProxyPort }
39236
- });
39237
- }
39238
- }
39239
39533
  async function migratePinnedToFavoriteTasks(taskStateStore, userSettingsStore, log) {
39240
39534
  const settings = await userSettingsStore.getSettings();
39241
39535
  if (settings.favoriteTasks.length > 0) return;
@@ -39245,6 +39539,77 @@ async function migratePinnedToFavoriteTasks(taskStateStore, userSettingsStore, l
39245
39539
  await userSettingsStore.updateSetting("favoriteTasks", pinnedIds);
39246
39540
  log({ event: "pinned_to_favorites_migrated", count: pinnedIds.length });
39247
39541
  }
39542
+ function isObjectRecord(v2) {
39543
+ return typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
39544
+ }
39545
+ function extractLegacyPreviewState(elementId, rawData) {
39546
+ if (!isObjectRecord(rawData)) return null;
39547
+ const ownerUserId = typeof rawData.ownerUserId === "string" && rawData.ownerUserId.length > 0 ? rawData.ownerUserId : null;
39548
+ if (!ownerUserId) return null;
39549
+ const state = { elementId, ownerUserId };
39550
+ let hasLegacyField = false;
39551
+ if (typeof rawData.port === "number" && Number.isInteger(rawData.port) && rawData.port > 0) {
39552
+ state.port = rawData.port;
39553
+ hasLegacyField = true;
39554
+ }
39555
+ if (typeof rawData.url === "string" && rawData.url.length > 0) {
39556
+ state.url = rawData.url;
39557
+ hasLegacyField = true;
39558
+ }
39559
+ if (typeof rawData.proxyPort === "number" && Number.isInteger(rawData.proxyPort) && rawData.proxyPort > 0) {
39560
+ state.proxyPort = rawData.proxyPort;
39561
+ hasLegacyField = true;
39562
+ }
39563
+ if (typeof rawData.initialPath === "string" && rawData.initialPath.length > 0) {
39564
+ state.initialPath = rawData.initialPath;
39565
+ hasLegacyField = true;
39566
+ }
39567
+ if (typeof rawData.projectRoot === "string" && rawData.projectRoot.length > 0) {
39568
+ state.projectRoot = rawData.projectRoot;
39569
+ hasLegacyField = true;
39570
+ }
39571
+ return hasLegacyField ? state : null;
39572
+ }
39573
+ function migrateLegacyPreviewDataForTask(taskId, canvasRepo, store, log) {
39574
+ let seededCount = 0;
39575
+ const elements = canvasRepo.getElements(taskId);
39576
+ for (const el of elements) {
39577
+ if (el.data.type !== "preview") continue;
39578
+ const elementId = String(el.id);
39579
+ if (store.getByElementId(taskId, elementId)) continue;
39580
+ const legacy = extractLegacyPreviewState(elementId, el.data.data);
39581
+ if (!legacy) continue;
39582
+ store.putState(taskId, legacy);
39583
+ seededCount += 1;
39584
+ log({
39585
+ event: "preview_legacy_migrated",
39586
+ taskId,
39587
+ elementId,
39588
+ port: legacy.port,
39589
+ projectRoot: legacy.projectRoot
39590
+ });
39591
+ }
39592
+ return seededCount;
39593
+ }
39594
+ async function migrateLegacyPreviewData(canvasRepo, previewStateStore, taskStateStore, log) {
39595
+ const tasks = await taskStateStore.listTasks();
39596
+ let totalSeeded = 0;
39597
+ for (const taskId of Object.keys(tasks)) {
39598
+ if (!await canvasRepo.hasLocalDoc(taskId)) continue;
39599
+ try {
39600
+ totalSeeded += migrateLegacyPreviewDataForTask(taskId, canvasRepo, previewStateStore, log);
39601
+ } catch (err) {
39602
+ log({
39603
+ event: "preview_legacy_migrate_task_failed",
39604
+ taskId,
39605
+ error: err instanceof Error ? err.message : String(err)
39606
+ });
39607
+ }
39608
+ }
39609
+ if (totalSeeded > 0) {
39610
+ log({ event: "preview_legacy_migrate_complete", seededCount: totalSeeded });
39611
+ }
39612
+ }
39248
39613
  async function sweepStaleTasks(taskStateStore, taskManager, log) {
39249
39614
  const tasks = await taskStateStore.listTasks();
39250
39615
  const actions = planStaleSweep(tasks, (id) => taskManager.isRunning(id));
@@ -39372,7 +39737,7 @@ function assertNever3(value) {
39372
39737
 
39373
39738
  // src/services/bootstrap/self-update-lock.ts
39374
39739
  import { mkdir as mkdir7, readFile as readFile16, stat as stat5, unlink as unlink6, writeFile as writeFile11 } from "fs/promises";
39375
- import { dirname as dirname10, join as join24 } from "path";
39740
+ import { dirname as dirname9, join as join24 } from "path";
39376
39741
  var LOCK_FILENAME = ".lock";
39377
39742
  var STALE_LOCK_MS = 10 * 60 * 1e3;
39378
39743
  var LockFileSchema = external_exports.object({
@@ -39464,7 +39829,7 @@ async function resolveEexist(shipyardHome, path2, ownerPid, now, isProcessAlive2
39464
39829
  }
39465
39830
  async function tryAcquireLockExclusive(shipyardHome, pid, now, isProcessAlive2) {
39466
39831
  const path2 = lockPath(shipyardHome);
39467
- await mkdir7(dirname10(path2), { recursive: true });
39832
+ await mkdir7(dirname9(path2), { recursive: true });
39468
39833
  const body = JSON.stringify({ pid, startedAt: now() });
39469
39834
  while (true) {
39470
39835
  try {
@@ -40025,7 +40390,7 @@ function createCanvasResourceResolver(deps) {
40025
40390
 
40026
40391
  // src/services/credentials/vault-key-manager.ts
40027
40392
  import { mkdir as mkdir8, readFile as readFile17, writeFile as writeFile12 } from "fs/promises";
40028
- import { dirname as dirname11, join as join26 } from "path";
40393
+ import { dirname as dirname10, join as join26 } from "path";
40029
40394
 
40030
40395
  // src/services/credentials/vault-crypto.ts
40031
40396
  async function generateVaultKey() {
@@ -40111,12 +40476,12 @@ async function resolveMachineId(shipyardHome) {
40111
40476
  } catch {
40112
40477
  }
40113
40478
  const id = globalThis.crypto.randomUUID();
40114
- await mkdir8(dirname11(idPath), { recursive: true });
40479
+ await mkdir8(dirname10(idPath), { recursive: true });
40115
40480
  await writeFile12(idPath, id, { encoding: "utf-8", mode: 384 });
40116
40481
  return id;
40117
40482
  }
40118
40483
  async function saveKeyLocally(keyPath, key) {
40119
- await mkdir8(dirname11(keyPath), { recursive: true });
40484
+ await mkdir8(dirname10(keyPath), { recursive: true });
40120
40485
  await writeFile12(keyPath, key, { encoding: "utf-8", mode: 384 });
40121
40486
  }
40122
40487
 
@@ -40814,7 +41179,7 @@ function createGitCheckpointService() {
40814
41179
  import { createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
40815
41180
 
40816
41181
  // src/services/harness/comment-server.ts
40817
- import { randomUUID as randomUUID3 } from "crypto";
41182
+ import { randomUUID as randomUUID4 } from "crypto";
40818
41183
  import { access as access2, readFile as readFile20 } from "fs/promises";
40819
41184
  import { join as join29, relative as relative3 } from "path";
40820
41185
  var TOOL_DESCRIPTION = [
@@ -40933,7 +41298,7 @@ function resolveShortCommentId(annotations, input) {
40933
41298
  }
40934
41299
  function buildReplyAnnotation(parent, body) {
40935
41300
  const base3 = {
40936
- commentId: randomUUID3(),
41301
+ commentId: randomUUID4(),
40937
41302
  body,
40938
41303
  authorName: "Claude Code",
40939
41304
  authorKind: "agent",
@@ -41235,7 +41600,7 @@ async function handleReplyComment(ctx, parentCommentId, comment2) {
41235
41600
  async function handlePlanComment(ctx, anchorText, comment2) {
41236
41601
  const annotation = {
41237
41602
  annotationType: "plan-text",
41238
- commentId: randomUUID3(),
41603
+ commentId: randomUUID4(),
41239
41604
  anchorText,
41240
41605
  anchorContext: null,
41241
41606
  body: comment2,
@@ -41285,7 +41650,7 @@ async function handleDiffComment(ctx, filePath, lineNumber, comment2) {
41285
41650
  const lineContentHash = lineContent ? djb2Hash(lineContent) : "";
41286
41651
  const annotation = {
41287
41652
  annotationType: "diff-hunk",
41288
- commentId: randomUUID3(),
41653
+ commentId: randomUUID4(),
41289
41654
  filePath: normalizedPath,
41290
41655
  lineNumber,
41291
41656
  lineContent,
@@ -41977,7 +42342,7 @@ var PresentPortSchema = external_exports.object({
41977
42342
  width: external_exports.number().int().positive().optional().describe("Canvas only."),
41978
42343
  height: external_exports.number().int().positive().optional().describe("Canvas only."),
41979
42344
  projectRoot: external_exports.string().optional().describe(
41980
- "Absolute path to the dev server's package.json directory. Overrides cwd-based auto-detection. Provide this when you know the project root (e.g. the app you just spawned) \u2014 it makes the canvas publish URL stable across port changes. Use for Docker / remote / monorepo cases where lsof cwd detection would land on the wrong package.json."
42345
+ "Absolute path to the dev server's package.json directory. Binds this preview to a stable app identity. Restarting your dev server on a new port (or a fresh `present` call) with the same projectRoot will reuse the same canvas element \u2014 no duplicate frames, and the published URL is stable across port restarts. Without projectRoot, every port change creates a new element."
41981
42346
  )
41982
42347
  });
41983
42348
  var PresentUrlSchema = external_exports.object({
@@ -42006,7 +42371,7 @@ var PresentToolInputShape = {
42006
42371
  height: external_exports.number().int().positive().optional().describe("Canvas only; ignored elsewhere."),
42007
42372
  initialPath: external_exports.string().optional().describe('SPA route for source="port" on canvas (e.g. "/settings").'),
42008
42373
  projectRoot: external_exports.string().optional().describe(
42009
- `Absolute path to the dev server's package.json dir (source="port" only). Overrides lsof cwd auto-detect. Provide when you know it \u2014 makes the publish URL stable across port restarts.`
42374
+ `Absolute path to the dev server's package.json dir (source="port" only). Binds this preview to a stable app identity. Restarting your dev server on a new port with the same projectRoot will reuse the same canvas element \u2014 no duplicate frames, and the published URL is stable across port restarts. Without projectRoot, every port change creates a new element.`
42010
42375
  ),
42011
42376
  force: external_exports.boolean().optional().describe("Re-apply an already-placed slug to canvas after editing. Slug+canvas only.")
42012
42377
  };
@@ -51285,6 +51650,7 @@ function createHarnessMcpServer(ctx) {
51285
51650
  projectRoot: ctx.projectRoot,
51286
51651
  previewProxy: ctx.previewProxy,
51287
51652
  publishedArtifactStore: ctx.publishedArtifactStore,
51653
+ previewStateStore: ctx.previewStateStore,
51288
51654
  getDetectedPorts: ctx.getDetectedPorts
51289
51655
  };
51290
51656
  const server = createSdkMcpServer({
@@ -51307,37 +51673,53 @@ function createHarnessMcpServer(ctx) {
51307
51673
  }
51308
51674
 
51309
51675
  // src/services/harness/visualization-file-watcher.ts
51310
- import { randomUUID as randomUUID4 } from "crypto";
51676
+ import { randomUUID as randomUUID5 } from "crypto";
51311
51677
  import { watch as watch3 } from "fs";
51312
51678
  import { mkdir as mkdir10, readFile as readFile22, rename as rename9, writeFile as writeFile15 } from "fs/promises";
51313
- import { dirname as dirname12, join as join31 } from "path";
51679
+ import { dirname as dirname11, join as join31 } from "path";
51314
51680
  var PREVIEW_DEFAULT_W = 1200;
51315
51681
  var PREVIEW_DEFAULT_H = 800;
51316
51682
  function previewDataToLoroValue(data) {
51317
- const out = { ownerUserId: data.ownerUserId };
51318
- if (data.port !== void 0) out.port = data.port;
51319
- if (data.url !== void 0) out.url = data.url;
51320
- if (data.proxyPort !== void 0) out.proxyPort = data.proxyPort;
51321
- if (data.initialPath !== void 0) out.initialPath = data.initialPath;
51322
- return out;
51683
+ return { ownerUserId: data.ownerUserId };
51323
51684
  }
51324
- function isObjectRecord(v2) {
51685
+ function isObjectRecord2(v2) {
51325
51686
  return typeof v2 === "object" && v2 !== null && !Array.isArray(v2);
51326
51687
  }
51327
51688
  function extractPreviewData(raw) {
51328
- if (!isObjectRecord(raw)) return null;
51689
+ if (!isObjectRecord2(raw)) return null;
51329
51690
  const out = {};
51330
- if (typeof raw.port === "number") out.port = raw.port;
51331
- if (typeof raw.url === "string") out.url = raw.url;
51332
- if (typeof raw.proxyPort === "number") out.proxyPort = raw.proxyPort;
51333
51691
  if (typeof raw.ownerUserId === "string") out.ownerUserId = raw.ownerUserId;
51334
- if (typeof raw.initialPath === "string") out.initialPath = raw.initialPath;
51692
+ return out;
51693
+ }
51694
+ function buildPreviewState(elementId, params, ownerUserId, existing) {
51695
+ const state = { elementId, ownerUserId };
51696
+ const port = params.port ?? existing?.port;
51697
+ if (port !== void 0) state.port = port;
51698
+ const url = params.url ?? existing?.url;
51699
+ if (url !== void 0) state.url = url;
51700
+ const proxyPort = params.proxyPort ?? existing?.proxyPort;
51701
+ if (proxyPort !== void 0) state.proxyPort = proxyPort;
51702
+ const initialPath = params.initialPath ?? existing?.initialPath;
51703
+ if (initialPath !== void 0) state.initialPath = initialPath;
51704
+ const projectRoot = params.projectRoot ?? existing?.projectRoot;
51705
+ if (typeof projectRoot === "string" && projectRoot.length > 0) state.projectRoot = projectRoot;
51706
+ return state;
51707
+ }
51708
+ function mergedPreviewData(data, state) {
51709
+ const out = {
51710
+ ownerUserId: data.ownerUserId
51711
+ };
51712
+ if (state.port !== void 0) out.port = state.port;
51713
+ if (state.url !== void 0) out.url = state.url;
51714
+ if (state.proxyPort !== void 0) out.proxyPort = state.proxyPort;
51715
+ if (state.initialPath !== void 0) out.initialPath = state.initialPath;
51716
+ if (state.projectRoot !== void 0) out.projectRoot = state.projectRoot;
51335
51717
  return out;
51336
51718
  }
51337
51719
  var DEBOUNCE_MS3 = 200;
51338
51720
  async function atomicWrite2(filePath, content) {
51339
- await mkdir10(dirname12(filePath), { recursive: true });
51340
- const tmpPath = `${filePath}.${randomUUID4()}.tmp`;
51721
+ await mkdir10(dirname11(filePath), { recursive: true });
51722
+ const tmpPath = `${filePath}.${randomUUID5()}.tmp`;
51341
51723
  await writeFile15(tmpPath, content, "utf-8");
51342
51724
  await rename9(tmpPath, filePath);
51343
51725
  }
@@ -51506,7 +51888,7 @@ var VisualizationFileWatcher = class {
51506
51888
  if (this.#disposed) return;
51507
51889
  if (this.#watchedFiles.has(filePath)) return;
51508
51890
  this.#watchedFiles.add(filePath);
51509
- const dirPath = dirname12(filePath);
51891
+ const dirPath = dirname11(filePath);
51510
51892
  const existing = this.#dirWatchers.get(dirPath);
51511
51893
  if (existing) {
51512
51894
  existing.refCount += 1;
@@ -51546,7 +51928,7 @@ var VisualizationFileWatcher = class {
51546
51928
  }
51547
51929
  #stopFileWatch(filePath) {
51548
51930
  if (!this.#watchedFiles.delete(filePath)) return;
51549
- const dirPath = dirname12(filePath);
51931
+ const dirPath = dirname11(filePath);
51550
51932
  const entry = this.#dirWatchers.get(dirPath);
51551
51933
  if (entry) {
51552
51934
  entry.refCount -= 1;
@@ -51564,39 +51946,56 @@ var VisualizationFileWatcher = class {
51564
51946
  /**
51565
51947
  * Idempotent create-or-update of a preview canvas element.
51566
51948
  *
51567
- * The match key is `{taskId, port || url}`: if an element already exists
51568
- * with the same type='preview' and matching port (or url), we update its
51569
- * `data` in place and return its id. Otherwise we create a new element.
51949
+ * Dedupe order (pointer-only CRDT, PROTOCOL_VERSION 36):
51950
+ * 1. If params has `projectRoot` AND `previewStateStore` already has
51951
+ * an entry with a matching `projectRoot` update that entry's
51952
+ * port / url / proxyPort in place (same elementId, no new canvas
51953
+ * element). This is the port-swap upsert: Vite restarts on a new
51954
+ * port, daemon calls `present({projectRoot})` again, and the
51955
+ * existing element's iframe URL flips — no duplicates.
51956
+ * 2. Else if the CRDT has a `preview` element with matching
51957
+ * `ownerUserId` AND the store has an entry with matching port —
51958
+ * update that entry. This covers the legacy / no-projectRoot path
51959
+ * where dedupe keys off the live port alone.
51960
+ * 3. Else create a new canvas element (pointer-only) + a new store
51961
+ * entry carrying the mutable state.
51570
51962
  *
51571
- * The check+write happens in the same tick this function is synchronous
51572
- * with respect to the canvas repository, so a concurrent caller cannot
51573
- * race the lookup. (loro-crdt mutations are synchronous via `change()`.)
51963
+ * In every case, the store is written with the element's projectRoot
51964
+ * (if known) so the publish pipeline can look it up by elementId.
51965
+ *
51966
+ * loro-crdt mutations are synchronous via `change()`, so the
51967
+ * check+write against the CRDT cannot race a concurrent caller.
51574
51968
  */
51575
51969
  createOrUpdatePreviewElement(params) {
51576
51970
  const data = this.registry.planPreviewElement(params);
51577
51971
  const loroData = previewDataToLoroValue(data);
51972
+ const store = this.#deps.previewStateStore ?? null;
51973
+ if (store && params.projectRoot !== void 0 && params.projectRoot.length > 0) {
51974
+ const existing = store.findByProjectRoot(params.taskId, params.projectRoot);
51975
+ if (existing) {
51976
+ const elementId2 = existing.elementId;
51977
+ const mergedState = buildPreviewState(elementId2, params, data.ownerUserId, existing);
51978
+ store.putState(params.taskId, mergedState);
51979
+ return {
51980
+ kind: "updated",
51981
+ elementId: elementId2,
51982
+ data: mergedPreviewData(data, mergedState)
51983
+ };
51984
+ }
51985
+ }
51578
51986
  const existingElements = this.#deps.canvasRepo.getElements(params.taskId);
51579
- const match2 = existingElements.find((el) => {
51580
- if (el.data.type !== "preview") return false;
51581
- const elData = extractPreviewData(el.data.data);
51582
- if (!elData) return false;
51583
- if (params.port !== void 0) return elData.port === params.port;
51584
- if (params.url !== void 0) return elData.url === params.url;
51585
- return false;
51586
- });
51587
- if (match2) {
51588
- const matchTreeId = match2.id;
51589
- const preservedData = (() => {
51590
- if (data.projectRoot !== void 0) return data;
51591
- const existing = extractPreviewData(match2.data.data);
51592
- if (existing?.projectRoot) {
51593
- return { ...data, projectRoot: existing.projectRoot };
51594
- }
51595
- return data;
51596
- })();
51597
- const preservedLoroData = previewDataToLoroValue(preservedData);
51598
- this.#deps.canvasRepo.updateElement(params.taskId, matchTreeId, { data: preservedLoroData });
51599
- return { kind: "updated", elementId: `${match2.id}`, data: preservedData };
51987
+ const legacyMatch = store ? this.#findLegacyPreviewMatch(existingElements, params, data.ownerUserId) : null;
51988
+ if (legacyMatch && store) {
51989
+ const matchTreeId = legacyMatch.id;
51990
+ const elementId2 = `${matchTreeId}`;
51991
+ const existingState = store.getByElementId(params.taskId, elementId2);
51992
+ const mergedState = buildPreviewState(elementId2, params, data.ownerUserId, existingState);
51993
+ store.putState(params.taskId, mergedState);
51994
+ return {
51995
+ kind: "updated",
51996
+ elementId: elementId2,
51997
+ data: mergedPreviewData(data, mergedState)
51998
+ };
51600
51999
  }
51601
52000
  const positionable = existingElements.map((el) => ({
51602
52001
  x: el.data.x,
@@ -51616,7 +52015,26 @@ var VisualizationFileWatcher = class {
51616
52015
  zIndex: position.zIndex,
51617
52016
  data: loroData
51618
52017
  });
51619
- return { kind: "created", elementId: `${nodeId}`, data };
52018
+ const elementId = `${nodeId}`;
52019
+ if (store) {
52020
+ const freshState = buildPreviewState(elementId, params, data.ownerUserId, void 0);
52021
+ store.putState(params.taskId, freshState);
52022
+ }
52023
+ return { kind: "created", elementId, data };
52024
+ }
52025
+ #findLegacyPreviewMatch(existingElements, params, ownerUserId) {
52026
+ const store = this.#deps.previewStateStore;
52027
+ if (!store) return null;
52028
+ for (const el of existingElements) {
52029
+ if (el.data.type !== "preview") continue;
52030
+ const elData = extractPreviewData(el.data.data);
52031
+ if (!elData || elData.ownerUserId !== ownerUserId) continue;
52032
+ const state = store.getByElementId(params.taskId, `${el.id}`);
52033
+ if (!state) continue;
52034
+ if (params.port !== void 0 && state.port === params.port) return el;
52035
+ if (params.url !== void 0 && state.url === params.url) return el;
52036
+ }
52037
+ return null;
51620
52038
  }
51621
52039
  async #createCanvasElement(effect) {
51622
52040
  const existingElements = this.#deps.canvasRepo.getElements(effect.taskId);
@@ -51757,17 +52175,7 @@ var VisualizationRegistry = class {
51757
52175
  * idempotency key derived from `{taskId, port || url}`.
51758
52176
  */
51759
52177
  planPreviewElement(params) {
51760
- const data = {
51761
- ownerUserId: params.ownerUserId
51762
- };
51763
- if (params.port !== void 0) data.port = params.port;
51764
- if (params.url !== void 0) data.url = params.url;
51765
- if (params.proxyPort !== void 0) data.proxyPort = params.proxyPort;
51766
- if (params.initialPath !== void 0) data.initialPath = params.initialPath;
51767
- if (typeof params.projectRoot === "string" && params.projectRoot.length > 0) {
51768
- data.projectRoot = params.projectRoot;
51769
- }
51770
- return data;
52178
+ return { ownerUserId: params.ownerUserId };
51771
52179
  }
51772
52180
  /**
51773
52181
  * Force-refresh the canvas element for a presented visualization, regardless
@@ -75398,7 +75806,7 @@ var ScheduleEvaluator = class {
75398
75806
  };
75399
75807
 
75400
75808
  // src/services/serve-factory-helpers.ts
75401
- import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "fs";
75809
+ import { existsSync as existsSync6, readdirSync as readdirSync3, readFileSync as readFileSync7 } from "fs";
75402
75810
  import { homedir as homedir3 } from "os";
75403
75811
  import { join as join34 } from "path";
75404
75812
 
@@ -77978,7 +78386,7 @@ function rehydrateVizRegistry(registry, vizWatcher, resolvedTaskId, vizDir, log)
77978
78386
  const registryPath = join34(vizDir, resolvedTaskId, "registry.json");
77979
78387
  if (!existsSync6(registryPath)) return Promise.resolve();
77980
78388
  try {
77981
- const raw = readFileSync6(registryPath, "utf-8");
78389
+ const raw = readFileSync7(registryPath, "utf-8");
77982
78390
  const parsed = PersistedVisualizationRegistrySchema.safeParse(JSON.parse(raw));
77983
78391
  if (parsed.success) {
77984
78392
  const effects = registry.rehydrate(resolvedTaskId, parsed.data);
@@ -78003,7 +78411,7 @@ function rehydrateVizRegistry(registry, vizWatcher, resolvedTaskId, vizDir, log)
78003
78411
  }
78004
78412
  function prehydrateAllVizWatchers(vizDir, getOrCreateVizWatcher) {
78005
78413
  try {
78006
- for (const entry of readdirSync2(vizDir, { withFileTypes: true })) {
78414
+ for (const entry of readdirSync3(vizDir, { withFileTypes: true })) {
78007
78415
  if (entry.isDirectory() && existsSync6(join34(vizDir, entry.name, "registry.json"))) {
78008
78416
  getOrCreateVizWatcher(entry.name);
78009
78417
  }
@@ -78729,7 +79137,7 @@ function resolveModelFamily(model) {
78729
79137
  if (model.includes("haiku")) return "haiku";
78730
79138
  return "sonnet";
78731
79139
  }
78732
- var CC_VERSION = "2.1.111";
79140
+ var CC_VERSION = "2.1.120";
78733
79141
  var BILLING_HEADER_PREFIX = `x-anthropic-billing-header: cc_version=${CC_VERSION}; cc_entrypoint=sdk-ts;`;
78734
79142
  function buildConversationSystemPrompt(model) {
78735
79143
  const family = resolveModelFamily(model);
@@ -78825,7 +79233,7 @@ async function resolveDirectApiCredentials(method) {
78825
79233
  }
78826
79234
 
78827
79235
  // src/services/session/direct-api-subprocess.ts
78828
- import { randomUUID as randomUUID5 } from "crypto";
79236
+ import { randomUUID as randomUUID6 } from "crypto";
78829
79237
 
78830
79238
  // ../../node_modules/.pnpm/@anthropic-ai+sdk@0.90.0_zod@4.3.6/node_modules/@anthropic-ai/sdk/internal/tslib.mjs
78831
79239
  function __classPrivateFieldSet(receiver, state, value, kind, f2) {
@@ -84994,7 +85402,7 @@ var DirectApiSubprocess = class _DirectApiSubprocess {
84994
85402
  const allToolNames = [...SERVER_SIDE_TOOLS.map((t) => t.name), ...tools.map((t) => t.name)];
84995
85403
  onEvent({
84996
85404
  type: "init_received",
84997
- sessionId: `direct-api-${randomUUID5()}`,
85405
+ sessionId: `direct-api-${randomUUID6()}`,
84998
85406
  metadata: {
84999
85407
  tools: allToolNames,
85000
85408
  skills: [],
@@ -85347,8 +85755,7 @@ async function handlePublish(ctx, input) {
85347
85755
  previewPort: target.previewPort,
85348
85756
  ...target.canvasElementId !== void 0 && {
85349
85757
  canvasElementId: target.canvasElementId
85350
- },
85351
- ...target.projectRoot !== void 0 && { projectRoot: target.projectRoot }
85758
+ }
85352
85759
  } : { kind: "canvasElementId", canvasElementId: target.canvasElementId };
85353
85760
  const outcome = await runPublish(
85354
85761
  {
@@ -85361,6 +85768,7 @@ async function handlePublish(ctx, input) {
85361
85768
  taskId: ctx.taskId,
85362
85769
  vizWatcher: ctx.vizWatcher,
85363
85770
  publishedArtifactStore: ctx.publishedArtifactStore,
85771
+ previewStateStore: ctx.previewStateStore,
85364
85772
  getDetectedPorts: ctx.getDetectedPorts
85365
85773
  },
85366
85774
  { onProgress: () => {
@@ -85680,7 +86088,7 @@ function createShipyardResolver() {
85680
86088
 
85681
86089
  // src/services/storage/annotation-store.ts
85682
86090
  import { mkdir as mkdir11, readFile as readFile25, rename as rename10, writeFile as writeFile16 } from "fs/promises";
85683
- import { dirname as dirname13, join as join35 } from "path";
86091
+ import { dirname as dirname12, join as join35 } from "path";
85684
86092
  var LegacyBaseFields = external_exports.object({
85685
86093
  commentId: external_exports.string(),
85686
86094
  body: external_exports.string(),
@@ -85732,7 +86140,7 @@ function buildAnnotationStore(dataDir) {
85732
86140
  }
85733
86141
  async function atomicWrite4(taskId, data) {
85734
86142
  const fp = filePath(taskId);
85735
- await mkdir11(dirname13(fp), { recursive: true });
86143
+ await mkdir11(dirname12(fp), { recursive: true });
85736
86144
  const tmpPath = `${fp}.tmp`;
85737
86145
  await writeFile16(tmpPath, JSON.stringify(data, null, 2), "utf-8");
85738
86146
  await rename10(tmpPath, fp);
@@ -86102,7 +86510,7 @@ async function seedBuiltInTemplates(store, builtIns, dismissed) {
86102
86510
 
86103
86511
  // src/services/storage/credentials-vault-store.ts
86104
86512
  import { mkdir as mkdir13, readFile as readFile27, rename as rename12, writeFile as writeFile18 } from "fs/promises";
86105
- import { dirname as dirname14 } from "path";
86513
+ import { dirname as dirname13 } from "path";
86106
86514
  function buildCredentialsVaultStore(filePath) {
86107
86515
  let cache2 = null;
86108
86516
  const listeners = /* @__PURE__ */ new Set();
@@ -86116,7 +86524,7 @@ function buildCredentialsVaultStore(filePath) {
86116
86524
  }
86117
86525
  }
86118
86526
  async function ensureDir() {
86119
- await mkdir13(dirname14(filePath), { recursive: true });
86527
+ await mkdir13(dirname13(filePath), { recursive: true });
86120
86528
  }
86121
86529
  async function readStore() {
86122
86530
  if (cache2) return cache2;
@@ -86646,9 +87054,9 @@ function buildObservableConversationStore(inner) {
86646
87054
  }
86647
87055
 
86648
87056
  // src/services/storage/projects-store.ts
86649
- import { randomUUID as randomUUID6 } from "crypto";
87057
+ import { randomUUID as randomUUID7 } from "crypto";
86650
87058
  import { mkdir as mkdir15, readFile as readFile29, rename as rename14, writeFile as writeFile20 } from "fs/promises";
86651
- import { dirname as dirname15 } from "path";
87059
+ import { dirname as dirname14 } from "path";
86652
87060
  var ProjectsArraySchema = external_exports.array(ProjectSchema);
86653
87061
  function buildProjectsStore(filePath) {
86654
87062
  let cache2 = null;
@@ -86663,7 +87071,7 @@ function buildProjectsStore(filePath) {
86663
87071
  }
86664
87072
  }
86665
87073
  async function ensureDir() {
86666
- await mkdir15(dirname15(filePath), { recursive: true });
87074
+ await mkdir15(dirname14(filePath), { recursive: true });
86667
87075
  }
86668
87076
  async function readStore() {
86669
87077
  if (cache2) return cache2;
@@ -86723,7 +87131,7 @@ function buildProjectsStore(filePath) {
86723
87131
  const current2 = await readStore();
86724
87132
  const nextOrder = current2.length === 0 ? 0 : Math.max(...current2.map((p2) => p2.order)) + 1;
86725
87133
  const project = {
86726
- id: randomUUID6(),
87134
+ id: randomUUID7(),
86727
87135
  name,
86728
87136
  order: nextOrder,
86729
87137
  createdAt: Date.now(),
@@ -86787,7 +87195,7 @@ function buildProjectsStore(filePath) {
86787
87195
 
86788
87196
  // src/services/storage/rate-limit-store.ts
86789
87197
  import { mkdir as mkdir16, readFile as readFile30, rename as rename15, writeFile as writeFile21 } from "fs/promises";
86790
- import { dirname as dirname16, join as join38 } from "path";
87198
+ import { dirname as dirname15, join as join38 } from "path";
86791
87199
  var RATE_LIMIT_STORE_VERSION = 2;
86792
87200
  var RateLimitRecordSchema = external_exports.object({
86793
87201
  info: RateLimitInfoSchema,
@@ -86969,7 +87377,7 @@ function migrateV1toV2(v1) {
86969
87377
  return { schemaVersion: RATE_LIMIT_STORE_VERSION, records };
86970
87378
  }
86971
87379
  async function atomicWrite3(filePath, data) {
86972
- await mkdir16(dirname16(filePath), { recursive: true });
87380
+ await mkdir16(dirname15(filePath), { recursive: true });
86973
87381
  const tmpPath = `${filePath}.tmp`;
86974
87382
  await writeFile21(tmpPath, JSON.stringify(data, null, 2), "utf-8");
86975
87383
  await rename15(tmpPath, filePath);
@@ -86980,7 +87388,7 @@ import { join as join39 } from "path";
86980
87388
 
86981
87389
  // src/services/storage/json-document-store.ts
86982
87390
  import { mkdir as mkdir17, readFile as readFile31, rename as rename16, writeFile as writeFile22 } from "fs/promises";
86983
- import { dirname as dirname17 } from "path";
87391
+ import { dirname as dirname16 } from "path";
86984
87392
  function buildJsonDocumentStore(opts) {
86985
87393
  const { filePath, recordSchema, currentVersion, migrate } = opts;
86986
87394
  let cache2 = null;
@@ -86995,7 +87403,7 @@ function buildJsonDocumentStore(opts) {
86995
87403
  }
86996
87404
  }
86997
87405
  async function ensureDir() {
86998
- await mkdir17(dirname17(filePath), { recursive: true });
87406
+ await mkdir17(dirname16(filePath), { recursive: true });
86999
87407
  }
87000
87408
  async function readStore() {
87001
87409
  if (cache2) return cache2;
@@ -87231,7 +87639,7 @@ function buildTemplateStore(dataDir) {
87231
87639
 
87232
87640
  // src/services/storage/user-settings-store.ts
87233
87641
  import { mkdir as mkdir19, readFile as readFile33, rename as rename18, writeFile as writeFile24 } from "fs/promises";
87234
- import { dirname as dirname18 } from "path";
87642
+ import { dirname as dirname17 } from "path";
87235
87643
  function buildUserSettingsStore(filePath) {
87236
87644
  let cache2 = null;
87237
87645
  const listeners = /* @__PURE__ */ new Set();
@@ -87245,7 +87653,7 @@ function buildUserSettingsStore(filePath) {
87245
87653
  }
87246
87654
  }
87247
87655
  async function ensureDir() {
87248
- await mkdir19(dirname18(filePath), { recursive: true });
87656
+ await mkdir19(dirname17(filePath), { recursive: true });
87249
87657
  }
87250
87658
  async function readStore() {
87251
87659
  if (cache2) return cache2;
@@ -87336,7 +87744,7 @@ function buildUserSettingsStore(filePath) {
87336
87744
  }
87337
87745
 
87338
87746
  // src/services/task/orchestrator/task.ts
87339
- import { randomUUID as randomUUID9 } from "crypto";
87747
+ import { randomUUID as randomUUID10 } from "crypto";
87340
87748
  import { join as join50 } from "path";
87341
87749
 
87342
87750
  // src/services/event-batching.ts
@@ -90190,13 +90598,13 @@ function skipForMainChannel(content) {
90190
90598
  }
90191
90599
 
90192
90600
  // src/services/plan/plan-handler.ts
90193
- import { existsSync as existsSync8, readdirSync as readdirSync3, statSync as statSync2 } from "fs";
90601
+ import { existsSync as existsSync8, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
90194
90602
  import { readFile as readFile35 } from "fs/promises";
90195
90603
  import { homedir as homedir5 } from "os";
90196
90604
  import { join as join44 } from "path";
90197
90605
 
90198
90606
  // src/services/plan/plan-file-bridge.ts
90199
- import { createHash as createHash3, randomUUID as randomUUID7 } from "crypto";
90607
+ import { createHash as createHash3, randomUUID as randomUUID8 } from "crypto";
90200
90608
  import { existsSync as existsSync7, watch as watch4 } from "fs";
90201
90609
  import { readFile as readFile34, rename as rename19, writeFile as writeFile25 } from "fs/promises";
90202
90610
  import { homedir as homedir4 } from "os";
@@ -90461,7 +90869,7 @@ var PlanFileBridge = class _PlanFileBridge {
90461
90869
  const content = this.#config.planRepo.getContent(this.#config.taskId);
90462
90870
  const hash = contentHash(content);
90463
90871
  if (hash === this.#lastWrittenHash) return;
90464
- const tmpPath = `${this.#filePath}.${randomUUID7()}.tmp`;
90872
+ const tmpPath = `${this.#filePath}.${randomUUID8()}.tmp`;
90465
90873
  await writeFile25(tmpPath, content, "utf-8");
90466
90874
  await rename19(tmpPath, this.#filePath);
90467
90875
  this.#lastWrittenHash = hash;
@@ -91214,7 +91622,7 @@ var PlanHandler = class {
91214
91622
  const plansDir = join44(homedir5(), ".claude", "plans");
91215
91623
  if (!existsSync8(plansDir)) return null;
91216
91624
  try {
91217
- const files = readdirSync3(plansDir).filter((f2) => f2.endsWith(".md")).map((f2) => ({
91625
+ const files = readdirSync4(plansDir).filter((f2) => f2.endsWith(".md")).map((f2) => ({
91218
91626
  path: join44(plansDir, f2),
91219
91627
  mtime: statSync2(join44(plansDir, f2)).mtimeMs
91220
91628
  })).sort((a, b2) => b2.mtime - a.mtime);
@@ -91833,14 +92241,14 @@ function planPostCompactPending(input) {
91833
92241
  }
91834
92242
 
91835
92243
  // src/services/task/compaction-snapshot.ts
91836
- import { randomUUID as randomUUID8 } from "crypto";
92244
+ import { randomUUID as randomUUID9 } from "crypto";
91837
92245
  import { homedir as homedir6 } from "os";
91838
92246
  import { join as pathJoin } from "path";
91839
92247
  function resolveArchiveAbsolutePath(channelId) {
91840
92248
  return pathJoin(homedir6(), ".shipyard", "data", "channels", `${channelId}.jsonl`);
91841
92249
  }
91842
92250
  function planCompactionSnapshot(input) {
91843
- const threadId = randomUUID8();
92251
+ const threadId = randomUUID9();
91844
92252
  const channelId = buildThreadChannelId(input.taskId, threadId);
91845
92253
  const compactionCount = input.messages.filter(
91846
92254
  (m2) => m2.content.some((b2) => b2.type === "compaction_boundary")
@@ -91877,7 +92285,7 @@ function planCompactionSnapshot(input) {
91877
92285
  stats
91878
92286
  };
91879
92287
  const boundaryMessage = {
91880
- messageId: randomUUID8(),
92288
+ messageId: randomUUID9(),
91881
92289
  channelId: input.channelId,
91882
92290
  participantId: input.humanParticipantId,
91883
92291
  senderKind: "human",
@@ -92757,7 +93165,7 @@ var RewindCheckpointHandler = class {
92757
93165
 
92758
93166
  // src/services/task/side-thread-registry.ts
92759
93167
  import { mkdir as mkdir20, readFile as readFile36, rename as rename20, writeFile as writeFile26 } from "fs/promises";
92760
- import { dirname as dirname19, join as join45 } from "path";
93168
+ import { dirname as dirname18, join as join45 } from "path";
92761
93169
  var ThreadFileSchema = external_exports.object({
92762
93170
  threads: external_exports.record(external_exports.string(), ThreadMetadataSchema)
92763
93171
  });
@@ -93118,7 +93526,7 @@ var SideThreadRegistry = class {
93118
93526
  threads[threadId] = entry.metadata;
93119
93527
  }
93120
93528
  const filePath = this.#filePath();
93121
- await mkdir20(dirname19(filePath), { recursive: true });
93529
+ await mkdir20(dirname18(filePath), { recursive: true });
93122
93530
  const tmpPath = `${filePath}.tmp`;
93123
93531
  await writeFile26(tmpPath, JSON.stringify({ threads }, null, 2), "utf-8");
93124
93532
  await rename20(tmpPath, filePath);
@@ -93533,7 +93941,7 @@ async function resolveResources(ctx, content, history2, excludeUris) {
93533
93941
  import { watch as watch5 } from "fs";
93534
93942
  import { readdir as readdir10, readFile as readFile37 } from "fs/promises";
93535
93943
  import { homedir as homedir7 } from "os";
93536
- import { basename as basename5, dirname as dirname20, join as join46 } from "path";
93944
+ import { basename as basename5, dirname as dirname19, join as join46 } from "path";
93537
93945
  var VALID_STATUSES2 = /* @__PURE__ */ new Set(["pending", "in_progress", "completed"]);
93538
93946
  function sanitize(id) {
93539
93947
  return id.replace(/[^a-zA-Z0-9_-]/g, "-");
@@ -93586,7 +93994,7 @@ function createCCTaskFileWatcher(listId, log) {
93586
93994
  let debounceTimer = null;
93587
93995
  const DEBOUNCE_MS4 = 200;
93588
93996
  const targetDirName = basename5(dir);
93589
- const parentDir = dirname20(dir);
93997
+ const parentDir = dirname19(dir);
93590
93998
  function scheduleRead() {
93591
93999
  if (debounceTimer) clearTimeout(debounceTimer);
93592
94000
  debounceTimer = setTimeout(() => {
@@ -96285,7 +96693,7 @@ var Task = class {
96285
96693
  instructions
96286
96694
  });
96287
96695
  const archiveMessages = plan.messagesToCopy.map((msg) => ({
96288
- messageId: randomUUID9(),
96696
+ messageId: randomUUID10(),
96289
96697
  channelId: plan.threadMetadata.channelId,
96290
96698
  participantId: msg.participantId,
96291
96699
  senderKind: msg.senderKind,
@@ -100444,7 +100852,7 @@ function buildThemeStore(dataDir) {
100444
100852
  // src/services/themes/vscode-scanner.ts
100445
100853
  import { readdir as readdir13, readFile as readFile39, stat as stat11 } from "fs/promises";
100446
100854
  import { homedir as homedir9 } from "os";
100447
- import { dirname as dirname21, join as join53, normalize as normalize5, resolve as resolve2 } from "path";
100855
+ import { dirname as dirname20, join as join53, normalize as normalize5, resolve as resolve2 } from "path";
100448
100856
  var VSCodeThemeEntrySchema2 = external_exports.object({
100449
100857
  extensionId: external_exports.string(),
100450
100858
  name: external_exports.string(),
@@ -100599,7 +101007,7 @@ async function readVSCodeThemeWithIncludes(absolutePath) {
100599
101007
  const obj = await readObject(canonical);
100600
101008
  const include = typeof obj.include === "string" ? obj.include : null;
100601
101009
  if (!include) return obj;
100602
- const basePath = resolve2(dirname21(canonical), include);
101010
+ const basePath = resolve2(dirname20(canonical), include);
100603
101011
  const base3 = await walk(basePath, depth + 1);
100604
101012
  return mergeThemeObjects(base3, obj);
100605
101013
  };
@@ -100912,7 +101320,8 @@ async function createDaemon(deps) {
100912
101320
  const watcher = new VisualizationFileWatcher(registry, {
100913
101321
  canvasRepo,
100914
101322
  vizDir,
100915
- log: deps.log
101323
+ log: deps.log,
101324
+ previewStateStore: deps.previewStateStore ?? null
100916
101325
  });
100917
101326
  watcher.setSendControlMessage((msg) => taskManager.broadcastControl(msg));
100918
101327
  watcher.setRehydrationPromise(
@@ -100937,26 +101346,25 @@ async function createDaemon(deps) {
100937
101346
  }
100938
101347
  const trackedPreviewPorts = /* @__PURE__ */ new Map();
100939
101348
  const previewLifecycleSubs = /* @__PURE__ */ new Map();
100940
- function readLocalPreviewPort(dataField, selfUserId) {
100941
- if (!isRecord4(dataField)) return void 0;
100942
- if (dataField.ownerUserId !== selfUserId) return void 0;
100943
- const port = dataField.port;
100944
- return typeof port === "number" ? port : void 0;
101349
+ function isLocalPreviewElement(dataField, selfUserId) {
101350
+ if (!isRecord4(dataField)) return false;
101351
+ return dataField.ownerUserId === selfUserId;
100945
101352
  }
100946
101353
  function collectLocalPreviewPorts(taskId) {
100947
101354
  const current2 = /* @__PURE__ */ new Map();
101355
+ const store2 = deps.previewStateStore ?? null;
101356
+ if (!store2) return current2;
100948
101357
  for (const el of canvasRepo.getElements(taskId)) {
100949
101358
  if (el.data.type !== "preview") continue;
100950
- const port = readLocalPreviewPort(el.data.data, deps.auth.userId);
100951
- if (port !== void 0) current2.set(String(el.id), port);
101359
+ if (!isLocalPreviewElement(el.data.data, deps.auth.userId)) continue;
101360
+ const state = store2.getByElementId(taskId, String(el.id));
101361
+ const port = state?.port;
101362
+ if (typeof port === "number") current2.set(String(el.id), port);
100952
101363
  }
100953
101364
  return current2;
100954
101365
  }
100955
- function reconcilePreviewRefs(taskId) {
100956
- const current2 = collectLocalPreviewPorts(taskId);
100957
- const tracked = trackedPreviewPorts.get(taskId) ?? /* @__PURE__ */ new Map();
100958
- trackedPreviewPorts.set(taskId, current2);
100959
- for (const [elementId, port] of tracked) {
101366
+ function releaseRemovedProxyRefs(previous, current2) {
101367
+ for (const [elementId, port] of previous) {
100960
101368
  const stillPresent = current2.get(elementId);
100961
101369
  if (stillPresent === void 0 || stillPresent !== port) {
100962
101370
  deps.previewProxy.release(port).catch(() => {
@@ -100964,6 +101372,22 @@ async function createDaemon(deps) {
100964
101372
  }
100965
101373
  }
100966
101374
  }
101375
+ function sweepOrphanedPreviewState(taskId, liveElementIds) {
101376
+ const store2 = deps.previewStateStore ?? null;
101377
+ if (!store2) return;
101378
+ for (const state of store2.listByTask(taskId)) {
101379
+ if (liveElementIds.has(state.elementId)) continue;
101380
+ const el = canvasRepo.getElements(taskId).find((e) => String(e.id) === state.elementId);
101381
+ if (!el) store2.deleteByElementId(taskId, state.elementId);
101382
+ }
101383
+ }
101384
+ function reconcilePreviewRefs(taskId) {
101385
+ const current2 = collectLocalPreviewPorts(taskId);
101386
+ const tracked = trackedPreviewPorts.get(taskId) ?? /* @__PURE__ */ new Map();
101387
+ trackedPreviewPorts.set(taskId, current2);
101388
+ releaseRemovedProxyRefs(tracked, current2);
101389
+ sweepOrphanedPreviewState(taskId, new Set(current2.keys()));
101390
+ }
100967
101391
  function ensurePreviewLifecycleTracking(taskId) {
100968
101392
  if (previewLifecycleSubs.has(taskId)) return;
100969
101393
  const unsub = canvasRepo.subscribe(
@@ -101003,6 +101427,7 @@ async function createDaemon(deps) {
101003
101427
  getAuthToken: () => deps.auth.token,
101004
101428
  projectRoot: cwd ?? deps.workspaceRoot,
101005
101429
  publishedArtifactStore: deps.publishedArtifactStore ?? null,
101430
+ previewStateStore: deps.previewStateStore ?? null,
101006
101431
  getDetectedPorts: deps.getDetectedPorts ?? (() => [])
101007
101432
  });
101008
101433
  const options = buildSpawnOptions({
@@ -101042,6 +101467,7 @@ async function createDaemon(deps) {
101042
101467
  getAuthToken: () => deps.auth.token,
101043
101468
  projectRoot,
101044
101469
  publishedArtifactStore: deps.publishedArtifactStore ?? null,
101470
+ previewStateStore: deps.previewStateStore ?? null,
101045
101471
  getDetectedPorts: deps.getDetectedPorts ?? (() => [])
101046
101472
  });
101047
101473
  deps.log({ event: "conversation_backend", backend: "direct-api", model, taskId });
@@ -101334,6 +101760,9 @@ async function createDaemon(deps) {
101334
101760
  await sweepStaleTasks(taskStateStore, taskManager, deps.log);
101335
101761
  await migratePinnedToFavoriteTasks(taskStateStore, userSettingsStore, deps.log);
101336
101762
  await rehydrateCollabQueues(collabQueuePersistence, taskManager, deps.log);
101763
+ if (deps.previewStateStore) {
101764
+ await migrateLegacyPreviewData(canvasRepo, deps.previewStateStore, taskStateStore, deps.log);
101765
+ }
101337
101766
  const warmedPreviewTasks = await rehydrateCanvasPreviews(
101338
101767
  canvasRepo,
101339
101768
  deps.previewProxy,
@@ -101931,6 +102360,14 @@ function filterOutboundForCollab(msg, collabTaskId, getTaskSync) {
101931
102360
  case "background_agent_update":
101932
102361
  case "viz_content":
101933
102362
  case "viz_content_batch":
102363
+ /**
102364
+ * published_artifacts_state and preview_elements_state are per-task
102365
+ * but visible to all collaborators so the chip + iframe on each
102366
+ * canvas element reflect the same reality for everyone. Same rule
102367
+ * as the other task-scoped messages: match taskId or drop.
102368
+ */
102369
+ case "published_artifacts_state":
102370
+ case "preview_elements_state":
101934
102371
  if ("taskId" in msg && msg.taskId !== collabTaskId) return null;
101935
102372
  return msg;
101936
102373
  /** Preview: collab peers need port detection and preview targets */
@@ -101956,13 +102393,6 @@ function filterOutboundForCollab(msg, collabTaskId, getTaskSync) {
101956
102393
  case "publish_progress":
101957
102394
  case "publish_result":
101958
102395
  return null;
101959
- /**
101960
- * published_artifacts_state is per-task but visible to all collaborators
101961
- * so the chip on each element reflects the same reality for everyone.
101962
- * Filter by taskId so a peer only sees state for tasks they participate in.
101963
- */
101964
- case "published_artifacts_state":
101965
- return msg.taskId === collabTaskId ? msg : null;
101966
102396
  /** Exhaustiveness check: compile error if a new type is unhandled */
101967
102397
  default: {
101968
102398
  const _exhaustive = msg;
@@ -102516,7 +102946,7 @@ import { readdir as readdir14 } from "fs/promises";
102516
102946
  import { execFile as execFile8, spawn as spawn5 } from "child_process";
102517
102947
  import { closeSync, openSync } from "fs";
102518
102948
  import { access as access3, chmod as chmod2, constants, mkdir as mkdir24, writeFile as writeFile30 } from "fs/promises";
102519
- import { dirname as dirname22, isAbsolute as isAbsolute2, join as join55 } from "path";
102949
+ import { dirname as dirname21, isAbsolute as isAbsolute2, join as join55 } from "path";
102520
102950
  var GIT_TIMEOUT_MS = 3e4;
102521
102951
  var MAX_BUFFER = 10 * 1024 * 1024;
102522
102952
  var BASE_REF_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9/_.-]*$/;
@@ -102779,7 +103209,7 @@ async function removeWorktree(worktreePath) {
102779
103209
  if (!worktreePath.includes("-wt/")) {
102780
103210
  throw new Error("worktreePath must be under a -wt/ parent directory");
102781
103211
  }
102782
- await runGit(["worktree", "remove", worktreePath], dirname22(worktreePath));
103212
+ await runGit(["worktree", "remove", worktreePath], dirname21(worktreePath));
102783
103213
  }
102784
103214
 
102785
103215
  // src/services/channels/control-channel-infra-handlers.ts
@@ -103274,7 +103704,7 @@ function runPluginOp(pluginName, marketplace, action, ctx) {
103274
103704
  }
103275
103705
 
103276
103706
  // src/services/channels/read-recent-logs.ts
103277
- import { createReadStream, readdirSync as readdirSync4 } from "fs";
103707
+ import { createReadStream, readdirSync as readdirSync5 } from "fs";
103278
103708
  import { join as join56 } from "path";
103279
103709
  import { createInterface } from "readline";
103280
103710
  var MAX_BYTES = 5e4;
@@ -103332,7 +103762,7 @@ async function readRecentLogs(logDir, windowMinutes, maxBytes = MAX_BYTES) {
103332
103762
  function discoverLogFiles(logDir) {
103333
103763
  let entries;
103334
103764
  try {
103335
- entries = readdirSync4(logDir);
103765
+ entries = readdirSync5(logDir);
103336
103766
  } catch {
103337
103767
  return [];
103338
103768
  }
@@ -104919,6 +105349,7 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
104919
105349
  }
104920
105350
  };
104921
105351
  const publishedArtifactStore = deps.publishedArtifactStore;
105352
+ const previewStateStore = deps.previewStateStore;
104922
105353
  const getDetectedPorts = deps.getDetectedPorts;
104923
105354
  handlePublishRequest({
104924
105355
  correlationId,
@@ -104931,6 +105362,7 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
104931
105362
  projectRoot: wsRoot,
104932
105363
  vizWatcher,
104933
105364
  publishedArtifactStore,
105365
+ previewStateStore,
104934
105366
  getDetectedPorts,
104935
105367
  sendControl: wrappedSendControl,
104936
105368
  log: (entry) => {
@@ -105150,6 +105582,24 @@ function wireControlChannel(rawChannel, daemon, logAdapter, deps) {
105150
105582
  });
105151
105583
  });
105152
105584
  }
105585
+ if (deps?.previewStateStore) {
105586
+ const previewStore = deps.previewStateStore;
105587
+ daemon.taskStateStore.listTasks().then((tasks) => {
105588
+ for (const taskId of Object.keys(tasks)) {
105589
+ const entries = previewStore.listByTask(taskId);
105590
+ controlHandler.sendControl({
105591
+ type: "preview_elements_state",
105592
+ taskId,
105593
+ entries
105594
+ });
105595
+ }
105596
+ }).catch((err) => {
105597
+ logAdapter({
105598
+ event: "preview_elements_initial_push_failed",
105599
+ error: err instanceof Error ? err.message : String(err)
105600
+ });
105601
+ });
105602
+ }
105153
105603
  const userSettingsUnsub = daemon.userSettingsStore.subscribe((settings) => {
105154
105604
  controlHandler.sendControl({ type: "user_settings_updated", settings });
105155
105605
  });
@@ -105684,7 +106134,7 @@ import { promisify as promisify7 } from "util";
105684
106134
 
105685
106135
  // src/shared/file-io-path-safety.ts
105686
106136
  import { realpath } from "fs/promises";
105687
- import { basename as basename6, dirname as dirname23, join as join57, normalize as normalize6 } from "path";
106137
+ import { basename as basename6, dirname as dirname22, join as join57, normalize as normalize6 } from "path";
105688
106138
  async function safeAbsolutePath(userPath, isHidden2, allowedHiddenNames) {
105689
106139
  const normalized = prepareAbsolutePath(userPath, isHidden2, allowedHiddenNames);
105690
106140
  if (normalized === null) return null;
@@ -105706,13 +106156,13 @@ async function canonicalizeOrRecombine(path2) {
105706
106156
  try {
105707
106157
  return await realpath(path2);
105708
106158
  } catch (err) {
105709
- if (!isEnoent3(err)) return null;
105710
- const parentCanonical = await realpath(dirname23(path2)).catch(() => null);
106159
+ if (!isEnoent4(err)) return null;
106160
+ const parentCanonical = await realpath(dirname22(path2)).catch(() => null);
105711
106161
  if (parentCanonical === null) return null;
105712
106162
  return join57(parentCanonical, basename6(path2));
105713
106163
  }
105714
106164
  }
105715
- function isEnoent3(err) {
106165
+ function isEnoent4(err) {
105716
106166
  return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
105717
106167
  }
105718
106168
  function checkSegmentsAllowed(path2, isHidden2, allowedHiddenNames) {
@@ -107022,13 +107472,13 @@ function wireThreadErrorFallback(daemon, dc, taskId, threadId, channelId, log) {
107022
107472
  // src/shared/pty-manager.ts
107023
107473
  import { accessSync, chmodSync, constants as constants2 } from "fs";
107024
107474
  import { createRequire as createRequire4 } from "module";
107025
- import { dirname as dirname24, resolve as resolve4 } from "path";
107475
+ import { dirname as dirname23, resolve as resolve4 } from "path";
107026
107476
  import * as pty from "node-pty";
107027
107477
  function ensureSpawnHelperExecutable() {
107028
107478
  if (globalThis.process.platform === "win32") return;
107029
107479
  try {
107030
107480
  const req = createRequire4(import.meta.url);
107031
- const nodePtyDir = dirname24(req.resolve("node-pty/package.json"));
107481
+ const nodePtyDir = dirname23(req.resolve("node-pty/package.json"));
107032
107482
  const spawnHelper = resolve4(
107033
107483
  nodePtyDir,
107034
107484
  "prebuilds",
@@ -107487,8 +107937,9 @@ function buildCollabRoomManager(deps) {
107487
107937
  configStore: pluginConfigStore,
107488
107938
  previewProxy,
107489
107939
  presencePool: presencePoolRef,
107490
- /** Collab peers cannot publish — null store and empty ports enforce read-only access. */
107940
+ /** Collab peers cannot publish — null stores enforce read-only access. */
107491
107941
  publishedArtifactStore: null,
107942
+ previewStateStore: null,
107492
107943
  getDetectedPorts: () => []
107493
107944
  });
107494
107945
  if (loadedPluginsRef.current.length > 0) {
@@ -107770,7 +108221,7 @@ function buildCollabRoomManager(deps) {
107770
108221
  import { execSync } from "child_process";
107771
108222
  import { existsSync as existsSync9 } from "fs";
107772
108223
  import { createRequire as createRequire5 } from "module";
107773
- import { dirname as dirname25, join as join59 } from "path";
108224
+ import { dirname as dirname24, join as join59 } from "path";
107774
108225
 
107775
108226
  // src/services/bootstrap/self-update.ts
107776
108227
  import { execFile as execFile11, spawn as spawn9 } from "child_process";
@@ -108433,7 +108884,7 @@ function resolveClaudeCodePath(log) {
108433
108884
  try {
108434
108885
  const req = createRequire5(import.meta.url);
108435
108886
  const sdkMain = req.resolve("@anthropic-ai/claude-agent-sdk");
108436
- const p2 = join59(dirname25(sdkMain), "cli.js");
108887
+ const p2 = join59(dirname24(sdkMain), "cli.js");
108437
108888
  if (existsSync9(p2)) return ok("sdk_bundled", p2);
108438
108889
  } catch {
108439
108890
  }
@@ -108697,7 +109148,8 @@ function buildSharedChannelCallbacks(deps) {
108697
109148
  terminalPtys,
108698
109149
  signalingHandle,
108699
109150
  sessionServerUrl,
108700
- publishedArtifactStore
109151
+ publishedArtifactStore,
109152
+ previewStateStore
108701
109153
  } = deps;
108702
109154
  return {
108703
109155
  onPeerDataChannel: (machineId) => {
@@ -108720,6 +109172,7 @@ function buildSharedChannelCallbacks(deps) {
108720
109172
  sessionServerUrl,
108721
109173
  getAuthToken: () => auth3.token,
108722
109174
  publishedArtifactStore,
109175
+ previewStateStore,
108723
109176
  getDetectedPorts: () => detectedPortsRef.current
108724
109177
  });
108725
109178
  portDetectorRef.current?.resend();
@@ -109133,6 +109586,10 @@ async function serve(options = {}) {
109133
109586
  rootDir: join62(dataDir, "published"),
109134
109587
  logger: log
109135
109588
  });
109589
+ const previewStateStore = createPreviewStateStore({
109590
+ rootDir: join62(dataDir, "preview-state"),
109591
+ logger: log
109592
+ });
109136
109593
  const daemon = await createDaemon({
109137
109594
  shipyardHome,
109138
109595
  dataDir,
@@ -109152,10 +109609,12 @@ async function serve(options = {}) {
109152
109609
  previewProxy,
109153
109610
  sessionServerUrl,
109154
109611
  publishedArtifactStore,
109612
+ previewStateStore,
109155
109613
  getDetectedPorts: () => detectedPortsRef.current
109156
109614
  });
109157
109615
  daemon.healthMetrics.start();
109158
109616
  publishedArtifactStore.setSendControlMessage((msg) => daemon.taskManager.broadcastControl(msg));
109617
+ previewStateStore.setSendControlMessage((msg) => daemon.taskManager.broadcastControl(msg));
109159
109618
  const pluginsDir = join62(shipyardHome, "plugins");
109160
109619
  await mkdir28(pluginsDir, { recursive: true });
109161
109620
  let loadedPlugins = [];
@@ -109287,7 +109746,8 @@ async function serve(options = {}) {
109287
109746
  fileWatcherPool,
109288
109747
  terminalPtys,
109289
109748
  sessionServerUrl,
109290
- publishedArtifactStore
109749
+ publishedArtifactStore,
109750
+ previewStateStore
109291
109751
  } : null;
109292
109752
  const peerManager = peerSetupDeps ? buildPeerManager(peerSetupDeps) : null;
109293
109753
  if (peerSetupDeps && !localDirectRef.current) {
@@ -109400,6 +109860,7 @@ async function serve(options = {}) {
109400
109860
  log.info("Graceful shutdown initiated");
109401
109861
  portDetector.dispose();
109402
109862
  publishedArtifactStore.dispose();
109863
+ previewStateStore.dispose();
109403
109864
  await previewProxy.stop();
109404
109865
  pluginFileWatcher.dispose();
109405
109866
  await fileWatcherPool.dispose();
@@ -109436,4 +109897,4 @@ export {
109436
109897
  _testing,
109437
109898
  serve
109438
109899
  };
109439
- //# sourceMappingURL=serve-QLYUK5XN.js.map
109900
+ //# sourceMappingURL=serve-2J25TKJX.js.map