@saadnvd1/agent-os 0.3.631 → 0.3.639
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/api/sessions/[id]/pr/route.ts +3 -3
- package/app/api/sessions/[id]/setup/route.ts +3 -3
- package/app/api/tasks/[id]/approve-plan/route.ts +5 -1
- package/app/api/tasks/[id]/arm/route.ts +6 -5
- package/app/api/tasks/[id]/chat-facts/route.ts +2 -2
- package/app/api/tasks/[id]/chat-tail/route.ts +2 -2
- package/app/api/tasks/[id]/checks/route.ts +1 -1
- package/app/api/tasks/[id]/diff/route.ts +1 -1
- package/app/api/tasks/[id]/done/route.ts +2 -2
- package/app/api/tasks/[id]/drop/route.ts +6 -2
- package/app/api/tasks/[id]/export/route.ts +3 -4
- package/app/api/tasks/[id]/merge/route.ts +3 -3
- package/app/api/tasks/[id]/message/route.ts +2 -2
- package/app/api/tasks/[id]/model/route.ts +5 -1
- package/app/api/tasks/[id]/move/route.ts +5 -1
- package/app/api/tasks/[id]/moved/route.ts +6 -8
- package/app/api/tasks/[id]/plan/route.ts +35 -0
- package/app/api/tasks/[id]/plan-edits/route.ts +6 -5
- package/app/api/tasks/[id]/priority/route.ts +4 -1
- package/app/api/tasks/[id]/resume/route.ts +3 -4
- package/app/api/tasks/[id]/stop/route.ts +2 -2
- package/app/api/tasks/route.ts +5 -2
- package/app/globals.css +28 -0
- package/components/Chat/ChatPanel.tsx +2 -11
- package/components/Chat/EventTurns.tsx +1 -7
- package/components/Chat/Markdown.tsx +1 -0
- package/components/Chat/Messages.tsx +1 -1
- package/lib/approvals/enable.ts +9 -2
- package/lib/away/report.ts +4 -4
- package/lib/away/run.ts +2 -2
- package/lib/bookmarks/store.ts +3 -3
- package/lib/bus/held-reply.ts +2 -2
- package/lib/bus/index.ts +3 -3
- package/lib/chat/compact-context.ts +1 -1
- package/lib/chat/events.ts +4 -0
- package/lib/chat/group.ts +3 -1
- package/lib/chat/hold.ts +3 -3
- package/lib/chat/reading-order.ts +50 -0
- package/lib/chat/refresh.ts +1 -1
- package/lib/chat/registry.ts +3 -3
- package/lib/chat/runner.ts +1 -1
- package/lib/chat/turns.ts +22 -3
- package/lib/chat/worker/host.ts +3 -0
- package/lib/chat/worker/main.ts +1 -1
- package/lib/db/index.ts +5 -1
- package/lib/db/lumifyhub.ts +8 -9
- package/lib/db/machine-scope.ts +12 -0
- package/lib/db/migrations/20261015T1600_plan_waits_for_caller.ts +19 -0
- package/lib/db/migrations/20261015T1800_split_sessions_tables.ts +172 -0
- package/lib/db/migrations/README.md +7 -0
- package/lib/db/queries.ts +16 -31
- package/lib/db/session-parts/columns.ts +186 -0
- package/lib/db/session-parts/fill.ts +33 -0
- package/lib/db/session-parts/index.ts +9 -0
- package/lib/db/session-parts/pauses.ts +11 -0
- package/lib/db/session-parts/placement.ts +11 -0
- package/lib/db/session-parts/plans.ts +31 -0
- package/lib/db/session-parts/prs.ts +31 -0
- package/lib/db/session-parts/tasks.ts +27 -0
- package/lib/db/session-parts/view.ts +43 -0
- package/lib/db/session-parts/write.ts +69 -0
- package/lib/db/types.ts +6 -1
- package/lib/done/archive.ts +2 -2
- package/lib/done/auto-archive.ts +1 -1
- package/lib/done/bulk.ts +1 -1
- package/lib/done/index.ts +8 -14
- package/lib/env-setup.ts +17 -4
- package/lib/health/evidence.ts +1 -1
- package/lib/health/facts.ts +1 -1
- package/lib/health/pieces.ts +1 -1
- package/lib/hosts/peer-actions.ts +2 -2
- package/lib/hosts/peer-mirror.ts +2 -2
- package/lib/hosts/peer-sync.ts +69 -32
- package/lib/learning-cards/input.ts +1 -1
- package/lib/learning-cards/store.ts +1 -1
- package/lib/models/plan-step.ts +83 -46
- package/lib/models/resume.ts +5 -7
- package/lib/notify/index.ts +1 -1
- package/lib/notify/push/events.ts +1 -1
- package/lib/notify/push/index.ts +1 -1
- package/lib/orchestrator/act.ts +1 -1
- package/lib/orchestrator/armed.ts +17 -15
- package/lib/orchestrator/ask-approvals.ts +1 -1
- package/lib/orchestrator/ask-heads.ts +1 -1
- package/lib/orchestrator/ask-settle.ts +1 -1
- package/lib/orchestrator/brakes.ts +1 -1
- package/lib/orchestrator/digest.ts +2 -2
- package/lib/orchestrator/external-pr.ts +2 -2
- package/lib/orchestrator/facts.ts +4 -28
- package/lib/orchestrator/held-asks.ts +3 -3
- package/lib/orchestrator/home.ts +2 -2
- package/lib/orchestrator/overview.ts +1 -1
- package/lib/orchestrator/pr-wake.ts +3 -5
- package/lib/orchestrator/questions.ts +1 -1
- package/lib/orchestrator/quick-store.ts +1 -1
- package/lib/orchestrator/quick.ts +10 -7
- package/lib/orchestrator/read.ts +1 -1
- package/lib/orchestrator/rebase.ts +8 -11
- package/lib/orchestrator/reconcile.ts +2 -2
- package/lib/orchestrator/signoff.ts +3 -1
- package/lib/orchestrator/targets.ts +1 -1
- package/lib/orchestrator/task-state.ts +4 -2
- package/lib/plans/features.ts +1 -1
- package/lib/plans/release.ts +1 -1
- package/lib/ports.ts +1 -1
- package/lib/resurrect/find.ts +1 -1
- package/lib/resurrect/index.ts +24 -18
- package/lib/schedules/store.ts +1 -1
- package/lib/session-audit/sources.ts +3 -3
- package/lib/session-logs/index.ts +1 -1
- package/lib/session-titles.ts +1 -1
- package/lib/sessions/setup-progress.ts +1 -1
- package/lib/sessions/stop.ts +1 -1
- package/lib/sessions/worktree-setup.ts +2 -1
- package/lib/simulator/index.ts +1 -1
- package/lib/simulator/task-devices.ts +1 -1
- package/lib/stacks/board.ts +1 -1
- package/lib/stacks/land.ts +1 -1
- package/lib/stacks/restack.ts +1 -1
- package/lib/stacks/start.ts +2 -2
- package/lib/stacks/tick.ts +1 -1
- package/lib/stacks/view.ts +1 -1
- package/lib/status/incremental.ts +1 -1
- package/lib/tasks/contract/actions.ts +35 -0
- package/lib/tasks/contract/authorize.ts +82 -0
- package/lib/tasks/contract/client.ts +296 -0
- package/lib/tasks/{held-remote.ts → contract/held-calls.ts} +35 -66
- package/lib/tasks/contract/index.ts +28 -0
- package/lib/tasks/contract/plan-path.ts +7 -0
- package/lib/tasks/contract/plan.ts +274 -0
- package/lib/tasks/contract/routes.ts +26 -0
- package/lib/tasks/contract/start.ts +122 -0
- package/lib/tasks/contract/sync.ts +126 -0
- package/lib/tasks/contract/types.ts +41 -0
- package/lib/tasks/{remote-views.ts → contract/views.ts} +11 -18
- package/lib/tasks/finish.ts +10 -9
- package/lib/tasks/held-replies.ts +7 -13
- package/lib/tasks/held.ts +36 -17
- package/lib/tasks/hold-checks.ts +134 -0
- package/lib/tasks/holds.ts +281 -0
- package/lib/tasks/import.ts +26 -14
- package/lib/tasks/index.ts +56 -22
- package/lib/tasks/launch-gate.ts +1 -1
- package/lib/tasks/move-bundle.ts +17 -2
- package/lib/tasks/move-carry.ts +15 -7
- package/lib/tasks/move-flow.ts +29 -36
- package/lib/tasks/move-guard.ts +10 -2
- package/lib/tasks/move-recover.ts +6 -25
- package/lib/tasks/move.ts +28 -27
- package/lib/tasks/moved-away.ts +1 -1
- package/lib/tasks/pause-hold.ts +13 -5
- package/lib/tasks/pause.ts +21 -54
- package/lib/tasks/placement.ts +16 -16
- package/lib/tasks/plan-view.ts +11 -3
- package/lib/tasks/queue-candidates.ts +151 -0
- package/lib/tasks/queue-placement.ts +155 -0
- package/lib/tasks/queue-rows.ts +236 -0
- package/lib/tasks/queue-start.ts +213 -0
- package/lib/tasks/queue-tick.ts +148 -0
- package/lib/tasks/queue-types.ts +141 -0
- package/lib/tasks/queue-view.ts +112 -0
- package/lib/tasks/queue.ts +67 -1102
- package/lib/tasks/schedule.ts +44 -147
- package/lib/tasks/session.ts +7 -4
- package/lib/tasks/start.ts +4 -4
- package/lib/tasks/starter.ts +8 -11
- package/lib/usage/calls.ts +1 -1
- package/lib/usage/compare.ts +1 -1
- package/lib/usage/quality.ts +1 -1
- package/lib/usage/turns.ts +1 -1
- package/lib/voice/vocab.ts +1 -1
- package/package.json +1 -1
- package/scripts/backfill-session-logs.ts +1 -1
- package/scripts/check +55 -7
- package/lib/tasks/auto-hold.ts +0 -17
- package/lib/tasks/held-routes.ts +0 -33
- package/lib/tasks/remote.ts +0 -238
package/lib/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/env-setup.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import * as fs from "fs";
|
|
10
10
|
import * as path from "path";
|
|
11
|
+
import { gitOk } from "./git-exec";
|
|
11
12
|
import { bringDependencies, runCommand } from "./worktree-deps";
|
|
12
13
|
import { inside, realExisting } from "./worktree-deps/paths";
|
|
13
14
|
import { seedBuildCaches } from "./worktree-deps/caches";
|
|
@@ -40,16 +41,20 @@ export interface SetupResult {
|
|
|
40
41
|
durationMs: number;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
// A gitignored CLAUDE.md holds the project's private notes. Only one the
|
|
45
|
+
// worktree confirms is ignored is copied: any other would be committable.
|
|
46
|
+
const PRIVATE_NOTES = "CLAUDE.md";
|
|
47
|
+
|
|
43
48
|
/**
|
|
44
|
-
* Find env files to copy (excludes .env.example)
|
|
49
|
+
* Find env files to copy (excludes .env.example), and the private notes
|
|
45
50
|
*/
|
|
46
51
|
export function findEnvFiles(projectPath: string): string[] {
|
|
47
52
|
try {
|
|
48
53
|
const files = fs.readdirSync(projectPath);
|
|
49
54
|
return files.filter(
|
|
50
55
|
(f) =>
|
|
51
|
-
f.startsWith(".env") &&
|
|
52
|
-
|
|
56
|
+
((f.startsWith(".env") && !f.endsWith(".example")) ||
|
|
57
|
+
f === PRIVATE_NOTES) &&
|
|
53
58
|
fs.statSync(path.join(projectPath, f)).isFile()
|
|
54
59
|
);
|
|
55
60
|
} catch {
|
|
@@ -58,7 +63,7 @@ export function findEnvFiles(projectPath: string): string[] {
|
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
/**
|
|
61
|
-
* Copy env files from source to worktree
|
|
66
|
+
* Copy env files (and the private CLAUDE.md) from source to worktree
|
|
62
67
|
*/
|
|
63
68
|
export async function copyEnvFiles(
|
|
64
69
|
sourcePath: string,
|
|
@@ -71,6 +76,14 @@ export async function copyEnvFiles(
|
|
|
71
76
|
try {
|
|
72
77
|
const src = path.join(sourcePath, file);
|
|
73
78
|
const dest = path.join(worktreePath, file);
|
|
79
|
+
const there = await fs.promises.lstat(dest).catch(() => null);
|
|
80
|
+
if (file === PRIVATE_NOTES) {
|
|
81
|
+
if (there || !(await gitOk(worktreePath, ["check-ignore", "-q", file])))
|
|
82
|
+
continue;
|
|
83
|
+
} else if (there?.isSymbolicLink()) {
|
|
84
|
+
// Never written through: a committed link could point outside.
|
|
85
|
+
await fs.promises.unlink(dest);
|
|
86
|
+
}
|
|
74
87
|
await fs.promises.copyFile(src, dest);
|
|
75
88
|
copied.push(file);
|
|
76
89
|
} catch (error) {
|
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
|
@@ -24,6 +24,7 @@ import { addNote } from "../orchestrator/notes";
|
|
|
24
24
|
import { amendBrief, scopeChangesOf } from "../orchestrator/amendments";
|
|
25
25
|
import { PIECES_PROMPT, foldedBrief, pieceBrief } from "../plans/pieces";
|
|
26
26
|
import { orchestratorOf } from "../tasks/starter";
|
|
27
|
+
import { isRemoteHost } from "../hosts";
|
|
27
28
|
import { refreshBase } from "../tasks/refresh-base";
|
|
28
29
|
import {
|
|
29
30
|
createFeature,
|
|
@@ -32,6 +33,7 @@ import {
|
|
|
32
33
|
planPathOf,
|
|
33
34
|
splitOf,
|
|
34
35
|
} from "../plans/features";
|
|
36
|
+
import { advancePlan, setPlan } from "../db/session-parts";
|
|
35
37
|
|
|
36
38
|
export const PLAN_FILE = ".agentos/plan.md";
|
|
37
39
|
|
|
@@ -121,8 +123,35 @@ function piecesLine(r: NonNullable<ReturnType<typeof planFor>>): string {
|
|
|
121
123
|
return `${r.pieces.length} pieces${folded}: ${parts.join("; ")}`;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
// The "plan written" line for a task: where the plan is, what comes next,
|
|
127
|
+
// its first lines and how it split. `file` is where this machine can read it.
|
|
128
|
+
export function planNoteFor(
|
|
129
|
+
s: Session,
|
|
130
|
+
file: string,
|
|
131
|
+
next: string,
|
|
132
|
+
again = "",
|
|
133
|
+
after = ""
|
|
134
|
+
): string {
|
|
135
|
+
let head = `Task "${s.name}": no plan file was written${again}; ${next}${after}.`;
|
|
136
|
+
try {
|
|
137
|
+
const text = fs.readFileSync(file, "utf8");
|
|
138
|
+
const summary = planSummary(text);
|
|
139
|
+
head = `Task "${s.name}": plan written to ${file}; ${next}.`;
|
|
140
|
+
if (summary.length) head += `\n${summary.map((l) => `- ${l}`).join("\n")}`;
|
|
141
|
+
const pieces = planFor(s, text);
|
|
142
|
+
if (pieces && pieces.pieces.length > 1) head += `\n${piecesLine(pieces)}`;
|
|
143
|
+
if (pieces?.folded.length)
|
|
144
|
+
head += `\nPlan listed ${pieces.listed} pieces, building ${pieces.pieces.length}: ${pieces.folded.map((f) => `${f.name} folded (${f.why})`).join("; ")}`;
|
|
145
|
+
} catch {
|
|
146
|
+
// No plan file: the line says so.
|
|
147
|
+
}
|
|
148
|
+
return head;
|
|
149
|
+
}
|
|
150
|
+
|
|
124
151
|
// Tells the orchestrator that started a task its plan is written, once.
|
|
125
152
|
function announcePlan(s: Session, ref: string | null, retried = false): void {
|
|
153
|
+
// The machine that placed it announces the plan, from its own copy.
|
|
154
|
+
if (s.plan_waits_for_caller) return;
|
|
126
155
|
if (!orchestratorOf(s.started_by) && !s.held_by_device) return;
|
|
127
156
|
const workspaceId = sessionWorkspace(s);
|
|
128
157
|
// A task held for another machine's orchestrator can't reach it: the line
|
|
@@ -134,23 +163,9 @@ function announcePlan(s: Session, ref: string | null, retried = false): void {
|
|
|
134
163
|
? `implementing on ${ref}`
|
|
135
164
|
: "waiting for approval, not implementing (approve_plan to go ahead, drop to reject)";
|
|
136
165
|
const again = retried ? " (asked once more for it first)" : "";
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
const summary = planSummary(fs.readFileSync(file, "utf8"));
|
|
140
|
-
head = `Task "${s.name}": plan written to ${file}; ${next}.`;
|
|
141
|
-
if (summary.length) head += `\n${summary.map((l) => `- ${l}`).join("\n")}`;
|
|
142
|
-
const pieces = planFor(s, fs.readFileSync(file, "utf8"));
|
|
143
|
-
if (pieces && pieces.pieces.length > 1) head += `\n${piecesLine(pieces)}`;
|
|
144
|
-
if (pieces?.folded.length)
|
|
145
|
-
head += `\nPlan listed ${pieces.listed} pieces, building ${pieces.pieces.length}: ${pieces.folded.map((f) => `${f.name} folded (${f.why})`).join("; ")}`;
|
|
146
|
-
} catch {
|
|
147
|
-
// No plan file: the line says so.
|
|
148
|
-
}
|
|
166
|
+
const head = planNoteFor(s, file, next, again, ref ? " anyway" : "");
|
|
149
167
|
if (s.held_by_device) {
|
|
150
|
-
db.
|
|
151
|
-
head,
|
|
152
|
-
s.id
|
|
153
|
-
);
|
|
168
|
+
setPlan(db, s.id, { plan_note: head });
|
|
154
169
|
return;
|
|
155
170
|
}
|
|
156
171
|
queueEvent(workspaceId!, `plan-done:${s.id}`, s.id, head);
|
|
@@ -210,9 +225,9 @@ export const implementMessageId = (sessionId: string) =>
|
|
|
210
225
|
type Send = (sessionId: string, id: string) => Promise<void>;
|
|
211
226
|
|
|
212
227
|
function getSession(sessionId: string): Session | undefined {
|
|
213
|
-
return db
|
|
214
|
-
|
|
215
|
-
| undefined;
|
|
228
|
+
return db
|
|
229
|
+
.prepare(`SELECT * FROM session_rows WHERE id = ?`)
|
|
230
|
+
.get(sessionId) as Session | undefined;
|
|
216
231
|
}
|
|
217
232
|
|
|
218
233
|
// Planning (or, once approved, awaiting) becomes implementing, once: the
|
|
@@ -244,7 +259,10 @@ async function startImplementing(
|
|
|
244
259
|
? undefined
|
|
245
260
|
: fresh.note &&
|
|
246
261
|
`Note: ${fresh.note}. Rebase onto the latest base before you open the PR.`;
|
|
247
|
-
|
|
262
|
+
// A plan another machine placed here was checked there, against its
|
|
263
|
+
// projects; this one's workspace doesn't know them.
|
|
264
|
+
let split =
|
|
265
|
+
planText && !s.plan_waits_for_caller ? splitOf(s, planText) : null;
|
|
248
266
|
const workspaceId = sessionWorkspace(s);
|
|
249
267
|
// Approved despite pieces that couldn't be registered: built as one piece,
|
|
250
268
|
// and told so, rather than guessing which repository each belongs to.
|
|
@@ -253,13 +271,7 @@ async function startImplementing(
|
|
|
253
271
|
let nothingOwn = false;
|
|
254
272
|
let featureId: string | null = null;
|
|
255
273
|
const claimed = db.transaction(() => {
|
|
256
|
-
|
|
257
|
-
.prepare(
|
|
258
|
-
`UPDATE sessions SET plan_phase = 'implementing'
|
|
259
|
-
WHERE id = ? AND plan_phase = ? AND task_status = 'running'`
|
|
260
|
-
)
|
|
261
|
-
.run(sessionId, from).changes;
|
|
262
|
-
if (!changed) return false;
|
|
274
|
+
if (!advancePlan(db, sessionId, from, "implementing")) return false;
|
|
263
275
|
setSessionModel(sessionId, routed);
|
|
264
276
|
let piece: string | undefined;
|
|
265
277
|
let note = amendment;
|
|
@@ -441,9 +453,12 @@ export async function handoffAfterPlan(
|
|
|
441
453
|
forget(sessionId);
|
|
442
454
|
// No approvals mode doesn't stop at the plan to wait for one. Pieces that
|
|
443
455
|
// couldn't be registered always do: building on a guess is the bug.
|
|
444
|
-
const auto = !!s.plan_only && noApprovalsOn();
|
|
456
|
+
const auto = !!s.plan_only && noApprovalsOn() && !s.plan_waits_for_caller;
|
|
445
457
|
const planText = readPlan(s);
|
|
446
|
-
const pieceErrors =
|
|
458
|
+
const pieceErrors =
|
|
459
|
+
(planText && !s.plan_waits_for_caller
|
|
460
|
+
? splitOf(s, planText)?.errors
|
|
461
|
+
: null) ?? [];
|
|
447
462
|
if ((!s.plan_only || auto) && !pieceErrors.length) {
|
|
448
463
|
const ref = await startImplementing(
|
|
449
464
|
s,
|
|
@@ -462,13 +477,7 @@ export async function handoffAfterPlan(
|
|
|
462
477
|
});
|
|
463
478
|
return ref !== null;
|
|
464
479
|
}
|
|
465
|
-
|
|
466
|
-
.prepare(
|
|
467
|
-
`UPDATE sessions SET plan_phase = 'awaiting'
|
|
468
|
-
WHERE id = ? AND plan_phase = 'planning' AND task_status = 'running'`
|
|
469
|
-
)
|
|
470
|
-
.run(sessionId).changes;
|
|
471
|
-
if (!awaiting) return false;
|
|
480
|
+
if (!advancePlan(db, sessionId, "planning", "awaiting")) return false;
|
|
472
481
|
try {
|
|
473
482
|
announcePlan(s, null, retried);
|
|
474
483
|
} catch (e) {
|
|
@@ -482,14 +491,17 @@ export async function handoffAfterPlan(
|
|
|
482
491
|
s.id,
|
|
483
492
|
`Plan of "${s.name}": couldn't register its pieces: ${pieceErrors.join("; ")}. It is held, not building. Message the task to fix the plan, or approve_plan to build it as one piece.`
|
|
484
493
|
);
|
|
494
|
+
// The machine that placed a plan sends its own push.
|
|
485
495
|
try {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
496
|
+
if (!s.plan_waits_for_caller) {
|
|
497
|
+
const text = readPlan(s);
|
|
498
|
+
const planned = text ? planFor(s, text) : null;
|
|
499
|
+
(await import("../notify/push/events")).pushPlanAwaiting(
|
|
500
|
+
s.id,
|
|
501
|
+
s.name,
|
|
502
|
+
planned?.pieces.length ?? 1
|
|
503
|
+
);
|
|
504
|
+
}
|
|
493
505
|
} catch (e) {
|
|
494
506
|
console.error("plan push failed:", e);
|
|
495
507
|
}
|
|
@@ -512,7 +524,7 @@ export async function recheckPlanning(
|
|
|
512
524
|
): Promise<void> {
|
|
513
525
|
const rows = db
|
|
514
526
|
.prepare(
|
|
515
|
-
`SELECT id FROM
|
|
527
|
+
`SELECT id FROM session_rows
|
|
516
528
|
WHERE plan_phase = 'planning' AND task_status = 'running'
|
|
517
529
|
AND archived_at IS NULL AND worktree_path IS NOT NULL`
|
|
518
530
|
)
|
|
@@ -560,9 +572,12 @@ export async function approvePlan(
|
|
|
560
572
|
);
|
|
561
573
|
const text = message?.trim() || undefined;
|
|
562
574
|
const workspaceId = sessionWorkspace(s);
|
|
575
|
+
// A plan another machine placed here is approved by that machine, which
|
|
576
|
+
// keeps the brief its review reads.
|
|
577
|
+
const placedHere = !!s.plan_waits_for_caller && !isRemoteHost(s.host_id);
|
|
563
578
|
// Every approval is on the brief, so the review judges the PR as the
|
|
564
579
|
// implementation of the plan, not as plan-only work.
|
|
565
|
-
if (workspaceId) {
|
|
580
|
+
if (workspaceId && !placedHere) {
|
|
566
581
|
if (text) {
|
|
567
582
|
amendBrief(workspaceId, s.id, text);
|
|
568
583
|
addNote(
|
|
@@ -571,7 +586,11 @@ export async function approvePlan(
|
|
|
571
586
|
);
|
|
572
587
|
} else amendBrief(workspaceId, s.id, APPROVAL_TEXT[by], "approval");
|
|
573
588
|
}
|
|
574
|
-
|
|
589
|
+
// On another machine, the plan was written there: this machine decides
|
|
590
|
+
// where it is built, and the approval follows it.
|
|
591
|
+
const ref = isRemoteHost(s.host_id)
|
|
592
|
+
? await (await import("../tasks/contract/plan")).continuePlanThere(s, text)
|
|
593
|
+
: await startImplementing(s, "awaiting", send, text);
|
|
575
594
|
if (!ref) throw new Error(`${s.name} isn't waiting for plan approval`);
|
|
576
595
|
if (by === "no-approvals")
|
|
577
596
|
recordAutoApproval({
|
|
@@ -583,3 +602,21 @@ export async function approvePlan(
|
|
|
583
602
|
});
|
|
584
603
|
return { ref, amendments: scopeChangesOf(s.id).length };
|
|
585
604
|
}
|
|
605
|
+
|
|
606
|
+
// A plan that moved here to be built (contract/plan.ts): implementation starts
|
|
607
|
+
// on this machine, once, with the approval's change if it had one.
|
|
608
|
+
export async function implementArrived(
|
|
609
|
+
sessionId: string,
|
|
610
|
+
message?: string
|
|
611
|
+
): Promise<string | null> {
|
|
612
|
+
const s = getSession(sessionId);
|
|
613
|
+
if (!s || s.plan_phase !== "awaiting" || s.task_status !== "running")
|
|
614
|
+
throw new Error(`${s?.name ?? "That task"} isn't waiting for a plan`);
|
|
615
|
+
const { sendQueuedNow } = await import("../chat/runner");
|
|
616
|
+
return startImplementing(
|
|
617
|
+
s,
|
|
618
|
+
"awaiting",
|
|
619
|
+
(id, m) => sendQueuedNow(id, m),
|
|
620
|
+
message
|
|
621
|
+
);
|
|
622
|
+
}
|
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
|
{
|
package/lib/notify/push/index.ts
CHANGED
|
@@ -57,7 +57,7 @@ export interface PushOptions {
|
|
|
57
57
|
|
|
58
58
|
function isMirrored(sessionId: string): boolean {
|
|
59
59
|
const s = db
|
|
60
|
-
.prepare(`SELECT host_id, peer_mirror FROM
|
|
60
|
+
.prepare(`SELECT host_id, peer_mirror FROM session_rows WHERE id = ?`)
|
|
61
61
|
.get(sessionId) as
|
|
62
62
|
| { host_id: string | null; peer_mirror: number | null }
|
|
63
63
|
| undefined;
|
package/lib/orchestrator/act.ts
CHANGED
|
@@ -439,7 +439,7 @@ export async function resurrect(
|
|
|
439
439
|
if (!reason.trim()) throw new Error("Say why it's restored");
|
|
440
440
|
const id = resolveEnded(ref, { workspaceId });
|
|
441
441
|
const row = db
|
|
442
|
-
.prepare(`SELECT task_status, archived_at FROM
|
|
442
|
+
.prepare(`SELECT task_status, archived_at FROM session_rows WHERE id = ?`)
|
|
443
443
|
.get(id) as { task_status: string | null; archived_at: string | null };
|
|
444
444
|
const task =
|
|
445
445
|
row.task_status === "dropped" ||
|