@saadnvd1/agent-os 0.3.517 → 0.3.518

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 (46) hide show
  1. package/README.md +20 -3
  2. package/app/api/tasks/[id]/arm/route.ts +5 -0
  3. package/app/api/tasks/[id]/chat-tail/route.ts +15 -0
  4. package/app/api/tasks/[id]/done/route.ts +15 -0
  5. package/app/api/tasks/[id]/export/route.ts +6 -1
  6. package/app/api/tasks/[id]/merge/route.ts +9 -1
  7. package/app/api/tasks/[id]/message/route.ts +39 -0
  8. package/app/api/tasks/[id]/moved/route.ts +6 -1
  9. package/app/api/tasks/[id]/resume/route.ts +6 -1
  10. package/app/api/tasks/[id]/stop/route.ts +14 -0
  11. package/app/api/tasks/import/route.ts +11 -1
  12. package/lib/bus/index.ts +15 -2
  13. package/lib/db/migrations.ts +25 -0
  14. package/lib/db/types.ts +7 -0
  15. package/lib/done/bulk.ts +8 -0
  16. package/lib/done/index.ts +15 -0
  17. package/lib/models/plan-step.ts +13 -3
  18. package/lib/orchestrator/act.ts +41 -1
  19. package/lib/orchestrator/armed.ts +6 -1
  20. package/lib/orchestrator/brakes.ts +9 -0
  21. package/lib/orchestrator/brief.ts +2 -1
  22. package/lib/orchestrator/facts.ts +72 -21
  23. package/lib/orchestrator/read.ts +14 -2
  24. package/lib/orchestrator/serve.ts +6 -0
  25. package/lib/orchestrator/shell-guard.ts +15 -0
  26. package/lib/orchestrator/tool-names.ts +1 -0
  27. package/lib/orchestrator/tool-schemas.ts +11 -0
  28. package/lib/orchestrator/tools.ts +1 -0
  29. package/lib/sessions/stop.ts +15 -1
  30. package/lib/tasks/branch-worktree.ts +16 -1
  31. package/lib/tasks/finish.ts +5 -1
  32. package/lib/tasks/held-remote.ts +80 -0
  33. package/lib/tasks/held-routes.ts +33 -0
  34. package/lib/tasks/held.ts +28 -0
  35. package/lib/tasks/import.ts +60 -7
  36. package/lib/tasks/index.ts +11 -1
  37. package/lib/tasks/last-active.ts +25 -0
  38. package/lib/tasks/move-bundle.ts +53 -1
  39. package/lib/tasks/move-carry.ts +102 -0
  40. package/lib/tasks/move-flow.ts +34 -7
  41. package/lib/tasks/move-guard.ts +17 -13
  42. package/lib/tasks/move-recover.ts +2 -0
  43. package/lib/tasks/move.ts +34 -3
  44. package/lib/tasks/remote.ts +3 -0
  45. package/lib/tasks/session.ts +1 -1
  46. package/package.json +1 -1
@@ -0,0 +1,25 @@
1
+ // A session's last sign of life, in ms: its row, its tmux pane, its chat.
2
+
3
+ import { db, type Session } from "../db";
4
+ import { statusDetector } from "../status-detector";
5
+
6
+ export const sqliteMs = (t: string | null | undefined) =>
7
+ t ? Date.parse(`${t.replace(" ", "T")}Z`) || 0 : 0;
8
+
9
+ export function latestChatAt(sessionId: string): number {
10
+ const row = db
11
+ .prepare(
12
+ `SELECT data FROM chat_items WHERE session_id = ? ORDER BY seq DESC LIMIT 1`
13
+ )
14
+ .get(sessionId) as { data: string } | undefined;
15
+ return row
16
+ ? ((JSON.parse(row.data) as { createdAt?: number }).createdAt ?? 0)
17
+ : 0;
18
+ }
19
+
20
+ export const lastActiveOf = (s: Session): number =>
21
+ Math.max(
22
+ sqliteMs(s.updated_at),
23
+ statusDetector.getTimestamp(s.tmux_name) * 1000,
24
+ latestChatAt(s.id)
25
+ );
@@ -14,8 +14,10 @@ export const MAX_TRANSCRIPT_BYTES = 64 * 1024 * 1024;
14
14
  // whole bundle stays under what a linked machine reads of an answer
