agent-relay-server 0.26.0 → 0.27.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-relay-server",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Lightweight HTTP message relay for inter-agent communication across machines",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -33,7 +33,7 @@
33
33
  "CONTRIBUTING.md"
34
34
  ],
35
35
  "dependencies": {
36
- "agent-relay-sdk": "0.2.15"
36
+ "agent-relay-sdk": "0.2.16"
37
37
  },
38
38
  "scripts": {
39
39
  "prepack": "bun run build:dashboard:bundle >&2",
package/public/index.html CHANGED
@@ -11806,36 +11806,36 @@ function downloadText(filename, text, type) {
11806
11806
  }
11807
11807
  //#endregion
11808
11808
  //#region ../sdk/src/provider-catalog.ts
11809
- var CLAUDE_LOW_TO_MAX = [
11809
+ var CLAUDE_LOW_TO_MAX$1 = [
11810
11810
  "low",
11811
11811
  "medium",
11812
11812
  "high",
11813
11813
  "max"
11814
11814
  ];
11815
- var CLAUDE_LOW_TO_XHIGH_MAX = [
11815
+ var CLAUDE_LOW_TO_XHIGH_MAX$1 = [
11816
11816
  "low",
11817
11817
  "medium",
11818
11818
  "high",
11819
11819
  "xhigh",
11820
11820
  "max"
11821
11821
  ];
11822
- var CODEX_REASONING = [
11822
+ var CODEX_REASONING$1 = [
11823
11823
  "low",
11824
11824
  "medium",
11825
11825
  "high",
11826
11826
  "xhigh"
11827
11827
  ];
11828
- var CONTEXT_200K = {
11828
+ var CONTEXT_200K$1 = {
11829
11829
  value: 2e5,
11830
11830
  source: "catalog",
11831
11831
  confidence: "declared"
11832
11832
  };
11833
- var CONTEXT_1M = {
11833
+ var CONTEXT_1M$1 = {
11834
11834
  value: 1e6,
11835
11835
  source: "catalog",
11836
11836
  confidence: "declared"
11837
11837
  };
11838
- var CODE_MODEL_CAPABILITIES = {
11838
+ var CODE_MODEL_CAPABILITIES$1 = {
11839
11839
  modalities: {
11840
11840
  input: {
11841
11841
  text: true,
@@ -11852,88 +11852,96 @@ var CODE_MODEL_CAPABILITIES = {
11852
11852
  source: "catalog",
11853
11853
  confidence: "declared"
11854
11854
  };
11855
- function codeModel(limits) {
11855
+ function codeModel$1(limits) {
11856
11856
  return {
11857
11857
  ...limits ? { limits } : {},
11858
- capabilities: CODE_MODEL_CAPABILITIES
11858
+ capabilities: CODE_MODEL_CAPABILITIES$1
11859
11859
  };
11860
11860
  }
11861
- var PROVIDER_CATALOG = {
11861
+ var PROVIDER_CATALOG$1 = {
11862
11862
  claude: {
11863
11863
  provider: "claude",
11864
11864
  label: "Claude Code",
11865
11865
  defaultModel: "sonnet-4.6",
11866
11866
  models: [
11867
+ {
11868
+ alias: "fable-5",
11869
+ label: "Fable 5",
11870
+ providerModel: "claude-fable-5",
11871
+ efforts: CLAUDE_LOW_TO_XHIGH_MAX$1,
11872
+ defaultEffort: "medium",
11873
+ ...codeModel$1({ contextWindowTokens: CONTEXT_1M$1 })
11874
+ },
11867
11875
  {
11868
11876
  alias: "opus-4.8",
11869
11877
  label: "Opus 4.8",
11870
11878
  providerModel: "claude-opus-4-8",
11871
- efforts: CLAUDE_LOW_TO_XHIGH_MAX,
11879
+ efforts: CLAUDE_LOW_TO_XHIGH_MAX$1,
11872
11880
  defaultEffort: "medium",
11873
- ...codeModel({ contextWindowTokens: CONTEXT_200K })
11881
+ ...codeModel$1({ contextWindowTokens: CONTEXT_200K$1 })
11874
11882
  },
11875
11883
  {
11876
11884
  alias: "opus-4.8[1m]",
11877
11885
  label: "Opus 4.8 1M",
11878
11886
  providerModel: "claude-opus-4-8[1m]",
11879
- efforts: CLAUDE_LOW_TO_XHIGH_MAX,
11887
+ efforts: CLAUDE_LOW_TO_XHIGH_MAX$1,
11880
11888
  defaultEffort: "medium",
11881
- ...codeModel({ contextWindowTokens: CONTEXT_1M })
11889
+ ...codeModel$1({ contextWindowTokens: CONTEXT_1M$1 })
11882
11890
  },
11883
11891
  {
11884
11892
  alias: "opus-4.7",
11885
11893
  label: "Opus 4.7",
11886
11894
  providerModel: "claude-opus-4-7",
11887
- efforts: CLAUDE_LOW_TO_XHIGH_MAX,
11895
+ efforts: CLAUDE_LOW_TO_XHIGH_MAX$1,
11888
11896
  defaultEffort: "medium",
11889
- ...codeModel({ contextWindowTokens: CONTEXT_200K })
11897
+ ...codeModel$1({ contextWindowTokens: CONTEXT_200K$1 })
11890
11898
  },
11891
11899
  {
11892
11900
  alias: "opus-4.7[1m]",
11893
11901
  label: "Opus 4.7 1M",
11894
11902
  providerModel: "claude-opus-4-7[1m]",
11895
- efforts: CLAUDE_LOW_TO_XHIGH_MAX,
11903
+ efforts: CLAUDE_LOW_TO_XHIGH_MAX$1,
11896
11904
  defaultEffort: "medium",
11897
- ...codeModel({ contextWindowTokens: CONTEXT_1M })
11905
+ ...codeModel$1({ contextWindowTokens: CONTEXT_1M$1 })
11898
11906
  },
11899
11907
  {
11900
11908
  alias: "opus-4.6",
11901
11909
  label: "Opus 4.6",
11902
11910
  providerModel: "claude-opus-4-6",
11903
- efforts: CLAUDE_LOW_TO_MAX,
11911
+ efforts: CLAUDE_LOW_TO_MAX$1,
11904
11912
  defaultEffort: "medium",
11905
- ...codeModel({ contextWindowTokens: CONTEXT_200K })
11913
+ ...codeModel$1({ contextWindowTokens: CONTEXT_200K$1 })
11906
11914
  },
11907
11915
  {
11908
11916
  alias: "opus-4.6[1m]",
11909
11917
  label: "Opus 4.6 1M",
11910
11918
  providerModel: "claude-opus-4-6[1m]",
11911
- efforts: CLAUDE_LOW_TO_MAX,
11919
+ efforts: CLAUDE_LOW_TO_MAX$1,
11912
11920
  defaultEffort: "medium",
11913
- ...codeModel({ contextWindowTokens: CONTEXT_1M })
11921
+ ...codeModel$1({ contextWindowTokens: CONTEXT_1M$1 })
11914
11922
  },
11915
11923
  {
11916
11924
  alias: "sonnet-4.6",
11917
11925
  label: "Sonnet 4.6",
11918
11926
  providerModel: "claude-sonnet-4-6",
11919
- efforts: CLAUDE_LOW_TO_MAX,
11927
+ efforts: CLAUDE_LOW_TO_MAX$1,
11920
11928
  defaultEffort: "medium",
11921
- ...codeModel({ contextWindowTokens: CONTEXT_200K })
11929
+ ...codeModel$1({ contextWindowTokens: CONTEXT_200K$1 })
11922
11930
  },
11923
11931
  {
11924
11932
  alias: "sonnet-4.6[1m]",
11925
11933
  label: "Sonnet 4.6 1M",
11926
11934
  providerModel: "claude-sonnet-4-6[1m]",
11927
- efforts: CLAUDE_LOW_TO_MAX,
11935
+ efforts: CLAUDE_LOW_TO_MAX$1,
11928
11936
  defaultEffort: "medium",
11929
- ...codeModel({ contextWindowTokens: CONTEXT_1M })
11937
+ ...codeModel$1({ contextWindowTokens: CONTEXT_1M$1 })
11930
11938
  },
11931
11939
  {
11932
11940
  alias: "haiku",
11933
11941
  label: "Haiku",
11934
11942
  providerModel: "haiku",
11935
11943
  efforts: [],
11936
- ...codeModel()
11944
+ ...codeModel$1()
11937
11945
  }
11938
11946
  ]
11939
11947
  },
@@ -11946,49 +11954,49 @@ var PROVIDER_CATALOG = {
11946
11954
  alias: "gpt-5.5",
11947
11955
  label: "GPT-5.5",
11948
11956
  providerModel: "gpt-5.5",
11949
- efforts: CODEX_REASONING,
11957
+ efforts: CODEX_REASONING$1,
11950
11958
  defaultEffort: "medium",
11951
- ...codeModel({ contextWindowTokens: CONTEXT_200K })
11959
+ ...codeModel$1({ contextWindowTokens: CONTEXT_200K$1 })
11952
11960
  },
11953
11961
  {
11954
11962
  alias: "gpt-5.4",
11955
11963
  label: "GPT-5.4",
11956
11964
  providerModel: "gpt-5.4",
11957
- efforts: CODEX_REASONING,
11965
+ efforts: CODEX_REASONING$1,
11958
11966
  defaultEffort: "medium",
11959
- ...codeModel({ contextWindowTokens: CONTEXT_200K })
11967
+ ...codeModel$1({ contextWindowTokens: CONTEXT_200K$1 })
11960
11968
  },
11961
11969
  {
11962
11970
  alias: "gpt-5.4-mini",
11963
11971
  label: "GPT-5.4 Mini",
11964
11972
  providerModel: "gpt-5.4-mini",
11965
- efforts: CODEX_REASONING,
11973
+ efforts: CODEX_REASONING$1,
11966
11974
  defaultEffort: "medium",
11967
- ...codeModel({ contextWindowTokens: CONTEXT_200K })
11975
+ ...codeModel$1({ contextWindowTokens: CONTEXT_200K$1 })
11968
11976
  },
11969
11977
  {
11970
11978
  alias: "gpt-5.3-codex",
11971
11979
  label: "GPT-5.3 Codex",
11972
11980
  providerModel: "gpt-5.3-codex",
11973
- efforts: CODEX_REASONING,
11981
+ efforts: CODEX_REASONING$1,
11974
11982
  defaultEffort: "medium",
11975
- ...codeModel({ contextWindowTokens: CONTEXT_200K })
11983
+ ...codeModel$1({ contextWindowTokens: CONTEXT_200K$1 })
11976
11984
  },
11977
11985
  {
11978
11986
  alias: "gpt-5.3-codex-spark",
11979
11987
  label: "GPT-5.3 Codex Spark",
11980
11988
  providerModel: "gpt-5.3-codex-spark",
11981
- efforts: CODEX_REASONING,
11989
+ efforts: CODEX_REASONING$1,
11982
11990
  defaultEffort: "high",
11983
- ...codeModel({ contextWindowTokens: CONTEXT_200K })
11991
+ ...codeModel$1({ contextWindowTokens: CONTEXT_200K$1 })
11984
11992
  },
11985
11993
  {
11986
11994
  alias: "gpt-5.2",
11987
11995
  label: "GPT-5.2",
11988
11996
  providerModel: "gpt-5.2",
11989
- efforts: CODEX_REASONING,
11997
+ efforts: CODEX_REASONING$1,
11990
11998
  defaultEffort: "medium",
11991
- ...codeModel({ contextWindowTokens: CONTEXT_200K })
11999
+ ...codeModel$1({ contextWindowTokens: CONTEXT_200K$1 })
11992
12000
  }
11993
12001
  ]
11994
12002
  }
@@ -11999,7 +12007,7 @@ function firstAvailableProvider(orchestrator) {
11999
12007
  return orchestrator?.providers[0] ?? "claude";
12000
12008
  }
12001
12009
  function defaultModelFor(provider) {
12002
- return PROVIDER_CATALOG[provider]?.defaultModel ?? "";
12010
+ return PROVIDER_CATALOG$1[provider]?.defaultModel ?? "";
12003
12011
  }
12004
12012
  function pathWithinBase(path, baseDir) {
12005
12013
  const value = path.trim().replace(/\\/g, "/").replace(/\/+$/, "");
@@ -13876,7 +13884,7 @@ var useRelayStore = create$1()(persist((set, get) => ({
13876
13884
  const provCaps = agent.providerCapabilities;
13877
13885
  const provider = provCaps?.model?.provider || (typeof agent.meta?.provider === "string" ? agent.meta.provider : "");
13878
13886
  const reportedModel = provCaps?.model?.alias || provCaps?.model?.id || "";
13879
- const model = ((provider ? PROVIDER_CATALOG[provider] : void 0)?.models.find((m) => m.alias === reportedModel || m.providerModel === reportedModel))?.alias || "";
13887
+ const model = ((provider ? PROVIDER_CATALOG$1[provider] : void 0)?.models.find((m) => m.alias === reportedModel || m.providerModel === reportedModel))?.alias || "";
13880
13888
  const effort = provCaps?.model?.effort || "";
13881
13889
  const cwd = typeof agent.meta?.cwd === "string" ? agent.meta.cwd : "";
13882
13890
  const approval = provCaps?.session?.approvalMode || (typeof agent.meta?.approvalMode === "string" ? agent.meta.approvalMode : "guarded");
@@ -91948,10 +91956,10 @@ function wrap(middleware, callback) {
91948
91956
  //#endregion
91949
91957
  //#region node_modules/vfile/lib/minpath.browser.js
91950
91958
  var minpath = {
91951
- basename: basename$2,
91952
- dirname,
91959
+ basename: basename$3,
91960
+ dirname: dirname$1,
91953
91961
  extname,
91954
- join,
91962
+ join: join$1,
91955
91963
  sep: "/"
91956
91964
  };
91957
91965
  /**
@@ -91964,7 +91972,7 @@ var minpath = {
91964
91972
  * @returns {string}
91965
91973
  * Stem or basename.
91966
91974
  */
91967
- function basename$2(path, extname) {
91975
+ function basename$3(path, extname) {
91968
91976
  if (extname !== void 0 && typeof extname !== "string") throw new TypeError("\"ext\" argument must be a string");
91969
91977
  assertPath$1(path);
91970
91978
  let start = 0;
@@ -92016,7 +92024,7 @@ function basename$2(path, extname) {
92016
92024
  * @returns {string}
92017
92025
  * File path.
92018
92026
  */
92019
- function dirname(path) {
92027
+ function dirname$1(path) {
92020
92028
  assertPath$1(path);
92021
92029
  if (path.length === 0) return ".";
92022
92030
  let end = -1;
@@ -92077,7 +92085,7 @@ function extname(path) {
92077
92085
  * @returns {string}
92078
92086
  * File path.
92079
92087
  */
92080
- function join(...segments) {
92088
+ function join$1(...segments) {
92081
92089
  let index = -1;
92082
92090
  /** @type {string | undefined} */
92083
92091
  let joined;
@@ -101370,10 +101378,10 @@ function mergeObjects(target, ...sources) {
101370
101378
  });
101371
101379
  return target;
101372
101380
  }
101373
- function basename$1(path) {
101381
+ function basename$2(path) {
101374
101382
  const idx = ~path.lastIndexOf("/") || ~path.lastIndexOf("\\");
101375
101383
  if (idx === 0) return path;
101376
- else if (~idx === path.length - 1) return basename$1(path.substring(0, path.length - 1));
101384
+ else if (~idx === path.length - 1) return basename$2(path.substring(0, path.length - 1));
101377
101385
  else return path.substr(~idx + 1);
101378
101386
  }
101379
101387
  function strcmp(a, b) {
@@ -102426,7 +102434,7 @@ var init_dist$2 = __esmMin((() => {
102426
102434
  this._contentNameIsCapturing = RegexSource.hasCaptures(this._contentName);
102427
102435
  }
102428
102436
  get debugName() {
102429
- const location = this.$location ? `${basename$1(this.$location.filename)}:${this.$location.line}` : "unknown";
102437
+ const location = this.$location ? `${basename$2(this.$location.filename)}:${this.$location.line}` : "unknown";
102430
102438
  return `${this.constructor.name}#${this.id} @ ${location}`;
102431
102439
  }
102432
102440
  getName(lineText, captureIndices) {
@@ -108590,7 +108598,7 @@ function getShiki() {
108590
108598
  function byteLength(value) {
108591
108599
  return new TextEncoder().encode(value).length;
108592
108600
  }
108593
- function basename(path) {
108601
+ function basename$1(path) {
108594
108602
  return path.split("/").filter(Boolean).pop() || path;
108595
108603
  }
108596
108604
  function detectShebangLanguage(content) {
@@ -108609,7 +108617,7 @@ function detectShebangLanguage(content) {
108609
108617
  }
108610
108618
  function detectCodeLanguage(path, mediaType, languageHint, content) {
108611
108619
  if (languageHint) return languageHint;
108612
- const name = basename(path).toLowerCase();
108620
+ const name = basename$1(path).toLowerCase();
108613
108621
  if (BASENAME_LANGUAGES[name]) return BASENAME_LANGUAGES[name];
108614
108622
  if (name.endsWith(".d.ts")) return "typescript";
108615
108623
  const shebangLanguage = detectShebangLanguage(content);
@@ -129418,6 +129426,51 @@ function OrchestratorsView() {
129418
129426
  });
129419
129427
  }
129420
129428
  //#endregion
129429
+ //#region ../sdk/dist/types.js
129430
+ /** Terminal workspace statuses shared across server + dashboard filters. */
129431
+ var TERMINAL_WORKSPACE_STATUS_VALUES = [
129432
+ "cleaned",
129433
+ "merged",
129434
+ "abandoned"
129435
+ ];
129436
+ var CONTEXT_200K = {
129437
+ value: 2e5,
129438
+ source: "catalog",
129439
+ confidence: "declared"
129440
+ };
129441
+ var CONTEXT_1M = {
129442
+ value: 1e6,
129443
+ source: "catalog",
129444
+ confidence: "declared"
129445
+ };
129446
+ var CODE_MODEL_CAPABILITIES = {
129447
+ modalities: {
129448
+ input: {
129449
+ text: true,
129450
+ image: true
129451
+ },
129452
+ output: { text: true }
129453
+ },
129454
+ tools: {
129455
+ code: true,
129456
+ review: true,
129457
+ debug: true,
129458
+ refactor: true
129459
+ },
129460
+ source: "catalog",
129461
+ confidence: "declared"
129462
+ };
129463
+ function codeModel(limits) {
129464
+ return {
129465
+ ...limits ? { limits } : {},
129466
+ capabilities: CODE_MODEL_CAPABILITIES
129467
+ };
129468
+ }
129469
+ ({ ...codeModel({ contextWindowTokens: CONTEXT_1M }) }), { ...codeModel({ contextWindowTokens: CONTEXT_200K }) }, { ...codeModel({ contextWindowTokens: CONTEXT_1M }) }, { ...codeModel({ contextWindowTokens: CONTEXT_200K }) }, { ...codeModel({ contextWindowTokens: CONTEXT_1M }) }, { ...codeModel({ contextWindowTokens: CONTEXT_200K }) }, { ...codeModel({ contextWindowTokens: CONTEXT_1M }) }, { ...codeModel({ contextWindowTokens: CONTEXT_200K }) }, { ...codeModel({ contextWindowTokens: CONTEXT_1M }) }, { ...codeModel() }, { ...codeModel({ contextWindowTokens: CONTEXT_200K }) }, { ...codeModel({ contextWindowTokens: CONTEXT_200K }) }, { ...codeModel({ contextWindowTokens: CONTEXT_200K }) }, { ...codeModel({ contextWindowTokens: CONTEXT_200K }) }, { ...codeModel({ contextWindowTokens: CONTEXT_200K }) }, { ...codeModel({ contextWindowTokens: CONTEXT_200K }) };
129470
+ (0, (/* @__PURE__ */ __commonJSMin(((exports, module) => {
129471
+ module.exports = {};
129472
+ })))().tmpdir)();
129473
+ //#endregion
129421
129474
  //#region src/components/views/workspaces.tsx
129422
129475
  var LIVE_STATUSES = new Set([
129423
129476
  "active",
@@ -129474,7 +129527,7 @@ var STATUS_ORDER = {
129474
129527
  abandoned: 7,
129475
129528
  cleaned: 8
129476
129529
  };
129477
- var TERMINAL_STATUSES = new Set(["cleaned", "merged"]);
129530
+ var TERMINAL_STATUSES = new Set(TERMINAL_WORKSPACE_STATUS_VALUES);
129478
129531
  function shortPath(path) {
129479
129532
  const parts = path.split("/").filter(Boolean);
129480
129533
  if (parts.length <= 3) return path || "-";
@@ -133144,7 +133197,7 @@ function blankForm(orchestratorId = "") {
133144
133197
  orchestratorId,
133145
133198
  targetMode: "existing_agent",
133146
133199
  provider: "codex",
133147
- model: PROVIDER_CATALOG.codex.defaultModel || "",
133200
+ model: PROVIDER_CATALOG$1.codex.defaultModel || "",
133148
133201
  effort: "",
133149
133202
  profile: "default-relay",
133150
133203
  label: "",
@@ -133183,7 +133236,7 @@ function automationToForm(automation) {
133183
133236
  orchestratorId: automation.orchestratorId,
133184
133237
  targetMode: policy.mode,
133185
133238
  provider: policy.mode === "on_demand_agent" ? policy.provider : policy.selector.provider || "codex",
133186
- model: policy.mode === "on_demand_agent" ? policy.model || PROVIDER_CATALOG[policy.provider].defaultModel || "" : "",
133239
+ model: policy.mode === "on_demand_agent" ? policy.model || PROVIDER_CATALOG$1[policy.provider].defaultModel || "" : "",
133187
133240
  effort: policy.mode === "on_demand_agent" ? policy.effort || "" : "",
133188
133241
  profile: policy.mode === "on_demand_agent" ? policy.profile || "default-relay" : "default-relay",
133189
133242
  label: policy.mode === "existing_agent" ? policy.selector.label || "" : "",
@@ -133593,21 +133646,21 @@ function AutomationCard({ automation, runs, now, selected, onEdit, onRun, onTogg
133593
133646
  }
133594
133647
  function AutomationEditor({ form, selected, saving, orchestrators, agentProfiles, onChange, onSave }) {
133595
133648
  const providers = orchestrators.find((orch) => orch.id === form.orchestratorId)?.providers || [];
133596
- const models = PROVIDER_CATALOG[form.provider]?.models || [];
133649
+ const models = PROVIDER_CATALOG$1[form.provider]?.models || [];
133597
133650
  const efforts = models.find((model) => model.alias === form.model)?.efforts || [];
133598
133651
  function selectOrchestrator(orchestratorId) {
133599
133652
  const provider = orchestrators.find((orch) => orch.id === orchestratorId)?.providers[0] || form.provider;
133600
133653
  onChange({
133601
133654
  orchestratorId,
133602
133655
  provider,
133603
- model: PROVIDER_CATALOG[provider]?.defaultModel || "",
133656
+ model: PROVIDER_CATALOG$1[provider]?.defaultModel || "",
133604
133657
  effort: ""
133605
133658
  });
133606
133659
  }
133607
133660
  function selectProvider(provider) {
133608
133661
  onChange({
133609
133662
  provider,
133610
- model: PROVIDER_CATALOG[provider]?.defaultModel || "",
133663
+ model: PROVIDER_CATALOG$1[provider]?.defaultModel || "",
133611
133664
  effort: ""
133612
133665
  });
133613
133666
  }
@@ -157363,7 +157416,7 @@ function OrchestratorSpawnModal() {
157363
157416
  const agentProfiles = useRelayStore((s) => s.agentProfiles);
157364
157417
  const orch = orchestrators.find((o) => o.id === orchId);
157365
157418
  const providers = orch?.providers || [];
157366
- const models = PROVIDER_CATALOG[provider]?.models || [];
157419
+ const models = PROVIDER_CATALOG$1[provider]?.models || [];
157367
157420
  const selectedModel = models.find((m) => m.alias === model);
157368
157421
  const efforts = selectedModel?.efforts || [];
157369
157422
  const canSpawn = Boolean(orch && providers.length > 0 && cwd.trim());
@@ -157373,7 +157426,7 @@ function OrchestratorSpawnModal() {
157373
157426
  set({
157374
157427
  spawnOrchId: nextId,
157375
157428
  spawnProvider: nextProvider,
157376
- spawnModel: nextProvider ? PROVIDER_CATALOG[nextProvider]?.defaultModel || "" : "",
157429
+ spawnModel: nextProvider ? PROVIDER_CATALOG$1[nextProvider]?.defaultModel || "" : "",
157377
157430
  spawnEffort: "",
157378
157431
  spawnCwd: nextOrch?.baseDir || ""
157379
157432
  });
@@ -157381,7 +157434,7 @@ function OrchestratorSpawnModal() {
157381
157434
  function selectProvider(nextProvider) {
157382
157435
  set({
157383
157436
  spawnProvider: nextProvider,
157384
- spawnModel: PROVIDER_CATALOG[nextProvider]?.defaultModel || "",
157437
+ spawnModel: PROVIDER_CATALOG$1[nextProvider]?.defaultModel || "",
157385
157438
  spawnEffort: ""
157386
157439
  });
157387
157440
  }
@@ -157710,7 +157763,7 @@ function ManagedPolicyModal() {
157710
157763
  const submitPolicy = useRelayStore((s) => s.submitPolicy);
157711
157764
  const orch = orchestrators.find((o) => o.id === m.orchestratorId);
157712
157765
  const providers = orch?.providers || [];
157713
- const models = PROVIDER_CATALOG[m.provider]?.models || [];
157766
+ const models = PROVIDER_CATALOG$1[m.provider]?.models || [];
157714
157767
  const selectedModel = models.find((model) => model.alias === m.model);
157715
157768
  const efforts = selectedModel?.efforts || [];
157716
157769
  const isEditing = Boolean(m.editing);
@@ -157726,7 +157779,7 @@ function ManagedPolicyModal() {
157726
157779
  update({
157727
157780
  orchestratorId,
157728
157781
  provider,
157729
- model: PROVIDER_CATALOG[provider]?.defaultModel || "",
157782
+ model: PROVIDER_CATALOG$1[provider]?.defaultModel || "",
157730
157783
  effort: ""
157731
157784
  });
157732
157785
  }
@@ -157734,7 +157787,7 @@ function ManagedPolicyModal() {
157734
157787
  update({
157735
157788
  provider,
157736
157789
  rig: "",
157737
- model: PROVIDER_CATALOG[provider]?.defaultModel || "",
157790
+ model: PROVIDER_CATALOG$1[provider]?.defaultModel || "",
157738
157791
  effort: ""
157739
157792
  });
157740
157793
  }
package/src/routes.ts CHANGED
@@ -167,7 +167,7 @@ import { errMessage, isRecord, SPAWN_PROVIDERS, VALID_WORKSPACE_MODES, VALID_EFF
167
167
  import { effectiveProviderCatalogList } from "./provider-catalog-store";
168
168
  import { buildManagedSpawnParams, effectiveManagedPolicyWorkspaceMode } from "./managed-policy";
169
169
  import { buildSpawnCommand, generateSpawnRequestId, resolveSpawnModelParams, type SpawnModelParams } from "./spawn-command";
170
- import { requestWorkspaceMerge } from "./workspace-merge";
170
+ import { isOwnerAlive, withOwnerOnline } from "./workspace-merge";
171
171
  import { claimMetadataPatch, workspaceActiveClaim } from "./workspace-claim";
172
172
  import {
173
173
  applyWorkspaceAction,
@@ -3813,7 +3813,7 @@ const getWorkspaces: Handler = (req) => {
3813
3813
  const repoRoot = cleanString(url.searchParams.get("repoRoot") ?? undefined, "repoRoot", { max: 1000 });
3814
3814
  const ownerAgentId = cleanString(url.searchParams.get("agentId") ?? undefined, "agentId", { max: 240 });
3815
3815
  const status = optionalEnum(url.searchParams.get("status") ?? undefined, "status", VALID_WORKSPACE_STATUSES) as WorkspaceStatus | undefined;
3816
- return json(listWorkspaces({ repoRoot, ownerAgentId, status }));
3816
+ return json(listWorkspaces({ repoRoot, ownerAgentId, status }).map(withOwnerOnline));
3817
3817
  } catch (e) {
3818
3818
  if (e instanceof ValidationError) return error(e.message, 400);
3819
3819
  throw e;
@@ -3823,7 +3823,7 @@ const getWorkspaces: Handler = (req) => {
3823
3823
  const getWorkspaceById: Handler = (_req, params) => {
3824
3824
  const workspace = getWorkspace(params.id!);
3825
3825
  if (!workspace) return error("workspace not found", 404);
3826
- return json(workspace);
3826
+ return json(withOwnerOnline(workspace));
3827
3827
  };
3828
3828
 
3829
3829
  // Per-repo coordination state: persistent steward records (survive offline gaps)
@@ -4006,7 +4006,7 @@ const getWorkspaceDiagnostics: Handler = async (_req, params) => {
4006
4006
  const workspace = getWorkspace(params.id!);
4007
4007
  if (!workspace) return error("workspace not found", 404);
4008
4008
  const owner = workspace.ownerAgentId ? getAgent(workspace.ownerAgentId) : null;
4009
- const ownerOnline = Boolean(owner) && owner!.status !== "offline";
4009
+ const ownerOnline = isOwnerAlive(workspace.ownerAgentId);
4010
4010
  const orch = listOrchestrators().find((candidate) => isPathWithinBase(workspace.sourceCwd, candidate.baseDir));
4011
4011
  const orchOnline = Boolean(orch) && orch!.status === "online";
4012
4012
  const fetched = await fetchWorkspaceGitState(workspace);
@@ -4055,7 +4055,7 @@ const postWorkspaceCleanupStale: Handler = async (req) => {
4055
4055
  const cleaned: string[] = [];
4056
4056
  for (const ws of candidates) {
4057
4057
  const owner = ws.ownerAgentId ? getAgent(ws.ownerAgentId) : null;
4058
- const ownerOnline = Boolean(owner) && owner!.status !== "offline";
4058
+ const ownerOnline = isOwnerAlive(ws.ownerAgentId);
4059
4059
  if (ownerOnline) continue; // never clean a live owner's worktree
4060
4060
  if (offlineOwnerOnly && !ws.ownerAgentId) { /* no owner recorded — still eligible */ }
4061
4061
  if (workspaceActiveClaim(ws)) continue; // respect steward claims
@@ -4117,7 +4117,7 @@ const postWorkspaceAction: Handler = async (req, params) => {
4117
4117
  // plus the directive projection + land receipt so the CLI `--wait` and any
4118
4118
  // HTTP caller get the same legible answer.
4119
4119
  return json({
4120
- workspace: waited.workspace,
4120
+ workspace: withOwnerOnline(waited.workspace),
4121
4121
  guidance: describeWorkspacePhase(waited.workspace),
4122
4122
  ...(landed ? { landed } : {}),
4123
4123
  fromStatus: waited.fromStatus,
@@ -4125,7 +4125,7 @@ const postWorkspaceAction: Handler = async (req, params) => {
4125
4125
  timedOut: waited.timedOut,
4126
4126
  });
4127
4127
  }
4128
- return json(workspace);
4128
+ return json(withOwnerOnline(workspace));
4129
4129
  }
4130
4130
 
4131
4131
  // Everything else delegates to the shared core (one home, shared with the
@@ -4138,6 +4138,7 @@ const postWorkspaceAction: Handler = async (req, params) => {
4138
4138
  metadata: cleanMeta(parsed.body.metadata) ?? {},
4139
4139
  strategy: optionalEnum(parsed.body.strategy, "strategy", ["pr", "rebase-ff", "auto"] as const, "auto") as WorkspaceMergeStrategy,
4140
4140
  deleteBranch: typeof parsed.body.deleteBranch === "boolean" ? parsed.body.deleteBranch : undefined,
4141
+ force: parsed.body.force === true,
4141
4142
  prTitle: cleanString(parsed.body.prTitle, "prTitle", { max: 240 }),
4142
4143
  prBody: cleanString(parsed.body.prBody, "prBody", { max: 8000 }),
4143
4144
  purpose: cleanString(parsed.body.purpose, "purpose", { max: 120 }),
@@ -4146,7 +4147,7 @@ const postWorkspaceAction: Handler = async (req, params) => {
4146
4147
  });
4147
4148
  if (!result.ok) return error(result.error, result.httpStatus);
4148
4149
  if (result.command) emitCommand(result.command);
4149
- const payload: Record<string, unknown> = { workspace: result.workspace };
4150
+ const payload: Record<string, unknown> = { workspace: withOwnerOnline(result.workspace) };
4150
4151
  if (result.command) payload.command = result.command;
4151
4152
  if (result.claim !== undefined) payload.claim = result.claim;
4152
4153
  return json(payload, result.httpStatus);
@@ -19,8 +19,9 @@ import {
19
19
  updateWorkspaceStatus,
20
20
  } from "./db";
21
21
  import { emitActivityEvent } from "./sse";
22
- import { requestWorkspaceMerge } from "./workspace-merge";
22
+ import { isOwnerAlive, requestWorkspaceMerge } from "./workspace-merge";
23
23
  import { claimMetadataPatch, workspaceActiveClaim } from "./workspace-claim";
24
+ import { TERMINAL_WORKSPACE_STATUSES } from "./workspace-phase";
24
25
  import type { Command, WorkspaceMergeStrategy, WorkspaceRecord, WorkspaceStatus } from "./types";
25
26
 
26
27
  // Single source of truth for the action verb set. The route's `optionalEnum` and
@@ -50,6 +51,8 @@ export interface ApplyWorkspaceActionInput {
50
51
  deleteBranch?: boolean;
51
52
  prTitle?: string;
52
53
  prBody?: string;
54
+ // cleanup
55
+ force?: boolean;
53
56
  // claim / release-claim
54
57
  purpose?: string;
55
58
  // deps-refresh
@@ -203,6 +206,20 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
203
206
  const nextStatus = STATUS_BY_ACTION[action];
204
207
  if (!nextStatus) return { ok: false, httpStatus: 400, error: `unsupported action: ${action}` };
205
208
 
209
+ if (
210
+ action === "cleanup" &&
211
+ input.force !== true &&
212
+ workspace.mode === "isolated" &&
213
+ !TERMINAL_WORKSPACE_STATUSES.has(workspace.status) &&
214
+ isOwnerAlive(workspace.ownerAgentId)
215
+ ) {
216
+ return {
217
+ ok: false,
218
+ httpStatus: 409,
219
+ error: `workspace ${workspace.id} owner is still online; pass force:true to clean it up intentionally`,
220
+ };
221
+ }
222
+
206
223
  const updated = updateWorkspaceStatus(workspace.id, nextStatus, {
207
224
  ...metadata,
208
225
  ...(detail ? { detail } : {}),
@@ -215,7 +232,7 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
215
232
  let command: Command | undefined;
216
233
  if (requiresCommand) {
217
234
  // Only `cleanup` reaches here — `merge` returned early via the shared helper.
218
- const built = buildWorkspaceCleanupCommand(workspace, agentId ?? "dashboard");
235
+ const built = buildWorkspaceCleanupCommand(workspace, agentId ?? "dashboard", { force: input.force === true });
219
236
  if (!built.ok) return { ok: false, httpStatus: built.status, error: built.error };
220
237
  command = built.command;
221
238
  }
@@ -237,7 +254,20 @@ export function applyWorkspaceAction(workspace: WorkspaceRecord, input: ApplyWor
237
254
  export function buildWorkspaceCleanupCommand(
238
255
  workspace: WorkspaceRecord,
239
256
  requestedBy: string,
257
+ opts: { force?: boolean } = {},
240
258
  ): { ok: true; command: Command } | { ok: false; status: number; error: string } {
259
+ if (
260
+ opts.force !== true &&
261
+ workspace.mode === "isolated" &&
262
+ !TERMINAL_WORKSPACE_STATUSES.has(workspace.status) &&
263
+ isOwnerAlive(workspace.ownerAgentId)
264
+ ) {
265
+ return {
266
+ ok: false,
267
+ status: 409,
268
+ error: `workspace ${workspace.id} owner is still online; pass force:true to clean it up intentionally`,
269
+ };
270
+ }
241
271
  const owners = listOrchestrators().filter((candidate) => isPathWithinBase(workspace.sourceCwd, candidate.baseDir));
242
272
  const owner = owners.find((candidate) => candidate.status === "online") ?? owners[0];
243
273
  if (!owner) return { ok: false, status: 409, error: "no orchestrator owns this workspace path; use DELETE /api/workspaces/:id to purge the record" };
@@ -32,12 +32,16 @@ export type RequestWorkspaceMergeResult =
32
32
 
33
33
  // The owner is "alive" while its relay agent exists and isn't offline (online or
34
34
  // a borderline-stale disconnect both count — don't nuke a worktree on a blip).
35
- function isOwnerAlive(ownerAgentId: string | undefined): boolean {
35
+ export function isOwnerAlive(ownerAgentId: string | undefined): boolean {
36
36
  if (!ownerAgentId) return false;
37
37
  const agent = getAgent(ownerAgentId);
38
38
  return Boolean(agent) && agent!.status !== "offline";
39
39
  }
40
40
 
41
+ export function withOwnerOnline<T extends { ownerAgentId?: string }>(workspace: T): T & { ownerOnline: boolean } {
42
+ return { ...workspace, ownerOnline: isOwnerAlive(workspace.ownerAgentId) };
43
+ }
44
+
41
45
  /**
42
46
  * Dispatch a base merge for an isolated workspace, serialized by the per-repo
43
47
  * merge lease (issue #157). Single source of truth shared by the manual
@@ -12,13 +12,14 @@
12
12
  // prune` (a no-op while the directory exists) never was.
13
13
 
14
14
  import { resolve } from "node:path";
15
- import { RELAY_TOKEN_HEADER, errMessage } from "agent-relay-sdk";
16
- import type { WorkspaceMergePreview, WorkspaceOrphan, WorkspaceProbe, WorkspaceRecord } from "./types";
17
- import { createActivityEvent, listOrchestrators, listWorkspaces } from "./db";
15
+ import { RELAY_TOKEN_HEADER } from "agent-relay-sdk";
16
+ import type { WorkspaceMergePreview, WorkspaceOrphan, WorkspaceProbe, WorkspaceRecord, WorkspaceStatus } from "./types";
17
+ import { createActivityEvent, getWorkspace, listOrchestrators, listWorkspaces, updateWorkspaceStatus } from "./db";
18
18
  import { createCommand } from "./commands-db";
19
19
  import { emitRelayEvent } from "./events";
20
20
  import { isPathWithinBase } from "./utils";
21
21
  import { TERMINAL_WORKSPACE_STATUSES, worktreeReapable, type WorktreeReapState } from "./workspace-phase";
22
+ import { isOwnerAlive } from "./workspace-merge";
22
23
 
23
24
  // Don't re-flag the same un-landed orphan every sweep — surface it once, then
24
25
  // stay quiet for this window. In-memory (keyed by worktree path) like the
@@ -28,6 +29,7 @@ const UNLANDED_FLAG_COOLDOWN_MS = Number(process.env.AGENT_RELAY_ORPHAN_FLAG_COO
28
29
  // remove them (parity with the session reaper's detect-only switch).
29
30
  const orphanWorktreeReapEnabled = () => process.env.AGENT_RELAY_ORPHAN_WORKTREE_REAP !== "0";
30
31
  const flaggedAt = new Map<string, number>();
32
+ const IN_FLIGHT_MISSING_WORKTREE_STATUSES = new Set<WorkspaceStatus>(["merge_planned", "cleanup_requested"]);
31
33
 
32
34
  export function resetOrphanWorktreeStateForTests(): void {
33
35
  flaggedAt.clear();
@@ -77,6 +79,39 @@ async function fetchWorktreeReapState(apiUrl: string, worktreePath: string, base
77
79
  }
78
80
  }
79
81
 
82
+ type MissingBranchProbe = { kind: "gone" } | { kind: "preview"; preview: WorkspaceMergePreview } | { kind: "unavailable" };
83
+
84
+ async function fetchBranchReapState(
85
+ apiUrl: string,
86
+ repoRoot: string,
87
+ branch: string | undefined,
88
+ baseRef?: string,
89
+ baseSha?: string,
90
+ ): Promise<MissingBranchProbe> {
91
+ if (!branch) return { kind: "unavailable" };
92
+ const query = new URLSearchParams({ repoRoot, branch, checkPr: "1" });
93
+ if (baseRef) query.set("baseRef", baseRef);
94
+ if (baseSha) query.set("baseSha", baseSha);
95
+ try {
96
+ const res = await fetch(`${apiUrl}/api/workspace/branch-merge-preview?${query.toString()}`, {
97
+ headers: relayHeaders(),
98
+ signal: AbortSignal.timeout(8_000),
99
+ });
100
+ if (res.status === 404) return { kind: "gone" };
101
+ if (!res.ok) return { kind: "unavailable" };
102
+ return { kind: "preview", preview: await res.json() as WorkspaceMergePreview };
103
+ } catch {
104
+ return { kind: "unavailable" };
105
+ }
106
+ }
107
+
108
+ function previewReapable(preview: WorkspaceMergePreview): boolean | undefined {
109
+ if (preview.error) return undefined;
110
+ const hasSignal = preview.landed === true || typeof preview.ahead === "number" || typeof preview.unmergedAhead === "number";
111
+ if (!hasSignal) return undefined;
112
+ return worktreeReapable({ landed: preview.landed, ahead: preview.ahead, unmergedAhead: preview.unmergedAhead, dirtyCount: 0 });
113
+ }
114
+
80
115
  function onlineOrchestrators(): OnlineOrchestrator[] {
81
116
  return listOrchestrators()
82
117
  .filter((orch) => orch.status === "online" && orch.apiUrl && orch.agentId)
@@ -93,7 +128,16 @@ function knownRepoRoots(workspaces: WorkspaceRecord[]): string[] {
93
128
  export interface CollectOrphansResult {
94
129
  orphans: WorkspaceOrphan[];
95
130
  /** Live isolated rows whose worktree is missing on disk (DB→disk drift). */
96
- missingWorktrees: Array<{ workspaceId: string; worktreePath: string; repoRoot: string; status: string }>;
131
+ missingWorktrees: Array<{
132
+ workspaceId: string;
133
+ worktreePath: string;
134
+ repoRoot: string;
135
+ status: WorkspaceStatus;
136
+ branch?: string;
137
+ baseRef?: string;
138
+ baseSha?: string;
139
+ ownerAgentId?: string;
140
+ }>;
97
141
  reason?: string;
98
142
  }
99
143
 
@@ -128,7 +172,16 @@ export async function collectWorkspaceOrphans(): Promise<CollectOrphansResult> {
128
172
  // DB→disk drift: a live isolated row whose worktree is no longer on disk.
129
173
  for (const [path, ws] of liveRowsByPath) {
130
174
  if (ws.mode === "isolated" && !onDisk.has(path)) {
131
- missingWorktrees.push({ workspaceId: ws.id, worktreePath: ws.worktreePath, repoRoot, status: ws.status });
175
+ missingWorktrees.push({
176
+ workspaceId: ws.id,
177
+ worktreePath: ws.worktreePath,
178
+ repoRoot,
179
+ status: ws.status,
180
+ branch: ws.branch,
181
+ baseRef: ws.baseRef,
182
+ baseSha: ws.baseSha,
183
+ ownerAgentId: ws.ownerAgentId,
184
+ });
132
185
  }
133
186
  }
134
187
 
@@ -208,6 +261,8 @@ export async function reapOrphanedWorktrees(): Promise<Record<string, unknown>>
208
261
  const reapEnabled = orphanWorktreeReapEnabled();
209
262
  const reaped: string[] = [];
210
263
  const flagged: string[] = [];
264
+ const autoAbandoned: string[] = [];
265
+ const flaggedMissingWorktrees: string[] = [];
211
266
  const now = Date.now();
212
267
 
213
268
  for (const orphan of orphans) {
@@ -255,22 +310,103 @@ export async function reapOrphanedWorktrees(): Promise<Record<string, unknown>>
255
310
  });
256
311
  }
257
312
 
258
- // DB→disk drift is observability-only: a live row whose worktree vanished is
259
- // surfaced, not auto-deleted (the row may still be mid-land or recoverable).
260
313
  for (const missing of missingWorktrees) {
261
- const key = `missing:${missing.worktreePath}`;
314
+ const workspace = getWorkspace(missing.workspaceId);
315
+ if (!workspace || TERMINAL_WORKSPACE_STATUSES.has(workspace.status) || workspace.mode !== "isolated" || !workspace.worktreePath) continue;
316
+ const key = `missing:${workspace.worktreePath}`;
317
+ const ownerAlive = isOwnerAlive(workspace.ownerAgentId);
318
+ const inFlight = IN_FLIGHT_MISSING_WORKTREE_STATUSES.has(workspace.status);
262
319
  const last = flaggedAt.get(key) ?? 0;
320
+ if (ownerAlive || inFlight) {
321
+ if (now - last < UNLANDED_FLAG_COOLDOWN_MS) continue;
322
+ flaggedAt.set(key, now);
323
+ createActivityEvent({
324
+ clientId: `workspace-row-no-worktree-${workspace.id}-${now}`,
325
+ kind: "state",
326
+ title: "Workspace row has no worktree on disk",
327
+ body: `Workspace ${workspace.id} (${workspace.status}) points at ${workspace.worktreePath}, which no longer exists on disk — disk/DB drift.`,
328
+ meta: workspace.id,
329
+ icon: "ti-unlink",
330
+ view: "orchestrators",
331
+ metadata: {
332
+ source: "server",
333
+ maintenanceJobId: "workspace-orphan-reaper",
334
+ workspaceId: workspace.id,
335
+ worktreePath: workspace.worktreePath,
336
+ status: workspace.status,
337
+ ownerAlive,
338
+ inFlight,
339
+ },
340
+ });
341
+ continue;
342
+ }
343
+
344
+ const orch = orchestrators.find((candidate) => candidate.apiUrl && isPathWithinBase(workspace.repoRoot, candidate.baseDir));
345
+ const probe = orch?.apiUrl
346
+ ? await fetchBranchReapState(orch.apiUrl, workspace.repoRoot, workspace.branch, workspace.baseRef, workspace.baseSha)
347
+ : { kind: "unavailable" } as const;
348
+ const safeToAbandon = probe.kind === "gone"
349
+ ? true
350
+ : probe.kind === "preview"
351
+ ? previewReapable(probe.preview)
352
+ : undefined;
353
+
354
+ if (safeToAbandon === true) {
355
+ const reasonText = probe.kind === "gone" ? "missing worktree; branch ref gone" : "missing worktree; branch already landed";
356
+ const updated = updateWorkspaceStatus(workspace.id, "abandoned", {
357
+ autoAbandoned: true,
358
+ abandonedReason: reasonText,
359
+ abandonedAt: now,
360
+ });
361
+ if (!updated) continue;
362
+ autoAbandoned.push(workspace.id);
363
+ flaggedAt.delete(key);
364
+ createActivityEvent({
365
+ clientId: `workspace-row-auto-abandoned-${workspace.id}-${now}`,
366
+ kind: "state",
367
+ title: "Workspace auto-abandoned",
368
+ body: `${workspace.branch ?? workspace.id} in ${workspace.repoRoot} — worktree missing and ${probe.kind === "gone" ? "branch ref is gone" : "branch has fully landed"}`,
369
+ meta: workspace.branch ?? workspace.id,
370
+ icon: "ti-clock-x",
371
+ view: "orchestrators",
372
+ metadata: {
373
+ source: "server",
374
+ maintenanceJobId: "workspace-orphan-reaper",
375
+ workspaceId: workspace.id,
376
+ worktreePath: workspace.worktreePath,
377
+ branch: workspace.branch,
378
+ reason: reasonText,
379
+ },
380
+ });
381
+ continue;
382
+ }
383
+
263
384
  if (now - last < UNLANDED_FLAG_COOLDOWN_MS) continue;
264
385
  flaggedAt.set(key, now);
386
+ flaggedMissingWorktrees.push(workspace.id);
387
+ const detail = probe.kind === "preview"
388
+ ? `${probe.preview.unmergedAhead ?? probe.preview.ahead ?? "?"} un-landed commit(s) still recoverable on branch ${workspace.branch ?? workspace.id}`
389
+ : workspace.branch
390
+ ? `host could not confirm whether branch ${workspace.branch} has landed`
391
+ : "host could not confirm whether recoverable branch work remains";
265
392
  createActivityEvent({
266
- clientId: `workspace-row-no-worktree-${missing.workspaceId}-${now}`,
393
+ clientId: `workspace-row-needs-attention-${workspace.id}-${now}`,
267
394
  kind: "state",
268
- title: "Workspace row has no worktree on disk",
269
- body: `Workspace ${missing.workspaceId} (${missing.status}) points at ${missing.worktreePath}, which no longer exists on disk disk/DB drift.`,
270
- meta: missing.workspaceId,
271
- icon: "ti-unlink",
395
+ title: "Missing-worktree workspace needs attention",
396
+ body: `${workspace.id} in ${workspace.repoRoot} has no worktree on disk and cannot be auto-abandoned safely — ${detail}. Recover via the branch if needed, then abandon or clean it up explicitly.`,
397
+ meta: workspace.id,
398
+ icon: "ti-alert-triangle",
272
399
  view: "orchestrators",
273
- metadata: { source: "server", maintenanceJobId: "workspace-orphan-reaper", workspaceId: missing.workspaceId, worktreePath: missing.worktreePath, status: missing.status },
400
+ metadata: {
401
+ source: "server",
402
+ maintenanceJobId: "workspace-orphan-reaper",
403
+ workspaceId: workspace.id,
404
+ worktreePath: workspace.worktreePath,
405
+ branch: workspace.branch,
406
+ status: workspace.status,
407
+ probe: probe.kind,
408
+ ...(probe.kind === "preview" ? { ahead: probe.preview.ahead, unmergedAhead: probe.preview.unmergedAhead, landed: probe.preview.landed } : {}),
409
+ },
274
410
  });
275
411
  }
276
412
 
@@ -283,6 +419,8 @@ export async function reapOrphanedWorktrees(): Promise<Record<string, unknown>>
283
419
  scanned: orphans.length,
284
420
  reaped,
285
421
  flagged,
422
+ autoAbandoned,
423
+ flaggedMissingWorktrees,
286
424
  missingWorktrees: missingWorktrees.map((m) => m.workspaceId),
287
425
  reapEnabled,
288
426
  };
@@ -14,12 +14,13 @@
14
14
  // "handed off, healthy, wait" guidance, and `actionNeeded:false` is the explicit
15
15
  // anti-panic signal.
16
16
 
17
+ import { TERMINAL_WORKSPACE_STATUS_VALUES } from "agent-relay-sdk";
17
18
  import type { WorkspaceRecord, WorkspaceStatus } from "./types";
18
19
 
19
20
  // Statuses where the worktree's lifecycle is over — landed or torn down. Single
20
21
  // home; imported by maintenance (stale reap), routes (orphan scan), and the MCP
21
22
  // initialize primer (don't brief an agent on a dead workspace). Was duplicated.
22
- export const TERMINAL_WORKSPACE_STATUSES = new Set<WorkspaceStatus>(["cleaned", "merged", "abandoned"]);
23
+ export const TERMINAL_WORKSPACE_STATUSES = new Set<WorkspaceStatus>(TERMINAL_WORKSPACE_STATUS_VALUES);
23
24
 
24
25
  // The "handed off, waiting to land" statuses — an agent has finished and the
25
26
  // auto-merge-back is responsible for getting the branch onto base. SINGLE HOME: