@saadnvd1/agent-os 0.3.631 → 0.3.639
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/app/api/sessions/[id]/pr/route.ts +3 -3
- package/app/api/sessions/[id]/setup/route.ts +3 -3
- package/app/api/tasks/[id]/approve-plan/route.ts +5 -1
- package/app/api/tasks/[id]/arm/route.ts +6 -5
- package/app/api/tasks/[id]/chat-facts/route.ts +2 -2
- package/app/api/tasks/[id]/chat-tail/route.ts +2 -2
- package/app/api/tasks/[id]/checks/route.ts +1 -1
- package/app/api/tasks/[id]/diff/route.ts +1 -1
- package/app/api/tasks/[id]/done/route.ts +2 -2
- package/app/api/tasks/[id]/drop/route.ts +6 -2
- package/app/api/tasks/[id]/export/route.ts +3 -4
- package/app/api/tasks/[id]/merge/route.ts +3 -3
- package/app/api/tasks/[id]/message/route.ts +2 -2
- package/app/api/tasks/[id]/model/route.ts +5 -1
- package/app/api/tasks/[id]/move/route.ts +5 -1
- package/app/api/tasks/[id]/moved/route.ts +6 -8
- package/app/api/tasks/[id]/plan/route.ts +35 -0
- package/app/api/tasks/[id]/plan-edits/route.ts +6 -5
- package/app/api/tasks/[id]/priority/route.ts +4 -1
- package/app/api/tasks/[id]/resume/route.ts +3 -4
- package/app/api/tasks/[id]/stop/route.ts +2 -2
- package/app/api/tasks/route.ts +5 -2
- package/app/globals.css +28 -0
- package/components/Chat/ChatPanel.tsx +2 -11
- package/components/Chat/EventTurns.tsx +1 -7
- package/components/Chat/Markdown.tsx +1 -0
- package/components/Chat/Messages.tsx +1 -1
- package/lib/approvals/enable.ts +9 -2
- package/lib/away/report.ts +4 -4
- package/lib/away/run.ts +2 -2
- package/lib/bookmarks/store.ts +3 -3
- package/lib/bus/held-reply.ts +2 -2
- package/lib/bus/index.ts +3 -3
- package/lib/chat/compact-context.ts +1 -1
- package/lib/chat/events.ts +4 -0
- package/lib/chat/group.ts +3 -1
- package/lib/chat/hold.ts +3 -3
- package/lib/chat/reading-order.ts +50 -0
- package/lib/chat/refresh.ts +1 -1
- package/lib/chat/registry.ts +3 -3
- package/lib/chat/runner.ts +1 -1
- package/lib/chat/turns.ts +22 -3
- package/lib/chat/worker/host.ts +3 -0
- package/lib/chat/worker/main.ts +1 -1
- package/lib/db/index.ts +5 -1
- package/lib/db/lumifyhub.ts +8 -9
- package/lib/db/machine-scope.ts +12 -0
- package/lib/db/migrations/20261015T1600_plan_waits_for_caller.ts +19 -0
- package/lib/db/migrations/20261015T1800_split_sessions_tables.ts +172 -0
- package/lib/db/migrations/README.md +7 -0
- package/lib/db/queries.ts +16 -31
- package/lib/db/session-parts/columns.ts +186 -0
- package/lib/db/session-parts/fill.ts +33 -0
- package/lib/db/session-parts/index.ts +9 -0
- package/lib/db/session-parts/pauses.ts +11 -0
- package/lib/db/session-parts/placement.ts +11 -0
- package/lib/db/session-parts/plans.ts +31 -0
- package/lib/db/session-parts/prs.ts +31 -0
- package/lib/db/session-parts/tasks.ts +27 -0
- package/lib/db/session-parts/view.ts +43 -0
- package/lib/db/session-parts/write.ts +69 -0
- package/lib/db/types.ts +6 -1
- package/lib/done/archive.ts +2 -2
- package/lib/done/auto-archive.ts +1 -1
- package/lib/done/bulk.ts +1 -1
- package/lib/done/index.ts +8 -14
- package/lib/env-setup.ts +17 -4
- package/lib/health/evidence.ts +1 -1
- package/lib/health/facts.ts +1 -1
- package/lib/health/pieces.ts +1 -1
- package/lib/hosts/peer-actions.ts +2 -2
- package/lib/hosts/peer-mirror.ts +2 -2
- package/lib/hosts/peer-sync.ts +69 -32
- package/lib/learning-cards/input.ts +1 -1
- package/lib/learning-cards/store.ts +1 -1
- package/lib/models/plan-step.ts +83 -46
- package/lib/models/resume.ts +5 -7
- package/lib/notify/index.ts +1 -1
- package/lib/notify/push/events.ts +1 -1
- package/lib/notify/push/index.ts +1 -1
- package/lib/orchestrator/act.ts +1 -1
- package/lib/orchestrator/armed.ts +17 -15
- package/lib/orchestrator/ask-approvals.ts +1 -1
- package/lib/orchestrator/ask-heads.ts +1 -1
- package/lib/orchestrator/ask-settle.ts +1 -1
- package/lib/orchestrator/brakes.ts +1 -1
- package/lib/orchestrator/digest.ts +2 -2
- package/lib/orchestrator/external-pr.ts +2 -2
- package/lib/orchestrator/facts.ts +4 -28
- package/lib/orchestrator/held-asks.ts +3 -3
- package/lib/orchestrator/home.ts +2 -2
- package/lib/orchestrator/overview.ts +1 -1
- package/lib/orchestrator/pr-wake.ts +3 -5
- package/lib/orchestrator/questions.ts +1 -1
- package/lib/orchestrator/quick-store.ts +1 -1
- package/lib/orchestrator/quick.ts +10 -7
- package/lib/orchestrator/read.ts +1 -1
- package/lib/orchestrator/rebase.ts +8 -11
- package/lib/orchestrator/reconcile.ts +2 -2
- package/lib/orchestrator/signoff.ts +3 -1
- package/lib/orchestrator/targets.ts +1 -1
- package/lib/orchestrator/task-state.ts +4 -2
- package/lib/plans/features.ts +1 -1
- package/lib/plans/release.ts +1 -1
- package/lib/ports.ts +1 -1
- package/lib/resurrect/find.ts +1 -1
- package/lib/resurrect/index.ts +24 -18
- package/lib/schedules/store.ts +1 -1
- package/lib/session-audit/sources.ts +3 -3
- package/lib/session-logs/index.ts +1 -1
- package/lib/session-titles.ts +1 -1
- package/lib/sessions/setup-progress.ts +1 -1
- package/lib/sessions/stop.ts +1 -1
- package/lib/sessions/worktree-setup.ts +2 -1
- package/lib/simulator/index.ts +1 -1
- package/lib/simulator/task-devices.ts +1 -1
- package/lib/stacks/board.ts +1 -1
- package/lib/stacks/land.ts +1 -1
- package/lib/stacks/restack.ts +1 -1
- package/lib/stacks/start.ts +2 -2
- package/lib/stacks/tick.ts +1 -1
- package/lib/stacks/view.ts +1 -1
- package/lib/status/incremental.ts +1 -1
- package/lib/tasks/contract/actions.ts +35 -0
- package/lib/tasks/contract/authorize.ts +82 -0
- package/lib/tasks/contract/client.ts +296 -0
- package/lib/tasks/{held-remote.ts → contract/held-calls.ts} +35 -66
- package/lib/tasks/contract/index.ts +28 -0
- package/lib/tasks/contract/plan-path.ts +7 -0
- package/lib/tasks/contract/plan.ts +274 -0
- package/lib/tasks/contract/routes.ts +26 -0
- package/lib/tasks/contract/start.ts +122 -0
- package/lib/tasks/contract/sync.ts +126 -0
- package/lib/tasks/contract/types.ts +41 -0
- package/lib/tasks/{remote-views.ts → contract/views.ts} +11 -18
- package/lib/tasks/finish.ts +10 -9
- package/lib/tasks/held-replies.ts +7 -13
- package/lib/tasks/held.ts +36 -17
- package/lib/tasks/hold-checks.ts +134 -0
- package/lib/tasks/holds.ts +281 -0
- package/lib/tasks/import.ts +26 -14
- package/lib/tasks/index.ts +56 -22
- package/lib/tasks/launch-gate.ts +1 -1
- package/lib/tasks/move-bundle.ts +17 -2
- package/lib/tasks/move-carry.ts +15 -7
- package/lib/tasks/move-flow.ts +29 -36
- package/lib/tasks/move-guard.ts +10 -2
- package/lib/tasks/move-recover.ts +6 -25
- package/lib/tasks/move.ts +28 -27
- package/lib/tasks/moved-away.ts +1 -1
- package/lib/tasks/pause-hold.ts +13 -5
- package/lib/tasks/pause.ts +21 -54
- package/lib/tasks/placement.ts +16 -16
- package/lib/tasks/plan-view.ts +11 -3
- package/lib/tasks/queue-candidates.ts +151 -0
- package/lib/tasks/queue-placement.ts +155 -0
- package/lib/tasks/queue-rows.ts +236 -0
- package/lib/tasks/queue-start.ts +213 -0
- package/lib/tasks/queue-tick.ts +148 -0
- package/lib/tasks/queue-types.ts +141 -0
- package/lib/tasks/queue-view.ts +112 -0
- package/lib/tasks/queue.ts +67 -1102
- package/lib/tasks/schedule.ts +44 -147
- package/lib/tasks/session.ts +7 -4
- package/lib/tasks/start.ts +4 -4
- package/lib/tasks/starter.ts +8 -11
- package/lib/usage/calls.ts +1 -1
- package/lib/usage/compare.ts +1 -1
- package/lib/usage/quality.ts +1 -1
- package/lib/usage/turns.ts +1 -1
- package/lib/voice/vocab.ts +1 -1
- package/package.json +1 -1
- package/scripts/backfill-session-logs.ts +1 -1
- package/scripts/check +55 -7
- package/lib/tasks/auto-hold.ts +0 -17
- package/lib/tasks/held-routes.ts +0 -33
- package/lib/tasks/remote.ts +0 -238
package/lib/away/report.ts
CHANGED
|
@@ -137,7 +137,7 @@ function merged(
|
|
|
137
137
|
const rows = db
|
|
138
138
|
.prepare(
|
|
139
139
|
`SELECT s.id, s.name, p.name AS project, p.id AS project_id, s.pr_number, s.pr_url
|
|
140
|
-
FROM
|
|
140
|
+
FROM session_rows s JOIN projects p ON p.id = s.project_id
|
|
141
141
|
WHERE p.workspace_id = ? AND s.task_status = 'merged'
|
|
142
142
|
AND s.merged_at >= ? AND s.merged_at <= ?
|
|
143
143
|
ORDER BY s.merged_at, s.id`
|
|
@@ -178,7 +178,7 @@ function inFlight(workspaceId: string, awayId: number): InFlightItem[] {
|
|
|
178
178
|
.prepare(
|
|
179
179
|
`SELECT s.id, s.name, p.name AS project, s.pr_number, s.pr_url,
|
|
180
180
|
s.merge_armed_at AS armed_at
|
|
181
|
-
FROM
|
|
181
|
+
FROM session_rows s JOIN projects p ON p.id = s.project_id
|
|
182
182
|
WHERE p.workspace_id = ? AND s.role IS NULL AND s.archived_at IS NULL
|
|
183
183
|
AND s.task_status = 'running'
|
|
184
184
|
AND EXISTS (SELECT 1 FROM away_actions a WHERE a.away_id = ?
|
|
@@ -224,7 +224,7 @@ function failed(workspaceId: string, away: AwayRow, end: string) {
|
|
|
224
224
|
const deploys = db
|
|
225
225
|
.prepare(
|
|
226
226
|
`SELECT a.detail, s.name, s.pr_number, s.pr_url FROM away_actions a
|
|
227
|
-
LEFT JOIN
|
|
227
|
+
LEFT JOIN session_rows s ON s.id = a.subject
|
|
228
228
|
WHERE a.away_id = ? AND a.workspace_id = ? AND a.kind = 'deploy_failed'
|
|
229
229
|
ORDER BY a.id`
|
|
230
230
|
)
|
|
@@ -236,7 +236,7 @@ function failed(workspaceId: string, away: AwayRow, end: string) {
|
|
|
236
236
|
}[];
|
|
237
237
|
const ended = db
|
|
238
238
|
.prepare(
|
|
239
|
-
`SELECT s.name, s.task_status, s.pr_number, s.pr_url FROM
|
|
239
|
+
`SELECT s.name, s.task_status, s.pr_number, s.pr_url FROM session_rows s
|
|
240
240
|
JOIN projects p ON p.id = s.project_id
|
|
241
241
|
WHERE p.workspace_id = ? AND s.task_status IN ('dropped', 'done')
|
|
242
242
|
AND s.task_ended_at >= ? AND s.task_ended_at <= ?
|
package/lib/away/run.ts
CHANGED
|
@@ -69,7 +69,7 @@ function orchestrated(): { workspaceId: string; orchestratorId: string }[] {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
const sessionRow = (id: string) =>
|
|
72
|
-
db.prepare(`SELECT * FROM
|
|
72
|
+
db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
73
73
|
| Session
|
|
74
74
|
| undefined;
|
|
75
75
|
|
|
@@ -330,7 +330,7 @@ async function checkDeploys(
|
|
|
330
330
|
const found: DeployFinding[] = [];
|
|
331
331
|
const merged = db
|
|
332
332
|
.prepare(
|
|
333
|
-
`SELECT s.* FROM
|
|
333
|
+
`SELECT s.* FROM session_rows s JOIN projects p ON p.id = s.project_id
|
|
334
334
|
WHERE p.workspace_id = ? AND s.task_status = 'merged' AND s.merged_at >= ?
|
|
335
335
|
AND NOT EXISTS (SELECT 1 FROM away_actions a
|
|
336
336
|
WHERE a.away_id = ? AND a.key = 'deploy:' || s.id)`
|
package/lib/bookmarks/store.ts
CHANGED
|
@@ -57,7 +57,7 @@ type Joined = Row & {
|
|
|
57
57
|
// The bookmark with its chat's state as of now.
|
|
58
58
|
const SELECT = `
|
|
59
59
|
SELECT b.*, s.id AS s_id, s.task_status, s.archived_at
|
|
60
|
-
FROM chat_bookmarks b LEFT JOIN
|
|
60
|
+
FROM chat_bookmarks b LEFT JOIN session_rows s ON s.id = b.session_id`;
|
|
61
61
|
|
|
62
62
|
function toBookmark(r: Joined): Bookmark {
|
|
63
63
|
return {
|
|
@@ -141,7 +141,7 @@ export function createBookmark(input: {
|
|
|
141
141
|
throw new BookmarkError("scope must be 'item' or 'turn'", 400);
|
|
142
142
|
const note = cleanNote(input.note);
|
|
143
143
|
const session = db
|
|
144
|
-
.prepare(`SELECT * FROM
|
|
144
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
145
145
|
.get(sessionId) as Session | undefined;
|
|
146
146
|
if (!session) throw new BookmarkError("Session not found", 404);
|
|
147
147
|
const existing = db
|
|
@@ -308,7 +308,7 @@ export function insertMovedBookmarks(
|
|
|
308
308
|
): void {
|
|
309
309
|
if (!rows.length) return;
|
|
310
310
|
const session = db
|
|
311
|
-
.prepare(`SELECT * FROM
|
|
311
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
312
312
|
.get(sessionId) as Session | undefined;
|
|
313
313
|
if (!session) return;
|
|
314
314
|
const workspace = sessionWorkspace(session);
|
package/lib/bus/held-reply.ts
CHANGED
|
@@ -68,7 +68,7 @@ export function pendingReplies(device: string, limit = 50): PendingReply[] {
|
|
|
68
68
|
db
|
|
69
69
|
.prepare(
|
|
70
70
|
`SELECT m.id, m.from_id AS fromId, m.body, m.created_at AS createdAt
|
|
71
|
-
FROM bus_messages m JOIN
|
|
71
|
+
FROM bus_messages m JOIN session_rows s ON s.id = m.from_id
|
|
72
72
|
WHERE m.relay_state = 'pending' AND s.held_by_device = ?
|
|
73
73
|
ORDER BY m.id LIMIT ?`
|
|
74
74
|
)
|
|
@@ -81,7 +81,7 @@ export function ackReplies(device: string, ids: number[]): void {
|
|
|
81
81
|
const mark = db.prepare(
|
|
82
82
|
`UPDATE bus_messages SET relay_state = 'relayed', delivered_at = datetime('now')
|
|
83
83
|
WHERE id = ? AND relay_state = 'pending'
|
|
84
|
-
AND from_id IN (SELECT id FROM
|
|
84
|
+
AND from_id IN (SELECT id FROM session_rows WHERE held_by_device = ?)`
|
|
85
85
|
);
|
|
86
86
|
db.transaction(() => {
|
|
87
87
|
for (const id of ids) mark.run(id, device);
|
package/lib/bus/index.ts
CHANGED
|
@@ -15,7 +15,7 @@ import { deliverToPane, type Delivery } from "./delivery";
|
|
|
15
15
|
import { resolveRef, wasNames, type Candidate } from "./resolve";
|
|
16
16
|
import { tmuxPane } from "./tmux-pane";
|
|
17
17
|
import { pauseHoldOf, pausedNote } from "../tasks/pause-hold";
|
|
18
|
-
import { isHeldRemote, relayToHeldTask } from "../tasks/held-
|
|
18
|
+
import { isHeldRemote, relayToHeldTask } from "../tasks/contract/held-calls";
|
|
19
19
|
import {
|
|
20
20
|
assertTakesMessages,
|
|
21
21
|
findQueuedTarget,
|
|
@@ -43,7 +43,7 @@ export interface Peer {
|
|
|
43
43
|
function liveSessions(): Session[] {
|
|
44
44
|
return db
|
|
45
45
|
.prepare(
|
|
46
|
-
`SELECT * FROM
|
|
46
|
+
`SELECT * FROM session_rows WHERE (task_status IS NULL OR task_status = 'running')
|
|
47
47
|
AND archived_at IS NULL
|
|
48
48
|
ORDER BY updated_at DESC`
|
|
49
49
|
)
|
|
@@ -63,7 +63,7 @@ function candidates(sessions: Session[]): Candidate[] {
|
|
|
63
63
|
|
|
64
64
|
const sessionById = (id: string | null | undefined) =>
|
|
65
65
|
id
|
|
66
|
-
? (db.prepare(`SELECT * FROM
|
|
66
|
+
? (db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
67
67
|
| Session
|
|
68
68
|
| undefined)
|
|
69
69
|
: undefined;
|
|
@@ -28,7 +28,7 @@ async function gitStatus(cwd: string): Promise<string> {
|
|
|
28
28
|
export async function compactContext(sessionId: string): Promise<string> {
|
|
29
29
|
const session = db
|
|
30
30
|
.prepare(
|
|
31
|
-
`SELECT working_directory, task_prompt, role FROM
|
|
31
|
+
`SELECT working_directory, task_prompt, role FROM session_rows WHERE id = ?`
|
|
32
32
|
)
|
|
33
33
|
.get(sessionId) as
|
|
34
34
|
| Pick<Session, "working_directory" | "task_prompt" | "role">
|
package/lib/chat/events.ts
CHANGED
|
@@ -149,6 +149,10 @@ export type ChatItem =
|
|
|
149
149
|
// Saved since messages were tagged where they're sent: without an
|
|
150
150
|
// origin, the reader typed it. Older items are matched by their text.
|
|
151
151
|
tagged?: true;
|
|
152
|
+
// Whether it was sent while a turn ran: the agent then reads it once
|
|
153
|
+
// that turn ends, so what follows until then is still that turn's.
|
|
154
|
+
// Always set when saved; absent only on history from before it was.
|
|
155
|
+
midTurn?: boolean;
|
|
152
156
|
// The provider's id for this message, to undo file changes back to it.
|
|
153
157
|
checkpoint?: string;
|
|
154
158
|
})
|
package/lib/chat/group.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ChatItem } from "./events";
|
|
2
|
+
import { readingOrder } from "./reading-order";
|
|
2
3
|
import { eventTurnAt, type EventTurn } from "./turns";
|
|
3
4
|
|
|
4
5
|
export type ToolItem = Extract<ChatItem, { kind: "tool" }>;
|
|
@@ -23,9 +24,10 @@ export const STANDALONE_TOOLS = new Set(["Skill", "Task", "Agent"]);
|
|
|
23
24
|
// collapsible block, so a long turn reads as what was said, not every step.
|
|
24
25
|
// Undone messages fold into one block behind the undo that took them back.
|
|
25
26
|
export function groupTimeline(
|
|
26
|
-
|
|
27
|
+
all: ChatItem[],
|
|
27
28
|
opts: GroupOptions = {}
|
|
28
29
|
): TimelineBlock[] {
|
|
30
|
+
const items = opts.foldEvents ? readingOrder(all) : all;
|
|
29
31
|
const blocks: TimelineBlock[] = [];
|
|
30
32
|
let tools: ToolItem[] = [];
|
|
31
33
|
const flush = () => {
|
package/lib/chat/hold.ts
CHANGED
|
@@ -15,10 +15,10 @@ import { db } from "../db";
|
|
|
15
15
|
|
|
16
16
|
// The SQL form, for queries that take the session id as @sid.
|
|
17
17
|
export const HELD_SQL = `(
|
|
18
|
-
EXISTS (SELECT 1 FROM
|
|
18
|
+
EXISTS (SELECT 1 FROM session_rows WHERE id = @sid AND task_status = 'moving')
|
|
19
19
|
OR EXISTS (SELECT 1 FROM stack_items i JOIN stacks s ON s.id = i.stack_id
|
|
20
20
|
WHERE i.session_id = @sid AND s.status = 'landing')
|
|
21
|
-
OR EXISTS (SELECT 1 FROM
|
|
21
|
+
OR EXISTS (SELECT 1 FROM session_rows WHERE id = @sid
|
|
22
22
|
AND pause_state IN ('pausing', 'paused'))
|
|
23
23
|
)`;
|
|
24
24
|
|
|
@@ -58,7 +58,7 @@ const holdRow = (sessionId: string) =>
|
|
|
58
58
|
`SELECT task_status, moved_to, quick_of, pause_state, paused_until,
|
|
59
59
|
EXISTS (SELECT 1 FROM stack_items i JOIN stacks s ON s.id = i.stack_id
|
|
60
60
|
WHERE i.session_id = @sid AND s.status = 'landing') AS landing
|
|
61
|
-
FROM
|
|
61
|
+
FROM session_rows WHERE id = @sid`
|
|
62
62
|
)
|
|
63
63
|
.get({ sid: sessionId }) as HoldRow | undefined;
|
|
64
64
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { ChatItem } from "./events";
|
|
2
|
+
import { activityOrigin } from "./turns";
|
|
3
|
+
|
|
4
|
+
// Activity sent while a turn runs is saved when sent but read by the agent
|
|
5
|
+
// once that turn ends. It moves to after the turn's end, so the turn a
|
|
6
|
+
// person's message started stays whole and the activity gets its own reply.
|
|
7
|
+
//
|
|
8
|
+
// Several waiting at one turn's end are taken a turn each: the first starts
|
|
9
|
+
// after the end, the next after the following one. Whether the agent
|
|
10
|
+
// answers them together or apart isn't recorded, and this way a turn each
|
|
11
|
+
// keeps every reply with its own event; answered together, the first takes
|
|
12
|
+
// the reply and the rest show as bare rows.
|
|
13
|
+
//
|
|
14
|
+
// A message saved with `midTurn` is trusted either way. Only history from
|
|
15
|
+
// before the field is guessed, from whether a turn was open when it landed.
|
|
16
|
+
export function readingOrder(items: ChatItem[]): ChatItem[] {
|
|
17
|
+
const out: ChatItem[] = [];
|
|
18
|
+
const waiting: ChatItem[] = [];
|
|
19
|
+
let open = false;
|
|
20
|
+
let moved = false;
|
|
21
|
+
const release = () => out.push(...waiting.splice(0));
|
|
22
|
+
for (const item of items) {
|
|
23
|
+
if (item.kind === "user") {
|
|
24
|
+
const waits = item.midTurn === undefined ? open : item.midTurn;
|
|
25
|
+
if (waits && activityOrigin(item)) {
|
|
26
|
+
waiting.push(item);
|
|
27
|
+
moved = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
release();
|
|
31
|
+
out.push(item);
|
|
32
|
+
open = true;
|
|
33
|
+
} else if (item.kind === "turn_end") {
|
|
34
|
+
out.push(item);
|
|
35
|
+
open = false;
|
|
36
|
+
const next = waiting.shift();
|
|
37
|
+
if (next) out.push(next);
|
|
38
|
+
} else if (
|
|
39
|
+
item.kind === "undo" ||
|
|
40
|
+
item.kind === "error" ||
|
|
41
|
+
item.kind === "command_output"
|
|
42
|
+
) {
|
|
43
|
+
release();
|
|
44
|
+
out.push(item);
|
|
45
|
+
open = false;
|
|
46
|
+
} else out.push(item);
|
|
47
|
+
}
|
|
48
|
+
release();
|
|
49
|
+
return moved ? out : items;
|
|
50
|
+
}
|
package/lib/chat/refresh.ts
CHANGED
|
@@ -163,7 +163,7 @@ export function autoCompactEnv(sessionId: string): Record<string, string> {
|
|
|
163
163
|
const row = db
|
|
164
164
|
.prepare(
|
|
165
165
|
`SELECT role, task_status, plan_phase, started_by, model_provider
|
|
166
|
-
FROM
|
|
166
|
+
FROM session_rows WHERE id = ?`
|
|
167
167
|
)
|
|
168
168
|
.get(sessionId) as
|
|
169
169
|
| {
|
package/lib/chat/registry.ts
CHANGED
|
@@ -112,9 +112,9 @@ export function emit(sessionId: string, m: ChatServerMessage): void {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
export function getSession(sessionId: string): Session {
|
|
115
|
-
const s = db
|
|
116
|
-
|
|
117
|
-
| undefined;
|
|
115
|
+
const s = db
|
|
116
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
117
|
+
.get(sessionId) as Session | undefined;
|
|
118
118
|
if (!s) throw new Error("Session not found");
|
|
119
119
|
return s;
|
|
120
120
|
}
|
package/lib/chat/runner.ts
CHANGED
|
@@ -549,7 +549,7 @@ async function resumeCutOff(sessionId: string): Promise<void> {
|
|
|
549
549
|
.prepare(
|
|
550
550
|
`SELECT s.name, s.view, s.archived_at, s.task_status, s.role,
|
|
551
551
|
COALESCE(s.workspace_id, p.workspace_id) AS workspace_id
|
|
552
|
-
FROM
|
|
552
|
+
FROM session_rows s LEFT JOIN projects p ON p.id = s.project_id
|
|
553
553
|
WHERE s.id = ?`
|
|
554
554
|
)
|
|
555
555
|
.get(sessionId) as
|
package/lib/chat/turns.ts
CHANGED
|
@@ -18,8 +18,6 @@ export interface EventTurn {
|
|
|
18
18
|
at: number;
|
|
19
19
|
// Still being answered.
|
|
20
20
|
live: boolean;
|
|
21
|
-
// The reply opened a question for the reader.
|
|
22
|
-
hasQuestion: boolean;
|
|
23
21
|
}
|
|
24
22
|
|
|
25
23
|
const FOLDED_KINDS = new Set(["event", "peer", "schedule", "system"]);
|
|
@@ -39,6 +37,26 @@ export function activityOrigin(item: UserItem): ChatOrigin | null {
|
|
|
39
37
|
export const isQuestionTool = (item: ChatItem): boolean =>
|
|
40
38
|
item.kind === "tool" && /(^|__)question$/.test(item.name);
|
|
41
39
|
|
|
40
|
+
export const isAskTool = (item: ChatItem): boolean =>
|
|
41
|
+
item.kind === "tool" && /(^|__)ask_saad$/.test(item.name);
|
|
42
|
+
|
|
43
|
+
// The assistant message a question was asked in (its turn's last words
|
|
44
|
+
// before the call), else the call itself.
|
|
45
|
+
export function questionSource(items: ChatItem[], text: string): string | null {
|
|
46
|
+
const at = items.findLastIndex(
|
|
47
|
+
(i) =>
|
|
48
|
+
isQuestionTool(i) &&
|
|
49
|
+
(i as { input?: { text?: string } }).input?.text === text
|
|
50
|
+
);
|
|
51
|
+
if (at === -1) return null;
|
|
52
|
+
for (let i = at - 1; i >= 0; i--) {
|
|
53
|
+
const item = items[i];
|
|
54
|
+
if (item.kind === "user" || item.kind === "turn_end") break;
|
|
55
|
+
if (item.kind === "assistant" && item.text.trim()) return item.id;
|
|
56
|
+
}
|
|
57
|
+
return items[at].id;
|
|
58
|
+
}
|
|
59
|
+
|
|
42
60
|
export function plainSummary(text: string, max = 140): string {
|
|
43
61
|
const line =
|
|
44
62
|
text
|
|
@@ -89,6 +107,8 @@ export function eventTurnAt(
|
|
|
89
107
|
(i) =>
|
|
90
108
|
i.kind === "plan" ||
|
|
91
109
|
i.kind === "error" ||
|
|
110
|
+
isQuestionTool(i) ||
|
|
111
|
+
isAskTool(i) ||
|
|
92
112
|
(i.kind === "approval" && i.status === "pending")
|
|
93
113
|
);
|
|
94
114
|
if (needsReader) return null;
|
|
@@ -105,7 +125,6 @@ export function eventTurnAt(
|
|
|
105
125
|
task: taskName(origin),
|
|
106
126
|
at: user.createdAt,
|
|
107
127
|
live: !ended && end >= items.length,
|
|
108
|
-
hasQuestion: replies.some(isQuestionTool),
|
|
109
128
|
},
|
|
110
129
|
};
|
|
111
130
|
}
|
package/lib/chat/worker/host.ts
CHANGED
|
@@ -564,6 +564,8 @@ export class ChatHost {
|
|
|
564
564
|
this.sent.add(m.id);
|
|
565
565
|
this.currentTurn = m.id;
|
|
566
566
|
this.hardStopAt = 0;
|
|
567
|
+
// A message sent now stops the turn and is read next: nothing waits.
|
|
568
|
+
const midTurn = this.busy() && !now;
|
|
567
569
|
if (this.busy()) this.behind++;
|
|
568
570
|
// Sent now, it stops the running turn, which still ends after this.
|
|
569
571
|
if (!this.busy()) now = false;
|
|
@@ -599,6 +601,7 @@ export class ChatHost {
|
|
|
599
601
|
peer: m.peer,
|
|
600
602
|
origin: m.origin,
|
|
601
603
|
tagged: true as const,
|
|
604
|
+
midTurn,
|
|
602
605
|
createdAt: Date.now(),
|
|
603
606
|
checkpoint,
|
|
604
607
|
};
|
package/lib/chat/worker/main.ts
CHANGED
|
@@ -30,7 +30,7 @@ async function main(sessionId: string) {
|
|
|
30
30
|
const { ChatHost } = await import("./host");
|
|
31
31
|
|
|
32
32
|
const row = db
|
|
33
|
-
.prepare(`SELECT * FROM
|
|
33
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
34
34
|
.get(sessionId) as Session | undefined;
|
|
35
35
|
if (!row) {
|
|
36
36
|
console.error(`No session ${sessionId}`);
|
package/lib/db/index.ts
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import Database from "better-sqlite3";
|
|
2
|
-
import path from "path";
|
|
3
2
|
import fs from "fs";
|
|
4
3
|
import { createSchema } from "./schema";
|
|
5
4
|
import { runMigrations } from "./migrations";
|
|
6
5
|
import { dbPath } from "./path";
|
|
7
6
|
|
|
7
|
+
import { machineDb } from "./machine-scope";
|
|
8
|
+
|
|
8
9
|
// Re-export types and queries
|
|
9
10
|
export * from "./types";
|
|
10
11
|
export { queries, sessionListColumns } from "./queries";
|
|
11
12
|
export * from "./lumifyhub";
|
|
12
13
|
export * from "./lumifyhub-docs";
|
|
13
14
|
export * from "./stacks";
|
|
15
|
+
export * from "./session-parts";
|
|
14
16
|
|
|
15
17
|
const DB_PATH = dbPath();
|
|
16
18
|
const LOCK_PATH = DB_PATH + ".init-lock";
|
|
@@ -89,6 +91,8 @@ export function initDb(): Database.Database {
|
|
|
89
91
|
let _db: Database.Database | null = null;
|
|
90
92
|
|
|
91
93
|
export function getDb(): Database.Database {
|
|
94
|
+
const scoped = machineDb.getStore();
|
|
95
|
+
if (scoped) return scoped;
|
|
92
96
|
if (!_db) {
|
|
93
97
|
_db = initDb();
|
|
94
98
|
}
|
package/lib/db/lumifyhub.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type Database from "better-sqlite3";
|
|
2
|
+
import { setTask } from "./session-parts";
|
|
2
3
|
|
|
3
4
|
// The one LumifyHub account this AgentOS is connected to. The token never
|
|
4
5
|
// leaves the server: callers that talk to the UI map this to a status.
|
|
@@ -118,23 +119,21 @@ export const lumifyhubQueries = {
|
|
|
118
119
|
sessionId: string,
|
|
119
120
|
card: { cardId: string; boardId: string; list: string }
|
|
120
121
|
) =>
|
|
121
|
-
db
|
|
122
|
-
.
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
122
|
+
setTask(db, sessionId, {
|
|
123
|
+
lh_card_id: card.cardId,
|
|
124
|
+
lh_board_id: card.boardId,
|
|
125
|
+
lh_card_list: card.list,
|
|
126
|
+
}),
|
|
126
127
|
|
|
127
128
|
setTaskCardList: (db: Database.Database, sessionId: string, list: string) =>
|
|
128
|
-
db
|
|
129
|
-
.prepare(`UPDATE sessions SET lh_card_list = ? WHERE id = ?`)
|
|
130
|
-
.run(list, sessionId),
|
|
129
|
+
setTask(db, sessionId, { lh_card_list: list }),
|
|
131
130
|
|
|
132
131
|
linkedCardIds: (db: Database.Database, boardId: string) =>
|
|
133
132
|
new Set(
|
|
134
133
|
(
|
|
135
134
|
db
|
|
136
135
|
.prepare(
|
|
137
|
-
`SELECT lh_card_id FROM
|
|
136
|
+
`SELECT lh_card_id FROM session_rows WHERE lh_board_id = ? AND lh_card_id IS NOT NULL`
|
|
138
137
|
)
|
|
139
138
|
.all(boardId) as { lh_card_id: string }[]
|
|
140
139
|
).map((r) => r.lh_card_id)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import type Database from "better-sqlite3";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests only: runs a function against another database, so one process can
|
|
6
|
+
* stand in for two machines. `getDb()` (and so `db`) follows the scope; in
|
|
7
|
+
* production nothing is ever scoped and it returns the one database.
|
|
8
|
+
*/
|
|
9
|
+
export const machineDb = new AsyncLocalStorage<Database.Database>();
|
|
10
|
+
|
|
11
|
+
export const runOnMachine = <T>(database: Database.Database, fn: () => T): T =>
|
|
12
|
+
machineDb.run(database, fn);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Migration } from "../migration-types";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
key: "20261015T1600_plan_waits_for_caller",
|
|
5
|
+
up: (db) => {
|
|
6
|
+
// A plan another machine placed here: it stops at the plan for that
|
|
7
|
+
// machine to decide what comes next. On the machine that placed it, the
|
|
8
|
+
// approval's message waits for the move home. Additive.
|
|
9
|
+
const add = (col: string, type: string) => {
|
|
10
|
+
const columns = db.prepare(`PRAGMA table_info(sessions)`).all() as {
|
|
11
|
+
name: string;
|
|
12
|
+
}[];
|
|
13
|
+
if (!columns.some((c) => c.name === col))
|
|
14
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN ${col} ${type}`);
|
|
15
|
+
};
|
|
16
|
+
add("plan_waits_for_caller", "INTEGER");
|
|
17
|
+
add("plan_handoff_message", "TEXT");
|
|
18
|
+
},
|
|
19
|
+
} satisfies Migration;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { Migration } from "../migration-types";
|
|
2
|
+
import type { PartTable } from "../session-parts/columns";
|
|
3
|
+
import { createSessionRowsView } from "../session-parts/view";
|
|
4
|
+
|
|
5
|
+
// The split of sessions into focused tables, as it was when this shipped:
|
|
6
|
+
// frozen here, so changing the live split never changes what this did.
|
|
7
|
+
const PARTS: Record<string, { columns: [string, string][] }> = {
|
|
8
|
+
session_tasks: {
|
|
9
|
+
columns: [
|
|
10
|
+
["task_prompt", "TEXT"],
|
|
11
|
+
["task_brief", "TEXT"],
|
|
12
|
+
["task_status", "TEXT"],
|
|
13
|
+
["task_since", "TEXT"],
|
|
14
|
+
["task_ended_at", "TEXT"],
|
|
15
|
+
["merged_at", "TEXT"],
|
|
16
|
+
["priority", "TEXT"],
|
|
17
|
+
["quick_of", "TEXT"],
|
|
18
|
+
["moved_to", "TEXT"],
|
|
19
|
+
["moved_from", "TEXT"],
|
|
20
|
+
["model_overrides", "TEXT"],
|
|
21
|
+
["lh_card_id", "TEXT"],
|
|
22
|
+
["lh_board_id", "TEXT"],
|
|
23
|
+
["lh_card_list", "TEXT"],
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
session_prs: {
|
|
27
|
+
columns: [
|
|
28
|
+
["pr_url", "TEXT"],
|
|
29
|
+
["pr_number", "INTEGER"],
|
|
30
|
+
["pr_status", "TEXT"],
|
|
31
|
+
["merge_armed_at", "TEXT"],
|
|
32
|
+
["merge_armed_seen", "TEXT"],
|
|
33
|
+
["rebased_from", "TEXT"],
|
|
34
|
+
["rebased_to", "TEXT"],
|
|
35
|
+
["rebase_asked_head", "TEXT"],
|
|
36
|
+
["pr_wake_since", "TEXT"],
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
session_plans: {
|
|
40
|
+
columns: [
|
|
41
|
+
["plan_phase", "TEXT"],
|
|
42
|
+
["plan_only", "INTEGER"],
|
|
43
|
+
["plan_decided", "INTEGER"],
|
|
44
|
+
["plan_reason", "TEXT"],
|
|
45
|
+
["plan_note", "TEXT"],
|
|
46
|
+
["plan_edits", "TEXT"],
|
|
47
|
+
["plan_waits_for_caller", "INTEGER"],
|
|
48
|
+
["plan_handoff_message", "TEXT"],
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
session_placement: {
|
|
52
|
+
columns: [
|
|
53
|
+
["peer_mirror", "INTEGER NOT NULL DEFAULT 0"],
|
|
54
|
+
["placed_note", "TEXT"],
|
|
55
|
+
["held_by_device", "TEXT"],
|
|
56
|
+
["held_from", "TEXT"],
|
|
57
|
+
["held_for", "TEXT"],
|
|
58
|
+
["held_for_name", "TEXT"],
|
|
59
|
+
["started_by_device", "TEXT"],
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
session_pauses: {
|
|
63
|
+
columns: [
|
|
64
|
+
["pause_state", "TEXT"],
|
|
65
|
+
["paused_until", "TEXT"],
|
|
66
|
+
["pause_id", "INTEGER"],
|
|
67
|
+
["pause_note", "TEXT"],
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const hasValue = (prefix: string, column: string) =>
|
|
73
|
+
column === "peer_mirror"
|
|
74
|
+
? `COALESCE(${prefix}.${column}, 0) != 0`
|
|
75
|
+
: `${prefix}.${column} IS NOT NULL`;
|
|
76
|
+
|
|
77
|
+
export default {
|
|
78
|
+
key: "20261015T1800_split_sessions_tables",
|
|
79
|
+
up: (db) => {
|
|
80
|
+
// Additive: five tables, each keyed by the session and holding a row only
|
|
81
|
+
// for a session with a value in its group; the session_rows view that
|
|
82
|
+
// reads them back as one flat row; and triggers that keep a write to a
|
|
83
|
+
// legacy sessions column (an older build, a chat worker that has not
|
|
84
|
+
// restarted) landing in the new tables. No sessions column is touched.
|
|
85
|
+
// Writers in this build go through lib/db/session-parts. Dropping the old
|
|
86
|
+
// columns is a later step (docs/decisions/2026-10-15-split-sessions-table.md).
|
|
87
|
+
db.transaction(() => {
|
|
88
|
+
for (const [table, { columns }] of Object.entries(PARTS)) {
|
|
89
|
+
const names = columns.map(([name]) => name);
|
|
90
|
+
|
|
91
|
+
db.exec(
|
|
92
|
+
`CREATE TABLE IF NOT EXISTS ${table} (
|
|
93
|
+
session_id TEXT PRIMARY KEY REFERENCES sessions(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
|
94
|
+
${columns.map(([n, type]) => `${n} ${type}`).join(",\n ")}
|
|
95
|
+
)`
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const anyValue = (prefix: string) =>
|
|
99
|
+
names.map((n) => hasValue(prefix, n)).join(" OR ");
|
|
100
|
+
db.exec(
|
|
101
|
+
`INSERT OR IGNORE INTO ${table} (session_id, ${names.join(", ")})
|
|
102
|
+
SELECT s.id, ${names.map((n) => `s.${n}`).join(", ")}
|
|
103
|
+
FROM sessions s WHERE ${anyValue("s")}`
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
db.exec(
|
|
107
|
+
`CREATE TRIGGER IF NOT EXISTS ${table}_from_sessions_ins
|
|
108
|
+
AFTER INSERT ON sessions
|
|
109
|
+
WHEN ${anyValue("NEW")}
|
|
110
|
+
BEGIN
|
|
111
|
+
INSERT OR REPLACE INTO ${table} (session_id, ${names.join(", ")})
|
|
112
|
+
VALUES (NEW.id, ${names.map((n) => `NEW.${n}`).join(", ")});
|
|
113
|
+
END`
|
|
114
|
+
);
|
|
115
|
+
db.exec(
|
|
116
|
+
`CREATE TRIGGER IF NOT EXISTS ${table}_from_sessions_upd
|
|
117
|
+
AFTER UPDATE OF ${names.join(", ")} ON sessions
|
|
118
|
+
WHEN ${names.map((n) => `NEW.${n} IS NOT OLD.${n}`).join(" OR ")}
|
|
119
|
+
BEGIN
|
|
120
|
+
INSERT INTO ${table} (session_id, ${names.join(", ")})
|
|
121
|
+
VALUES (NEW.id, ${names.map((n) => `NEW.${n}`).join(", ")})
|
|
122
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
123
|
+
${names
|
|
124
|
+
.map(
|
|
125
|
+
(n) =>
|
|
126
|
+
`${n} = CASE WHEN NEW.${n} IS NOT OLD.${n} THEN excluded.${n} ELSE ${table}.${n} END`
|
|
127
|
+
)
|
|
128
|
+
.join(",\n ")};
|
|
129
|
+
END`
|
|
130
|
+
);
|
|
131
|
+
db.exec(
|
|
132
|
+
`CREATE TRIGGER IF NOT EXISTS ${table}_from_sessions_del
|
|
133
|
+
AFTER DELETE ON sessions
|
|
134
|
+
BEGIN
|
|
135
|
+
DELETE FROM ${table} WHERE session_id = OLD.id;
|
|
136
|
+
END`
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
for (const [suffix, event] of [
|
|
140
|
+
["ins", "INSERT"],
|
|
141
|
+
["upd", "UPDATE"],
|
|
142
|
+
["del", "DELETE"],
|
|
143
|
+
])
|
|
144
|
+
db.exec(
|
|
145
|
+
`CREATE TRIGGER IF NOT EXISTS ${table}_bump_sessions_${suffix}
|
|
146
|
+
AFTER ${event} ON ${table}
|
|
147
|
+
BEGIN
|
|
148
|
+
INSERT INTO change_versions (topic, version) VALUES ('sessions', 1)
|
|
149
|
+
ON CONFLICT(topic) DO UPDATE SET version = version + 1;
|
|
150
|
+
END`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
db.exec(
|
|
155
|
+
`CREATE INDEX IF NOT EXISTS idx_session_tasks_status ON session_tasks(task_status)`
|
|
156
|
+
);
|
|
157
|
+
db.exec(
|
|
158
|
+
`CREATE INDEX IF NOT EXISTS idx_session_tasks_moved_from ON session_tasks(moved_from)`
|
|
159
|
+
);
|
|
160
|
+
db.exec(
|
|
161
|
+
`CREATE INDEX IF NOT EXISTS idx_session_tasks_quick_of ON session_tasks(quick_of) WHERE quick_of IS NOT NULL`
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
createSessionRowsView(
|
|
165
|
+
db,
|
|
166
|
+
Object.fromEntries(
|
|
167
|
+
Object.entries(PARTS).map(([t, p]) => [t, p.columns.map(([n]) => n)])
|
|
168
|
+
) as Record<PartTable, string[]>
|
|
169
|
+
);
|
|
170
|
+
})();
|
|
171
|
+
},
|
|
172
|
+
} satisfies Migration;
|
|
@@ -33,3 +33,10 @@ export default {
|
|
|
33
33
|
`lib/db/migrations.ts` loads. `lib/db/migrations.test.ts` also checks order, the
|
|
34
34
|
legacy bodies, and that a database migrated before the split would run nothing
|
|
35
35
|
again.
|
|
36
|
+
- Adding a column to `sessions`? `session_rows` (the view that reads a session
|
|
37
|
+
with its columns from the tables they moved to) lists the columns at the time
|
|
38
|
+
it was built. Add the name to `SESSION_ROW_COLUMNS` in
|
|
39
|
+
`lib/db/session-parts/columns.ts` and call `createSessionRowsView(db)` after
|
|
40
|
+
the `ALTER`; `lib/db/split-sessions-migration.test.ts` fails if the view and
|
|
41
|
+
the table disagree. Columns that belong to a group go in `PART_COLUMNS` and a
|
|
42
|
+
new migration for that table instead.
|