@shawnowen/comet-mcp 2.3.1 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +86 -19
  2. package/dist/alert-dispatcher.d.ts +23 -0
  3. package/dist/alert-dispatcher.js +101 -0
  4. package/dist/bound-session.d.ts +23 -0
  5. package/dist/bound-session.js +119 -0
  6. package/dist/bridge-config.d.ts +6 -0
  7. package/dist/bridge-config.js +78 -0
  8. package/dist/cdp-client.d.ts +40 -4
  9. package/dist/cdp-client.js +502 -155
  10. package/dist/comet-ai.d.ts +15 -0
  11. package/dist/comet-ai.js +114 -38
  12. package/dist/delegate-binding.d.ts +19 -0
  13. package/dist/delegate-binding.js +73 -0
  14. package/dist/discovery/capability-entry.d.ts +215 -0
  15. package/dist/discovery/capability-entry.js +13 -0
  16. package/dist/discovery/description-template.d.ts +40 -0
  17. package/dist/discovery/description-template.js +61 -0
  18. package/dist/discovery/golden-queries.fixture.d.ts +22 -0
  19. package/dist/discovery/golden-queries.fixture.js +137 -0
  20. package/dist/discovery/mcp-source.d.ts +38 -0
  21. package/dist/discovery/mcp-source.js +70 -0
  22. package/dist/discovery/metadata-completeness.d.ts +48 -0
  23. package/dist/discovery/metadata-completeness.js +83 -0
  24. package/dist/discovery/registry.d.ts +35 -0
  25. package/dist/discovery/registry.js +35 -0
  26. package/dist/discovery/safety.d.ts +44 -0
  27. package/dist/discovery/safety.js +59 -0
  28. package/dist/discovery/schema-validator.d.ts +36 -0
  29. package/dist/discovery/schema-validator.js +257 -0
  30. package/dist/discovery/source-error.d.ts +47 -0
  31. package/dist/discovery/source-error.js +95 -0
  32. package/dist/discovery/tool-meta.d.ts +41 -0
  33. package/dist/discovery/tool-meta.js +229 -0
  34. package/dist/discovery/virtual-tools.d.ts +20 -0
  35. package/dist/discovery/virtual-tools.js +69 -0
  36. package/dist/http-server.js +2067 -47
  37. package/dist/index.js +3163 -710
  38. package/dist/observer.d.ts +47 -0
  39. package/dist/observer.js +516 -0
  40. package/dist/session-registry.d.ts +57 -0
  41. package/dist/session-registry.js +500 -0
  42. package/dist/sidecar-artifacts.d.ts +49 -0
  43. package/dist/sidecar-artifacts.js +146 -0
  44. package/dist/snapshot-capture.d.ts +3 -0
  45. package/dist/snapshot-capture.js +91 -0
  46. package/dist/tab-group-archive.js +3 -1
  47. package/dist/tab-groups.d.ts +7 -0
  48. package/dist/tab-groups.js +21 -3
  49. package/dist/task-thread-aggregator.d.ts +34 -0
  50. package/dist/task-thread-aggregator.js +480 -0
  51. package/dist/task-thread-canonical.d.ts +142 -0
  52. package/dist/task-thread-canonical.js +116 -0
  53. package/dist/types.d.ts +237 -0
  54. package/dist/window-bindings.d.ts +112 -0
  55. package/dist/window-bindings.js +476 -0
  56. package/extension/background.js +1556 -300
  57. package/extension/icons/icon.svg +9 -0
  58. package/extension/icons/icon128.png +0 -0
  59. package/extension/icons/icon16.png +0 -0
  60. package/extension/icons/icon48.png +0 -0
  61. package/extension/manifest.json +19 -4
  62. package/extension/session-logic.js +2383 -0
  63. package/extension/session-manager.html +299 -0
  64. package/extension/sidepanel.css +5323 -528
  65. package/extension/sidepanel.html +282 -2
  66. package/extension/sidepanel.js +10075 -951
  67. package/extension/window-policy.js +162 -0
  68. package/package.json +10 -7
  69. package/vendor/lifecycle-mcp-adapter.mjs +103 -0
  70. package/vendor/lifecycle-metadata.mjs +252 -0
  71. package/vendor/readiness-report.mjs +742 -0
  72. package/dist/cdp-client.d.ts.map +0 -1
  73. package/dist/cdp-client.js.map +0 -1
  74. package/dist/comet-ai.d.ts.map +0 -1
  75. package/dist/comet-ai.js.map +0 -1
  76. package/dist/http-server.d.ts.map +0 -1
  77. package/dist/http-server.js.map +0 -1
  78. package/dist/index.d.ts.map +0 -1
  79. package/dist/index.js.map +0 -1
  80. package/dist/tab-group-archive.d.ts.map +0 -1
  81. package/dist/tab-group-archive.js.map +0 -1
  82. package/dist/tab-groups.d.ts.map +0 -1
  83. package/dist/tab-groups.js.map +0 -1
  84. package/dist/types.d.ts.map +0 -1
  85. package/dist/types.js.map +0 -1
