@saadnvd1/agent-os 0.3.489 → 0.3.493
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.
- package/README.md +32 -2
- package/components/Chat/ChatPanel.tsx +8 -2
- package/components/Chat/Tools.tsx +9 -3
- package/components/SessionList/OrchestratorPinRow.test.ts +2 -0
- package/components/SessionList/OrchestratorPinRow.tsx +36 -0
- package/components/SessionList/RowContext.tsx +4 -0
- package/components/SessionList/SessionRow.tsx +17 -4
- package/components/SessionList/SessionShelves.tsx +8 -2
- package/components/SessionList/SidebarBody.tsx +17 -1
- package/components/SessionList/index.tsx +4 -0
- package/components/SessionList/useSidebarData.ts +64 -18
- package/data/chat/useChat.ts +12 -1
- package/lib/chat/bodyCache.ts +69 -0
- package/lib/chat/store.ts +89 -0
- package/lib/chat/worker/host.ts +11 -3
- package/lib/orchestrator/brief.ts +1 -1
- package/lib/orchestrator/shell-guard.ts +536 -0
- package/lib/orchestrator/shell-parse.ts +289 -0
- package/lib/orchestrator/tool-names.ts +13 -22
- package/lib/orchestrator/write-guard.ts +54 -57
- package/lib/sidebar/queued.ts +3 -0
- package/lib/sidebar/rollup.ts +58 -0
- package/lib/sidebar/shelves.ts +24 -0
- package/lib/tasks/queue.ts +3 -0
- package/package.json +1 -1
- package/stores/sidebarUi.ts +15 -3
- package/lib/orchestrator/git-guard.ts +0 -201
package/lib/chat/store.ts
CHANGED
|
@@ -4,22 +4,102 @@ import type { ChatItem } from "./events";
|
|
|
4
4
|
// Items keep the position they were first written at, so replacing one
|
|
5
5
|
// (a finished tool call, a streamed message's final text) never reorders.
|
|
6
6
|
export function saveItem(sessionId: string, item: ChatItem): void {
|
|
7
|
+
flushItems();
|
|
7
8
|
db.prepare(
|
|
8
9
|
`INSERT INTO chat_items (session_id, item_id, seq, data)
|
|
9
10
|
VALUES (?, ?, (SELECT COALESCE(MAX(seq), 0) + 1 FROM chat_items WHERE session_id = ?), ?)
|
|
10
11
|
ON CONFLICT(session_id, item_id) DO UPDATE SET data = excluded.data`
|
|
11
12
|
).run(sessionId, item.id, sessionId, JSON.stringify(item));
|
|
13
|
+
seqs.delete(sessionId);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// The streaming path. Every update of an item is a write, so they're held
|
|
17
|
+
// in memory (the latest state of each item) and written together, in one
|
|
18
|
+
// transaction, every FLUSH_MS. Callers flush at anything that mustn't be
|
|
19
|
+
// lost: an item finishing, a turn ending, an error, an interrupt, a close.
|
|
20
|
+
// Every reader here flushes first; one that queries the table itself may be
|
|
21
|
+
// up to FLUSH_MS behind.
|
|
22
|
+
export const FLUSH_MS = 250;
|
|
23
|
+
|
|
24
|
+
type Pending = { seq: number; data: string };
|
|
25
|
+
type Seqs = { next: number; of: Map<string, number> };
|
|
26
|
+
|
|
27
|
+
const pending = new Map<string, Map<string, Pending>>();
|
|
28
|
+
const seqs = new Map<string, Seqs>();
|
|
29
|
+
let timer: NodeJS.Timeout | undefined;
|
|
30
|
+
|
|
31
|
+
// Seeded once from the table, then kept: an item gets its seq the first
|
|
32
|
+
// time it's seen, the same one it would have got from MAX(seq) + 1.
|
|
33
|
+
function seqsOf(sessionId: string): Seqs {
|
|
34
|
+
let s = seqs.get(sessionId);
|
|
35
|
+
if (!s) {
|
|
36
|
+
const row = db
|
|
37
|
+
.prepare(
|
|
38
|
+
`SELECT COALESCE(MAX(seq), 0) AS m FROM chat_items WHERE session_id = ?`
|
|
39
|
+
)
|
|
40
|
+
.get(sessionId) as { m: number };
|
|
41
|
+
s = { next: row.m + 1, of: new Map() };
|
|
42
|
+
seqs.set(sessionId, s);
|
|
43
|
+
}
|
|
44
|
+
return s;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function queueItem(
|
|
48
|
+
sessionId: string,
|
|
49
|
+
item: ChatItem,
|
|
50
|
+
{ flush = false }: { flush?: boolean } = {}
|
|
51
|
+
): void {
|
|
52
|
+
const s = seqsOf(sessionId);
|
|
53
|
+
let seq = s.of.get(item.id);
|
|
54
|
+
if (seq === undefined) {
|
|
55
|
+
seq = s.next++;
|
|
56
|
+
s.of.set(item.id, seq);
|
|
57
|
+
}
|
|
58
|
+
let items = pending.get(sessionId);
|
|
59
|
+
if (!items) pending.set(sessionId, (items = new Map()));
|
|
60
|
+
items.set(item.id, { seq, data: JSON.stringify(item) });
|
|
61
|
+
if (flush) flushItems();
|
|
62
|
+
else if (!timer) {
|
|
63
|
+
timer = setTimeout(flushItems, FLUSH_MS);
|
|
64
|
+
timer.unref();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function flushItems(): void {
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
timer = undefined;
|
|
71
|
+
if (!pending.size) return;
|
|
72
|
+
try {
|
|
73
|
+
const insert = db.prepare(
|
|
74
|
+
`INSERT INTO chat_items (session_id, item_id, seq, data) VALUES (?, ?, ?, ?)
|
|
75
|
+
ON CONFLICT(session_id, item_id) DO UPDATE SET data = excluded.data`
|
|
76
|
+
);
|
|
77
|
+
db.transaction(() => {
|
|
78
|
+
for (const [sessionId, items] of pending)
|
|
79
|
+
for (const [itemId, p] of items)
|
|
80
|
+
insert.run(sessionId, itemId, p.seq, p.data);
|
|
81
|
+
})();
|
|
82
|
+
pending.clear();
|
|
83
|
+
} catch (error) {
|
|
84
|
+
// Kept for the next try rather than lost.
|
|
85
|
+
console.error("Chat items not written:", error);
|
|
86
|
+
timer = setTimeout(flushItems, FLUSH_MS);
|
|
87
|
+
timer.unref();
|
|
88
|
+
}
|
|
12
89
|
}
|
|
13
90
|
|
|
14
91
|
// Moves an item after every other, as if first written now.
|
|
15
92
|
export function moveToEnd(sessionId: string, itemId: string): void {
|
|
93
|
+
flushItems();
|
|
16
94
|
db.prepare(
|
|
17
95
|
`UPDATE chat_items SET seq = (SELECT MAX(seq) + 1 FROM chat_items WHERE session_id = ?)
|
|
18
96
|
WHERE session_id = ? AND item_id = ?`
|
|
19
97
|
).run(sessionId, sessionId, itemId);
|
|
98
|
+
seqs.delete(sessionId);
|
|
20
99
|
}
|
|
21
100
|
|
|
22
101
|
export function listItems(sessionId: string): ChatItem[] {
|
|
102
|
+
flushItems();
|
|
23
103
|
return (
|
|
24
104
|
db
|
|
25
105
|
.prepare(`SELECT data FROM chat_items WHERE session_id = ? ORDER BY seq`)
|
|
@@ -31,6 +111,7 @@ export function getItem(
|
|
|
31
111
|
sessionId: string,
|
|
32
112
|
itemId: string
|
|
33
113
|
): ChatItem | undefined {
|
|
114
|
+
flushItems();
|
|
34
115
|
const row = db
|
|
35
116
|
.prepare(`SELECT data FROM chat_items WHERE session_id = ? AND item_id = ?`)
|
|
36
117
|
.get(sessionId, itemId) as { data: string } | undefined;
|
|
@@ -38,6 +119,7 @@ export function getItem(
|
|
|
38
119
|
}
|
|
39
120
|
|
|
40
121
|
export function hasItem(sessionId: string, itemId: string): boolean {
|
|
122
|
+
flushItems();
|
|
41
123
|
return !!db
|
|
42
124
|
.prepare(`SELECT 1 FROM chat_items WHERE session_id = ? AND item_id = ?`)
|
|
43
125
|
.get(sessionId, itemId);
|
|
@@ -49,6 +131,7 @@ export function lastItems(
|
|
|
49
131
|
n: number,
|
|
50
132
|
kind?: ChatItem["kind"]
|
|
51
133
|
): ChatItem[] {
|
|
134
|
+
flushItems();
|
|
52
135
|
const rows = (
|
|
53
136
|
kind
|
|
54
137
|
? db
|
|
@@ -69,6 +152,7 @@ export function lastItems(
|
|
|
69
152
|
// What the agent has said since the last message to it, as text: where a
|
|
70
153
|
// chat task's BLOCKED: line stays until someone answers it.
|
|
71
154
|
export function saidSinceLastMessage(sessionId: string, n = 20): string {
|
|
155
|
+
flushItems();
|
|
72
156
|
const items = lastItems(sessionId, n);
|
|
73
157
|
return items
|
|
74
158
|
.slice(items.findLastIndex((i) => i.kind === "user") + 1)
|
|
@@ -81,6 +165,7 @@ export function itemsOfKind<K extends ChatItem["kind"]>(
|
|
|
81
165
|
sessionId: string,
|
|
82
166
|
kind: K
|
|
83
167
|
): Extract<ChatItem, { kind: K }>[] {
|
|
168
|
+
flushItems();
|
|
84
169
|
return (
|
|
85
170
|
db
|
|
86
171
|
.prepare(
|
|
@@ -95,6 +180,7 @@ export function itemsOfKind<K extends ChatItem["kind"]>(
|
|
|
95
180
|
export function runningTools(
|
|
96
181
|
sessionId: string
|
|
97
182
|
): Extract<ChatItem, { kind: "tool" }>[] {
|
|
183
|
+
flushItems();
|
|
98
184
|
return (
|
|
99
185
|
db
|
|
100
186
|
.prepare(
|
|
@@ -107,6 +193,8 @@ export function runningTools(
|
|
|
107
193
|
}
|
|
108
194
|
|
|
109
195
|
export function deleteItems(sessionId: string): void {
|
|
196
|
+
pending.delete(sessionId);
|
|
197
|
+
seqs.delete(sessionId);
|
|
110
198
|
db.prepare(`DELETE FROM chat_items WHERE session_id = ?`).run(sessionId);
|
|
111
199
|
}
|
|
112
200
|
|
|
@@ -135,6 +223,7 @@ export function settle(items: ChatItem[]): ChatItem[] {
|
|
|
135
223
|
|
|
136
224
|
// What a chat session is working on: the first line of its latest message.
|
|
137
225
|
export function lastUserTask(sessionId: string): string | null {
|
|
226
|
+
flushItems();
|
|
138
227
|
const row = db
|
|
139
228
|
.prepare(
|
|
140
229
|
`SELECT data FROM chat_items WHERE session_id = ? AND item_id LIKE 'user-%'
|
package/lib/chat/worker/host.ts
CHANGED
|
@@ -22,9 +22,10 @@ import type {
|
|
|
22
22
|
} from "../events";
|
|
23
23
|
import {
|
|
24
24
|
itemsOfKind,
|
|
25
|
+
flushItems,
|
|
25
26
|
moveToEnd,
|
|
27
|
+
queueItem,
|
|
26
28
|
runningTools,
|
|
27
|
-
saveItem,
|
|
28
29
|
settle,
|
|
29
30
|
} from "../store";
|
|
30
31
|
import { claimNext, enqueue, listQueue, moveToFront } from "../queued";
|
|
@@ -261,7 +262,10 @@ export class ChatHost {
|
|
|
261
262
|
}
|
|
262
263
|
|
|
263
264
|
private record(item: ChatItem): void {
|
|
264
|
-
|
|
265
|
+
// Streaming updates are written in batches; anything else right away.
|
|
266
|
+
queueItem(this.session.id, item, {
|
|
267
|
+
flush: !("streaming" in item && item.streaming),
|
|
268
|
+
});
|
|
265
269
|
this.emit({ type: "item", item });
|
|
266
270
|
}
|
|
267
271
|
|
|
@@ -354,6 +358,7 @@ export class ChatHost {
|
|
|
354
358
|
}
|
|
355
359
|
} else if (e.type === "state") {
|
|
356
360
|
if (e.state === "idle") {
|
|
361
|
+
flushItems();
|
|
357
362
|
// A turn cut off mid-sentence (Esc, Stop) keeps what it streamed.
|
|
358
363
|
this.settleStreaming();
|
|
359
364
|
this.closeTools();
|
|
@@ -406,6 +411,7 @@ export class ChatHost {
|
|
|
406
411
|
this.settleStreaming();
|
|
407
412
|
this.closeTools();
|
|
408
413
|
this.showHandedOver();
|
|
414
|
+
flushItems();
|
|
409
415
|
this.setState("idle");
|
|
410
416
|
}
|
|
411
417
|
}
|
|
@@ -485,7 +491,7 @@ export class ChatHost {
|
|
|
485
491
|
checkpoint,
|
|
486
492
|
};
|
|
487
493
|
if (handOver) {
|
|
488
|
-
|
|
494
|
+
queueItem(this.session.id, user, { flush: true });
|
|
489
495
|
this.handedOver.push(user);
|
|
490
496
|
} else this.record(user);
|
|
491
497
|
this.setState("running");
|
|
@@ -631,6 +637,7 @@ export class ChatHost {
|
|
|
631
637
|
}
|
|
632
638
|
return;
|
|
633
639
|
case "interrupt":
|
|
640
|
+
flushItems();
|
|
634
641
|
await this.modeChange.catch(() => {});
|
|
635
642
|
if (cmd.hard) {
|
|
636
643
|
this.hardStopAt = Date.now();
|
|
@@ -877,6 +884,7 @@ export class ChatHost {
|
|
|
877
884
|
clearTimeout(this.retireTimer);
|
|
878
885
|
clearTimeout(this.restTimer);
|
|
879
886
|
this.closed = true;
|
|
887
|
+
flushItems();
|
|
880
888
|
this.onClose();
|
|
881
889
|
this.conversation.close();
|
|
882
890
|
}
|
|
@@ -124,7 +124,7 @@ Acting:
|
|
|
124
124
|
- \`${TOOL_NAMES.disarm}\` (task): stop an armed task merging by itself.
|
|
125
125
|
- \`${TOOL_NAMES.ask_saad}\` (title, detail, link?, kind, task?, sha?): park an item on Saad's asks list and carry on; it never waits.
|
|
126
126
|
|
|
127
|
-
Act through these tools, not the shell: they're scoped to this workspace and braked. The shell runs
|
|
127
|
+
Act through these tools, not the shell: they're scoped to this workspace and braked. The shell runs anything without asking (Saad chose this on 2026-10-11), so use it to read and investigate yourself: \`gh pr list\`, \`gh run view --log-failed\`, \`gh api\` GETs, \`curl\`, \`sqlite3 -readonly\`, git in any repository, a skill's scripts, pipes and chains. Never use it to get around the tools. A deny list refuses, naming the tool to use instead: merging, closing or retargeting a PR (\`${TOOL_NAMES.sign_off}\`, \`${TOOL_NAMES.arm}\`, \`${TOOL_NAMES.land}\`, \`${TOOL_NAMES.drop}\`), \`git push\` (\`${TOOL_NAMES.quick_pr}\`), writing in a project repository or task worktree other than your own quick changes (\`${TOOL_NAMES.start_task}\`, \`${TOOL_NAMES.quick_change}\`), writing AgentOS's state (sqlite3 without \`-readonly\`, or files in ~/.agent-os outside your folder), stopping or restarting AgentOS or its tmux, and reading secret files or tokenvault. The list catches honest mistakes, not a determined bypass: a script or \`python -c\` could do any of these unseen, and doing so would break Saad's trust in every gate. \`aos notify "<text>"\` pushes a message to Saad's phone. Use it for what he'd want to see now and asked for: the morning report, a real milestone (a stack landed, a blocker only he can clear). Never for routine status: one a minute at most, and he reads the rest here. You can read files anywhere (Read, Grep, Glob), and write them (Write, Edit, NotebookEdit) anywhere except: a project repository or task worktree other than your own open quick changes' worktrees (start a task, or a quick change for a small edit, so the change goes through CI, review and scope), AgentOS's own state in ~/.agent-os outside your folder, secret files (.env, keys, ~/.ssh, ~/.aws, tokenvault and the like), and files that make something run: dotfiles and dot-folders at the top of your home (shell, git, Claude and tool config), ~/Library and /Library's launch agents, git's core.hooksPath folders, the folders on PATH, and any .git, .claude, .mcp.json, CLAUDE.md or AGENTS.md, which would hand you a shell or run code around the gates. Each write is checked before it runs, on the path with symlinks and \`..\` resolved; a refused one says why.
|
|
128
128
|
|
|
129
129
|
## Untrusted text
|
|
130
130
|
|