@saadnvd1/agent-os 0.3.698 → 0.3.715

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.
Files changed (226) hide show
  1. package/README.md +1 -0
  2. package/app/api/bus/groups/route.ts +44 -0
  3. package/app/api/bus/send/route.ts +11 -1
  4. package/app/api/bus/spawn/route.ts +12 -1
  5. package/app/api/bus/wait/route.ts +22 -0
  6. package/app/api/claims/[id]/route.ts +20 -0
  7. package/app/api/claims/check/route.ts +23 -0
  8. package/app/api/claims/check.test.ts +113 -0
  9. package/app/api/claims/exchange/route.ts +42 -0
  10. package/app/api/claims/outside.test.ts +90 -0
  11. package/app/api/claims/route.test.ts +193 -0
  12. package/app/api/claims/route.ts +73 -0
  13. package/app/api/hosts/[id]/route.ts +9 -1
  14. package/app/api/peer/hello/reveal/route.ts +6 -0
  15. package/app/api/peer/hello/route.ts +9 -0
  16. package/app/api/peer/hello/status/route.ts +6 -0
  17. package/app/api/peer/unlink/route.ts +17 -0
  18. package/app/api/peers/[hostId]/audit/route.ts +14 -0
  19. package/app/api/peers/[hostId]/route.ts +20 -0
  20. package/app/api/peers/account/route.ts +9 -0
  21. package/app/api/peers/gate.test.ts +174 -0
  22. package/app/api/peers/link/[id]/confirm/route.ts +27 -0
  23. package/app/api/peers/link/[id]/route.ts +20 -0
  24. package/app/api/peers/link/route.ts +23 -0
  25. package/app/api/peers/pending/[id]/accept/route.ts +31 -0
  26. package/app/api/peers/pending/[id]/decline/route.ts +13 -0
  27. package/app/api/peers/pending/route.test.ts +37 -0
  28. package/app/api/peers/pending/route.ts +14 -0
  29. package/app/api/session-groups/[id]/restore/route.ts +11 -0
  30. package/app/api/session-groups/[id]/route.ts +25 -0
  31. package/app/api/session-groups/order/route.ts +15 -0
  32. package/app/api/session-groups/route.ts +21 -0
  33. package/app/api/sessions/[id]/fork/route.ts +3 -0
  34. package/app/api/sessions/[id]/group/route.ts +18 -0
  35. package/app/api/sessions/[id]/route.ts +14 -4
  36. package/app/api/sessions/[id]/summarize/route.ts +2 -0
  37. package/app/api/sessions/claims.test.ts +122 -0
  38. package/app/api/sessions/group/route.ts +17 -0
  39. package/app/api/sessions/route.ts +31 -13
  40. package/app/page.tsx +7 -0
  41. package/components/Claims/ClaimFiles.tsx +38 -0
  42. package/components/Claims/ClaimRow.tsx +82 -0
  43. package/components/Claims/ClaimsSheet.tsx +176 -0
  44. package/components/Claims/OverlapPill.tsx +31 -0
  45. package/components/Claims/StartClaimsNotice.tsx +70 -0
  46. package/components/Claims/claims-ui.test.ts +161 -0
  47. package/components/Claims/format.ts +57 -0
  48. package/components/Claims/index.ts +3 -0
  49. package/components/Draft/DraftPanel.tsx +40 -2
  50. package/components/Hosts/AccountMachines.tsx +194 -0
  51. package/components/Hosts/HostRow.tsx +195 -45
  52. package/components/Hosts/HostsDialog.tsx +49 -37
  53. package/components/Hosts/IncomingLinkSheet.tsx +246 -0
  54. package/components/Hosts/LinkCodeSheet.tsx +186 -0
  55. package/components/Hosts/PeerActivitySheet.tsx +111 -0
  56. package/components/Hosts/sheet.ts +3 -0
  57. package/components/SessionList/GroupShelves.tsx +195 -0
  58. package/components/SessionList/MoveToGroupMenu.tsx +105 -0
  59. package/components/SessionList/RowSubtitle.tsx +18 -2
  60. package/components/SessionList/SelectionToolbar.tsx +52 -1
  61. package/components/SessionList/SessionRow.tsx +1 -0
  62. package/components/SessionList/SessionRowMenu.tsx +17 -0
  63. package/components/SessionList/SessionShelves.tsx +5 -0
  64. package/components/SessionList/SidebarBody.tsx +6 -0
  65. package/components/SessionList/SidebarContextMenu.test.ts +104 -0
  66. package/components/SessionList/SidebarContextMenu.tsx +59 -0
  67. package/components/SessionList/SidebarHeader.tsx +5 -0
  68. package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
  69. package/components/SessionList/index.tsx +18 -14
  70. package/components/SessionList/useSidebarData.ts +10 -0
  71. package/components/Welcome/ConnectBlock.tsx +1 -1
  72. package/components/ui/notice.tsx +46 -0
  73. package/components/ui/status-pill.tsx +39 -0
  74. package/data/claims/index.ts +2 -0
  75. package/data/claims/keys.ts +5 -0
  76. package/data/claims/queries.ts +47 -0
  77. package/data/peers/index.ts +2 -0
  78. package/data/peers/keys.ts +7 -0
  79. package/data/peers/queries.ts +172 -0
  80. package/data/push/topics.ts +4 -0
  81. package/data/session-groups/index.ts +156 -0
  82. package/data/sessions/index.ts +0 -1
  83. package/data/sessions/pin.ts +2 -6
  84. package/data/sessions/queries.ts +10 -29
  85. package/data/sessions/shelf.ts +2 -6
  86. package/data/tasks/index.ts +2 -0
  87. package/hooks/useSessions.ts +0 -12
  88. package/lib/__fixtures__/peer-rpc.ts +65 -33
  89. package/lib/agents/brief.ts +3 -1
  90. package/lib/agents/spawn.ts +4 -0
  91. package/lib/bus/format.ts +25 -4
  92. package/lib/bus/index.ts +121 -106
  93. package/lib/bus/resolve.ts +16 -2
  94. package/lib/bus/targets.ts +122 -0
  95. package/lib/bus/waits/kick.ts +12 -0
  96. package/lib/bus/waits/lines.ts +69 -0
  97. package/lib/bus/waits/register.ts +83 -0
  98. package/lib/bus/waits/store.ts +196 -0
  99. package/lib/bus/waits/tick.ts +192 -0
  100. package/lib/claims/collectLocal.ts +121 -0
  101. package/lib/claims/describe.ts +41 -0
  102. package/lib/claims/exchange.ts +91 -0
  103. package/lib/claims/goal.ts +45 -0
  104. package/lib/claims/index.ts +10 -0
  105. package/lib/claims/loop.ts +51 -0
  106. package/lib/claims/machine.ts +21 -0
  107. package/lib/claims/mine.ts +43 -0
  108. package/lib/claims/notifyOverlaps.ts +101 -0
  109. package/lib/claims/onMerged.ts +85 -0
  110. package/lib/claims/outside.ts +36 -0
  111. package/lib/claims/overlap.ts +87 -0
  112. package/lib/claims/overlapCache.ts +86 -0
  113. package/lib/claims/overlapText.ts +36 -0
  114. package/lib/claims/peers.ts +56 -0
  115. package/lib/claims/startCheck.ts +100 -0
  116. package/lib/claims/startWarning.ts +29 -0
  117. package/lib/claims/store.ts +366 -0
  118. package/lib/claims/types.ts +102 -0
  119. package/lib/claims/view.ts +80 -0
  120. package/lib/claims/yield.ts +24 -0
  121. package/lib/connect/control-api.ts +108 -2
  122. package/lib/connect/heartbeat.ts +11 -3
  123. package/lib/connect/serve.ts +2 -0
  124. package/lib/db/changes.ts +6 -0
  125. package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
  126. package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
  127. package/lib/db/migrations/20261020T1600_bus_waits.ts +35 -0
  128. package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
  129. package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
  130. package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
  131. package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
  132. package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
  133. package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
  134. package/lib/db/queries.ts +0 -40
  135. package/lib/db/session-parts/columns.ts +1 -0
  136. package/lib/db/types.ts +20 -8
  137. package/lib/doctor/checks.ts +64 -1
  138. package/lib/done/archive.ts +2 -0
  139. package/lib/git-remote.ts +43 -0
  140. package/lib/hosts/index.ts +10 -1
  141. package/lib/orchestration.ts +2 -0
  142. package/lib/orchestrator/act.ts +54 -4
  143. package/lib/orchestrator/brief.ts +2 -1
  144. package/lib/orchestrator/claim-wait.ts +71 -0
  145. package/lib/orchestrator/conditions.ts +28 -1
  146. package/lib/orchestrator/describe.ts +19 -1
  147. package/lib/orchestrator/facts.ts +22 -0
  148. package/lib/orchestrator/serve.ts +37 -2
  149. package/lib/orchestrator/tool-names.ts +1 -0
  150. package/lib/orchestrator/tool-schemas.ts +31 -1
  151. package/lib/orchestrator/tools.ts +3 -1
  152. package/lib/orchestrator/watcher.ts +19 -1
  153. package/lib/peer/account.ts +270 -0
  154. package/lib/peer/allowlist.ts +1 -1
  155. package/lib/peer/cli.ts +24 -0
  156. package/lib/peer/client.ts +26 -1
  157. package/lib/peer/code.ts +3 -0
  158. package/lib/peer/hello-route.ts +32 -0
  159. package/lib/peer/hello.ts +346 -0
  160. package/lib/peer/link-cli.ts +248 -0
  161. package/lib/peer/links.ts +117 -5
  162. package/lib/peer/machines.ts +84 -0
  163. package/lib/peer/name.ts +32 -0
  164. package/lib/peer/notify.ts +32 -0
  165. package/lib/peer/pairing-out.ts +382 -0
  166. package/lib/peer/pairing.ts +432 -0
  167. package/lib/peer/pin.ts +62 -0
  168. package/lib/peer/reasons.ts +7 -0
  169. package/lib/peer/revoke.ts +75 -0
  170. package/lib/peer/rpc-intake.ts +37 -8
  171. package/lib/peer/rpc-server.ts +72 -6
  172. package/lib/peer/unlink.ts +62 -0
  173. package/lib/peer/unlinked-proof.ts +44 -0
  174. package/lib/projects.ts +5 -2
  175. package/lib/schedules/when.ts +1 -1
  176. package/lib/security/auth.ts +10 -2
  177. package/lib/security/demo.ts +7 -0
  178. package/lib/security/gate.ts +3 -0
  179. package/lib/session-groups/assignSessions.ts +81 -0
  180. package/lib/session-groups/cli.ts +61 -0
  181. package/lib/session-groups/createGroup.ts +28 -0
  182. package/lib/session-groups/index.ts +21 -0
  183. package/lib/session-groups/removeGroup.ts +36 -0
  184. package/lib/session-groups/reorderGroups.ts +31 -0
  185. package/lib/session-groups/resolveGroup.ts +19 -0
  186. package/lib/session-groups/route.ts +32 -0
  187. package/lib/session-groups/store.ts +85 -0
  188. package/lib/session-groups/types.ts +25 -0
  189. package/lib/session-groups/updateGroup.ts +35 -0
  190. package/lib/session-groups/workspace.ts +21 -0
  191. package/lib/sessions/end.ts +3 -0
  192. package/lib/sessions/launch.ts +5 -2
  193. package/lib/sidebar/rollup.ts +1 -0
  194. package/lib/sidebar/row-menu.ts +2 -0
  195. package/lib/sidebar/shelves.ts +90 -15
  196. package/lib/sidebar/visible-order.ts +5 -0
  197. package/lib/tasks/hold-checks.ts +3 -1
  198. package/lib/tasks/holds.ts +3 -0
  199. package/lib/tasks/index.ts +5 -0
  200. package/lib/tasks/queue-rows.ts +8 -1
  201. package/lib/tasks/queue-start.ts +1 -0
  202. package/lib/tasks/queue-types.ts +9 -1
  203. package/lib/tasks/queue.ts +15 -5
  204. package/lib/tasks/schedule-types.ts +1 -0
  205. package/lib/test-temp.ts +19 -3
  206. package/lib/workspaces.ts +11 -3
  207. package/lib/worktree-sweep/testTemp.ts +23 -106
  208. package/package.json +1 -1
  209. package/scripts/agent-os +3 -1
  210. package/scripts/autodeploy +2 -0
  211. package/scripts/autodeploy.test.ts +31 -1
  212. package/scripts/claim-hook.sh +31 -0
  213. package/scripts/claim-hook.test.ts +230 -0
  214. package/scripts/lib/cli-links.sh +36 -0
  215. package/scripts/lib/commands.sh +5 -0
  216. package/scripts/lib/machine.sh +27 -2
  217. package/scripts/lib/machine.test.ts +25 -3
  218. package/scripts/sync-cli-links +7 -0
  219. package/scripts/sync-cli-links.test.ts +69 -0
  220. package/server.ts +6 -0
  221. package/stores/claimsUi.ts +22 -0
  222. package/app/api/groups/[...path]/route.ts +0 -136
  223. package/app/api/groups/route.ts +0 -93
  224. package/data/groups/index.ts +0 -1
  225. package/data/groups/mutations.ts +0 -95
  226. package/hooks/useGroups.ts +0 -37