15
15
  // (lib/hosts/remote-api), since a move back here reads it as one.
16
16
  export const MAX_CHAT_BYTES = 12 * 1024 * 1024;
17
+ // A task's plan file is a page or two; this is far past that.
18
+ export const MAX_PLAN_BYTES = 1024 * 1024;
17
19
  export const MAX_BUNDLE_BYTES =
18
- MAX_TRANSCRIPT_BYTES + MAX_CHAT_BYTES + 1024 * 1024;
20
+ MAX_TRANSCRIPT_BYTES + MAX_CHAT_BYTES + MAX_PLAN_BYTES + 1024 * 1024;
19
21
 
20
22
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
21
23
  export const isSessionId = (id: unknown): id is string =>
@@ -50,6 +52,25 @@ export interface TaskBundle {
50
52
  // A chat task's own state; absent for a terminal task, or from a machine
51
53
  // that predates chat moves.
52
54
  chat?: ChatBundle;
55
+ // What a task started by an orchestrator carries besides its work; absent
56
+ // for any other task, or from a machine that predates these moves.
57
+ orchestrated?: OrchestratedBundle;
58
+ }
59
+
60
+ export interface OrchestratedBundle {
61
+ // The machine whose orchestrator holds the task, for the other machine's
62
+ // screens and refusals.
63
+ heldFrom: string;
64
+ // When the task first started (an ISO time), so a PR opened before the
65
+ // move still counts as the task's.
66
+ since: string | null;
67
+ // The model's provider and a task's own models, as they were; an arriving
68
+ // machine without that provider refuses the task rather than change it.
69
+ modelProvider: string | null;
70
+ modelOverrides: string | null;
71
+ planPhase: "planning" | "implementing" | null;
72
+ // .agentos/plan.md, which is never in git.
73
+ planFile: string | null;
53
74
  }
54
75
 
