@hermespilot/link 0.7.7-beta.0 → 0.7.8-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,8 @@ import Router from "@koa/router";
4
4
 
5
5
  // src/conversations/conversation-service.ts
6
6
  import { EventEmitter } from "events";
7
- import { createHash as createHash6, randomUUID as randomUUID10 } from "crypto";
7
+ import { createHash as createHash6, randomUUID as randomUUID11 } from "crypto";
8
+ import path24 from "path";
8
9
 
9
10
  // src/database/link-database.ts
10
11
  import { mkdir } from "fs/promises";
@@ -35,6 +36,7 @@ var MIGRATIONS = [
35
36
  title TEXT NOT NULL,
36
37
  status TEXT NOT NULL,
37
38
  hermes_session_id TEXT NOT NULL,
39
+ workspace_id TEXT,
38
40
  profile TEXT,
39
41
  model TEXT,
40
42
  provider TEXT,
@@ -58,6 +60,8 @@ var MIGRATIONS = [
58
60
  ON conversation_stats(model);
59
61
  CREATE INDEX IF NOT EXISTS idx_conversation_stats_profile
60
62
  ON conversation_stats(profile);
63
+ CREATE INDEX IF NOT EXISTS idx_conversation_stats_workspace_status_updated
64
+ ON conversation_stats(workspace_id, status, updated_at);
61
65
  `
62
66
  },
63
67
  {
@@ -234,6 +238,7 @@ async function listConversationStatsPage(paths, input) {
234
238
  try {
235
239
  const conditions = ["status = ?"];
236
240
  const params = [status];
241
+ appendWorkspaceFilter(conditions, params, input.workspace);
237
242
  if (input.cursor) {
238
243
  conditions.push(`(
239
244
  updated_at < ?
@@ -276,7 +281,8 @@ async function searchConversationStatsPage(paths, input) {
276
281
  return listConversationStatsPage(paths, {
277
282
  limit,
278
283
  cursor: input.cursor,
279
- status
284
+ status,
285
+ workspace: input.workspace
280
286
  });
281
287
  }
282
288
  const db = openDatabase(paths);
@@ -286,6 +292,7 @@ async function searchConversationStatsPage(paths, input) {
286
292
  status,
287
293
  `%${escapeSqlLike(query.toLowerCase())}%`
288
294
  ];
295
+ appendWorkspaceFilter(conditions, params, input.workspace);
289
296
  if (input.cursor) {
290
297
  conditions.push(`(
291
298
  updated_at < ?
@@ -818,7 +825,12 @@ function ensureProfileIdentitySchema(db) {
818
825
  "ALTER TABLE conversation_stats ADD COLUMN profile_name_snapshot TEXT;"
819
826
  );
820
827
  }
828
+ if (!conversationColumns.has("workspace_id")) {
829
+ db.exec("ALTER TABLE conversation_stats ADD COLUMN workspace_id TEXT;");
830
+ }
821
831
  db.exec(`
832
+ CREATE INDEX IF NOT EXISTS idx_conversation_stats_workspace_status_updated
833
+ ON conversation_stats(workspace_id, status, updated_at);
822
834
  CREATE INDEX IF NOT EXISTS idx_conversation_stats_profile_uid
823
835
  ON conversation_stats(profile_uid);
824
836
  CREATE INDEX IF NOT EXISTS idx_conversation_stats_profile_name_snapshot
@@ -835,6 +847,7 @@ function conversationStatsUpsertSql() {
835
847
  title,
836
848
  status,
837
849
  hermes_session_id,
850
+ workspace_id,
838
851
  profile_uid,
839
852
  profile_name_snapshot,
840
853
  profile,
@@ -850,12 +863,13 @@ function conversationStatsUpsertSql() {
850
863
  updated_at,
851
864
  deleted_at,
852
865
  stats_updated_at
853
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
866
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
854
867
  ON CONFLICT(conversation_id) DO UPDATE SET
855
868
  kind = excluded.kind,
856
869
  title = excluded.title,
857
870
  status = excluded.status,
858
871
  hermes_session_id = excluded.hermes_session_id,
872
+ workspace_id = excluded.workspace_id,
859
873
  profile_uid = excluded.profile_uid,
860
874
  profile_name_snapshot = excluded.profile_name_snapshot,
861
875
  profile = excluded.profile,
@@ -880,6 +894,7 @@ function conversationStatsParams(record) {
880
894
  record.title,
881
895
  record.status,
882
896
  record.hermesSessionId,
897
+ record.workspaceId ?? null,
883
898
  record.profileUid ?? null,
884
899
  record.profileNameSnapshot ?? record.profile ?? null,
885
900
  record.profile ?? record.profileNameSnapshot ?? null,
@@ -897,6 +912,17 @@ function conversationStatsParams(record) {
897
912
  record.statsUpdatedAt
898
913
  ];
899
914
  }
915
+ function appendWorkspaceFilter(conditions, params, filter) {
916
+ if (!filter || filter.kind === "all") {
917
+ return;
918
+ }
919
+ if (filter.kind === "default") {
920
+ conditions.push("(workspace_id IS NULL OR workspace_id = '')");
921
+ return;
922
+ }
923
+ conditions.push("workspace_id = ?");
924
+ params.push(filter.workspaceId);
925
+ }
900
926
  function runUsageFactUpsertSql() {
901
927
  return `
902
928
  INSERT INTO run_usage_facts (
@@ -1642,6 +1668,11 @@ var messages = {
1642
1668
  "error.relayChallengeInvalid": "Relay did not return a valid install challenge.",
1643
1669
  "error.relayLinkInvalid": "Relay did not return a valid link_id.",
1644
1670
  "error.relayEmpty": "Relay returned an empty response.",
1671
+ "error.workspaceNameConflict": "A workspace with this name already exists.",
1672
+ "error.workspaceNameRequired": "Workspace name is required.",
1673
+ "error.workspaceNameTooLong": "Workspace name is too long.",
1674
+ "error.workspaceNotFound": "Workspace was not found.",
1675
+ "error.workspaceIdInvalid": "Workspace id is invalid.",
1645
1676
  "error.serverHttp": "HermesPilot Server request failed with HTTP {status}.",
1646
1677
  "error.pairingServerUnreachable": "Could not reach HermesPilot Server while creating the pairing session. Check whether {url} is reachable, then try again. If you use a proxy network, add hermes-server.clawpilot.me and hermes-relay.clawpilot.me to the proxy exclusion list, or temporarily turn off VPN/proxy and retry.",
1647
1678
  "error.pairingRelayUnreachable": "Could not reach Hermes Relay while creating the pairing session. Check whether {url} is reachable, then try again. If you use a proxy network, add hermes-server.clawpilot.me and hermes-relay.clawpilot.me to the proxy exclusion list, or temporarily turn off VPN/proxy and retry.",
@@ -1883,6 +1914,11 @@ var messages = {
1883
1914
  "error.relayChallengeInvalid": "Relay \u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684\u5B89\u88C5\u6311\u6218\u3002",
1884
1915
  "error.relayLinkInvalid": "Relay \u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684 link_id\u3002",
1885
1916
  "error.relayEmpty": "Relay \u8FD4\u56DE\u4E86\u7A7A\u54CD\u5E94\u3002",
1917
+ "error.workspaceNameConflict": "\u5DF2\u5B58\u5728\u540C\u540D\u5DE5\u4F5C\u533A\u3002",
1918
+ "error.workspaceNameRequired": "\u8BF7\u8F93\u5165\u5DE5\u4F5C\u533A\u540D\u79F0\u3002",
1919
+ "error.workspaceNameTooLong": "\u5DE5\u4F5C\u533A\u540D\u79F0\u592A\u957F\u3002",
1920
+ "error.workspaceNotFound": "\u5DE5\u4F5C\u533A\u4E0D\u5B58\u5728\u3002",
1921
+ "error.workspaceIdInvalid": "\u5DE5\u4F5C\u533A\u6807\u8BC6\u65E0\u6548\u3002",
1886
1922
  "error.serverHttp": "HermesPilot Server \u8BF7\u6C42\u5931\u8D25\uFF0CHTTP \u72B6\u6001\u7801\uFF1A{status}\u3002",
1887
1923
  "error.pairingServerUnreachable": "\u521B\u5EFA\u914D\u5BF9\u4F1A\u8BDD\u65F6\u65E0\u6CD5\u8FDE\u63A5 HermesPilot Server\u3002\u8BF7\u5148\u786E\u8BA4 {url} \u53EF\u4EE5\u8BBF\u95EE\uFF0C\u7136\u540E\u91CD\u8BD5\u3002\u91CD\u70B9\u63D0\u9192\uFF1A\u5982\u679C\u4F60\u4F7F\u7528\u4E86\u4EE3\u7406\u7F51\u7EDC\uFF0C\u53EF\u4EE5\u628A hermes-server.clawpilot.me \u548C hermes-relay.clawpilot.me \u52A0\u5165\u4EE3\u7406\u6392\u9664\u540D\u5355\uFF0C\u6216\u4E34\u65F6\u5173\u95ED VPN/\u4EE3\u7406\u540E\u518D\u8BD5\u3002",
1888
1924
  "error.pairingRelayUnreachable": "\u521B\u5EFA\u914D\u5BF9\u4F1A\u8BDD\u65F6\u65E0\u6CD5\u8FDE\u63A5 Hermes Relay\u3002\u8BF7\u5148\u786E\u8BA4 {url} \u53EF\u4EE5\u8BBF\u95EE\uFF0C\u7136\u540E\u91CD\u8BD5\u3002\u91CD\u70B9\u63D0\u9192\uFF1A\u5982\u679C\u4F60\u4F7F\u7528\u4E86\u4EE3\u7406\u7F51\u7EDC\uFF0C\u53EF\u4EE5\u628A hermes-server.clawpilot.me \u548C hermes-relay.clawpilot.me \u52A0\u5165\u4EE3\u7406\u6392\u9664\u540D\u5355\uFF0C\u6216\u4E34\u65F6\u5173\u95ED VPN/\u4EE3\u7406\u540E\u518D\u8BD5\u3002",
@@ -1944,6 +1980,21 @@ function translateKnownError(message, language) {
1944
1980
  if (message === "Relay returned an empty response") {
1945
1981
  return translate(language, "error.relayEmpty");
1946
1982
  }
1983
+ if (message === "workspace name already exists") {
1984
+ return translate(language, "error.workspaceNameConflict");
1985
+ }
1986
+ if (message === "workspace name is required") {
1987
+ return translate(language, "error.workspaceNameRequired");
1988
+ }
1989
+ if (message === "workspace name is too long") {
1990
+ return translate(language, "error.workspaceNameTooLong");
1991
+ }
1992
+ if (message === "workspace was not found") {
1993
+ return translate(language, "error.workspaceNotFound");
1994
+ }
1995
+ if (message === "workspace_id is invalid") {
1996
+ return translate(language, "error.workspaceIdInvalid");
1997
+ }
1947
1998
  const portInUse = /^listen EADDRINUSE: address already in use .*:(?<port>\d+)$/u.exec(message);
1948
1999
  if (portInUse?.groups?.port) {
1949
2000
  return translate(language, "error.portInUse", { port: portInUse.groups.port });
@@ -6496,7 +6547,7 @@ async function listCronOutputFiles(profileName, jobId) {
6496
6547
  orderTimeMs: fileStat.mtimeMs
6497
6548
  });
6498
6549
  }
6499
- return files.sort((left, right) => left.orderTimeMs - right.orderTimeMs).map(({ path: path33, mtime }) => ({ path: path33, mtime }));
6550
+ return files.sort((left, right) => left.orderTimeMs - right.orderTimeMs).map(({ path: path34, mtime }) => ({ path: path34, mtime }));
6500
6551
  }
6501
6552
  function readCronOutputTimestamp(fileName) {
6502
6553
  const match = fileName.match(
@@ -6585,7 +6636,7 @@ function isConversationMissingError(error) {
6585
6636
  }
6586
6637
 
6587
6638
  // src/constants.ts
6588
- var LINK_VERSION = "0.7.7-beta.0";
6639
+ var LINK_VERSION = "0.7.8-beta.0";
6589
6640
  var LINK_COMMAND = "hermeslink";
6590
6641
  var LINK_DEFAULT_PORT = 52379;
6591
6642
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -6605,6 +6656,7 @@ function resolveRuntimePaths(homeDir = resolveRuntimeHome()) {
6605
6656
  credentialsFile: path5.join(homeDir, "credentials.json"),
6606
6657
  databaseFile: path5.join(homeDir, "link.db"),
6607
6658
  conversationsDir: path5.join(homeDir, "conversations"),
6659
+ workspacesFile: path5.join(homeDir, "workspaces.json"),
6608
6660
  blobsDir: path5.join(homeDir, "blobs"),
6609
6661
  indexesDir: path5.join(homeDir, "indexes"),
6610
6662
  logsDir: path5.join(homeDir, "logs"),
@@ -7266,7 +7318,14 @@ async function deleteHermesSession(sessionId, profileName = "default") {
7266
7318
  status: readSessionDeleteStatus(output.stdout, output.stderr)
7267
7319
  };
7268
7320
  } catch (error) {
7269
- if (isSessionNotFoundOutput(readExecErrorOutput(error))) {
7321
+ const output = readExecErrorOutput(error);
7322
+ if (isProfileNotFoundOutput(output)) {
7323
+ return {
7324
+ session_id: normalizedSessionId,
7325
+ status: "profile_not_found"
7326
+ };
7327
+ }
7328
+ if (isSessionNotFoundOutput(output)) {
7270
7329
  return {
7271
7330
  session_id: normalizedSessionId,
7272
7331
  status: "not_found"
@@ -7328,6 +7387,9 @@ ${stderr.toString()}`;
7328
7387
  if (isSessionNotFoundOutput(output)) {
7329
7388
  return "not_found";
7330
7389
  }
7390
+ if (isProfileNotFoundOutput(output)) {
7391
+ return "profile_not_found";
7392
+ }
7331
7393
  if (/deleted session\b/i.test(output)) {
7332
7394
  return "deleted";
7333
7395
  }
@@ -7336,6 +7398,9 @@ ${stderr.toString()}`;
7336
7398
  function isSessionNotFoundOutput(output) {
7337
7399
  return /\bsession\b[\s\S]*\bnot found\b/i.test(output);
7338
7400
  }
7401
+ function isProfileNotFoundOutput(output) {
7402
+ return /\bprofile\b[\s\S]*\bdoes not exist\b/i.test(output);
7403
+ }
7339
7404
  function readExecErrorOutput(error) {
7340
7405
  if (typeof error !== "object" || error === null) {
7341
7406
  return "";
@@ -8571,6 +8636,7 @@ import { setTimeout as delay3 } from "timers/promises";
8571
8636
  import WebSocket from "ws";
8572
8637
  var CONNECT_TIMEOUT_MS = 15e3;
8573
8638
  var REQUEST_TIMEOUT_MS = 12e4;
8639
+ var USAGE_REQUEST_TIMEOUT_MS = 1e4;
8574
8640
  var PROMPT_SUBMIT_TIMEOUT_MS = 0;
8575
8641
  var READY_TIMEOUT_MS = 1e4;
8576
8642
  var BACKEND_START_TIMEOUT_MS = 45e3;
@@ -8598,12 +8664,23 @@ async function streamTuiGatewayRun(input) {
8598
8664
  profileName
8599
8665
  });
8600
8666
  await client.waitForSessionDrain(started.runtimeSessionId, input.signal);
8667
+ const usageBaseline = await client.readSessionUsage(started.runtimeSessionId).catch((error) => {
8668
+ void input.logger?.debug("tui_gateway_usage_baseline_read_failed", {
8669
+ profile: profileName,
8670
+ session_id: started.runtimeSessionId,
8671
+ error: error instanceof Error ? error.message : String(error)
8672
+ });
8673
+ return started.resumed ? null : {};
8674
+ });
8601
8675
  const events = client.submitPrompt({
8602
8676
  sessionId: started.runtimeSessionId,
8603
8677
  text: buildTuiGatewayPromptText(input),
8604
8678
  signal: input.signal
8605
8679
  });
8606
- return { started, events };
8680
+ return {
8681
+ started,
8682
+ events: normalizeTuiGatewayRunUsageDeltas(events, usageBaseline)
8683
+ };
8607
8684
  }
8608
8685
  function buildTuiGatewayPromptText(input) {
8609
8686
  return input.input;
@@ -8641,6 +8718,61 @@ async function respondTuiGatewayApproval(input) {
8641
8718
  );
8642
8719
  return { resolved: readNumber2(result, "resolved") ?? 0 };
8643
8720
  }
8721
+ async function setTuiGatewaySessionArchived(input) {
8722
+ const sessionId = input.sessionId.trim();
8723
+ if (!sessionId) {
8724
+ throw new LinkHttpError(
8725
+ 400,
8726
+ "hermes_session_id_required",
8727
+ "Hermes session id is required"
8728
+ );
8729
+ }
8730
+ if (sessionId.startsWith("hp_")) {
8731
+ return;
8732
+ }
8733
+ const profileName = normalizeProfileName2(input.profileName);
8734
+ const backend = await ensureTuiGatewayBackend({
8735
+ profileName,
8736
+ paths: input.paths,
8737
+ logger: input.logger
8738
+ });
8739
+ const response = await fetch(
8740
+ `${backend.baseUrl}/api/sessions/${encodeURIComponent(sessionId)}`,
8741
+ {
8742
+ method: "PATCH",
8743
+ headers: {
8744
+ "content-type": "application/json",
8745
+ "X-Hermes-Session-Token": backend.token
8746
+ },
8747
+ body: JSON.stringify({ archived: input.archived })
8748
+ }
8749
+ ).catch((error) => {
8750
+ throw new LinkHttpError(
8751
+ 502,
8752
+ "hermes_session_archive_failed",
8753
+ error instanceof Error ? error.message : "Hermes session archive request failed"
8754
+ );
8755
+ });
8756
+ touchBackend(profileName);
8757
+ if (response.ok) {
8758
+ return;
8759
+ }
8760
+ const detail = await response.text().catch(() => "");
8761
+ if (response.status === 404) {
8762
+ void input.logger?.debug("hermes_session_archive_missing", {
8763
+ profile: profileName,
8764
+ session_id: sessionId,
8765
+ archived: input.archived,
8766
+ detail
8767
+ });
8768
+ return;
8769
+ }
8770
+ throw new LinkHttpError(
8771
+ 502,
8772
+ "hermes_session_archive_failed",
8773
+ `Hermes session archive request failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}`
8774
+ );
8775
+ }
8644
8776
  async function readTuiGatewayStatus(input = {}) {
8645
8777
  const profile = normalizeProfileName2(input.profileName);
8646
8778
  const client = clients.get(profile);
@@ -9060,6 +9192,13 @@ var TuiGatewayClient = class {
9060
9192
  queue.closed.finally(cleanup).catch(() => void 0);
9061
9193
  return queue;
9062
9194
  }
9195
+ async readSessionUsage(sessionId) {
9196
+ return await this.request(
9197
+ "session.usage",
9198
+ { session_id: sessionId },
9199
+ USAGE_REQUEST_TIMEOUT_MS
9200
+ );
9201
+ }
9063
9202
  async interruptSession(sessionId) {
9064
9203
  const queue = new GatewayEventQueue(sessionId);
9065
9204
  this.eventQueues.add(queue);
@@ -9357,6 +9496,94 @@ var GatewayEventQueue = class {
9357
9496
  });
9358
9497
  }
9359
9498
  };
9499
+ async function* normalizeTuiGatewayRunUsageDeltas(events, baseline) {
9500
+ for await (const event of events) {
9501
+ yield normalizeTuiGatewayRunUsageDelta(event, baseline);
9502
+ }
9503
+ }
9504
+ function normalizeTuiGatewayRunUsageDelta(event, baseline) {
9505
+ if (baseline === null || event.payloadType !== "run.completed" && event.payloadType !== "run.failed" && event.payloadType !== "run.cancelled") {
9506
+ return event;
9507
+ }
9508
+ const usage = normalizeTuiGatewayUsageDelta(event.payload.usage, baseline);
9509
+ if (!usage) {
9510
+ return event;
9511
+ }
9512
+ return {
9513
+ ...event,
9514
+ payload: {
9515
+ ...event.payload,
9516
+ usage
9517
+ }
9518
+ };
9519
+ }
9520
+ function normalizeTuiGatewayUsageDelta(usageValue, baseline) {
9521
+ const usage = toRecord3(usageValue);
9522
+ if (Object.keys(usage).length === 0) {
9523
+ return null;
9524
+ }
9525
+ const inputTokens = usageDelta(usage, baseline, [
9526
+ "input",
9527
+ "input_tokens",
9528
+ "prompt",
9529
+ "prompt_tokens"
9530
+ ]);
9531
+ const outputTokens = usageDelta(usage, baseline, [
9532
+ "output",
9533
+ "output_tokens",
9534
+ "completion",
9535
+ "completion_tokens"
9536
+ ]);
9537
+ const totalTokens = totalUsageDelta(
9538
+ usage,
9539
+ baseline,
9540
+ inputTokens,
9541
+ outputTokens
9542
+ );
9543
+ const contextTokens = readNumber2(usage, "context_used") ?? readNumber2(usage, "context_tokens") ?? readNumber2(usage, "current_context_tokens") ?? readNumber2(usage, "last_prompt_tokens");
9544
+ const contextWindow = readNumber2(usage, "context_max") ?? readNumber2(usage, "context_window") ?? readNumber2(usage, "context_length");
9545
+ const usagePercent = readNumber2(usage, "context_percent") ?? readNumber2(usage, "usage_percent");
9546
+ if (inputTokens === void 0 && outputTokens === void 0 && totalTokens === void 0 && contextTokens === void 0) {
9547
+ return null;
9548
+ }
9549
+ return {
9550
+ ...usage,
9551
+ ...inputTokens !== void 0 ? { input_tokens: inputTokens } : {},
9552
+ ...outputTokens !== void 0 ? { output_tokens: outputTokens } : {},
9553
+ ...totalTokens !== void 0 ? { total_tokens: totalTokens } : {},
9554
+ ...contextTokens !== void 0 ? { context_tokens: contextTokens } : {},
9555
+ ...contextWindow !== void 0 ? { context_window: contextWindow } : {},
9556
+ ...usagePercent !== void 0 ? { usage_percent: usagePercent } : {}
9557
+ };
9558
+ }
9559
+ function usageDelta(current, baseline, keys) {
9560
+ const currentValue = readFirstNumber(current, keys);
9561
+ if (currentValue === void 0) {
9562
+ return void 0;
9563
+ }
9564
+ const baselineValue = readFirstNumber(baseline, keys) ?? 0;
9565
+ return Math.max(0, currentValue - baselineValue);
9566
+ }
9567
+ function totalUsageDelta(current, baseline, inputTokens, outputTokens) {
9568
+ const currentTotal = readFirstNumber(current, ["total", "total_tokens"]);
9569
+ const baselineTotal = readFirstNumber(baseline, ["total", "total_tokens"]);
9570
+ if (currentTotal !== void 0 && (baselineTotal !== void 0 || Object.keys(baseline).length === 0)) {
9571
+ return Math.max(0, currentTotal - (baselineTotal ?? 0));
9572
+ }
9573
+ if (inputTokens !== void 0 || outputTokens !== void 0) {
9574
+ return (inputTokens ?? 0) + (outputTokens ?? 0);
9575
+ }
9576
+ return void 0;
9577
+ }
9578
+ function readFirstNumber(payload, keys) {
9579
+ for (const key of keys) {
9580
+ const value = readNumber2(payload, key);
9581
+ if (value !== void 0) {
9582
+ return value;
9583
+ }
9584
+ }
9585
+ return void 0;
9586
+ }
9360
9587
  function normalizeGatewayEvent(event) {
9361
9588
  const payload = toRecord3(event.payload);
9362
9589
  const base = {
@@ -9642,7 +9869,14 @@ function readString5(payload, key) {
9642
9869
  }
9643
9870
  function readNumber2(payload, key) {
9644
9871
  const value = payload[key];
9645
- return typeof value === "number" && Number.isFinite(value) ? value : null;
9872
+ if (typeof value === "number" && Number.isFinite(value)) {
9873
+ return Math.max(0, Math.floor(value));
9874
+ }
9875
+ if (typeof value === "string" && value.trim()) {
9876
+ const parsed = Number.parseInt(value.replaceAll(",", ""), 10);
9877
+ return Number.isFinite(parsed) ? Math.max(0, parsed) : void 0;
9878
+ }
9879
+ return void 0;
9646
9880
  }
9647
9881
  function readText(payload, key) {
9648
9882
  const value = payload[key];
@@ -9850,6 +10084,7 @@ function toStatsIndexRecord(manifest, stats = manifest.stats ?? buildConversatio
9850
10084
  title: manifest.title,
9851
10085
  status: manifest.status,
9852
10086
  hermesSessionId: manifest.hermes_session_id,
10087
+ workspaceId: manifest.workspace_id ?? null,
9853
10088
  profileUid: stats.profile_uid ?? null,
9854
10089
  profileNameSnapshot: stats.profile_name_snapshot ?? stats.profile ?? null,
9855
10090
  profile: stats.profile ?? stats.profile_name_snapshot ?? null,
@@ -9875,6 +10110,25 @@ function latestUsageRun(snapshot) {
9875
10110
  }
9876
10111
  return void 0;
9877
10112
  }
10113
+ function latestContextUsageRun(snapshot) {
10114
+ let latestEstimatedRun;
10115
+ let latestUsageRun2;
10116
+ for (const run of [...snapshot.runs].reverse()) {
10117
+ if (!isAgentRun(run) || !run.usage) {
10118
+ continue;
10119
+ }
10120
+ latestUsageRun2 ??= run;
10121
+ if (run.usage.context_tokens === void 0) {
10122
+ continue;
10123
+ }
10124
+ if (run.usage.context_source === "estimated") {
10125
+ latestEstimatedRun ??= run;
10126
+ continue;
10127
+ }
10128
+ return run;
10129
+ }
10130
+ return latestEstimatedRun ?? latestUsageRun2;
10131
+ }
9878
10132
  function latestRuntimeRun(snapshot) {
9879
10133
  for (const run of [...snapshot.runs].reverse()) {
9880
10134
  if (isAgentRun(run) && (run.model || run.profile_uid || run.profile_name_snapshot || run.profile || run.usage)) {
@@ -9999,8 +10253,204 @@ function firstRecord(...values) {
9999
10253
  return {};
10000
10254
  }
10001
10255
 
10002
- // src/conversations/blob-store.ts
10256
+ // src/conversations/workspaces.ts
10003
10257
  import { randomUUID as randomUUID4 } from "crypto";
10258
+ var MAX_WORKSPACE_NAME_LENGTH = 40;
10259
+ var DEFAULT_WORKSPACE_ICON = "grid";
10260
+ var WORKSPACE_ICON_KEYS = /* @__PURE__ */ new Set([
10261
+ "grid",
10262
+ "folder",
10263
+ "briefcase",
10264
+ "code",
10265
+ "cube",
10266
+ "rocket",
10267
+ "albums",
10268
+ "bookmark",
10269
+ "heart",
10270
+ "library",
10271
+ "planet",
10272
+ "school"
10273
+ ]);
10274
+ async function listWorkspaces(paths) {
10275
+ const store = await readWorkspaceStore(paths);
10276
+ return sortWorkspaces(store.workspaces);
10277
+ }
10278
+ async function createWorkspace(paths, input) {
10279
+ const name = normalizeWorkspaceName(input.name);
10280
+ const icon = normalizeWorkspaceIcon(input.icon);
10281
+ let created;
10282
+ await updateWorkspaceStore(paths, (store) => {
10283
+ assertUniqueWorkspaceName(store.workspaces, name);
10284
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10285
+ created = {
10286
+ id: `ws_${randomUUID4().replaceAll("-", "")}`,
10287
+ name,
10288
+ icon,
10289
+ created_at: now,
10290
+ updated_at: now
10291
+ };
10292
+ return {
10293
+ ...store,
10294
+ workspaces: [...store.workspaces, created]
10295
+ };
10296
+ });
10297
+ return created;
10298
+ }
10299
+ async function renameWorkspace(paths, workspaceId, input) {
10300
+ const id = normalizeWorkspaceId(workspaceId);
10301
+ const name = normalizeWorkspaceName(input.name);
10302
+ const icon = normalizeWorkspaceIcon(input.icon);
10303
+ let renamed;
10304
+ await updateWorkspaceStore(paths, (store) => {
10305
+ assertUniqueWorkspaceName(store.workspaces, name, id);
10306
+ const index = store.workspaces.findIndex((workspace) => workspace.id === id);
10307
+ if (index < 0) {
10308
+ throw workspaceNotFound();
10309
+ }
10310
+ const next = [...store.workspaces];
10311
+ renamed = {
10312
+ ...next[index],
10313
+ name,
10314
+ icon,
10315
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
10316
+ };
10317
+ next[index] = renamed;
10318
+ return { ...store, workspaces: next };
10319
+ });
10320
+ return renamed;
10321
+ }
10322
+ async function deleteWorkspace(paths, workspaceId) {
10323
+ const id = normalizeWorkspaceId(workspaceId);
10324
+ let deleted;
10325
+ await updateWorkspaceStore(paths, (store) => {
10326
+ const next = store.workspaces.filter((workspace) => {
10327
+ if (workspace.id === id) {
10328
+ deleted = workspace;
10329
+ return false;
10330
+ }
10331
+ return true;
10332
+ });
10333
+ if (!deleted) {
10334
+ throw workspaceNotFound();
10335
+ }
10336
+ return { ...store, workspaces: next };
10337
+ });
10338
+ return deleted;
10339
+ }
10340
+ async function assertWorkspaceExists(paths, workspaceId) {
10341
+ const id = normalizeOptionalWorkspaceId(workspaceId);
10342
+ if (!id) {
10343
+ return void 0;
10344
+ }
10345
+ const store = await readWorkspaceStore(paths);
10346
+ if (!store.workspaces.some((workspace) => workspace.id === id)) {
10347
+ throw workspaceNotFound();
10348
+ }
10349
+ return id;
10350
+ }
10351
+ function normalizeOptionalWorkspaceId(value) {
10352
+ if (value == null) {
10353
+ return void 0;
10354
+ }
10355
+ const normalized = value.trim();
10356
+ if (!normalized || normalized === "default") {
10357
+ return void 0;
10358
+ }
10359
+ return normalizeWorkspaceId(normalized);
10360
+ }
10361
+ function normalizeWorkspaceId(value) {
10362
+ const normalized = value.trim();
10363
+ if (!/^ws_[a-zA-Z0-9]+$/u.test(normalized)) {
10364
+ throw new LinkHttpError(400, "workspace_id_invalid", "workspace_id is invalid");
10365
+ }
10366
+ return normalized;
10367
+ }
10368
+ function normalizeWorkspaceName(value) {
10369
+ const normalized = value.replace(/\s+/gu, " ").trim();
10370
+ if (!normalized) {
10371
+ throw new LinkHttpError(
10372
+ 400,
10373
+ "workspace_name_required",
10374
+ "workspace name is required"
10375
+ );
10376
+ }
10377
+ if (Array.from(normalized).length > MAX_WORKSPACE_NAME_LENGTH) {
10378
+ throw new LinkHttpError(
10379
+ 400,
10380
+ "workspace_name_too_long",
10381
+ "workspace name is too long"
10382
+ );
10383
+ }
10384
+ return normalized;
10385
+ }
10386
+ function normalizeWorkspaceIcon(value) {
10387
+ const normalized = value?.trim() ?? "";
10388
+ if (!normalized) {
10389
+ return DEFAULT_WORKSPACE_ICON;
10390
+ }
10391
+ return WORKSPACE_ICON_KEYS.has(normalized) ? normalized : DEFAULT_WORKSPACE_ICON;
10392
+ }
10393
+ function assertUniqueWorkspaceName(workspaces, name, exceptWorkspaceId) {
10394
+ const normalized = name.toLocaleLowerCase();
10395
+ if (workspaces.some(
10396
+ (workspace) => workspace.id !== exceptWorkspaceId && workspace.name.toLocaleLowerCase() === normalized
10397
+ )) {
10398
+ throw new LinkHttpError(
10399
+ 409,
10400
+ "workspace_name_conflict",
10401
+ "workspace name already exists"
10402
+ );
10403
+ }
10404
+ }
10405
+ async function readWorkspaceStore(paths) {
10406
+ return normalizeWorkspaceStore(await readJsonFile(paths.workspacesFile));
10407
+ }
10408
+ async function updateWorkspaceStore(paths, update) {
10409
+ return updateJsonFile(
10410
+ paths.workspacesFile,
10411
+ (current) => update(normalizeWorkspaceStore(current)),
10412
+ 384
10413
+ );
10414
+ }
10415
+ function normalizeWorkspaceStore(store) {
10416
+ if (!store || !Array.isArray(store.workspaces)) {
10417
+ return { schema_version: 1, workspaces: [] };
10418
+ }
10419
+ return {
10420
+ schema_version: 1,
10421
+ workspaces: store.workspaces.map(normalizeWorkspaceRecord).filter(
10422
+ (workspace) => Boolean(workspace)
10423
+ )
10424
+ };
10425
+ }
10426
+ function normalizeWorkspaceRecord(value) {
10427
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10428
+ return null;
10429
+ }
10430
+ const record = value;
10431
+ const id = typeof record.id === "string" ? record.id.trim() : "";
10432
+ const name = typeof record.name === "string" ? record.name.trim() : "";
10433
+ const icon = normalizeWorkspaceIcon(
10434
+ typeof record.icon === "string" ? record.icon : void 0
10435
+ );
10436
+ const createdAt = typeof record.created_at === "string" ? record.created_at.trim() : "";
10437
+ const updatedAt = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
10438
+ if (!id || !name || !createdAt || !updatedAt) {
10439
+ return null;
10440
+ }
10441
+ return { id, name, icon, created_at: createdAt, updated_at: updatedAt };
10442
+ }
10443
+ function sortWorkspaces(workspaces) {
10444
+ return [...workspaces].sort(
10445
+ (left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id)
10446
+ );
10447
+ }
10448
+ function workspaceNotFound() {
10449
+ return new LinkHttpError(404, "workspace_not_found", "workspace was not found");
10450
+ }
10451
+
10452
+ // src/conversations/blob-store.ts
10453
+ import { randomUUID as randomUUID5 } from "crypto";
10004
10454
  import { mkdir as mkdir6, readFile as readFile7, readdir as readdir4, rm as rm3, stat as stat6, writeFile } from "fs/promises";
10005
10455
  import path10 from "path";
10006
10456
 
@@ -10453,7 +10903,7 @@ async function writeConversationBlob(paths, conversationId, input, options) {
10453
10903
  if (input.bytes.byteLength > options.maxBytes) {
10454
10904
  throw new LinkHttpError(413, "blob_too_large", "Blob is too large");
10455
10905
  }
10456
- const id = `blob_${randomUUID4().replaceAll("-", "")}`;
10906
+ const id = `blob_${randomUUID5().replaceAll("-", "")}`;
10457
10907
  const filePath = blobPath(paths, id);
10458
10908
  await mkdir6(path10.dirname(filePath), { recursive: true, mode: 448 });
10459
10909
  await writeFile(filePath, input.bytes, { mode: 384 });
@@ -10592,13 +11042,6 @@ async function listConversationBlobIds(paths, conversationId) {
10592
11042
  }
10593
11043
  return blobIds;
10594
11044
  }
10595
- async function removeConversationFiles(paths, conversationId) {
10596
- assertValidConversationId(conversationId);
10597
- await rm3(path10.join(paths.conversationsDir, conversationId), {
10598
- recursive: true,
10599
- force: true
10600
- });
10601
- }
10602
11045
  function conversationAttachmentsDir(paths, conversationId) {
10603
11046
  assertValidConversationId(conversationId);
10604
11047
  return path10.join(paths.conversationsDir, conversationId, "attachments");
@@ -10681,7 +11124,7 @@ async function readConversationProfileSummary(paths, manifest) {
10681
11124
  }
10682
11125
  async function buildConversationRuntimeMetadata(paths, manifest, snapshot) {
10683
11126
  const current = await readCurrentConversationRuntime(paths, manifest);
10684
- const usageRun = latestUsageRun(snapshot);
11127
+ const usageRun = latestContextUsageRun(snapshot);
10685
11128
  const profileUid = current.profileUid;
10686
11129
  const profileName = current.profileName;
10687
11130
  const profilePresentation = {
@@ -10828,6 +11271,7 @@ function toSummary(manifest, snapshot, profile) {
10828
11271
  id: manifest.id,
10829
11272
  title: manifest.title,
10830
11273
  title_source: manifest.title_source,
11274
+ workspace_id: manifest.workspace_id ?? null,
10831
11275
  created_at: manifest.created_at,
10832
11276
  updated_at: manifest.updated_at,
10833
11277
  last_event_seq: manifest.last_event_seq,
@@ -10878,7 +11322,7 @@ function isRealtimeRunStatus(status) {
10878
11322
  }
10879
11323
 
10880
11324
  // src/conversations/slash-commands.ts
10881
- import { randomUUID as randomUUID5 } from "crypto";
11325
+ import { randomUUID as randomUUID6 } from "crypto";
10882
11326
  var MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,127}$/u;
10883
11327
  function isValidModelId(value) {
10884
11328
  return MODEL_ID_PATTERN.test(value);
@@ -10975,7 +11419,7 @@ function parseSlashCommandInput(content, language = "zh-CN") {
10975
11419
  }
10976
11420
  function createSlashCommandUserMessage(input) {
10977
11421
  return {
10978
- id: `msg_${randomUUID5().replaceAll("-", "")}`,
11422
+ id: `msg_${randomUUID6().replaceAll("-", "")}`,
10979
11423
  schema_version: 1,
10980
11424
  conversation_id: input.conversationId,
10981
11425
  role: "user",
@@ -11025,7 +11469,7 @@ function slashHelpMessage(language = "zh-CN") {
11025
11469
  ].join("\n");
11026
11470
  }
11027
11471
  function freshHermesSessionId(conversationId) {
11028
- return `hp_${conversationId}_${randomUUID5().replaceAll("-", "").slice(0, 12)}`;
11472
+ return `hp_${conversationId}_${randomUUID6().replaceAll("-", "").slice(0, 12)}`;
11029
11473
  }
11030
11474
  function nextVerboseMode(current) {
11031
11475
  const modes = [
@@ -11663,7 +12107,7 @@ function safePathSegment(value, fallback) {
11663
12107
  }
11664
12108
 
11665
12109
  // src/conversations/conversation-archive-plans.ts
11666
- import { randomUUID as randomUUID6 } from "crypto";
12110
+ import { randomUUID as randomUUID7 } from "crypto";
11667
12111
  import { mkdir as mkdir8 } from "fs/promises";
11668
12112
  import path12 from "path";
11669
12113
  var PLAN_ID_PATTERN = /^archive_[a-f0-9]{32}$/u;
@@ -11675,7 +12119,7 @@ var ConversationArchivePlanStore = class {
11675
12119
  async create(conversationIds) {
11676
12120
  const now = (/* @__PURE__ */ new Date()).toISOString();
11677
12121
  const plan = {
11678
- id: `archive_${randomUUID6().replaceAll("-", "")}`,
12122
+ id: `archive_${randomUUID7().replaceAll("-", "")}`,
11679
12123
  status: "prepared",
11680
12124
  created_at: now,
11681
12125
  updated_at: now,
@@ -11727,7 +12171,7 @@ function normalizePlanId(planId) {
11727
12171
  }
11728
12172
 
11729
12173
  // src/conversations/conversation-clear-plans.ts
11730
- import { randomUUID as randomUUID7 } from "crypto";
12174
+ import { randomUUID as randomUUID8 } from "crypto";
11731
12175
  import { mkdir as mkdir9 } from "fs/promises";
11732
12176
  import path13 from "path";
11733
12177
  var PLAN_ID_PATTERN2 = /^clear_[a-f0-9]{32}$/u;
@@ -11739,7 +12183,7 @@ var ConversationClearPlanStore = class {
11739
12183
  async create(conversationIds, targetStatus = "active") {
11740
12184
  const now = (/* @__PURE__ */ new Date()).toISOString();
11741
12185
  const plan = {
11742
- id: `clear_${randomUUID7().replaceAll("-", "")}`,
12186
+ id: `clear_${randomUUID8().replaceAll("-", "")}`,
11743
12187
  status: "prepared",
11744
12188
  target_status: targetStatus,
11745
12189
  created_at: now,
@@ -11940,6 +12384,11 @@ function collectHermesSessionDeleteTargets(manifest, snapshot) {
11940
12384
  }
11941
12385
  return targets;
11942
12386
  }
12387
+ function collectHermesSessionMutationTargets(manifest, snapshot) {
12388
+ return collectHermesSessionDeleteTargets(manifest, snapshot).filter(
12389
+ (target) => !target.sessionId.startsWith("hp_")
12390
+ );
12391
+ }
11943
12392
  function arraysEqual(left, right) {
11944
12393
  if (left.length !== right.length) {
11945
12394
  return false;
@@ -12389,6 +12838,7 @@ var ConversationMaintenanceCoordinator = class {
12389
12838
  }
12390
12839
  const archivedAt = (/* @__PURE__ */ new Date()).toISOString();
12391
12840
  const snapshot = await this.deps.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
12841
+ await this.setHermesSessionsArchived(manifest, snapshot, true);
12392
12842
  await this.deps.store.writeManifest({
12393
12843
  ...manifest,
12394
12844
  status: "archived",
@@ -12429,6 +12879,7 @@ var ConversationMaintenanceCoordinator = class {
12429
12879
  }
12430
12880
  const unarchivedAt = (/* @__PURE__ */ new Date()).toISOString();
12431
12881
  const snapshot = await this.deps.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
12882
+ await this.setHermesSessionsArchived(manifest, snapshot, false);
12432
12883
  const next = {
12433
12884
  ...manifest,
12434
12885
  status: "active",
@@ -12532,6 +12983,18 @@ var ConversationMaintenanceCoordinator = class {
12532
12983
  }
12533
12984
  return results;
12534
12985
  }
12986
+ async setHermesSessionsArchived(manifest, snapshot, archived) {
12987
+ const targets = collectHermesSessionMutationTargets(manifest, snapshot);
12988
+ for (const target of targets) {
12989
+ await setTuiGatewaySessionArchived({
12990
+ paths: this.deps.paths,
12991
+ logger: this.deps.logger,
12992
+ profileName: target.profileName,
12993
+ sessionId: target.sessionId,
12994
+ archived
12995
+ });
12996
+ }
12997
+ }
12535
12998
  async pruneConversationBlobReferences(conversationId, blobIds) {
12536
12999
  for (const blobId of blobIds) {
12537
13000
  try {
@@ -13056,7 +13519,7 @@ function stripCompressionTitleSuffix(value) {
13056
13519
  }
13057
13520
 
13058
13521
  // src/conversations/conversation-turns.ts
13059
- import { randomUUID as randomUUID8 } from "crypto";
13522
+ import { randomUUID as randomUUID9 } from "crypto";
13060
13523
  var MESSAGE_ORDER_STEP_MS = 10;
13061
13524
  var ASSISTANT_ORDER_OFFSET_MS = 1;
13062
13525
  function createAgentTurnDraft(input) {
@@ -13305,10 +13768,10 @@ function createAssistantMessage(input) {
13305
13768
  };
13306
13769
  }
13307
13770
  function createMessageId() {
13308
- return `msg_${randomUUID8().replaceAll("-", "")}`;
13771
+ return `msg_${randomUUID9().replaceAll("-", "")}`;
13309
13772
  }
13310
13773
  function createRunId() {
13311
- return `run_${randomUUID8().replaceAll("-", "")}`;
13774
+ return `run_${randomUUID9().replaceAll("-", "")}`;
13312
13775
  }
13313
13776
  function hasActiveOrQueuedRuns(snapshot) {
13314
13777
  return snapshot.runs.some(
@@ -14600,11 +15063,17 @@ var ConversationQueryCoordinator = class {
14600
15063
  status,
14601
15064
  limit,
14602
15065
  cursor,
14603
- fallback: () => this.listConversationPageFromStore({ status, limit, cursor }),
15066
+ fallback: () => this.listConversationPageFromStore({
15067
+ status,
15068
+ limit,
15069
+ cursor,
15070
+ workspace: options.workspace
15071
+ }),
14604
15072
  listPage: (pageCursor) => listConversationStatsPage(this.deps.paths, {
14605
15073
  status,
14606
15074
  limit,
14607
- cursor: pageCursor
15075
+ cursor: pageCursor,
15076
+ workspace: options.workspace
14608
15077
  })
14609
15078
  });
14610
15079
  }
@@ -14693,11 +15162,11 @@ var ConversationQueryCoordinator = class {
14693
15162
  }
14694
15163
  };
14695
15164
  }
14696
- async listConversationsFromStore(status = "active") {
15165
+ async listConversationsFromStore(status = "active", workspace) {
14697
15166
  const summaries = [];
14698
15167
  for (const conversationId of await this.deps.store.listConversationIds()) {
14699
15168
  const manifest = await this.deps.store.readManifest(conversationId).catch(() => null);
14700
- if (!manifest || manifest.status !== status) {
15169
+ if (!manifest || manifest.status !== status || !matchesWorkspaceFilter(manifest, workspace)) {
14701
15170
  continue;
14702
15171
  }
14703
15172
  const snapshot = await this.deps.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
@@ -14709,7 +15178,10 @@ var ConversationQueryCoordinator = class {
14709
15178
  );
14710
15179
  }
14711
15180
  async listConversationPageFromStore(input) {
14712
- const all = await this.listConversationsFromStore(input.status);
15181
+ const all = await this.listConversationsFromStore(
15182
+ input.status,
15183
+ input.workspace
15184
+ );
14713
15185
  const startIndex = input.cursor ? all.findIndex(
14714
15186
  (summary) => isAfterConversationListCursor(summary, input.cursor)
14715
15187
  ) : 0;
@@ -14914,6 +15386,16 @@ function readNonEmptyString(value) {
14914
15386
  function normalizeConversationSearchQuery(value) {
14915
15387
  return typeof value === "string" ? value.trim() : "";
14916
15388
  }
15389
+ function matchesWorkspaceFilter(manifest, filter) {
15390
+ if (!filter || filter.kind === "all") {
15391
+ return true;
15392
+ }
15393
+ const workspaceId = manifest.workspace_id?.trim() ?? "";
15394
+ if (filter.kind === "default") {
15395
+ return workspaceId === "";
15396
+ }
15397
+ return workspaceId === filter.workspaceId;
15398
+ }
14917
15399
  function isAfterConversationListCursor(summary, cursor) {
14918
15400
  return summary.updated_at < cursor.updatedAt || summary.updated_at === cursor.updatedAt && summary.id < cursor.conversationId;
14919
15401
  }
@@ -15106,7 +15588,7 @@ function isNodeError9(error, code) {
15106
15588
  }
15107
15589
 
15108
15590
  // src/conversations/hermes-session-sync.ts
15109
- import { randomUUID as randomUUID9 } from "crypto";
15591
+ import { randomUUID as randomUUID10 } from "crypto";
15110
15592
  import { readdir as readdir7, readFile as readFile10, stat as stat10 } from "fs/promises";
15111
15593
  import path17 from "path";
15112
15594
 
@@ -17068,7 +17550,7 @@ function toLinkMessage(input) {
17068
17550
  const sessionId = readString10(input.message, "session_id") ?? input.sessionId;
17069
17551
  const createdAt = isoFromHermesTime(input.message.timestamp) ?? new Date(Date.now() + input.index).toISOString();
17070
17552
  return {
17071
- id: `msg_${randomUUID9().replaceAll("-", "")}`,
17553
+ id: `msg_${randomUUID10().replaceAll("-", "")}`,
17072
17554
  schema_version: 1,
17073
17555
  conversation_id: input.conversationId,
17074
17556
  role,
@@ -17251,7 +17733,7 @@ async function isFile(filePath) {
17251
17733
  });
17252
17734
  }
17253
17735
  function createConversationId() {
17254
- return `conv_${randomUUID9().replaceAll("-", "")}`;
17736
+ return `conv_${randomUUID10().replaceAll("-", "")}`;
17255
17737
  }
17256
17738
  function isoFromHermesTime(value) {
17257
17739
  const numeric = readNumber3(value);
@@ -17942,10 +18424,10 @@ function parseHermesApiCapabilities(payload) {
17942
18424
  sessionKeyHeader: readString12(features, "session_key_header")
17943
18425
  };
17944
18426
  }
17945
- async function callHermesApi(path33, init, options) {
18427
+ async function callHermesApi(path34, init, options) {
17946
18428
  const method = init.method ?? "GET";
17947
18429
  const startedAt = Date.now();
17948
- void options.logger?.debug("hermes_api_request_started", { method, path: path33 });
18430
+ void options.logger?.debug("hermes_api_request_started", { method, path: path34 });
17949
18431
  const availability = await ensureHermesApiServerAvailable({
17950
18432
  fetchImpl: options.fetchImpl,
17951
18433
  logger: options.logger,
@@ -17954,7 +18436,7 @@ async function callHermesApi(path33, init, options) {
17954
18436
  });
17955
18437
  let config = availability.configResult.apiServer;
17956
18438
  const fetcher = options.fetchImpl ?? fetch;
17957
- const request = () => fetchHermesApi(fetcher, config, path33, init, options);
18439
+ const request = () => fetchHermesApi(fetcher, config, path34, init, options);
17958
18440
  let response;
17959
18441
  try {
17960
18442
  response = await request();
@@ -17962,7 +18444,7 @@ async function callHermesApi(path33, init, options) {
17962
18444
  logHermesApiError(
17963
18445
  options.logger,
17964
18446
  method,
17965
- path33,
18447
+ path34,
17966
18448
  options.profileName,
17967
18449
  startedAt,
17968
18450
  error
@@ -17973,7 +18455,7 @@ async function callHermesApi(path33, init, options) {
17973
18455
  logHermesApiResponse(
17974
18456
  options.logger,
17975
18457
  method,
17976
- path33,
18458
+ path34,
17977
18459
  options.profileName,
17978
18460
  startedAt,
17979
18461
  response
@@ -17982,7 +18464,7 @@ async function callHermesApi(path33, init, options) {
17982
18464
  }
17983
18465
  void options.logger?.warn("hermes_api_request_retrying_after_401", {
17984
18466
  method,
17985
- path: path33,
18467
+ path: path34,
17986
18468
  profile: options.profileName ?? "default",
17987
18469
  port: config.port ?? null,
17988
18470
  duration_ms: Date.now() - startedAt
@@ -18001,7 +18483,7 @@ async function callHermesApi(path33, init, options) {
18001
18483
  logHermesApiError(
18002
18484
  options.logger,
18003
18485
  method,
18004
- path33,
18486
+ path34,
18005
18487
  options.profileName,
18006
18488
  startedAt,
18007
18489
  error
@@ -18011,7 +18493,7 @@ async function callHermesApi(path33, init, options) {
18011
18493
  logHermesApiResponse(
18012
18494
  options.logger,
18013
18495
  method,
18014
- path33,
18496
+ path34,
18015
18497
  options.profileName,
18016
18498
  startedAt,
18017
18499
  response
@@ -18021,7 +18503,7 @@ async function callHermesApi(path33, init, options) {
18021
18503
  }
18022
18504
  void options.logger?.warn("hermes_api_request_repairing_after_401", {
18023
18505
  method,
18024
- path: path33,
18506
+ path: path34,
18025
18507
  profile: options.profileName ?? "default",
18026
18508
  port: config.port ?? null,
18027
18509
  duration_ms: Date.now() - startedAt
@@ -18042,7 +18524,7 @@ async function callHermesApi(path33, init, options) {
18042
18524
  logHermesApiError(
18043
18525
  options.logger,
18044
18526
  method,
18045
- path33,
18527
+ path34,
18046
18528
  options.profileName,
18047
18529
  startedAt,
18048
18530
  error
@@ -18052,21 +18534,21 @@ async function callHermesApi(path33, init, options) {
18052
18534
  logHermesApiResponse(
18053
18535
  options.logger,
18054
18536
  method,
18055
- path33,
18537
+ path34,
18056
18538
  options.profileName,
18057
18539
  startedAt,
18058
18540
  response
18059
18541
  );
18060
18542
  return response;
18061
18543
  }
18062
- async function fetchHermesApi(fetcher, config, path33, init, options) {
18544
+ async function fetchHermesApi(fetcher, config, path34, init, options) {
18063
18545
  const headers = new Headers(init.headers);
18064
18546
  headers.set("accept", headers.get("accept") ?? "application/json");
18065
18547
  if (config.key) {
18066
18548
  headers.set("x-api-key", config.key);
18067
18549
  headers.set("authorization", `Bearer ${config.key}`);
18068
18550
  }
18069
- return await fetcher(`http://127.0.0.1:${config.port}${path33}`, {
18551
+ return await fetcher(`http://127.0.0.1:${config.port}${path34}`, {
18070
18552
  ...init,
18071
18553
  headers
18072
18554
  }).catch((error) => {
@@ -18075,10 +18557,10 @@ async function fetchHermesApi(fetcher, config, path33, init, options) {
18075
18557
  }
18076
18558
  void options.logger?.warn("hermes_api_server_connect_failed", {
18077
18559
  method: String(init.method ?? "GET").toUpperCase(),
18078
- path: path33,
18560
+ path: path34,
18079
18561
  profile: options.profileName ?? "default",
18080
18562
  port: config.port ?? null,
18081
- url: `http://127.0.0.1:${config.port}${path33}`,
18563
+ url: `http://127.0.0.1:${config.port}${path34}`,
18082
18564
  error: error instanceof Error ? error.message : String(error)
18083
18565
  });
18084
18566
  throw new LinkHttpError(
@@ -18088,10 +18570,10 @@ async function fetchHermesApi(fetcher, config, path33, init, options) {
18088
18570
  );
18089
18571
  });
18090
18572
  }
18091
- function logHermesApiResponse(logger, method, path33, profileName, startedAt, response) {
18573
+ function logHermesApiResponse(logger, method, path34, profileName, startedAt, response) {
18092
18574
  const fields = {
18093
18575
  method,
18094
- path: path33,
18576
+ path: path34,
18095
18577
  profile: profileName ?? "default",
18096
18578
  status: response.status,
18097
18579
  duration_ms: Date.now() - startedAt
@@ -18112,10 +18594,10 @@ async function logHermesApiFailureResponse(logger, fields, response) {
18112
18594
  ...upstreamError ? { upstream_error: upstreamError } : {}
18113
18595
  });
18114
18596
  }
18115
- function logHermesApiError(logger, method, path33, profileName, startedAt, error) {
18597
+ function logHermesApiError(logger, method, path34, profileName, startedAt, error) {
18116
18598
  void logger?.warn("hermes_api_request_failed", {
18117
18599
  method,
18118
- path: path33,
18600
+ path: path34,
18119
18601
  profile: profileName ?? "default",
18120
18602
  duration_ms: Date.now() - startedAt,
18121
18603
  ...error instanceof LinkHttpError ? { status: error.status, code: error.code } : {},
@@ -23140,7 +23622,7 @@ ${details.join("\n")}` : localizedEmptyHermesResponseMessage(language);
23140
23622
  payload: { message: assistant }
23141
23623
  });
23142
23624
  }
23143
- const contextUsage = contextUsagePayload(run);
23625
+ const contextUsage = contextUsagePayloadForSnapshot(snapshot);
23144
23626
  await this.deps.appendEvent(conversationId, {
23145
23627
  type: "run.completed",
23146
23628
  message_id: assistant?.id,
@@ -23208,7 +23690,7 @@ ${details.join("\n")}` : localizedEmptyHermesResponseMessage(language);
23208
23690
  assistant?.id,
23209
23691
  expiredApprovals
23210
23692
  );
23211
- const contextUsage = contextUsagePayload(run);
23693
+ const contextUsage = contextUsagePayloadForSnapshot(snapshot);
23212
23694
  await this.deps.appendEvent(conversationId, {
23213
23695
  type: "run.failed",
23214
23696
  message_id: assistant?.id,
@@ -23411,11 +23893,16 @@ ${details.join("\n")}` : localizedEmptyHermesResponseMessage(language);
23411
23893
  payload: { message: assistant, cancelled: true }
23412
23894
  });
23413
23895
  }
23896
+ const contextUsage = contextUsagePayloadForSnapshot(snapshot);
23414
23897
  const event = await this.deps.appendEvent(conversationId, {
23415
23898
  type: "run.cancelled",
23416
23899
  message_id: assistant?.id,
23417
23900
  run_id: runId,
23418
- payload: { run, reason: options.reason }
23901
+ payload: {
23902
+ run,
23903
+ reason: options.reason,
23904
+ ...contextUsage ? { context: contextUsage } : {}
23905
+ }
23419
23906
  });
23420
23907
  await this.deps.persistConversationStats(conversationId, snapshot);
23421
23908
  void this.deps.logger.info("conversation_run_cancelled", {
@@ -23777,6 +24264,10 @@ function appendAgentEventBlock2(message, event, updatedAt) {
23777
24264
  }
23778
24265
  message.blocks = blocks;
23779
24266
  }
24267
+ function contextUsagePayloadForSnapshot(snapshot) {
24268
+ const run = latestContextUsageRun(snapshot);
24269
+ return run ? contextUsagePayload(run) : null;
24270
+ }
23780
24271
  function contextUsagePayload(run) {
23781
24272
  const usage = run.usage;
23782
24273
  const runtimeContext = resolveRuntimeContextUsage({
@@ -23989,6 +24480,7 @@ function isNodeError17(error, code) {
23989
24480
 
23990
24481
  // src/conversations/conversation-service.ts
23991
24482
  var ALL_CONVERSATION_EVENTS = "conversation:*";
24483
+ var HERMES_ARCHIVE_STATE_SYNC_ID = "hermes-agent-archive-state-v1";
23992
24484
  function isConversationNotificationEvent(event) {
23993
24485
  const type = event.type.toLowerCase();
23994
24486
  return type === "conversation.created" || type === "conversation.updated" || isAlwaysPublishedNotificationEvent(type) || type === "message.created" || type === "message.completed" || type === "message.failed" || type === "run.started" || type === "run.queued" || type === "run.completed" || type === "run.failed" || type === "run.cancelled" || type === "run.canceled" || type === "approval.requested" || readPayloadBool(event.payload, "requires_action") || readPayloadBool(event.payload, "requires_user_action") || readPayloadBool(event.payload, "requires_approval");
@@ -24115,6 +24607,7 @@ var ConversationService = class {
24115
24607
  return manifest;
24116
24608
  }
24117
24609
  const snapshot = await this.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
24610
+ await this.setHermesSessionsArchived(manifest, snapshot, false);
24118
24611
  const unarchivedAt = (/* @__PURE__ */ new Date()).toISOString();
24119
24612
  const next = {
24120
24613
  ...manifest,
@@ -24183,12 +24676,16 @@ var ConversationService = class {
24183
24676
  async createConversation(input = {}) {
24184
24677
  await this.store.ensureConversationsDir();
24185
24678
  const now = (/* @__PURE__ */ new Date()).toISOString();
24186
- const id = `conv_${randomUUID10().replaceAll("-", "")}`;
24679
+ const id = `conv_${randomUUID11().replaceAll("-", "")}`;
24187
24680
  const title = input.title?.trim() || DEFAULT_CONVERSATION_TITLE;
24188
24681
  const profile = await resolveConversationProfileTarget(
24189
24682
  this.paths,
24190
24683
  input.profileName
24191
24684
  );
24685
+ const workspaceId = await assertWorkspaceExists(
24686
+ this.paths,
24687
+ input.workspaceId
24688
+ );
24192
24689
  const manifest = {
24193
24690
  id,
24194
24691
  schema_version: 1,
@@ -24204,6 +24701,7 @@ var ConversationService = class {
24204
24701
  profile: profile.profileName,
24205
24702
  owner_account_id: input.accountId,
24206
24703
  owner_app_instance_id: input.appInstanceId,
24704
+ workspace_id: workspaceId ?? null,
24207
24705
  created_at: now,
24208
24706
  updated_at: now,
24209
24707
  last_event_seq: 0
@@ -24214,6 +24712,7 @@ var ConversationService = class {
24214
24712
  payload: {
24215
24713
  title,
24216
24714
  title_source: manifest.title_source,
24715
+ workspace_id: workspaceId ?? null,
24217
24716
  profile: {
24218
24717
  uid: profile.profileUid,
24219
24718
  name: profile.profileName,
@@ -24368,7 +24867,7 @@ var ConversationService = class {
24368
24867
  updated_at: now
24369
24868
  } : manifest;
24370
24869
  const message = {
24371
- id: `msg_${randomUUID10().replaceAll("-", "")}`,
24870
+ id: `msg_${randomUUID11().replaceAll("-", "")}`,
24372
24871
  schema_version: 1,
24373
24872
  conversation_id: manifest.id,
24374
24873
  role: "assistant",
@@ -24559,6 +25058,7 @@ var ConversationService = class {
24559
25058
  if (result.imported_count > 0 || result.reprojected_count > 0) {
24560
25059
  await this.rebuildStatisticsIndex();
24561
25060
  }
25061
+ await this.syncHermesArchiveStatesBestEffort();
24562
25062
  return result;
24563
25063
  })();
24564
25064
  this.hermesSessionSyncPromise = task;
@@ -25126,6 +25626,136 @@ var ConversationService = class {
25126
25626
  async unarchiveConversation(conversationId) {
25127
25627
  return this.maintenance.unarchiveConversation(conversationId);
25128
25628
  }
25629
+ listWorkspaces() {
25630
+ return listWorkspaces(this.paths);
25631
+ }
25632
+ createWorkspace(input) {
25633
+ return createWorkspace(this.paths, input);
25634
+ }
25635
+ renameWorkspace(workspaceId, input) {
25636
+ return renameWorkspace(this.paths, workspaceId, input);
25637
+ }
25638
+ async deleteWorkspace(workspaceId) {
25639
+ const workspace = await deleteWorkspace(this.paths, workspaceId);
25640
+ const movedConversationCount = await this.clearWorkspaceFromConversations(workspace.id);
25641
+ return { workspace, movedConversationCount };
25642
+ }
25643
+ async setConversationWorkspace(conversationId, workspaceId) {
25644
+ const normalizedWorkspaceId = await assertWorkspaceExists(
25645
+ this.paths,
25646
+ workspaceId
25647
+ );
25648
+ return this.withConversationLock(conversationId, async () => {
25649
+ const manifest = await this.store.readRunnableManifest(conversationId);
25650
+ const currentWorkspaceId = manifest.workspace_id ?? null;
25651
+ const nextWorkspaceId = normalizedWorkspaceId ?? null;
25652
+ const snapshot = await this.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
25653
+ if (currentWorkspaceId !== nextWorkspaceId) {
25654
+ const nextManifest = {
25655
+ ...manifest,
25656
+ workspace_id: nextWorkspaceId
25657
+ };
25658
+ await this.store.writeManifest(nextManifest);
25659
+ await this.persistConversationStatsWithoutManifestRewrite(
25660
+ nextManifest,
25661
+ snapshot
25662
+ );
25663
+ const conversation = await this.queries.summarizeConversation(
25664
+ nextManifest,
25665
+ snapshot
25666
+ );
25667
+ return {
25668
+ conversation_id: conversationId,
25669
+ workspace_id: nextWorkspaceId,
25670
+ conversation
25671
+ };
25672
+ }
25673
+ return {
25674
+ conversation_id: conversationId,
25675
+ workspace_id: currentWorkspaceId,
25676
+ conversation: await this.queries.summarizeConversation(
25677
+ manifest,
25678
+ snapshot
25679
+ )
25680
+ };
25681
+ });
25682
+ }
25683
+ async clearWorkspaceFromConversations(workspaceId) {
25684
+ const normalizedWorkspaceId = normalizeOptionalWorkspaceId(workspaceId);
25685
+ if (!normalizedWorkspaceId) {
25686
+ return 0;
25687
+ }
25688
+ let movedConversationCount = 0;
25689
+ for (const conversationId of await this.store.listConversationIds()) {
25690
+ await this.withConversationLock(conversationId, async () => {
25691
+ const manifest = await this.store.readManifest(conversationId).catch(() => null);
25692
+ if (!manifest || manifest.workspace_id !== normalizedWorkspaceId) {
25693
+ return;
25694
+ }
25695
+ const nextManifest = {
25696
+ ...manifest,
25697
+ workspace_id: null
25698
+ };
25699
+ await this.store.writeManifest(nextManifest);
25700
+ const snapshot = await this.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
25701
+ await this.persistConversationStatsWithoutManifestRewrite(
25702
+ nextManifest,
25703
+ snapshot
25704
+ );
25705
+ movedConversationCount += 1;
25706
+ });
25707
+ }
25708
+ return movedConversationCount;
25709
+ }
25710
+ async syncHermesArchiveStatesBestEffort() {
25711
+ const markerPath = this.hermesArchiveStateSyncMarkerPath();
25712
+ const marker = await readJsonFile(
25713
+ markerPath
25714
+ ).catch(() => null);
25715
+ if (marker?.id === HERMES_ARCHIVE_STATE_SYNC_ID) {
25716
+ return;
25717
+ }
25718
+ let failedCount = 0;
25719
+ for (const conversationId of await this.store.listConversationIds()) {
25720
+ const manifest = await this.store.readManifest(conversationId).catch(() => null);
25721
+ if (!manifest || manifest.status !== "archived") {
25722
+ continue;
25723
+ }
25724
+ const snapshot = await this.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
25725
+ await this.setHermesSessionsArchived(manifest, snapshot, true).catch(
25726
+ (error) => {
25727
+ failedCount += 1;
25728
+ void this.logger.debug("hermes_archive_state_sync_failed", {
25729
+ conversation_id: conversationId,
25730
+ archived: true,
25731
+ error: error instanceof Error ? error.message : String(error)
25732
+ });
25733
+ }
25734
+ );
25735
+ }
25736
+ if (failedCount > 0) {
25737
+ return;
25738
+ }
25739
+ await writeJsonFile(markerPath, {
25740
+ id: HERMES_ARCHIVE_STATE_SYNC_ID,
25741
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
25742
+ });
25743
+ }
25744
+ async setHermesSessionsArchived(manifest, snapshot, archived) {
25745
+ const targets = collectHermesSessionMutationTargets(manifest, snapshot);
25746
+ for (const target of targets) {
25747
+ await setTuiGatewaySessionArchived({
25748
+ paths: this.paths,
25749
+ logger: this.logger,
25750
+ profileName: target.profileName,
25751
+ sessionId: target.sessionId,
25752
+ archived
25753
+ });
25754
+ }
25755
+ }
25756
+ hermesArchiveStateSyncMarkerPath() {
25757
+ return path24.join(this.paths.indexesDir, "hermes-archive-state-sync.json");
25758
+ }
25129
25759
  prepareClearAllConversationPlan(targetStatus) {
25130
25760
  return this.maintenance.prepareClearAllConversationPlan(targetStatus);
25131
25761
  }
@@ -25162,45 +25792,15 @@ var ConversationService = class {
25162
25792
  if (!manifest || !conversationMatchesProfile(manifest, profileName, profileUid)) {
25163
25793
  continue;
25164
25794
  }
25165
- const deleted = await this.withConversationLock(
25166
- conversationId,
25167
- async () => {
25168
- const lockedManifest = await this.store.readManifest(conversationId).catch(() => null);
25169
- if (!lockedManifest || !conversationMatchesProfile(lockedManifest, profileName, profileUid)) {
25170
- return false;
25171
- }
25172
- this.abortActiveRunsForConversation(conversationId);
25173
- const snapshot = await this.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
25174
- const blobIds = /* @__PURE__ */ new Set([
25175
- ...collectBlobIds(snapshot),
25176
- ...await listConversationBlobIds(this.paths, conversationId).catch(
25177
- () => []
25178
- )
25179
- ]);
25180
- for (const blobId of blobIds) {
25181
- await pruneConversationBlobReference(
25182
- this.paths,
25183
- conversationId,
25184
- blobId
25185
- ).catch((error) => {
25186
- void this.logger.warn("profile_delete_blob_gc_failed", {
25187
- profile: profileName,
25188
- conversation_id: conversationId,
25189
- blob_id: blobId,
25190
- error: error instanceof Error ? error.message : String(error)
25191
- });
25192
- });
25193
- }
25194
- await removeConversationFiles(this.paths, conversationId);
25195
- await removeConversationDeliveryStaging(
25196
- this.paths,
25197
- conversationId
25198
- ).catch(() => void 0);
25199
- return true;
25200
- }
25201
- );
25202
- if (deleted) {
25795
+ try {
25796
+ await this.deleteConversation(conversationId);
25203
25797
  deletedConversationIds.push(conversationId);
25798
+ } catch (error) {
25799
+ void this.logger.warn("profile_delete_conversation_cleanup_failed", {
25800
+ profile: profileName,
25801
+ conversation_id: conversationId,
25802
+ error: error instanceof Error ? error.message : String(error)
25803
+ });
25204
25804
  }
25205
25805
  }
25206
25806
  await deleteConversationStatsForProfile(this.paths, {
@@ -25231,6 +25831,13 @@ var ConversationService = class {
25231
25831
  statsOverride
25232
25832
  );
25233
25833
  }
25834
+ async persistConversationStatsWithoutManifestRewrite(manifest, snapshot) {
25835
+ const stats = manifest.stats ?? buildConversationStats(manifest, snapshot);
25836
+ await upsertConversationStats(
25837
+ this.paths,
25838
+ toStatsIndexRecord({ ...manifest, stats }, stats)
25839
+ );
25840
+ }
25234
25841
  async appendEvent(conversationId, input) {
25235
25842
  const event = await this.store.appendEvent(conversationId, input);
25236
25843
  this.emitter.emit(this.liveEventName(conversationId), event);
@@ -25345,7 +25952,7 @@ function approvalRestartText(language, zh, en) {
25345
25952
  }
25346
25953
 
25347
25954
  // src/security/devices.ts
25348
- import { randomBytes as randomBytes3, randomUUID as randomUUID11, timingSafeEqual, createHash as createHash7 } from "crypto";
25955
+ import { randomBytes as randomBytes3, randomUUID as randomUUID12, timingSafeEqual, createHash as createHash7 } from "crypto";
25349
25956
  var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
25350
25957
  var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
25351
25958
  var DEVICE_SEEN_WRITE_INTERVAL_MS = 60 * 60 * 1e3;
@@ -25363,7 +25970,7 @@ async function createDeviceSession(input, paths = resolveRuntimePaths()) {
25363
25970
  }
25364
25971
  }
25365
25972
  const device = {
25366
- id: `dev_${randomUUID11().replaceAll("-", "")}`,
25973
+ id: `dev_${randomUUID12().replaceAll("-", "")}`,
25367
25974
  label: normalizeDeviceLabel(input.label),
25368
25975
  platform: normalizeDevicePlatform(input.platform),
25369
25976
  model: normalizeDeviceModel(input.model),
@@ -26240,7 +26847,8 @@ function registerConversationRoutes(router, options) {
26240
26847
  ctx.set("cache-control", "no-store");
26241
26848
  const result = await conversations.listConversationPage({
26242
26849
  limit: readLimit(ctx.query.limit),
26243
- cursor
26850
+ cursor,
26851
+ workspace: readConversationWorkspaceFilter(ctx.query)
26244
26852
  });
26245
26853
  const localized = localizeConversationListPage(result, language);
26246
26854
  ctx.body = {
@@ -26309,7 +26917,8 @@ function registerConversationRoutes(router, options) {
26309
26917
  title: readString19(body, "title") ?? void 0,
26310
26918
  profileName: readOptionalProfileName(body),
26311
26919
  accountId: auth.accountId,
26312
- appInstanceId: auth.appInstanceId
26920
+ appInstanceId: auth.appInstanceId,
26921
+ workspaceId: readConversationWorkspaceId(body)
26313
26922
  }),
26314
26923
  language
26315
26924
  )
@@ -26691,6 +27300,21 @@ function registerConversationRoutes(router, options) {
26691
27300
  };
26692
27301
  }
26693
27302
  );
27303
+ router.patch("/api/v1/conversations/:conversationId/workspace", async (ctx) => {
27304
+ await authenticateRequest(ctx, paths);
27305
+ const language = readPreferredLanguage(ctx);
27306
+ const body = await readJsonBody(ctx.req);
27307
+ ctx.body = {
27308
+ ok: true,
27309
+ ...localizeConversationResult(
27310
+ await conversations.setConversationWorkspace(
27311
+ ctx.params.conversationId,
27312
+ readConversationWorkspaceId(body)
27313
+ ),
27314
+ language
27315
+ )
27316
+ };
27317
+ });
26694
27318
  router.delete("/api/v1/conversations/:conversationId", async (ctx) => {
26695
27319
  const auth = await authenticateRequest(ctx, paths);
26696
27320
  const result = await conversations.deleteConversation(
@@ -26784,6 +27408,25 @@ function readConversationListForce(query) {
26784
27408
  const raw = Array.isArray(query.force) ? query.force[0] : query.force;
26785
27409
  return readBoolean3(raw) === true;
26786
27410
  }
27411
+ function readConversationWorkspaceFilter(query) {
27412
+ const workspace = readQueryString(query.workspace) ?? readQueryString(query.workspace_id);
27413
+ if (!workspace || workspace === "all") {
27414
+ return { kind: "all" };
27415
+ }
27416
+ if (workspace === "default") {
27417
+ return { kind: "default" };
27418
+ }
27419
+ return { kind: "workspace", workspaceId: workspace };
27420
+ }
27421
+ function readConversationWorkspaceId(body) {
27422
+ if (Object.prototype.hasOwnProperty.call(body, "workspace_id") || Object.prototype.hasOwnProperty.call(body, "workspaceId")) {
27423
+ return readString19(body, "workspace_id") ?? readString19(body, "workspaceId");
27424
+ }
27425
+ if (Object.prototype.hasOwnProperty.call(body, "workspace")) {
27426
+ return readString19(body, "workspace");
27427
+ }
27428
+ return void 0;
27429
+ }
26787
27430
  function localizeConversationListPage(page, language) {
26788
27431
  return {
26789
27432
  ...page,
@@ -26997,11 +27640,11 @@ function isSseRequestContext(ctx) {
26997
27640
  }
26998
27641
  return isSseRequestPath(ctx.path) || isActiveSseSocket(ctx.req.socket);
26999
27642
  }
27000
- function isSseRequestPath(path33) {
27001
- if (!path33) {
27643
+ function isSseRequestPath(path34) {
27644
+ if (!path34) {
27002
27645
  return false;
27003
27646
  }
27004
- return path33 === "/api/v1/conversations/events" || path33 === "/api/v1/profile-creation/events" || path33 === "/api/v1/hermes/update/events" || path33 === "/api/v1/link/update/events" || /^\/api\/v1\/conversations\/[^/]+\/events$/u.test(path33) || /^\/api\/v1\/runs\/[^/]+\/events$/u.test(path33);
27647
+ return path34 === "/api/v1/conversations/events" || path34 === "/api/v1/profile-creation/events" || path34 === "/api/v1/hermes/update/events" || path34 === "/api/v1/link/update/events" || /^\/api\/v1\/conversations\/[^/]+\/events$/u.test(path34) || /^\/api\/v1\/runs\/[^/]+\/events$/u.test(path34);
27005
27648
  }
27006
27649
  function isExpectedClientDisconnectError2(error, options = {}) {
27007
27650
  if (!(error instanceof Error)) {
@@ -27833,7 +28476,7 @@ function errorMessage3(error) {
27833
28476
 
27834
28477
  // src/hermes/profile-identity.ts
27835
28478
  import { readFile as readFile16, stat as stat17 } from "fs/promises";
27836
- import path24 from "path";
28479
+ import path25 from "path";
27837
28480
  var MAX_SOUL_MD_LENGTH = 2e4;
27838
28481
  async function readHermesProfileIdentity(profileName, paths) {
27839
28482
  await assertProfileExists3(profileName, paths);
@@ -27890,7 +28533,7 @@ async function assertProfileExists3(profileName, paths) {
27890
28533
  }
27891
28534
  }
27892
28535
  function resolveSoulPath(profileName) {
27893
- return path24.join(resolveHermesProfileDir(profileName), "SOUL.md");
28536
+ return path25.join(resolveHermesProfileDir(profileName), "SOUL.md");
27894
28537
  }
27895
28538
  function isNodeError18(error, code) {
27896
28539
  return error instanceof Error && "code" in error && error.code === code;
@@ -27906,13 +28549,13 @@ import {
27906
28549
  rm as rm7,
27907
28550
  stat as stat19
27908
28551
  } from "fs/promises";
27909
- import path26 from "path";
28552
+ import path27 from "path";
27910
28553
  import YAML5 from "yaml";
27911
28554
 
27912
28555
  // src/hermes/link-skill.ts
27913
28556
  import { readFile as readFile17, stat as stat18 } from "fs/promises";
27914
28557
  import os5 from "os";
27915
- import path25 from "path";
28558
+ import path26 from "path";
27916
28559
  import YAML4 from "yaml";
27917
28560
  var HERMES_LINK_SKILL_ROOT_DIR = "hermes-skills";
27918
28561
  var HERMES_LINK_SKILL_DIR = "hermes-link";
@@ -27997,7 +28640,7 @@ Do not modify Hermes profiles, delete user data, edit config files, or kill proc
27997
28640
  async function ensureHermesLinkSkillInstalledForProfiles(options = {}) {
27998
28641
  const paths = options.paths ?? resolveRuntimePaths();
27999
28642
  const externalDir = resolveHermesLinkSkillExternalDir(paths);
28000
- const skillPath = path25.join(
28643
+ const skillPath = path26.join(
28001
28644
  externalDir,
28002
28645
  HERMES_LINK_SKILL_DIR,
28003
28646
  HERMES_LINK_SKILL_FILE
@@ -28079,7 +28722,7 @@ function withDefaultProfilePlaceholder2(profiles) {
28079
28722
  ];
28080
28723
  }
28081
28724
  function resolveHermesLinkSkillExternalDir(paths = resolveRuntimePaths()) {
28082
- return path25.join(paths.homeDir, HERMES_LINK_SKILL_ROOT_DIR);
28725
+ return path26.join(paths.homeDir, HERMES_LINK_SKILL_ROOT_DIR);
28083
28726
  }
28084
28727
  async function writeHermesLinkSkill(skillPath) {
28085
28728
  const existing = await readFile17(skillPath, "utf8").catch((error) => {
@@ -28174,11 +28817,11 @@ function appendExternalDir(current, externalDir, hermesHome) {
28174
28817
  const seen = new Set(
28175
28818
  entries.map((entry) => resolveExternalDirEntry(entry, hermesHome))
28176
28819
  );
28177
- const normalizedExternalDir = path25.resolve(externalDir);
28820
+ const normalizedExternalDir = path26.resolve(externalDir);
28178
28821
  return seen.has(normalizedExternalDir) ? entries : [...entries, normalizedExternalDir];
28179
28822
  }
28180
28823
  function externalDirsInclude(current, externalDir, hermesHome) {
28181
- const normalizedExternalDir = path25.resolve(externalDir);
28824
+ const normalizedExternalDir = path26.resolve(externalDir);
28182
28825
  return readExternalDirEntries(current).some(
28183
28826
  (entry) => resolveExternalDirEntry(entry, hermesHome) === normalizedExternalDir
28184
28827
  );
@@ -28189,14 +28832,14 @@ function readExternalDirEntries(value) {
28189
28832
  }
28190
28833
  function resolveExternalDirEntry(entry, hermesHome) {
28191
28834
  const expanded = expandHome(expandEnvVars(entry));
28192
- return path25.resolve(path25.isAbsolute(expanded) ? expanded : path25.join(hermesHome, expanded));
28835
+ return path26.resolve(path26.isAbsolute(expanded) ? expanded : path26.join(hermesHome, expanded));
28193
28836
  }
28194
28837
  function expandHome(value) {
28195
28838
  if (value === "~") {
28196
28839
  return os5.homedir();
28197
28840
  }
28198
- if (value.startsWith(`~${path25.sep}`) || value.startsWith("~/")) {
28199
- return path25.join(os5.homedir(), value.slice(2));
28841
+ if (value.startsWith(`~${path26.sep}`) || value.startsWith("~/")) {
28842
+ return path26.join(os5.homedir(), value.slice(2));
28200
28843
  }
28201
28844
  return value;
28202
28845
  }
@@ -28748,7 +29391,7 @@ function collectEnvKeys(value, keys = /* @__PURE__ */ new Set()) {
28748
29391
  return keys;
28749
29392
  }
28750
29393
  async function writeEnvValues(profileName, values) {
28751
- const envPath = path26.join(resolveHermesProfileDir(profileName), ".env");
29394
+ const envPath = path27.join(resolveHermesProfileDir(profileName), ".env");
28752
29395
  const existingRaw = await readFile18(envPath, "utf8").catch((error) => {
28753
29396
  if (isNodeError20(error, "ENOENT")) {
28754
29397
  return "";
@@ -28785,8 +29428,8 @@ async function writeEnvValues(profileName, values) {
28785
29428
  await atomicWriteFilePreservingMetadata(envPath, nextRaw);
28786
29429
  }
28787
29430
  async function copySkills(sourceProfile, targetProfile) {
28788
- const sourceSkills = path26.join(resolveHermesProfileDir(sourceProfile), "skills");
28789
- const targetSkills = path26.join(resolveHermesProfileDir(targetProfile), "skills");
29431
+ const sourceSkills = path27.join(resolveHermesProfileDir(sourceProfile), "skills");
29432
+ const targetSkills = path27.join(resolveHermesProfileDir(targetProfile), "skills");
28790
29433
  if (!await pathExists2(sourceSkills)) {
28791
29434
  return;
28792
29435
  }
@@ -28881,10 +29524,10 @@ async function readProfileCreationLogLines(paths) {
28881
29524
  );
28882
29525
  }
28883
29526
  function profileCreationStatePath(paths) {
28884
- return path26.join(paths.runDir, "profile-create-state.json");
29527
+ return path27.join(paths.runDir, "profile-create-state.json");
28885
29528
  }
28886
29529
  function profileCreationLogPath(paths) {
28887
- return path26.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
29530
+ return path27.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
28888
29531
  }
28889
29532
  async function clearProfileCreationLogFiles(paths) {
28890
29533
  const primary = profileCreationLogPath(paths);
@@ -29173,7 +29816,7 @@ import {
29173
29816
  readFile as readFile19,
29174
29817
  stat as stat20
29175
29818
  } from "fs/promises";
29176
- import path27 from "path";
29819
+ import path28 from "path";
29177
29820
  import YAML6 from "yaml";
29178
29821
  var ENTRY_DELIMITER = "\n\xA7\n";
29179
29822
  var DEFAULT_MEMORY_LIMIT = 2200;
@@ -29504,7 +30147,7 @@ async function saveProviderSettings(profileName, provider, patch) {
29504
30147
  });
29505
30148
  await patchJsonProviderConfig(
29506
30149
  profileName,
29507
- path27.join("hindsight", "config.json"),
30150
+ path28.join("hindsight", "config.json"),
29508
30151
  {
29509
30152
  mode: patch.mode,
29510
30153
  api_url: patch.apiUrl,
@@ -29707,7 +30350,7 @@ async function patchHermesMemoryLimits(profileName, patch) {
29707
30350
  await atomicWriteFilePreservingMetadata(configPath, document.toString());
29708
30351
  }
29709
30352
  function resolveMemoryDir(profileName) {
29710
- return path27.join(resolveHermesProfileDir(profileName), "memories");
30353
+ return path28.join(resolveHermesProfileDir(profileName), "memories");
29711
30354
  }
29712
30355
  async function readMemoryStore(profileName, target, limits) {
29713
30356
  const filePath = memoryFilePath(profileName, target);
@@ -29768,7 +30411,7 @@ async function writeMemoryEntries(profileName, target, entries) {
29768
30411
  );
29769
30412
  }
29770
30413
  function memoryFilePath(profileName, target) {
29771
- return path27.join(
30414
+ return path28.join(
29772
30415
  resolveMemoryDir(profileName),
29773
30416
  target === "user" ? "USER.md" : "MEMORY.md"
29774
30417
  );
@@ -29828,7 +30471,7 @@ async function readCustomProviderSetupSummary(profileName) {
29828
30471
  configurable: true,
29829
30472
  configured: true,
29830
30473
  configurationIssue: null,
29831
- providerConfigPath: path27.join(
30474
+ providerConfigPath: path28.join(
29832
30475
  resolveHermesProfileDir(profileName),
29833
30476
  "<provider>.json"
29834
30477
  ),
@@ -30222,7 +30865,7 @@ async function readProviderSettings(profileName, provider) {
30222
30865
  stringSetting(
30223
30866
  "dbPath",
30224
30867
  "SQLite \u6570\u636E\u5E93\u8DEF\u5F84",
30225
- config.db_path ?? path27.join(resolveHermesProfileDir(profileName), "memory_store.db")
30868
+ config.db_path ?? path28.join(resolveHermesProfileDir(profileName), "memory_store.db")
30226
30869
  ),
30227
30870
  booleanSetting("autoExtract", "\u4F1A\u8BDD\u7ED3\u675F\u81EA\u52A8\u62BD\u53D6", config.auto_extract ?? false),
30228
30871
  numberSetting("defaultTrust", "\u9ED8\u8BA4\u4FE1\u4EFB\u5206", config.default_trust ?? 0.5),
@@ -30258,7 +30901,7 @@ async function readProviderSettings(profileName, provider) {
30258
30901
  stringSetting(
30259
30902
  "workingDirectory",
30260
30903
  "\u5DE5\u4F5C\u76EE\u5F55",
30261
- path27.join(resolveHermesProfileDir(profileName), "byterover"),
30904
+ path28.join(resolveHermesProfileDir(profileName), "byterover"),
30262
30905
  false
30263
30906
  )
30264
30907
  ];
@@ -30267,16 +30910,16 @@ async function readProviderSettings(profileName, provider) {
30267
30910
  }
30268
30911
  function memoryProviderConfigPath(profileName, provider) {
30269
30912
  if (provider === "honcho") {
30270
- return path27.join(resolveHermesProfileDir(profileName), "honcho.json");
30913
+ return path28.join(resolveHermesProfileDir(profileName), "honcho.json");
30271
30914
  }
30272
30915
  if (provider === "mem0") {
30273
- return path27.join(resolveHermesProfileDir(profileName), "mem0.json");
30916
+ return path28.join(resolveHermesProfileDir(profileName), "mem0.json");
30274
30917
  }
30275
30918
  if (provider === "supermemory") {
30276
- return path27.join(resolveHermesProfileDir(profileName), "supermemory.json");
30919
+ return path28.join(resolveHermesProfileDir(profileName), "supermemory.json");
30277
30920
  }
30278
30921
  if (provider === "hindsight") {
30279
- return path27.join(
30922
+ return path28.join(
30280
30923
  resolveHermesProfileDir(profileName),
30281
30924
  "hindsight",
30282
30925
  "config.json"
@@ -30285,13 +30928,13 @@ function memoryProviderConfigPath(profileName, provider) {
30285
30928
  return null;
30286
30929
  }
30287
30930
  function customProviderConfigPath(profileName, provider) {
30288
- return path27.join(
30931
+ return path28.join(
30289
30932
  resolveHermesProfileDir(profileName),
30290
30933
  `${normalizeCustomProviderId(provider)}.json`
30291
30934
  );
30292
30935
  }
30293
30936
  function customProviderRegistryPath(profileName) {
30294
- return path27.join(
30937
+ return path28.join(
30295
30938
  resolveHermesProfileDir(profileName),
30296
30939
  CUSTOM_PROVIDER_REGISTRY_FILE
30297
30940
  );
@@ -30343,7 +30986,7 @@ async function saveCustomProviderRegistryEntry(profileName, provider) {
30343
30986
  );
30344
30987
  }
30345
30988
  async function discoverUserMemoryProviderDescriptors(profileName) {
30346
- const pluginsDir = path27.join(resolveHermesProfileDir(profileName), "plugins");
30989
+ const pluginsDir = path28.join(resolveHermesProfileDir(profileName), "plugins");
30347
30990
  const entries = await readdir10(pluginsDir, { withFileTypes: true }).catch(
30348
30991
  (error) => {
30349
30992
  if (isNodeError21(error, "ENOENT")) {
@@ -30363,7 +31006,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
30363
31006
  } catch {
30364
31007
  continue;
30365
31008
  }
30366
- const providerDir = path27.join(pluginsDir, entry.name);
31009
+ const providerDir = path28.join(pluginsDir, entry.name);
30367
31010
  if (!await isMemoryProviderPluginDir(providerDir)) {
30368
31011
  continue;
30369
31012
  }
@@ -30377,7 +31020,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
30377
31020
  return descriptors;
30378
31021
  }
30379
31022
  async function isUserMemoryProviderInstalled(profileName, provider) {
30380
- const providerDir = path27.join(
31023
+ const providerDir = path28.join(
30381
31024
  resolveHermesProfileDir(profileName),
30382
31025
  "plugins",
30383
31026
  normalizeCustomProviderId(provider)
@@ -30385,7 +31028,7 @@ async function isUserMemoryProviderInstalled(profileName, provider) {
30385
31028
  return isMemoryProviderPluginDir(providerDir);
30386
31029
  }
30387
31030
  async function isMemoryProviderPluginDir(providerDir) {
30388
- const source = await readFile19(path27.join(providerDir, "__init__.py"), "utf8").catch(
31031
+ const source = await readFile19(path28.join(providerDir, "__init__.py"), "utf8").catch(
30389
31032
  (error) => {
30390
31033
  if (isNodeError21(error, "ENOENT")) {
30391
31034
  return "";
@@ -30397,7 +31040,7 @@ async function isMemoryProviderPluginDir(providerDir) {
30397
31040
  return sample.includes("register_memory_provider") || sample.includes("MemoryProvider");
30398
31041
  }
30399
31042
  async function readPluginMetadata(providerDir) {
30400
- const raw = await readFile19(path27.join(providerDir, "plugin.yaml"), "utf8").catch(
31043
+ const raw = await readFile19(path28.join(providerDir, "plugin.yaml"), "utf8").catch(
30401
31044
  (error) => {
30402
31045
  if (isNodeError21(error, "ENOENT")) {
30403
31046
  return "";
@@ -30409,10 +31052,10 @@ async function readPluginMetadata(providerDir) {
30409
31052
  }
30410
31053
  async function resolveByteRoverCli() {
30411
31054
  const candidates = [
30412
- ...(process.env.PATH ?? "").split(path27.delimiter).filter(Boolean).map((dir) => path27.join(dir, "brv")),
30413
- path27.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
31055
+ ...(process.env.PATH ?? "").split(path28.delimiter).filter(Boolean).map((dir) => path28.join(dir, "brv")),
31056
+ path28.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
30414
31057
  "/usr/local/bin/brv",
30415
- path27.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
31058
+ path28.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
30416
31059
  ].filter(Boolean);
30417
31060
  for (const candidate of candidates) {
30418
31061
  const found = await access3(candidate).then(() => true).catch(() => false);
@@ -30473,7 +31116,7 @@ async function patchHermesMemoryEnv(profileName, patch) {
30473
31116
  if (entries.length === 0) {
30474
31117
  return;
30475
31118
  }
30476
- const envPath = path27.join(resolveHermesProfileDir(profileName), ".env");
31119
+ const envPath = path28.join(resolveHermesProfileDir(profileName), ".env");
30477
31120
  const existingRaw = await readFile19(envPath, "utf8").catch((error) => {
30478
31121
  if (isNodeError21(error, "ENOENT")) {
30479
31122
  return "";
@@ -30644,7 +31287,7 @@ async function readActiveMemoryProvider(profileName) {
30644
31287
  return provider;
30645
31288
  }
30646
31289
  async function patchJsonProviderConfig(profileName, relativePath, patch) {
30647
- const configPath = path27.join(
31290
+ const configPath = path28.join(
30648
31291
  resolveHermesProfileDir(profileName),
30649
31292
  relativePath
30650
31293
  );
@@ -30673,7 +31316,7 @@ async function readJsonObject(filePath) {
30673
31316
  } catch {
30674
31317
  throw new HermesMemoryError(
30675
31318
  "memory_provider_config_invalid",
30676
- `${path27.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
31319
+ `${path28.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
30677
31320
  );
30678
31321
  }
30679
31322
  }
@@ -31297,7 +31940,7 @@ function toMemoryHttpError(error) {
31297
31940
 
31298
31941
  // src/hermes/skills.ts
31299
31942
  import { readFile as readFile20, readdir as readdir11 } from "fs/promises";
31300
- import path28 from "path";
31943
+ import path29 from "path";
31301
31944
  import YAML7 from "yaml";
31302
31945
  var HermesSkillNotFoundError = class extends Error {
31303
31946
  constructor(skillName) {
@@ -31311,7 +31954,7 @@ var EXCLUDED_SKILL_DIRS = /* @__PURE__ */ new Set([".git", ".github", ".hub"]);
31311
31954
  async function listHermesProfileSkills(profileName, paths = resolveRuntimePaths()) {
31312
31955
  const profile = await readExistingProfile(profileName, paths);
31313
31956
  const profileDir = resolveHermesProfileDir(profile.name);
31314
- const skillsRoot = path28.join(profileDir, "skills");
31957
+ const skillsRoot = path29.join(profileDir, "skills");
31315
31958
  const [skillFiles, disabled, provenance] = await Promise.all([
31316
31959
  findSkillFiles(skillsRoot),
31317
31960
  readDisabledSkillNames(resolveHermesConfigPath(profile.name)),
@@ -31402,7 +32045,7 @@ async function collectSkillFiles(directory, results) {
31402
32045
  if (EXCLUDED_SKILL_DIRS.has(entry.name)) {
31403
32046
  continue;
31404
32047
  }
31405
- const entryPath = path28.join(directory, entry.name);
32048
+ const entryPath = path29.join(directory, entry.name);
31406
32049
  if (entry.isDirectory()) {
31407
32050
  await collectSkillFiles(entryPath, results);
31408
32051
  continue;
@@ -31424,10 +32067,10 @@ async function readSkillMetadata(input) {
31424
32067
  if (raw === null) {
31425
32068
  return null;
31426
32069
  }
31427
- const skillDir = path28.dirname(input.skillFile);
32070
+ const skillDir = path29.dirname(input.skillFile);
31428
32071
  const { frontmatter, body } = parseSkillDocument(raw.slice(0, 4e3));
31429
32072
  const name = normalizeSkillName(
31430
- readString21(frontmatter.name) ?? path28.basename(skillDir)
32073
+ readString21(frontmatter.name) ?? path29.basename(skillDir)
31431
32074
  );
31432
32075
  if (!name) {
31433
32076
  return null;
@@ -31446,7 +32089,7 @@ async function readSkillMetadata(input) {
31446
32089
  enabled: !input.disabled.has(name),
31447
32090
  source: provenance.source,
31448
32091
  trust: provenance.trust,
31449
- relativePath: path28.relative(input.skillsRoot, skillDir)
32092
+ relativePath: path29.relative(input.skillsRoot, skillDir)
31450
32093
  };
31451
32094
  }
31452
32095
  function parseSkillDocument(raw) {
@@ -31467,8 +32110,8 @@ function parseSkillDocument(raw) {
31467
32110
  }
31468
32111
  }
31469
32112
  function categoryFromPath(skillsRoot, skillFile) {
31470
- const relative = path28.relative(skillsRoot, skillFile);
31471
- const parts = relative.split(path28.sep).filter(Boolean);
32113
+ const relative = path29.relative(skillsRoot, skillFile);
32114
+ const parts = relative.split(path29.sep).filter(Boolean);
31472
32115
  return parts.length >= 3 ? parts[0] : null;
31473
32116
  }
31474
32117
  function firstBodyDescription(body) {
@@ -31515,7 +32158,7 @@ async function readSkillProvenance(root) {
31515
32158
  return provenance;
31516
32159
  }
31517
32160
  async function readBundledSkillNames(root) {
31518
- const raw = await readFile20(path28.join(root, ".bundled_manifest"), "utf8").catch(
32161
+ const raw = await readFile20(path29.join(root, ".bundled_manifest"), "utf8").catch(
31519
32162
  (error) => {
31520
32163
  if (isNodeError22(error, "ENOENT")) {
31521
32164
  return "";
@@ -31538,7 +32181,7 @@ async function readBundledSkillNames(root) {
31538
32181
  return names;
31539
32182
  }
31540
32183
  async function readHubInstalledSkills(root) {
31541
- const raw = await readFile20(path28.join(root, ".hub", "lock.json"), "utf8").catch(
32184
+ const raw = await readFile20(path29.join(root, ".hub", "lock.json"), "utf8").catch(
31542
32185
  (error) => {
31543
32186
  if (isNodeError22(error, "ENOENT")) {
31544
32187
  return "";
@@ -32201,7 +32844,7 @@ function readModelList(payload) {
32201
32844
  import { EventEmitter as EventEmitter3 } from "events";
32202
32845
  import { spawn as spawn4 } from "child_process";
32203
32846
  import { mkdir as mkdir13, readFile as readFile21, rm as rm8 } from "fs/promises";
32204
- import path29 from "path";
32847
+ import path30 from "path";
32205
32848
  var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
32206
32849
  var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
32207
32850
  var RELEASE_FETCH_TIMEOUT_MS = 5e3;
@@ -32479,13 +33122,13 @@ async function readUpdateLogLines(paths) {
32479
33122
  );
32480
33123
  }
32481
33124
  function releaseCachePath(paths) {
32482
- return path29.join(paths.indexesDir, "hermes-release-check.json");
33125
+ return path30.join(paths.indexesDir, "hermes-release-check.json");
32483
33126
  }
32484
33127
  function updateStatePath(paths) {
32485
- return path29.join(paths.runDir, "hermes-update-state.json");
33128
+ return path30.join(paths.runDir, "hermes-update-state.json");
32486
33129
  }
32487
33130
  function updateLogPath(paths) {
32488
- return path29.join(paths.logsDir, UPDATE_LOG_FILE);
33131
+ return path30.join(paths.logsDir, UPDATE_LOG_FILE);
32489
33132
  }
32490
33133
  async function clearUpdateLogFiles(paths) {
32491
33134
  const primary = updateLogPath(paths);
@@ -32586,12 +33229,12 @@ function readString22(payload, key) {
32586
33229
  import { spawn as spawn6 } from "child_process";
32587
33230
  import { EventEmitter as EventEmitter4 } from "events";
32588
33231
  import { mkdir as mkdir16, readFile as readFile23, rm as rm11 } from "fs/promises";
32589
- import path31 from "path";
33232
+ import path32 from "path";
32590
33233
 
32591
33234
  // src/daemon/process.ts
32592
33235
  import { spawn as spawn5 } from "child_process";
32593
33236
  import { mkdir as mkdir15, readFile as readFile22, rm as rm10, writeFile as writeFile4 } from "fs/promises";
32594
- import path30 from "path";
33237
+ import path31 from "path";
32595
33238
 
32596
33239
  // src/daemon/service.ts
32597
33240
  import { createServer } from "http";
@@ -34538,7 +35181,7 @@ async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
34538
35181
  await mkdir15(paths.logsDir, { recursive: true, mode: 448 });
34539
35182
  const log = createRotatingTextLogWriter({
34540
35183
  paths,
34541
- fileName: path30.basename(daemonLogFile(paths))
35184
+ fileName: path31.basename(daemonLogFile(paths))
34542
35185
  });
34543
35186
  const scriptPath = currentCliScriptPath();
34544
35187
  const write = (chunk) => {
@@ -34836,7 +35479,7 @@ function terminateChild(child, previousForceKillTimer) {
34836
35479
  }
34837
35480
  }
34838
35481
  function supervisorStopIntentPath(paths) {
34839
- return path30.join(paths.runDir, "supervisor-stop-intent.json");
35482
+ return path31.join(paths.runDir, "supervisor-stop-intent.json");
34840
35483
  }
34841
35484
  async function writeSupervisorStopIntent(paths, pid) {
34842
35485
  await mkdir15(paths.runDir, { recursive: true, mode: 448 });
@@ -35399,7 +36042,7 @@ async function buildOfficialInstallCommand(options, targetVersion) {
35399
36042
  };
35400
36043
  }
35401
36044
  function buildUnixInstallCommand(installerUrl) {
35402
- const nodeBinDir = path31.dirname(process.execPath);
36045
+ const nodeBinDir = path32.dirname(process.execPath);
35403
36046
  const fetchScript = [
35404
36047
  quoteShellToken(process.execPath),
35405
36048
  "--input-type=module",
@@ -35669,10 +36312,10 @@ async function readUpdateLogLines2(paths) {
35669
36312
  );
35670
36313
  }
35671
36314
  function updateStatePath2(paths) {
35672
- return path31.join(paths.runDir, "link-update-state.json");
36315
+ return path32.join(paths.runDir, "link-update-state.json");
35673
36316
  }
35674
36317
  function updateLogPath2(paths) {
35675
- return path31.join(paths.logsDir, UPDATE_LOG_FILE2);
36318
+ return path32.join(paths.logsDir, UPDATE_LOG_FILE2);
35676
36319
  }
35677
36320
  async function clearUpdateLogFiles2(paths) {
35678
36321
  const primary = updateLogPath2(paths);
@@ -35756,7 +36399,7 @@ function readString23(payload, key) {
35756
36399
  }
35757
36400
 
35758
36401
  // src/pairing/pairing.ts
35759
- import path32 from "path";
36402
+ import path33 from "path";
35760
36403
  import { rm as rm12 } from "fs/promises";
35761
36404
 
35762
36405
  // src/relay/bootstrap.ts
@@ -36096,10 +36739,10 @@ async function loadRequiredIdentity2(paths) {
36096
36739
  }
36097
36740
  return identity;
36098
36741
  }
36099
- async function postServerJson(serverBaseUrl, path33, body, options) {
36742
+ async function postServerJson(serverBaseUrl, path34, body, options) {
36100
36743
  let response;
36101
36744
  try {
36102
- response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path33}`, {
36745
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path34}`, {
36103
36746
  method: "POST",
36104
36747
  headers: {
36105
36748
  accept: "application/json",
@@ -36147,10 +36790,10 @@ function pairingErrorSnapshot(stage, error) {
36147
36790
  occurred_at: (/* @__PURE__ */ new Date()).toISOString()
36148
36791
  };
36149
36792
  }
36150
- async function patchServerJson(serverBaseUrl, path33, token, body, options) {
36793
+ async function patchServerJson(serverBaseUrl, path34, token, body, options) {
36151
36794
  let response;
36152
36795
  try {
36153
- response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path33}`, {
36796
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path34}`, {
36154
36797
  method: "PATCH",
36155
36798
  headers: {
36156
36799
  accept: "application/json",
@@ -36198,10 +36841,10 @@ function createPairingNetworkError(input) {
36198
36841
  );
36199
36842
  }
36200
36843
  function pairingClaimPath(sessionId, paths) {
36201
- return path32.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
36844
+ return path33.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
36202
36845
  }
36203
36846
  function pairingSessionPath(sessionId, paths) {
36204
- return path32.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
36847
+ return path33.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
36205
36848
  }
36206
36849
  function qrPreferredUrls(routes) {
36207
36850
  return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
@@ -36272,6 +36915,7 @@ function registerSystemRoutes(router, options) {
36272
36915
  conversation_queue_limit: MAX_CONVERSATION_QUEUED_RUNS,
36273
36916
  responses_interrupted_previous_response: true,
36274
36917
  conversation_rename: true,
36918
+ conversation_workspaces: true,
36275
36919
  blobs: true,
36276
36920
  devices: true,
36277
36921
  device_delete: true,
@@ -36758,6 +37402,51 @@ function readDeviceModelHeader(ctx) {
36758
37402
  return value ? value.slice(0, 128) : null;
36759
37403
  }
36760
37404
 
37405
+ // src/http/routes/workspaces.ts
37406
+ function registerWorkspaceRoutes(router, options) {
37407
+ const { paths, conversations } = options;
37408
+ router.get("/api/v1/workspaces", async (ctx) => {
37409
+ await authenticateRequest(ctx, paths);
37410
+ ctx.set("cache-control", "no-store");
37411
+ ctx.body = {
37412
+ ok: true,
37413
+ workspaces: await conversations.listWorkspaces()
37414
+ };
37415
+ });
37416
+ router.post("/api/v1/workspaces", async (ctx) => {
37417
+ await authenticateRequest(ctx, paths);
37418
+ const body = await readJsonBody(ctx.req);
37419
+ ctx.status = 201;
37420
+ ctx.body = {
37421
+ ok: true,
37422
+ workspace: await conversations.createWorkspace({
37423
+ name: readString19(body, "name") ?? "",
37424
+ icon: readString19(body, "icon") ?? void 0
37425
+ })
37426
+ };
37427
+ });
37428
+ router.patch("/api/v1/workspaces/:workspaceId", async (ctx) => {
37429
+ await authenticateRequest(ctx, paths);
37430
+ const body = await readJsonBody(ctx.req);
37431
+ ctx.body = {
37432
+ ok: true,
37433
+ workspace: await conversations.renameWorkspace(ctx.params.workspaceId, {
37434
+ name: readString19(body, "name") ?? "",
37435
+ icon: readString19(body, "icon") ?? void 0
37436
+ })
37437
+ };
37438
+ });
37439
+ router.delete("/api/v1/workspaces/:workspaceId", async (ctx) => {
37440
+ await authenticateRequest(ctx, paths);
37441
+ const result = await conversations.deleteWorkspace(ctx.params.workspaceId);
37442
+ ctx.body = {
37443
+ ok: true,
37444
+ workspace: result.workspace,
37445
+ moved_conversation_count: result.movedConversationCount
37446
+ };
37447
+ });
37448
+ }
37449
+
36761
37450
  // src/http/routes/hermes-updates.ts
36762
37451
  function registerHermesUpdateRoutes(router, options) {
36763
37452
  const { paths, logger } = options;
@@ -37447,6 +38136,7 @@ async function createApp(options = {}) {
37447
38136
  conversations,
37448
38137
  syncCronDeliveries
37449
38138
  });
38139
+ registerWorkspaceRoutes(router, { paths, conversations });
37450
38140
  registerConversationRoutes(router, { paths, logger, conversations });
37451
38141
  registerRunRoutes(router, { paths, logger, conversations });
37452
38142
  registerProfileRoutes(router, { paths, logger, conversations });