agor-live 0.6.2 → 0.6.4

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.
@@ -230,11 +230,6 @@ async function getDaemonUrl() {
230
230
  const envPort = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : void 0;
231
231
  const port = envPort || config.daemon?.port || defaults.daemon?.port || 3030;
232
232
  const host = config.daemon?.host || defaults.daemon?.host || "localhost";
233
- const codespaceName = process.env.CODESPACE_NAME;
234
- const codespacesDomain = process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN;
235
- if (codespaceName && codespacesDomain) {
236
- return `https://${codespaceName}-${port}.${codespacesDomain}`;
237
- }
238
233
  return `http://${host}:${port}`;
239
234
  }
240
235
  function loadConfigSync() {
@@ -69,10 +69,14 @@ declare function unsetConfigValue(key: string): Promise<void>;
69
69
  /**
70
70
  * Get daemon URL from config
71
71
  *
72
- * Reads daemon host and port from config (with env var overrides).
73
- * Automatically detects GitHub Codespaces environment if DAEMON_URL not explicitly set.
72
+ * Returns internal daemon URL for backend-to-backend communication.
73
+ * Always returns localhost-based URL since all backend components (daemon, CLI, SDKs)
74
+ * run in the same environment.
74
75
  *
75
- * @returns Daemon URL (e.g., "http://localhost:3030" or Codespaces URL)
76
+ * For external access (browser UI), use frontend's getDaemonUrl() which detects
77
+ * the appropriate public URL via window.location.
78
+ *
79
+ * @returns Daemon URL (e.g., "http://localhost:3030")
76
80
  */
77
81
  declare function getDaemonUrl(): Promise<string>;
78
82
  /**
@@ -69,10 +69,14 @@ declare function unsetConfigValue(key: string): Promise<void>;
69
69
  /**
70
70
  * Get daemon URL from config
71
71
  *
72
- * Reads daemon host and port from config (with env var overrides).
73
- * Automatically detects GitHub Codespaces environment if DAEMON_URL not explicitly set.
72
+ * Returns internal daemon URL for backend-to-backend communication.
73
+ * Always returns localhost-based URL since all backend components (daemon, CLI, SDKs)
74
+ * run in the same environment.
74
75
  *
75
- * @returns Daemon URL (e.g., "http://localhost:3030" or Codespaces URL)
76
+ * For external access (browser UI), use frontend's getDaemonUrl() which detects
77
+ * the appropriate public URL via window.location.
78
+ *
79
+ * @returns Daemon URL (e.g., "http://localhost:3030")
76
80
  */
77
81
  declare function getDaemonUrl(): Promise<string>;
78
82
  /**
@@ -156,11 +156,6 @@ async function getDaemonUrl() {
156
156
  const envPort = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : void 0;
157
157
  const port = envPort || config.daemon?.port || defaults.daemon?.port || 3030;
158
158
  const host = config.daemon?.host || defaults.daemon?.host || "localhost";
159
- const codespaceName = process.env.CODESPACE_NAME;
160
- const codespacesDomain = process.env.GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN;
161
- if (codespaceName && codespacesDomain) {
162
- return `https://${codespaceName}-${port}.${codespacesDomain}`;
163
- }
164
159
  return `http://${host}:${port}`;
165
160
  }
166
161
  function loadConfigSync() {
@@ -2112,6 +2112,47 @@ var MessagesRepository = class {
2112
2112
  // src/db/repositories/repos.ts
2113
2113
  var import_drizzle_orm8 = require("drizzle-orm");
2114
2114
  init_ids();
2115
+
2116
+ // src/db/repositories/merge-utils.ts
2117
+ function isPlainObject(value) {
2118
+ return value !== null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date) && Object.getPrototypeOf(value) === Object.prototype;
2119
+ }
2120
+ function deepMerge(target, source) {
2121
+ const result = { ...target };
2122
+ for (const key in source) {
2123
+ if (!Object.hasOwn(source, key)) {
2124
+ continue;
2125
+ }
2126
+ const sourceValue = source[key];
2127
+ const targetValue = target[key];
2128
+ if (sourceValue === void 0) {
2129
+ continue;
2130
+ }
2131
+ if (sourceValue === null) {
2132
+ result[key] = null;
2133
+ continue;
2134
+ }
2135
+ if (Array.isArray(sourceValue)) {
2136
+ result[key] = sourceValue;
2137
+ continue;
2138
+ }
2139
+ if (isPlainObject(sourceValue)) {
2140
+ if (isPlainObject(targetValue)) {
2141
+ result[key] = deepMerge(
2142
+ targetValue,
2143
+ sourceValue
2144
+ );
2145
+ } else {
2146
+ result[key] = sourceValue;
2147
+ }
2148
+ continue;
2149
+ }
2150
+ result[key] = sourceValue;
2151
+ }
2152
+ return result;
2153
+ }
2154
+
2155
+ // src/db/repositories/repos.ts
2115
2156
  var RepoRepository = class {
2116
2157
  constructor(db) {
2117
2158
  this.db = db;
@@ -2250,27 +2291,29 @@ var RepoRepository = class {
2250
2291
  return this.findAll();
2251
2292
  }
2252
2293
  /**
2253
- * Update repo by ID
2294
+ * Update repo by ID (atomic with database-level transaction)
2295
+ *
2296
+ * Uses a transaction to ensure read-merge-write is atomic, preventing race conditions
2297
+ * when multiple updates happen concurrently (e.g., permission_config updates).
2254
2298
  */
2255
2299
  async update(id, updates) {
2256
2300
  try {
2257
2301
  const fullId = await this.resolveId(id);
2258
- const current = await this.findById(fullId);
2259
- if (!current) {
2260
- throw new EntityNotFoundError("Repo", id);
2261
- }
2262
- const merged = { ...current, ...updates };
2263
- const insert = this.repoToInsert(merged);
2264
- await this.db.update(repos).set({
2265
- slug: insert.slug,
2266
- updated_at: /* @__PURE__ */ new Date(),
2267
- data: insert.data
2268
- }).where((0, import_drizzle_orm8.eq)(repos.repo_id, fullId));
2269
- const updated = await this.findById(fullId);
2270
- if (!updated) {
2271
- throw new RepositoryError("Failed to retrieve updated repo");
2272
- }
2273
- return updated;
2302
+ return await this.db.transaction(async (tx) => {
2303
+ const currentRow = await tx.select().from(repos).where((0, import_drizzle_orm8.eq)(repos.repo_id, fullId)).get();
2304
+ if (!currentRow) {
2305
+ throw new EntityNotFoundError("Repo", id);
2306
+ }
2307
+ const current = this.rowToRepo(currentRow);
2308
+ const merged = deepMerge(current, updates);
2309
+ const insert = this.repoToInsert(merged);
2310
+ await tx.update(repos).set({
2311
+ slug: insert.slug,
2312
+ updated_at: /* @__PURE__ */ new Date(),
2313
+ data: insert.data
2314
+ }).where((0, import_drizzle_orm8.eq)(repos.repo_id, fullId));
2315
+ return merged;
2316
+ });
2274
2317
  } catch (error) {
2275
2318
  if (error instanceof RepositoryError) throw error;
2276
2319
  if (error instanceof EntityNotFoundError) throw error;
@@ -2580,45 +2623,37 @@ var SessionRepository = class {
2580
2623
  }
2581
2624
  }
2582
2625
  /**
2583
- * Update session by ID
2626
+ * Update session by ID (atomic with database-level transaction)
2627
+ *
2628
+ * Uses a transaction to ensure read-merge-write is atomic, preventing race conditions
2629
+ * when multiple updates happen concurrently (e.g., user changes settings while permission
2630
+ * hook is saving allowedTools).
2584
2631
  */
2585
2632
  async update(id, updates) {
2586
2633
  try {
2587
2634
  const fullId = await this.resolveId(id);
2588
- const current = await this.findById(fullId);
2589
- if (!current) {
2590
- throw new EntityNotFoundError("Session", id);
2591
- }
2592
- const merged = {
2593
- ...current,
2594
- ...updates
2595
- };
2596
- if (updates.permission_config) {
2597
- console.log(`\u{1F4DD} [SessionRepository] Merging permission_config update`);
2598
- console.log(
2599
- ` Before merge - current.permission_config: ${JSON.stringify(current.permission_config)}`
2600
- );
2601
- console.log(` Update permission_config: ${JSON.stringify(updates.permission_config)}`);
2602
- console.log(
2603
- ` After merge - merged.permission_config: ${JSON.stringify(merged.permission_config)}`
2604
- );
2605
- }
2606
- const insert = this.sessionToInsert(merged);
2607
- console.log(`\u{1F5C4}\uFE0F [SessionRepository] Writing to DB:`);
2608
- console.log(
2609
- ` insert.data.permission_config: ${JSON.stringify(insert.data.permission_config)}`
2610
- );
2611
- await this.db.update(sessions).set({
2612
- status: insert.status,
2613
- updated_at: /* @__PURE__ */ new Date(),
2614
- data: insert.data
2615
- }).where((0, import_drizzle_orm9.eq)(sessions.session_id, fullId));
2616
- console.log(`\u2705 [SessionRepository] DB update complete`);
2617
- const updated = await this.findById(fullId);
2618
- if (!updated) {
2619
- throw new RepositoryError("Failed to retrieve updated session");
2620
- }
2621
- return updated;
2635
+ return await this.db.transaction(async (tx) => {
2636
+ const currentRow = await tx.select().from(sessions).where((0, import_drizzle_orm9.eq)(sessions.session_id, fullId)).get();
2637
+ if (!currentRow) {
2638
+ throw new EntityNotFoundError("Session", id);
2639
+ }
2640
+ const current = this.rowToSession(currentRow);
2641
+ const merged = deepMerge(current, updates);
2642
+ if (updates.permission_config) {
2643
+ console.log(`\u{1F4DD} [SessionRepository] Merging permission_config update (atomic tx)`);
2644
+ console.log(` Before merge: ${JSON.stringify(current.permission_config)}`);
2645
+ console.log(` Update: ${JSON.stringify(updates.permission_config)}`);
2646
+ console.log(` After merge: ${JSON.stringify(merged.permission_config)}`);
2647
+ }
2648
+ const insert = this.sessionToInsert(merged);
2649
+ await tx.update(sessions).set({
2650
+ status: insert.status,
2651
+ updated_at: /* @__PURE__ */ new Date(),
2652
+ data: insert.data
2653
+ }).where((0, import_drizzle_orm9.eq)(sessions.session_id, fullId));
2654
+ console.log(`\u2705 [SessionRepository] Atomic update complete`);
2655
+ return merged;
2656
+ });
2622
2657
  } catch (error) {
2623
2658
  if (error instanceof RepositoryError) throw error;
2624
2659
  if (error instanceof EntityNotFoundError) throw error;
@@ -2982,9 +3017,7 @@ var TaskRepository = class {
2982
3017
  const inserts = taskList.map((task) => this.taskToInsert(task));
2983
3018
  await this.db.insert(tasks).values(inserts);
2984
3019
  const taskIds = inserts.map((t) => t.task_id);
2985
- const rows = await this.db.select().from(tasks).where(
2986
- import_drizzle_orm11.sql`${tasks.task_id} IN ${import_drizzle_orm11.sql.raw(`(${taskIds.map((id) => `'${id}'`).join(",")})`)}`
2987
- );
3020
+ const rows = await this.db.select().from(tasks).where(import_drizzle_orm11.sql`${tasks.task_id} IN ${import_drizzle_orm11.sql.raw(`(${taskIds.map((id) => `'${id}'`).join(",")})`)}`);
2988
3021
  return rows.map((row) => this.rowToTask(row));
2989
3022
  } catch (error) {
2990
3023
  throw new RepositoryError(
@@ -3067,27 +3100,29 @@ var TaskRepository = class {
3067
3100
  }
3068
3101
  }
3069
3102
  /**
3070
- * Update task by ID
3103
+ * Update task by ID (atomic with database-level transaction)
3104
+ *
3105
+ * Uses a transaction to ensure read-merge-write is atomic, preventing race conditions
3106
+ * when multiple updates happen concurrently (e.g., task status + message_range updates).
3071
3107
  */
3072
3108
  async update(id, updates) {
3073
3109
  try {
3074
3110
  const fullId = await this.resolveId(id);
3075
- const current = await this.findById(fullId);
3076
- if (!current) {
3077
- throw new EntityNotFoundError("Task", id);
3078
- }
3079
- const merged = { ...current, ...updates };
3080
- const insert = this.taskToInsert(merged);
3081
- await this.db.update(tasks).set({
3082
- status: insert.status,
3083
- completed_at: insert.completed_at,
3084
- data: insert.data
3085
- }).where((0, import_drizzle_orm11.eq)(tasks.task_id, fullId));
3086
- const updated = await this.findById(fullId);
3087
- if (!updated) {
3088
- throw new RepositoryError("Failed to retrieve updated task");
3089
- }
3090
- return updated;
3111
+ return await this.db.transaction(async (tx) => {
3112
+ const currentRow = await tx.select().from(tasks).where((0, import_drizzle_orm11.eq)(tasks.task_id, fullId)).get();
3113
+ if (!currentRow) {
3114
+ throw new EntityNotFoundError("Task", id);
3115
+ }
3116
+ const current = this.rowToTask(currentRow);
3117
+ const merged = deepMerge(current, updates);
3118
+ const insert = this.taskToInsert(merged);
3119
+ await tx.update(tasks).set({
3120
+ status: insert.status,
3121
+ completed_at: insert.completed_at,
3122
+ data: insert.data
3123
+ }).where((0, import_drizzle_orm11.eq)(tasks.task_id, fullId));
3124
+ return merged;
3125
+ });
3091
3126
  } catch (error) {
3092
3127
  if (error instanceof RepositoryError) throw error;
3093
3128
  if (error instanceof EntityNotFoundError) throw error;
@@ -3240,24 +3275,37 @@ var WorktreeRepository = class {
3240
3275
  return rows.map((row) => this.rowToWorktree(row));
3241
3276
  }
3242
3277
  /**
3243
- * Update worktree by ID
3278
+ * Update worktree by ID (atomic with database-level transaction)
3279
+ *
3280
+ * Uses a transaction to ensure read-merge-write is atomic, preventing race conditions
3281
+ * when multiple updates happen concurrently (e.g., schedule config + environment updates).
3244
3282
  */
3245
3283
  async update(id, updates) {
3246
3284
  const existing = await this.findById(id);
3247
3285
  if (!existing) {
3248
3286
  throw new EntityNotFoundError("Worktree", id);
3249
3287
  }
3250
- const merged = {
3251
- ...existing,
3252
- ...updates,
3253
- worktree_id: existing.worktree_id,
3254
- repo_id: existing.repo_id,
3255
- created_at: existing.created_at,
3256
- updated_at: (/* @__PURE__ */ new Date()).toISOString()
3257
- };
3258
- const insert = this.worktreeToInsert(merged);
3259
- const [row] = await this.db.update(worktrees).set(insert).where((0, import_drizzle_orm12.eq)(worktrees.worktree_id, existing.worktree_id)).returning();
3260
- return this.rowToWorktree(row);
3288
+ return await this.db.transaction(async (tx) => {
3289
+ const currentRow = await tx.select().from(worktrees).where((0, import_drizzle_orm12.eq)(worktrees.worktree_id, existing.worktree_id)).get();
3290
+ if (!currentRow) {
3291
+ throw new EntityNotFoundError("Worktree", id);
3292
+ }
3293
+ const current = this.rowToWorktree(currentRow);
3294
+ const merged = deepMerge(current, {
3295
+ ...updates,
3296
+ worktree_id: current.worktree_id,
3297
+ // Never change ID
3298
+ repo_id: current.repo_id,
3299
+ // Never change repo
3300
+ created_at: current.created_at,
3301
+ // Never change created timestamp
3302
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
3303
+ // Always update timestamp
3304
+ });
3305
+ const insert = this.worktreeToInsert(merged);
3306
+ const [row] = await tx.update(worktrees).set(insert).where((0, import_drizzle_orm12.eq)(worktrees.worktree_id, current.worktree_id)).returning();
3307
+ return this.rowToWorktree(row);
3308
+ });
3261
3309
  }
3262
3310
  /**
3263
3311
  * Delete worktree by ID
@@ -2,8 +2,8 @@ export { and, desc, eq, inArray, like, or, sql } from 'drizzle-orm';
2
2
  import bcryptjs from 'bcryptjs';
3
3
  import { d as Database } from '../client-B7p2_7Ut.cjs';
4
4
  export { H as BoardCommentInsert, G as BoardCommentRow, p as BoardInsert, F as BoardObjectInsert, E as BoardObjectRow, B as BoardRow, e as DEFAULT_DB_PATH, a as DatabaseConnectionError, D as DbConfig, z as MCPServerInsert, y as MCPServerRow, o as MessageInsert, M as MessageRow, q as RepoInsert, R as RepoRow, l as SessionInsert, C as SessionMCPServerInsert, A as SessionMCPServerRow, S as SessionRow, n as TaskInsert, T as TaskRow, x as UserInsert, U as UserRow, v as WorktreeInsert, W as WorktreeRow, k as boardComments, i as boardObjects, g as boards, c as createDatabase, b as createDatabaseAsync, f as createLocalDatabase, h as mcpServers, m as messages, r as repos, j as sessionMcpServers, s as sessions, t as tasks, u as users, w as worktrees } from '../client-B7p2_7Ut.cjs';
5
- import { B as BaseRepository } from '../worktrees-Df_shHCZ.cjs';
6
- export { A as AmbiguousIdError, E as EntityNotFoundError, M as MCPServerRepository, a as MessagesRepository, b as RepoRepository, R as RepositoryError, S as SessionMCPServerRepository, c as SessionRepository, W as WorktreeRepository } from '../worktrees-Df_shHCZ.cjs';
5
+ import { B as BaseRepository } from '../worktrees-BEIruvGs.cjs';
6
+ export { A as AmbiguousIdError, E as EntityNotFoundError, M as MCPServerRepository, a as MessagesRepository, b as RepoRepository, R as RepositoryError, S as SessionMCPServerRepository, c as SessionRepository, W as WorktreeRepository } from '../worktrees-BEIruvGs.cjs';
7
7
  import { B as BoardComment } from '../board-comment-CaUiaZB5.cjs';
8
8
  import { B as BoardID, W as WorktreeID } from '../id-DMqyogFB.cjs';
9
9
  import { a as BoardEntityObject, e as Board, d as BoardObject } from '../board-Oa2OJ-K3.cjs';
@@ -506,7 +506,10 @@ declare class TaskRepository implements BaseRepository<Task, Partial<Task>> {
506
506
  */
507
507
  findByStatus(status: Task['status']): Promise<Task[]>;
508
508
  /**
509
- * Update task by ID
509
+ * Update task by ID (atomic with database-level transaction)
510
+ *
511
+ * Uses a transaction to ensure read-merge-write is atomic, preventing race conditions
512
+ * when multiple updates happen concurrently (e.g., task status + message_range updates).
510
513
  */
511
514
  update(id: string, updates: Partial<Task>): Promise<Task>;
512
515
  /**
@@ -2,8 +2,8 @@ export { and, desc, eq, inArray, like, or, sql } from 'drizzle-orm';
2
2
  import bcryptjs from 'bcryptjs';
3
3
  import { d as Database } from '../client-CklMhh5V.js';
4
4
  export { H as BoardCommentInsert, G as BoardCommentRow, p as BoardInsert, F as BoardObjectInsert, E as BoardObjectRow, B as BoardRow, e as DEFAULT_DB_PATH, a as DatabaseConnectionError, D as DbConfig, z as MCPServerInsert, y as MCPServerRow, o as MessageInsert, M as MessageRow, q as RepoInsert, R as RepoRow, l as SessionInsert, C as SessionMCPServerInsert, A as SessionMCPServerRow, S as SessionRow, n as TaskInsert, T as TaskRow, x as UserInsert, U as UserRow, v as WorktreeInsert, W as WorktreeRow, k as boardComments, i as boardObjects, g as boards, c as createDatabase, b as createDatabaseAsync, f as createLocalDatabase, h as mcpServers, m as messages, r as repos, j as sessionMcpServers, s as sessions, t as tasks, u as users, w as worktrees } from '../client-CklMhh5V.js';
5
- import { B as BaseRepository } from '../worktrees-DCLpNeje.js';
6
- export { A as AmbiguousIdError, E as EntityNotFoundError, M as MCPServerRepository, a as MessagesRepository, b as RepoRepository, R as RepositoryError, S as SessionMCPServerRepository, c as SessionRepository, W as WorktreeRepository } from '../worktrees-DCLpNeje.js';
5
+ import { B as BaseRepository } from '../worktrees-Dv8M5-3U.js';
6
+ export { A as AmbiguousIdError, E as EntityNotFoundError, M as MCPServerRepository, a as MessagesRepository, b as RepoRepository, R as RepositoryError, S as SessionMCPServerRepository, c as SessionRepository, W as WorktreeRepository } from '../worktrees-Dv8M5-3U.js';
7
7
  import { B as BoardComment } from '../board-comment-BCrDUioT.js';
8
8
  import { B as BoardID, W as WorktreeID } from '../id-DMqyogFB.js';
9
9
  import { a as BoardEntityObject, e as Board, d as BoardObject } from '../board-ava4cdq5.js';
@@ -506,7 +506,10 @@ declare class TaskRepository implements BaseRepository<Task, Partial<Task>> {
506
506
  */
507
507
  findByStatus(status: Task['status']): Promise<Task[]>;
508
508
  /**
509
- * Update task by ID
509
+ * Update task by ID (atomic with database-level transaction)
510
+ *
511
+ * Uses a transaction to ensure read-merge-write is atomic, preventing race conditions
512
+ * when multiple updates happen concurrently (e.g., task status + message_range updates).
510
513
  */
511
514
  update(id: string, updates: Partial<Task>): Promise<Task>;
512
515
  /**