55
76
  export interface ChatBundle {
@@ -101,6 +122,34 @@ function validateChat(c: ChatBundle): ChatBundle {
101
122
  return { ...c, plan: !!c.plan };
102
123
  }
103
124
 
125
+ const PHASES = ["planning", "implementing"];
126
+
127
+ function validateOrchestrated(o: OrchestratedBundle): OrchestratedBundle {
128
+ if (!o || typeof o !== "object") throw new Error("Bad orchestrated task");
129
+ if (
130
+ !isStrOrNull(o.since) ||
131
+ !isStrOrNull(o.modelProvider) ||
132
+ !isStrOrNull(o.modelOverrides) ||
133
+ !isStrOrNull(o.planFile) ||
134
+ !(o.planPhase === null || PHASES.includes(o.planPhase as string))
135
+ )
136
+ throw new Error("Bad orchestrated task");
137
+ if (o.since !== null && Number.isNaN(Date.parse(o.since)))
138
+ throw new Error("Bad start time");
139
+ if ((o.planFile?.length ?? 0) > MAX_PLAN_BYTES)
140
+ throw new Error("The plan is too large to move");
141
+ return {
142
+ heldFrom: String(o.heldFrom || "another machine")
143
+ .replace(/[^\w.@ -]/g, "")
144
+ .slice(0, 100),
145
+ since: o.since,
146
+ modelProvider: o.modelProvider,
147
+ modelOverrides: o.modelOverrides,
148
+ planPhase: o.planPhase,
149
+ planFile: o.planFile,
150
+ };
151
+ }
152
+
104
153
  export function validateBundle(b: TaskBundle): TaskBundle {
105
154
  if (!b || typeof b !== "object") throw new Error("No task to import");
106
155
  if (!isSessionId(b.moveId)) throw new Error("Bad move id");
@@ -130,6 +179,9 @@ export function validateBundle(b: TaskBundle): TaskBundle {
130
179
  }
131
180
  return {
132
181
  ...b,
182
+ orchestrated: b.orchestrated
183
+ ? validateOrchestrated(b.orchestrated)
184
+ : undefined,
133
185
  chat: b.chat ? validateChat(b.chat) : undefined,
134
186
  name: b.name.slice(0, 200),
135
187
  from: String(b.from || "another machine")
@@ -0,0 +1,102 @@
1
+ /**
2
+ * An orchestrator's state about a task is keyed by the task's session id, and
3
+ * a move gives the task a new one on each machine. Carrying re-keys that state
4
+ * from the old id to the new, inside the transaction that makes the new row,
5
+ * so the gates, failure counts, asks, scope changes and events judge the task
6
+ * the same after a move. move-carry.test.ts fails on any table that keys by a
7
+ * session and is in neither list, so a new one gets decided.
8
+ */
9
+
10
+ import type Database from "better-sqlite3";
11
+ import { db } from "../db";
12
+
13
+ // table, column, and how the id sits in it: the whole value, `task:<id>`, or
14
+ // the tail of a `<condition>:<id>` key.
15
+ export const REKEYED = [
16
+ ["orchestrator_starts", "target", "id"],
17
+ ["orchestrator_checks", "session_id", "id"],
18
+ ["orchestrator_gate_failures", "session_id", "id"],
19
+ ["orchestrator_gate_failure_commits", "session_id", "id"],
20
+ ["orchestrator_asks", "subject", "task"],
21
+ ["brief_amendments", "session_id", "id"],
22
+ ["task_pr_wakes", "session_id", "id"],
23
+ ["orchestrator_events", "subject", "id"],
24
+ ["orchestrator_events", "key", "tail"],
25
+ ["check_runs", "session_id", "id"],
26
+ ["away_actions", "subject", "task"],
27
+ ["task_queue", "after_task", "id"],
28
+ ] as const;
29
+
30
+ // Session-keyed columns that stay on the old id, and why.
31
+ export const KEPT = [
32
+ ["orchestrator_event_log", "subject", "history is never rewritten"],
33
+ ["stack_items", "session_id", "stacked tasks don't move"],
34
+ [
35
+ "task_queue",
36
+ "after_any",
37
+ "a JSON list; a queued task waits on any of them",
38
+ ],
39
+ ] as const;
40
+
41
+ // Copied onto the new row where it has no value.
42
+ export const CARRIED_COLUMNS = [
43
+ "started_by",
44
+ "plan_decided",
45
+ "plan_reason",
46
+ "plan_phase",
47
+ "model_provider",
48
+ "model_overrides",
49
+ "merge_armed_at",
50
+ "merge_armed_seen",
51
+ "rebased_from",
52
+ "rebased_to",
53
+ "rebase_asked_head",
54
+ "pr_wake_since",
55
+ "pr_url",
56
+ "pr_number",
57
+ "pr_status",
58
+ ] as const;
59
+
60
+ /** The orchestration columns that make a session "an orchestrator's task". */
61
+ export const orchestrationOf = (session: Record<string, unknown>) =>
62
+ Object.fromEntries(
63
+ CARRIED_COLUMNS.map((c) => [c, session[c] ?? null] as const)
64
+ );
65
+
66
+ export function carryTaskState(
67
+ fromId: string,
68
+ toId: string,
69
+ handle: Database.Database = db
70
+ ): void {
71
+ if (fromId === toId) return;
72
+ for (const [table, col, shape] of REKEYED) {
73
+ if (shape === "id")
74
+ handle
75
+ .prepare(`UPDATE ${table} SET ${col} = ? WHERE ${col} = ?`)
76
+ .run(toId, fromId);
77
+ else if (shape === "task")
78
+ handle
79
+ .prepare(`UPDATE ${table} SET ${col} = ? WHERE ${col} = ?`)
80
+ .run(`task:${toId}`, `task:${fromId}`);
81
+ else
82
+ handle
83
+ .prepare(
84
+ `UPDATE OR IGNORE ${table}
85
+ SET ${col} = substr(${col}, 1, length(${col}) - ?) || ?
86
+ WHERE ${col} LIKE ?`
87
+ )
88
+ .run(fromId.length, toId, `%:${fromId}`);
89
+ }
90
+ const from = handle
91
+ .prepare(`SELECT * FROM sessions WHERE id = ?`)
92
+ .get(fromId) as Record<string, unknown> | undefined;
93
+ if (!from) return;
94
+ const sets = [...CARRIED_COLUMNS, "task_since"]
95
+ .map((c) => `${c} = COALESCE(${c}, @${c})`)
96
+ .join(", ");
97
+ handle.prepare(`UPDATE sessions SET ${sets} WHERE id = @id`).run({
98
+ ...orchestrationOf(from),
99
+ task_since: (from.task_since as string | null) ?? from.created_at ?? null,
100
+ id: toId,
101
+ });
102
+ }
@@ -11,7 +11,10 @@ import { isRemoteHost } from "../hosts";
11
11
  import { requireHostLink, type HostLink } from "../hosts/remote-api";
12
12
  import { getTaskSession } from "./session";
13
13
  import { exportOrResume, markMoved, resumeTask, type TaskBundle } from "./move";
14
- import { moveRefusal } from "./move-guard";
14
+ import { isOrchestrated, moveRefusal } from "./move-guard";
15
+ import { holderRefusal } from "./held";
16
+ import { isPaused } from "../orchestrator/pause";
17
+ import { sessionWorkspace } from "../models/resolve";
15
18
  import { arrived as arrivedHere, importTask } from "./import";
16
19
  import {
17
20
  forgetHostTasks,
@@ -34,7 +37,8 @@ const LIVE = new Set(["running", "moving"]);
34
37
  const g = globalThis as unknown as { __agentosMovesInFlight?: Set<string> };
35
38
  const inFlight = (g.__agentosMovesInFlight ??= new Set());
36
39
 
37
- export async function moveTask(id: string, toHostId: string): Promise<Session> {
40
+ /** Why this task can't move to that machine now, as an error; or nothing. */
41
+ export function assertMovable(id: string, toHostId: string): Session {
38
42
  const session = getTaskSession(id);
39
43
  if (!LIVE.has(session.task_status ?? ""))
40
44
  throw new Error(`Task is already ${session.task_status}`);
@@ -47,9 +51,22 @@ export async function moveTask(id: string, toHostId: string): Promise<Session> {
47
51
  : "It's already on this machine"
48
52
  );
49
53
  }
54
+ // A person here can't move what another machine's orchestrator holds.
55
+ const held = holderRefusal(session, null);
56
+ if (held) throw new Error(held);
50
57
  const refusal = moveRefusal(session);
51
58
  if (refusal) throw new Error(refusal);
59
+ // Pause stops the orchestrator's new work, and a move is some.
60
+ const workspace = sessionWorkspace(session);
61
+ if (workspace && isOrchestrated(session) && isPaused(workspace))
62
+ throw new Error("The orchestrator is paused; moves wait for Resume");
52
63
  if (inFlight.has(id)) throw new Error("It's already moving");
64
+ return session;
65
+ }
66
+
67
+ export async function moveTask(id: string, toHostId: string): Promise<Session> {
68
+ const session = assertMovable(id, toHostId);
69
+ const toRemote = isRemoteHost(toHostId);
53
70
  inFlight.add(id);
54
71
  try {
55
72
  const moved = toRemote
@@ -66,19 +83,27 @@ export async function moveTask(id: string, toHostId: string): Promise<Session> {
66
83
  }
67
84
 
68
85
  // A machine on an older AgentOS would take a chat task as a terminal one,
69
- // or refuse to hand one over.
70
- async function requireChatMoves(link: HostLink): Promise<void> {
86
+ // or an orchestrator's task as a plain one that anyone there could merge, or
87
+ // refuse to hand one over.
88
+ async function requireCapability(
89
+ link: HostLink,
90
+ capability: string,
91
+ what: string
92
+ ): Promise<void> {
71
93
  const { capabilities = [] } = await hostTasks(link, true).catch((err) => {
72
94
  throw new Error(
73
- `Can't ask ${link.hostName} whether it takes chat tasks: ${(err as Error).message}`
95
+ `Can't ask ${link.hostName} whether it takes ${what}: ${(err as Error).message}`
74
96
  );
75
97
  });
76
- if (!capabilities.includes("chat-move"))
98
+ if (!capabilities.includes(capability))
77
99
  throw new Error(
78
- `${link.hostName}'s AgentOS can't move chat tasks yet; update it first`
100
+ `${link.hostName}'s AgentOS can't move ${what} yet; update it first`
79
101
  );
80
102
  }
81
103
 
104
+ const requireChatMoves = (link: HostLink) =>
105
+ requireCapability(link, "chat-move", "chat tasks");
106
+
82
107
  const unconfirmed = (where: string, err: unknown) =>
83
108
  new Error(
84
109
  `Couldn't confirm the move with ${where} (${(err as Error).message}). It's paused as "moving": press Move again to finish it.`
@@ -88,6 +113,8 @@ async function moveOut(session: Session, hostId: string): Promise<Session> {
88
113
  const link = requireHostLink(hostId);
89
114
  const chat = session.view === "chat";
90
115
  if (chat) await requireChatMoves(link);
116
+ if (isOrchestrated(session))
117
+ await requireCapability(link, "orchestrated-move", "orchestrator tasks");
91
118
  startProgress(
92
119
  session.id,
93
120
  link.hostName,
@@ -1,13 +1,24 @@
1
1
  /**
2
2
  * Tasks that can't move: one waiting on a person (an escalated gate or an
3
- * open ask), on a LumifyHub card, in a stack, or started by an orchestrator
4
- * (its brakes, ceiling and Pause count only what runs here). Their guard state is keyed
5
- * by this session and wouldn't follow it, so the other machine could merge
6
- * what's waiting on them.
3
+ * open ask), on a LumifyHub card, or in a stack. A task an orchestrator
4
+ * started does move: its state is re-keyed to the new session (move-carry),
5
+ * and the machine it lands on holds it for that orchestrator's device
6
+ * (held.ts), so nothing there merges it.
7
7
  */
8
8
 
9
9
  import { db, type Session } from "../db";
10
10
  import { escalations } from "../orchestrator/gates";
11
+ import { taskSubject } from "../orchestrator/asks";
12
+
13
+ /** Started by an orchestrator, or held for one by the machine it left. */
14
+ export function isOrchestrated(session: Session): boolean {
15
+ if (session.started_by || session.held_by_device) return true;
16
+ return !!db
17
+ .prepare(
18
+ `SELECT 1 FROM orchestrator_starts WHERE kind != 'stack' AND target = ? LIMIT 1`
19
+ )
20
+ .get(session.id);
21
+ }
11
22
 
12
23
  export function moveRefusal(session: Session): string | null {
13
24
  if (session.quick_of)
@@ -16,9 +27,9 @@ export function moveRefusal(session: Session): string | null {
16
27
  return "It's waiting on you (an escalated gate), so it stays here";
17
28
  const ask = db
18
29
  .prepare(
19
- `SELECT 1 FROM orchestrator_asks WHERE subject = ? AND status = 'open' LIMIT 1`
30
+ `SELECT 1 FROM orchestrator_asks WHERE subject IN (?, ?) AND status = 'open' LIMIT 1`
20
31
  )
21
- .get(session.id);
32
+ .get(session.id, taskSubject(session.id));
22
33
  if (ask) return "It has an open ask waiting on you, so it stays here";
23
34
  const row = db
24
35
  .prepare(
@@ -31,12 +42,5 @@ export function moveRefusal(session: Session): string | null {
31
42
  | undefined;
32
43
  if (row?.lh_card_id) return "Card tasks run on this machine only for now";
33
44
  if (row?.stacked) return "Stacked tasks run on this machine only for now";
34
- const orchestrated = db
35
- .prepare(
36
- `SELECT 1 FROM orchestrator_starts WHERE kind != 'stack' AND target = ? LIMIT 1`
37
- )
38
- .get(session.id);
39
- if (orchestrated)
40
- return "An orchestrator's tasks run on this machine only for now";
41
45
  return null;
42
46
  }
@@ -12,6 +12,7 @@ import { hostApi, hostLink, type HostLink } from "../hosts/remote-api";
12
12
  import { getTaskSession } from "./session";
13
13
  import { markMoved, resumeTask } from "./move";
14
14
  import { forgetHostTasks, mirrorTask, unknownOutcome } from "./remote";
15
+ import { carryTaskState } from "./move-carry";
15
16
 
16
17
  /** POST, repeated when the outcome is unknown (the calls are idempotent). */
17
18
  export async function tell(
@@ -50,6 +51,7 @@ export function settleMovedOut(
50
51
  db.transaction(() => {
51
52
  markMoved(session.id, hostName);
52
53
  mirrorTask(hostId, session.project_id, there);
54
+ carryTaskState(session.id, there.id);
53
55
  })();
54
56
  forgetHostTasks(hostId);
55
57
  return db
package/lib/tasks/move.ts CHANGED
@@ -6,18 +6,26 @@
6
6
  * it off and a second move of it is refused.
7
7
  */
8
8
 
9
+ import fs from "fs";
9
10
  import os from "os";
11
+ import path from "path";
10
12
  import { db, type Session } from "../db";
11
13
  import { getProject } from "../projects";
12
14
  import { endTmuxSession } from "../tmux/end";
13
15
  import { launchClaude } from "../agents/launch";
14
16
  import { buildTaskBrief } from "./brief";
15
17
  import { git as runGit } from "../git-exec";
16
- import { getTaskSession } from "./session";
18
+ import { getTaskSession, isoUTC } from "./session";
17
19
  import { projectRef } from "./project-ref";
18
- import { moveRefusal } from "./move-guard";
19
- import { MAX_TRANSCRIPT_BYTES, type TaskBundle } from "./move-bundle";
20
+ import { isOrchestrated, moveRefusal } from "./move-guard";
21
+ import {
22
+ MAX_PLAN_BYTES,
23
+ MAX_TRANSCRIPT_BYTES,
24
+ type OrchestratedBundle,
25
+ type TaskBundle,
26
+ } from "./move-bundle";
20
27
  import { stepProgress } from "./move-progress";
28
+ import { PLAN_FILE } from "../models/plan-step";
21
29
  import { dropSessionDatabase } from "../project-config/database";
22
30
  import { releaseChat } from "../chat/runner";
23
31
  import { collectWhileMoving } from "../chat/hold";
@@ -98,6 +106,28 @@ function claimMoving(id: string, to: string): { fresh: boolean } {
98
106
  // Set by exportTask: did this call stop a running agent, or retry a move?
99
107
  const freshClaims = new Set<string>();
100
108
 
109
+ const readPlanFile = (cwd: string): string | null => {
110
+ try {
111
+ const text = fs.readFileSync(path.join(cwd, PLAN_FILE), "utf8");
112
+ return text.length > MAX_PLAN_BYTES ? null : text;
113
+ } catch {
114
+ return null;
115
+ }
116
+ };
117
+
118
+ // What an orchestrator's task carries besides its work (the orchestrator's
119
+ // own state is re-keyed here; see move-carry).
120
+ function orchestratedBundle(session: Session): OrchestratedBundle {
121
+ return {
122
+ heldFrom: session.held_from || os.hostname(),
123
+ since: isoUTC(session.task_since ?? session.created_at),
124
+ modelProvider: session.model_provider ?? null,
125
+ modelOverrides: session.model_overrides ?? null,
126
+ planPhase: session.plan_phase ?? null,
127
+ planFile: readPlanFile(session.working_directory),
128
+ };
129
+ }
130
+
101
131
  /**
102
132
  * Stops the agent and pushes everything. On failure the task stays moving;
103
133
  * the caller resumes it (exportOrResume) when it knows nothing arrived.
@@ -163,6 +193,7 @@ export async function exportTask(id: string, to: string): Promise<TaskBundle> {
163
193
  ? { sessionId: claudeId, cwd, home: os.homedir(), transcript }
164
194
  : null,
165
195
  chat: chatBundle,
196
+ orchestrated: isOrchestrated(now) ? orchestratedBundle(now) : undefined,
166
197
  };
167
198
  } finally {
168
199
  packed();
@@ -27,6 +27,9 @@ export const TASK_CAPABILITIES = [
27
27
  "chat-turn",
28
28
  "keyed-start",
29
29
  "chat-move",
30
+ // Holds an orchestrator's task for the machine that sent it, and answers
31
+ // its messages, reads, stops and done (lib/tasks/held.ts).
32
+ "orchestrated-move",
30
33
  ] as const;
31
34
 
32
35
  export interface HostTasks {
@@ -170,7 +170,7 @@ export async function prFor(
170
170
  const branch = renamed ?? session.branch_name;
171
171
  if (!branch) return null;
172
172
  // A PR older than the task is an earlier use of the branch's name.
173
- const since = isoUTC(session.created_at);
173
+ const since = isoUTC(session.task_since ?? session.created_at);
174
174
  let pr: TaskPR | null;
175
175
  if (renamed) {
176
176
  // Follow a rename only on gh's answer: a failure leaves the row put.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.517",
3
+ "version": "0.3.518",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"