@saadnvd1/agent-os 0.3.637 → 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 +3 -4
- 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 -5
- 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 +4 -6
- 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 +1 -1
- package/lib/approvals/enable.ts +1 -1
- 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/hold.ts +3 -3
- package/lib/chat/refresh.ts +1 -1
- package/lib/chat/registry.ts +3 -3
- package/lib/chat/runner.ts +1 -1
- 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/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/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/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 +10 -24
- 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/{remote-plan-path.ts → contract/plan-path.ts} +2 -2
- package/lib/tasks/{remote-plan.ts → contract/plan.ts} +76 -110
- 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 +14 -18
- package/lib/tasks/import.ts +19 -15
- package/lib/tasks/index.ts +34 -22
- package/lib/tasks/launch-gate.ts +1 -1
- package/lib/tasks/move-carry.ts +15 -7
- package/lib/tasks/move-flow.ts +29 -36
- package/lib/tasks/move-guard.ts +1 -1
- package/lib/tasks/move-recover.ts +6 -25
- package/lib/tasks/move.ts +23 -23
- package/lib/tasks/moved-away.ts +1 -1
- package/lib/tasks/pause-hold.ts +13 -5
- package/lib/tasks/pause.ts +19 -23
- package/lib/tasks/plan-view.ts +1 -1
- package/lib/tasks/queue-placement.ts +1 -1
- package/lib/tasks/queue-rows.ts +4 -4
- package/lib/tasks/queue-start.ts +5 -3
- package/lib/tasks/queue-tick.ts +2 -2
- package/lib/tasks/queue.ts +14 -6
- 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/lib/tasks/held-routes.ts +0 -33
- package/lib/tasks/remote.ts +0 -255
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/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,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.
|
package/lib/db/queries.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type Database from "better-sqlite3";
|
|
2
|
+
import { SESSION_ROW_COLUMNS } from "./session-parts/columns";
|
|
2
3
|
|
|
3
4
|
// Prepared statement cache
|
|
4
5
|
const stmtCache = new Map<string, Database.Statement>();
|
|
@@ -13,28 +14,18 @@ function getStmt(db: Database.Database, sql: string): Database.Statement {
|
|
|
13
14
|
return stmt;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
// Every
|
|
17
|
+
// Every session column but the two large text ones, which only the single
|
|
17
18
|
// session paths read; a list gets has_brief for whether a brief exists.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
db.prepare(`PRAGMA table_info(sessions)`).all() as { name: string }[]
|
|
24
|
-
)
|
|
25
|
-
.map((c) => c.name)
|
|
26
|
-
.filter((n) => n !== "task_brief" && n !== "system_prompt")
|
|
27
|
-
.join(",");
|
|
28
|
-
listColumns.set(db, cols);
|
|
29
|
-
}
|
|
19
|
+
// Read from session_rows (lib/db/session-parts).
|
|
20
|
+
const LIST_COLUMNS = SESSION_ROW_COLUMNS.filter(
|
|
21
|
+
(n) => n !== "task_brief" && n !== "system_prompt"
|
|
22
|
+
);
|
|
23
|
+
export function sessionListColumns(_db: Database.Database, alias = ""): string {
|
|
30
24
|
const prefix = alias ? `${alias}.` : "";
|
|
31
|
-
const list =
|
|
32
|
-
.split(",")
|
|
33
|
-
.map((c) => `${prefix}${c}`)
|
|
34
|
-
.join(", ");
|
|
25
|
+
const list = LIST_COLUMNS.map((c) => `${prefix}${c}`).join(", ");
|
|
35
26
|
// The recipe run or feature piece a session is, as JSON, for its row's chip.
|
|
36
|
-
const recipe = `(SELECT json_object('id', r.id, 'name', r.name, 'seq', rr.seq, 'status', rr.status) FROM recipe_runs rr JOIN recipes r ON r.id = rr.recipe_id WHERE rr.session_id = ${alias || "
|
|
37
|
-
const piece = `(SELECT json_object('feature', f.id, 'name', f.name, 'position', fp.position, 'total', (SELECT COUNT(*) FROM feature_pieces WHERE feature_id = f.id AND status != 'dropped')) FROM feature_pieces fp JOIN features f ON f.id = fp.feature_id WHERE fp.session_id = ${alias || "
|
|
27
|
+
const recipe = `(SELECT json_object('id', r.id, 'name', r.name, 'seq', rr.seq, 'status', rr.status) FROM recipe_runs rr JOIN recipes r ON r.id = rr.recipe_id WHERE rr.session_id = ${alias || "session_rows"}.id)`;
|
|
28
|
+
const piece = `(SELECT json_object('feature', f.id, 'name', f.name, 'position', fp.position, 'total', (SELECT COUNT(*) FROM feature_pieces WHERE feature_id = f.id AND status != 'dropped')) FROM feature_pieces fp JOIN features f ON f.id = fp.feature_id WHERE fp.session_id = ${alias || "session_rows"}.id)`;
|
|
38
29
|
return `${list}, (${prefix}task_brief IS NOT NULL AND ${prefix}task_brief != '') AS has_brief, ${recipe} AS recipe_run, ${piece} AS feature_piece`;
|
|
39
30
|
}
|
|
40
31
|
|
|
@@ -48,12 +39,12 @@ export const queries = {
|
|
|
48
39
|
),
|
|
49
40
|
|
|
50
41
|
getSession: (db: Database.Database) =>
|
|
51
|
-
getStmt(db, `SELECT * FROM
|
|
42
|
+
getStmt(db, `SELECT * FROM session_rows WHERE id = ?`),
|
|
52
43
|
|
|
53
44
|
getAllSessions: (db: Database.Database) =>
|
|
54
45
|
getStmt(
|
|
55
46
|
db,
|
|
56
|
-
`SELECT ${sessionListColumns(db)} FROM
|
|
47
|
+
`SELECT ${sessionListColumns(db)} FROM session_rows ORDER BY updated_at DESC`
|
|
57
48
|
),
|
|
58
49
|
|
|
59
50
|
// The sidebar's sessions: not archived, and not a dropped task (a
|
|
@@ -62,7 +53,7 @@ export const queries = {
|
|
|
62
53
|
getSidebarSessions: (db: Database.Database) =>
|
|
63
54
|
getStmt(
|
|
64
55
|
db,
|
|
65
|
-
`SELECT ${sessionListColumns(db)} FROM
|
|
56
|
+
`SELECT ${sessionListColumns(db)} FROM session_rows
|
|
66
57
|
WHERE archived_at IS NULL
|
|
67
58
|
AND (task_status IS NULL
|
|
68
59
|
OR task_status NOT IN ('dropped', 'moved'))
|
|
@@ -103,12 +94,6 @@ export const queries = {
|
|
|
103
94
|
`UPDATE sessions SET worktree_path = ?, branch_name = ?, base_branch = ?, dev_server_port = ?, updated_at = datetime('now') WHERE id = ?`
|
|
104
95
|
),
|
|
105
96
|
|
|
106
|
-
updateSessionPR: (db: Database.Database) =>
|
|
107
|
-
getStmt(
|
|
108
|
-
db,
|
|
109
|
-
`UPDATE sessions SET pr_url = ?, pr_number = ?, pr_status = ?, updated_at = datetime('now') WHERE id = ?`
|
|
110
|
-
),
|
|
111
|
-
|
|
112
97
|
updateSessionGroup: (db: Database.Database) =>
|
|
113
98
|
getStmt(
|
|
114
99
|
db,
|
|
@@ -118,7 +103,7 @@ export const queries = {
|
|
|
118
103
|
getSessionsByGroup: (db: Database.Database) =>
|
|
119
104
|
getStmt(
|
|
120
105
|
db,
|
|
121
|
-
`SELECT * FROM
|
|
106
|
+
`SELECT * FROM session_rows WHERE group_path = ? ORDER BY updated_at DESC`
|
|
122
107
|
),
|
|
123
108
|
|
|
124
109
|
moveSessionsToGroup: (db: Database.Database) =>
|
|
@@ -136,14 +121,14 @@ export const queries = {
|
|
|
136
121
|
getSessionsByProject: (db: Database.Database) =>
|
|
137
122
|
getStmt(
|
|
138
123
|
db,
|
|
139
|
-
`SELECT * FROM
|
|
124
|
+
`SELECT * FROM session_rows WHERE project_id = ? ORDER BY updated_at DESC`
|
|
140
125
|
),
|
|
141
126
|
|
|
142
127
|
// Orchestration
|
|
143
128
|
getWorkersByConductor: (db: Database.Database) =>
|
|
144
129
|
getStmt(
|
|
145
130
|
db,
|
|
146
|
-
`SELECT * FROM
|
|
131
|
+
`SELECT * FROM session_rows WHERE conductor_session_id = ? ORDER BY created_at ASC`
|
|
147
132
|
),
|
|
148
133
|
|
|
149
134
|
updateWorkerStatus: (db: Database.Database) =>
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { Session } from "@/lib/db/types";
|
|
2
|
+
|
|
3
|
+
// The sessions columns that live in their own tables, by concern. Each table
|
|
4
|
+
// is keyed by session_id and holds a row only for a session that has a value
|
|
5
|
+
// in the group; `session_rows` joins them back into the flat shape of a
|
|
6
|
+
// session. See docs/decisions/2026-10-15-split-sessions-table.md.
|
|
7
|
+
export const PART_COLUMNS = {
|
|
8
|
+
session_tasks: [
|
|
9
|
+
"task_prompt",
|
|
10
|
+
"task_brief",
|
|
11
|
+
"task_status",
|
|
12
|
+
"task_since",
|
|
13
|
+
"task_ended_at",
|
|
14
|
+
"merged_at",
|
|
15
|
+
"priority",
|
|
16
|
+
"quick_of",
|
|
17
|
+
"moved_to",
|
|
18
|
+
"moved_from",
|
|
19
|
+
"model_overrides",
|
|
20
|
+
"lh_card_id",
|
|
21
|
+
"lh_board_id",
|
|
22
|
+
"lh_card_list",
|
|
23
|
+
],
|
|
24
|
+
session_prs: [
|
|
25
|
+
"pr_url",
|
|
26
|
+
"pr_number",
|
|
27
|
+
"pr_status",
|
|
28
|
+
"merge_armed_at",
|
|
29
|
+
"merge_armed_seen",
|
|
30
|
+
"rebased_from",
|
|
31
|
+
"rebased_to",
|
|
32
|
+
"rebase_asked_head",
|
|
33
|
+
"pr_wake_since",
|
|
34
|
+
],
|
|
35
|
+
session_plans: [
|
|
36
|
+
"plan_phase",
|
|
37
|
+
"plan_only",
|
|
38
|
+
"plan_decided",
|
|
39
|
+
"plan_reason",
|
|
40
|
+
"plan_note",
|
|
41
|
+
"plan_edits",
|
|
42
|
+
"plan_waits_for_caller",
|
|
43
|
+
"plan_handoff_message",
|
|
44
|
+
],
|
|
45
|
+
session_placement: [
|
|
46
|
+
"peer_mirror",
|
|
47
|
+
"placed_note",
|
|
48
|
+
"held_by_device",
|
|
49
|
+
"held_from",
|
|
50
|
+
"held_for",
|
|
51
|
+
"held_for_name",
|
|
52
|
+
"started_by_device",
|
|
53
|
+
],
|
|
54
|
+
session_pauses: ["pause_state", "paused_until", "pause_id", "pause_note"],
|
|
55
|
+
} as const satisfies Record<string, readonly (keyof Session)[]>;
|
|
56
|
+
|
|
57
|
+
export type PartTable = keyof typeof PART_COLUMNS;
|
|
58
|
+
export type PartColumn<T extends PartTable> = (typeof PART_COLUMNS)[T][number];
|
|
59
|
+
export type PartPatch<T extends PartTable> = {
|
|
60
|
+
[C in PartColumn<T>]?: Session[C & keyof Session];
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const PART_TABLES = Object.keys(PART_COLUMNS) as PartTable[];
|
|
64
|
+
|
|
65
|
+
// Everything else is an INTEGER or TEXT by this list.
|
|
66
|
+
export const INTEGER_COLUMNS: ReadonlySet<string> = new Set([
|
|
67
|
+
"pr_number",
|
|
68
|
+
"plan_only",
|
|
69
|
+
"plan_decided",
|
|
70
|
+
"plan_waits_for_caller",
|
|
71
|
+
"peer_mirror",
|
|
72
|
+
"pause_id",
|
|
73
|
+
]);
|
|
74
|
+
|
|
75
|
+
const partOfColumn = new Map<string, PartTable>(
|
|
76
|
+
PART_TABLES.flatMap((t) =>
|
|
77
|
+
(PART_COLUMNS[t] as readonly string[]).map((c) => [c, t] as const)
|
|
78
|
+
)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
export const isMovedColumn = (column: string): boolean =>
|
|
82
|
+
partOfColumn.has(column);
|
|
83
|
+
|
|
84
|
+
export const partOf = (column: string): PartTable | undefined =>
|
|
85
|
+
partOfColumn.get(column);
|
|
86
|
+
|
|
87
|
+
// Every column of the sessions table, in order: the shape of a session row
|
|
88
|
+
// and of `session_rows`. A migration that adds a sessions column adds it here
|
|
89
|
+
// and rebuilds the view (createSessionRowsView).
|
|
90
|
+
export const SESSION_ROW_COLUMNS: readonly string[] = [
|
|
91
|
+
"id",
|
|
92
|
+
"name",
|
|
93
|
+
"created_at",
|
|
94
|
+
"updated_at",
|
|
95
|
+
"status",
|
|
96
|
+
"working_directory",
|
|
97
|
+
"parent_session_id",
|
|
98
|
+
"claude_session_id",
|
|
99
|
+
"model",
|
|
100
|
+
"system_prompt",
|
|
101
|
+
"group_path",
|
|
102
|
+
"agent_type",
|
|
103
|
+
"worktree_path",
|
|
104
|
+
"branch_name",
|
|
105
|
+
"base_branch",
|
|
106
|
+
"dev_server_port",
|
|
107
|
+
"pr_url",
|
|
108
|
+
"pr_number",
|
|
109
|
+
"pr_status",
|
|
110
|
+
"conductor_session_id",
|
|
111
|
+
"worker_task",
|
|
112
|
+
"worker_status",
|
|
113
|
+
"auto_approve",
|
|
114
|
+
"project_id",
|
|
115
|
+
"tmux_name",
|
|
116
|
+
"host_id",
|
|
117
|
+
"task_prompt",
|
|
118
|
+
"task_status",
|
|
119
|
+
"view",
|
|
120
|
+
"lh_card_id",
|
|
121
|
+
"lh_board_id",
|
|
122
|
+
"lh_card_list",
|
|
123
|
+
"chat_access",
|
|
124
|
+
"chat_resume_at",
|
|
125
|
+
"last_seen_at",
|
|
126
|
+
"role",
|
|
127
|
+
"workspace_id",
|
|
128
|
+
"orch_token",
|
|
129
|
+
"archived_at",
|
|
130
|
+
"pinned",
|
|
131
|
+
"chat_suggestion",
|
|
132
|
+
"chat_plan",
|
|
133
|
+
"chat_context",
|
|
134
|
+
"chat_usage",
|
|
135
|
+
"name_source",
|
|
136
|
+
"setup_status",
|
|
137
|
+
"setup_ms",
|
|
138
|
+
"setup_error",
|
|
139
|
+
"task_brief",
|
|
140
|
+
"moved_to",
|
|
141
|
+
"moved_from",
|
|
142
|
+
"port_slot",
|
|
143
|
+
"ports",
|
|
144
|
+
"database",
|
|
145
|
+
"peer_mirror",
|
|
146
|
+
"chat_restarted",
|
|
147
|
+
"chat_tools_digest",
|
|
148
|
+
"merge_armed_at",
|
|
149
|
+
"merge_armed_seen",
|
|
150
|
+
"rebased_from",
|
|
151
|
+
"rebased_to",
|
|
152
|
+
"rebase_asked_head",
|
|
153
|
+
"chat_refreshed",
|
|
154
|
+
"pr_wake_since",
|
|
155
|
+
"merged_at",
|
|
156
|
+
"task_ended_at",
|
|
157
|
+
"model_provider",
|
|
158
|
+
"model_overrides",
|
|
159
|
+
"plan_phase",
|
|
160
|
+
"quick_of",
|
|
161
|
+
"started_by",
|
|
162
|
+
"plan_decided",
|
|
163
|
+
"plan_reason",
|
|
164
|
+
"task_since",
|
|
165
|
+
"held_by_device",
|
|
166
|
+
"held_from",
|
|
167
|
+
"plan_note",
|
|
168
|
+
"held_for",
|
|
169
|
+
"held_for_name",
|
|
170
|
+
"plan_only",
|
|
171
|
+
"plan_edits",
|
|
172
|
+
"active_at",
|
|
173
|
+
"settled_at",
|
|
174
|
+
"snoozed_until",
|
|
175
|
+
"snoozed_at",
|
|
176
|
+
"priority",
|
|
177
|
+
"pause_state",
|
|
178
|
+
"paused_until",
|
|
179
|
+
"pause_id",
|
|
180
|
+
"placed_note",
|
|
181
|
+
"started_by_device",
|
|
182
|
+
"pause_note",
|
|
183
|
+
"started_via",
|
|
184
|
+
"plan_waits_for_caller",
|
|
185
|
+
"plan_handoff_message",
|
|
186
|
+
];
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
import { partOf, type PartTable } from "./columns";
|
|
3
|
+
import { writePart } from "./write";
|
|
4
|
+
|
|
5
|
+
// Copies values onto a session's moved columns where it has none yet: a
|
|
6
|
+
// column that already holds a value, and a null to copy, are left alone.
|
|
7
|
+
// Names that aren't moved columns are the caller's to write.
|
|
8
|
+
export function fillMissing(
|
|
9
|
+
db: Database.Database,
|
|
10
|
+
sessionId: string,
|
|
11
|
+
values: Record<string, string | number | null | undefined>
|
|
12
|
+
): void {
|
|
13
|
+
const byTable = new Map<PartTable, string[]>();
|
|
14
|
+
for (const [column, value] of Object.entries(values)) {
|
|
15
|
+
const table = partOf(column);
|
|
16
|
+
if (!table || value === null || value === undefined) continue;
|
|
17
|
+
byTable.set(table, [...(byTable.get(table) ?? []), column]);
|
|
18
|
+
}
|
|
19
|
+
db.transaction(() => {
|
|
20
|
+
for (const [table, columns] of byTable) {
|
|
21
|
+
const current = db
|
|
22
|
+
.prepare(`SELECT ${columns.join(", ")} FROM session_rows WHERE id = ?`)
|
|
23
|
+
.get(sessionId) as Record<string, unknown> | undefined;
|
|
24
|
+
if (!current) continue;
|
|
25
|
+
const patch = Object.fromEntries(
|
|
26
|
+
columns
|
|
27
|
+
.filter((c) => current[c] === null)
|
|
28
|
+
.map((c) => [c, values[c]] as const)
|
|
29
|
+
);
|
|
30
|
+
writePart(db, table, sessionId, patch as never);
|
|
31
|
+
}
|
|
32
|
+
})();
|
|
33
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./columns";
|
|
2
|
+
export { createSessionRowsView } from "./view";
|
|
3
|
+
export { writePart, type When } from "./write";
|
|
4
|
+
export { setTask, startTaskClock } from "./tasks";
|
|
5
|
+
export { setPr, recordPr } from "./prs";
|
|
6
|
+
export { setPlan, advancePlan } from "./plans";
|
|
7
|
+
export { setPlacement } from "./placement";
|
|
8
|
+
export { setPause } from "./pauses";
|
|
9
|
+
export { fillMissing } from "./fill";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
import type { PartPatch } from "./columns";
|
|
3
|
+
import { writePart, type When } from "./write";
|
|
4
|
+
|
|
5
|
+
// A task's timed pause.
|
|
6
|
+
export const setPause = (
|
|
7
|
+
db: Database.Database,
|
|
8
|
+
sessionId: string,
|
|
9
|
+
patch: PartPatch<"session_pauses">,
|
|
10
|
+
when?: When<"session_pauses">
|
|
11
|
+
): boolean => writePart(db, "session_pauses", sessionId, patch, when);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
import type { PartPatch } from "./columns";
|
|
3
|
+
import { writePart, type When } from "./write";
|
|
4
|
+
|
|
5
|
+
// Where a task came from and who holds it: mirror, holder, starting device.
|
|
6
|
+
export const setPlacement = (
|
|
7
|
+
db: Database.Database,
|
|
8
|
+
sessionId: string,
|
|
9
|
+
patch: PartPatch<"session_placement">,
|
|
10
|
+
when?: When<"session_placement">
|
|
11
|
+
): boolean => writePart(db, "session_placement", sessionId, patch, when);
|