@saadnvd1/agent-os 0.3.777 → 0.3.778

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.
@@ -64,15 +64,35 @@ function sessionLine(f: SessionFacts): string {
64
64
  return parts.join(" | ");
65
65
  }
66
66
 
67
- // The sessions tool's answer: one line per session, busiest news first.
67
+ const compactLine = (f: SessionFacts) =>
68
+ `${f.name} (${f.project ?? "no project"}, id ${shortId(f.id)})`;
69
+
70
+ // Nothing to say about it beyond that it is idle.
71
+ const plainIdle = (f: SessionFacts) =>
72
+ f.status === "idle" &&
73
+ !f.activity &&
74
+ !f.task &&
75
+ !f.plan &&
76
+ !f.group &&
77
+ !f.stack &&
78
+ !f.compacting &&
79
+ !f.waits?.on.length &&
80
+ !f.waits?.by.length;
81
+
82
+ // The sessions tool's answer: one line per live session, busiest news first;
83
+ // then the done and plain idle ones, compactly, so a done can be seen to have
84
+ // worked.
68
85
  export function describeSessions(
69
86
  workspace: string,
70
87
  facts: SessionFacts[],
71
88
  groups: readonly string[] = []
72
89
  ): string {
73
90
  if (!facts.length) return `No sessions are running in ${workspace}.`;
91
+ const done = facts.filter((f) => f.done && f.status !== "running");
92
+ const idle = facts.filter((f) => !f.done && plainIdle(f));
93
+ const shown = facts.filter((f) => !done.includes(f) && !idle.includes(f));
74
94
  const rank = { waiting: 0, running: 1, idle: 2, dead: 3 };
75
- const sorted = [...facts].sort((a, b) => rank[a.status] - rank[b.status]);
95
+ const sorted = [...shown].sort((a, b) => rank[a.status] - rank[b.status]);
76
96
  const counts = (["running", "waiting"] as const)
77
97
  .map((s) => {
78
98
  const n = facts.filter((f) => f.status === s).length;
@@ -84,6 +104,10 @@ export function describeSessions(
84
104
  return [
85
105
  `${facts.length} ${noun} in ${workspace}${summary}:`,
86
106
  ...sorted.map(sessionLine),
107
+ ...(idle.length ? [`Idle: ${idle.map(compactLine).join("; ")}.`] : []),
108
+ ...(done.length
109
+ ? [`Done (on Saad's Done shelf): ${done.map(compactLine).join("; ")}.`]
110
+ : []),
87
111
  ...(groups.length
88
112
  ? [`Saad's groups here: ${groups.map((g) => `"${g}"`).join(", ")}.`]
89
113
  : []),
@@ -27,6 +27,7 @@ import { commitTime, repoOf } from "./repo";
27
27
  import { CHECKLESS_MS, checklessFor, ciSettleIn } from "./task-state";
28
28
  import { repoHasCI } from "./repo-ci";
29
29
  import { markStatusDirty } from "../status/dirty";
30
+ import { settledHow } from "../sidebar/shelves";
30
31
 
31
32
  export type FactStatus = "running" | "waiting" | "idle" | "dead";
32
33
 
@@ -46,6 +47,8 @@ export interface SessionFacts {
46
47
  compacting?: boolean;
47
48
  // Saad's group it sits in, when it is in one.
48
49
  group?: string | null;
50
+ // On Saad's Done shelf: marked done (or quiet for days) and not woken since.
51
+ done?: boolean;
49
52
  // Last sign of life, in ms.
50
53
  lastActive: number;
51
54
  // Open waits (aos send --await, aos wait): who it waits on, and who waits on it.
@@ -346,6 +349,7 @@ export async function sessionFacts(
346
349
  compacting:
347
350
  s.view === "chat" && !hostLink(s.host_id) && !!chatCompacting(s.id),
348
351
  group: groups.get(s.session_group_id ?? "") ?? null,
352
+ done: settledHow(s, Date.now()) !== null,
349
353
  lastActive: await lastActiveFor(s),
350
354
  branch: s.branch_name,
351
355
  quick: !!s.quick_of,
@@ -116,7 +116,7 @@ const answer = async (call: Promise<string>): Promise<CallToolResult> => {
116
116
 
117
117
  const DESCRIPTIONS: Record<ToolName, string> = {
118
118
  sessions:
119
- "Every session in this workspace: project, view, status, what it's doing, task/PR/CI state and stack position; then external sessions (tmux sessions AgentOS didn't start, here or on other machines, working in a workspace repository), read-only, with their branch's PR.",
119
+ "Every session in this workspace: project, view, status, what it's doing, task/PR/CI state and stack position; plain idle and done sessions (on Saad's Done shelf) are listed compactly after them; then external sessions (tmux sessions AgentOS didn't start, here or on other machines, working in a workspace repository), read-only, with their branch's PR.",
120
120
  read: "The end of one session's terminal or chat, or an external session's screen, as plain text (capped at about 4k tokens).",
121
121
  cards: "Cards on this workspace's linked LumifyHub boards, by list.",
122
122
  logs: "Session logs AgentOS wrote when tasks merged: the last N headers (task, PR, cost, duration) of the workspace or one project, or one log in full (brief, plan, timeline, usage, outcome). Read straight from the database; nothing is summarised.",
@@ -85,7 +85,8 @@ function commit(sessionName: string, next: Records): boolean {
85
85
  if (same(before, after)) return false;
86
86
  if (after && after.state !== "idle" && after.state !== before?.state)
87
87
  db.prepare(
88
- `UPDATE sessions SET updated_at = datetime('now'), active_at = datetime('now') WHERE tmux_name = ? OR id = ?`
88
+ `UPDATE sessions SET updated_at = datetime('now'), active_at = datetime('now') WHERE (tmux_name = ? OR id = ?)
89
+ AND (settled_at IS NULL OR active_at > settled_at)`
89
90
  ).run(sessionName, getSessionIdFromName(sessionName));
90
91
  return true;
91
92
  }
@@ -1,7 +1,9 @@
1
1
  import { getDb } from "./db";
2
2
 
3
3
  // A session did something: a message, a run starting, a need. Written at most
4
- // once a minute, unless it wakes a snooze or brings a settled session back.
4
+ // once a minute, unless it wakes a snooze. Never for a settled session: what
5
+ // comes after Done (the stop's own events, a status refresh) isn't the work
6
+ // starting again. Only a prompt does that (the chat worker writes it).
5
7
  export function touchActive(sessionId: string): void {
6
8
  try {
7
9
  getDb()
@@ -9,8 +11,8 @@ export function touchActive(sessionId: string): void {
9
11
  `UPDATE sessions SET active_at = datetime('now')
10
12
  WHERE id = ? AND (active_at IS NULL
11
13
  OR active_at < datetime('now', '-60 seconds')
12
- OR (snoozed_at IS NOT NULL AND active_at <= snoozed_at)
13
- OR (settled_at IS NOT NULL AND active_at <= settled_at))`
14
+ OR (snoozed_at IS NOT NULL AND active_at <= snoozed_at))
15
+ AND (settled_at IS NULL OR active_at > settled_at)`
14
16
  )
15
17
  .run(sessionId);
16
18
  } catch (err) {
@@ -220,7 +220,10 @@ export function settledHow(
220
220
  now: number
221
221
  ): "manual" | "auto" | null {
222
222
  const active = activeMs(session, now);
223
- if (session.settled_at && active <= ms(session.settled_at)) return "manual";
223
+ // A settled session's own row is the clock: updated_at moves with
224
+ // bookkeeping, so it never takes a done session off the shelf.
225
+ const own = session.peer_mirror || session.active_at ? active : 0;
226
+ if (session.settled_at && own <= ms(session.settled_at)) return "manual";
224
227
  return now - active >= SETTLE_AFTER_MS ? "auto" : null;
225
228
  }
226
229
 
@@ -421,7 +421,8 @@ async function collect(full = false): Promise<StatusSnapshot> {
421
421
 
422
422
  // Batch update sessions and claude_session_id in a single transaction
423
423
  const updateStatusStmt = db.prepare(
424
- "UPDATE sessions SET updated_at = datetime('now'), active_at = datetime('now') WHERE id = ?"
424
+ `UPDATE sessions SET updated_at = datetime('now'), active_at = datetime('now')
425
+ WHERE id = ? AND (settled_at IS NULL OR active_at > settled_at)`
425
426
  );
426
427
  const updateClaudeIdStmt = db.prepare(
427
428
  "UPDATE sessions SET claude_session_id = ? WHERE id = ? AND (claude_session_id IS NULL OR claude_session_id != ?)"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.777",
3
+ "version": "0.3.778",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"