@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
|
@@ -0,0 +1,31 @@
|
|
|
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 plan step: phase, note, edits and the hand-off.
|
|
6
|
+
export const setPlan = (
|
|
7
|
+
db: Database.Database,
|
|
8
|
+
sessionId: string,
|
|
9
|
+
patch: PartPatch<"session_plans">,
|
|
10
|
+
when?: When<"session_plans">
|
|
11
|
+
): boolean => writePart(db, "session_plans", sessionId, patch, when);
|
|
12
|
+
|
|
13
|
+
// Moves a plan from one phase to the next, only while the task is running
|
|
14
|
+
// and still in `from` (a step another caller took first returns false).
|
|
15
|
+
export function advancePlan(
|
|
16
|
+
db: Database.Database,
|
|
17
|
+
sessionId: string,
|
|
18
|
+
from: string,
|
|
19
|
+
to: NonNullable<PartPatch<"session_plans">["plan_phase"]>
|
|
20
|
+
): boolean {
|
|
21
|
+
return db.transaction(() => {
|
|
22
|
+
const running = db
|
|
23
|
+
.prepare(
|
|
24
|
+
`SELECT 1 FROM session_rows WHERE id = ? AND task_status = 'running'`
|
|
25
|
+
)
|
|
26
|
+
.get(sessionId);
|
|
27
|
+
return running
|
|
28
|
+
? setPlan(db, sessionId, { plan_phase: to }, { plan_phase: from })
|
|
29
|
+
: false;
|
|
30
|
+
})();
|
|
31
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
import type { Session } from "@/lib/db/types";
|
|
3
|
+
import type { PartPatch } from "./columns";
|
|
4
|
+
import { writePart, type When } from "./write";
|
|
5
|
+
|
|
6
|
+
// A session's PR and the merge and rebase state kept for it.
|
|
7
|
+
export const setPr = (
|
|
8
|
+
db: Database.Database,
|
|
9
|
+
sessionId: string,
|
|
10
|
+
patch: PartPatch<"session_prs">,
|
|
11
|
+
when?: When<"session_prs">
|
|
12
|
+
): boolean => writePart(db, "session_prs", sessionId, patch, when);
|
|
13
|
+
|
|
14
|
+
// A PR found for the session: its link, number and state, and the session
|
|
15
|
+
// counts as touched.
|
|
16
|
+
export function recordPr(
|
|
17
|
+
db: Database.Database,
|
|
18
|
+
sessionId: string,
|
|
19
|
+
pr: { url: string; number: number; state: string }
|
|
20
|
+
): void {
|
|
21
|
+
db.transaction(() => {
|
|
22
|
+
setPr(db, sessionId, {
|
|
23
|
+
pr_url: pr.url,
|
|
24
|
+
pr_number: pr.number,
|
|
25
|
+
pr_status: pr.state as Session["pr_status"],
|
|
26
|
+
});
|
|
27
|
+
db.prepare(
|
|
28
|
+
`UPDATE sessions SET updated_at = datetime('now') WHERE id = ?`
|
|
29
|
+
).run(sessionId);
|
|
30
|
+
})();
|
|
31
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
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 brief, status, priority, models and card: lib/db/session-parts/columns.ts.
|
|
6
|
+
export const setTask = (
|
|
7
|
+
db: Database.Database,
|
|
8
|
+
sessionId: string,
|
|
9
|
+
patch: PartPatch<"session_tasks">,
|
|
10
|
+
when?: When<"session_tasks">
|
|
11
|
+
): boolean => writePart(db, "session_tasks", sessionId, patch, when);
|
|
12
|
+
|
|
13
|
+
// The task's first start, kept across moves: set once, from created_at when
|
|
14
|
+
// the caller has no better time.
|
|
15
|
+
export function startTaskClock(
|
|
16
|
+
db: Database.Database,
|
|
17
|
+
sessionId: string,
|
|
18
|
+
at?: string | null
|
|
19
|
+
): void {
|
|
20
|
+
const row = db
|
|
21
|
+
.prepare(`SELECT created_at, task_since FROM session_rows WHERE id = ?`)
|
|
22
|
+
.get(sessionId) as
|
|
23
|
+
| { created_at: string; task_since: string | null }
|
|
24
|
+
| undefined;
|
|
25
|
+
if (!row || row.task_since) return;
|
|
26
|
+
setTask(db, sessionId, { task_since: at ?? row.created_at });
|
|
27
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
import { PART_COLUMNS, type PartTable } from "./columns";
|
|
3
|
+
|
|
4
|
+
const ALIAS: Record<PartTable, string> = {
|
|
5
|
+
session_tasks: "t",
|
|
6
|
+
session_prs: "pr",
|
|
7
|
+
session_plans: "pl",
|
|
8
|
+
session_placement: "pp",
|
|
9
|
+
session_pauses: "pa",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// `session_rows`: the sessions table with each moved column read from its own
|
|
13
|
+
// table, under the same names and in the same order, so a row is the flat
|
|
14
|
+
// session it always was. A column with a default on sessions
|
|
15
|
+
// (peer_mirror) keeps it. Rebuild it in any migration that adds a sessions
|
|
16
|
+
// column. `parts` is the split as that migration knew it.
|
|
17
|
+
export function createSessionRowsView(
|
|
18
|
+
db: Database.Database,
|
|
19
|
+
parts: Record<PartTable, readonly string[]> = PART_COLUMNS
|
|
20
|
+
): void {
|
|
21
|
+
const source = new Map<string, string>();
|
|
22
|
+
for (const table of Object.keys(parts) as PartTable[])
|
|
23
|
+
for (const c of parts[table]) source.set(c, ALIAS[table]);
|
|
24
|
+
const sessionColumns = (
|
|
25
|
+
db.prepare(`PRAGMA table_info(sessions)`).all() as { name: string }[]
|
|
26
|
+
).map((c) => c.name);
|
|
27
|
+
const select = sessionColumns
|
|
28
|
+
.map((c) => {
|
|
29
|
+
const alias = source.get(c);
|
|
30
|
+
if (!alias) return `s.${c}`;
|
|
31
|
+
return c === "peer_mirror"
|
|
32
|
+
? `COALESCE(${alias}.${c}, 0) AS ${c}`
|
|
33
|
+
: `${alias}.${c} AS ${c}`;
|
|
34
|
+
})
|
|
35
|
+
.join(", ");
|
|
36
|
+
const joins = (Object.keys(parts) as PartTable[])
|
|
37
|
+
.map((t) => `LEFT JOIN ${t} ${ALIAS[t]} ON ${ALIAS[t]}.session_id = s.id`)
|
|
38
|
+
.join(" ");
|
|
39
|
+
db.exec(`DROP VIEW IF EXISTS session_rows`);
|
|
40
|
+
db.exec(
|
|
41
|
+
`CREATE VIEW session_rows AS SELECT ${select} FROM sessions s ${joins}`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
import { PART_COLUMNS, type PartPatch, type PartTable } from "./columns";
|
|
3
|
+
|
|
4
|
+
type Value = string | number | null;
|
|
5
|
+
|
|
6
|
+
// A condition on the part row's current values, checked with the write in one
|
|
7
|
+
// transaction: every column must equal one of its listed values (null for
|
|
8
|
+
// "unset").
|
|
9
|
+
export type When<T extends PartTable> = {
|
|
10
|
+
[C in keyof PartPatch<T>]?: Value | readonly Value[];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const sameAs = (current: unknown, wanted: Value | readonly Value[]): boolean =>
|
|
14
|
+
(Array.isArray(wanted) ? (wanted as Value[]) : [wanted as Value]).some(
|
|
15
|
+
(w) => (current ?? null) === w
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
// The one writer of the part tables. Writes the patched columns to the part
|
|
19
|
+
// table, then mirrors the same values onto the legacy sessions columns, which
|
|
20
|
+
// stay until they are dropped (docs/decisions/2026-10-15-split-sessions-table.md):
|
|
21
|
+
// that UPDATE is the only dual-write, and the step that drops the columns
|
|
22
|
+
// deletes it. Returns whether it wrote (false: the session is gone or `when`
|
|
23
|
+
// no longer holds).
|
|
24
|
+
export function writePart<T extends PartTable>(
|
|
25
|
+
db: Database.Database,
|
|
26
|
+
table: T,
|
|
27
|
+
sessionId: string,
|
|
28
|
+
patch: PartPatch<T>,
|
|
29
|
+
when?: When<T>
|
|
30
|
+
): boolean {
|
|
31
|
+
const allowed = PART_COLUMNS[table] as readonly string[];
|
|
32
|
+
const columns = Object.keys(patch).filter(
|
|
33
|
+
(c) => (patch as Record<string, unknown>)[c] !== undefined
|
|
34
|
+
);
|
|
35
|
+
for (const c of [...columns, ...Object.keys(when ?? {})])
|
|
36
|
+
if (!allowed.includes(c)) throw new Error(`${c} is not a ${table} column`);
|
|
37
|
+
if (columns.length === 0) return false;
|
|
38
|
+
|
|
39
|
+
const run = db.transaction((): boolean => {
|
|
40
|
+
if (when && Object.keys(when).length > 0) {
|
|
41
|
+
const current = db
|
|
42
|
+
.prepare(
|
|
43
|
+
`SELECT ${Object.keys(when).join(", ")} FROM session_rows WHERE id = ?`
|
|
44
|
+
)
|
|
45
|
+
.get(sessionId) as Record<string, unknown> | undefined;
|
|
46
|
+
if (!current) return false;
|
|
47
|
+
for (const [c, wanted] of Object.entries(when))
|
|
48
|
+
if (!sameAs(current[c], wanted as Value | readonly Value[]))
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const values = { id: sessionId, ...(patch as Record<string, Value>) };
|
|
52
|
+
const inserted = db
|
|
53
|
+
.prepare(
|
|
54
|
+
`INSERT INTO ${table} (session_id, ${columns.join(", ")})
|
|
55
|
+
SELECT id, ${columns.map((c) => `@${c}`).join(", ")}
|
|
56
|
+
FROM sessions WHERE id = @id
|
|
57
|
+
ON CONFLICT(session_id) DO UPDATE SET
|
|
58
|
+
${columns.map((c) => `${c} = excluded.${c}`).join(", ")}`
|
|
59
|
+
)
|
|
60
|
+
.run(values);
|
|
61
|
+
if (inserted.changes === 0) return false;
|
|
62
|
+
db.prepare(
|
|
63
|
+
`UPDATE sessions SET ${columns.map((c) => `${c} = @${c}`).join(", ")}
|
|
64
|
+
WHERE id = @id`
|
|
65
|
+
).run(values);
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
return run();
|
|
69
|
+
}
|
package/lib/done/archive.ts
CHANGED
|
@@ -34,7 +34,7 @@ export function unarchiveSession(id: string): Session {
|
|
|
34
34
|
`UPDATE sessions SET archived_at = NULL WHERE id = ? AND archived_at IS NOT NULL`
|
|
35
35
|
)
|
|
36
36
|
.run(id).changes;
|
|
37
|
-
const row = db.prepare(`SELECT * FROM
|
|
37
|
+
const row = db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as
|
|
38
38
|
| Session
|
|
39
39
|
| undefined;
|
|
40
40
|
if (!row) throw new Error("Session not found");
|
|
@@ -48,7 +48,7 @@ export function listArchived(workspaceId?: string): ArchivedView[] {
|
|
|
48
48
|
.prepare(
|
|
49
49
|
`SELECT s.id, s.name, s.project_id, s.view, s.task_status, s.branch_name,
|
|
50
50
|
s.pr_url, s.worktree_path, s.archived_at, p.name AS project_name, p.workspace_id AS project_workspace
|
|
51
|
-
FROM
|
|
51
|
+
FROM session_rows s LEFT JOIN projects p ON p.id = s.project_id
|
|
52
52
|
WHERE s.archived_at IS NOT NULL
|
|
53
53
|
AND (? IS NULL OR p.workspace_id = ?)
|
|
54
54
|
ORDER BY s.archived_at DESC, s.updated_at DESC`
|
package/lib/done/auto-archive.ts
CHANGED
|
@@ -33,7 +33,7 @@ const inDone = (s: Session) =>
|
|
|
33
33
|
export async function archiveStaleDone(now = Date.now()): Promise<string[]> {
|
|
34
34
|
const rows = db
|
|
35
35
|
.prepare(
|
|
36
|
-
`SELECT * FROM
|
|
36
|
+
`SELECT * FROM session_rows
|
|
37
37
|
WHERE archived_at IS NULL AND pinned = 0 AND role IS NULL
|
|
38
38
|
AND peer_mirror = 0 AND quick_of IS NULL
|
|
39
39
|
AND (pr_status IS NULL OR pr_status != 'open')`
|
package/lib/done/bulk.ts
CHANGED
|
@@ -50,7 +50,7 @@ function scopeSessions(scope: DoneScope): Session[] {
|
|
|
50
50
|
if (!projectIds.length) return [];
|
|
51
51
|
return db
|
|
52
52
|
.prepare(
|
|
53
|
-
`SELECT * FROM
|
|
53
|
+
`SELECT * FROM session_rows
|
|
54
54
|
WHERE project_id IN (${projectIds.map(() => "?").join(", ")})
|
|
55
55
|
AND role IS NULL AND archived_at IS NULL
|
|
56
56
|
-- A linked machine's own sessions are cleaned up there.
|
package/lib/done/index.ts
CHANGED
|
@@ -31,10 +31,11 @@ import { releasePorts } from "../ports";
|
|
|
31
31
|
import { dropSessionDatabase } from "../project-config/database";
|
|
32
32
|
import { doneOnPeer, peerSessionLink } from "../hosts/peer-actions";
|
|
33
33
|
import { hostLink } from "../hosts/remote-api";
|
|
34
|
-
import { doneHeldTask, isHeldRemote } from "../tasks/held-
|
|
34
|
+
import { doneHeldTask, isHeldRemote } from "../tasks/contract/held-calls";
|
|
35
35
|
import { captureMergedTask } from "../session-logs";
|
|
36
36
|
import { sessionEnded } from "../experiments/hooks";
|
|
37
37
|
import { pushMerged } from "../notify/push/events";
|
|
38
|
+
import { setPr, setTask } from "../db/session-parts";
|
|
38
39
|
|
|
39
40
|
export interface DoneOptions {
|
|
40
41
|
// Who asked: the orchestrator's gate failures count toward Saad, as its
|
|
@@ -108,11 +109,10 @@ async function mergeThroughGates(
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
function mark(s: Session, status: "merged" | "done", pr: TaskPR | null) {
|
|
111
|
-
db.
|
|
112
|
-
status
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
).run(s.id);
|
|
112
|
+
db.transaction(() => {
|
|
113
|
+
setTask(db, s.id, { task_status: status });
|
|
114
|
+
if (status === "merged") setPr(db, s.id, { pr_status: "merged" });
|
|
115
|
+
})();
|
|
116
116
|
endPauseOnFinish(s.id);
|
|
117
117
|
syncTaskCardInBackground(s, status, pr);
|
|
118
118
|
void sessionEnded(s.id, status);
|
|
@@ -133,10 +133,7 @@ export async function doneSession(
|
|
|
133
133
|
if (s.role === "orchestrator")
|
|
134
134
|
throw new Error("An orchestrator isn't marked done.");
|
|
135
135
|
if (s.archived_at) throw new Error(`${s.name} is already archived.`);
|
|
136
|
-
if (s.task_prompt)
|
|
137
|
-
db.prepare(`UPDATE sessions SET task_status = 'done' WHERE id = ?`).run(
|
|
138
|
-
s.id
|
|
139
|
-
);
|
|
136
|
+
if (s.task_prompt) setTask(db, s.id, { task_status: "done" });
|
|
140
137
|
archiveSession(s.id);
|
|
141
138
|
return {
|
|
142
139
|
id: s.id,
|
|
@@ -180,10 +177,7 @@ export async function doneSession(
|
|
|
180
177
|
// agent and removes the worktree, which is where they are.
|
|
181
178
|
await doneHeldTask(s);
|
|
182
179
|
if (plan.mark) mark(s, plan.mark, plan.pr);
|
|
183
|
-
else
|
|
184
|
-
db.prepare(
|
|
185
|
-
`UPDATE sessions SET task_status = 'done' WHERE id = ? AND task_status = 'running'`
|
|
186
|
-
).run(s.id);
|
|
180
|
+
else setTask(db, s.id, { task_status: "done" }, { task_status: "running" });
|
|
187
181
|
worktree = {
|
|
188
182
|
action: "removed",
|
|
189
183
|
why: `on ${hostLink(s.host_id)?.hostName ?? "its machine"}`,
|
package/lib/health/evidence.ts
CHANGED
|
@@ -61,7 +61,7 @@ function schedulerSnapshot(f: HealthFacts): string {
|
|
|
61
61
|
const running = db
|
|
62
62
|
.prepare(
|
|
63
63
|
`SELECT s.name, s.pause_state, s.plan_only, s.plan_phase, s.task_status
|
|
64
|
-
FROM
|
|
64
|
+
FROM session_rows s WHERE s.task_status IN ('running', 'moving')
|
|
65
65
|
AND s.archived_at IS NULL AND s.quick_of IS NULL LIMIT 20`
|
|
66
66
|
)
|
|
67
67
|
.all() as {
|
package/lib/health/facts.ts
CHANGED
|
@@ -123,7 +123,7 @@ function armed(): ArmedFact[] {
|
|
|
123
123
|
db
|
|
124
124
|
.prepare(
|
|
125
125
|
`SELECT s.id, s.name, s.merge_armed_at, s.pr_number, p.workspace_id
|
|
126
|
-
FROM
|
|
126
|
+
FROM session_rows s LEFT JOIN projects p ON p.id = s.project_id
|
|
127
127
|
WHERE s.merge_armed_at IS NOT NULL AND s.archived_at IS NULL
|
|
128
128
|
AND COALESCE(s.task_status, 'running') = 'running'`
|
|
129
129
|
)
|
package/lib/health/pieces.ts
CHANGED
|
@@ -39,7 +39,7 @@ export function mergedSince(
|
|
|
39
39
|
return (
|
|
40
40
|
db
|
|
41
41
|
.prepare(
|
|
42
|
-
`SELECT id, name, pr_number, task_prompt, updated_at FROM
|
|
42
|
+
`SELECT id, name, pr_number, task_prompt, updated_at FROM session_rows
|
|
43
43
|
WHERE project_id = ? AND task_status = 'merged'
|
|
44
44
|
AND updated_at >= datetime(?)`
|
|
45
45
|
)
|
|
@@ -15,7 +15,7 @@ import { isValidTmuxName } from "./attach";
|
|
|
15
15
|
import { homeRelative } from "./discover";
|
|
16
16
|
import { toPeerSession } from "./peer-sessions";
|
|
17
17
|
import { insertMirror } from "./peer-sync";
|
|
18
|
-
import { hostTasks, postIdempotent } from "../tasks/
|
|
18
|
+
import { list as hostTasks, postIdempotent } from "../tasks/contract/client";
|
|
19
19
|
import type { ProjectRef } from "../tasks/project-ref";
|
|
20
20
|
import {
|
|
21
21
|
HostApiError,
|
|
@@ -83,7 +83,7 @@ const path = (id: string, rest = "") =>
|
|
|
83
83
|
`/api/sessions/${encodeURIComponent(id)}${rest}`;
|
|
84
84
|
|
|
85
85
|
const row = (id: string) =>
|
|
86
|
-
db.prepare(`SELECT * FROM
|
|
86
|
+
db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(id) as Session;
|
|
87
87
|
|
|
88
88
|
export async function renameOnPeer(
|
|
89
89
|
link: HostLink,
|
package/lib/hosts/peer-mirror.ts
CHANGED
|
@@ -16,7 +16,7 @@ export async function mirrorPeerSession(
|
|
|
16
16
|
sessionId: string
|
|
17
17
|
): Promise<Session> {
|
|
18
18
|
const existing = db
|
|
19
|
-
.prepare(`SELECT * FROM
|
|
19
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
20
20
|
.get(sessionId) as Session | undefined;
|
|
21
21
|
if (existing) {
|
|
22
22
|
if (existing.host_id !== hostId)
|
|
@@ -43,6 +43,6 @@ export async function mirrorPeerSession(
|
|
|
43
43
|
)
|
|
44
44
|
);
|
|
45
45
|
return db
|
|
46
|
-
.prepare(`SELECT * FROM
|
|
46
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
47
47
|
.get(peer.id) as Session;
|
|
48
48
|
}
|
package/lib/hosts/peer-sync.ts
CHANGED
|
@@ -6,37 +6,45 @@
|
|
|
6
6
|
* go there (peer-actions.ts).
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { db, queries, type Project } from "../db";
|
|
9
|
+
import { db, queries, type Project, type Session } from "../db";
|
|
10
10
|
import { isValidAgentType } from "../providers";
|
|
11
11
|
import { projectMatcher } from "./discover";
|
|
12
12
|
import type { PeerSession } from "./peer-sessions";
|
|
13
|
+
import { setPlacement, setPr } from "../db/session-parts";
|
|
13
14
|
|
|
14
15
|
export function insertMirror(
|
|
15
16
|
peer: PeerSession,
|
|
16
17
|
projectId: string | null
|
|
17
18
|
): void {
|
|
18
|
-
db.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
19
|
+
db.transaction(() => {
|
|
20
|
+
const inserted = db
|
|
21
|
+
.prepare(
|
|
22
|
+
`INSERT INTO sessions (id, name, tmux_name, working_directory, model, group_path,
|
|
23
|
+
agent_type, project_id, host_id, view, name_source, updated_at)
|
|
24
|
+
VALUES (?, ?, ?, ?, ?, 'sessions', ?, ?, ?, ?, 'user',
|
|
25
|
+
COALESCE(?, datetime('now')))
|
|
26
|
+
ON CONFLICT(id) DO NOTHING`
|
|
27
|
+
)
|
|
28
|
+
.run(
|
|
29
|
+
peer.id,
|
|
30
|
+
peer.name,
|
|
31
|
+
peer.tmuxName || `${peer.agentType}-${peer.id}`,
|
|
32
|
+
peer.path || "~",
|
|
33
|
+
peer.model,
|
|
34
|
+
isValidAgentType(peer.agentType) ? peer.agentType : "shell",
|
|
35
|
+
projectId,
|
|
36
|
+
peer.hostId,
|
|
37
|
+
peer.view,
|
|
38
|
+
peer.updatedAt
|
|
39
|
+
);
|
|
40
|
+
if (!inserted.changes) return;
|
|
41
|
+
setPr(db, peer.id, {
|
|
42
|
+
pr_url: peer.prUrl,
|
|
43
|
+
pr_number: peer.prNumber,
|
|
44
|
+
pr_status: peer.prStatus as Session["pr_status"],
|
|
45
|
+
});
|
|
46
|
+
setPlacement(db, peer.id, { peer_mirror: 1 });
|
|
47
|
+
})();
|
|
40
48
|
}
|
|
41
49
|
|
|
42
50
|
// One listing mirrors at most this many: a listing is another machine's
|
|
@@ -71,17 +79,46 @@ export function syncPeerMirrors(
|
|
|
71
79
|
);
|
|
72
80
|
// That machine lists the id, so it's its session: a row mirrored on open
|
|
73
81
|
// before this column existed is marked too.
|
|
74
|
-
const
|
|
82
|
+
const current = db.prepare(
|
|
83
|
+
`SELECT name, view, tmux_name, project_id, pr_url, pr_number, pr_status,
|
|
84
|
+
updated_at, peer_mirror
|
|
85
|
+
FROM session_rows WHERE id = ? AND host_id = ? AND task_prompt IS NULL`
|
|
86
|
+
);
|
|
87
|
+
const touch = db.prepare(
|
|
75
88
|
`UPDATE sessions SET name = ?, view = ?, tmux_name = ?, project_id = ?,
|
|
76
|
-
|
|
77
|
-
updated_at = COALESCE(?, updated_at), peer_mirror = 1
|
|
78
|
-
WHERE id = ? AND host_id = ? AND task_prompt IS NULL
|
|
79
|
-
AND NOT (name IS ? AND view IS ? AND tmux_name IS ? AND project_id IS ?
|
|
80
|
-
AND pr_url IS ? AND pr_number IS ? AND pr_status IS ?
|
|
81
|
-
AND updated_at IS COALESCE(?, updated_at) AND peer_mirror = 1)`
|
|
89
|
+
updated_at = COALESCE(?, updated_at) WHERE id = ?`
|
|
82
90
|
);
|
|
91
|
+
const update = (
|
|
92
|
+
peer: PeerSession,
|
|
93
|
+
fields: (string | number | null)[]
|
|
94
|
+
): void => {
|
|
95
|
+
const [name, view, tmuxName, projectId, prUrl, prNumber, prStatus, at] =
|
|
96
|
+
fields;
|
|
97
|
+
const cur = current.get(peer.id, hostId) as
|
|
98
|
+
| Record<string, string | number | null>
|
|
99
|
+
| undefined;
|
|
100
|
+
if (!cur) return;
|
|
101
|
+
const same =
|
|
102
|
+
cur.name === name &&
|
|
103
|
+
cur.view === view &&
|
|
104
|
+
cur.tmux_name === tmuxName &&
|
|
105
|
+
cur.project_id === projectId &&
|
|
106
|
+
cur.pr_url === prUrl &&
|
|
107
|
+
cur.pr_number === prNumber &&
|
|
108
|
+
cur.pr_status === prStatus &&
|
|
109
|
+
cur.updated_at === (at ?? cur.updated_at) &&
|
|
110
|
+
cur.peer_mirror === 1;
|
|
111
|
+
if (same) return;
|
|
112
|
+
touch.run(name, view, tmuxName, projectId, at, peer.id);
|
|
113
|
+
setPr(db, peer.id, {
|
|
114
|
+
pr_url: prUrl as string | null,
|
|
115
|
+
pr_number: prNumber as number | null,
|
|
116
|
+
pr_status: prStatus as Session["pr_status"],
|
|
117
|
+
});
|
|
118
|
+
setPlacement(db, peer.id, { peer_mirror: 1 });
|
|
119
|
+
};
|
|
83
120
|
const mirrors = db.prepare(
|
|
84
|
-
`SELECT id FROM
|
|
121
|
+
`SELECT id FROM session_rows WHERE host_id = ? AND peer_mirror = 1
|
|
85
122
|
AND task_prompt IS NULL AND archived_at IS NULL
|
|
86
123
|
AND created_at < datetime(?, 'unixepoch')`
|
|
87
124
|
);
|
|
@@ -112,7 +149,7 @@ export function syncPeerMirrors(
|
|
|
112
149
|
peer.prStatus,
|
|
113
150
|
peer.updatedAt,
|
|
114
151
|
];
|
|
115
|
-
update
|
|
152
|
+
update(peer, fields);
|
|
116
153
|
}
|
|
117
154
|
if (!complete) return;
|
|
118
155
|
const now = new Set(listed.map((p) => p.id));
|
|
@@ -80,7 +80,7 @@ export async function readSession(id: string): Promise<SessionInput | null> {
|
|
|
80
80
|
? (
|
|
81
81
|
db
|
|
82
82
|
.prepare(
|
|
83
|
-
`SELECT s.task_prompt AS p FROM
|
|
83
|
+
`SELECT s.task_prompt AS p FROM session_rows s JOIN projects p ON p.id = s.project_id
|
|
84
84
|
WHERE p.workspace_id = ? AND s.id != ? AND s.quick_of IS NULL
|
|
85
85
|
AND s.task_prompt IS NOT NULL
|
|
86
86
|
AND s.created_at >= datetime('now', ?)
|
|
@@ -30,7 +30,7 @@ export function listPending(): PendingRow[] {
|
|
|
30
30
|
`SELECT lc.*, s.name AS session_name,
|
|
31
31
|
COALESCE(lc.workspace_id, s.workspace_id, p.workspace_id) AS ws
|
|
32
32
|
FROM learning_cards lc
|
|
33
|
-
JOIN
|
|
33
|
+
JOIN session_rows s ON s.id = lc.session_id
|
|
34
34
|
LEFT JOIN projects p ON p.id = s.project_id
|
|
35
35
|
WHERE lc.status = 'pending'
|
|
36
36
|
ORDER BY lc.created_at DESC, lc.id DESC`
|
package/lib/models/plan-step.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
planPathOf,
|
|
34
34
|
splitOf,
|
|
35
35
|
} from "../plans/features";
|
|
36
|
+
import { advancePlan, setPlan } from "../db/session-parts";
|
|
36
37
|
|
|
37
38
|
export const PLAN_FILE = ".agentos/plan.md";
|
|
38
39
|
|
|
@@ -164,10 +165,7 @@ function announcePlan(s: Session, ref: string | null, retried = false): void {
|
|
|
164
165
|
const again = retried ? " (asked once more for it first)" : "";
|
|
165
166
|
const head = planNoteFor(s, file, next, again, ref ? " anyway" : "");
|
|
166
167
|
if (s.held_by_device) {
|
|
167
|
-
db.
|
|
168
|
-
head,
|
|
169
|
-
s.id
|
|
170
|
-
);
|
|
168
|
+
setPlan(db, s.id, { plan_note: head });
|
|
171
169
|
return;
|
|
172
170
|
}
|
|
173
171
|
queueEvent(workspaceId!, `plan-done:${s.id}`, s.id, head);
|
|
@@ -227,9 +225,9 @@ export const implementMessageId = (sessionId: string) =>
|
|
|
227
225
|
type Send = (sessionId: string, id: string) => Promise<void>;
|
|
228
226
|
|
|
229
227
|
function getSession(sessionId: string): Session | undefined {
|
|
230
|
-
return db
|
|
231
|
-
|
|
232
|
-
| undefined;
|
|
228
|
+
return db
|
|
229
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
230
|
+
.get(sessionId) as Session | undefined;
|
|
233
231
|
}
|
|
234
232
|
|
|
235
233
|
// Planning (or, once approved, awaiting) becomes implementing, once: the
|
|
@@ -273,13 +271,7 @@ async function startImplementing(
|
|
|
273
271
|
let nothingOwn = false;
|
|
274
272
|
let featureId: string | null = null;
|
|
275
273
|
const claimed = db.transaction(() => {
|
|
276
|
-
|
|
277
|
-
.prepare(
|
|
278
|
-
`UPDATE sessions SET plan_phase = 'implementing'
|
|
279
|
-
WHERE id = ? AND plan_phase = ? AND task_status = 'running'`
|
|
280
|
-
)
|
|
281
|
-
.run(sessionId, from).changes;
|
|
282
|
-
if (!changed) return false;
|
|
274
|
+
if (!advancePlan(db, sessionId, from, "implementing")) return false;
|
|
283
275
|
setSessionModel(sessionId, routed);
|
|
284
276
|
let piece: string | undefined;
|
|
285
277
|
let note = amendment;
|
|
@@ -485,13 +477,7 @@ export async function handoffAfterPlan(
|
|
|
485
477
|
});
|
|
486
478
|
return ref !== null;
|
|
487
479
|
}
|
|
488
|
-
|
|
489
|
-
.prepare(
|
|
490
|
-
`UPDATE sessions SET plan_phase = 'awaiting'
|
|
491
|
-
WHERE id = ? AND plan_phase = 'planning' AND task_status = 'running'`
|
|
492
|
-
)
|
|
493
|
-
.run(sessionId).changes;
|
|
494
|
-
if (!awaiting) return false;
|
|
480
|
+
if (!advancePlan(db, sessionId, "planning", "awaiting")) return false;
|
|
495
481
|
try {
|
|
496
482
|
announcePlan(s, null, retried);
|
|
497
483
|
} catch (e) {
|
|
@@ -538,7 +524,7 @@ export async function recheckPlanning(
|
|
|
538
524
|
): Promise<void> {
|
|
539
525
|
const rows = db
|
|
540
526
|
.prepare(
|
|
541
|
-
`SELECT id FROM
|
|
527
|
+
`SELECT id FROM session_rows
|
|
542
528
|
WHERE plan_phase = 'planning' AND task_status = 'running'
|
|
543
529
|
AND archived_at IS NULL AND worktree_path IS NOT NULL`
|
|
544
530
|
)
|
|
@@ -603,7 +589,7 @@ export async function approvePlan(
|
|
|
603
589
|
// On another machine, the plan was written there: this machine decides
|
|
604
590
|
// where it is built, and the approval follows it.
|
|
605
591
|
const ref = isRemoteHost(s.host_id)
|
|
606
|
-
? await (await import("../tasks/
|
|
592
|
+
? await (await import("../tasks/contract/plan")).continuePlanThere(s, text)
|
|
607
593
|
: await startImplementing(s, "awaiting", send, text);
|
|
608
594
|
if (!ref) throw new Error(`${s.name} isn't waiting for plan approval`);
|
|
609
595
|
if (by === "no-approvals")
|
|
@@ -617,7 +603,7 @@ export async function approvePlan(
|
|
|
617
603
|
return { ref, amendments: scopeChangesOf(s.id).length };
|
|
618
604
|
}
|
|
619
605
|
|
|
620
|
-
// A plan that moved here to be built (
|
|
606
|
+
// A plan that moved here to be built (contract/plan.ts): implementation starts
|
|
621
607
|
// on this machine, once, with the approval's change if it had one.
|
|
622
608
|
export async function implementArrived(
|
|
623
609
|
sessionId: string,
|
package/lib/models/resume.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
type ModelOverride,
|
|
20
20
|
} from "./resolve";
|
|
21
21
|
import { sessionRole } from "./session-role";
|
|
22
|
+
import { setTask } from "../db/session-parts";
|
|
22
23
|
|
|
23
24
|
export const RESUME_MESSAGE =
|
|
24
25
|
"You were paused. Carry on where you left off: check the worktree and the conversation above for where things stand, then finish the task.";
|
|
@@ -45,9 +46,9 @@ export async function resumeOnModel(
|
|
|
45
46
|
model: unknown,
|
|
46
47
|
message = RESUME_MESSAGE
|
|
47
48
|
): Promise<ResumeResult> {
|
|
48
|
-
const s = db
|
|
49
|
-
|
|
50
|
-
| undefined;
|
|
49
|
+
const s = db
|
|
50
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
51
|
+
.get(sessionId) as Session | undefined;
|
|
51
52
|
if (!s?.task_status) throw new Error("No such task");
|
|
52
53
|
if (s.task_status !== "running")
|
|
53
54
|
throw new Error(
|
|
@@ -65,10 +66,7 @@ export async function resumeOnModel(
|
|
|
65
66
|
throw new Error(
|
|
66
67
|
"It's mid-turn: stop it first, or wait for the turn to end"
|
|
67
68
|
);
|
|
68
|
-
db
|
|
69
|
-
JSON.stringify(override),
|
|
70
|
-
sessionId
|
|
71
|
-
);
|
|
69
|
+
setTask(db, sessionId, { model_overrides: JSON.stringify(override) });
|
|
72
70
|
if (launchPending(sessionId)) {
|
|
73
71
|
setSessionModel(sessionId, routed);
|
|
74
72
|
return { ref: routed.ref, resumed: false };
|
package/lib/notify/index.ts
CHANGED
|
@@ -160,7 +160,7 @@ export async function notifyFrom(
|
|
|
160
160
|
text: string
|
|
161
161
|
): Promise<Outcome> {
|
|
162
162
|
const session = from
|
|
163
|
-
? (db.prepare(`SELECT * FROM
|
|
163
|
+
? (db.prepare(`SELECT * FROM session_rows WHERE id = ?`).get(from) as
|
|
164
164
|
| Session
|
|
165
165
|
| undefined)
|
|
166
166
|
: undefined;
|
|
@@ -92,7 +92,7 @@ export const pushPlanAwaiting = (
|
|
|
92
92
|
export const pushPlanReady = (sessionId: string, itemId: string) => {
|
|
93
93
|
// Only a task has a screen to open; a plain chat's plan goes to the list.
|
|
94
94
|
const task = db
|
|
95
|
-
.prepare(`SELECT task_status FROM
|
|
95
|
+
.prepare(`SELECT task_status FROM session_rows WHERE id = ?`)
|
|
96
96
|
.get(sessionId) as { task_status: string | null } | undefined;
|
|
97
97
|
fire(
|
|
98
98
|
{
|