@saadnvd1/agent-os 0.3.596 → 0.3.600
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/spawn/route.ts +35 -16
- package/app/api/lumifyhub/cards/[cardId]/run/route.ts +3 -0
- package/app/api/tasks/route.ts +3 -0
- package/app/page.tsx +2 -1
- package/components/SessionList/SessionRow.tsx +2 -1
- package/lib/agents/spawn.ts +16 -4
- package/lib/chat/refresh.ts +2 -1
- package/lib/chat/runner.ts +29 -0
- package/lib/chat/store.ts +53 -0
- package/lib/db/migration-types.ts +15 -0
- package/lib/db/migrations/0001_add_group_path_to_sessions.ts +11 -0
- package/lib/db/migrations/0002_add_agent_type_to_sessions.ts +11 -0
- package/lib/db/migrations/0003_add_worktree_columns_to_sessions.ts +12 -0
- package/lib/db/migrations/0004_add_pr_tracking_to_sessions.ts +11 -0
- package/lib/db/migrations/0005_add_group_path_index.ts +11 -0
- package/lib/db/migrations/0006_add_orchestration_columns_to_sessions.ts +16 -0
- package/lib/db/migrations/0007_add_auto_approve_to_sessions.ts +11 -0
- package/lib/db/migrations/0008_add_dev_server_columns.ts +19 -0
- package/lib/db/migrations/0009_add_project_id_to_sessions.ts +17 -0
- package/lib/db/migrations/0010_add_project_id_to_dev_servers.ts +36 -0
- package/lib/db/migrations/0011_add_tmux_name_to_sessions.ts +13 -0
- package/lib/db/migrations/0012_add_initial_prompt_to_projects.ts +9 -0
- package/lib/db/migrations/0013_add_project_repositories_table.ts +22 -0
- package/lib/db/migrations/0014_add_hosts.ts +23 -0
- package/lib/db/migrations/0015_add_workspaces.ts +18 -0
- package/lib/db/migrations/0016_add_task_columns_to_sessions.ts +13 -0
- package/lib/db/migrations/0017_add_bus_messages.ts +24 -0
- package/lib/db/migrations/0018_add_chat.ts +23 -0
- package/lib/db/migrations/0019_add_lumifyhub.ts +36 -0
- package/lib/db/migrations/0020_add_lumifyhub_published_docs.ts +18 -0
- package/lib/db/migrations/0021_add_chat_access.ts +14 -0
- package/lib/db/migrations/0022_add_last_seen.ts +10 -0
- package/lib/db/migrations/0023_mark_existing_sessions_seen.ts +12 -0
- package/lib/db/migrations/0024_add_stacks.ts +49 -0
- package/lib/db/migrations/0025_add_stack_item_attempts.ts +16 -0
- package/lib/db/migrations/0026_add_workspace_orchestrator.ts +47 -0
- package/lib/db/migrations/0027_add_devices_and_settings.ts +28 -0
- package/lib/db/migrations/0028_add_orchestrator_acting.ts +70 -0
- package/lib/db/migrations/0029_add_orchestrator_asks_and_pause.ts +81 -0
- package/lib/db/migrations/0030_add_sessions_archived_at.ts +14 -0
- package/lib/db/migrations/0031_add_stack_item_restacked_heads.ts +22 -0
- package/lib/db/migrations/0032_add_sessions_pinned.ts +12 -0
- package/lib/db/migrations/0033_add_program_status.ts +18 -0
- package/lib/db/migrations/0034_add_session_names.ts +21 -0
- package/lib/db/migrations/0035_add_artifacts.ts +23 -0
- package/lib/db/migrations/0036_add_chat_queue.ts +26 -0
- package/lib/db/migrations/0037_add_chat_plan_and_turns.ts +39 -0
- package/lib/db/migrations/0038_add_schedules.ts +56 -0
- package/lib/db/migrations/0039_add_session_name_source.ts +21 -0
- package/lib/db/migrations/0040_add_schedule_targets_and_phone_notify.ts +41 -0
- package/lib/db/migrations/0041_add_task_setup.ts +17 -0
- package/lib/db/migrations/0042_add_host_links_and_moved_tasks.ts +29 -0
- package/lib/db/migrations/0043_scratch_project.ts +14 -0
- package/lib/db/migrations/0044_chat_item_kind.ts +21 -0
- package/lib/db/migrations/0045_change_versions.ts +13 -0
- package/lib/db/migrations/0046_session_port_slots.ts +21 -0
- package/lib/db/migrations/0047_pin_orchestrators.ts +12 -0
- package/lib/db/migrations/0048_session_database.ts +15 -0
- package/lib/db/migrations/0049_peer_mirror.ts +14 -0
- package/lib/db/migrations/0050_task_queue.ts +44 -0
- package/lib/db/migrations/0051_chat_queue_sent_by.ts +16 -0
- package/lib/db/migrations/0052_sessions_chat_restarted.ts +17 -0
- package/lib/db/migrations/0053_brief_amendments_failure_sha_ask_raised_by.ts +37 -0
- package/lib/db/migrations/0054_sessions_chat_tools_digest.ts +15 -0
- package/lib/db/migrations/0055_sessions_merge_armed_and_rebase.ts +25 -0
- package/lib/db/migrations/0056_orchestrator_rules.ts +39 -0
- package/lib/db/migrations/0057_task_pr_wakes.ts +34 -0
- package/lib/db/migrations/0058_away_mode.ts +71 -0
- package/lib/db/migrations/0059_gate_failure_commits.ts +33 -0
- package/lib/db/migrations/0060_check_runs_and_compactions.ts +44 -0
- package/lib/db/migrations/0061_model_routing.ts +30 -0
- package/lib/db/migrations/0062_sessions_quick_of.ts +19 -0
- package/lib/db/migrations/0063_one_off_schedules.ts +16 -0
- package/lib/db/migrations/0064_chat_compactions_phase.ts +15 -0
- package/lib/db/migrations/0065_orchestrator_triage.ts +23 -0
- package/lib/db/migrations/0066_workspace_orch_mcp_servers.ts +16 -0
- package/lib/db/migrations/0067_orchestrator_event_log_suppressed.ts +14 -0
- package/lib/db/migrations/0068_usage_calls_and_rollups.ts +74 -0
- package/lib/db/migrations/0069_session_logs.ts +26 -0
- package/lib/db/migrations/0070_chat_items_running.ts +15 -0
- package/lib/db/migrations/0071_devices_push.ts +29 -0
- package/lib/db/migrations/0072_push_outbox.ts +33 -0
- package/lib/db/migrations/0073_orchestrated_task_moves.ts +27 -0
- package/lib/db/migrations/0074_held_task_replies.ts +27 -0
- package/lib/db/migrations/0075_plan_only_tasks.ts +17 -0
- package/lib/db/migrations/0076_welcome_backfill.ts +16 -0
- package/lib/db/migrations/0077_simulator_leases.ts +21 -0
- package/lib/db/migrations/0078_orchestrator_questions.ts +34 -0
- package/lib/db/migrations/0079_task_queue_agent.ts +13 -0
- package/lib/db/migrations/0080_recipes.ts +129 -0
- package/lib/db/migrations/0081_schedules_into_recipes.ts +18 -0
- package/lib/db/migrations/0082_session_shelf_times.ts +23 -0
- package/lib/db/migrations/0083_native_devices_can_approve.ts +18 -0
- package/lib/db/migrations/0084_approval_audit.ts +20 -0
- package/lib/db/migrations/0085_experiment_usage.ts +35 -0
- package/lib/db/migrations/0086_learning_cards.ts +28 -0
- package/lib/db/migrations/0087_plan_pieces_reconcile.ts +29 -0
- package/lib/db/migrations/0088_shared_links.ts +25 -0
- package/lib/db/migrations/0089_passkeys_device_keys.ts +23 -0
- package/lib/db/migrations/20261014T0900_task_scheduling.ts +57 -0
- package/lib/db/migrations/20261014T1235_placed_mirror_started_by.ts +19 -0
- package/lib/db/migrations/20261014T1600_queued_task_messages.ts +21 -0
- package/lib/db/migrations/20261014T1800_task_starter_device.ts +20 -0
- package/lib/db/migrations/20261014T1900_pause_note.ts +13 -0
- package/lib/db/migrations/20261014T2000_chat_bookmarks.ts +33 -0
- package/lib/db/migrations/20261014T2100_gate_failure_ask_held.ts +18 -0
- package/lib/db/migrations/20261014T2110_task_start_record.ts +21 -0
- package/lib/db/migrations/20261014T2200_end_orphaned_tasks.ts +23 -0
- package/lib/db/migrations/README.md +35 -0
- package/lib/db/migrations-legacy-checksums.json +9 -8
- package/lib/db/migrations-split-snapshot.json +1759 -0
- package/lib/db/migrations.ts +76 -2337
- package/lib/db/types.ts +2 -0
- package/lib/learning-cards/apply.ts +7 -1
- package/lib/models/plan-step.ts +25 -8
- package/lib/models/resolve.ts +1 -1
- package/lib/models/resume.ts +1 -1
- package/lib/models/roles.ts +0 -16
- package/lib/models/session-role.ts +19 -0
- package/lib/orchestrator/act.ts +22 -7
- package/lib/orchestrator/amendments.ts +10 -3
- package/lib/orchestrator/serve.ts +1 -0
- package/lib/orchestrator/testing.ts +8 -0
- package/lib/orchestrator/tool-schemas.ts +9 -0
- package/lib/orchestrator/tools.ts +1 -1
- package/lib/plans/claim.ts +131 -0
- package/lib/plans/release.ts +5 -2
- package/lib/recipes/run.ts +6 -0
- package/lib/schedules/start.ts +1 -0
- package/lib/sessions/fork.ts +10 -0
- package/lib/sessions/launch.ts +5 -1
- package/lib/stacks/start.ts +1 -0
- package/lib/tasks/held-remote.ts +4 -2
- package/lib/tasks/held-replies.ts +5 -2
- package/lib/tasks/held.ts +2 -1
- package/lib/tasks/index.ts +85 -46
- package/lib/tasks/move-carry.ts +12 -1
- package/lib/tasks/move-guard.ts +2 -1
- package/lib/tasks/move.ts +7 -5
- package/lib/tasks/pause.ts +1 -1
- package/lib/tasks/queue.ts +54 -31
- package/lib/tasks/remote.ts +3 -1
- package/lib/tasks/starter.ts +124 -0
- package/lib/usage/calls.ts +2 -1
- package/lib/usage/turns.ts +2 -1
- package/package.json +6 -3
- package/scripts/check +1 -0
- package/scripts/gen-migrations-index.mjs +44 -0
- package/scripts/screenshots/demo.ts +5 -1
- package/scripts/screenshots/run.ts +5 -1
|
@@ -27,18 +27,21 @@ interface Pulled {
|
|
|
27
27
|
body: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
// Held means an orchestrator started it, as isHeldRemote reads it.
|
|
31
|
+
const HELD_BY = `started_by IN (SELECT id FROM sessions WHERE role = 'orchestrator')`;
|
|
32
|
+
|
|
30
33
|
const heldMirror = (hostId: string, id: string) =>
|
|
31
34
|
db
|
|
32
35
|
.prepare(
|
|
33
36
|
`SELECT * FROM sessions WHERE id = ? AND host_id = ?
|
|
34
|
-
AND
|
|
37
|
+
AND ${HELD_BY} AND task_status IN ('running', 'moving')`
|
|
35
38
|
)
|
|
36
39
|
.get(id, hostId) as Session | undefined;
|
|
37
40
|
|
|
38
41
|
export function hasHeldMirrors(hostId: string): boolean {
|
|
39
42
|
return !!db
|
|
40
43
|
.prepare(
|
|
41
|
-
`SELECT 1 FROM sessions WHERE host_id = ? AND
|
|
44
|
+
`SELECT 1 FROM sessions WHERE host_id = ? AND ${HELD_BY}
|
|
42
45
|
AND task_prompt IS NOT NULL AND task_status IN ('running', 'moving')`
|
|
43
46
|
)
|
|
44
47
|
.get(hostId);
|
package/lib/tasks/held.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* a green CI, past the gates its orchestrator judges it by.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { orchestratorOf } from "./starter";
|
|
8
9
|
import os from "os";
|
|
9
10
|
import { db, type Session } from "../db";
|
|
10
11
|
import type { OrchestratedBundle } from "./move-bundle";
|
|
@@ -102,7 +103,7 @@ type Holdable = Pick<
|
|
|
102
103
|
export function mayHold(s: Holdable, device: string): boolean {
|
|
103
104
|
if (!s.task_prompt || s.task_status !== "running") return false;
|
|
104
105
|
if (s.host_id && s.host_id !== "local") return false;
|
|
105
|
-
if (s.started_by || s.held_by_device) return false;
|
|
106
|
+
if (orchestratorOf(s.started_by) || s.held_by_device) return false;
|
|
106
107
|
return (
|
|
107
108
|
s.started_by_device === device || s.started_by_device === UNRECORDED_STARTER
|
|
108
109
|
);
|
package/lib/tasks/index.ts
CHANGED
|
@@ -48,7 +48,9 @@ import {
|
|
|
48
48
|
} from "./state";
|
|
49
49
|
import { expandHome, prFor, storedPR, taskSessions } from "./session";
|
|
50
50
|
import { InProgressError, type OrchestratedBundle } from "./move-bundle";
|
|
51
|
-
import { holdFor, mayHold
|
|
51
|
+
import { holdFor, mayHold } from "./held";
|
|
52
|
+
import { orchestratorOf, recordStart, type StartVia } from "./starter";
|
|
53
|
+
import { claimable, claimPiece, releaseClaim } from "../plans/claim";
|
|
52
54
|
import { nameFor } from "../session-titles";
|
|
53
55
|
import { taskSetupOf, type TaskSetup } from "./setup";
|
|
54
56
|
import { taskPlan } from "./plan-view";
|
|
@@ -136,8 +138,13 @@ export async function createTask(opts: {
|
|
|
136
138
|
planReason?: string | null;
|
|
137
139
|
// Stop after the plan and wait for approval (implies plan).
|
|
138
140
|
planOnly?: boolean;
|
|
139
|
-
// The
|
|
141
|
+
// The session that started it (null: a person), the session it came from,
|
|
142
|
+
// and how: every start says (tasks/starter.ts).
|
|
140
143
|
startedBy?: string | null;
|
|
144
|
+
parentSessionId?: string | null;
|
|
145
|
+
startedVia: StartVia;
|
|
146
|
+
// The waiting plan piece it builds: claimed for this task.
|
|
147
|
+
pieceId?: number;
|
|
141
148
|
// Started from this LumifyHub card, which the task then moves.
|
|
142
149
|
cardId?: string;
|
|
143
150
|
// Stacked: cut from another task's pushed branch, at this exact commit.
|
|
@@ -165,7 +172,7 @@ export async function createTask(opts: {
|
|
|
165
172
|
// (held.ts): that machine's paired device and its description of the task.
|
|
166
173
|
held?: { device: string; orchestrated: OrchestratedBundle };
|
|
167
174
|
// The paired device that sent this keyed start: the only one that may hold it later.
|
|
168
|
-
startedByDevice?: string;
|
|
175
|
+
startedByDevice?: string | null;
|
|
169
176
|
}): Promise<Session> {
|
|
170
177
|
if (opts.id) {
|
|
171
178
|
const existing = queries.getSession(db).get(opts.id) as Session | undefined;
|
|
@@ -222,6 +229,7 @@ async function startTask(
|
|
|
222
229
|
if (!prompt) throw new Error("Describe the task");
|
|
223
230
|
const project = getProject(opts.projectId);
|
|
224
231
|
if (!project || project.is_uncategorized) throw new Error("Pick a project");
|
|
232
|
+
if (opts.pieceId) claimable(opts.pieceId);
|
|
225
233
|
const hostId = opts.hostId ?? project.host_id;
|
|
226
234
|
const view = opts.view ?? DEFAULT_START_VIEW;
|
|
227
235
|
const agent = opts.agent ?? "claude";
|
|
@@ -251,6 +259,16 @@ async function startTask(
|
|
|
251
259
|
view,
|
|
252
260
|
startedBy: opts.startedBy,
|
|
253
261
|
});
|
|
262
|
+
// The mirror's started_by stays orchestrator-only: a held-remote task is
|
|
263
|
+
// relayed, stopped and answered through it. A plain calling task is its
|
|
264
|
+
// parent and started_via only.
|
|
265
|
+
recordStart(session.id, {
|
|
266
|
+
startedBy: orchestratorOf(opts.startedBy),
|
|
267
|
+
parentSessionId: opts.parentSessionId ?? null,
|
|
268
|
+
startedVia: opts.startedVia,
|
|
269
|
+
startedByDevice: opts.held?.device ?? opts.startedByDevice,
|
|
270
|
+
});
|
|
271
|
+
if (opts.pieceId) claimPiece(opts.pieceId, session.id);
|
|
254
272
|
opts.onCreated?.(session.id);
|
|
255
273
|
return session;
|
|
256
274
|
}
|
|
@@ -295,50 +313,71 @@ async function startTask(
|
|
|
295
313
|
const model = routing.first.model;
|
|
296
314
|
const tmuxName = `${provider.id}-${id}`;
|
|
297
315
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
316
|
+
// Claimed right before the row exists, with nothing awaited between, so the
|
|
317
|
+
// release sweep can't start the same piece.
|
|
318
|
+
if (opts.pieceId) {
|
|
319
|
+
try {
|
|
320
|
+
claimPiece(opts.pieceId, id);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
await discardLeftoverStart(projectPath, feature).catch(() => {});
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
queries
|
|
328
|
+
.createSession(db)
|
|
329
|
+
.run(
|
|
330
|
+
id,
|
|
331
|
+
naming.name,
|
|
332
|
+
tmuxName,
|
|
333
|
+
wt.worktreePath,
|
|
334
|
+
null,
|
|
335
|
+
model,
|
|
336
|
+
null,
|
|
337
|
+
"sessions",
|
|
338
|
+
agent,
|
|
339
|
+
1,
|
|
340
|
+
project.id,
|
|
341
|
+
"local"
|
|
342
|
+
);
|
|
343
|
+
queries
|
|
344
|
+
.updateSessionWorktree(db)
|
|
345
|
+
.run(wt.worktreePath, wt.branchName, baseBranch, null, id);
|
|
346
|
+
// Everything the launch needs is on the row, so a restart can resume it.
|
|
347
|
+
db.prepare(
|
|
348
|
+
`UPDATE sessions SET task_prompt = ?, task_brief = ?, task_status = 'running', setup_status = 'running', name_source = ?, view = ?,
|
|
349
|
+
model_provider = ?, model_overrides = ?, plan_phase = ?, plan_only = ?,
|
|
350
|
+
plan_decided = ?, plan_reason = ? WHERE id = ?`
|
|
351
|
+
).run(
|
|
352
|
+
prompt,
|
|
353
|
+
buildTaskBrief({
|
|
354
|
+
branch: wt.branchName,
|
|
355
|
+
baseBranch,
|
|
356
|
+
stack: opts.base?.stack,
|
|
357
|
+
}),
|
|
358
|
+
naming.source,
|
|
359
|
+
view,
|
|
360
|
+
routing.first.provider.id === ANTHROPIC
|
|
361
|
+
? null
|
|
362
|
+
: routing.first.provider.id,
|
|
363
|
+
routing.override ? JSON.stringify(routing.override) : null,
|
|
364
|
+
routing.plan ? "planning" : null,
|
|
365
|
+
routing.planOnly ? 1 : null,
|
|
366
|
+
routing.planOnly ? 1 : opts.plan === undefined ? null : opts.plan ? 1 : 0,
|
|
367
|
+
opts.planReason?.trim() || null,
|
|
368
|
+
id
|
|
313
369
|
);
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
branch: wt.branchName,
|
|
326
|
-
baseBranch,
|
|
327
|
-
stack: opts.base?.stack,
|
|
328
|
-
}),
|
|
329
|
-
naming.source,
|
|
330
|
-
view,
|
|
331
|
-
routing.first.provider.id === ANTHROPIC ? null : routing.first.provider.id,
|
|
332
|
-
routing.override ? JSON.stringify(routing.override) : null,
|
|
333
|
-
routing.plan ? "planning" : null,
|
|
334
|
-
routing.planOnly ? 1 : null,
|
|
335
|
-
routing.planOnly ? 1 : opts.plan === undefined ? null : opts.plan ? 1 : 0,
|
|
336
|
-
opts.planReason?.trim() || null,
|
|
337
|
-
opts.startedBy ?? null,
|
|
338
|
-
id
|
|
339
|
-
);
|
|
340
|
-
const starter = opts.held?.device ?? opts.startedByDevice;
|
|
341
|
-
if (starter) recordStarter(id, starter);
|
|
370
|
+
recordStart(id, {
|
|
371
|
+
startedBy: opts.startedBy ?? null,
|
|
372
|
+
parentSessionId: opts.parentSessionId ?? null,
|
|
373
|
+
startedVia: opts.startedVia,
|
|
374
|
+
startedByDevice: opts.held?.device ?? opts.startedByDevice,
|
|
375
|
+
});
|
|
376
|
+
} catch (error) {
|
|
377
|
+
// The claim was made for a session that never existed.
|
|
378
|
+
if (opts.pieceId) releaseClaim(opts.pieceId, id);
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
342
381
|
if (opts.held) holdFor(id, opts.held.device, opts.held.orchestrated);
|
|
343
382
|
opts.onCreated?.(id);
|
|
344
383
|
void naming.refine?.(id);
|
package/lib/tasks/move-carry.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type Database from "better-sqlite3";
|
|
11
11
|
import { db } from "../db";
|
|
12
|
+
import { orchestratorOf } from "./starter";
|
|
12
13
|
|
|
13
14
|
// table, column, and how the id sits in it: the whole value, `task:<id>`, or
|
|
14
15
|
// the tail of a `<condition>:<id>` key.
|
|
@@ -59,9 +60,19 @@ export const CARRIED_COLUMNS = [
|
|
|
59
60
|
] as const;
|
|
60
61
|
|
|
61
62
|
/** The orchestration columns that make a session "an orchestrator's task". */
|
|
63
|
+
// started_by carries as the orchestrator only: a task a plain task started is
|
|
64
|
+
// not held, and a mirror reading it as held would relay to the wrong session.
|
|
62
65
|
export const orchestrationOf = (session: Record<string, unknown>) =>
|
|
63
66
|
Object.fromEntries(
|
|
64
|
-
CARRIED_COLUMNS.map(
|
|
67
|
+
CARRIED_COLUMNS.map(
|
|
68
|
+
(c) =>
|
|
69
|
+
[
|
|
70
|
+
c,
|
|
71
|
+
c === "started_by"
|
|
72
|
+
? orchestratorOf(session.started_by as string | null)
|
|
73
|
+
: (session[c] ?? null),
|
|
74
|
+
] as const
|
|
75
|
+
)
|
|
65
76
|
);
|
|
66
77
|
|
|
67
78
|
export function carryTaskState(
|
package/lib/tasks/move-guard.ts
CHANGED
|
@@ -10,10 +10,11 @@ import { db, type Session } from "../db";
|
|
|
10
10
|
import { escalations } from "../orchestrator/gates";
|
|
11
11
|
import { taskSubject } from "../orchestrator/asks";
|
|
12
12
|
import { usesGitCrypt } from "../worktree-add";
|
|
13
|
+
import { orchestratorOf } from "./starter";
|
|
13
14
|
|
|
14
15
|
/** Started by an orchestrator, or held for one by the machine it left. */
|
|
15
16
|
export function isOrchestrated(session: Session): boolean {
|
|
16
|
-
if (session.started_by || session.held_by_device) return true;
|
|
17
|
+
if (orchestratorOf(session.started_by) || session.held_by_device) return true;
|
|
17
18
|
return !!db
|
|
18
19
|
.prepare(
|
|
19
20
|
`SELECT 1 FROM orchestrator_starts WHERE kind != 'stack' AND target = ? LIMIT 1`
|
package/lib/tasks/move.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { dropSessionDatabase } from "../project-config/database";
|
|
|
30
30
|
import { releaseChat } from "../chat/runner";
|
|
31
31
|
import { collectWhileMoving } from "../chat/hold";
|
|
32
32
|
import { packChat, stopChatForMove } from "./move-chat";
|
|
33
|
+
import { orchestratorOf as starterOrchestrator } from "./starter";
|
|
33
34
|
import {
|
|
34
35
|
isClaudeSessionId,
|
|
35
36
|
latestSessionId,
|
|
@@ -122,11 +123,12 @@ function orchestratorOf(
|
|
|
122
123
|
): { id: string; name: string } | undefined {
|
|
123
124
|
if (session.held_for && session.held_for_name)
|
|
124
125
|
return { id: session.held_for, name: session.held_for_name };
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
126
|
+
const by = starterOrchestrator(session.started_by);
|
|
127
|
+
if (!by) return undefined;
|
|
128
|
+
const o = db.prepare(`SELECT name FROM sessions WHERE id = ?`).get(by) as
|
|
129
|
+
| { name: string }
|
|
130
|
+
| undefined;
|
|
131
|
+
return o ? { id: by, name: o.name } : undefined;
|
|
130
132
|
}
|
|
131
133
|
|
|
132
134
|
// What an orchestrator's task carries besides its work (the orchestrator's
|
package/lib/tasks/pause.ts
CHANGED
|
@@ -28,7 +28,7 @@ import { launchPending } from "./launch-gate";
|
|
|
28
28
|
import { killTmux } from "./start";
|
|
29
29
|
import { launchClaude } from "../agents/launch";
|
|
30
30
|
import { resolveRole, sessionWorkspace, taskOverride } from "../models/resolve";
|
|
31
|
-
import { sessionRole } from "../models/
|
|
31
|
+
import { sessionRole } from "../models/session-role";
|
|
32
32
|
import { holdingEscalations } from "../orchestrator/gates";
|
|
33
33
|
import { openBySubject, workSubject } from "../orchestrator/asks";
|
|
34
34
|
import { queueEventOnce } from "../orchestrator/events";
|
package/lib/tasks/queue.ts
CHANGED
|
@@ -85,6 +85,8 @@ import {
|
|
|
85
85
|
resumeHoldFor,
|
|
86
86
|
setPauseNote,
|
|
87
87
|
} from "./pause";
|
|
88
|
+
import { orchestratorOf, type StartVia } from "./starter";
|
|
89
|
+
import { claimPiece } from "../plans/claim";
|
|
88
90
|
|
|
89
91
|
export type QueueStatus =
|
|
90
92
|
| "queued"
|
|
@@ -107,6 +109,9 @@ export interface QueueRow {
|
|
|
107
109
|
plan_reason: string | null;
|
|
108
110
|
plan_only: number | null;
|
|
109
111
|
started_by: string | null;
|
|
112
|
+
parent_session_id: string | null;
|
|
113
|
+
started_by_device: string | null;
|
|
114
|
+
started_via: string | null;
|
|
110
115
|
view: "chat" | "terminal" | null;
|
|
111
116
|
agent_type: string | null;
|
|
112
117
|
base_branch: string | null;
|
|
@@ -306,6 +311,11 @@ export interface QueueRequest {
|
|
|
306
311
|
planReason?: string | null;
|
|
307
312
|
planOnly?: boolean;
|
|
308
313
|
startedBy?: string | null;
|
|
314
|
+
parentSessionId?: string | null;
|
|
315
|
+
startedByDevice?: string | null;
|
|
316
|
+
startedVia: StartVia;
|
|
317
|
+
// The waiting plan piece it builds, claimed for its queue id.
|
|
318
|
+
pieceId?: number;
|
|
309
319
|
view?: "chat" | "terminal";
|
|
310
320
|
agent?: AgentType;
|
|
311
321
|
baseBranch?: string;
|
|
@@ -412,36 +422,43 @@ export function queueIfNeeded(req: QueueRequest): QueuedTaskView | null {
|
|
|
412
422
|
if (!waits && !full && !loaded) return null;
|
|
413
423
|
if (!req.prompt.trim()) throw new Error("Describe the task");
|
|
414
424
|
const id = randomUUID();
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
425
|
+
// The row and the piece claim stand or fall together.
|
|
426
|
+
db.transaction(() => {
|
|
427
|
+
db.prepare(
|
|
428
|
+
`INSERT INTO task_queue (id, project_id, prompt, name, model, model_overrides, plan, plan_reason, plan_only, started_by, parent_session_id, started_by_device, started_via, view, agent_type, base_branch,
|
|
429
|
+
host_id, after_task, after_any, origin_workspace_id, priority, machine_needs, position)
|
|
430
|
+
VALUES (@id, @project_id, @prompt, @name, @model, @model_overrides, @plan, @plan_reason, @plan_only, @started_by, @parent_session_id, @started_by_device, @started_via, @view, @agent_type, @base_branch,
|
|
431
|
+
@host_id, @after_task, @after_any, @origin, @priority, @machine_needs,
|
|
432
|
+
(SELECT COALESCE(MAX(position), 0) + 1 FROM task_queue))`
|
|
433
|
+
).run({
|
|
434
|
+
id,
|
|
435
|
+
project_id: project.id,
|
|
436
|
+
prompt: req.prompt.trim(),
|
|
437
|
+
name: req.name?.trim() || null,
|
|
438
|
+
model: typeof req.model === "string" ? req.model || null : null,
|
|
439
|
+
model_overrides:
|
|
440
|
+
req.model && typeof req.model === "object"
|
|
441
|
+
? JSON.stringify(req.model)
|
|
442
|
+
: null,
|
|
443
|
+
plan: req.plan === undefined ? null : req.plan ? 1 : 0,
|
|
444
|
+
plan_reason: req.planReason?.trim() || null,
|
|
445
|
+
plan_only: req.planOnly ? 1 : null,
|
|
446
|
+
started_by: req.startedBy ?? null,
|
|
447
|
+
parent_session_id: req.parentSessionId ?? null,
|
|
448
|
+
started_by_device: req.startedByDevice ?? null,
|
|
449
|
+
started_via: req.startedVia,
|
|
450
|
+
view: req.view ?? null,
|
|
451
|
+
agent_type: req.agent ?? null,
|
|
452
|
+
base_branch: req.baseBranch || null,
|
|
453
|
+
host_id: req.hostId || null,
|
|
454
|
+
after_task: afterTask,
|
|
455
|
+
after_any: afterAny ? JSON.stringify(afterAny) : null,
|
|
456
|
+
origin: req.originWorkspaceId ?? null,
|
|
457
|
+
priority: req.priority ?? DEFAULT_PRIORITY,
|
|
458
|
+
machine_needs: setting ? JSON.stringify(setting) : null,
|
|
459
|
+
});
|
|
460
|
+
if (req.pieceId) claimPiece(req.pieceId, id);
|
|
461
|
+
})();
|
|
445
462
|
tickQueueSoon();
|
|
446
463
|
return queuedView(id)!;
|
|
447
464
|
}
|
|
@@ -729,6 +746,10 @@ async function startRow(
|
|
|
729
746
|
planReason: r.plan_reason,
|
|
730
747
|
planOnly: !!r.plan_only,
|
|
731
748
|
startedBy: r.started_by,
|
|
749
|
+
parentSessionId: r.parent_session_id,
|
|
750
|
+
startedByDevice: r.started_by_device,
|
|
751
|
+
startedVia: (r.started_via ??
|
|
752
|
+
(r.started_by ? "session" : "person")) as StartVia,
|
|
732
753
|
view: r.view ?? undefined,
|
|
733
754
|
agent: (r.agent_type as AgentType | null) ?? undefined,
|
|
734
755
|
baseBranch: r.base_branch ?? undefined,
|
|
@@ -1022,7 +1043,9 @@ export async function tickOnce(now = Date.now()): Promise<"stale" | void> {
|
|
|
1022
1043
|
)) {
|
|
1023
1044
|
const r = rows.find((x) => x.id === c.id)!;
|
|
1024
1045
|
const options = (peers.get(r.project_id) ?? []).filter(
|
|
1025
|
-
(p) =>
|
|
1046
|
+
(p) =>
|
|
1047
|
+
!taken.has(p.hostId) &&
|
|
1048
|
+
(!orchestratorOf(r.started_by) || p.holds !== false)
|
|
1026
1049
|
);
|
|
1027
1050
|
const spot = place(c, home, options, built.get(c.id)!.needs, now);
|
|
1028
1051
|
if (!spot) continue;
|
package/lib/tasks/remote.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { projectRef } from "./project-ref";
|
|
|
18
18
|
import { orchestratedStart } from "./held";
|
|
19
19
|
import type { TaskStatus } from "./state";
|
|
20
20
|
import type { TaskView } from "./index";
|
|
21
|
+
import { orchestratorOf } from "./starter";
|
|
21
22
|
|
|
22
23
|
// What this machine's task API does, for another machine deciding whether
|
|
23
24
|
// it can trust it with a pinned merge or a move; keyed-start: a session
|
|
@@ -140,7 +141,8 @@ export async function startRemoteTask(
|
|
|
140
141
|
}
|
|
141
142
|
): Promise<Session> {
|
|
142
143
|
const link = requireHostLink(hostId);
|
|
143
|
-
const { startedBy, ...rest } = opts;
|
|
144
|
+
const { startedBy: by, ...rest } = opts;
|
|
145
|
+
const startedBy = orchestratorOf(by);
|
|
144
146
|
if (startedBy)
|
|
145
147
|
await requireCapability(
|
|
146
148
|
link,
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Who started a task and how. started_by is the session that started it (the
|
|
2
|
+
// calling task, chat or orchestrator; null for a person), parent_session_id
|
|
3
|
+
// the session it came from, started_via how it began.
|
|
4
|
+
|
|
5
|
+
import path from "path";
|
|
6
|
+
import { db } from "../db";
|
|
7
|
+
|
|
8
|
+
export type StartVia =
|
|
9
|
+
| "session"
|
|
10
|
+
| "orchestrator"
|
|
11
|
+
| "person"
|
|
12
|
+
| "plan"
|
|
13
|
+
| "schedule"
|
|
14
|
+
| "recipe"
|
|
15
|
+
| "stack"
|
|
16
|
+
| "remote";
|
|
17
|
+
|
|
18
|
+
export interface Starter {
|
|
19
|
+
startedBy: string | null;
|
|
20
|
+
parentSessionId: string | null;
|
|
21
|
+
startedVia: StartVia;
|
|
22
|
+
startedByDevice?: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const PERSON: Starter = {
|
|
26
|
+
startedBy: null,
|
|
27
|
+
parentSessionId: null,
|
|
28
|
+
startedVia: "person",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const personStarter = (device?: string | null): Starter => ({
|
|
32
|
+
...PERSON,
|
|
33
|
+
startedByDevice: device ?? null,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
interface Caller {
|
|
37
|
+
id: string;
|
|
38
|
+
role: string | null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The session making the call: `from` when it names a real session, else the
|
|
42
|
+
// one whose worktree holds `cwd`. A project folder is shared by many
|
|
43
|
+
// sessions, so only a worktree identifies one, and only a task's: a cwd
|
|
44
|
+
// outside every known task worktree names nobody. Remaining trust: any local
|
|
45
|
+
// process can still name a task by its id or its worktree path, exactly as
|
|
46
|
+
// it could name one in `from`; this records who asked, it grants nothing
|
|
47
|
+
// (a piece it claims is that task's own plan's).
|
|
48
|
+
export function callerSession(input: {
|
|
49
|
+
from?: unknown;
|
|
50
|
+
cwd?: unknown;
|
|
51
|
+
}): Caller | null {
|
|
52
|
+
if (typeof input.from === "string" && input.from) {
|
|
53
|
+
const row = db
|
|
54
|
+
.prepare(`SELECT id, role FROM sessions WHERE id = ?`)
|
|
55
|
+
.get(input.from) as Caller | undefined;
|
|
56
|
+
if (row) return row;
|
|
57
|
+
}
|
|
58
|
+
if (typeof input.cwd !== "string" || !path.isAbsolute(input.cwd)) return null;
|
|
59
|
+
const cwd = path.resolve(input.cwd);
|
|
60
|
+
const rows = db
|
|
61
|
+
.prepare(
|
|
62
|
+
`SELECT id, role, worktree_path FROM sessions
|
|
63
|
+
WHERE worktree_path IS NOT NULL AND archived_at IS NULL
|
|
64
|
+
AND task_status IS NOT NULL
|
|
65
|
+
AND (host_id IS NULL OR host_id = 'local')`
|
|
66
|
+
)
|
|
67
|
+
.all() as (Caller & { worktree_path: string })[];
|
|
68
|
+
let best: (Caller & { worktree_path: string }) | null = null;
|
|
69
|
+
for (const r of rows) {
|
|
70
|
+
const wt = path.resolve(r.worktree_path);
|
|
71
|
+
if (
|
|
72
|
+
(cwd === wt || cwd.startsWith(wt + path.sep)) &&
|
|
73
|
+
(!best || wt.length > best.worktree_path.length)
|
|
74
|
+
)
|
|
75
|
+
best = r;
|
|
76
|
+
}
|
|
77
|
+
return best;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function starterOf(
|
|
81
|
+
caller: Caller | null,
|
|
82
|
+
device?: string | null
|
|
83
|
+
): Starter {
|
|
84
|
+
if (!caller) return personStarter(device);
|
|
85
|
+
return {
|
|
86
|
+
startedBy: caller.id,
|
|
87
|
+
parentSessionId: caller.id,
|
|
88
|
+
startedVia: caller.role === "orchestrator" ? "orchestrator" : "session",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The orchestrator that owns a session's work: follows started_by up to the
|
|
93
|
+
// first orchestrator, null when the chain ends at a person. It gives up after
|
|
94
|
+
// 6 hops (a deeper chain, or a cycle, counts as no orchestrator), so a bad
|
|
95
|
+
// row can never loop it.
|
|
96
|
+
export function orchestratorOf(startedBy: string | null | undefined) {
|
|
97
|
+
const seen = new Set<string>();
|
|
98
|
+
let id = startedBy ?? null;
|
|
99
|
+
for (let hops = 0; id && hops < 6 && !seen.has(id); hops++) {
|
|
100
|
+
seen.add(id);
|
|
101
|
+
const row = db
|
|
102
|
+
.prepare(`SELECT role, started_by FROM sessions WHERE id = ?`)
|
|
103
|
+
.get(id) as
|
|
104
|
+
| { role: string | null; started_by: string | null }
|
|
105
|
+
| undefined;
|
|
106
|
+
if (!row) return null;
|
|
107
|
+
if (row.role === "orchestrator") return id;
|
|
108
|
+
id = row.started_by;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function recordStart(id: string, s: Starter): void {
|
|
114
|
+
db.prepare(
|
|
115
|
+
`UPDATE sessions SET started_by = ?, parent_session_id = (SELECT id FROM sessions WHERE id = ?), started_via = ?,
|
|
116
|
+
started_by_device = COALESCE(?, started_by_device) WHERE id = ?`
|
|
117
|
+
).run(
|
|
118
|
+
s.startedBy,
|
|
119
|
+
s.parentSessionId,
|
|
120
|
+
s.startedVia,
|
|
121
|
+
s.startedByDevice ?? null,
|
|
122
|
+
id
|
|
123
|
+
);
|
|
124
|
+
}
|
package/lib/usage/calls.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { db } from "../db";
|
|
9
|
-
import { ANTHROPIC
|
|
9
|
+
import { ANTHROPIC } from "../models/roles";
|
|
10
|
+
import { sessionRole } from "../models/session-role";
|
|
10
11
|
import { ctxOf, weighted, type TokenRow } from "./weights";
|
|
11
12
|
import type { CallKind, CallRow, CallSource, CallUsage } from "./types";
|
|
12
13
|
|
package/lib/usage/turns.ts
CHANGED
|
@@ -8,7 +8,8 @@ import { db, type Session } from "../db";
|
|
|
8
8
|
import type { UsageTotals } from "../chat/events";
|
|
9
9
|
import { totalTokens, turnDelta } from "../chat/context";
|
|
10
10
|
import { readUsage } from "../orchestrator/usage";
|
|
11
|
-
import { ANTHROPIC
|
|
11
|
+
import { ANTHROPIC } from "../models/roles";
|
|
12
|
+
import { sessionRole } from "../models/session-role";
|
|
12
13
|
|
|
13
14
|
// Where a worker's totals start: a resumed conversation continues from the
|
|
14
15
|
// totals its transcript saved, which the last worker stored; a new one
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saadnvd1/agent-os",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.600",
|
|
4
4
|
"description": "Self-hosted web UI for managing Claude Code sessions",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agent-os": "scripts/agent-os"
|
|
@@ -29,11 +29,13 @@
|
|
|
29
29
|
"LICENSE"
|
|
30
30
|
],
|
|
31
31
|
"scripts": {
|
|
32
|
-
"predev": "node scripts/ensure-native.mjs",
|
|
32
|
+
"predev": "node scripts/ensure-native.mjs && node scripts/gen-migrations-index.mjs",
|
|
33
33
|
"dev": "AGENTOS_DEV=1 tsx server.ts",
|
|
34
|
+
"prebuild": "node scripts/gen-migrations-index.mjs",
|
|
34
35
|
"build": "next build",
|
|
35
|
-
"prestart": "node scripts/ensure-native.mjs",
|
|
36
|
+
"prestart": "node scripts/ensure-native.mjs && node scripts/gen-migrations-index.mjs",
|
|
36
37
|
"start": "NODE_ENV=production tsx server.ts",
|
|
38
|
+
"pretypecheck": "node scripts/gen-migrations-index.mjs",
|
|
37
39
|
"typecheck": "tsc --noEmit",
|
|
38
40
|
"setup": "bash scripts/setup.sh",
|
|
39
41
|
"postinstall": "chmod +x node_modules/node-pty/prebuilds/*/spawn-helper 2>/dev/null || true",
|
|
@@ -42,6 +44,7 @@
|
|
|
42
44
|
"tauri:dev": "tauri dev",
|
|
43
45
|
"tauri:build": "tauri build",
|
|
44
46
|
"tauri:build:linux": "docker build -t agentos-linux -f src-tauri/Dockerfile.linux src-tauri && docker run --rm -v $(pwd)/src-tauri/target:/app/target agentos-linux",
|
|
47
|
+
"prelint": "node scripts/gen-migrations-index.mjs",
|
|
45
48
|
"lint": "eslint .",
|
|
46
49
|
"format": "prettier --write .",
|
|
47
50
|
"format:check": "prettier --check .",
|
package/scripts/check
CHANGED
|
@@ -7,6 +7,7 @@ cd "$(dirname "$0")/.."
|
|
|
7
7
|
|
|
8
8
|
step() { printf '\n▸ %s\n' "$1"; }
|
|
9
9
|
|
|
10
|
+
step "migrations"; node scripts/gen-migrations-index.mjs && node scripts/gen-migrations-index.mjs --check
|
|
10
11
|
step "terms"; npx tsx scripts/hooks/banned-terms.ts --tree
|
|
11
12
|
step "typecheck"; npx tsc --noEmit
|
|
12
13
|
step "lint"; npx eslint . --max-warnings=40
|