@@ -56,6 +56,7 @@ cmd_install() {
56
56
 
57
57
  mkdir -p "$bin_dir"
58
58
  ln -sf "$REPO_DIR/scripts/agent-os" "$bin_dir/agent-os"
59
+ "$REPO_DIR/scripts/sync-cli-links" create
59
60
 
60
61
  # Check if ~/.local/bin is in PATH
61
62
  if [[ ":$PATH:" != *":$bin_dir:"* ]]; then
@@ -304,6 +305,8 @@ cmd_update() {
304
305
  log_success "Update complete!"
305
306
  fi
306
307
 
308
+ scripts/sync-cli-links || log_warn "Could not sync the CLI links"
309
+
307
310
  if [[ "$was_running" == true ]]; then
308
311
  cmd_start
309
312
  fi
@@ -558,6 +561,8 @@ cmd_help() {
558
561
  echo " pair Add a phone, tablet or laptop (prints a QR code)"
559
562
  echo " connect Turn on AgentOS Connect for this machine (in development)"
560
563
  echo " disconnect Turn Connect off; the tunnel closes within seconds"
564
+ echo " link Link machines on your Connect account (link <name>, link accept <code>)"
565
+ echo " unlink Unlink a machine linked through Connect"
561
566
  echo " passkeys reset Revoke every passkey (recovery; asks you to confirm)"
562
567
  echo " logs Tail server logs"
563
568
  echo " update Update to latest version"
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env bash
2
2
  # `agent-os machine`: make this Linux box (a WSL distro, a DGX Spark) linkable
3
- # from another AgentOS: sshd for pairing, Tailscale for reaching it, the
4
- # server kept running. It never edits sshd_config or authorized_keys.
3
+ # from another AgentOS. Bare, it explains the Connect path (sign in to Connect,
4
+ # then `agent-os link`) and changes nothing. With --tailscale it sets up
5
+ # sshd for pairing, Tailscale for reaching it and the server kept running. It
6
+ # never edits sshd_config or authorized_keys.
5
7
 
6
8
  # The ssh service's unit name differs by distro (Debian: ssh, others: sshd).
7
9
  ssh_unit() {
@@ -21,7 +23,26 @@ tailscale_name() {
21
23
  | head -1 | sed 's/\.$//'
22
24
  }
23
25
 
26
+ machine_connect_help() {
27
+ log_info "Recommended: link through AgentOS Connect. It works from any network and needs no ssh or Tailscale (Pro)."
28
+ echo " 1. On this machine: agent-os connect (sign in with the code from runagentos.com)"
29
+ echo " 2. On the other one: open Machines and press Link next to this machine"
30
+ echo " (or run 'agent-os link <name>'). It shows a 6-digit code; type it here:"
31
+ echo " agent-os link accept <code>"
32
+ echo " 3. This machine then shows a second code. Type it back on the machine you're"
33
+ echo " linking from (in Machines, or at the 'agent-os link' prompt) to finish."
34
+ echo ""
35
+ log_info "No Connect? Use ssh over Tailscale instead: agent-os machine --tailscale"
36
+ }
37
+
24
38
  cmd_machine() {
39
+ local tailscale=0 arg
40
+ for arg in "$@"; do
41
+ case "$arg" in
42
+ --tailscale) tailscale=1 ;;
43
+ *) log_error "Unknown option '$arg'. Usage: agent-os machine [--tailscale]"; exit 1 ;;
44
+ esac
45
+ done
25
46
  if [[ "$OS" == "macos" ]]; then
26
47
  log_info "A Mac is linked from the other side: on the machine you'll link from, open Machines and add this Mac."
27
48
  return 0
@@ -30,6 +51,10 @@ cmd_machine() {
30
51
  log_error "AgentOS is not installed. Run 'agent-os install' first."
31
52
  exit 1
32
53
  fi
54
+ if [[ "$tailscale" == 0 ]]; then
55
+ machine_connect_help
56
+ return 0
57
+ fi
33
58
  if ! has_systemd; then
34
59
  if [[ "${WSL:-0}" != 0 ]]; then
35
60
  log_error "This needs systemd. Run 'agent-os enable', which offers to turn it on, then try again."
@@ -42,7 +42,11 @@ const log = () =>
42
42
  const record = (name: string) =>
43
43
  fake(name, `echo "${name} $*" >> "${dir}/calls.log"`);
44
44
 
45
- function machine(env: Record<string, string> = {}, os = "debian") {
45
+ function machine(
46
+ env: Record<string, string> = {},
47
+ os = "debian",
48
+ args = "--tailscale"
49
+ ) {
46
50
  mkdirSync(join(dir, "repo"), { recursive: true });
47
51
  return spawnSync(
48
52
  "/bin/bash",
@@ -50,7 +54,7 @@ function machine(env: Record<string, string> = {}, os = "debian") {
50
54
  "-euo",
51
55
  "pipefail",
52
56
  "-c",
53
- `source "${LIB}/common.sh"; source "${LIB}/prerequisites.sh"; source "${LIB}/commands.sh"; source "${LIB}/machine.sh"; WSL=2 OS=${os} REPO_DIR="${dir}/repo" PORT=3011; has_systemd() { return 0; }; is_running() { return 0; }; cmd_enable() { echo "enable called"; }; cmd_machine`,
57
+ `source "${LIB}/common.sh"; source "${LIB}/prerequisites.sh"; source "${LIB}/commands.sh"; source "${LIB}/machine.sh"; WSL=2 OS=${os} REPO_DIR="${dir}/repo" PORT=3011; has_systemd() { return 0; }; is_running() { return 0; }; cmd_enable() { echo "enable called"; }; cmd_machine ${args}`,
54
58
  join(dir, "agent-os"),
55
59
  ],
56
60
  {
@@ -86,6 +90,24 @@ beforeEach(() => {
86
90
  afterEach(() => rmSync(dir, { recursive: true, force: true }));
87
91
 
88
92
  describe("agent-os machine", () => {
93
+ it("bare, points at Connect first, mentions --tailscale and changes nothing", () => {
94
+ const r = machine({}, "debian", "");
95
+ expect(r.status).toBe(0);
96
+ expect(r.stdout).toContain("agent-os connect");
97
+ expect(r.stdout).toContain("agent-os link accept");
98
+ expect(r.stdout).toContain("second code");
99
+ expect(r.stdout).toContain("agent-os machine --tailscale");
100
+ expect(r.stdout).not.toContain("enable called");
101
+ expect(log()).toBe("");
102
+ });
103
+
104
+ it("rejects an unknown option", () => {
105
+ const r = machine({}, "debian", "--bogus");
106
+ expect(r.status).toBe(1);
107
+ expect(r.stdout + r.stderr).toContain("Usage: agent-os machine");
108
+ expect(log()).toBe("");
109
+ });
110
+
89
111
  it("stops with the install pointer when Tailscale is missing", () => {
90
112
  const r = machine();
91
113
  expect(r.status).toBe(1);
@@ -128,7 +150,7 @@ describe("agent-os machine", () => {
128
150
  "/bin/bash",
129
151
  [
130
152
  "-c",
131
- `source "${LIB}/common.sh"; source "${LIB}/machine.sh"; WSL=2 OS=debian REPO_DIR="${dir}/repo"; has_systemd() { return 1; }; cmd_machine`,
153
+ `source "${LIB}/common.sh"; source "${LIB}/machine.sh"; WSL=2 OS=debian REPO_DIR="${dir}/repo"; has_systemd() { return 1; }; cmd_machine --tailscale`,
132
154
  ],
133
155
  {
134
156
  encoding: "utf8",
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ # Point ~/.local/bin/{agent-os,aos} at the checkout the live server runs from.
3
+ set -euo pipefail
4
+ here="$(cd "$(dirname "$0")/.." && pwd)"
5
+ # shellcheck source=lib/cli-links.sh
6
+ source "$here/scripts/lib/cli-links.sh"
7
+ sync_cli_links "$(live_checkout "$here")" "${1:-}"
@@ -0,0 +1,69 @@
1
+ import { execFileSync } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { describe, expect, it } from "vitest";
5
+ import { tempDir } from "@/lib/test-temp";
6
+
7
+ const script = path.resolve(__dirname, "sync-cli-links");
8
+
9
+ function setup() {
10
+ const root = tempDir("aos-synclinks-");
11
+ const home = path.join(root, "home");
12
+ const bin = path.join(root, "bin");
13
+ fs.mkdirSync(home);
14
+ fs.mkdirSync(bin);
15
+ const clone = (name: string) => {
16
+ const dir = path.join(root, name);
17
+ fs.mkdirSync(path.join(dir, "bin"), { recursive: true });
18
+ fs.mkdirSync(path.join(dir, "scripts"), { recursive: true });
19
+ fs.writeFileSync(path.join(dir, "bin/aos"), "");
20
+ fs.writeFileSync(path.join(dir, "scripts/agent-os"), "");
21
+ return dir;
22
+ };
23
+ const live = clone("live");
24
+ const old = clone("old");
25
+ fs.writeFileSync(
26
+ path.join(home, "live-server.json"),
27
+ JSON.stringify({ cwd: live })
28
+ );
29
+ const run = (...args: string[]) =>
30
+ execFileSync(script, args, {
31
+ encoding: "utf-8",
32
+ env: { ...process.env, AOS_BIN_DIR: bin, AGENT_OS_HOME: home },
33
+ });
34
+ return { bin, home, live, old, run };
35
+ }
36
+
37
+ describe("sync-cli-links", () => {
38
+ it("repoints a link into another clone and logs it", () => {
39
+ const t = setup();
40
+ fs.symlinkSync(path.join(t.old, "bin/aos"), path.join(t.bin, "aos"));
41
+ const out = t.run();
42
+ expect(fs.readlinkSync(path.join(t.bin, "aos"))).toBe(
43
+ path.join(t.live, "bin/aos")
44
+ );
45
+ expect(out).toContain("repointed");
46
+ expect(out).toContain(t.old);
47
+ expect(t.run()).toBe("");
48
+ });
49
+
50
+ it("leaves absent links alone unless asked to create them", () => {
51
+ const t = setup();
52
+ t.run();
53
+ expect(fs.existsSync(path.join(t.bin, "aos"))).toBe(false);
54
+ t.run("create");
55
+ expect(fs.readlinkSync(path.join(t.bin, "aos"))).toBe(
56
+ path.join(t.live, "bin/aos")
57
+ );
58
+ expect(fs.readlinkSync(path.join(t.bin, "agent-os"))).toBe(
59
+ path.join(t.live, "scripts/agent-os")
60
+ );
61
+ });
62
+
63
+ it("never replaces a real file", () => {
64
+ const t = setup();
65
+ fs.writeFileSync(path.join(t.bin, "aos"), "mine");
66
+ t.run("create");
67
+ expect(fs.readFileSync(path.join(t.bin, "aos"), "utf-8")).toBe("mine");
68
+ });
69
+ });
package/server.ts CHANGED
@@ -14,12 +14,14 @@ import { setStartGate } from "./lib/stacks/tick";
14
14
  import { stackStartGate } from "./lib/orchestrator/brakes";
15
15
  import { buildId } from "./lib/build";
16
16
  import { startOrchestratorWatcher } from "./lib/orchestrator/watcher";
17
+ import { startWaitWatcher } from "./lib/bus/waits/tick";
17
18
  import { installErrorCounter } from "./lib/health/errors";
18
19
  import { recordBoot } from "./lib/health/boots";
19
20
  import { realDeps, schedulesEnabled, startScheduler } from "./lib/schedules";
20
21
  import { startSettling } from "./lib/recipes";
21
22
  import { startAutoArchive } from "./lib/done/auto-archive";
22
23
  import { startWorktreeSweep } from "./lib/worktree-sweep/start";
24
+ import { startClaimsLoop } from "./lib/claims";
23
25
  import { startUsageRollup, usageRollupEnabled } from "./lib/usage/nightly";
24
26
  import { resumePhoneOutbox } from "./lib/notify";
25
27
  import { resumePushOutbox } from "./lib/notify/push";
@@ -476,6 +478,8 @@ app.prepare().then(async () => {
476
478
  if (process.env.AGENTOS_STACKS !== "off") startStackWatcher();
477
479
  // Each workspace's orchestrator hears about its sessions as events.
478
480
  if (process.env.AGENTOS_ORCHESTRATOR !== "off") startOrchestratorWatcher();
481
+ // Sessions waiting on each other (aos send --await, aos wait).
482
+ startWaitWatcher();
479
483
  // Schedules tick here, once a minute, and nowhere else.
480
484
  if (schedulesEnabled(process.env)) startScheduler(realDeps);
481
485
  // Looking for a newer version, and running an update only when one is
@@ -494,6 +498,8 @@ app.prepare().then(async () => {
494
498
  startAutoArchive();
495
499
  // Settled worktrees older than the setting are removed; branches stay.
496
500
  startWorktreeSweep();
501
+ // Who is working on what, across linked machines.
502
+ if (!process.env.AGENTOS_DEMO) startClaimsLoop();
497
503
  resumePhoneOutbox();
498
504
  resumePushOutbox();
499
505
  // The usage ledger's daily rollup and its ratchet alerts.
@@ -0,0 +1,22 @@
1
+ import { proxy } from "valtio";
2
+
3
+ // The claims sheet ("Work across machines"). `focus` narrows it to the claims
4
+ // of one overlapping pair, from a row's pill.
5
+ export const claimsUi = proxy<{ open: boolean; focus: string[] | null }>({
6
+ open: false,
7
+ focus: null,
8
+ });
9
+
10
+ export const claimsUiActions = {
11
+ open: (focus: string[] | null = null) => {
12
+ claimsUi.focus = focus;
13
+ claimsUi.open = true;
14
+ },
15
+ setOpen: (open: boolean) => {
16
+ claimsUi.open = open;
17
+ if (!open) claimsUi.focus = null;
18
+ },
19
+ showAll: () => {
20
+ claimsUi.focus = null;
21
+ },
22
+ };
@@ -1,136 +0,0 @@
1
- import { NextResponse } from "next/server";
2
- import { db, queries, type Group } from "@/lib/db";
3
-
4
- // GET /api/groups/[...path] - Get a single group
5
- export async function GET(
6
- request: Request,
7
- { params }: { params: Promise<{ path: string[] }> }
8
- ) {
9
- const { path: pathParts } = await params;
10
- const path = pathParts.join("/");
11
-
12
- try {
13
- const group = queries.getGroup(db).get(path) as Group | undefined;
14
-
15
- if (!group) {
16
- return NextResponse.json({ error: "Group not found" }, { status: 404 });
17
- }
18
-
19
- return NextResponse.json({
20
- group: { ...group, expanded: Boolean(group.expanded) },
21
- });
22
- } catch (error) {
23
- console.error("Error fetching group:", error);
24
- return NextResponse.json(
25
- { error: "Failed to fetch group" },
26
- { status: 500 }
27
- );
28
- }
29
- }
30
-
31
- // PATCH /api/groups/[...path] - Update group (name, expanded, order)
32
- export async function PATCH(
33
- request: Request,
34
- { params }: { params: Promise<{ path: string[] }> }
35
- ) {
36
- const { path: pathParts } = await params;
37
- const path = pathParts.join("/");
38
-
39
- try {
40
- const body = await request.json();
41
- const { name, expanded, sort_order } = body;
42
-
43
- // Check if group exists
44
- const group = queries.getGroup(db).get(path) as Group | undefined;
45
- if (!group) {
46
- return NextResponse.json({ error: "Group not found" }, { status: 404 });
47
- }
48
-
49
- // Protect default group from being renamed
50
- if (path === "sessions" && name !== undefined && name !== "Sessions") {
51
- return NextResponse.json(
52
- { error: "Cannot rename the default group" },
53
- { status: 400 }
54
- );
55
- }
56
-
57
- // Update name
58
- if (name !== undefined) {
59
- queries.updateGroupName(db).run(name, path);
60
- }
61
-
62
- // Update expanded state
63
- if (expanded !== undefined) {
64
- queries.updateGroupExpanded(db).run(expanded ? 1 : 0, path);
65
- }
66
-
67
- // Update sort order
68
- if (sort_order !== undefined) {
69
- queries.updateGroupOrder(db).run(sort_order, path);
70
- }
71
-
72
- const updatedGroup = queries.getGroup(db).get(path) as Group;
73
- return NextResponse.json({
74
- group: { ...updatedGroup, expanded: Boolean(updatedGroup.expanded) },
75
- });
76
- } catch (error) {
77
- console.error("Error updating group:", error);
78
- return NextResponse.json(
79
- { error: "Failed to update group" },
80
- { status: 500 }
81
- );
82
- }
83
- }
84
-
85
- // DELETE /api/groups/[...path] - Delete group (moves sessions to parent or default)
86
- export async function DELETE(
87
- request: Request,
88
- { params }: { params: Promise<{ path: string[] }> }
89
- ) {
90
- const { path: pathParts } = await params;
91
- const path = pathParts.join("/");
92
-
93
- try {
94
- // Protect default group
95
- if (path === "sessions") {
96
- return NextResponse.json(
97
- { error: "Cannot delete the default group" },
98
- { status: 400 }
99
- );
100
- }
101
-
102
- // Check if group exists
103
- const group = queries.getGroup(db).get(path) as Group | undefined;
104
- if (!group) {
105
- return NextResponse.json({ error: "Group not found" }, { status: 404 });
106
- }
107
-
108
- // Find parent group or use default
109
- const pathParts2 = path.split("/");
110
- pathParts2.pop();
111
- const parentPath =
112
- pathParts2.length > 0 ? pathParts2.join("/") : "sessions";
113
-
114
- // Move all sessions in this group to parent
115
- queries.moveSessionsToGroup(db).run(parentPath, path);
116
-
117
- // Also move sessions from any subgroups
118
- const allGroups = queries.getAllGroups(db).all() as Group[];
119
- const subgroups = allGroups.filter((g) => g.path.startsWith(path + "/"));
120
- for (const subgroup of subgroups) {
121
- queries.moveSessionsToGroup(db).run(parentPath, subgroup.path);
122
- queries.deleteGroup(db).run(subgroup.path);
123
- }
124
-
125
- // Delete the group
126
- queries.deleteGroup(db).run(path);
127
-
128
- return NextResponse.json({ success: true, movedTo: parentPath });
129
- } catch (error) {
130
- console.error("Error deleting group:", error);
131
- return NextResponse.json(
132
- { error: "Failed to delete group" },
133
- { status: 500 }
134
- );
135
- }
136
- }
@@ -1,93 +0,0 @@
1
- import { NextResponse } from "next/server";
2
- import { db, queries, type Group } from "@/lib/db";
3
-
4
- // GET /api/groups - List all groups
5
- export async function GET() {
6
- try {
7
- const groups = queries.getAllGroups(db).all() as Group[];
8
-
9
- // Convert expanded from 0/1 to boolean
10
- const formattedGroups = groups.map((g) => ({
11
- ...g,
12
- expanded: Boolean(g.expanded),
13
- }));
14
-
15
- return NextResponse.json({ groups: formattedGroups });
16
- } catch (error) {
17
- console.error("Error fetching groups:", error);
18
- return NextResponse.json(
19
- { error: "Failed to fetch groups" },
20
- { status: 500 }
21
- );
22
- }
23
- }
24
-
25
- // POST /api/groups - Create a new group
26
- export async function POST(request: Request) {
27
- try {
28
- const body = await request.json();
29
- const { name, parentPath } = body;
30
-
31
- if (!name || typeof name !== "string") {
32
- return NextResponse.json({ error: "Name is required" }, { status: 400 });
33
- }
34
-
35
- // Sanitize name to create path
36
- const sanitizedName = name
37
- .toLowerCase()
38
- .replace(/[^a-z0-9-]/g, "-")
39
- .replace(/-+/g, "-")
40
- .replace(/^-|-$/g, "");
41
-
42
- if (!sanitizedName) {
43
- return NextResponse.json(
44
- { error: "Invalid group name" },
45
- { status: 400 }
46
- );
47
- }
48
-
49
- // Build full path
50
- const path = parentPath ? `${parentPath}/${sanitizedName}` : sanitizedName;
51
-
52
- // Check if group already exists
53
- const existing = queries.getGroup(db).get(path) as Group | undefined;
54
- if (existing) {
55
- return NextResponse.json(
56
- { error: "Group already exists", group: existing },
57
- { status: 409 }
58
- );
59
- }
60
-
61
- // If parent path specified, ensure parent exists
62
- if (parentPath) {
63
- const parent = queries.getGroup(db).get(parentPath) as Group | undefined;
64
- if (!parent) {
65
- return NextResponse.json(
66
- { error: "Parent group does not exist" },
67
- { status: 400 }
68
- );
69
- }
70
- }
71
-
72
- // Get max sort order for new group
73
- const groups = queries.getAllGroups(db).all() as Group[];
74
- const maxOrder = groups.reduce((max, g) => Math.max(max, g.sort_order), 0);
75
-
76
- // Create the group
77
- queries.createGroup(db).run(path, name, maxOrder + 1);
78
-
79
- const newGroup = queries.getGroup(db).get(path) as Group;
80
- return NextResponse.json(
81
- {
82
- group: { ...newGroup, expanded: Boolean(newGroup.expanded) },
83
- },
84
- { status: 201 }
85
- );
86
- } catch (error) {
87
- console.error("Error creating group:", error);
88
- return NextResponse.json(
89
- { error: "Failed to create group" },
90
- { status: 500 }
91
- );
92
- }
93
- }
@@ -1 +0,0 @@
1
- export { useToggleGroup, useCreateGroup, useDeleteGroup } from "./mutations";
@@ -1,95 +0,0 @@
1
- import { useMutation, useQueryClient } from "@tanstack/react-query";
2
- import { sessionKeys } from "../sessions/keys";
3
-
4
- export function useToggleGroup() {
5
- const queryClient = useQueryClient();
6
-
7
- return useMutation({
8
- mutationFn: async ({
9
- path,
10
- expanded,
11
- }: {
12
- path: string;
13
- expanded: boolean;
14
- }) => {
15
- const res = await fetch(`/api/groups/${encodeURIComponent(path)}`, {
16
- method: "PATCH",
17
- headers: { "Content-Type": "application/json" },
18
- body: JSON.stringify({ expanded }),
19
- });
20
- if (!res.ok) throw new Error("Failed to toggle group");
21
- return res.json();
22
- },
23
- onMutate: async ({ path, expanded }) => {
24
- await queryClient.cancelQueries({ queryKey: sessionKeys.list() });
25
- const previous = queryClient.getQueryData(sessionKeys.list());
26
- queryClient.setQueryData(
27
- sessionKeys.list(),
28
- (
29
- old:
30
- | {
31
- sessions: unknown[];
32
- groups: Array<{ path: string; expanded: boolean }>;
33
- }
34
- | undefined
35
- ) =>
36
- old
37
- ? {
38
- ...old,
39
- groups: old.groups.map((g) =>
40
- g.path === path ? { ...g, expanded } : g
41
- ),
42
- }
43
- : old
44
- );
45
- return { previous };
46
- },
47
- onError: (_, __, context) => {
48
- if (context?.previous) {
49
- queryClient.setQueryData(sessionKeys.list(), context.previous);
50
- }
51
- },
52
- });
53
- }
54
-
55
- export function useCreateGroup() {
56
- const queryClient = useQueryClient();
57
-
58
- return useMutation({
59
- mutationFn: async ({
60
- name,
61
- parentPath,
62
- }: {
63
- name: string;
64
- parentPath?: string;
65
- }) => {
66
- const res = await fetch("/api/groups", {
67
- method: "POST",
68
- headers: { "Content-Type": "application/json" },
69
- body: JSON.stringify({ name, parentPath }),
70
- });
71
- if (!res.ok) throw new Error("Failed to create group");
72
- return res.json();
73
- },
74
- onSuccess: () => {
75
- queryClient.invalidateQueries({ queryKey: sessionKeys.list() });
76
- },
77
- });
78
- }
79
-
80
- export function useDeleteGroup() {
81
- const queryClient = useQueryClient();
82
-
83
- return useMutation({
84
- mutationFn: async (path: string) => {
85
- const res = await fetch(`/api/groups/${encodeURIComponent(path)}`, {
86
- method: "DELETE",
87
- });
88
- if (!res.ok) throw new Error("Failed to delete group");
89
- return res.json();
90
- },
91
- onSuccess: () => {
92
- queryClient.invalidateQueries({ queryKey: sessionKeys.list() });
93
- },
94
- });
95
- }
@@ -1,37 +0,0 @@
1
- import { useCallback } from "react";
2
- import { useToggleGroup, useCreateGroup, useDeleteGroup } from "@/data/groups";
3
-
4
- export function useGroups() {
5
- const toggleMutation = useToggleGroup();
6
- const createMutation = useCreateGroup();
7
- const deleteMutation = useDeleteGroup();
8
-
9
- const toggleGroup = useCallback(
10
- async (path: string, expanded: boolean) => {
11
- await toggleMutation.mutateAsync({ path, expanded });
12
- },
13
- [toggleMutation]
14
- );
15
-
16
- const createGroup = useCallback(
17
- async (name: string, parentPath?: string) => {
18
- await createMutation.mutateAsync({ name, parentPath });
19
- },
20
- [createMutation]
21
- );
22
-
23
- const deleteGroup = useCallback(
24
- async (path: string) => {
25
- if (!confirm("Delete this group? Sessions will be moved to parent."))
26
- return;
27
- await deleteMutation.mutateAsync(path);
28
- },
29
- [deleteMutation]
30
- );
31
-
32
- return {
33
- toggleGroup,
34
- createGroup,
35
- deleteGroup,
36
- };
37
- }