@saadnvd1/agent-os 0.3.507 → 0.3.509

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.
@@ -1,4 +1,4 @@
1
- import type { CallUsage } from "../usage/calls";
1
+ import type { CallUsage } from "../usage/types";
2
2
  import type { SearchResult } from "./find";
3
3
 
4
4
  /**
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Tells the Connect service this machine is alive: one signed beat when
3
+ * Connect starts, then one a minute. The answer can switch the machine off
4
+ * (the account disabled it) or say it is gone (removed from the account).
5
+ * Nothing here may throw: a rejection would take AgentOS down.
6
+ */
7
+
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import { ApiError, clean } from "./control-api";
11
+
12
+ export const HEARTBEAT_MS = 60_000;
13
+
14
+ export interface HeartbeatOptions {
15
+ beat: () => Promise<{ enabled: boolean }>;
16
+ /** The account disabled this machine: stop the tunnel. */
17
+ onDisabled: () => void;
18
+ /** The service no longer knows this machine. */
19
+ onGone: () => void;
20
+ log?: (line: string) => void;
21
+ intervalMs?: number;
22
+ }
23
+
24
+ export function startHeartbeat(opts: HeartbeatOptions) {
25
+ const log = opts.log ?? (() => {});
26
+ const every = opts.intervalMs ?? HEARTBEAT_MS;
27
+ let stopped = false;
28
+ let failing = false;
29
+ let timer: NodeJS.Timeout | null = null;
30
+
31
+ const stop = () => {
32
+ stopped = true;
33
+ if (timer) clearTimeout(timer);
34
+ timer = null;
35
+ };
36
+
37
+ const next = () => {
38
+ if (stopped) return;
39
+ timer = setTimeout(tick, every);
40
+ timer.unref?.();
41
+ };
42
+
43
+ const tick = async () => {
44
+ timer = null;
45
+ try {
46
+ const { enabled } = await opts.beat();
47
+ if (stopped) return;
48
+ failing = false;
49
+ if (enabled === false) {
50
+ stop();
51
+ log("connect: this machine was disabled in your account; tunnel off");
52
+ opts.onDisabled();
53
+ return;
54
+ }
55
+ } catch (err) {
56
+ if (stopped) return;
57
+ const status = err instanceof ApiError ? err.status : 0;
58
+ if (status === 401 || status === 404) {
59
+ stop();
60
+ log("connect: machine no longer linked, run agent-os connect");
61
+ opts.onGone();
62
+ return;
63
+ }
64
+ if (!failing) {
65
+ failing = true;
66
+ log(
67
+ `connect: heartbeat failed: ${clean((err as Error)?.message ?? String(err))}; retrying`
68
+ );
69
+ }
70
+ }
71
+ next();
72
+ };
73
+
74
+ void tick();
75
+ return { stop };
76
+ }
77
+
78
+ /** The running AgentOS version, from package.json. */
79
+ export function appVersion(cwd = process.cwd()): string {
80
+ try {
81
+ const pkg = JSON.parse(
82
+ fs.readFileSync(path.join(cwd, "package.json"), "utf8")
83
+ ) as { version?: string };
84
+ return pkg.version ?? "unknown";
85
+ } catch {
86
+ return "unknown";
87
+ }
88
+ }
@@ -28,7 +28,13 @@ import {
28
28
  type ConnectConfig,
29
29
  } from "./config";
30
30
  import { shouldStartConnect, takeConnectLock } from "./guard";
31
- import { checkApiUrl, requestTunnel, type Tunnel } from "./control-api";
31
+ import {
32
+ checkApiUrl,
33
+ heartbeat as sendHeartbeat,
34
+ requestTunnel,
35
+ type Tunnel,
36
+ } from "./control-api";
37
+ import { appVersion, startHeartbeat } from "./heartbeat";
32
38
  import { ensureCloudflared } from "./cloudflared";
33
39
  import { superviseTunnel, type Spawn, type TunnelState } from "./supervise";
34
40
 
@@ -93,12 +99,15 @@ export interface ConnectDeps {
93
99
  ) => Promise<Tunnel>;
94
100
  spawn?: Spawn;
95
101
  delays?: Parameters<typeof superviseTunnel>[0]["delays"];
102
+ heartbeat?: typeof sendHeartbeat;
103
+ heartbeatMs?: number;
96
104
  }
97
105
 
98
106
  interface Running {
99
107
  server: http.Server;
100
108
  sockets: Set<Socket>;
101
109
  supervisor: ReturnType<typeof superviseTunnel>;
110
+ heartbeat: ReturnType<typeof startHeartbeat>;
102
111
  release: () => void;
103
112
  }
104
113
 
