@saadnvd1/agent-os 0.3.637 → 0.3.644
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/bus/branches/route.ts +25 -0
- package/app/api/bus/resurrect/route.test.ts +3 -6
- 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/app/api/worktree-sweep/route.test.ts +40 -0
- package/app/api/worktree-sweep/route.ts +31 -0
- package/app/api/worktree-sweep/run/route.ts +26 -0
- package/components/Health/HealthPanel.tsx +4 -1
- package/components/Health/SweepRuns.tsx +125 -0
- package/components/Merge/GlobalMergeSection.tsx +2 -0
- package/components/Merge/ProjectMergeSection.tsx +2 -0
- package/components/Merge/WorktreeSweepSection.tsx +125 -0
- package/components/Settings/NoApprovalsSection.tsx +1 -1
- package/components/Settings/SettingsDialog.tsx +1 -1
- package/data/worktree-sweep/index.ts +56 -0
- package/lib/approvals/enable.ts +1 -1
- package/lib/away/report.ts +5 -5
- 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/backup.ts +49 -0
- 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/20261015T1733_gate_loop_guard.ts +40 -0
- package/lib/db/migrations/20261015T1800_split_sessions_tables.ts +172 -0
- package/lib/db/migrations/20261015T1830_drop_moved_session_columns.ts +173 -0
- package/lib/db/migrations/20261015T1900_worktree_sweep.ts +30 -0
- package/lib/db/migrations/README.md +11 -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 +10 -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/rows.ts +69 -0
- package/lib/db/session-parts/tasks.ts +27 -0
- package/lib/db/session-parts/view.ts +50 -0
- package/lib/db/session-parts/write.ts +61 -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/done/remote.ts +9 -20
- 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.test-helpers.ts +228 -0
- package/lib/orchestrator/armed.ts +18 -16
- 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/ask-verdict.ts +111 -0
- package/lib/orchestrator/asks.ts +26 -0
- package/lib/orchestrator/brakes.ts +1 -1
- package/lib/orchestrator/brief.ts +2 -1
- package/lib/orchestrator/digest.ts +2 -2
- package/lib/orchestrator/escalate.ts +25 -12
- package/lib/orchestrator/external-gates.ts +1 -0
- package/lib/orchestrator/external-pr.ts +2 -2
- package/lib/orchestrator/facts.ts +4 -28
- package/lib/orchestrator/gates.ts +94 -25
- package/lib/orchestrator/held-asks.ts +8 -11
- package/lib/orchestrator/held-review.ts +8 -4
- package/lib/orchestrator/home.ts +2 -2
- package/lib/orchestrator/loop-guard.ts +145 -0
- package/lib/orchestrator/merge-approvals.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 +11 -8
- package/lib/orchestrator/read.ts +1 -1
- package/lib/orchestrator/rebase.ts +8 -11
- package/lib/orchestrator/reconcile.ts +2 -2
- package/lib/orchestrator/review-prompt.ts +2 -0
- package/lib/orchestrator/review.ts +14 -4
- package/lib/orchestrator/signoff.test-helpers.ts +322 -0
- package/lib/orchestrator/signoff.ts +49 -11
- package/lib/orchestrator/targets.ts +1 -1
- package/lib/orchestrator/task-state.ts +4 -2
- package/lib/orchestrator/testing.ts +11 -14
- package/lib/orchestrator/watcher.ts +4 -0
- package/lib/plans/features.ts +1 -1
- package/lib/plans/release.ts +1 -1
- package/lib/ports.ts +1 -1
- package/lib/recipes/test-fixtures.ts +2 -3
- package/lib/resurrect/find.ts +1 -1
- package/lib/resurrect/index.ts +44 -20
- package/lib/resurrect/swept.ts +61 -0
- 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/testing.ts +17 -16
- 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 +30 -23
- 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-testing.ts +16 -7
- 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/pr-activity.ts +11 -2
- package/lib/tasks/prune-branches.ts +193 -0
- package/lib/tasks/prune-scope.ts +22 -0
- 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/remote-branch.ts +170 -0
- package/lib/tasks/session.ts +7 -4
- package/lib/tasks/start.ts +4 -4
- package/lib/tasks/starter.ts +8 -11
- package/lib/tasks/testing.ts +13 -7
- 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/lib/worktree-sweep/commits.ts +65 -0
- package/lib/worktree-sweep/findOrphans.ts +139 -0
- package/lib/worktree-sweep/findSettled.ts +112 -0
- package/lib/worktree-sweep/index.ts +10 -0
- package/lib/worktree-sweep/judge.ts +31 -0
- package/lib/worktree-sweep/panes.ts +18 -0
- package/lib/worktree-sweep/runSweep.ts +243 -0
- package/lib/worktree-sweep/settings.ts +83 -0
- package/lib/worktree-sweep/start.ts +64 -0
- package/lib/worktree-sweep/state.ts +36 -0
- package/lib/worktree-sweep/store.ts +96 -0
- package/lib/worktree-sweep/types.ts +53 -0
- package/package.json +1 -1
- package/scripts/backfill-session-logs.ts +1 -1
- package/scripts/screenshots/seed.ts +19 -18
- package/server.ts +3 -0
- package/lib/tasks/held-routes.ts +0 -33
- package/lib/tasks/remote.ts +0 -255
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,40 @@
|
|
|
1
|
+
import type { Migration } from "../migration-types";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
key: "20261015T1733_gate_loop_guard",
|
|
5
|
+
up: (db) => {
|
|
6
|
+
// The loop guard (orchestrator/loop-guard.ts): how many commits in a row
|
|
7
|
+
// a gate has failed on, the last failure's signature, whether it repeated
|
|
8
|
+
// the one before, and the reason the task went to Saad. Additive.
|
|
9
|
+
const columns = (table: string) =>
|
|
10
|
+
new Set(
|
|
11
|
+
(
|
|
12
|
+
db.prepare(`PRAGMA table_info(${table})`).all() as { name: string }[]
|
|
13
|
+
).map((c) => c.name)
|
|
14
|
+
);
|
|
15
|
+
const failures = columns("orchestrator_gate_failures");
|
|
16
|
+
if (!failures.has("streak")) {
|
|
17
|
+
db.exec(
|
|
18
|
+
`ALTER TABLE orchestrator_gate_failures ADD COLUMN streak INTEGER NOT NULL DEFAULT 0`
|
|
19
|
+
);
|
|
20
|
+
// A pass never reset the old count, so it's the most the streak can be.
|
|
21
|
+
db.exec(`UPDATE orchestrator_gate_failures SET streak = count`);
|
|
22
|
+
}
|
|
23
|
+
if (!failures.has("last_signature"))
|
|
24
|
+
db.exec(
|
|
25
|
+
`ALTER TABLE orchestrator_gate_failures ADD COLUMN last_signature TEXT`
|
|
26
|
+
);
|
|
27
|
+
if (!failures.has("repeated"))
|
|
28
|
+
db.exec(
|
|
29
|
+
`ALTER TABLE orchestrator_gate_failures ADD COLUMN repeated INTEGER NOT NULL DEFAULT 0`
|
|
30
|
+
);
|
|
31
|
+
if (!failures.has("escalated_why"))
|
|
32
|
+
db.exec(
|
|
33
|
+
`ALTER TABLE orchestrator_gate_failures ADD COLUMN escalated_why TEXT`
|
|
34
|
+
);
|
|
35
|
+
if (!columns("orchestrator_gate_failure_commits").has("signature"))
|
|
36
|
+
db.exec(
|
|
37
|
+
`ALTER TABLE orchestrator_gate_failure_commits ADD COLUMN signature TEXT`
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
} 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;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { Migration } from "../migration-types";
|
|
2
|
+
import type { PartTable } from "../session-parts/columns";
|
|
3
|
+
import { createSessionRowsView } from "../session-parts/view";
|
|
4
|
+
import { backupDatabase } from "../backup";
|
|
5
|
+
|
|
6
|
+
// The moved columns as 20261015T1800 split them, frozen here.
|
|
7
|
+
const PARTS: Record<PartTable, string[]> = {
|
|
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
|
+
};
|
|
56
|
+
|
|
57
|
+
const hasValue = (prefix: string, column: string) =>
|
|
58
|
+
column === "peer_mirror"
|
|
59
|
+
? `COALESCE(${prefix}.${column}, 0) != 0`
|
|
60
|
+
: `${prefix}.${column} IS NOT NULL`;
|
|
61
|
+
|
|
62
|
+
// A value in the part table wins (it is the newer write); one only on
|
|
63
|
+
// sessions is carried over.
|
|
64
|
+
const carried = (table: string, column: string) =>
|
|
65
|
+
column === "peer_mirror"
|
|
66
|
+
? `MAX(COALESCE(${table}.${column}, 0), COALESCE(s.${column}, 0))`
|
|
67
|
+
: `COALESCE(${table}.${column}, s.${column})`;
|
|
68
|
+
|
|
69
|
+
export default {
|
|
70
|
+
key: "20261015T1830_drop_moved_session_columns",
|
|
71
|
+
up: (db) => {
|
|
72
|
+
// The destructive half of the split (docs/decisions/2026-10-15-split-sessions-table.md):
|
|
73
|
+
// the 42 moved columns leave sessions. A full copy of the database comes
|
|
74
|
+
// first, and nothing changes unless it was written. Every value still only
|
|
75
|
+
// on sessions is carried into its table before the columns go.
|
|
76
|
+
const parts = Object.entries(PARTS) as [PartTable, string[]][];
|
|
77
|
+
const remaining = () => {
|
|
78
|
+
const onSessions = new Set(
|
|
79
|
+
(
|
|
80
|
+
db.prepare(`PRAGMA table_info(sessions)`).all() as { name: string }[]
|
|
81
|
+
).map((c) => c.name)
|
|
82
|
+
);
|
|
83
|
+
return parts.flatMap(([, cs]) => cs).filter((c) => onSessions.has(c));
|
|
84
|
+
};
|
|
85
|
+
if (remaining().length === 0) return;
|
|
86
|
+
|
|
87
|
+
const sessions = (
|
|
88
|
+
db.prepare(`SELECT COUNT(*) AS n FROM sessions`).get() as { n: number }
|
|
89
|
+
).n;
|
|
90
|
+
// An empty sessions table holds nothing these columns could lose.
|
|
91
|
+
if (sessions > 0) backupDatabase(db, "drop-moved-session-columns");
|
|
92
|
+
|
|
93
|
+
// Immediate, and re-read inside: another process opening the same
|
|
94
|
+
// database may have run this between the check and the lock.
|
|
95
|
+
db.transaction(() => {
|
|
96
|
+
const left = remaining();
|
|
97
|
+
if (left.length === 0) return;
|
|
98
|
+
for (const [table, all] of parts) {
|
|
99
|
+
const columns = all.filter((c) => left.includes(c));
|
|
100
|
+
if (columns.length === 0) continue;
|
|
101
|
+
db.exec(
|
|
102
|
+
`INSERT INTO ${table} (session_id, ${columns.join(", ")})
|
|
103
|
+
SELECT s.id, ${columns.map((c) => `s.${c}`).join(", ")}
|
|
104
|
+
FROM sessions s
|
|
105
|
+
WHERE (${columns.map((c) => hasValue("s", c)).join(" OR ")})
|
|
106
|
+
AND NOT EXISTS (SELECT 1 FROM ${table} p WHERE p.session_id = s.id)`
|
|
107
|
+
);
|
|
108
|
+
db.exec(
|
|
109
|
+
`UPDATE ${table} SET
|
|
110
|
+
${columns.map((c) => `${c} = ${carried(table, c)}`).join(",\n ")}
|
|
111
|
+
FROM sessions s WHERE s.id = ${table}.session_id`
|
|
112
|
+
);
|
|
113
|
+
db.exec(`DROP TRIGGER IF EXISTS ${table}_from_sessions_ins`);
|
|
114
|
+
db.exec(`DROP TRIGGER IF EXISTS ${table}_from_sessions_upd`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// merged_at and task_ended_at are stamped where task_status now lives.
|
|
118
|
+
// A first write of a status inserts the row; a later one updates it.
|
|
119
|
+
db.exec(`DROP TRIGGER IF EXISTS sessions_task_ended_at`);
|
|
120
|
+
db.exec(`DROP TRIGGER IF EXISTS sessions_merged_at`);
|
|
121
|
+
db.exec(`
|
|
122
|
+
CREATE TRIGGER IF NOT EXISTS session_tasks_ended_at_upd
|
|
123
|
+
AFTER UPDATE OF task_status ON session_tasks
|
|
124
|
+
WHEN NEW.task_status IN ('dropped', 'done')
|
|
125
|
+
AND OLD.task_status IS NOT NEW.task_status
|
|
126
|
+
AND NEW.task_ended_at IS OLD.task_ended_at
|
|
127
|
+
BEGIN
|
|
128
|
+
UPDATE session_tasks SET task_ended_at = datetime('now')
|
|
129
|
+
WHERE session_id = NEW.session_id;
|
|
130
|
+
END
|
|
131
|
+
`);
|
|
132
|
+
db.exec(`
|
|
133
|
+
CREATE TRIGGER IF NOT EXISTS session_tasks_ended_at_ins
|
|
134
|
+
AFTER INSERT ON session_tasks
|
|
135
|
+
WHEN NEW.task_status IN ('dropped', 'done')
|
|
136
|
+
AND NEW.task_ended_at IS NULL
|
|
137
|
+
BEGIN
|
|
138
|
+
UPDATE session_tasks SET task_ended_at = datetime('now')
|
|
139
|
+
WHERE session_id = NEW.session_id;
|
|
140
|
+
END
|
|
141
|
+
`);
|
|
142
|
+
db.exec(`
|
|
143
|
+
CREATE TRIGGER IF NOT EXISTS session_tasks_merged_at_upd
|
|
144
|
+
AFTER UPDATE OF task_status ON session_tasks
|
|
145
|
+
WHEN NEW.task_status = 'merged' AND OLD.task_status IS NOT 'merged'
|
|
146
|
+
-- unless the same statement set it (an import keeps its own)
|
|
147
|
+
AND NEW.merged_at IS OLD.merged_at
|
|
148
|
+
BEGIN
|
|
149
|
+
UPDATE session_tasks SET merged_at = datetime('now')
|
|
150
|
+
WHERE session_id = NEW.session_id;
|
|
151
|
+
END
|
|
152
|
+
`);
|
|
153
|
+
db.exec(`
|
|
154
|
+
CREATE TRIGGER IF NOT EXISTS session_tasks_merged_at_ins
|
|
155
|
+
AFTER INSERT ON session_tasks
|
|
156
|
+
WHEN NEW.task_status = 'merged' AND NEW.merged_at IS NULL
|
|
157
|
+
BEGIN
|
|
158
|
+
UPDATE session_tasks SET merged_at = datetime('now')
|
|
159
|
+
WHERE session_id = NEW.session_id;
|
|
160
|
+
END
|
|
161
|
+
`);
|
|
162
|
+
|
|
163
|
+
db.exec(`DROP VIEW IF EXISTS session_rows`);
|
|
164
|
+
db.exec(`DROP INDEX IF EXISTS idx_sessions_task_status`);
|
|
165
|
+
db.exec(`DROP INDEX IF EXISTS idx_sessions_moved_from`);
|
|
166
|
+
db.exec(`DROP INDEX IF EXISTS idx_sessions_quick_of`);
|
|
167
|
+
for (const column of left)
|
|
168
|
+
db.exec(`ALTER TABLE sessions DROP COLUMN ${column}`);
|
|
169
|
+
|
|
170
|
+
createSessionRowsView(db, PARTS);
|
|
171
|
+
}).immediate();
|
|
172
|
+
},
|
|
173
|
+
} satisfies Migration;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Migration } from "../migration-types";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
key: "20261015T1900_worktree_sweep",
|
|
5
|
+
up: (db) => {
|
|
6
|
+
// One row per clean-up run, kept for good, with what it removed and
|
|
7
|
+
// skipped as JSON; and which sessions' worktrees a run removed, so a
|
|
8
|
+
// restore can say so. Additive.
|
|
9
|
+
db.exec(`
|
|
10
|
+
CREATE TABLE IF NOT EXISTS worktree_sweep_runs (
|
|
11
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
12
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
13
|
+
finished_at TEXT,
|
|
14
|
+
dry_run INTEGER NOT NULL DEFAULT 0,
|
|
15
|
+
removed INTEGER NOT NULL DEFAULT 0,
|
|
16
|
+
freed_bytes INTEGER NOT NULL DEFAULT 0,
|
|
17
|
+
skipped INTEGER NOT NULL DEFAULT 0,
|
|
18
|
+
details TEXT,
|
|
19
|
+
error TEXT
|
|
20
|
+
);
|
|
21
|
+
CREATE TABLE IF NOT EXISTS session_worktree_sweeps (
|
|
22
|
+
session_id TEXT PRIMARY KEY,
|
|
23
|
+
path TEXT NOT NULL,
|
|
24
|
+
branch TEXT,
|
|
25
|
+
swept_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
26
|
+
run_id INTEGER
|
|
27
|
+
);
|
|
28
|
+
`);
|
|
29
|
+
},
|
|
30
|
+
} satisfies Migration;
|
|
@@ -33,3 +33,14 @@ 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, in the order of `SESSION_ROW_COLUMNS`. 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.
|
|
42
|
+
Columns that belong to a group go in `PART_COLUMNS` and a new migration for
|
|
43
|
+
that table instead.
|
|
44
|
+
- A destructive step (dropping a column or table) takes a full copy first:
|
|
45
|
+
`backupDatabase` in `lib/db/backup.ts` (`VACUUM INTO` under
|
|
46
|
+
`~/.agent-os/backups/`), and throws so the migration stops if it can't.
|
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) =>
|