flyspace-cli 0.1.15 → 0.1.16

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.
@@ -263,7 +263,7 @@ function stopAutomationRegistryPolling() {
263
263
  }
264
264
 
265
265
  // src/channels.ts
266
- import { existsSync as existsSync2, mkdirSync, statSync } from "fs";
266
+ import { existsSync as existsSync2, mkdirSync, realpathSync, statSync } from "fs";
267
267
  import { homedir } from "os";
268
268
  import { isAbsolute, resolve as resolve2, sep } from "path";
269
269
  function isLogicalCwd(p) {
@@ -272,11 +272,22 @@ function isLogicalCwd(p) {
272
272
  function logicalRoot() {
273
273
  return process.env["FLYSPACE_ROOT"] || resolve2(homedir(), "Flyspace");
274
274
  }
275
+ var workspaceRoots = /* @__PURE__ */ new Map();
276
+ async function refreshWorkspaceRoots() {
277
+ const { data, error } = await getSupabaseClient().from("workspaces").select("slug, root_path");
278
+ if (error) return;
279
+ workspaceRoots = new Map(
280
+ (data ?? []).filter((w) => w.root_path).map((w) => [w.slug, expandPath(w.root_path)])
281
+ );
282
+ }
275
283
  function resolveLogicalCwd(p) {
276
- return resolve2(logicalRoot(), p);
284
+ const [slug, name] = p.split("/");
285
+ const root = workspaceRoots.get(slug);
286
+ return root ? resolve2(root, name) : resolve2(logicalRoot(), p);
277
287
  }
278
288
  function ensureLogicalCwdDir(cwd) {
279
- if (!cwd.startsWith(logicalRoot() + sep) || existsSync2(cwd)) return;
289
+ const roots = [logicalRoot(), ...workspaceRoots.values()];
290
+ if (!roots.some((r) => cwd.startsWith(r + sep)) || existsSync2(cwd)) return;
280
291
  mkdirSync(cwd, { recursive: true });
281
292
  }
282
293
  function expandPath(p) {
@@ -358,6 +369,9 @@ function indexRegistry(list) {
358
369
  );
359
370
  }
360
371
  async function refreshChannelRegistry() {
372
+ const rootsBefore = JSON.stringify([...workspaceRoots]);
373
+ await refreshWorkspaceRoots();
374
+ if (JSON.stringify([...workspaceRoots]) !== rootsBefore) lastSnapshot2 = void 0;
361
375
  const { data, error } = await getSupabaseClient().from("channels").select("*").order("position", { ascending: true });
362
376
  if (error) {
363
377
  console.error("[channels] refreshChannelRegistry failed:", error.message);
@@ -522,7 +536,44 @@ function toRow(input) {
522
536
  if (input.archivedAt !== void 0) row["archived_at"] = input.archivedAt;
523
537
  return row;
524
538
  }
539
+ var ChannelCwdError = class extends Error {
540
+ constructor(message) {
541
+ super(message);
542
+ this.name = "ChannelCwdError";
543
+ }
544
+ };
545
+ function realpathOrSelf(p) {
546
+ try {
547
+ return realpathSync(p);
548
+ } catch {
549
+ return p;
550
+ }
551
+ }
552
+ async function assertCwdInWorkspace(profile, cwd) {
553
+ if (isLogicalCwd(cwd)) {
554
+ if (cwd.split("/")[0] !== profile) {
555
+ throw new ChannelCwdError(`Folder "${cwd}" belongs to another workspace.`);
556
+ }
557
+ return cwd;
558
+ }
559
+ const { data, error } = await getSupabaseClient().from("workspaces").select("slug, root_path");
560
+ if (error) throw new Error(error.message);
561
+ const workspaces = data ?? [];
562
+ const ownRoot = workspaces.find((w) => w.slug === profile)?.root_path;
563
+ const home = ownRoot?.match(/^(\/Users\/[^/]+|\/home\/[^/]+|\/root)(?=\/|$)/)?.[1];
564
+ const absolute = home && (cwd === "~" || cwd.startsWith("~/")) ? resolve2(home, cwd.slice(2)) : expandPath(cwd);
565
+ const path = realpathOrSelf(absolute);
566
+ const roots = workspaces.filter((w) => w.root_path).map((w) => ({ slug: w.slug, root: realpathOrSelf(expandPath(w.root_path)) }));
567
+ const owner = roots.filter(({ root }) => path === root || path.startsWith(root + sep)).sort((a, b) => b.root.length - a.root.length)[0];
568
+ if (owner ? owner.slug !== profile : roots.some((r) => r.slug === profile)) {
569
+ throw new ChannelCwdError(
570
+ owner ? `Folder "${cwd}" belongs to the "${owner.slug}" workspace.` : `Folder "${cwd}" is outside this workspace's folder.`
571
+ );
572
+ }
573
+ return absolute;
574
+ }
525
575
  async function createChannel(input) {
576
+ input = { ...input, cwd: await assertCwdInWorkspace(getProfile(), input.cwd) };
526
577
  const { data, error } = await getSupabaseClient().from("channels").insert({ ...toRow(input), profile: getProfile() }).select().single();
527
578
  if (error) throw new Error(error.message);
528
579
  const channel = rowToChannel(data);
@@ -547,6 +598,9 @@ async function updateChannelById(id, patch) {
547
598
  if (existing?.isDefault && (patch.visibility === "private" || patch.archivedAt || patch.isDefault === false || patch.name !== void 0 && patch.name.trim() !== existing.name)) {
548
599
  throw new DefaultChannelError();
549
600
  }
601
+ if (patch.cwd !== void 0) {
602
+ patch = { ...patch, cwd: await assertCwdInWorkspace(existing?.profile ?? getProfile(), patch.cwd) };
603
+ }
550
604
  const { data, error } = await getSupabaseClient().from("channels").update({ ...toRow(patch), updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", id).select().single();
551
605
  if (error) throw new Error(error.message);
552
606
  await refreshChannelRegistry();
@@ -1543,9 +1597,9 @@ function isAuthError(text) {
1543
1597
  }
1544
1598
  function authErrorMessage(raw) {
1545
1599
  return `The agent can't sign in to Claude right now (the Host's login expired and could not be refreshed).
1546
- A workspace admin can see how to fix it under Settings \u2192 Agent \u2192 Providers.
1600
+ To fix it, a workspace admin can open Settings \u2192 Agent \u2192 Providers and click Sign in next to Claude Code.
1547
1601
 
1548
- Original error: ${raw}`;
1602
+ > Original error: ${raw}`;
1549
1603
  }
1550
1604
  function isTransientError(text, apiErrorStatus) {
1551
1605
  if (apiErrorStatus == null) {
@@ -2111,7 +2165,7 @@ var featureFlags = {
2111
2165
  dictation: {
2112
2166
  title: "Dictation",
2113
2167
  description: "A mic button in the message composer that turns speech into text as you talk. Each phrase uses speech-to-text.",
2114
- stage: "dev"
2168
+ stage: "beta"
2115
2169
  },
2116
2170
  "agent-memory": {
2117
2171
  title: "Agent memory",
@@ -2161,6 +2215,7 @@ async function disableFeatureFlag(workspaceId, flagId) {
2161
2215
 
2162
2216
  // src/store/memory-store.ts
2163
2217
  var MEMORY_TYPES = ["preference", "decision", "fact", "reference"];
2218
+ var channelTarget = (channelId) => ({ scope: "channel", channelId });
2164
2219
  var NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
2165
2220
  var DESCRIPTION_MAX = 200;
2166
2221
  var BODY_MAX = 16e3;
@@ -2184,7 +2239,10 @@ function validateMemoryInput(input) {
2184
2239
  function rowToMemory(row) {
2185
2240
  return {
2186
2241
  id: row["id"],
2187
- channelId: row["channel_id"],
2242
+ scope: row["scope"] ?? "channel",
2243
+ workspaceId: row["workspace_id"],
2244
+ channelId: row["channel_id"] ?? null,
2245
+ userId: row["user_id"] ?? null,
2188
2246
  name: row["name"],
2189
2247
  description: row["description"],
2190
2248
  type: row["type"],
@@ -2192,23 +2250,40 @@ function rowToMemory(row) {
2192
2250
  sourceThreadId: row["source_thread_id"] ?? null,
2193
2251
  createdBy: row["created_by"],
2194
2252
  createdAt: row["created_at"],
2195
- updatedAt: row["updated_at"]
2253
+ updatedAt: row["updated_at"],
2254
+ lastUsedAt: row["last_used_at"] ?? null
2196
2255
  };
2197
2256
  }
2198
- async function listChannelMemories(channelId) {
2199
- const { data, error } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).order("updated_at", { ascending: false });
2200
- if (error) throw new Error(`listChannelMemories(${channelId}): ${error.message}`);
2257
+ function targetRow(target) {
2258
+ switch (target.scope) {
2259
+ case "channel":
2260
+ return { scope: "channel", channel_id: target.channelId };
2261
+ case "workspace":
2262
+ return { scope: "workspace", workspace_id: target.workspaceId };
2263
+ case "user":
2264
+ return { scope: "user", workspace_id: target.workspaceId, user_id: target.userId };
2265
+ }
2266
+ }
2267
+ function inTarget(query, target) {
2268
+ let q = query;
2269
+ for (const [column, value] of Object.entries(targetRow(target))) q = q.eq(column, value);
2270
+ return q;
2271
+ }
2272
+ var describeTarget = (t) => t.scope === "channel" ? t.channelId : t.scope === "workspace" ? `workspace ${t.workspaceId}` : `user ${t.userId}`;
2273
+ async function listMemories(target) {
2274
+ const { data, error } = await inTarget(getSupabaseClient().from("memories").select("*"), target).order("updated_at", { ascending: false });
2275
+ if (error) throw new Error(`listMemories(${describeTarget(target)}): ${error.message}`);
2201
2276
  return (data ?? []).map(rowToMemory);
2202
2277
  }
2203
- async function getChannelMemory(channelId, name) {
2204
- const { data, error } = await getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).eq("name", name).maybeSingle();
2205
- if (error) throw new Error(`getChannelMemory(${channelId}, ${name}): ${error.message}`);
2278
+ async function getMemory(target, name) {
2279
+ const { data, error } = await inTarget(getSupabaseClient().from("memories").select("*"), target).eq("name", name).maybeSingle();
2280
+ if (error) throw new Error(`getMemory(${describeTarget(target)}, ${name}): ${error.message}`);
2206
2281
  return data ? rowToMemory(data) : null;
2207
2282
  }
2208
- async function saveChannelMemory(input, actor) {
2283
+ async function saveMemory(target, input, actor) {
2209
2284
  validateMemoryInput(input);
2210
2285
  const supabase = getSupabaseClient();
2211
- const existing = await getChannelMemory(input.channelId, input.name);
2286
+ const existing = await getMemory(target, input.name);
2212
2287
  const fields = {
2213
2288
  description: input.description.trim(),
2214
2289
  type: input.type,
@@ -2219,9 +2294,9 @@ async function saveChannelMemory(input, actor) {
2219
2294
  if (existing && sameContent(existing, fields)) {
2220
2295
  return { memory: existing, created: false, changed: false };
2221
2296
  }
2222
- const { data, error } = existing ? await supabase.from("memories").update({ ...fields, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id).select("*").single() : await supabase.from("memories").insert({ ...fields, scope: "channel", channel_id: input.channelId, name: input.name }).select("*").single();
2297
+ const { data, error } = existing ? await supabase.from("memories").update({ ...fields, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", existing.id).select("*").single() : await supabase.from("memories").insert({ ...fields, ...targetRow(target), name: input.name }).select("*").single();
2223
2298
  if (error)
2224
- throw new Error(`saveChannelMemory(${input.channelId}, ${input.name}): ${error.message}`);
2299
+ throw new Error(`saveMemory(${describeTarget(target)}, ${input.name}): ${error.message}`);
2225
2300
  const memory = rowToMemory(data);
2226
2301
  await logMemoryEvent({
2227
2302
  memory,
@@ -2233,12 +2308,12 @@ async function saveChannelMemory(input, actor) {
2233
2308
  });
2234
2309
  return { memory, created: !existing, changed: true };
2235
2310
  }
2236
- async function updateChannelMemory(channelId, memoryId, patch, actor) {
2237
- const existing = (await listChannelMemories(channelId)).find((m) => m.id === memoryId);
2311
+ async function updateMemory(target, memoryId, patch, actor) {
2312
+ const existing = (await listMemories(target)).find((m) => m.id === memoryId);
2238
2313
  if (!existing) return null;
2239
- const { memory } = await saveChannelMemory(
2314
+ const { memory } = await saveMemory(
2315
+ target,
2240
2316
  {
2241
- channelId,
2242
2317
  name: existing.name,
2243
2318
  description: patch.description ?? existing.description,
2244
2319
  type: patch.type ?? existing.type,
@@ -2249,20 +2324,28 @@ async function updateChannelMemory(channelId, memoryId, patch, actor) {
2249
2324
  );
2250
2325
  return memory;
2251
2326
  }
2252
- async function searchChannelMemories(channelId, query) {
2327
+ async function searchMemories(target, query) {
2253
2328
  const words = query.replaceAll(/[,()*%\\]/g, " ").split(/\s+/).filter(Boolean);
2254
2329
  if (!words.length) return [];
2255
- let request = getSupabaseClient().from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId);
2330
+ let request = inTarget(getSupabaseClient().from("memories").select("*"), target);
2256
2331
  for (const word of words) {
2257
2332
  request = request.or(`name.ilike.*${word}*,description.ilike.*${word}*,body.ilike.*${word}*`);
2258
2333
  }
2259
2334
  const { data, error } = await request.order("updated_at", { ascending: false }).limit(20);
2260
- if (error) throw new Error(`searchChannelMemories(${channelId}): ${error.message}`);
2335
+ if (error) throw new Error(`searchMemories(${describeTarget(target)}): ${error.message}`);
2261
2336
  return (data ?? []).map(rowToMemory);
2262
2337
  }
2263
- async function deleteChannelMemory(channelId, idOrName, actor, threadId) {
2264
- const { data, error } = await getSupabaseClient().from("memories").delete().eq("scope", "channel").eq("channel_id", channelId).eq(isUuid(idOrName) ? "id" : "name", idOrName).select("*");
2265
- if (error) throw new Error(`deleteChannelMemory(${channelId}, ${idOrName}): ${error.message}`);
2338
+ var USED_GRANULARITY_MS = 24 * 60 * 6e4;
2339
+ async function markMemoryUsed(memory) {
2340
+ const cutoff = new Date(Date.now() - USED_GRANULARITY_MS).toISOString();
2341
+ if (memory.lastUsedAt && memory.lastUsedAt > cutoff) return;
2342
+ const { error } = await getSupabaseClient().from("memories").update({ last_used_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", memory.id).or(`last_used_at.is.null,last_used_at.lt.${cutoff}`);
2343
+ if (error) console.error(`[memory] markMemoryUsed(${memory.id}) failed:`, error.message);
2344
+ }
2345
+ async function deleteMemory(target, idOrName, actor, threadId) {
2346
+ const { data, error } = await inTarget(getSupabaseClient().from("memories").delete(), target).eq(isUuid(idOrName) ? "id" : "name", idOrName).select("*");
2347
+ if (error)
2348
+ throw new Error(`deleteMemory(${describeTarget(target)}, ${idOrName}): ${error.message}`);
2266
2349
  const deleted = (data ?? []).map(rowToMemory);
2267
2350
  for (const memory of deleted) {
2268
2351
  await logMemoryEvent({
@@ -2276,6 +2359,25 @@ async function deleteChannelMemory(channelId, idOrName, actor, threadId) {
2276
2359
  }
2277
2360
  return deleted.length > 0;
2278
2361
  }
2362
+ async function promoteChannelMemory(channelId, memoryId) {
2363
+ const supabase = getSupabaseClient();
2364
+ const existing = (await listChannelMemories(channelId)).find((m) => m.id === memoryId);
2365
+ if (!existing) return { ok: false, reason: "not-found" };
2366
+ if (await getMemory({ scope: "workspace", workspaceId: existing.workspaceId }, existing.name)) {
2367
+ return { ok: false, reason: "name-taken" };
2368
+ }
2369
+ const { data, error } = await supabase.from("memories").update({ scope: "workspace", channel_id: null, updated_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", memoryId).eq("scope", "channel").select("*").maybeSingle();
2370
+ if (error) throw new Error(`promoteChannelMemory(${memoryId}): ${error.message}`);
2371
+ if (!data) return { ok: false, reason: "not-found" };
2372
+ const memory = rowToMemory(data);
2373
+ await logMemoryEvent({ memory, action: "create", before: null, after: snapshot(memory), actor: "user", threadId: null });
2374
+ return { ok: true, memory };
2375
+ }
2376
+ var listChannelMemories = (channelId) => listMemories(channelTarget(channelId));
2377
+ var getChannelMemory = (channelId, name) => getMemory(channelTarget(channelId), name);
2378
+ var saveChannelMemory = ({ channelId, ...fields }, actor) => saveMemory(channelTarget(channelId), fields, actor);
2379
+ var updateChannelMemory = (channelId, memoryId, patch, actor) => updateMemory(channelTarget(channelId), memoryId, patch, actor);
2380
+ var deleteChannelMemory = (channelId, idOrName, actor, threadId) => deleteMemory(channelTarget(channelId), idOrName, actor, threadId);
2279
2381
  function unchangedSince(current, after) {
2280
2382
  return current && after ? sameContent(current, after) : !current && !after;
2281
2383
  }
@@ -2294,6 +2396,10 @@ function sameContent(m, other) {
2294
2396
  async function logMemoryEvent(e) {
2295
2397
  const { error } = await getSupabaseClient().from("memory_events").insert({
2296
2398
  channel_id: e.memory.channelId,
2399
+ // A channel event's workspace comes from its channel (0100 trigger).
2400
+ ...e.memory.channelId ? {} : { workspace_id: e.memory.workspaceId },
2401
+ scope: e.memory.scope,
2402
+ user_id: e.memory.userId,
2297
2403
  memory_id: e.action === "delete" ? null : e.memory.id,
2298
2404
  thread_id: e.threadId,
2299
2405
  name: e.memory.name,
@@ -2319,15 +2425,18 @@ function rowToEvent(row) {
2319
2425
  undoneAt: row["undone_at"] ?? null
2320
2426
  };
2321
2427
  }
2322
- async function listThreadMemoryEvents(channelId, threadId) {
2428
+ async function listMemoryEvents(channelId, filter = {}) {
2323
2429
  const supabase = getSupabaseClient();
2324
- const { data, error } = await supabase.from("memory_events").select("*").eq("channel_id", channelId).eq("thread_id", threadId).order("created_at", { ascending: false }).limit(50);
2325
- if (error) throw new Error(`listThreadMemoryEvents(${threadId}): ${error.message}`);
2430
+ let query = supabase.from("memory_events").select("*").eq("channel_id", channelId);
2431
+ if (filter.threadId) query = query.eq("thread_id", filter.threadId);
2432
+ if (filter.actor) query = query.eq("actor", filter.actor);
2433
+ const { data, error } = await query.order("created_at", { ascending: false }).limit(filter.limit ?? 50);
2434
+ if (error) throw new Error(`listMemoryEvents(${channelId}): ${error.message}`);
2326
2435
  const events = (data ?? []).map(rowToEvent);
2327
2436
  if (!events.length) return [];
2328
2437
  const names = [...new Set(events.map((e) => e.name))];
2329
2438
  const live = await supabase.from("memories").select("*").eq("scope", "channel").eq("channel_id", channelId).in("name", names);
2330
- if (live.error) throw new Error(`listThreadMemoryEvents(${threadId}): ${live.error.message}`);
2439
+ if (live.error) throw new Error(`listMemoryEvents(${channelId}): ${live.error.message}`);
2331
2440
  const byName = new Map(
2332
2441
  (live.data ?? []).map((r) => [
2333
2442
  r["name"],
@@ -2344,6 +2453,7 @@ async function listThreadMemoryEvents(channelId, threadId) {
2344
2453
  };
2345
2454
  });
2346
2455
  }
2456
+ var listThreadMemoryEvents = (channelId, threadId) => listMemoryEvents(channelId, { threadId, actor: "agent" });
2347
2457
  async function undoMemoryEvent(channelId, eventId) {
2348
2458
  const supabase = getSupabaseClient();
2349
2459
  const { data, error } = await supabase.from("memory_events").select("*").eq("id", eventId).eq("channel_id", channelId).maybeSingle();
@@ -2374,21 +2484,29 @@ async function undoMemoryEvent(channelId, eventId) {
2374
2484
  function isUuid(value) {
2375
2485
  return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
2376
2486
  }
2377
- function formatMemoryPrompt(channelId, memories) {
2378
- const lines = [];
2487
+ function formatMemoryPrompt(channelId, memories, extras = {}) {
2379
2488
  let used = 0;
2380
- let omitted = 0;
2381
- for (const m of memories) {
2382
- const line = `- \`${m.name}\` (${m.type}): ${m.description}`;
2383
- if (used + line.length > MEMORY_INDEX_MAX_CHARS) {
2384
- omitted++;
2385
- continue;
2489
+ const indexOf = (list, searchHint) => {
2490
+ const lines = [];
2491
+ let omitted = 0;
2492
+ for (const m of list) {
2493
+ const line = `- \`${m.name}\` (${m.type}): ${m.description}`;
2494
+ if (used + line.length > MEMORY_INDEX_MAX_CHARS) {
2495
+ omitted++;
2496
+ continue;
2497
+ }
2498
+ lines.push(line);
2499
+ used += line.length + 1;
2386
2500
  }
2387
- lines.push(line);
2388
- used += line.length + 1;
2389
- }
2390
- const index = lines.length ? lines.join("\n") + (omitted ? `
2391
- - (${omitted} older memories not listed; find them with \`memory search\`)` : "") : "(none yet)";
2501
+ if (!lines.length && !omitted) return "(none yet)";
2502
+ return lines.join("\n") + (omitted ? `
2503
+ - (${omitted} older memories not listed; find them with \`${searchHint}\`)` : "");
2504
+ };
2505
+ const user = extras.user;
2506
+ const userFlags2 = user ? `--scope user --user ${user.userId}` : "";
2507
+ const userIndex = user ? indexOf(user.memories, `memory search --channel ${channelId} ${userFlags2}`) : "";
2508
+ const channelIndex = indexOf(memories, "memory search");
2509
+ const workspaceIndex = extras.workspace?.length ? indexOf(extras.workspace, `memory search --channel ${channelId} --scope workspace`) : "";
2392
2510
  return [
2393
2511
  `## Channel memory`,
2394
2512
  ``,
@@ -2400,26 +2518,22 @@ function formatMemoryPrompt(channelId, memories) {
2400
2518
  `- Delete one that turned out wrong: \`flyspace memory delete --channel ${channelId} <name> [--thread <thread-id>]\``,
2401
2519
  ``,
2402
2520
  `Save when someone asks you to remember something, corrects you, or a decision is made that later threads will need. Save what is not derivable from the code, git history or docs: decisions and why, preferences, corrections, where external things live. Do not save one-off details of this conversation. Check the index first and update an existing memory rather than adding a near-duplicate. Write memories in English. Pass \`--thread\` with the thread id from the \`[context: ...]\` header, so the thread shows what changed and it can be undone there.`,
2521
+ ...user ? [
2522
+ ``,
2523
+ `A personal preference of @${user.handle} (language, tone, reply length, time zone, how they like to work) belongs in their personal memory instead, which only their own turns see: add \`${userFlags2}\` to the same commands. Never save someone else's preferences there, and never put project knowledge there.`
2524
+ ] : [],
2525
+ ...workspaceIndex ? [
2526
+ ``,
2527
+ `Workspace memories below apply to every channel. They are read-only to you: read one with \`--scope workspace\` on \`get\`; a Workspace Admin manages them.`
2528
+ ] : [],
2403
2529
  ``,
2530
+ ...user ? [`Personal memory of @${user.handle}:`, userIndex, ``] : [],
2404
2531
  `Index:`,
2405
- index
2532
+ channelIndex,
2533
+ ...workspaceIndex ? [``, `Workspace memory:`, workspaceIndex] : []
2406
2534
  ].join("\n");
2407
2535
  }
2408
2536
 
2409
- // src/store/memory-context.ts
2410
- async function loadAgentMemory(profile, channelId, opts = {}) {
2411
- const enabled = await listEnabledFeatureFlagsBySlug(profile).then((ids) => resolveFeatureFlags(ids, false)["agent-memory"]).catch((err) => {
2412
- console.error("[memory] feature flag lookup failed:", err.message);
2413
- return false;
2414
- });
2415
- if (!enabled || opts.readable === false || !channelId || !isUuid(channelId)) return { enabled };
2416
- const memoryPrompt = await listChannelMemories(channelId).then((memories) => formatMemoryPrompt(channelId, memories)).catch((err) => {
2417
- console.error("[memory] memory lookup failed:", err.message);
2418
- return void 0;
2419
- });
2420
- return memoryPrompt ? { enabled, memoryPrompt } : { enabled };
2421
- }
2422
-
2423
2537
  // src/store/workspace-store.ts
2424
2538
  function rowToMembership(row) {
2425
2539
  return {
@@ -2431,7 +2545,8 @@ function rowToMembership(row) {
2431
2545
  role: row["role"],
2432
2546
  status: row["status"],
2433
2547
  invitedAt: row["invited_at"],
2434
- acceptedAt: row["accepted_at"] ?? null
2548
+ acceptedAt: row["accepted_at"] ?? null,
2549
+ memoryAutoPaused: row["memory_auto_paused"] === true
2435
2550
  };
2436
2551
  }
2437
2552
  var WORKSPACE_CACHE_TTL_MS = 6e4;
@@ -2587,6 +2702,10 @@ async function listMembershipsByEmail(email) {
2587
2702
  if (error) throw new Error(`listMembershipsByEmail: ${error.message}`);
2588
2703
  return (data ?? []).map(rowToMembership);
2589
2704
  }
2705
+ async function setMemoryAutoPaused(workspaceId, userId, paused) {
2706
+ const { error } = await getSupabaseClient().from("memberships").update({ memory_auto_paused: paused }).eq("workspace_id", workspaceId).eq("user_id", userId);
2707
+ if (error) throw new Error(`setMemoryAutoPaused(${workspaceId}): ${error.message}`);
2708
+ }
2590
2709
  async function listMemberships(workspaceId) {
2591
2710
  const { data, error } = await getSupabaseClient().from("memberships").select("*").eq("workspace_id", workspaceId).order("invited_at", { ascending: true });
2592
2711
  if (error) throw new Error(`listMemberships(${workspaceId}): ${error.message}`);
@@ -2710,6 +2829,37 @@ async function setAgentPaused(workspaceId, paused) {
2710
2829
  if (error) throw new Error(`setAgentPaused(${workspaceId}): ${error.message}`);
2711
2830
  }
2712
2831
 
2832
+ // src/store/memory-context.ts
2833
+ async function loadAgentMemory(profile, channelId, opts = {}) {
2834
+ const enabled = await listEnabledFeatureFlagsBySlug(profile).then((ids) => resolveFeatureFlags(ids, false)["agent-memory"]).catch((err) => {
2835
+ console.error("[memory] feature flag lookup failed:", err.message);
2836
+ return false;
2837
+ });
2838
+ if (!enabled || opts.readable === false || !channelId || !isUuid(channelId)) return { enabled };
2839
+ const memoryPrompt = await buildPrompt(profile, channelId, opts).catch((err) => {
2840
+ console.error("[memory] memory lookup failed:", err.message);
2841
+ return void 0;
2842
+ });
2843
+ return memoryPrompt ? { enabled, memoryPrompt } : { enabled };
2844
+ }
2845
+ async function buildPrompt(profile, channelId, opts) {
2846
+ const workspace = await getWorkspaceBySlug(profile);
2847
+ const handle = opts.senderHandle?.toLowerCase();
2848
+ const sender = workspace && handle ? (await listMemberships(workspace.id)).find(
2849
+ (m) => m.userId && m.handle.toLowerCase() === handle
2850
+ ) : void 0;
2851
+ const [channel, shared, own] = await Promise.all([
2852
+ listMemories({ scope: "channel", channelId }),
2853
+ workspace ? listMemories({ scope: "workspace", workspaceId: workspace.id }) : [],
2854
+ workspace && sender ? listMemories({ scope: "user", workspaceId: workspace.id, userId: sender.userId }) : []
2855
+ ]);
2856
+ const extras = {
2857
+ workspace: shared,
2858
+ ...sender ? { user: { userId: sender.userId, handle: sender.handle, memories: own } } : {}
2859
+ };
2860
+ return formatMemoryPrompt(channelId, channel, extras);
2861
+ }
2862
+
2713
2863
  // src/dashboard/settings.ts
2714
2864
  import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync9, mkdirSync as mkdirSync6, rmSync as rmSync2 } from "fs";
2715
2865
  import { dirname, resolve as resolve5 } from "path";
@@ -4427,6 +4577,11 @@ import { CronExpressionParser } from "cron-parser";
4427
4577
  var Scheduler = class {
4428
4578
  jobs = /* @__PURE__ */ new Map();
4429
4579
  caughtUp = /* @__PURE__ */ new Set();
4580
+ // A run is only recorded when it finishes, so while one is in flight the
4581
+ // store still shows the previous tick. Without this guard, a registry
4582
+ // reload during a long run re-registers the schedule and catchup starts a
4583
+ // second, overlapping copy (sah-budget-fetch ran twice a day this way).
4584
+ running = /* @__PURE__ */ new Set();
4430
4585
  store;
4431
4586
  constructor(store) {
4432
4587
  this.store = store;
@@ -4478,6 +4633,11 @@ var Scheduler = class {
4478
4633
  wrappedRun();
4479
4634
  }
4480
4635
  async runAndRecord(name, runFn) {
4636
+ if (this.running.has(name)) {
4637
+ console.log(`[scheduler] ${name} is still running, skipping overlapping run`);
4638
+ return;
4639
+ }
4640
+ this.running.add(name);
4481
4641
  const startedAt = /* @__PURE__ */ new Date();
4482
4642
  const elapsed = () => Date.now() - startedAt.getTime();
4483
4643
  try {
@@ -4486,6 +4646,8 @@ var Scheduler = class {
4486
4646
  } catch (err) {
4487
4647
  console.error(`[scheduler] Error in ${name}:`, err);
4488
4648
  this.store?.recordRun(name, "error", startedAt, elapsed());
4649
+ } finally {
4650
+ this.running.delete(name);
4489
4651
  }
4490
4652
  }
4491
4653
  unregister(name) {
@@ -8718,6 +8880,7 @@ export {
8718
8880
  warnOnBrokenChannelCwds,
8719
8881
  resolveCwd,
8720
8882
  resolveChannelPrompt,
8883
+ ChannelCwdError,
8721
8884
  createChannel,
8722
8885
  DefaultChannelError,
8723
8886
  updateChannelById,
@@ -8745,6 +8908,7 @@ export {
8745
8908
  modelLabel,
8746
8909
  usageLimitedUntil,
8747
8910
  usageLimitedModels,
8911
+ clearClaudeAuthFailed,
8748
8912
  claudeAuthFailed,
8749
8913
  buildSystemPrompt,
8750
8914
  docsAccessPromptNote,
@@ -8761,16 +8925,23 @@ export {
8761
8925
  MEMORY_TYPES,
8762
8926
  isMemoryType,
8763
8927
  validateMemoryInput,
8928
+ listMemories,
8929
+ getMemory,
8930
+ saveMemory,
8931
+ updateMemory,
8932
+ searchMemories,
8933
+ markMemoryUsed,
8934
+ deleteMemory,
8935
+ promoteChannelMemory,
8764
8936
  listChannelMemories,
8765
8937
  getChannelMemory,
8766
8938
  saveChannelMemory,
8767
8939
  updateChannelMemory,
8768
- searchChannelMemories,
8769
8940
  deleteChannelMemory,
8941
+ listMemoryEvents,
8770
8942
  listThreadMemoryEvents,
8771
8943
  undoMemoryEvent,
8772
8944
  isUuid,
8773
- loadAgentMemory,
8774
8945
  getWorkspaceBySlug,
8775
8946
  getWorkspaceById,
8776
8947
  listWorkspaces,
@@ -8790,6 +8961,7 @@ export {
8790
8961
  getMembershipByEmail,
8791
8962
  getMembershipByUserId,
8792
8963
  listMembershipsByEmail,
8964
+ setMemoryAutoPaused,
8793
8965
  listMemberships,
8794
8966
  createMembership,
8795
8967
  HANDLE_PATTERN,
@@ -8806,6 +8978,7 @@ export {
8806
8978
  setAllowGuestAccess,
8807
8979
  getAgentPaused,
8808
8980
  setAgentPaused,
8981
+ loadAgentMemory,
8809
8982
  DEFAULT_TITLE_RULES,
8810
8983
  extractCategories,
8811
8984
  buildAutoTitle,