@@ -115,6 +124,9 @@ export function startConnect(
115
124
  let base: ConnectStatus = { state: "off" };
116
125
  let hostname = "";
117
126
  let running: Running | null = null;
127
+ // Set when the service disabled or dropped this machine; sync() leaves it
128
+ // off until connect.json is turned off or relinked.
129
+ let halted: string | null = null;
118
130
  // Set while the tunnel listener can't bind; it keeps trying.
119
131
  let bindError: string | undefined;
120
132
  g.__agentosConnect = () => {
@@ -140,6 +152,7 @@ export function startConnect(
140
152
  const down = (why: string) => {
141
153
  if (!running) return;
142
154
  bindError = undefined;
155
+ running.heartbeat.stop();
143
156
  running.supervisor.stop();
144
157
  running.sockets.forEach((s) => s.destroy());
145
158
  running.server.close();
@@ -162,6 +175,14 @@ export function startConnect(
162
175
  hostname = config.hostname;
163
176
  base = { state: "off", enrolled: true, hostname: hostname || undefined };
164
177
  if (!read.enabled) return;
178
+ if (halted) {
179
+ base = {
180
+ ...base,
181
+ state: "off",
182
+ reason: "stopped by the Connect service",
183
+ };
184
+ return;
185
+ }
165
186
  try {
166
187
  checkApiUrl(config.apiUrl, env.AGENTOS_CONNECT_OPERATOR === "1");
167
188
  } catch (err) {
@@ -225,7 +246,22 @@ export function startConnect(
225
246
  return { bin, token: tunnel.token };
226
247
  },
227
248
  });
228
- running = { server, sockets, supervisor, release: lock.release };
249
+ const beat = deps.heartbeat ?? sendHeartbeat;
250
+ const version = appVersion();
251
+ const heartbeat = startHeartbeat({
252
+ beat: () => beat(config, machineKey, version),
253
+ log,
254
+ intervalMs: deps.heartbeatMs,
255
+ onDisabled: () => {
256
+ halted = config.machineId;
257
+ down("disabled in your account");
258
+ },
259
+ onGone: () => {
260
+ halted = config.machineId;
261
+ down("machine no longer linked");
262
+ },
263
+ });
264
+ running = { server, sockets, supervisor, heartbeat, release: lock.release };
229
265
  server.listen(tunnelPort, "127.0.0.1");
230
266
  };
231
267
 
@@ -233,7 +269,12 @@ export function startConnect(
233
269
  const sync = () => {
234
270
  try {
235
271
  const read = readConnect(dir);
236
- const want = read.ok && read.enabled;
272
+ if (
273
+ halted &&
274
+ (!read.ok || !read.enabled || read.files.config.machineId !== halted)
275
+ )
276
+ halted = null;
277
+ const want = read.ok && read.enabled && !halted;
237
278
  if (running && !want) down(read.ok ? "turned off" : read.reason);
238
279
  else if (!running && want) up();
239
280
  else if (!running) up(); // refreshes what the status shows
@@ -8,46 +8,9 @@
8
8
  import { db } from "../db";
9
9
  import { ANTHROPIC, sessionRole } from "../models/roles";
10
10
  import { ctxOf, weighted, type TokenRow } from "./weights";
11
+ import type { CallKind, CallRow, CallSource, CallUsage } from "./types";
11
12
 
12
- export type CallKind = "main" | "sub" | "review" | "gate" | "helper";
13
- export type CallSource = "live" | "transcript" | "check_run" | "helper";
14
-
15
- export interface CallRow {
16
- callKey: string;
17
- at: number;
18
- sessionId: string | null;
19
- taskId: string | null;
20
- workspaceId: string | null;
21
- projectId: string | null;
22
- sessionName: string | null;
23
- role: string | null;
24
- kind: CallKind;
25
- agentType: string | null;
26
- model: string | null;
27
- provider: string;
28
- input: number;
29
- cacheRead: number;
30
- cacheWrite5m: number;
31
- cacheWrite1h: number;
32
- output: number;
33
- ctx: number | null;
34
- source: CallSource;
35
- }
36
-
37
- // What a driver reports for one response, before it's tied to a session.
38
- export type CallUsage = Pick<
39
- CallRow,
40
- | "callKey"
41
- | "at"
42
- | "kind"
43
- | "agentType"
44
- | "model"
45
- | "input"
46
- | "cacheRead"
47
- | "cacheWrite5m"
48
- | "cacheWrite1h"
49
- | "output"
50
- >;
13
+ export type { CallKind, CallRow, CallSource, CallUsage };
51
14
 
52
15
  interface RawUsage {
53
16
  input_tokens?: number;
@@ -0,0 +1,41 @@
1
+ /** Pure usage-ledger types, shared with the mobile app: no imports, nothing server-only. */
2
+
3
+ export type CallKind = "main" | "sub" | "review" | "gate" | "helper";
4
+ export type CallSource = "live" | "transcript" | "check_run" | "helper";
5
+
6
+ export interface CallRow {
7
+ callKey: string;
8
+ at: number;
9
+ sessionId: string | null;
10
+ taskId: string | null;
11
+ workspaceId: string | null;
12
+ projectId: string | null;
13
+ sessionName: string | null;
14
+ role: string | null;
15
+ kind: CallKind;
16
+ agentType: string | null;
17
+ model: string | null;
18
+ provider: string;
19
+ input: number;
20
+ cacheRead: number;
21
+ cacheWrite5m: number;
22
+ cacheWrite1h: number;
23
+ output: number;
24
+ ctx: number | null;
25
+ source: CallSource;
26
+ }
27
+
28
+ // What a driver reports for one response, before it's tied to a session.
29
+ export type CallUsage = Pick<
30
+ CallRow,
31
+ | "callKey"
32
+ | "at"
33
+ | "kind"
34
+ | "agentType"
35
+ | "model"
36
+ | "input"
37
+ | "cacheRead"
38
+ | "cacheWrite5m"
39
+ | "cacheWrite1h"
40
+ | "output"
41
+ >;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.507",
3
+ "version": "0.3.509",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"