switchroom 0.14.74 → 0.14.76

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.
@@ -10991,6 +10991,7 @@ var AgentToolsSchema = exports_external.object({
10991
10991
  var AgentMemorySchema = exports_external.object({
10992
10992
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
10993
10993
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
10994
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
10994
10995
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
10995
10996
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
10996
10997
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -11210,6 +11211,7 @@ var profileFields = {
11210
11211
  memory: exports_external.object({
11211
11212
  collection: exports_external.string().optional(),
11212
11213
  auto_recall: exports_external.boolean().optional(),
11214
+ file: exports_external.boolean().optional(),
11213
11215
  isolation: exports_external.enum(["default", "strict"]).optional(),
11214
11216
  recall: exports_external.object({
11215
11217
  max_memories: exports_external.number().int().min(0).optional(),
@@ -10991,6 +10991,7 @@ var AgentToolsSchema = exports_external.object({
10991
10991
  var AgentMemorySchema = exports_external.object({
10992
10992
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
10993
10993
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
10994
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
10994
10995
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
10995
10996
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
10996
10997
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -11210,6 +11211,7 @@ var profileFields = {
11210
11211
  memory: exports_external.object({
11211
11212
  collection: exports_external.string().optional(),
11212
11213
  auto_recall: exports_external.boolean().optional(),
11214
+ file: exports_external.boolean().optional(),
11213
11215
  isolation: exports_external.enum(["default", "strict"]).optional(),
11214
11216
  recall: exports_external.object({
11215
11217
  max_memories: exports_external.number().int().min(0).optional(),
@@ -13890,8 +13892,29 @@ class AuthBroker {
13890
13892
  return override;
13891
13893
  return auth.active ?? null;
13892
13894
  }
13893
- async opGetCredentials(socket, id, identity2) {
13895
+ servingAccount(identity2) {
13894
13896
  const account = this.callerAccount(identity2);
13897
+ if (identity2.kind !== "consumer")
13898
+ return account;
13899
+ return this.consumerAccountWithFailover(account);
13900
+ }
13901
+ isAccountExhausted(account) {
13902
+ const q = this.quota[account];
13903
+ return q !== undefined && q.exhausted_until > this.now();
13904
+ }
13905
+ consumerAccountWithFailover(pinned) {
13906
+ if (!pinned || !this.isAccountExhausted(pinned))
13907
+ return pinned;
13908
+ for (const cand of this.config.auth?.fallback_order ?? []) {
13909
+ if (cand === pinned || this.isAccountExhausted(cand))
13910
+ continue;
13911
+ if (readAccountCredentials(cand, this.home))
13912
+ return cand;
13913
+ }
13914
+ return pinned;
13915
+ }
13916
+ async opGetCredentials(socket, id, identity2) {
13917
+ const account = this.servingAccount(identity2);
13895
13918
  if (!account) {
13896
13919
  this.audit({ op: "get-credentials", identity: identity2, ok: false, error: "no-active-account" });
13897
13920
  socket.write(encodeError(id, "ACCOUNT_NOT_FOUND", "no active account configured"));
@@ -11739,6 +11739,7 @@ var AgentToolsSchema = exports_external.object({
11739
11739
  var AgentMemorySchema = exports_external.object({
11740
11740
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
11741
11741
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
11742
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
11742
11743
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
11743
11744
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
11744
11745
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -11958,6 +11959,7 @@ var profileFields = {
11958
11959
  memory: exports_external.object({
11959
11960
  collection: exports_external.string().optional(),
11960
11961
  auto_recall: exports_external.boolean().optional(),
11962
+ file: exports_external.boolean().optional(),
11961
11963
  isolation: exports_external.enum(["default", "strict"]).optional(),
11962
11964
  recall: exports_external.object({
11963
11965
  max_memories: exports_external.number().int().min(0).optional(),
@@ -13555,6 +13555,7 @@ var init_schema = __esm(() => {
13555
13555
  AgentMemorySchema = exports_external.object({
13556
13556
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
13557
13557
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
13558
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
13558
13559
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
13559
13560
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
13560
13561
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -13774,6 +13775,7 @@ var init_schema = __esm(() => {
13774
13775
  memory: exports_external.object({
13775
13776
  collection: exports_external.string().optional(),
13776
13777
  auto_recall: exports_external.boolean().optional(),
13778
+ file: exports_external.boolean().optional(),
13777
13779
  isolation: exports_external.enum(["default", "strict"]).optional(),
13778
13780
  recall: exports_external.object({
13779
13781
  max_memories: exports_external.number().int().min(0).optional(),
@@ -28938,6 +28940,82 @@ var init_manifest = __esm(() => {
28938
28940
  ]);
28939
28941
  });
28940
28942
 
28943
+ // src/cli/doctor-memory.ts
28944
+ import { execFileSync as execFileSync17 } from "node:child_process";
28945
+ function classifyShmSize(bytes) {
28946
+ const mib = Math.round(bytes / 1024 / 1024);
28947
+ if (bytes < MIN_HINDSIGHT_SHM_BYTES) {
28948
+ return {
28949
+ name: "hindsight shm-size",
28950
+ status: "fail",
28951
+ detail: `${mib}MB \u2014 PostgreSQL needs ~533MB+ for shared segments; writes will ` + `fail with "No space left on device"`,
28952
+ fix: "Recreate hindsight with a larger shm. The launch path now sets " + "--shm-size=2g (#2190); pull a release that includes it and run " + "`switchroom memory --restart`, or recreate the container manually " + "preserving the `switchroom-hindsight-data` volume."
28953
+ };
28954
+ }
28955
+ const gib = (bytes / 1024 / 1024 / 1024).toFixed(bytes % 1024 ** 3 === 0 ? 0 : 1);
28956
+ return { name: "hindsight shm-size", status: "ok", detail: `${gib}g` };
28957
+ }
28958
+ function classifyExtractionLogs(logs) {
28959
+ const noSpace = /No space left on device|could not resize shared memory/i.test(logs);
28960
+ const llmError = /Claude Code returned an error result|claude_code_llm[\s\S]{0,80}error|Fact extraction failed|Content extraction failed/i.test(logs);
28961
+ const quotaHint = /weekly limit|api_error_status["':\s]+429|\b429\b|hit your[\s\S]{0,24}limit/i.test(logs);
28962
+ const zeroFacts = (logs.match(/Extract facts:\s*0 facts/gi) ?? []).length;
28963
+ const okFacts = (logs.match(/Extract facts:\s*[1-9]\d* facts/gi) ?? []).length;
28964
+ if (noSpace) {
28965
+ return {
28966
+ name: "hindsight extraction",
28967
+ status: "fail",
28968
+ detail: "shared-memory exhaustion in recent logs \u2014 memory writes are failing",
28969
+ fix: "See the `hindsight shm-size` check \u2014 the container's /dev/shm is too small."
28970
+ };
28971
+ }
28972
+ if (llmError && okFacts === 0) {
28973
+ return {
28974
+ name: "hindsight extraction",
28975
+ status: "fail",
28976
+ detail: "fact-extraction LLM calls are failing" + (quotaHint ? " (429 / weekly-limit detected)" : "") + " \u2014 retains are accepted but extract 0 facts, so nothing becomes recallable",
28977
+ fix: "Usually hindsight's `auth.consumers[hindsight].account` is quota-" + "exhausted or its OAuth broke. Repoint it to an account with quota in " + "switchroom.yaml, then `docker restart switchroom-auth-broker` and " + "`docker restart switchroom-hindsight` (single-file config mount needs " + "the broker restart to re-read). Confirm with a headless `claude` run " + "inside the container."
28978
+ };
28979
+ }
28980
+ if (zeroFacts >= 3 && okFacts === 0) {
28981
+ return {
28982
+ name: "hindsight extraction",
28983
+ status: "warn",
28984
+ detail: `${zeroFacts} recent extractions produced 0 facts and none succeeded \u2014 ` + "fact extraction may be failing",
28985
+ fix: "Inspect `docker logs switchroom-hindsight` for the extraction error."
28986
+ };
28987
+ }
28988
+ return {
28989
+ name: "hindsight extraction",
28990
+ status: "ok",
28991
+ detail: okFacts > 0 ? `healthy (${okFacts} recent successful extractions)` : "no recent extraction activity to assess"
28992
+ };
28993
+ }
28994
+ function checkHindsightContainerHealth(opts) {
28995
+ const name = opts?.containerName ?? "switchroom-hindsight";
28996
+ const exec = opts?.exec ?? ((cmd, args) => execFileSync17(cmd, args, { stdio: ["ignore", "pipe", "ignore"], timeout: 8000 }).toString());
28997
+ const results = [];
28998
+ let shmRaw;
28999
+ try {
29000
+ shmRaw = exec("docker", ["inspect", name, "--format", "{{.HostConfig.ShmSize}}"]).trim();
29001
+ } catch {
29002
+ return [];
29003
+ }
29004
+ const shmBytes = parseInt(shmRaw, 10);
29005
+ if (Number.isFinite(shmBytes) && shmBytes > 0) {
29006
+ results.push(classifyShmSize(shmBytes));
29007
+ }
29008
+ try {
29009
+ const logs = exec("docker", ["logs", "--since", "10m", name]);
29010
+ results.push(classifyExtractionLogs(logs));
29011
+ } catch {}
29012
+ return results;
29013
+ }
29014
+ var MIN_HINDSIGHT_SHM_BYTES;
29015
+ var init_doctor_memory = __esm(() => {
29016
+ MIN_HINDSIGHT_SHM_BYTES = 1024 * 1024 * 1024;
29017
+ });
29018
+
28941
29019
  // src/cli/doctor-docker.ts
28942
29020
  import { readFileSync as readFileSync47 } from "node:fs";
28943
29021
  function imageTagOf(ref) {
@@ -31017,7 +31095,7 @@ var init_doctor_agent_smoke = __esm(() => {
31017
31095
  });
31018
31096
 
31019
31097
  // src/cli/doctor-vault-broker-durability.ts
31020
- import { execFileSync as execFileSync17 } from "node:child_process";
31098
+ import { execFileSync as execFileSync18 } from "node:child_process";
31021
31099
  import { existsSync as existsSync54, statSync as statSync22 } from "node:fs";
31022
31100
  import { homedir as homedir33 } from "node:os";
31023
31101
  import { join as join55 } from "node:path";
@@ -31079,7 +31157,7 @@ function spawnDockerStat(p) {
31079
31157
  }
31080
31158
  function spawnDockerStatForContainer(containerName2, p) {
31081
31159
  try {
31082
- const stdout = execFileSync17("docker", ["exec", containerName2, "stat", "-c", "%i %s", p], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
31160
+ const stdout = execFileSync18("docker", ["exec", containerName2, "stat", "-c", "%i %s", p], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
31083
31161
  return { status: 0, stdout, stderr: "", error: null };
31084
31162
  } catch (err) {
31085
31163
  const e = err;
@@ -31153,7 +31231,7 @@ function probeBrokerUnlocked(opts) {
31153
31231
  }
31154
31232
  function defaultBrokerStatusProbe() {
31155
31233
  try {
31156
- const out = execFileSync17("switchroom", ["vault", "broker", "status"], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
31234
+ const out = execFileSync18("switchroom", ["vault", "broker", "status"], { stdio: ["ignore", "pipe", "pipe"], timeout: 3000, encoding: "utf8" });
31157
31235
  const parsed = JSON.parse(out.trim());
31158
31236
  if (!parsed.running)
31159
31237
  return null;
@@ -31947,6 +32025,7 @@ async function checkHindsight(config) {
31947
32025
  detail: `${probe2.serverName} ${probe2.serverVersion} at ${host}:${port}`
31948
32026
  });
31949
32027
  results.push(checkHindsightConsumer(config));
32028
+ results.push(...checkHindsightContainerHealth());
31950
32029
  for (const [agentName, agentConfig] of Object.entries(config.agents)) {
31951
32030
  const bankId = agentConfig.memory?.collection ?? agentName;
31952
32031
  const hasBankMission = !!agentConfig.memory?.bank_mission;
@@ -33076,6 +33155,7 @@ var init_doctor = __esm(() => {
33076
33155
  init_accounts();
33077
33156
  init_manifest();
33078
33157
  init_hindsight();
33158
+ init_doctor_memory();
33079
33159
  init_doctor_docker();
33080
33160
  init_doctor_auth_broker();
33081
33161
  init_doctor_hostd();
@@ -49601,8 +49681,8 @@ var {
49601
49681
  } = import__.default;
49602
49682
 
49603
49683
  // src/build-info.ts
49604
- var VERSION = "0.14.74";
49605
- var COMMIT_SHA = "7e9ebb67";
49684
+ var VERSION = "0.14.76";
49685
+ var COMMIT_SHA = "e7ab9ec6";
49606
49686
 
49607
49687
  // src/cli/agent.ts
49608
49688
  init_source();
@@ -51112,6 +51192,10 @@ function seedWorkspaceBootstrapFiles(params) {
51112
51192
  }
51113
51193
  if (entry === ".gitkeep")
51114
51194
  continue;
51195
+ if (params.seedMemoryFile === false && relPath.replace(/\.hbs$/, "") === "MEMORY.md") {
51196
+ params.skipped.push(join8(agentWorkspaceDir, "MEMORY.md"));
51197
+ continue;
51198
+ }
51115
51199
  if (entry.endsWith(".hbs")) {
51116
51200
  const destRel = relPath.replace(/\.hbs$/, "");
51117
51201
  const destPath = join8(agentWorkspaceDir, destRel);
@@ -51757,7 +51841,8 @@ function scaffoldAgent(name, agentConfigRaw, agentsDir, telegramConfig, switchro
51757
51841
  context,
51758
51842
  created,
51759
51843
  skipped,
51760
- rewrittenWithBackup
51844
+ rewrittenWithBackup,
51845
+ seedMemoryFile: agentConfig.memory?.file !== false
51761
51846
  });
51762
51847
  ensureClaudeMdSymlinks(phase5WorkspaceDir, created);
51763
51848
  const persistentHomeDir = join8(agentDir, "home");
@@ -52786,7 +52871,8 @@ ${body}
52786
52871
  context: workspaceContext,
52787
52872
  created: changes,
52788
52873
  skipped: [],
52789
- rewrittenWithBackup: changes
52874
+ rewrittenWithBackup: changes,
52875
+ seedMemoryFile: agentConfig.memory?.file !== false
52790
52876
  });
52791
52877
  ensureClaudeMdSymlinks(reconcileWorkspaceDir, changes);
52792
52878
  }
@@ -66066,6 +66152,7 @@ var HINDSIGHT_DEFAULT_RECALL_MAX_CONCURRENT = 8;
66066
66152
  var HINDSIGHT_DEFAULT_MEM_LIMIT = "4g";
66067
66153
  var HINDSIGHT_DEFAULT_MEM_RESERVATION = "2g";
66068
66154
  var HINDSIGHT_DEFAULT_PIDS_LIMIT = 1000;
66155
+ var HINDSIGHT_DEFAULT_SHM_SIZE = "2g";
66069
66156
  function isPortFree(port) {
66070
66157
  return new Promise((resolve27) => {
66071
66158
  const server = createServer4();
@@ -66156,6 +66243,7 @@ function startHindsight(ports) {
66156
66243
  `--memory=${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
66157
66244
  `--memory-reservation=${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
66158
66245
  `--pids-limit=${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
66246
+ `--shm-size=${HINDSIGHT_DEFAULT_SHM_SIZE}`,
66159
66247
  "-p",
66160
66248
  `127.0.0.1:${apiPort}:8888`,
66161
66249
  "-p",
@@ -66209,6 +66297,7 @@ function generateHindsightComposeSnippet() {
66209
66297
  ` mem_limit: ${HINDSIGHT_DEFAULT_MEM_LIMIT}`,
66210
66298
  ` mem_reservation: ${HINDSIGHT_DEFAULT_MEM_RESERVATION}`,
66211
66299
  ` pids_limit: ${HINDSIGHT_DEFAULT_PIDS_LIMIT}`,
66300
+ ` shm_size: ${HINDSIGHT_DEFAULT_SHM_SIZE}`,
66212
66301
  " volumes:",
66213
66302
  " - switchroom-hindsight-data:/home/hindsight/.pg0",
66214
66303
  ` - ${HINDSIGHT_BROKER_SOCK_VOLUME}:/run/switchroom/auth-broker`,
@@ -71403,10 +71492,66 @@ async function proposeConfigEditViaHostd(args) {
71403
71492
  }
71404
71493
  }
71405
71494
 
71495
+ // src/web/api.ts
71496
+ import { randomUUID as randomUUID4 } from "node:crypto";
71497
+
71498
+ // src/web/microsoft-connect.ts
71499
+ init_oauth2();
71500
+ init_resolver();
71501
+ init_client2();
71502
+ async function startMicrosoftConnect(deps = {}) {
71503
+ const resolved = resolveMicrosoftClientId(deps.configClientId);
71504
+ if (isVaultReference(resolved.clientId)) {
71505
+ return { kind: "byo-vault", ref: resolved.clientId };
71506
+ }
71507
+ const scopes = selectMicrosoftScopes(deps.orgMode ?? false);
71508
+ const cfg = { client_id: resolved.clientId, scopes };
71509
+ try {
71510
+ const device = await (deps.requestDeviceCode ?? requestDeviceCode2)(cfg);
71511
+ return { kind: "started", device, clientId: resolved.clientId, scopes, source: resolved.source };
71512
+ } catch (err) {
71513
+ return { kind: "error", message: err.message };
71514
+ }
71515
+ }
71516
+ async function runMicrosoftConnectPoll(flow, deps = {}) {
71517
+ const now = deps.now ?? Date.now;
71518
+ const cfg = { client_id: flow.clientId, scopes: flow.scopes };
71519
+ let tokens;
71520
+ try {
71521
+ tokens = await (deps.pollDeviceToken ?? pollDeviceToken2)(cfg, flow.device, { now });
71522
+ } catch (err) {
71523
+ return { state: "failed", message: err.message };
71524
+ }
71525
+ const built = buildMicrosoftCredentials({
71526
+ tokens,
71527
+ clientId: flow.clientId,
71528
+ accountEmail: "",
71529
+ fallbackScope: flow.scopes.join(" "),
71530
+ now
71531
+ });
71532
+ if (!built.credentials.microsoftOauth.refreshToken) {
71533
+ return { state: "no-refresh-token" };
71534
+ }
71535
+ const account = built.resolvedEmail;
71536
+ if (!account) {
71537
+ return { state: "failed", message: "Microsoft returned no account identity (no id_token)." };
71538
+ }
71539
+ const addAccount = deps.addAccount ?? ((label, creds) => withAuthBrokerClient((client2) => client2.addAccount(label, creds, true, "microsoft")));
71540
+ try {
71541
+ await addAccount(account, built.credentials);
71542
+ } catch (err) {
71543
+ return { state: "failed", message: err.message };
71544
+ }
71545
+ return {
71546
+ state: "connected",
71547
+ account,
71548
+ accountType: built.credentials.microsoftOauth.accountType
71549
+ };
71550
+ }
71551
+
71406
71552
  // src/web/api.ts
71407
71553
  init_account_store();
71408
71554
  init_client2();
71409
- import { randomUUID as randomUUID4 } from "node:crypto";
71410
71555
 
71411
71556
  // telegram-plugin/registry/turns-schema.ts
71412
71557
  import { chmodSync as chmodSync8, mkdirSync as mkdirSync26 } from "fs";
@@ -72017,6 +72162,71 @@ function reapConnectionAccessStatuses(now = Date.now()) {
72017
72162
  function handleGetConnectionAccessStatus(requestId) {
72018
72163
  return connectionAccessStatuses.get(requestId) ?? { state: "unknown" };
72019
72164
  }
72165
+ var microsoftConnectStatuses = new Map;
72166
+ function reapMicrosoftConnects(now = Date.now()) {
72167
+ for (const [id, s] of microsoftConnectStatuses) {
72168
+ const ttl = s.state === "pending" ? s.expiresInSec * 1000 + 60000 : 30 * 60000;
72169
+ if (now - s.startedAt > ttl)
72170
+ microsoftConnectStatuses.delete(id);
72171
+ }
72172
+ }
72173
+ async function handleStartMicrosoftConnect(config, deps = {}) {
72174
+ const now = deps.now ?? Date.now;
72175
+ const configClientId = config.microsoft_workspace?.microsoft_client_id;
72176
+ const orgMode = deps.orgMode ?? config.microsoft_workspace?.org_mode === true;
72177
+ const started = await startMicrosoftConnect({ ...deps, configClientId, orgMode });
72178
+ if (started.kind === "byo-vault") {
72179
+ return {
72180
+ ok: false,
72181
+ error: `This install uses a vaulted custom Microsoft app (${started.ref}) the dashboard can't read. ` + `Connect from the host: switchroom auth microsoft account add <email>.`
72182
+ };
72183
+ }
72184
+ if (started.kind === "error") {
72185
+ return { ok: false, error: started.message };
72186
+ }
72187
+ const requestId = randomUUID4();
72188
+ microsoftConnectStatuses.set(requestId, {
72189
+ state: "pending",
72190
+ startedAt: now(),
72191
+ userCode: started.device.user_code,
72192
+ verificationUri: started.device.verification_uri,
72193
+ expiresInSec: started.device.expires_in
72194
+ });
72195
+ reapMicrosoftConnects(now());
72196
+ runMicrosoftConnectPoll({ device: started.device, clientId: started.clientId, scopes: started.scopes }, deps).then((res) => {
72197
+ const startedAt = microsoftConnectStatuses.get(requestId)?.startedAt ?? now();
72198
+ if (res.state === "connected") {
72199
+ microsoftConnectStatuses.set(requestId, {
72200
+ state: "connected",
72201
+ startedAt,
72202
+ account: res.account,
72203
+ accountType: res.accountType
72204
+ });
72205
+ captureEvent("microsoft_connect", { outcome: "connected", source: "web_api" });
72206
+ } else {
72207
+ const reason = res.state === "no-refresh-token" ? "Microsoft returned no refresh token (account would expire in ~1h)." : res.message;
72208
+ microsoftConnectStatuses.set(requestId, { state: "failed", startedAt, reason });
72209
+ }
72210
+ }).catch((err) => {
72211
+ const startedAt = microsoftConnectStatuses.get(requestId)?.startedAt ?? now();
72212
+ microsoftConnectStatuses.set(requestId, {
72213
+ state: "failed",
72214
+ startedAt,
72215
+ reason: err instanceof Error ? err.message : String(err)
72216
+ });
72217
+ captureException(err, { action: "microsoft_connect" });
72218
+ });
72219
+ return {
72220
+ ok: true,
72221
+ requestId,
72222
+ userCode: started.device.user_code,
72223
+ verificationUri: started.device.verification_uri,
72224
+ expiresInSec: started.device.expires_in
72225
+ };
72226
+ }
72227
+ function handleGetMicrosoftConnectStatus(requestId) {
72228
+ return microsoftConnectStatuses.get(requestId) ?? { state: "unknown" };
72229
+ }
72020
72230
  function handleSetConnectionAccess(configPath, config, args, deps = {}) {
72021
72231
  const provider = args.provider;
72022
72232
  if (provider !== "google" && provider !== "microsoft") {
@@ -72800,6 +73010,16 @@ function parseRoute(pathname, method) {
72800
73010
  params: { requestId: decodeURIComponent(accessStatusMatch[1]) }
72801
73011
  };
72802
73012
  }
73013
+ if (method === "POST" && pathname === "/api/connections/microsoft/connect") {
73014
+ return { handler: "startMicrosoftConnect", params: {} };
73015
+ }
73016
+ const msConnectMatch = pathname.match(/^\/api\/connections\/microsoft\/connect\/([^/]+)$/);
73017
+ if (method === "GET" && msConnectMatch) {
73018
+ return {
73019
+ handler: "getMicrosoftConnectStatus",
73020
+ params: { requestId: decodeURIComponent(msConnectMatch[1]) }
73021
+ };
73022
+ }
72803
73023
  if (method === "POST" && pathname === "/api/auth/use") {
72804
73024
  return { handler: "useAccount", params: {} };
72805
73025
  }
@@ -72976,6 +73196,13 @@ function startWebServer(config, port, hostname = "127.0.0.1", configPath) {
72976
73196
  }
72977
73197
  case "getConnectionAccessStatus":
72978
73198
  return jsonResponse(handleGetConnectionAccessStatus(route.params.requestId));
73199
+ case "startMicrosoftConnect":
73200
+ return (async () => {
73201
+ const result = await handleStartMicrosoftConnect(freshConfig());
73202
+ return jsonResponse(result, result.ok ? 200 : 400);
73203
+ })();
73204
+ case "getMicrosoftConnectStatus":
73205
+ return jsonResponse(handleGetMicrosoftConnectStatus(route.params.requestId));
72979
73206
  case "refreshQuota": {
72980
73207
  return (async () => {
72981
73208
  let body = {};
@@ -75714,7 +75941,7 @@ import {
75714
75941
  } from "node:fs";
75715
75942
  import { dirname as dirname15, join as join60 } from "node:path";
75716
75943
  import { homedir as homedir35 } from "node:os";
75717
- import { execFileSync as execFileSync18 } from "node:child_process";
75944
+ import { execFileSync as execFileSync19 } from "node:child_process";
75718
75945
 
75719
75946
  class PythonEnvError extends Error {
75720
75947
  stderr;
@@ -75761,7 +75988,7 @@ function ensurePythonEnv(opts) {
75761
75988
  }
75762
75989
  mkdirSync33(dirname15(venvDir), { recursive: true });
75763
75990
  try {
75764
- execFileSync18(hostPython, ["-m", "venv", venvDir], { stdio: "pipe" });
75991
+ execFileSync19(hostPython, ["-m", "venv", venvDir], { stdio: "pipe" });
75765
75992
  } catch (err) {
75766
75993
  const e = err;
75767
75994
  throw new PythonEnvError(`Failed to create venv for skill "${skillName}" with ${hostPython}: ${e.message}`, e.stderr?.toString());
@@ -75773,7 +76000,7 @@ function ensurePythonEnv(opts) {
75773
76000
  delete childEnv.PIP_TARGET;
75774
76001
  delete childEnv.PIP_PREFIX;
75775
76002
  delete childEnv.PYTHONUSERBASE;
75776
- execFileSync18(pipBin, ["install", "--disable-pip-version-check", "-r", requirementsPath], { stdio: "pipe", env: childEnv });
76003
+ execFileSync19(pipBin, ["install", "--disable-pip-version-check", "-r", requirementsPath], { stdio: "pipe", env: childEnv });
75777
76004
  } catch (err) {
75778
76005
  const e = err;
75779
76006
  throw new PythonEnvError(`Failed to install requirements for skill "${skillName}": ${e.message}`, e.stderr?.toString());
@@ -75802,7 +76029,7 @@ import {
75802
76029
  } from "node:fs";
75803
76030
  import { dirname as dirname16, join as join61 } from "node:path";
75804
76031
  import { homedir as homedir36 } from "node:os";
75805
- import { execFileSync as execFileSync19 } from "node:child_process";
76032
+ import { execFileSync as execFileSync20 } from "node:child_process";
75806
76033
 
75807
76034
  class NodeEnvError extends Error {
75808
76035
  stderr;
@@ -75886,10 +76113,10 @@ function ensureNodeEnv(opts) {
75886
76113
  try {
75887
76114
  if (installer === "bun") {
75888
76115
  const args = copiedLockfile ? ["install", "--frozen-lockfile"] : ["install"];
75889
- execFileSync19("bun", args, { cwd: envDir, stdio: "pipe" });
76116
+ execFileSync20("bun", args, { cwd: envDir, stdio: "pipe" });
75890
76117
  } else {
75891
76118
  const args = copiedLockfile ? ["ci"] : ["install"];
75892
- execFileSync19("npm", args, { cwd: envDir, stdio: "pipe" });
76119
+ execFileSync20("npm", args, { cwd: envDir, stdio: "pipe" });
75893
76120
  }
75894
76121
  } catch (err) {
75895
76122
  const e = err;
@@ -77217,7 +77444,7 @@ function registerDebugCommand(program3) {
77217
77444
  init_source();
77218
77445
 
77219
77446
  // src/worktree/claim.ts
77220
- import { execFileSync as execFileSync20 } from "node:child_process";
77447
+ import { execFileSync as execFileSync21 } from "node:child_process";
77221
77448
  import { closeSync as closeSync12, mkdirSync as mkdirSync36, openSync as openSync12, existsSync as existsSync66, unlinkSync as unlinkSync13 } from "node:fs";
77222
77449
  import { join as join66, resolve as resolve42 } from "node:path";
77223
77450
  import { homedir as homedir39 } from "node:os";
@@ -77384,7 +77611,7 @@ async function claimWorktree(input, codeRepos) {
77384
77611
  releaseLock();
77385
77612
  }
77386
77613
  try {
77387
- execFileSync20("git", ["worktree", "add", "-b", branch, worktreePath], {
77614
+ execFileSync21("git", ["worktree", "add", "-b", branch, worktreePath], {
77388
77615
  cwd: repoPath,
77389
77616
  stdio: "pipe"
77390
77617
  });
@@ -77397,7 +77624,7 @@ async function claimWorktree(input, codeRepos) {
77397
77624
  }
77398
77625
 
77399
77626
  // src/worktree/release.ts
77400
- import { execFileSync as execFileSync21 } from "node:child_process";
77627
+ import { execFileSync as execFileSync22 } from "node:child_process";
77401
77628
  import { existsSync as existsSync67 } from "node:fs";
77402
77629
  function releaseWorktree(input) {
77403
77630
  const { id } = input;
@@ -77408,7 +77635,7 @@ function releaseWorktree(input) {
77408
77635
  let gitSuccess = true;
77409
77636
  if (existsSync67(record2.path)) {
77410
77637
  try {
77411
- execFileSync21("git", ["worktree", "remove", "--force", record2.path], {
77638
+ execFileSync22("git", ["worktree", "remove", "--force", record2.path], {
77412
77639
  cwd: record2.repo,
77413
77640
  stdio: "pipe"
77414
77641
  });
@@ -77444,16 +77671,16 @@ function listWorktrees() {
77444
77671
  }
77445
77672
 
77446
77673
  // src/worktree/reaper.ts
77447
- import { execFileSync as execFileSync22 } from "node:child_process";
77674
+ import { execFileSync as execFileSync23 } from "node:child_process";
77448
77675
  import { existsSync as existsSync68 } from "node:fs";
77449
77676
  var STALE_THRESHOLD_MS = 10 * 60 * 1000;
77450
77677
  function isPathInUse(path7) {
77451
77678
  try {
77452
- execFileSync22("fuser", [path7], { stdio: "pipe" });
77679
+ execFileSync23("fuser", [path7], { stdio: "pipe" });
77453
77680
  return true;
77454
77681
  } catch {}
77455
77682
  try {
77456
- const out = execFileSync22("lsof", ["-t", path7], {
77683
+ const out = execFileSync23("lsof", ["-t", path7], {
77457
77684
  stdio: ["ignore", "pipe", "ignore"]
77458
77685
  }).toString().trim();
77459
77686
  if (out.length > 0)
@@ -77463,7 +77690,7 @@ function isPathInUse(path7) {
77463
77690
  }
77464
77691
  function hasUncommittedChanges(repoPath, worktreePath) {
77465
77692
  try {
77466
- const out = execFileSync22("git", ["-C", worktreePath, "status", "--porcelain"], { stdio: "pipe" }).toString();
77693
+ const out = execFileSync23("git", ["-C", worktreePath, "status", "--porcelain"], { stdio: "pipe" }).toString();
77467
77694
  return out.trim().length > 0;
77468
77695
  } catch {
77469
77696
  return false;
@@ -77477,7 +77704,7 @@ function reapRecord(record2) {
77477
77704
  warning = `[worktree-reaper] Reaped worktree with uncommitted changes: ` + `id=${id} branch=${branch} agent=${ownerAgent ?? "unknown"} path=${path7}`;
77478
77705
  }
77479
77706
  try {
77480
- execFileSync22("git", ["worktree", "remove", "--force", path7], {
77707
+ execFileSync23("git", ["worktree", "remove", "--force", path7], {
77481
77708
  cwd: repo,
77482
77709
  stdio: "pipe"
77483
77710
  });
@@ -78960,7 +79187,7 @@ agents:
78960
79187
  init_resolver();
78961
79188
  import { dirname as dirname21, join as join72, resolve as resolve44 } from "node:path";
78962
79189
  import { homedir as homedir41 } from "node:os";
78963
- import { execFileSync as execFileSync23 } from "node:child_process";
79190
+ import { execFileSync as execFileSync24 } from "node:child_process";
78964
79191
  init_vault();
78965
79192
  init_loader();
78966
79193
  init_loader();
@@ -79368,7 +79595,7 @@ async function ensureHostMountSources(config) {
79368
79595
  }
79369
79596
  function detectComposeV2() {
79370
79597
  try {
79371
- const out = execFileSync23("docker", ["compose", "version"], {
79598
+ const out = execFileSync24("docker", ["compose", "version"], {
79372
79599
  stdio: ["ignore", "pipe", "pipe"],
79373
79600
  encoding: "utf8"
79374
79601
  });
@@ -559,6 +559,51 @@
559
559
  }
560
560
  }
561
561
 
562
+ // Start an in-browser Microsoft connect: show the device code + link,
563
+ // then poll until the operator completes sign-in on Microsoft's site.
564
+ async function connectMicrosoft() {
565
+ const card = document.getElementById('ms-connect-card');
566
+ const show = (html) => { if (card) card.innerHTML = html; };
567
+ show('<div class="loading" style="padding:.8rem">Starting…</div>');
568
+ try {
569
+ const res = await fetch(`${API}/api/connections/microsoft/connect`, { method: 'POST', headers: authHeaders() });
570
+ const data = await res.json();
571
+ if (!res.ok || !data.ok) { show(''); showError(data.error || `HTTP ${res.status}`); return; }
572
+ const url = data.verificationUri, code = data.userCode;
573
+ show(`<div class="account-card" style="border-color:var(--accent)">
574
+ <div class="account-card-header"><div class="account-label">Connect a Microsoft account</div></div>
575
+ <div style="padding:.3rem 0;line-height:1.7">
576
+ 1. Open <a href="${escapeHtml(url)}" target="_blank" rel="noopener" style="color:var(--accent)">${escapeHtml(url)}</a><br>
577
+ 2. Enter code: <code style="font-size:1.15rem;letter-spacing:.08em">${escapeHtml(code)}</code><br>
578
+ 3. Approve the requested permissions (Mail, Calendar, Files).
579
+ </div>
580
+ <div id="ms-connect-status" style="color:var(--text-dim);margin-top:.3rem">Waiting for sign-in… (this card expires in ~15 min)</div>
581
+ </div>`);
582
+ const statusEl = () => document.getElementById('ms-connect-status');
583
+ const started = Date.now();
584
+ const poll = async () => {
585
+ const sres = await fetch(`${API}/api/connections/microsoft/connect/${encodeURIComponent(data.requestId)}`, { headers: authHeaders() });
586
+ const s = sres.ok ? await sres.json() : { state: 'failed', reason: `HTTP ${sres.status}` };
587
+ if (s.state === 'pending') {
588
+ if (Date.now() - started > ((data.expiresInSec || 900) * 1000 + 30000)) { const e = statusEl(); if (e) e.textContent = 'Expired — click Connect to try again.'; return; }
589
+ setTimeout(poll, 3000);
590
+ return;
591
+ }
592
+ if (s.state === 'connected') {
593
+ show(`<div class="loading" style="padding:.8rem;color:var(--green)">✓ Connected ${escapeHtml(s.account)} (${escapeHtml(s.accountType)}). Use the access toggles below to grant an agent.</div>`);
594
+ fetchConnections();
595
+ } else {
596
+ show('');
597
+ showError(s.reason || 'connect failed');
598
+ }
599
+ };
600
+ setTimeout(poll, 3000);
601
+ } catch (err) {
602
+ show('');
603
+ showError(err.message);
604
+ }
605
+ }
606
+
562
607
  async function fetchSchedule() {
563
608
  try {
564
609
  const res = await fetch(`${API}/api/schedule`, { headers: authHeaders() });
@@ -1115,11 +1160,18 @@
1115
1160
  google.map(a => renderOAuthAccountCard(a, { showType: false, provider: 'google', agentNames })).join(''),
1116
1161
  );
1117
1162
 
1118
- const microsoftSection = _connectionSection(
1119
- 'Microsoft 365',
1120
- 'No Microsoft accounts. Connect one from Telegram with <code>/connect microsoft</code> (admin DM), or <code>switchroom auth microsoft account add</code>.',
1121
- microsoft.map(a => renderOAuthAccountCard(a, { showType: true, provider: 'microsoft', agentNames })).join(''),
1122
- );
1163
+ const msCards = microsoft.map(a => renderOAuthAccountCard(a, { showType: true, provider: 'microsoft', agentNames })).join('');
1164
+ const microsoftSection = `
1165
+ <div style="margin-bottom:1.5rem">
1166
+ <h3 style="margin:0 0 .6rem;font-size:.95rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em">
1167
+ Microsoft 365
1168
+ <button onclick="connectMicrosoft()" class="usage-pill primary" style="margin-left:.6rem;cursor:pointer;border:none;text-transform:none;font-weight:600">+ Connect a Microsoft account</button>
1169
+ </h3>
1170
+ <div id="ms-connect-card"></div>
1171
+ ${msCards
1172
+ ? `<div class="accounts-grid">${msCards}</div>`
1173
+ : `<div class="loading" style="padding:.8rem">No Microsoft accounts yet — click <b>Connect a Microsoft account</b> above (or <code>/connect microsoft</code> from Telegram).</div>`}
1174
+ </div>`;
1123
1175
 
1124
1176
  let notionCards = '';
1125
1177
  if (notion.configured) {
@@ -13726,6 +13726,7 @@ var AgentToolsSchema = exports_external.object({
13726
13726
  var AgentMemorySchema = exports_external.object({
13727
13727
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
13728
13728
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
13729
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
13729
13730
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
13730
13731
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
13731
13732
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -13945,6 +13946,7 @@ var profileFields = {
13945
13946
  memory: exports_external.object({
13946
13947
  collection: exports_external.string().optional(),
13947
13948
  auto_recall: exports_external.boolean().optional(),
13949
+ file: exports_external.boolean().optional(),
13948
13950
  isolation: exports_external.enum(["default", "strict"]).optional(),
13949
13951
  recall: exports_external.object({
13950
13952
  max_memories: exports_external.number().int().min(0).optional(),
@@ -11312,6 +11312,7 @@ var init_schema = __esm(() => {
11312
11312
  AgentMemorySchema = exports_external.object({
11313
11313
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
11314
11314
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
11315
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
11315
11316
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
11316
11317
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
11317
11318
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -11531,6 +11532,7 @@ var init_schema = __esm(() => {
11531
11532
  memory: exports_external.object({
11532
11533
  collection: exports_external.string().optional(),
11533
11534
  auto_recall: exports_external.boolean().optional(),
11535
+ file: exports_external.boolean().optional(),
11534
11536
  isolation: exports_external.enum(["default", "strict"]).optional(),
11535
11537
  recall: exports_external.object({
11536
11538
  max_memories: exports_external.number().int().min(0).optional(),
@@ -11312,6 +11312,7 @@ var init_schema = __esm(() => {
11312
11312
  AgentMemorySchema = exports_external.object({
11313
11313
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
11314
11314
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
11315
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
11315
11316
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
11316
11317
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
11317
11318
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -11531,6 +11532,7 @@ var init_schema = __esm(() => {
11531
11532
  memory: exports_external.object({
11532
11533
  collection: exports_external.string().optional(),
11533
11534
  auto_recall: exports_external.boolean().optional(),
11535
+ file: exports_external.boolean().optional(),
11534
11536
  isolation: exports_external.enum(["default", "strict"]).optional(),
11535
11537
  recall: exports_external.object({
11536
11538
  max_memories: exports_external.number().int().min(0).optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.14.74",
3
+ "version": "0.14.76",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23815,6 +23815,7 @@ var init_schema = __esm(() => {
23815
23815
  AgentMemorySchema = exports_external.object({
23816
23816
  collection: exports_external.string().describe("Hindsight collection name for this agent"),
23817
23817
  auto_recall: exports_external.boolean().default(true).describe("Auto-search memories before each response"),
23818
+ file: exports_external.boolean().default(true).describe("Maintain a curated workspace MEMORY.md file (seeded once, " + "auto-loaded every turn). Set false for hindsight-only memory: " + "the file is not seeded or re-created, so once migrated into " + "Hindsight and deleted it stays gone. Recall + directives carry " + "the memory instead. Cascade: override (per-agent wins over default)."),
23818
23819
  isolation: exports_external.enum(["default", "strict"]).default("default").describe("strict = never shared cross-agent, default = eligible for reflect"),
23819
23820
  bank_mission: exports_external.string().optional().describe("Bank-level mission statement used during recall to contextualize results"),
23820
23821
  retain_mission: exports_external.string().optional().describe("Instructions for the fact extraction LLM during retain"),
@@ -24034,6 +24035,7 @@ var init_schema = __esm(() => {
24034
24035
  memory: exports_external.object({
24035
24036
  collection: exports_external.string().optional(),
24036
24037
  auto_recall: exports_external.boolean().optional(),
24038
+ file: exports_external.boolean().optional(),
24037
24039
  isolation: exports_external.enum(["default", "strict"]).optional(),
24038
24040
  recall: exports_external.object({
24039
24041
  max_memories: exports_external.number().int().min(0).optional(),
@@ -52819,10 +52821,10 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
52819
52821
  }
52820
52822
 
52821
52823
  // ../src/build-info.ts
52822
- var VERSION = "0.14.74";
52823
- var COMMIT_SHA = "7e9ebb67";
52824
- var COMMIT_DATE = "2026-06-06T02:56:40Z";
52825
- var LATEST_PR = 2189;
52824
+ var VERSION = "0.14.76";
52825
+ var COMMIT_SHA = "e7ab9ec6";
52826
+ var COMMIT_DATE = "2026-06-06T08:50:42Z";
52827
+ var LATEST_PR = 2196;
52826
52828
  var COMMITS_AHEAD_OF_TAG = 0;
52827
52829
 
52828
52830
  // gateway/boot-version.ts