@@ -0,0 +1,476 @@
1
+ import { mkdir, readFile, rename, rm, unlink, writeFile } from "fs/promises";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { randomUUID } from "crypto";
5
+ import { execFileSync } from "child_process";
6
+ export class WindowBindingConflictError extends Error {
7
+ conflicts;
8
+ constructor(message, conflicts) {
9
+ super(message);
10
+ this.conflicts = conflicts;
11
+ this.name = "WindowBindingConflictError";
12
+ }
13
+ }
14
+ export class CodexIdentityError extends Error {
15
+ missingFields;
16
+ constructor(message, missingFields) {
17
+ super(message);
18
+ this.missingFields = missingFields;
19
+ this.name = "CodexIdentityError";
20
+ }
21
+ }
22
+ const DEFAULT_BINDING_DIR = path.join(os.homedir(), ".claude", "comet-browser");
23
+ const DEFAULT_BINDING_FILE = path.join(DEFAULT_BINDING_DIR, "window-bindings.json");
24
+ const LOCK_RETRY_MS = 25;
25
+ const LOCK_TIMEOUT_MS = 5_000;
26
+ const VALID_CODEX_ROLES = new Set([
27
+ "session_agent",
28
+ "worktree_orchestrator",
29
+ "fleet_orchestrator",
30
+ ]);
31
+ const VALID_PROFILE_OWNERS = new Set([
32
+ "agent",
33
+ "human",
34
+ "shared_legacy",
35
+ "unknown",
36
+ ]);
37
+ function nowIso() {
38
+ return new Date().toISOString();
39
+ }
40
+ function normalizeRunIds(runIds = []) {
41
+ return [...new Set(runIds.filter((runId) => runId.trim().length > 0))].sort();
42
+ }
43
+ function firstNonEmpty(...values) {
44
+ return values.find((value) => value !== undefined && value.trim().length > 0)?.trim();
45
+ }
46
+ function gitOutput(cwd, args) {
47
+ try {
48
+ return execFileSync("git", args, {
49
+ cwd,
50
+ encoding: "utf-8",
51
+ stdio: ["ignore", "pipe", "ignore"],
52
+ }).trim();
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
58
+ function repoSlugFromRemote(remoteUrl) {
59
+ if (!remoteUrl)
60
+ return undefined;
61
+ const match = remoteUrl.match(/[:/]([^/:]+\/[^/]+?)(?:\.git)?$/);
62
+ return match?.[1];
63
+ }
64
+ function deriveRepoSlug(cwd) {
65
+ return (repoSlugFromRemote(gitOutput(cwd, ["config", "--get", "remote.origin.url"])) ??
66
+ path.basename(cwd));
67
+ }
68
+ function deriveBranchName(cwd) {
69
+ return gitOutput(cwd, ["branch", "--show-current"]) || "unknown";
70
+ }
71
+ export function resolveCodexSessionRole(role) {
72
+ const normalized = role
73
+ ?.trim()
74
+ .toLowerCase()
75
+ .replace(/[-\s]+/g, "_");
76
+ if (normalized && VALID_CODEX_ROLES.has(normalized)) {
77
+ return normalized;
78
+ }
79
+ if (normalized === "fleet" || normalized === "fleet_orchestrator_agent") {
80
+ return "fleet_orchestrator";
81
+ }
82
+ if (normalized === "orchestrator" || normalized === "worktree") {
83
+ return "worktree_orchestrator";
84
+ }
85
+ return "session_agent";
86
+ }
87
+ export function deriveCodexSessionIdentity(input = {}, context = {}) {
88
+ const env = context.env ?? process.env;
89
+ const cwd = path.resolve(input.worktreePath ?? env.CODEX_WORKTREE_PATH ?? context.cwd ?? process.cwd());
90
+ const codexSessionId = firstNonEmpty(input.codexSessionId, env.CODEX_SESSION_ID, env.CODEX_RUN_ID, input.fallbackAgentId);
91
+ const projectThreadId = firstNonEmpty(input.projectThreadId, env.CODEX_PROJECT_THREAD_ID, env.COMET_TASK_GROUP, input.fallbackTaskThreadId);
92
+ const repoSlug = firstNonEmpty(input.repoSlug, env.CODEX_REPO_SLUG) ?? deriveRepoSlug(cwd);
93
+ const branchName = firstNonEmpty(input.branchName, env.CODEX_BRANCH_NAME, env.GIT_BRANCH) ?? deriveBranchName(cwd);
94
+ const missingFields = [
95
+ ["codexSessionId", codexSessionId],
96
+ ["projectThreadId", projectThreadId],
97
+ ["worktreePath", cwd],
98
+ ["repoSlug", repoSlug],
99
+ ["branchName", branchName],
100
+ ];
101
+ const missingFieldNames = missingFields.filter(([, value]) => !value).map(([field]) => field);
102
+ if (input.strict && missingFieldNames.length > 0) {
103
+ throw new CodexIdentityError("Missing required Codex identity fields", missingFieldNames);
104
+ }
105
+ const resolvedCodexSessionId = codexSessionId ?? "manual-codex-session";
106
+ const resolvedProjectThreadId = projectThreadId ?? "manual-project-thread";
107
+ return {
108
+ codexSessionId: resolvedCodexSessionId,
109
+ projectThreadId: resolvedProjectThreadId,
110
+ projectThreadFamily: firstNonEmpty(input.projectThreadFamily, env.CODEX_PROJECT_THREAD_FAMILY),
111
+ worktreePath: cwd,
112
+ repoSlug,
113
+ branchName,
114
+ sessionKey: firstNonEmpty(input.sessionKey, env.CODEX_SESSION_KEY) ??
115
+ `${resolvedCodexSessionId}:${resolvedProjectThreadId}`,
116
+ role: resolveCodexSessionRole(firstNonEmpty(input.role, env.CODEX_SESSION_ROLE, env.CODEX_ROLE)),
117
+ };
118
+ }
119
+ export function isBindingInWorktreeScope(caller, binding) {
120
+ if (caller.role === "fleet_orchestrator")
121
+ return true;
122
+ if (caller.role === "session_agent")
123
+ return caller.sessionKey === binding.sessionKey;
124
+ if (caller.worktreePath === binding.worktreePath)
125
+ return true;
126
+ return Boolean(caller.projectThreadFamily &&
127
+ binding.projectThreadFamily &&
128
+ caller.projectThreadFamily === binding.projectThreadFamily);
129
+ }
130
+ export function canReadBinding(caller, binding) {
131
+ return isBindingInWorktreeScope(caller, binding);
132
+ }
133
+ export function canMutateBinding(caller, binding, options = {}) {
134
+ if (caller.role === "session_agent") {
135
+ return caller.sessionKey === binding.sessionKey;
136
+ }
137
+ if (!isBindingInWorktreeScope(caller, binding))
138
+ return false;
139
+ const explicitTarget = options.targetBindingId === binding.bindingId;
140
+ if (!explicitTarget)
141
+ return false;
142
+ if (caller.role === "worktree_orchestrator" || caller.role === "fleet_orchestrator") {
143
+ return Boolean(options.reason?.trim() && options.audit);
144
+ }
145
+ return false;
146
+ }
147
+ export function assertBindingMutationAllowed(caller, binding, options = {}) {
148
+ if (canMutateBinding(caller, binding, options))
149
+ return;
150
+ throw new Error(`BINDING_MUTATION_SCOPE_VIOLATION: ${caller.role} ${caller.sessionKey} cannot mutate binding ${binding.bindingId} without explicit scope, target, reason, and audit`);
151
+ }
152
+ export function normalizeProfileOwner(owner) {
153
+ const normalized = owner?.trim().toLowerCase();
154
+ if (normalized && VALID_PROFILE_OWNERS.has(normalized)) {
155
+ return normalized;
156
+ }
157
+ return "unknown";
158
+ }
159
+ export function assertBindingProfileAllowed(identity, binding) {
160
+ if (identity.role !== "session_agent")
161
+ return;
162
+ if (binding.profileOwner === "agent")
163
+ return;
164
+ throw new Error(`PROFILE_OWNERSHIP_VIOLATION: binding profile ${binding.profileId} is ${binding.profileOwner}-owned and cannot be mutated by normal agents`);
165
+ }
166
+ function bindingStorePaths(storePath) {
167
+ const file = storePath ?? DEFAULT_BINDING_FILE;
168
+ return {
169
+ file,
170
+ dir: path.dirname(file),
171
+ lockDir: `${file}.lock`,
172
+ };
173
+ }
174
+ function emptySnapshot() {
175
+ return {
176
+ version: 1,
177
+ bindings: {},
178
+ runBindingIndex: {},
179
+ updatedAt: nowIso(),
180
+ };
181
+ }
182
+ async function sleep(ms) {
183
+ await new Promise((resolve) => setTimeout(resolve, ms));
184
+ }
185
+ async function withFileLock(lockDir, fn) {
186
+ const startedAt = Date.now();
187
+ while (true) {
188
+ try {
189
+ await mkdir(lockDir);
190
+ break;
191
+ }
192
+ catch (err) {
193
+ const code = err.code;
194
+ if (code !== "EEXIST" || Date.now() - startedAt > LOCK_TIMEOUT_MS)
195
+ throw err;
196
+ await sleep(LOCK_RETRY_MS);
197
+ }
198
+ }
199
+ try {
200
+ return await fn();
201
+ }
202
+ finally {
203
+ await rm(lockDir, { recursive: true, force: true });
204
+ }
205
+ }
206
+ async function readSnapshot(file) {
207
+ try {
208
+ const raw = await readFile(file, "utf-8");
209
+ const parsed = JSON.parse(raw);
210
+ return {
211
+ version: 1,
212
+ bindings: parsed.bindings ?? {},
213
+ runBindingIndex: parsed.runBindingIndex ?? {},
214
+ updatedAt: parsed.updatedAt ?? nowIso(),
215
+ };
216
+ }
217
+ catch (err) {
218
+ const code = err.code;
219
+ if (code === "ENOENT")
220
+ return emptySnapshot();
221
+ throw err;
222
+ }
223
+ }
224
+ async function writeSnapshot(file, dir, snapshot) {
225
+ await mkdir(dir, { recursive: true });
226
+ const tmp = `${file}.tmp.${process.pid}.${Date.now()}`;
227
+ try {
228
+ await writeFile(tmp, `${JSON.stringify(snapshot, null, 2)}\n`, "utf-8");
229
+ await rename(tmp, file);
230
+ }
231
+ catch (err) {
232
+ try {
233
+ await unlink(tmp);
234
+ }
235
+ catch {
236
+ /* best-effort cleanup */
237
+ }
238
+ throw err;
239
+ }
240
+ }
241
+ function sameProjectThread(a, input) {
242
+ return (a.projectThreadId === input.projectThreadId &&
243
+ a.worktreePath === input.worktreePath &&
244
+ a.repoSlug === input.repoSlug);
245
+ }
246
+ function activeBindings(snapshot) {
247
+ return Object.values(snapshot.bindings).filter((binding) => binding.status === "active");
248
+ }
249
+ function rebuildRunBindingIndex(bindings) {
250
+ const updatedAt = nowIso();
251
+ const index = {};
252
+ for (const binding of Object.values(bindings)) {
253
+ for (const runId of binding.runIds) {
254
+ index[runId] = { runId, bindingId: binding.bindingId, updatedAt };
255
+ }
256
+ }
257
+ return index;
258
+ }
259
+ function applyRunIds(binding, runIds, index) {
260
+ binding.runIds = normalizeRunIds([...binding.runIds, ...runIds]);
261
+ const updatedAt = nowIso();
262
+ for (const runId of binding.runIds) {
263
+ index[runId] = { runId, bindingId: binding.bindingId, updatedAt };
264
+ }
265
+ }
266
+ function generateSidecarContextKey(input) {
267
+ return `sidecar:${input.repoSlug}:${input.projectThreadId}:${input.sessionKey}:${randomUUID()}`;
268
+ }
269
+ export class CodexWindowBindingStore {
270
+ file;
271
+ dir;
272
+ lockDir;
273
+ constructor(storePath) {
274
+ const paths = bindingStorePaths(storePath);
275
+ this.file = paths.file;
276
+ this.dir = paths.dir;
277
+ this.lockDir = paths.lockDir;
278
+ }
279
+ get path() {
280
+ return this.file;
281
+ }
282
+ async load() {
283
+ return readSnapshot(this.file);
284
+ }
285
+ async list() {
286
+ const snapshot = await this.load();
287
+ return Object.values(snapshot.bindings);
288
+ }
289
+ async get(bindingId) {
290
+ const snapshot = await this.load();
291
+ return snapshot.bindings[bindingId] ?? null;
292
+ }
293
+ async findActiveByIdentity(identity) {
294
+ const snapshot = await this.load();
295
+ return activeBindings(snapshot).find((binding) => sameProjectThread(binding, identity)) ?? null;
296
+ }
297
+ async findByRunId(runId) {
298
+ const snapshot = await this.load();
299
+ const indexEntry = snapshot.runBindingIndex[runId];
300
+ if (!indexEntry)
301
+ return null;
302
+ return snapshot.bindings[indexEntry.bindingId] ?? null;
303
+ }
304
+ async createOrReuse(input) {
305
+ return withFileLock(this.lockDir, async () => {
306
+ const snapshot = await readSnapshot(this.file);
307
+ const existing = activeBindings(snapshot).find((binding) => sameProjectThread(binding, input));
308
+ const runIds = normalizeRunIds(input.runIds);
309
+ const timestamp = nowIso();
310
+ if (existing) {
311
+ const repaired = existing.windowId !== input.windowId ||
312
+ existing.targetId !== (input.targetId ?? null) ||
313
+ existing.tabGroupId !== (input.tabGroupId ?? null);
314
+ existing.windowId = input.windowId;
315
+ existing.tabGroupId = input.tabGroupId ?? null;
316
+ existing.targetId = input.targetId ?? null;
317
+ existing.profileId = input.profileId ?? existing.profileId ?? "agent";
318
+ existing.profileAlias = input.profileAlias ?? existing.profileAlias ?? "oe";
319
+ existing.profileOwner = normalizeProfileOwner(input.profileOwner ?? existing.profileOwner);
320
+ existing.updatedAt = timestamp;
321
+ applyRunIds(existing, runIds, snapshot.runBindingIndex);
322
+ snapshot.updatedAt = timestamp;
323
+ this.assertNoActiveConflicts(snapshot);
324
+ await writeSnapshot(this.file, this.dir, snapshot);
325
+ return { binding: existing, action: repaired ? "repaired" : "reused" };
326
+ }
327
+ const conflictingWindowOwners = activeBindings(snapshot).filter((binding) => binding.windowId === input.windowId);
328
+ if (conflictingWindowOwners.length > 0) {
329
+ throw new WindowBindingConflictError(`Window ${input.windowId} is already owned by an active Codex binding`, conflictingWindowOwners);
330
+ }
331
+ if (input.sidecarContextKey &&
332
+ Object.values(snapshot.bindings).some((binding) => binding.sidecarContextKey === input.sidecarContextKey)) {
333
+ throw new WindowBindingConflictError(`Sidecar context ${input.sidecarContextKey} is already owned by an active Codex binding`, Object.values(snapshot.bindings).filter((binding) => binding.sidecarContextKey === input.sidecarContextKey));
334
+ }
335
+ const binding = {
336
+ bindingId: randomUUID(),
337
+ codexSessionId: input.codexSessionId,
338
+ projectThreadId: input.projectThreadId,
339
+ projectThreadFamily: input.projectThreadFamily,
340
+ worktreePath: input.worktreePath,
341
+ repoSlug: input.repoSlug,
342
+ branchName: input.branchName,
343
+ sessionKey: input.sessionKey,
344
+ role: input.role,
345
+ runIds,
346
+ windowId: input.windowId,
347
+ tabGroupId: input.tabGroupId ?? null,
348
+ targetId: input.targetId ?? null,
349
+ profileId: input.profileId ?? "agent",
350
+ profileAlias: input.profileAlias ?? "oe",
351
+ profileOwner: normalizeProfileOwner(input.profileOwner ?? "agent"),
352
+ sidecarContextKey: input.sidecarContextKey ?? generateSidecarContextKey(input),
353
+ status: "active",
354
+ createdAt: timestamp,
355
+ updatedAt: timestamp,
356
+ };
357
+ snapshot.bindings[binding.bindingId] = binding;
358
+ applyRunIds(binding, runIds, snapshot.runBindingIndex);
359
+ snapshot.updatedAt = timestamp;
360
+ this.assertNoActiveConflicts(snapshot);
361
+ await writeSnapshot(this.file, this.dir, snapshot);
362
+ return { binding, action: "created" };
363
+ });
364
+ }
365
+ async addRunId(bindingId, runId) {
366
+ return withFileLock(this.lockDir, async () => {
367
+ const snapshot = await readSnapshot(this.file);
368
+ const binding = snapshot.bindings[bindingId];
369
+ if (!binding)
370
+ throw new Error(`Unknown bindingId: ${bindingId}`);
371
+ binding.updatedAt = nowIso();
372
+ applyRunIds(binding, [runId], snapshot.runBindingIndex);
373
+ snapshot.updatedAt = binding.updatedAt;
374
+ await writeSnapshot(this.file, this.dir, snapshot);
375
+ return binding;
376
+ });
377
+ }
378
+ async transition(bindingId, status) {
379
+ return withFileLock(this.lockDir, async () => {
380
+ const snapshot = await readSnapshot(this.file);
381
+ const binding = snapshot.bindings[bindingId];
382
+ if (!binding)
383
+ throw new Error(`Unknown bindingId: ${bindingId}`);
384
+ binding.status = status;
385
+ binding.updatedAt = nowIso();
386
+ snapshot.updatedAt = binding.updatedAt;
387
+ await writeSnapshot(this.file, this.dir, snapshot);
388
+ return binding;
389
+ });
390
+ }
391
+ async transitionByRunId(runId, status) {
392
+ return withFileLock(this.lockDir, async () => {
393
+ const snapshot = await readSnapshot(this.file);
394
+ const indexEntry = snapshot.runBindingIndex[runId];
395
+ if (!indexEntry)
396
+ return null;
397
+ const binding = snapshot.bindings[indexEntry.bindingId];
398
+ if (!binding)
399
+ return null;
400
+ binding.status = status;
401
+ binding.updatedAt = nowIso();
402
+ snapshot.runBindingIndex[runId] = {
403
+ runId,
404
+ bindingId: binding.bindingId,
405
+ updatedAt: binding.updatedAt,
406
+ };
407
+ snapshot.updatedAt = binding.updatedAt;
408
+ await writeSnapshot(this.file, this.dir, snapshot);
409
+ return binding;
410
+ });
411
+ }
412
+ async classifyConflicts() {
413
+ return withFileLock(this.lockDir, async () => {
414
+ const snapshot = await readSnapshot(this.file);
415
+ const conflicts = this.findActiveConflicts(snapshot);
416
+ const timestamp = nowIso();
417
+ for (const conflict of conflicts) {
418
+ conflict.status = "conflict";
419
+ conflict.updatedAt = timestamp;
420
+ }
421
+ if (conflicts.length > 0) {
422
+ snapshot.updatedAt = timestamp;
423
+ snapshot.runBindingIndex = rebuildRunBindingIndex(snapshot.bindings);
424
+ await writeSnapshot(this.file, this.dir, snapshot);
425
+ }
426
+ return conflicts;
427
+ });
428
+ }
429
+ async markStaleForMissingWindows(liveWindowIds) {
430
+ const live = new Set(liveWindowIds);
431
+ return withFileLock(this.lockDir, async () => {
432
+ const snapshot = await readSnapshot(this.file);
433
+ const stale = activeBindings(snapshot).filter((binding) => !live.has(binding.windowId));
434
+ const timestamp = nowIso();
435
+ for (const binding of stale) {
436
+ binding.status = "stale";
437
+ binding.updatedAt = timestamp;
438
+ }
439
+ if (stale.length > 0) {
440
+ snapshot.updatedAt = timestamp;
441
+ await writeSnapshot(this.file, this.dir, snapshot);
442
+ }
443
+ return stale;
444
+ });
445
+ }
446
+ assertNoActiveConflicts(snapshot) {
447
+ const conflicts = this.findActiveConflicts(snapshot);
448
+ if (conflicts.length > 0) {
449
+ throw new WindowBindingConflictError("Active Codex window binding conflict", conflicts);
450
+ }
451
+ }
452
+ findActiveConflicts(snapshot) {
453
+ const active = activeBindings(snapshot);
454
+ const conflicted = new Set();
455
+ const byWindow = new Map();
456
+ const byProject = new Map();
457
+ for (const binding of active) {
458
+ const windowGroup = byWindow.get(binding.windowId) ?? [];
459
+ windowGroup.push(binding);
460
+ byWindow.set(binding.windowId, windowGroup);
461
+ const projectKey = `${binding.repoSlug}\0${binding.worktreePath}\0${binding.projectThreadId}`;
462
+ const projectGroup = byProject.get(projectKey) ?? [];
463
+ projectGroup.push(binding);
464
+ byProject.set(projectKey, projectGroup);
465
+ }
466
+ for (const group of [...byWindow.values(), ...byProject.values()]) {
467
+ if (group.length > 1) {
468
+ for (const binding of group)
469
+ conflicted.add(binding.bindingId);
470
+ }
471
+ }
472
+ return active.filter((binding) => conflicted.has(binding.bindingId));
473
+ }
474
+ }
475
+ export const windowBindingStore = new CodexWindowBindingStore();
476
+ //# sourceMappingURL=window-bindings.js.map