@saadnvd1/agent-os 0.3.700 → 0.3.723
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/README.md +2 -0
- package/app/api/agent-roles/route.test.ts +67 -0
- package/app/api/agent-roles/route.ts +61 -0
- package/app/api/analyst/[id]/recommendations/[recId]/again/route.ts +15 -0
- package/app/api/analyst/[id]/recommendations/[recId]/dismiss/route.ts +13 -0
- package/app/api/analyst/[id]/recommendations/[recId]/send/route.ts +13 -0
- package/app/api/analyst/[id]/review/route.ts +22 -0
- package/app/api/analyst/[id]/route.ts +44 -0
- package/app/api/analyst/route.ts +10 -0
- package/app/api/bus/groups/route.ts +44 -0
- package/app/api/bus/spawn/route.ts +34 -2
- package/app/api/claims/[id]/route.ts +20 -0
- package/app/api/claims/check/route.ts +23 -0
- package/app/api/claims/check.test.ts +113 -0
- package/app/api/claims/exchange/route.ts +42 -0
- package/app/api/claims/outside.test.ts +90 -0
- package/app/api/claims/route.test.ts +193 -0
- package/app/api/claims/route.ts +73 -0
- package/app/api/hosts/[id]/route.ts +9 -1
- package/app/api/models/availability/route.ts +9 -0
- package/app/api/models/choices/route.ts +9 -0
- package/app/api/peer/unlink/route.ts +17 -0
- package/app/api/peers/[hostId]/audit/route.ts +14 -0
- package/app/api/peers/[hostId]/route.ts +20 -0
- package/app/api/peers/gate.test.ts +64 -1
- package/app/api/peers/pending/route.test.ts +37 -0
- package/app/api/peers/pending/route.ts +11 -4
- package/app/api/session-groups/[id]/restore/route.ts +11 -0
- package/app/api/session-groups/[id]/route.ts +25 -0
- package/app/api/session-groups/order/route.ts +15 -0
- package/app/api/session-groups/route.ts +21 -0
- package/app/api/sessions/[id]/analyst/route.ts +11 -0
- package/app/api/sessions/[id]/crew/route.ts +38 -0
- package/app/api/sessions/[id]/export/estimate/route.ts +23 -0
- package/app/api/sessions/[id]/export/route.test.ts +125 -0
- package/app/api/sessions/[id]/export/route.ts +34 -0
- package/app/api/sessions/[id]/fork/route.ts +3 -0
- package/app/api/sessions/[id]/group/route.ts +18 -0
- package/app/api/sessions/[id]/model/route.ts +49 -0
- package/app/api/sessions/[id]/route.ts +14 -4
- package/app/api/sessions/[id]/summarize/route.ts +2 -0
- package/app/api/sessions/claims.test.ts +122 -0
- package/app/api/sessions/group/route.ts +17 -0
- package/app/api/sessions/route.ts +31 -14
- package/app/page.tsx +10 -0
- package/components/Chat/Messages.tsx +104 -10
- package/components/Claims/ClaimFiles.tsx +38 -0
- package/components/Claims/ClaimRow.tsx +82 -0
- package/components/Claims/ClaimsSheet.tsx +176 -0
- package/components/Claims/OverlapPill.tsx +31 -0
- package/components/Claims/StartClaimsNotice.tsx +70 -0
- package/components/Claims/claims-ui.test.ts +161 -0
- package/components/Claims/format.ts +57 -0
- package/components/Claims/index.ts +3 -0
- package/components/Draft/DraftChips.tsx +6 -1
- package/components/Draft/DraftPanel.tsx +90 -3
- package/components/Draft/ModelUnavailableDialog.tsx +78 -0
- package/components/Draft/RoleChips.tsx +110 -0
- package/components/Export/ExportActions.tsx +136 -0
- package/components/Export/ExportSheet.tsx +217 -0
- package/components/Export/RangePicker.tsx +144 -0
- package/components/Export/Seg.tsx +38 -0
- package/components/Export/TurnSelectBar.tsx +45 -0
- package/components/Hosts/AccountMachines.tsx +32 -18
- package/components/Hosts/HostRow.tsx +155 -14
- package/components/Hosts/IncomingLinkSheet.tsx +28 -10
- package/components/Hosts/PeerActivitySheet.tsx +111 -0
- package/components/Models/AgentRoleCard.tsx +114 -0
- package/components/Models/AgentRolesSection.tsx +132 -0
- package/components/Models/ModelBadges.tsx +48 -0
- package/components/Models/ModelsSection.tsx +4 -2
- package/components/Models/RoleModelSelect.tsx +87 -0
- package/components/Pane/DesktopTabBar.tsx +2 -0
- package/components/Pane/MobileTabBar.tsx +12 -0
- package/components/Pane/PaneMenu.tsx +10 -0
- package/components/SessionList/GroupShelves.tsx +195 -0
- package/components/SessionList/MoveToGroupMenu.tsx +105 -0
- package/components/SessionList/RowSubtitle.tsx +7 -1
- package/components/SessionList/SelectionToolbar.tsx +52 -1
- package/components/SessionList/SessionRow.tsx +19 -7
- package/components/SessionList/SessionRowMenu.tsx +23 -0
- package/components/SessionList/SessionShelves.tsx +5 -0
- package/components/SessionList/SidebarBody.tsx +6 -0
- package/components/SessionList/SidebarContextMenu.test.ts +104 -0
- package/components/SessionList/SidebarContextMenu.tsx +59 -0
- package/components/SessionList/SidebarHeader.tsx +5 -0
- package/components/SessionList/WorkspaceSwitcher.tsx +23 -6
- package/components/SessionList/index.tsx +18 -14
- package/components/SessionList/useSidebarData.ts +10 -0
- package/components/Sessions/RolePill.tsx +196 -0
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Share/ShareDialog.tsx +13 -0
- package/components/ui/notice.tsx +46 -0
- package/components/ui/status-pill.tsx +39 -0
- package/data/analyst/index.ts +3 -0
- package/data/analyst/keys.ts +6 -0
- package/data/analyst/mutations.ts +116 -0
- package/data/analyst/queries.ts +35 -0
- package/data/claims/index.ts +2 -0
- package/data/claims/keys.ts +5 -0
- package/data/claims/queries.ts +47 -0
- package/data/exports/index.ts +2 -0
- package/data/exports/keys.ts +3 -0
- package/data/exports/queries.ts +51 -0
- package/data/models/index.ts +98 -0
- package/data/peers/keys.ts +1 -0
- package/data/peers/queries.ts +31 -0
- package/data/push/topics.ts +9 -0
- package/data/session-groups/index.ts +156 -0
- package/data/sessions/index.ts +0 -1
- package/data/sessions/pin.ts +2 -6
- package/data/sessions/queries.ts +36 -29
- package/data/sessions/shelf.ts +2 -6
- package/data/tasks/index.ts +2 -0
- package/hooks/useSessions.ts +0 -12
- package/lib/__fixtures__/peer-rpc.ts +65 -33
- package/lib/agents/brief.ts +1 -0
- package/lib/agents/spawn.ts +4 -0
- package/lib/analyst/__fixtures__/items.ts +57 -0
- package/lib/analyst/__fixtures__/mocks.ts +49 -0
- package/lib/analyst/brief.ts +17 -0
- package/lib/analyst/buildDigest.ts +204 -0
- package/lib/analyst/createLink.ts +98 -0
- package/lib/analyst/endLink.ts +31 -0
- package/lib/analyst/forward.ts +109 -0
- package/lib/analyst/index.ts +16 -0
- package/lib/analyst/linkView.ts +38 -0
- package/lib/analyst/onTurnEnd.ts +119 -0
- package/lib/analyst/parseReview.ts +41 -0
- package/lib/analyst/pushDigest.ts +143 -0
- package/lib/analyst/resume.ts +35 -0
- package/lib/analyst/route.ts +50 -0
- package/lib/analyst/signals.ts +121 -0
- package/lib/analyst/store.ts +303 -0
- package/lib/analyst/types.ts +140 -0
- package/lib/bus/condense.ts +5 -1
- package/lib/bus/index.ts +13 -0
- package/lib/bus/read-scope.ts +12 -0
- package/lib/bus/role-alias.ts +70 -0
- package/lib/bus/targets.ts +3 -0
- package/lib/chat/drivers/opencode-mapper.ts +55 -1
- package/lib/chat/drivers/pi-approvals.ts +7 -4
- package/lib/chat/drivers/pi-mapper.ts +38 -0
- package/lib/chat/drivers/pi-text-tools.ts +104 -0
- package/lib/chat/drivers/pi.ts +8 -3
- package/lib/chat/drivers/turn-items.ts +36 -11
- package/lib/chat/drivers/turn-outcome.ts +108 -0
- package/lib/chat/events.ts +8 -0
- package/lib/chat/registry.ts +13 -0
- package/lib/chat/store.ts +19 -0
- package/lib/chat/turns.ts +1 -0
- package/lib/chat/worker/extras.ts +4 -2
- package/lib/claims/collectLocal.ts +123 -0
- package/lib/claims/describe.ts +41 -0
- package/lib/claims/exchange.ts +91 -0
- package/lib/claims/goal.ts +45 -0
- package/lib/claims/index.ts +10 -0
- package/lib/claims/loop.ts +51 -0
- package/lib/claims/machine.ts +21 -0
- package/lib/claims/mine.ts +43 -0
- package/lib/claims/notifyOverlaps.ts +101 -0
- package/lib/claims/onMerged.ts +85 -0
- package/lib/claims/outside.ts +36 -0
- package/lib/claims/overlap.ts +87 -0
- package/lib/claims/overlapCache.ts +86 -0
- package/lib/claims/overlapText.ts +36 -0
- package/lib/claims/peers.ts +56 -0
- package/lib/claims/startCheck.ts +100 -0
- package/lib/claims/startWarning.ts +29 -0
- package/lib/claims/store.ts +366 -0
- package/lib/claims/types.ts +102 -0
- package/lib/claims/view.ts +80 -0
- package/lib/claims/yield.ts +24 -0
- package/lib/db/changes.ts +12 -0
- package/lib/db/migrations/20261020T1400_work_claims.ts +35 -0
- package/lib/db/migrations/20261020T1515_peer_unlinked.ts +22 -0
- package/lib/db/migrations/20261020T1700_clean_connect_names.ts +18 -0
- package/lib/db/migrations/20261020T1800_session_groups.ts +49 -0
- package/lib/db/migrations/20261020T1900_task_queue_after_claim.ts +13 -0
- package/lib/db/migrations/20261020T2000_claim_notices.ts +20 -0
- package/lib/db/migrations/20261020T2100_claims_via_host.ts +21 -0
- package/lib/db/migrations/20261020T2200_work_claims_watch.ts +10 -0
- package/lib/db/migrations/20261021T1400_session_agent_roles.ts +20 -0
- package/lib/db/migrations/20261021T1430_analyst_links.ts +52 -0
- package/lib/db/queries.ts +0 -40
- package/lib/db/session-parts/columns.ts +3 -0
- package/lib/db/types.ts +19 -8
- package/lib/doctor/checks.ts +64 -1
- package/lib/done/archive.ts +2 -0
- package/lib/drafts.ts +4 -0
- package/lib/export/build.ts +470 -0
- package/lib/export/compact.ts +156 -0
- package/lib/export/estimate.ts +83 -0
- package/lib/export/fixtures.ts +45 -0
- package/lib/export/html.tsx +195 -0
- package/lib/export/json.ts +39 -0
- package/lib/export/load.ts +64 -0
- package/lib/export/markdown.ts +127 -0
- package/lib/export/request.ts +36 -0
- package/lib/export/service.ts +142 -0
- package/lib/export/summary.ts +86 -0
- package/lib/export/turns.ts +63 -0
- package/lib/export/view.ts +173 -0
- package/lib/git-remote.ts +43 -0
- package/lib/hosts/index.ts +10 -1
- package/lib/models/agent-binding.ts +102 -0
- package/lib/models/agent-roles-view.ts +157 -0
- package/lib/models/agent-roles.ts +87 -0
- package/lib/models/availability-refs.ts +17 -0
- package/lib/models/availability.ts +91 -0
- package/lib/models/catalog.ts +156 -0
- package/lib/models/config.ts +27 -0
- package/lib/models/locality.ts +33 -0
- package/lib/models/providers.ts +11 -0
- package/lib/models/resolve.ts +50 -12
- package/lib/models/roles.ts +12 -0
- package/lib/needs-you.ts +1 -1
- package/lib/orchestration.ts +2 -0
- package/lib/orchestrator/act.ts +54 -4
- package/lib/orchestrator/brief.ts +2 -1
- package/lib/orchestrator/claim-wait.ts +71 -0
- package/lib/orchestrator/conditions.ts +28 -1
- package/lib/orchestrator/describe.ts +6 -1
- package/lib/orchestrator/facts.ts +12 -2
- package/lib/orchestrator/read.ts +5 -1
- package/lib/orchestrator/serve.ts +37 -2
- package/lib/orchestrator/tool-names.ts +1 -0
- package/lib/orchestrator/tool-schemas.ts +31 -1
- package/lib/orchestrator/tools.ts +3 -1
- package/lib/orchestrator/untrusted.ts +5 -0
- package/lib/orchestrator/watcher.ts +19 -1
- package/lib/peer/account.ts +74 -2
- package/lib/peer/allowlist.ts +1 -1
- package/lib/peer/client.ts +26 -1
- package/lib/peer/link-cli.ts +19 -4
- package/lib/peer/links.ts +117 -5
- package/lib/peer/machines.ts +14 -2
- package/lib/peer/name.ts +32 -0
- package/lib/peer/pairing.ts +34 -5
- package/lib/peer/pin.ts +10 -3
- package/lib/peer/reasons.ts +7 -0
- package/lib/peer/revoke.ts +75 -0
- package/lib/peer/rpc-server.ts +53 -5
- package/lib/peer/unlink.ts +62 -0
- package/lib/peer/unlinked-proof.ts +44 -0
- package/lib/projects.ts +6 -2
- package/lib/security/demo.ts +7 -0
- package/lib/session-artifacts/agentos-files.ts +94 -0
- package/lib/session-artifacts/collect.ts +17 -0
- package/lib/session-groups/assignSessions.ts +81 -0
- package/lib/session-groups/cli.ts +61 -0
- package/lib/session-groups/createGroup.ts +28 -0
- package/lib/session-groups/index.ts +21 -0
- package/lib/session-groups/removeGroup.ts +36 -0
- package/lib/session-groups/reorderGroups.ts +31 -0
- package/lib/session-groups/resolveGroup.ts +19 -0
- package/lib/session-groups/route.ts +32 -0
- package/lib/session-groups/store.ts +85 -0
- package/lib/session-groups/types.ts +25 -0
- package/lib/session-groups/updateGroup.ts +35 -0
- package/lib/session-groups/workspace.ts +21 -0
- package/lib/sessions/crew.ts +72 -0
- package/lib/sessions/launch.ts +54 -11
- package/lib/settings-url.ts +1 -0
- package/lib/share/markdown.ts +2 -2
- package/lib/share/redact.ts +16 -10
- package/lib/share/render.tsx +1 -1
- package/lib/share/snapshot.ts +2 -2
- package/lib/sidebar/rollup.ts +1 -0
- package/lib/sidebar/row-menu.ts +8 -1
- package/lib/sidebar/shelves.ts +90 -15
- package/lib/sidebar/visible-order.ts +5 -0
- package/lib/tasks/hold-checks.ts +3 -1
- package/lib/tasks/holds.ts +3 -0
- package/lib/tasks/index.ts +11 -0
- package/lib/tasks/queue-rows.ts +8 -1
- package/lib/tasks/queue-start.ts +1 -0
- package/lib/tasks/queue-types.ts +9 -1
- package/lib/tasks/queue.ts +15 -5
- package/lib/tasks/schedule-types.ts +1 -0
- package/lib/test-temp.ts +19 -3
- package/lib/workspaces.ts +11 -3
- package/lib/worktree-deps/caches.ts +3 -2
- package/lib/worktree-deps/clone.ts +3 -1
- package/lib/worktree-deps/paths.ts +26 -0
- package/lib/worktree-sweep/testTemp.ts +23 -106
- package/package.json +1 -1
- package/scripts/agent-os +1 -1
- package/scripts/autodeploy +2 -0
- package/scripts/autodeploy.test.ts +31 -1
- package/scripts/claim-hook.sh +31 -0
- package/scripts/claim-hook.test.ts +230 -0
- package/scripts/lib/cli-links.sh +36 -0
- package/scripts/lib/commands.sh +3 -0
- package/scripts/lib/machine.sh +27 -2
- package/scripts/lib/machine.test.ts +25 -3
- package/scripts/sync-cli-links +7 -0
- package/scripts/sync-cli-links.test.ts +69 -0
- package/server.ts +12 -1
- package/stores/claimsUi.ts +22 -0
- package/stores/exportUi.ts +87 -0
- package/app/api/groups/[...path]/route.ts +0 -136
- package/app/api/groups/route.ts +0 -93
- package/data/groups/index.ts +0 -1
- package/data/groups/mutations.ts +0 -95
- package/hooks/useGroups.ts +0 -37
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Turns this machine's live sessions into claims: the repo, branch and the
|
|
2
|
+
// paths their diff touches so far. Only paths leave git, never content.
|
|
3
|
+
|
|
4
|
+
import { getDb } from "../db";
|
|
5
|
+
import { getCurrentBranch, getDefaultBranch } from "../git";
|
|
6
|
+
import { git } from "../git-exec";
|
|
7
|
+
import { repoKey } from "../git-remote";
|
|
8
|
+
import { IN_FLIGHT } from "../tasks/queue-rows";
|
|
9
|
+
import { expandHome } from "../tasks/session";
|
|
10
|
+
import { MAX_FILES } from "./types";
|
|
11
|
+
import {
|
|
12
|
+
localLiveSessionIds,
|
|
13
|
+
release,
|
|
14
|
+
upsertLocal,
|
|
15
|
+
type LocalClaim,
|
|
16
|
+
} from "./store";
|
|
17
|
+
|
|
18
|
+
interface SessionRow {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
working_directory: string;
|
|
22
|
+
worktree_path: string | null;
|
|
23
|
+
branch_name: string | null;
|
|
24
|
+
base_branch: string | null;
|
|
25
|
+
pr_url: string | null;
|
|
26
|
+
pr_status: string | null;
|
|
27
|
+
task_status: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const OPTS = { timeout: 5000 };
|
|
31
|
+
|
|
32
|
+
const paths = (out: string) => out.split("\0").filter(Boolean);
|
|
33
|
+
|
|
34
|
+
export async function changedFiles(
|
|
35
|
+
dir: string,
|
|
36
|
+
base: string
|
|
37
|
+
): Promise<string[]> {
|
|
38
|
+
const mergeBase = (
|
|
39
|
+
await git(dir, ["merge-base", "HEAD", `origin/${base}`], OPTS).catch(() =>
|
|
40
|
+
git(dir, ["merge-base", "HEAD", base], OPTS).catch(() => "")
|
|
41
|
+
)
|
|
42
|
+
).trim();
|
|
43
|
+
const [committed, untracked] = await Promise.all([
|
|
44
|
+
mergeBase
|
|
45
|
+
? git(dir, ["diff", "--name-only", "-z", mergeBase], OPTS)
|
|
46
|
+
: git(dir, ["diff", "--name-only", "-z", "HEAD"], OPTS).catch(() => ""),
|
|
47
|
+
git(dir, ["ls-files", "--others", "--exclude-standard", "-z"], OPTS),
|
|
48
|
+
]);
|
|
49
|
+
return [...new Set([...paths(committed), ...paths(untracked)])].filter(
|
|
50
|
+
(p) => !p.split("/").some((seg) => seg.endsWith(".aos-partial"))
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Sessions that can hold a claim: local, not a mirror of another machine's.
|
|
55
|
+
function candidates(): SessionRow[] {
|
|
56
|
+
return getDb()
|
|
57
|
+
.prepare(
|
|
58
|
+
`SELECT id, name, working_directory, worktree_path, branch_name, base_branch,
|
|
59
|
+
pr_url, pr_status, task_status
|
|
60
|
+
FROM session_rows
|
|
61
|
+
WHERE archived_at IS NULL AND COALESCE(peer_mirror, 0) = 0
|
|
62
|
+
AND (host_id IS NULL OR host_id = 'local')`
|
|
63
|
+
)
|
|
64
|
+
.all() as SessionRow[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const last = new Map<string, LocalClaim>();
|
|
68
|
+
|
|
69
|
+
// Writes this machine's claims and releases the ones whose session is gone,
|
|
70
|
+
// finished, or whose PR closed. A git failure keeps the last good paths.
|
|
71
|
+
export async function collectLocal(origin: {
|
|
72
|
+
id: string;
|
|
73
|
+
name: string;
|
|
74
|
+
}): Promise<void> {
|
|
75
|
+
const alive = new Set<string>();
|
|
76
|
+
for (const s of candidates()) {
|
|
77
|
+
try {
|
|
78
|
+
const done = s.pr_status === "merged" || s.pr_status === "closed";
|
|
79
|
+
const idleTask =
|
|
80
|
+
s.task_status &&
|
|
81
|
+
!IN_FLIGHT.includes(s.task_status) &&
|
|
82
|
+
s.pr_status !== "open";
|
|
83
|
+
if (done || idleTask) {
|
|
84
|
+
if (done) release(s.id, s.pr_status === "merged" ? "merged" : "done");
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const dir = expandHome(s.worktree_path || s.working_directory);
|
|
88
|
+
const repo = await repoKey(dir);
|
|
89
|
+
if (!repo) continue;
|
|
90
|
+
const branch = s.branch_name || (await getCurrentBranch(dir));
|
|
91
|
+
const base = s.base_branch || (await getDefaultBranch(dir));
|
|
92
|
+
let files: string[];
|
|
93
|
+
try {
|
|
94
|
+
files = await changedFiles(dir, base);
|
|
95
|
+
} catch {
|
|
96
|
+
files = last.get(s.id)?.files ?? [];
|
|
97
|
+
}
|
|
98
|
+
// A shell sitting on the default branch with no changes claims nothing.
|
|
99
|
+
if ((branch === base || branch === "HEAD") && !files.length) continue;
|
|
100
|
+
const claim: LocalClaim = {
|
|
101
|
+
sessionId: s.id,
|
|
102
|
+
sessionName: s.name,
|
|
103
|
+
repo,
|
|
104
|
+
branch,
|
|
105
|
+
base,
|
|
106
|
+
goal: s.name,
|
|
107
|
+
files: files.slice(0, MAX_FILES),
|
|
108
|
+
filesMore: Math.max(0, files.length - MAX_FILES),
|
|
109
|
+
prUrl: s.pr_url,
|
|
110
|
+
};
|
|
111
|
+
last.set(s.id, claim);
|
|
112
|
+
upsertLocal(origin, claim);
|
|
113
|
+
alive.add(s.id);
|
|
114
|
+
} catch {
|
|
115
|
+
// This session claims nothing this round.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
for (const id of localLiveSessionIds())
|
|
119
|
+
if (!alive.has(id)) {
|
|
120
|
+
release(id, "abandoned");
|
|
121
|
+
last.delete(id);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The claims as text for the orchestrator and `aos claims`. A goal and a
|
|
2
|
+
// session name are another machine's words: fenced as data.
|
|
3
|
+
|
|
4
|
+
import { untrusted } from "../orchestrator/untrusted";
|
|
5
|
+
import { machineId } from "./machine";
|
|
6
|
+
import { findOverlaps } from "./overlap";
|
|
7
|
+
import type { Claim } from "./types";
|
|
8
|
+
|
|
9
|
+
const ago = (ms: number) => {
|
|
10
|
+
const m = Math.max(0, Math.round(ms / 60_000));
|
|
11
|
+
return m < 1 ? "just now" : m < 60 ? `${m}m` : `${Math.round(m / 60)}h`;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function describeClaims(
|
|
15
|
+
claims: Claim[],
|
|
16
|
+
opts: { onlyOthers?: boolean; now?: number } = {}
|
|
17
|
+
): string {
|
|
18
|
+
const now = opts.now ?? Date.now();
|
|
19
|
+
const self = machineId();
|
|
20
|
+
const shown = opts.onlyOthers
|
|
21
|
+
? claims.filter((c) => c.origin !== self)
|
|
22
|
+
: claims;
|
|
23
|
+
if (!shown.length) return "";
|
|
24
|
+
const lines = shown.map((c) => {
|
|
25
|
+
const where = c.origin === self ? "here" : `on ${c.originName}`;
|
|
26
|
+
const files = c.files.length + c.filesMore;
|
|
27
|
+
return `- ${untrusted("claim", c.goal)} ${where}, ${c.branch ?? "no branch"}, ${files} file${files === 1 ? "" : "s"}, started ${ago(now - c.startedAt)} ago${c.prUrl ? `, ${c.prUrl}` : ""}`;
|
|
28
|
+
});
|
|
29
|
+
const overlaps = findOverlaps(claims).filter(
|
|
30
|
+
(o) => !opts.onlyOthers || o.a.origin !== self || o.b.origin !== self
|
|
31
|
+
);
|
|
32
|
+
const tail = overlaps.map(
|
|
33
|
+
(o) =>
|
|
34
|
+
`! ${o.strength} overlap (${o.kinds.join("+")}): ${untrusted("claim", o.a.goal)} (${o.a.originName}) and ${untrusted("claim", o.b.goal)} (${o.b.originName})${o.shared.length ? `, shared ${o.shared.slice(0, 5).join(", ")}` : ""}`
|
|
35
|
+
);
|
|
36
|
+
return [
|
|
37
|
+
"Claimed work (live sessions on linked machines):",
|
|
38
|
+
...lines,
|
|
39
|
+
...tail,
|
|
40
|
+
].join("\n");
|
|
41
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The claims exchange between linked machines: one request carries our claims
|
|
2
|
+
// and brings back theirs. The side that holds the link does the calling, so
|
|
3
|
+
// it works when only one machine can reach the other.
|
|
4
|
+
|
|
5
|
+
import { hostApi, HostApiError, type HostLink } from "../hosts/remote-api";
|
|
6
|
+
import { claimsShared, machineId, machineName } from "./machine";
|
|
7
|
+
import { exchangeTargets, peerMachineOf, rememberPeer } from "./peers";
|
|
8
|
+
import { upsertRemote, wireFor } from "./store";
|
|
9
|
+
import { exchangeBody, type ExchangeBody } from "./types";
|
|
10
|
+
|
|
11
|
+
const OLD_PEER_MS = 60 * 60_000;
|
|
12
|
+
const skipUntil = new Map<string, number>();
|
|
13
|
+
const failing = new Set<string>();
|
|
14
|
+
|
|
15
|
+
export const selfMachine = () => ({ id: machineId(), name: machineName() });
|
|
16
|
+
|
|
17
|
+
// Takes what a peer sent; returns how many claims were new or newer.
|
|
18
|
+
// viaHost is the link it arrived through, when we know it.
|
|
19
|
+
export function acceptExchange(
|
|
20
|
+
body: ExchangeBody,
|
|
21
|
+
viaHost: string | null = null,
|
|
22
|
+
now = Date.now()
|
|
23
|
+
): number {
|
|
24
|
+
if (!claimsShared()) return 0;
|
|
25
|
+
const self = machineId();
|
|
26
|
+
if (body.machine.id === self) return 0;
|
|
27
|
+
let n = 0;
|
|
28
|
+
for (const c of body.claims)
|
|
29
|
+
if (upsertRemote(self, body.machine.id, c, now, viaHost)) n++;
|
|
30
|
+
return n;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// What we answer a peer that called: our claims, not echoed back to it.
|
|
34
|
+
export function answerExchange(body: ExchangeBody): ExchangeBody {
|
|
35
|
+
const self = selfMachine();
|
|
36
|
+
return {
|
|
37
|
+
machine: self,
|
|
38
|
+
claims: claimsShared()
|
|
39
|
+
? wireFor(self.id, { machineId: body.machine.id })
|
|
40
|
+
: [],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function exchangeOver(
|
|
45
|
+
link: HostLink,
|
|
46
|
+
self: { id: string; name: string },
|
|
47
|
+
log: (line: string) => void
|
|
48
|
+
): Promise<boolean> {
|
|
49
|
+
const hostId = link.hostId;
|
|
50
|
+
if ((skipUntil.get(hostId) ?? 0) > Date.now()) return false;
|
|
51
|
+
try {
|
|
52
|
+
const raw = await hostApi<unknown>(link, "/api/claims/exchange", {
|
|
53
|
+
body: {
|
|
54
|
+
machine: self,
|
|
55
|
+
claims: wireFor(self.id, { machineId: peerMachineOf(hostId) }),
|
|
56
|
+
},
|
|
57
|
+
timeout: 8000,
|
|
58
|
+
});
|
|
59
|
+
const parsed = exchangeBody.safeParse(raw);
|
|
60
|
+
if (!parsed.success) throw new Error("its answer wasn't claims");
|
|
61
|
+
rememberPeer(hostId, parsed.data.machine.id);
|
|
62
|
+
acceptExchange(parsed.data, hostId);
|
|
63
|
+
failing.delete(hostId);
|
|
64
|
+
return true;
|
|
65
|
+
} catch (err) {
|
|
66
|
+
if (err instanceof HostApiError && err.status === 404) {
|
|
67
|
+
skipUntil.set(hostId, Date.now() + OLD_PEER_MS);
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (!failing.has(hostId)) {
|
|
71
|
+
failing.add(hostId);
|
|
72
|
+
log(
|
|
73
|
+
`claims: ${link.hostName} didn't answer: ${(err as Error)?.message ?? err}`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Each machine once per tick, over its best working link.
|
|
81
|
+
export async function syncPeers(
|
|
82
|
+
log: (line: string) => void = () => {}
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
if (!claimsShared()) return;
|
|
85
|
+
const self = selfMachine();
|
|
86
|
+
await Promise.all(
|
|
87
|
+
exchangeTargets().map(async (links) => {
|
|
88
|
+
for (const link of links) if (await exchangeOver(link, self, log)) return;
|
|
89
|
+
})
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// How alike two one-line goals read: the share of content words they have
|
|
2
|
+
// in common. Words every task title has (add, fix, update) carry nothing.
|
|
3
|
+
|
|
4
|
+
const STOP = new Set(
|
|
5
|
+
"a an and the to of for in on at with from by or as is are be it its this that into via add adds added fix fixes fixed update updates make makes use using new support improve implement allow when not no all use".split(
|
|
6
|
+
" "
|
|
7
|
+
)
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const stem = (w: string) =>
|
|
11
|
+
w.length > 5 && w.endsWith("ing")
|
|
12
|
+
? w.slice(0, -3)
|
|
13
|
+
: w.length > 3 && w.endsWith("s") && !w.endsWith("ss")
|
|
14
|
+
? w.slice(0, -1)
|
|
15
|
+
: w;
|
|
16
|
+
|
|
17
|
+
export function goalWords(goal: string): Set<string> {
|
|
18
|
+
return new Set(
|
|
19
|
+
goal
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.split(/[^a-z0-9]+/)
|
|
22
|
+
.filter((w) => w.length > 1 && !STOP.has(w))
|
|
23
|
+
.map(stem)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function goalSimilarity(
|
|
28
|
+
a: string,
|
|
29
|
+
b: string
|
|
30
|
+
): {
|
|
31
|
+
score: number;
|
|
32
|
+
shared: number;
|
|
33
|
+
} {
|
|
34
|
+
const x = goalWords(a);
|
|
35
|
+
const y = goalWords(b);
|
|
36
|
+
let shared = 0;
|
|
37
|
+
for (const w of x) if (y.has(w)) shared++;
|
|
38
|
+
const union = x.size + y.size - shared;
|
|
39
|
+
return { score: union ? shared / union : 0, shared };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const goalsAlike = (a: string, b: string) => {
|
|
43
|
+
const { score, shared } = goalSimilarity(a, b);
|
|
44
|
+
return score >= 0.5 && shared >= 2;
|
|
45
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { startClaimsLoop, claimsTick } from "./loop";
|
|
2
|
+
export { acceptExchange, answerExchange, selfMachine } from "./exchange";
|
|
3
|
+
export { liveClaim, liveClaims } from "./store";
|
|
4
|
+
export { findOverlaps, overlapBetween } from "./overlap";
|
|
5
|
+
export { describeClaims } from "./describe";
|
|
6
|
+
export { claimsShared, machineId, machineName } from "./machine";
|
|
7
|
+
export type { Claim, Overlap, ClaimWire } from "./types";
|
|
8
|
+
export { endOutside, registerOutside } from "./outside";
|
|
9
|
+
export { claimsSnapshot, refreshOverlapCache } from "./overlapCache";
|
|
10
|
+
export { claimsView } from "./view";
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { collectLocal } from "./collectLocal";
|
|
2
|
+
import { selfMachine, syncPeers } from "./exchange";
|
|
3
|
+
import { claimsShared } from "./machine";
|
|
4
|
+
import { dropViaInactive } from "./peers";
|
|
5
|
+
import { notifyOverlaps } from "./notifyOverlaps";
|
|
6
|
+
import { handleMerged } from "./onMerged";
|
|
7
|
+
import { refreshOverlapCache } from "./overlapCache";
|
|
8
|
+
import { purgeExpired } from "./store";
|
|
9
|
+
import { CLAIMS_SYNC_MS } from "./types";
|
|
10
|
+
|
|
11
|
+
export async function claimsTick(
|
|
12
|
+
log: (line: string) => void = () => {}
|
|
13
|
+
): Promise<void> {
|
|
14
|
+
if (!claimsShared()) return;
|
|
15
|
+
try {
|
|
16
|
+
await collectLocal(selfMachine());
|
|
17
|
+
await syncPeers(log);
|
|
18
|
+
await notifyOverlaps();
|
|
19
|
+
await handleMerged();
|
|
20
|
+
dropViaInactive();
|
|
21
|
+
purgeExpired();
|
|
22
|
+
refreshOverlapCache();
|
|
23
|
+
} catch (err) {
|
|
24
|
+
log(`claims: ${(err as Error)?.message ?? err}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// One tick a minute. Nothing here may throw into the server.
|
|
29
|
+
export function startClaimsLoop(
|
|
30
|
+
log: (line: string) => void = (l) => console.log(l)
|
|
31
|
+
): { stop: () => void } {
|
|
32
|
+
let stopped = false;
|
|
33
|
+
let timer: NodeJS.Timeout | null = null;
|
|
34
|
+
const next = () => {
|
|
35
|
+
if (stopped) return;
|
|
36
|
+
timer = setTimeout(run, CLAIMS_SYNC_MS);
|
|
37
|
+
timer.unref?.();
|
|
38
|
+
};
|
|
39
|
+
const run = async () => {
|
|
40
|
+
await claimsTick(log);
|
|
41
|
+
next();
|
|
42
|
+
};
|
|
43
|
+
timer = setTimeout(run, 5000);
|
|
44
|
+
timer.unref?.();
|
|
45
|
+
return {
|
|
46
|
+
stop: () => {
|
|
47
|
+
stopped = true;
|
|
48
|
+
if (timer) clearTimeout(timer);
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import os from "os";
|
|
3
|
+
import { getSetting, putSetting } from "../settings-store";
|
|
4
|
+
|
|
5
|
+
const ID_KEY = "claims.machine_id";
|
|
6
|
+
|
|
7
|
+
export function machineId(): string {
|
|
8
|
+
const have = getSetting(ID_KEY);
|
|
9
|
+
if (have) return have;
|
|
10
|
+
const id = randomUUID();
|
|
11
|
+
putSetting(ID_KEY, id);
|
|
12
|
+
return id;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function machineName(): string {
|
|
16
|
+
const set = getSetting("claims.machine_name")?.trim();
|
|
17
|
+
return (set || os.hostname().split(".")[0] || "machine").slice(0, 60);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Off: this machine neither sends nor takes claims.
|
|
21
|
+
export const claimsShared = () => getSetting("claims.share") !== "off";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The strong overlaps that touch a session on this machine, each seen from
|
|
2
|
+
// that session's side. Pure over the claims it is given.
|
|
3
|
+
|
|
4
|
+
import { findOverlaps } from "./overlap";
|
|
5
|
+
import { pairKey, whoYields } from "./yield";
|
|
6
|
+
import type { Claim } from "./types";
|
|
7
|
+
|
|
8
|
+
export interface SessionOverlap {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
pairKey: string;
|
|
11
|
+
mine: Claim;
|
|
12
|
+
other: Claim;
|
|
13
|
+
shared: string[];
|
|
14
|
+
kinds: ("branch" | "files" | "goal")[];
|
|
15
|
+
// This session is the one asked to give way.
|
|
16
|
+
yields: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sessionOverlaps(
|
|
20
|
+
self: string,
|
|
21
|
+
claims: Claim[]
|
|
22
|
+
): SessionOverlap[] {
|
|
23
|
+
const out: SessionOverlap[] = [];
|
|
24
|
+
for (const o of findOverlaps(claims)) {
|
|
25
|
+
if (o.strength !== "strong") continue;
|
|
26
|
+
const yielder = whoYields(o.a, o.b);
|
|
27
|
+
for (const [mine, other] of [
|
|
28
|
+
[o.a, o.b],
|
|
29
|
+
[o.b, o.a],
|
|
30
|
+
] as const)
|
|
31
|
+
if (mine.origin === self && mine.sessionId)
|
|
32
|
+
out.push({
|
|
33
|
+
sessionId: mine.sessionId,
|
|
34
|
+
pairKey: pairKey(o.a, o.b),
|
|
35
|
+
mine,
|
|
36
|
+
other,
|
|
37
|
+
shared: o.shared,
|
|
38
|
+
kinds: o.kinds,
|
|
39
|
+
yields: yielder === mine,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Each sync: tells a local session about a strong overlap it is in. An
|
|
2
|
+
// overlap must hold on two syncs in a row, and is told again only when the
|
|
3
|
+
// shared set grows. Both machines run this for their own sessions, so
|
|
4
|
+
// nothing is sent across machines. Never pauses or stops anything.
|
|
5
|
+
|
|
6
|
+
import { getDb } from "../db";
|
|
7
|
+
import { sendMessage } from "../bus";
|
|
8
|
+
import { machineId } from "./machine";
|
|
9
|
+
import { sessionOverlaps, type SessionOverlap } from "./mine";
|
|
10
|
+
import { sessionMessage } from "./overlapText";
|
|
11
|
+
import { liveClaims } from "./store";
|
|
12
|
+
|
|
13
|
+
export const HOLD_SYNCS = 2;
|
|
14
|
+
// A told pair that has not overlapped for this long is forgotten.
|
|
15
|
+
const FORGET_MS = 60 * 60_000;
|
|
16
|
+
|
|
17
|
+
export type Tell = (sessionId: string, body: string) => Promise<void>;
|
|
18
|
+
|
|
19
|
+
export const tellSession: Tell = async (sessionId, body) => {
|
|
20
|
+
await sendMessage({
|
|
21
|
+
fromId: null,
|
|
22
|
+
to: sessionId,
|
|
23
|
+
body,
|
|
24
|
+
fromLabel: "AgentOS",
|
|
25
|
+
origin: { kind: "system", label: "Overlap", body },
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
interface Notice {
|
|
30
|
+
pair_key: string;
|
|
31
|
+
hits: number;
|
|
32
|
+
shared: string;
|
|
33
|
+
told_at: number | null;
|
|
34
|
+
seen_at: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const parse = (raw: string): string[] => {
|
|
38
|
+
try {
|
|
39
|
+
const v = JSON.parse(raw);
|
|
40
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
41
|
+
} catch {
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export async function notifyOverlaps(
|
|
47
|
+
tell: Tell = tellSession,
|
|
48
|
+
now = Date.now()
|
|
49
|
+
): Promise<number> {
|
|
50
|
+
const db = getDb();
|
|
51
|
+
const byPair = new Map<string, SessionOverlap[]>();
|
|
52
|
+
for (const o of sessionOverlaps(machineId(), liveClaims({ now })))
|
|
53
|
+
byPair.set(o.pairKey, [...(byPair.get(o.pairKey) ?? []), o]);
|
|
54
|
+
const get = db.prepare(`SELECT * FROM claim_notices WHERE pair_key = ?`);
|
|
55
|
+
const save = db.prepare(
|
|
56
|
+
`INSERT INTO claim_notices (pair_key, hits, shared, told_at, seen_at)
|
|
57
|
+
VALUES (@key, @hits, @shared, @told, @now)
|
|
58
|
+
ON CONFLICT(pair_key) DO UPDATE SET hits = @hits, shared = @shared,
|
|
59
|
+
told_at = @told, seen_at = @now`
|
|
60
|
+
);
|
|
61
|
+
let told = 0;
|
|
62
|
+
for (const [key, sides] of byPair) {
|
|
63
|
+
const row = get.get(key) as Notice | undefined;
|
|
64
|
+
const hits = (row?.hits ?? 0) + 1;
|
|
65
|
+
const shared = sides[0].shared;
|
|
66
|
+
const known = new Set(parse(row?.shared ?? "[]"));
|
|
67
|
+
const grew = !!row?.told_at && shared.some((f) => !known.has(f));
|
|
68
|
+
const due = hits >= HOLD_SYNCS && (!row?.told_at || grew);
|
|
69
|
+
let delivered = false;
|
|
70
|
+
if (due)
|
|
71
|
+
for (const side of sides)
|
|
72
|
+
try {
|
|
73
|
+
await tell(side.sessionId, sessionMessage(side));
|
|
74
|
+
delivered = true;
|
|
75
|
+
} catch {
|
|
76
|
+
// Left untold when nobody got it: the next sync tries again.
|
|
77
|
+
}
|
|
78
|
+
if (delivered) told++;
|
|
79
|
+
save.run({
|
|
80
|
+
key,
|
|
81
|
+
hits,
|
|
82
|
+
shared: delivered ? JSON.stringify(shared) : (row?.shared ?? "[]"),
|
|
83
|
+
told: delivered ? now : (row?.told_at ?? null),
|
|
84
|
+
now,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// Consecutive means consecutive: a pair that dropped out starts again.
|
|
88
|
+
const rows = db.prepare(`SELECT * FROM claim_notices`).all() as Notice[];
|
|
89
|
+
for (const r of rows) {
|
|
90
|
+
if (byPair.has(r.pair_key)) continue;
|
|
91
|
+
if (!r.told_at || now - r.seen_at > FORGET_MS)
|
|
92
|
+
db.prepare(`DELETE FROM claim_notices WHERE pair_key = ?`).run(
|
|
93
|
+
r.pair_key
|
|
94
|
+
);
|
|
95
|
+
else if (r.hits)
|
|
96
|
+
db.prepare(`UPDATE claim_notices SET hits = 0 WHERE pair_key = ?`).run(
|
|
97
|
+
r.pair_key
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return told;
|
|
101
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// A winner of an overlap merged: the base moved under the session that gave
|
|
2
|
+
// way. One with a PR is left to rebaseConflicting once GitHub flags a
|
|
3
|
+
// conflict. One without gets a single message, and a refreshBase when it is
|
|
4
|
+
// idle with nothing uncommitted (refreshBase never loses work). Nothing here
|
|
5
|
+
// pauses or stops a session.
|
|
6
|
+
|
|
7
|
+
import { getDb } from "../db";
|
|
8
|
+
import { git } from "../git-exec";
|
|
9
|
+
import { statusOf } from "../orchestrator/facts";
|
|
10
|
+
import { getSessionRow } from "../schedules/store";
|
|
11
|
+
import { refreshBase, type RefreshTarget } from "../tasks/refresh-base";
|
|
12
|
+
import { expandHome } from "../tasks/session";
|
|
13
|
+
import { machineId } from "./machine";
|
|
14
|
+
import { mergedMessage } from "./overlapText";
|
|
15
|
+
import { claimById } from "./store";
|
|
16
|
+
import { tellSession, type Tell } from "./notifyOverlaps";
|
|
17
|
+
import { winner } from "./yield";
|
|
18
|
+
|
|
19
|
+
export interface MergedDeps {
|
|
20
|
+
tell: Tell;
|
|
21
|
+
idle: (sessionId: string) => Promise<boolean>;
|
|
22
|
+
clean: (dir: string) => Promise<boolean>;
|
|
23
|
+
refresh: (t: RefreshTarget) => Promise<unknown>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const realMerged: MergedDeps = {
|
|
27
|
+
tell: tellSession,
|
|
28
|
+
idle: async (id) => {
|
|
29
|
+
const s = getSessionRow(id);
|
|
30
|
+
return !!s && (await statusOf(s)).status === "idle";
|
|
31
|
+
},
|
|
32
|
+
clean: async (dir) =>
|
|
33
|
+
!(await git(dir, ["status", "--porcelain"], { timeout: 5000 })).trim(),
|
|
34
|
+
refresh: refreshBase,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
interface Row {
|
|
38
|
+
pair_key: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function handleMerged(
|
|
42
|
+
deps: MergedDeps = realMerged,
|
|
43
|
+
now = Date.now()
|
|
44
|
+
): Promise<number> {
|
|
45
|
+
const db = getDb();
|
|
46
|
+
const self = machineId();
|
|
47
|
+
const rows = db
|
|
48
|
+
.prepare(
|
|
49
|
+
`SELECT pair_key FROM claim_notices WHERE told_at IS NOT NULL AND merged_at IS NULL`
|
|
50
|
+
)
|
|
51
|
+
.all() as Row[];
|
|
52
|
+
const done = db.prepare(
|
|
53
|
+
`UPDATE claim_notices SET merged_at = ? WHERE pair_key = ?`
|
|
54
|
+
);
|
|
55
|
+
let handled = 0;
|
|
56
|
+
for (const { pair_key } of rows) {
|
|
57
|
+
const [a, b] = pair_key.split("|").map((id) => claimById(id));
|
|
58
|
+
if (!a || !b) continue;
|
|
59
|
+
const gone = [a, b].find((c) => c.released === "merged");
|
|
60
|
+
const mine = [a, b].find(
|
|
61
|
+
(c) => !c.released && c.origin === self && c.sessionId
|
|
62
|
+
);
|
|
63
|
+
if (!gone || !mine || winner(gone, mine) !== gone) continue;
|
|
64
|
+
try {
|
|
65
|
+
const s = getSessionRow(mine.sessionId!);
|
|
66
|
+
// A session with a PR is the rebase pipeline's.
|
|
67
|
+
if (s && !s.pr_url && s.pr_status !== "open") {
|
|
68
|
+
await deps.tell(s.id, mergedMessage(gone, s.base_branch ?? mine.base));
|
|
69
|
+
const dir = s.worktree_path ? expandHome(s.worktree_path) : null;
|
|
70
|
+
if (dir && (await deps.idle(s.id)) && (await deps.clean(dir)))
|
|
71
|
+
await deps.refresh({
|
|
72
|
+
name: s.name,
|
|
73
|
+
worktree: dir,
|
|
74
|
+
branch: s.branch_name,
|
|
75
|
+
base: s.base_branch,
|
|
76
|
+
});
|
|
77
|
+
handled++;
|
|
78
|
+
}
|
|
79
|
+
done.run(now, pair_key);
|
|
80
|
+
} catch {
|
|
81
|
+
// Tried again next sync.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return handled;
|
|
85
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Sessions AgentOS didn't start register a claim themselves, through
|
|
2
|
+
// `aos claim` or the hook. The CLI sends what git said in its own folder.
|
|
3
|
+
|
|
4
|
+
import { repoKeyOfRemote } from "../git-remote";
|
|
5
|
+
import { machineId, machineName } from "./machine";
|
|
6
|
+
import {
|
|
7
|
+
liveClaim,
|
|
8
|
+
newOutsideId,
|
|
9
|
+
releaseOutside,
|
|
10
|
+
upsertOutside,
|
|
11
|
+
} from "./store";
|
|
12
|
+
import type { Claim, OutsideBody } from "./types";
|
|
13
|
+
|
|
14
|
+
const self = () => ({ id: machineId(), name: machineName() });
|
|
15
|
+
|
|
16
|
+
// Null when the remote isn't one a claim can be keyed by, or the id is not
|
|
17
|
+
// this machine's outside claim.
|
|
18
|
+
export function registerOutside(b: OutsideBody): { claim: Claim } | null {
|
|
19
|
+
const repo = repoKeyOfRemote(b.remote);
|
|
20
|
+
if (!repo) return null;
|
|
21
|
+
const id = b.id ?? newOutsideId();
|
|
22
|
+
const prior = liveClaim(id);
|
|
23
|
+
const ok = upsertOutside(self(), {
|
|
24
|
+
id,
|
|
25
|
+
sessionName: b.name ?? null,
|
|
26
|
+
repo,
|
|
27
|
+
branch: b.branch ?? null,
|
|
28
|
+
base: b.base ?? null,
|
|
29
|
+
goal: b.goal || prior?.goal || "outside session",
|
|
30
|
+
files: b.files,
|
|
31
|
+
});
|
|
32
|
+
const claim = ok ? liveClaim(id) : null;
|
|
33
|
+
return claim ? { claim } : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const endOutside = (id: string) => releaseOutside(machineId(), id);
|