@saadnvd1/agent-os 0.3.553 → 0.3.556

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 (45) hide show
  1. package/app/api/sessions/[id]/done/route.test.ts +70 -0
  2. package/app/api/sessions/[id]/done/route.ts +32 -6
  3. package/app/api/updates/check/route.ts +17 -0
  4. package/app/api/updates/route.test.ts +205 -0
  5. package/app/api/updates/route.ts +11 -0
  6. package/app/api/updates/run/route.ts +50 -0
  7. package/app/api/updates/settings/route.ts +25 -0
  8. package/components/Chat/ChatPanel.tsx +18 -5
  9. package/components/Chat/Messages.test.ts +44 -0
  10. package/components/Chat/Messages.tsx +29 -7
  11. package/components/Chat/composer/ComposerBody.tsx +5 -2
  12. package/components/Chat/composer/hook.test.ts +24 -0
  13. package/components/Chat/composer/insert.ts +18 -0
  14. package/components/SessionList/useDoneAction.test.ts +149 -0
  15. package/components/SessionList/useDoneAction.ts +17 -12
  16. package/components/Settings/SettingsDialog.test.ts +3 -0
  17. package/components/Settings/SettingsDialog.tsx +9 -0
  18. package/components/Settings/commands.ts +9 -0
  19. package/components/Settings/entry-points.test.ts +1 -0
  20. package/components/Updates/UpdateActions.tsx +93 -0
  21. package/components/Updates/UpdateBanner.test.ts +198 -0
  22. package/components/Updates/UpdateBanner.tsx +212 -0
  23. package/components/Updates/UpdatesSection.tsx +150 -0
  24. package/components/Updates/index.ts +2 -0
  25. package/components/views/DesktopView.tsx +2 -0
  26. package/components/views/MobileView.tsx +2 -0
  27. package/data/done/index.ts +33 -7
  28. package/data/updates/index.ts +63 -0
  29. package/hooks/useDismissedUpdate.ts +56 -0
  30. package/hooks/useReloadWhenUpdated.ts +19 -0
  31. package/lib/chat/quote.ts +5 -0
  32. package/lib/done/deadline.ts +45 -0
  33. package/lib/done/index.ts +4 -1
  34. package/lib/settings-url.ts +1 -0
  35. package/lib/updates/banner.ts +114 -0
  36. package/lib/updates/check.ts +240 -0
  37. package/lib/updates/run.ts +452 -0
  38. package/lib/updates/settings.ts +38 -0
  39. package/lib/updates/version.ts +30 -0
  40. package/lib/updates/view.ts +80 -0
  41. package/package.json +1 -1
  42. package/scripts/redeploy +4 -0
  43. package/scripts/update-run.mjs +82 -0
  44. package/scripts/update-run.test.ts +64 -0
  45. package/server.ts +16 -0
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+ // Runs one update, started detached by the server so it survives the restart.
3
+ // Runs scripts/redeploy, follows its "::phase <name>" lines into the state
4
+ // file the server and the browser read, and writes how it ended.
5
+ // AGENTOS_UPDATE_STATE the state file (JSON, written atomically)
6
+ // AGENTOS_RESTART the restart command; none means "built, restart it yourself"
7
+ // AGENTOS_UPDATE_REDEPLOY the script to run (tests); default scripts/redeploy
8
+ import { spawn } from "node:child_process";
9
+ import fs from "node:fs";
10
+ import path from "node:path";
11
+ import readline from "node:readline";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ const file = process.env.AGENTOS_UPDATE_STATE;
15
+ if (!file) {
16
+ console.error("AGENTOS_UPDATE_STATE is not set");
17
+ process.exit(2);
18
+ }
19
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
20
+ const script =
21
+ process.env.AGENTOS_UPDATE_REDEPLOY || path.join(root, "scripts/redeploy");
22
+ const PHASES = new Set(["pulling", "installing", "building", "restarting"]);
23
+
24
+ function read() {
25
+ try {
26
+ return JSON.parse(fs.readFileSync(file, "utf8"));
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ function write(patch) {
32
+ const run = { ...(read() ?? {}), ...patch };
33
+ const tmp = `${file}.${process.pid}.tmp`;
34
+ fs.writeFileSync(tmp, JSON.stringify(run));
35
+ fs.renameSync(tmp, file);
36
+ }
37
+
38
+ const tail = [];
39
+ const keep = (line) => {
40
+ tail.push(line);
41
+ if (tail.length > 30) tail.shift();
42
+ };
43
+
44
+ const child = spawn("bash", [script], {
45
+ cwd: root,
46
+ env: process.env,
47
+ stdio: ["ignore", "pipe", "pipe"],
48
+ });
49
+ for (const stream of [child.stdout, child.stderr]) {
50
+ readline.createInterface({ input: stream }).on("line", (line) => {
51
+ const m = /^::phase (\w+)$/.exec(line);
52
+ if (m && PHASES.has(m[1])) return write({ phase: m[1] });
53
+ keep(line);
54
+ console.log(line);
55
+ });
56
+ }
57
+ child.on("error", (error) =>
58
+ write({
59
+ phase: "failed",
60
+ finishedAt: Date.now(),
61
+ error: error.message,
62
+ log: tail,
63
+ })
64
+ );
65
+ child.on("close", (code) => {
66
+ // The server may have marked this done already when it came back up.
67
+ const phase = read()?.phase;
68
+ if (code === 0) {
69
+ write({
70
+ phase: process.env.AGENTOS_RESTART ? "done" : "built",
71
+ finishedAt: Date.now(),
72
+ log: tail,
73
+ });
74
+ } else if (phase !== "done") {
75
+ write({
76
+ phase: "failed",
77
+ finishedAt: Date.now(),
78
+ error: `The update failed (exit ${code}). The running server was not changed unless it says otherwise below.`,
79
+ log: tail,
80
+ });
81
+ }
82
+ });
@@ -0,0 +1,64 @@
1
+ import { execFile } from "child_process";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { promisify } from "util";
6
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
7
+
8
+ const exec = promisify(execFile);
9
+ const runner = path.join(__dirname, "update-run.mjs");
10
+
11
+ let dir: string;
12
+ beforeEach(() => {
13
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), "aos-runner-"));
14
+ fs.writeFileSync(
15
+ path.join(dir, "state.json"),
16
+ JSON.stringify({ id: "r1", phase: "starting", from: "a" })
17
+ );
18
+ });
19
+ afterEach(() => fs.rmSync(dir, { recursive: true, force: true }));
20
+
21
+ const go = async (script: string, restart?: string) => {
22
+ const redeploy = path.join(dir, "redeploy");
23
+ fs.writeFileSync(redeploy, `#!/usr/bin/env bash\n${script}\n`, {
24
+ mode: 0o755,
25
+ });
26
+ const env: NodeJS.ProcessEnv = {
27
+ ...process.env,
28
+ AGENTOS_UPDATE_STATE: path.join(dir, "state.json"),
29
+ AGENTOS_UPDATE_REDEPLOY: redeploy,
30
+ };
31
+ if (restart) env.AGENTOS_RESTART = restart;
32
+ else delete env.AGENTOS_RESTART;
33
+ await exec(process.execPath, [runner], { env }).catch(() => {});
34
+ return JSON.parse(fs.readFileSync(path.join(dir, "state.json"), "utf8"));
35
+ };
36
+
37
+ describe("scripts/update-run.mjs", () => {
38
+ it("finishes done after a restart command, keeping the id and the log", async () => {
39
+ const s = await go(
40
+ 'echo "::phase pulling"\necho hello\necho "::phase restarting"',
41
+ "x"
42
+ );
43
+ expect(s).toMatchObject({ id: "r1", phase: "done", from: "a" });
44
+ expect(s.log).toEqual(["hello"]);
45
+ expect(s.finishedAt).toBeTypeOf("number");
46
+ });
47
+
48
+ it("finishes built when there was nothing to restart with", async () => {
49
+ expect((await go("echo ok")).phase).toBe("built");
50
+ });
51
+
52
+ it("fails with the last lines of output when redeploy fails", async () => {
53
+ const s = await go("echo boom >&2\nexit 3");
54
+ expect(s.phase).toBe("failed");
55
+ expect(s.error).toContain("exit 3");
56
+ expect(s.log).toContain("boom");
57
+ });
58
+
59
+ it("keeps only the last 30 lines", async () => {
60
+ const s = await go("for i in $(seq 1 50); do echo line$i; done\nexit 1");
61
+ expect(s.log).toHaveLength(30);
62
+ expect(s.log.at(-1)).toBe("line50");
63
+ });
64
+ });
package/server.ts CHANGED
@@ -44,6 +44,11 @@ import {
44
44
  import { lanEnabled } from "./lib/security/network-settings";
45
45
  import { startConnect } from "./lib/connect/serve";
46
46
  import { serverPort } from "./lib/connect/config";
47
+ import { startUpdateChecks, updateChecksEnabled } from "./lib/updates/check";
48
+ import {
49
+ reconcileOnBoot as reconcileUpdateRun,
50
+ startIdleWatcher,
51
+ } from "./lib/updates/run";
47
52
  import { startTailnetHttps } from "./lib/security/tailnet-https";
48
53
  import {
49
54
  setStatusSource,
@@ -413,6 +418,17 @@ app.prepare().then(async () => {
413
418
  if (process.env.AGENTOS_ORCHESTRATOR !== "off") startOrchestratorWatcher();
414
419
  // Schedules tick here, once a minute, and nowhere else.
415
420
  if (schedulesEnabled(process.env)) startScheduler(realDeps);
421
+ // Looking for a newer version, and running an update only when one is
422
+ // asked for (the banner, Settings > Updates).
423
+ try {
424
+ reconcileUpdateRun();
425
+ } catch (error) {
426
+ console.error("Reconciling the update run failed:", error);
427
+ }
428
+ if (updateChecksEnabled()) {
429
+ startUpdateChecks();
430
+ startIdleWatcher();
431
+ }
416
432
  startSettling();
417
433
  // Sessions that sat in Done for 30 days are archived.
418
434
  startAutoArchive();