@nledford/omp-coord 0.3.1 → 0.3.2
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/.omp-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +16 -1
- package/docs/commands.md +3 -2
- package/docs/configuration.md +1 -1
- package/docs/queues.md +1 -1
- package/package.json +1 -1
- package/src/adapters/cli/cli.ts +7 -3
- package/src/adapters/omp/api.ts +4 -0
- package/src/adapters/omp/extension.ts +55 -18
- package/src/adapters/sqlite/store.ts +24 -5
- package/src/app/coordinator.ts +18 -2
- package/src/config.ts +0 -7
- package/src/domain/agents.ts +4 -0
- package/src/domain/commands.ts +0 -24
- package/src/domain/model.ts +7 -0
- package/src/domain/sessions.ts +31 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"name": "omp-coord",
|
|
8
8
|
"source": "./",
|
|
9
9
|
"description": "Shared subagent caps, build and test queues, claims, pause, a decision log and messages across omp sessions working in one project on one machine.",
|
|
10
|
-
"version": "0.3.
|
|
10
|
+
"version": "0.3.2",
|
|
11
11
|
"author": { "name": "Nathaniel Ledford" },
|
|
12
12
|
"homepage": "https://github.com/nledford/omp-coord",
|
|
13
13
|
"repository": "https://github.com/nledford/omp-coord",
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@ Add each user-visible change under Unreleased as it lands, in one of: Added, Cha
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.2] - 2026-10-28
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- **Breaking:** the extension no longer starts agents' bash commands with `export OMP_COORD_AGENT=<agent id>;`, and no longer sets `OMP_COORD_AGENT` at all. The rewritten command showed up in the conversation, and agents copied the prefix into later calls. Instead `omp-coord run` finds the agent from `PI_SESSION_ID`, the id omp gives each bash call for its agent's conversation, which the extension records on the board (the main session as `Main`, each subagent when its run starts, dropped when it ends). A script that read `OMP_COORD_AGENT` inside an agent's command must read `PI_SESSION_ID` or ask the board instead; `OMP_COORD_AGENT=<name> omp-coord run …` still names the agent for a job started where omp gives no `PI_SESSION_ID`, or one the board doesn't know. Only commands a `queue` rule matches are rewritten now, as before 0.2.3; omp's `bash.patterns` rules and other extensions see every other command as the agent wrote it.
|
|
14
|
+
|
|
15
|
+
### Deprecated
|
|
16
|
+
|
|
17
|
+
- `tagAgentCommands` does nothing and is still accepted, so existing `.omp-coord.json` files keep loading; remove it. Queued jobs record their agent whether it was on or not.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- A job queued by an agent now records the agent whether or not `tagAgentCommands` was on. A board made by an older omp-coord gains the two new columns when this version opens it; while a session of the scope (or an older `omp-coord run`) still uses the old version, its saves drop the recorded ids: the main session's comes back within 2 s, but a subagent's only at its next run, so while any older omp-coord is in use a subagent's jobs may show no agent (display only). omp gives bash calls `PI_SESSION_ID` from 18.12.4 on (the plugin still runs on 18.12.0 to 18.12.3, where jobs record no agent unless a script sets `OMP_COORD_AGENT`).
|
|
22
|
+
|
|
9
23
|
## [0.3.1] - 2026-10-27
|
|
10
24
|
|
|
11
25
|
### Fixed
|
|
@@ -82,7 +96,8 @@ Add each user-visible change under Unreleased as it lands, in one of: Added, Cha
|
|
|
82
96
|
- An SQLite event log per scope, with retention.
|
|
83
97
|
- Packaging as an omp plugin, with a marketplace catalog; queued commands run on omp's own Bun.
|
|
84
98
|
|
|
85
|
-
[Unreleased]: https://github.com/nledford/omp-coord/compare/v0.3.
|
|
99
|
+
[Unreleased]: https://github.com/nledford/omp-coord/compare/v0.3.2...HEAD
|
|
100
|
+
[0.3.2]: https://github.com/nledford/omp-coord/compare/v0.3.1...v0.3.2
|
|
86
101
|
[0.3.1]: https://github.com/nledford/omp-coord/compare/v0.3.0...v0.3.1
|
|
87
102
|
[0.3.0]: https://github.com/nledford/omp-coord/compare/v0.2.3...v0.3.0
|
|
88
103
|
[0.2.3]: https://github.com/nledford/omp-coord/compare/v0.2.2...v0.2.3
|
package/docs/commands.md
CHANGED
|
@@ -61,7 +61,7 @@ Every time is milliseconds since the epoch (`Date.now()`); a session is its lett
|
|
|
61
61
|
| `line[]` | Sessions waiting for a slot: `pool`, `session`, `position` (from 1), `since`, `heldUntil` (`null` unless a freed slot is held for it). |
|
|
62
62
|
| `agents[]` | One per agent holding a slot, by session letter then `since`: `id` (`null` for a spawn with no key and no first run yet), `type`, `parent` (`Main` or an agent id), `session`, `pool` (`null`: outside the caps), `state` (`starting` or `running`), `since`. |
|
|
63
63
|
| `claims[]` | `kind` (`branch`, `worktree`, `pr`), `target`, `session`, `agent`, `note`, `since`. |
|
|
64
|
-
| `queues[]` | `name`, `order`, `slots` (now), `configuredSlots`, `running[]` and `waiting[]` (in the order they will start), each job: `id`, `session`, `owner` (the owning session's id, or `null` outside omp), `lane`, `priority`, `key` (or `null`), `command`, `agent` (omp's id of the agent whose command queued it, `"Main"` for the main agent; `null`
|
|
64
|
+
| `queues[]` | `name`, `order`, `slots` (now), `configuredSlots`, `running[]` and `waiting[]` (in the order they will start), each job: `id`, `session`, `owner` (the owning session's id, or `null` outside omp), `lane`, `priority`, `key` (or `null`), `command`, `agent` (omp's id of the agent whose command queued it, `"Main"` for the main agent; `null` for a job started outside an agent's bash call, on omp older than 18.12.4 (which sets no `PI_SESSION_ID`), and while any session of the scope runs an omp-coord older than this one, whose saves drop the ids the agent is found by), `state`, `queuedAt`, `startedAt` (`null` while waiting), `position` (from 1 while waiting, `null` while running). |
|
|
65
65
|
| `notes[]` | The latest three decisions, oldest first: `id`, `at`, `session` (the letter when it was written, kept after that session ends; `null` outside omp), `author`, `text`. |
|
|
66
66
|
|
|
67
67
|
```json
|
|
@@ -102,5 +102,6 @@ Exit codes: `0` success, the command's own for `run`, `75` a job not run, `124`
|
|
|
102
102
|
| `OMP_COORD_SESSION` | the extension | The session that owns jobs started from its agents' commands. |
|
|
103
103
|
| `OMP_COORD_PORTS` | the extension | The session's port block, e.g. `18700-18799`, when `resources.ports` is set. |
|
|
104
104
|
| `OMP_COORD_SCRATCH` | the extension | The session's scratch folder, when `resources.scratch` is set. |
|
|
105
|
-
| `
|
|
105
|
+
| `PI_SESSION_ID` | omp | The id of the conversation an agent's bash call belongs to. `omp-coord run` looks up which agent that is (the extension records it on the board: `Main` for the main session, the subagent's id for a subagent) and records it on the job. |
|
|
106
|
+
| `OMP_COORD_AGENT` | you | The agent to record on a job started where omp gave no `PI_SESSION_ID` (a script), or one the board doesn't know. The extension no longer sets it. |
|
|
106
107
|
| `OMP_COORD_JOB`, `OMP_COORD_QUEUE` | `omp-coord run` | The running job's id and queue, for nested runs. |
|
package/docs/configuration.md
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
| `queues.<name>.onDuplicate` | `"refuse"` | `"refuse"` or `"supersede"` a waiting job with the same key. |
|
|
23
23
|
| `queues.<name>.timeoutMinutes` | none | Stop a job's command after this long: SIGTERM to its process group, then SIGKILL to whatever of it is left 10 s later; it exits 124. `--timeout` overrides it per job. |
|
|
24
24
|
| `queues.<name>.loadSlots` | `[]` | `[{ "above": 150, "slots": 1 }]`: fewer slots under load; `0` holds the queue. |
|
|
25
|
-
| `tagAgentCommands` |
|
|
25
|
+
| `tagAgentCommands` | none | Deprecated and ignored: every queued job records the agent that queued it now (on omp 18.12.4 or later), without rewriting commands. Accepted so existing files keep loading; remove it. |
|
|
26
26
|
| `resources.ports` | none | `{ "from", "to", "perSession", "perAgent" (10) }`: each session gets a block of `perSession` ports, each agent a block of `perAgent` from its session's. |
|
|
27
27
|
| `resources.scratch` | none | Root of the scratch folders: `<scratch>/<letter>` per session, `<scratch>/<letter>/<agent id>` per agent. Relative to the config file. |
|
|
28
28
|
| `history.retentionDays` | `30` | Events and decision-log notes older than this are deleted. |
|
package/docs/queues.md
CHANGED
|
@@ -49,6 +49,6 @@ A command that puts its work in the background (`cargo test &`) holds the slot o
|
|
|
49
49
|
|
|
50
50
|
When an agent's bash command matches a `queue` rule under `commands`, the extension rewrites it to run `omp-coord run <queue> --owner <session> -- sh -c '<command>'` with omp's own binary as Bun (`BUN_BE_BUN=1`), so it needs no separate Bun install. The waiting happens inside the bash call, which counts against the bash tool's timeout: agents should run long jobs as background (`async`) bash calls. The rewritten command runs in `sh`, so shell variables the agent set earlier without exporting them are not visible to it.
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
`omp-coord run` records the agent whose command it is on each job, so queue lines read `session A/WaveImport` instead of only `session A`. omp (18.12.4 or later) gives every bash call the id of its agent's conversation (`PI_SESSION_ID`), and the extension records on the board which agent that is, so the command text is never touched: queue rules, omp's `bash.patterns` rules and other extensions' rewrites see exactly what the agent wrote. A script can name the agent itself with `OMP_COORD_AGENT`.
|
|
53
53
|
|
|
54
54
|
A command matching a `refuse` rule is refused with the rule's reason.
|
package/package.json
CHANGED
package/src/adapters/cli/cli.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
3
|
* omp-coord command line: runs commands through the shared build and test queues, from `just` recipes, scripts,
|
|
4
|
-
* terminals, or agents' bash calls (which the extension rewrites to go through here
|
|
4
|
+
* terminals, or agents' bash calls (which the extension rewrites to go through here when a rule queues them), and
|
|
5
|
+
* reports on the scope.
|
|
5
6
|
*
|
|
6
7
|
* omp-coord run <queue> [--lane L] [--priority N] [--key K] [--owner SESSION] [--timeout 90m] -- <command> [args…]
|
|
7
8
|
* omp-coord status [--agents | --json] | stats [days] | interrupted [hours] | notes [count] | note <text…>
|
|
@@ -24,6 +25,7 @@ import { EXIT_TIMED_OUT, runJob } from "../../app/job-runner.ts";
|
|
|
24
25
|
import { type Config, ConfigError, findConfigFile, loadConfig } from "../../config.ts";
|
|
25
26
|
import { statusView } from "../../domain/overview.ts";
|
|
26
27
|
import { MAX_PRIORITY } from "../../domain/queues.ts";
|
|
28
|
+
import { agentOfOmpSession } from "../../domain/sessions.ts";
|
|
27
29
|
import { osLoad } from "../os/load.ts";
|
|
28
30
|
import { groupRunning } from "../os/process-group.ts";
|
|
29
31
|
import { BoardStore, stateDir } from "../sqlite/store.ts";
|
|
@@ -243,14 +245,16 @@ async function run(args: string[]): Promise<number> {
|
|
|
243
245
|
mkdirSync(join(dir, "s"), { recursive: true, mode: 0o700 });
|
|
244
246
|
const id = randomBytes(4).toString("hex");
|
|
245
247
|
const address = join(dir, "s", `job-${id}.sock`);
|
|
248
|
+
// omp gives each bash call its agent's session id; the extension recorded which agent that is. A script outside
|
|
249
|
+
// omp (or one run before the extension bound the id) may name the agent itself.
|
|
250
|
+
const agent = agentOfOmpSession(store.read(), process.env.PI_SESSION_ID ?? "") || process.env.OMP_COORD_AGENT;
|
|
246
251
|
|
|
247
252
|
return runJob(
|
|
248
253
|
{
|
|
249
254
|
id,
|
|
250
255
|
queue,
|
|
251
256
|
owner: values.owner || process.env.OMP_COORD_SESSION || null,
|
|
252
|
-
|
|
253
|
-
...(process.env.OMP_COORD_AGENT ? { agent: process.env.OMP_COORD_AGENT } : {}),
|
|
257
|
+
...(agent ? { agent } : {}),
|
|
254
258
|
...(values.lane === undefined ? {} : { lane: values.lane }),
|
|
255
259
|
priority,
|
|
256
260
|
...(values.key === undefined ? {} : { key: values.key }),
|
package/src/adapters/omp/api.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface ExtensionContext {
|
|
|
19
19
|
mode: "tui" | "rpc" | "json" | "print";
|
|
20
20
|
hasUI: boolean;
|
|
21
21
|
agent: AgentIdentity;
|
|
22
|
+
/** omp's id for this agent's own conversation: the `PI_SESSION_ID` its bash calls carry (read-only). */
|
|
23
|
+
sessionManager?: { getSessionId?(): string };
|
|
22
24
|
ui: {
|
|
23
25
|
notify(message: string, type?: "info" | "warning" | "error"): void;
|
|
24
26
|
setStatus(key: string, text: string | undefined): void;
|
|
@@ -52,6 +54,8 @@ export type ToolCallResult = Block | { input: Record<string, unknown> };
|
|
|
52
54
|
export interface Events {
|
|
53
55
|
session_start: (event: unknown, ctx: ExtensionContext) => unknown;
|
|
54
56
|
session_shutdown: (event: unknown, ctx: ExtensionContext) => unknown;
|
|
57
|
+
session_switch: (event: unknown, ctx: ExtensionContext) => unknown;
|
|
58
|
+
session_branch: (event: unknown, ctx: ExtensionContext) => unknown;
|
|
55
59
|
agent_start: (event: unknown, ctx: ExtensionContext) => unknown;
|
|
56
60
|
agent_end: (event: unknown, ctx: ExtensionContext) => unknown;
|
|
57
61
|
before_subagent_spawn: (event: SpawnEvent, ctx: ExtensionContext) => Block | Promise<Block>;
|
|
@@ -17,7 +17,7 @@ import { Coordinator } from "../../app/coordinator.ts";
|
|
|
17
17
|
import type { Listener } from "../../app/ports.ts";
|
|
18
18
|
import { type Config, ConfigError, findConfigFile, loadConfig } from "../../config.ts";
|
|
19
19
|
import { spawnAllowance } from "../../domain/agents.ts";
|
|
20
|
-
import { decideCommand
|
|
20
|
+
import { decideCommand } from "../../domain/commands.ts";
|
|
21
21
|
import type { Claim } from "../../domain/model.ts";
|
|
22
22
|
import { osLoad } from "../os/load.ts";
|
|
23
23
|
import { BoardStore, stateDir } from "../sqlite/store.ts";
|
|
@@ -55,6 +55,8 @@ interface Runtime {
|
|
|
55
55
|
listener: Listener;
|
|
56
56
|
sessionId: string;
|
|
57
57
|
label: string;
|
|
58
|
+
/** omp's id for the main conversation, as agents' bash calls carry it (`PI_SESSION_ID`). */
|
|
59
|
+
ompSessionId: string | undefined;
|
|
58
60
|
/** The main session's binding and context: messages for the session go through them. */
|
|
59
61
|
pi: ExtensionAPI;
|
|
60
62
|
ctx: ExtensionContext;
|
|
@@ -98,14 +100,24 @@ export default function ompCoord(pi: ExtensionAPI): void {
|
|
|
98
100
|
shared.bindings.set(ctx.agent.id, pi);
|
|
99
101
|
return;
|
|
100
102
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
103
|
+
const rt = shared.runtime;
|
|
104
|
+
if (rt) {
|
|
105
|
+
rt.pi = pi;
|
|
106
|
+
rt.ctx = ctx;
|
|
107
|
+
syncMainSession(rt, ctx);
|
|
104
108
|
return;
|
|
105
109
|
}
|
|
106
110
|
await start(pi, ctx);
|
|
107
111
|
});
|
|
108
112
|
|
|
113
|
+
// omp keeps one process across /new, /resume and /fork, giving the main conversation a new id without a new
|
|
114
|
+
// `session_start`: its bash calls carry that id from now on.
|
|
115
|
+
for (const event of ["session_switch", "session_branch"] as const)
|
|
116
|
+
pi.on(event, (_event, ctx) => {
|
|
117
|
+
const rt = shared.runtime;
|
|
118
|
+
if (rt && ctx.agent.kind === "main") syncMainSession(rt, ctx);
|
|
119
|
+
});
|
|
120
|
+
|
|
109
121
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
110
122
|
if (ctx.agent.kind !== "main") {
|
|
111
123
|
if (shared.bindings.get(ctx.agent.id) === pi) shared.bindings.delete(ctx.agent.id);
|
|
@@ -149,6 +161,7 @@ export default function ompCoord(pi: ExtensionAPI): void {
|
|
|
149
161
|
const rt = shared.runtime;
|
|
150
162
|
if (!rt || !isTrackedSubagent(ctx)) return;
|
|
151
163
|
guarded(rt, "record an agent run", () => {
|
|
164
|
+
const ompSessionId = ompSessionIdOf(ctx);
|
|
152
165
|
const parentId = ctx.agent.parentId ?? "Main";
|
|
153
166
|
const count = rt.coordinator.agentStarted({
|
|
154
167
|
sessionId: rt.sessionId,
|
|
@@ -157,6 +170,7 @@ export default function ompCoord(pi: ExtensionAPI): void {
|
|
|
157
170
|
parentIsSubagent: parentId !== "Main",
|
|
158
171
|
agentType: ctx.agent.name.toLowerCase(),
|
|
159
172
|
evalWorker: rt.evalAgents.has(ctx.agent.id),
|
|
173
|
+
...(ompSessionId === undefined ? {} : { ompSessionId }),
|
|
160
174
|
});
|
|
161
175
|
refreshQueue(rt);
|
|
162
176
|
if (count && count.used > count.cap) {
|
|
@@ -183,6 +197,8 @@ export default function ompCoord(pi: ExtensionAPI): void {
|
|
|
183
197
|
if (!rt || event.toolName !== "bash") return undefined;
|
|
184
198
|
const command = event.input.command;
|
|
185
199
|
if (typeof command !== "string") return undefined;
|
|
200
|
+
// the id must be on the board before the command can reach `omp-coord run`, whatever event announced a switch
|
|
201
|
+
if (ctx.agent.kind === "main") syncMainSession(rt, ctx);
|
|
186
202
|
|
|
187
203
|
try {
|
|
188
204
|
const cwd = typeof event.input.cwd === "string" ? resolve(ctx.cwd, event.input.cwd) : ctx.cwd;
|
|
@@ -205,20 +221,15 @@ export default function ompCoord(pi: ExtensionAPI): void {
|
|
|
205
221
|
notify(rt, `omp-coord could not check claims: ${describe(error)}`, "error");
|
|
206
222
|
}
|
|
207
223
|
|
|
208
|
-
|
|
209
|
-
const own = rt.config.tagAgentCommands ? withoutAgentExports(command) : command;
|
|
210
|
-
const decision = decideCommand(rt.config.commands, own, {
|
|
224
|
+
const decision = decideCommand(rt.config.commands, command, {
|
|
211
225
|
...RUNNER,
|
|
212
226
|
owner: rt.sessionId,
|
|
213
227
|
config: rt.config.source,
|
|
214
228
|
});
|
|
215
229
|
if (decision.kind === "refuse") return { block: true, reason: `omp-coord: ${decision.reason}` };
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
// bash.patterns rules and other extensions' rewrites. Off, the hook changes only queued commands, as before.
|
|
220
|
-
if (rt.config.tagAgentCommands) return { input: { ...event.input, command: withAgent(ctx.agent.id, run) } };
|
|
221
|
-
if (decision.kind === "queue") return { input: { ...event.input, command: run } };
|
|
230
|
+
// Only a queued command is rewritten. Which agent runs it needs no rewrite: omp gives every bash call its agent's
|
|
231
|
+
// session id (`PI_SESSION_ID`), and the board maps that id to the agent (`session_start`, `agent_start`).
|
|
232
|
+
if (decision.kind === "queue") return { input: { ...event.input, command: decision.command } };
|
|
222
233
|
return undefined;
|
|
223
234
|
});
|
|
224
235
|
|
|
@@ -476,9 +487,10 @@ async function start(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
|
476
487
|
? "While this is the only session, spawns past the cap wait in omp's queue and start as slots free; with others, they are refused and this session holds a place in line."
|
|
477
488
|
: `Spawns past the cap are refused: ${typeof handle === "string" ? handle : "omp gave the extension no settings scope"}.`;
|
|
478
489
|
}
|
|
490
|
+
const ompSessionId = ompSessionIdOf(ctx);
|
|
479
491
|
let joined: Pick<Runtime, "store" | "coordinator" | "listener" | "sessionId" | "label">;
|
|
480
492
|
try {
|
|
481
|
-
joined = openSession(config, ctx.cwd);
|
|
493
|
+
joined = openSession(config, ctx.cwd, ompSessionId);
|
|
482
494
|
} catch (error) {
|
|
483
495
|
shared.inactive = `omp-coord is off: ${describe(error)}`;
|
|
484
496
|
ctx.ui.notify(shared.inactive, "error");
|
|
@@ -488,6 +500,7 @@ async function start(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
|
488
500
|
const rt: Runtime = {
|
|
489
501
|
config,
|
|
490
502
|
...joined,
|
|
503
|
+
ompSessionId,
|
|
491
504
|
pi,
|
|
492
505
|
ctx,
|
|
493
506
|
timer: undefined,
|
|
@@ -543,6 +556,7 @@ async function start(pi: ExtensionAPI, ctx: ExtensionContext): Promise<void> {
|
|
|
543
556
|
function openSession(
|
|
544
557
|
config: Config,
|
|
545
558
|
cwd: string,
|
|
559
|
+
ompSessionId: string | undefined,
|
|
546
560
|
): Pick<Runtime, "store" | "coordinator" | "listener" | "sessionId" | "label"> {
|
|
547
561
|
const dir = stateDir();
|
|
548
562
|
mkdirSync(join(dir, "s"), { recursive: true, mode: 0o700 });
|
|
@@ -552,7 +566,7 @@ function openSession(
|
|
|
552
566
|
const coordinator = new Coordinator(config, store, unixLiveness, Date, osLoad);
|
|
553
567
|
const sessionId = randomBytes(6).toString("hex");
|
|
554
568
|
listener = startBeacon(join(dir, "s", `${sessionId}.sock`));
|
|
555
|
-
const label = coordinator.join(identity(sessionId, cwd, listener));
|
|
569
|
+
const label = coordinator.join(identity(sessionId, cwd, listener, ompSessionId));
|
|
556
570
|
return { store, coordinator, listener, sessionId, label };
|
|
557
571
|
} catch (error) {
|
|
558
572
|
listener?.stop();
|
|
@@ -561,8 +575,29 @@ function openSession(
|
|
|
561
575
|
}
|
|
562
576
|
}
|
|
563
577
|
|
|
564
|
-
function identity(sessionId: string, cwd: string, listener: Listener) {
|
|
565
|
-
return {
|
|
578
|
+
function identity(sessionId: string, cwd: string, listener: Listener, ompSessionId: string | undefined) {
|
|
579
|
+
return {
|
|
580
|
+
id: sessionId,
|
|
581
|
+
pid: process.pid,
|
|
582
|
+
cwd,
|
|
583
|
+
address: listener.address,
|
|
584
|
+
...(ompSessionId === undefined ? {} : { ompSessionId }),
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/** Records the main conversation's omp session id on the board when it is not the one already there. */
|
|
589
|
+
function syncMainSession(rt: Runtime, ctx: ExtensionContext): void {
|
|
590
|
+
const id = ompSessionIdOf(ctx);
|
|
591
|
+
if (id === undefined || id === rt.ompSessionId) return;
|
|
592
|
+
guarded(rt, "record the session id", () => {
|
|
593
|
+
rt.coordinator.bindOmpSession(rt.sessionId, id);
|
|
594
|
+
rt.ompSessionId = id;
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** omp's id for the conversation this context belongs to: what its bash calls carry as `PI_SESSION_ID`. */
|
|
599
|
+
function ompSessionIdOf(ctx: ExtensionContext): string | undefined {
|
|
600
|
+
return ctx.sessionManager?.getSessionId?.() || undefined;
|
|
566
601
|
}
|
|
567
602
|
|
|
568
603
|
/** `<scratch>/<session letter>[/<agent id>]`, created on first use; undefined when the scope has no scratch root. */
|
|
@@ -600,7 +635,9 @@ async function tick(rt: Runtime): Promise<void> {
|
|
|
600
635
|
if (shared.runtime !== rt) return;
|
|
601
636
|
rt.ticks++;
|
|
602
637
|
const now = Date.now();
|
|
603
|
-
const { label, inbox, holds, board } = rt.coordinator.tick(
|
|
638
|
+
const { label, inbox, holds, board } = rt.coordinator.tick(
|
|
639
|
+
identity(rt.sessionId, rt.ctx.cwd, rt.listener, rt.ompSessionId),
|
|
640
|
+
);
|
|
604
641
|
rt.label = label;
|
|
605
642
|
rt.queue?.apply(spawnAllowance(board, rt.config.agents, rt.sessionId, now));
|
|
606
643
|
|
|
@@ -37,7 +37,8 @@ CREATE TABLE sessions (
|
|
|
37
37
|
started_at INTEGER NOT NULL,
|
|
38
38
|
config_hash TEXT NOT NULL,
|
|
39
39
|
ports_from INTEGER,
|
|
40
|
-
ports_to INTEGER
|
|
40
|
+
ports_to INTEGER,
|
|
41
|
+
omp_session TEXT
|
|
41
42
|
) STRICT;
|
|
42
43
|
CREATE TABLE slots (
|
|
43
44
|
id INTEGER PRIMARY KEY,
|
|
@@ -48,7 +49,8 @@ CREATE TABLE slots (
|
|
|
48
49
|
pool TEXT,
|
|
49
50
|
state TEXT NOT NULL CHECK (state IN ('starting', 'running')),
|
|
50
51
|
since INTEGER NOT NULL,
|
|
51
|
-
unqueued TEXT CHECK (unqueued IN ('revived', 'eval'))
|
|
52
|
+
unqueued TEXT CHECK (unqueued IN ('revived', 'eval')),
|
|
53
|
+
omp_session TEXT
|
|
52
54
|
) STRICT;
|
|
53
55
|
CREATE TABLE tickets (
|
|
54
56
|
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
@@ -258,6 +260,8 @@ export class BoardStore implements BoardRepository {
|
|
|
258
260
|
if (!has("jobs", "agent")) this.#db.exec("ALTER TABLE jobs ADD COLUMN agent TEXT");
|
|
259
261
|
if (!has("slots", "unqueued"))
|
|
260
262
|
this.#db.exec("ALTER TABLE slots ADD COLUMN unqueued TEXT CHECK (unqueued IN ('revived', 'eval'))");
|
|
263
|
+
if (!has("sessions", "omp_session")) this.#db.exec("ALTER TABLE sessions ADD COLUMN omp_session TEXT");
|
|
264
|
+
if (!has("slots", "omp_session")) this.#db.exec("ALTER TABLE slots ADD COLUMN omp_session TEXT");
|
|
261
265
|
}
|
|
262
266
|
|
|
263
267
|
#maybePruneEvents(now: number): void {
|
|
@@ -283,6 +287,7 @@ export class BoardStore implements BoardRepository {
|
|
|
283
287
|
config_hash: string;
|
|
284
288
|
ports_from: number | null;
|
|
285
289
|
ports_to: number | null;
|
|
290
|
+
omp_session: string | null;
|
|
286
291
|
},
|
|
287
292
|
[]
|
|
288
293
|
>("SELECT * FROM sessions")
|
|
@@ -296,6 +301,7 @@ export class BoardStore implements BoardRepository {
|
|
|
296
301
|
startedAt: r.started_at,
|
|
297
302
|
configHash: r.config_hash,
|
|
298
303
|
ports: r.ports_from === null || r.ports_to === null ? undefined : { from: r.ports_from, to: r.ports_to },
|
|
304
|
+
...(r.omp_session ? { ompSessionId: r.omp_session } : {}),
|
|
299
305
|
})),
|
|
300
306
|
slots: db
|
|
301
307
|
.query<
|
|
@@ -308,6 +314,7 @@ export class BoardStore implements BoardRepository {
|
|
|
308
314
|
state: "starting" | "running";
|
|
309
315
|
since: number;
|
|
310
316
|
unqueued: "revived" | "eval" | null;
|
|
317
|
+
omp_session: string | null;
|
|
311
318
|
},
|
|
312
319
|
[]
|
|
313
320
|
>("SELECT * FROM slots ORDER BY id")
|
|
@@ -321,6 +328,7 @@ export class BoardStore implements BoardRepository {
|
|
|
321
328
|
state: r.state,
|
|
322
329
|
since: r.since,
|
|
323
330
|
...(r.unqueued ? { unqueued: r.unqueued } : {}),
|
|
331
|
+
...(r.omp_session ? { ompSessionId: r.omp_session } : {}),
|
|
324
332
|
})),
|
|
325
333
|
waitlist: db
|
|
326
334
|
.query<{ session_id: string; pool: string; since: number; hold_until: number | null; notified: number }, []>(
|
|
@@ -452,7 +460,7 @@ export class BoardStore implements BoardRepository {
|
|
|
452
460
|
}
|
|
453
461
|
|
|
454
462
|
const session = db.query(
|
|
455
|
-
"INSERT INTO sessions (id, label, pid, cwd, socket, started_at, config_hash, ports_from, ports_to) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
463
|
+
"INSERT INTO sessions (id, label, pid, cwd, socket, started_at, config_hash, ports_from, ports_to, omp_session) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
456
464
|
);
|
|
457
465
|
for (const s of board.sessions)
|
|
458
466
|
session.run(
|
|
@@ -465,12 +473,23 @@ export class BoardStore implements BoardRepository {
|
|
|
465
473
|
s.configHash,
|
|
466
474
|
s.ports?.from ?? null,
|
|
467
475
|
s.ports?.to ?? null,
|
|
476
|
+
s.ompSessionId ?? null,
|
|
468
477
|
);
|
|
469
478
|
const slot = db.query(
|
|
470
|
-
"INSERT INTO slots (session_id, agent_id, parent_id, agent_type, pool, state, since, unqueued) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
|
|
479
|
+
"INSERT INTO slots (session_id, agent_id, parent_id, agent_type, pool, state, since, unqueued, omp_session) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
471
480
|
);
|
|
472
481
|
for (const s of board.slots)
|
|
473
|
-
slot.run(
|
|
482
|
+
slot.run(
|
|
483
|
+
s.sessionId,
|
|
484
|
+
s.agentId ?? null,
|
|
485
|
+
s.parentId,
|
|
486
|
+
s.agentType,
|
|
487
|
+
s.pool,
|
|
488
|
+
s.state,
|
|
489
|
+
s.since,
|
|
490
|
+
s.unqueued ?? null,
|
|
491
|
+
s.ompSessionId ?? null,
|
|
492
|
+
);
|
|
474
493
|
const ticket = db.query(
|
|
475
494
|
"INSERT INTO tickets (session_id, pool, since, hold_until, notified) VALUES (?, ?, ?, ?, ?)",
|
|
476
495
|
);
|
package/src/app/coordinator.ts
CHANGED
|
@@ -31,7 +31,7 @@ import type { Board, Claim, Note } from "../domain/model.ts";
|
|
|
31
31
|
import { addNote, type NoteResult } from "../domain/notes.ts";
|
|
32
32
|
import { type AgentPortsResult, agentPorts } from "../domain/ports.ts";
|
|
33
33
|
import { dropJobs, type EnqueueResult, schedule, setJobPriority } from "../domain/queues.ts";
|
|
34
|
-
import { registerSession, removeSession } from "../domain/sessions.ts";
|
|
34
|
+
import { bindOmpSession, registerSession, removeSession } from "../domain/sessions.ts";
|
|
35
35
|
import type { BoardRepository, Clock, Liveness, LoadProbe } from "./ports.ts";
|
|
36
36
|
|
|
37
37
|
export interface SessionIdentity {
|
|
@@ -40,6 +40,8 @@ export interface SessionIdentity {
|
|
|
40
40
|
cwd: string;
|
|
41
41
|
/** Liveness address this session answers on. */
|
|
42
42
|
address: string;
|
|
43
|
+
/** omp's id for the main conversation, kept on the board so the runner can tell whose bash call it is. */
|
|
44
|
+
ompSessionId?: string;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
export interface TickResult {
|
|
@@ -86,7 +88,15 @@ export class Coordinator {
|
|
|
86
88
|
#register(b: Board, me: SessionIdentity, now: number): string {
|
|
87
89
|
return registerSession(
|
|
88
90
|
b,
|
|
89
|
-
{
|
|
91
|
+
{
|
|
92
|
+
id: me.id,
|
|
93
|
+
pid: me.pid,
|
|
94
|
+
cwd: me.cwd,
|
|
95
|
+
socket: me.address,
|
|
96
|
+
configHash: this.config.hash,
|
|
97
|
+
...(me.ompSessionId === undefined ? {} : { ompSessionId: me.ompSessionId }),
|
|
98
|
+
now,
|
|
99
|
+
},
|
|
90
100
|
this.config.resources.ports,
|
|
91
101
|
);
|
|
92
102
|
}
|
|
@@ -152,6 +162,11 @@ export class Coordinator {
|
|
|
152
162
|
return this.repo.transact(b => requestSpawn(b, this.config.agents, { ...req, now: this.clock.now() }));
|
|
153
163
|
}
|
|
154
164
|
|
|
165
|
+
/** omp moved this session onto another main conversation (new, resumed or forked): its bash calls carry the new id. */
|
|
166
|
+
bindOmpSession(sessionId: string, ompSessionId: string): void {
|
|
167
|
+
this.repo.transact(b => bindOmpSession(b, sessionId, ompSessionId));
|
|
168
|
+
}
|
|
169
|
+
|
|
155
170
|
agentStarted(a: {
|
|
156
171
|
sessionId: string;
|
|
157
172
|
agentId: string;
|
|
@@ -159,6 +174,7 @@ export class Coordinator {
|
|
|
159
174
|
parentIsSubagent: boolean;
|
|
160
175
|
agentType: string;
|
|
161
176
|
evalWorker?: boolean;
|
|
177
|
+
ompSessionId?: string;
|
|
162
178
|
}) {
|
|
163
179
|
return this.repo.transact(b => agentStarted(b, this.config.agents, { ...a, now: this.clock.now() }));
|
|
164
180
|
}
|
package/src/config.ts
CHANGED
|
@@ -26,12 +26,6 @@ export interface Config {
|
|
|
26
26
|
commands: CommandRule[];
|
|
27
27
|
/** Build and test queues, machine-wide within the scope. */
|
|
28
28
|
queues: Queues;
|
|
29
|
-
/**
|
|
30
|
-
* Start each agent bash command with `export OMP_COORD_AGENT=<agent id>;` so queued jobs record whose they are.
|
|
31
|
-
* Off by default: the rewritten command no longer matches omp's `bash.patterns` rules and replaces other
|
|
32
|
-
* extensions' rewrites of the same call.
|
|
33
|
-
*/
|
|
34
|
-
tagAgentCommands: boolean;
|
|
35
29
|
/** Port ranges and scratch folders handed to sessions and their agents. */
|
|
36
30
|
resources: { ports: PortPolicy | undefined; scratch: string | undefined };
|
|
37
31
|
/** Retention of the event log and the decision log. */
|
|
@@ -192,7 +186,6 @@ export function parseConfig(raw: unknown, source: string, hash = "none"): Config
|
|
|
192
186
|
},
|
|
193
187
|
commands,
|
|
194
188
|
queues,
|
|
195
|
-
tagAgentCommands: flag(root.tagAgentCommands ?? false, "tagAgentCommands", fail),
|
|
196
189
|
resources: { ports, scratch },
|
|
197
190
|
history: {
|
|
198
191
|
maxAgeMs: count(history.retentionDays ?? 30, "history.retentionDays", fail) * 24 * 3_600_000,
|
package/src/domain/agents.ts
CHANGED
|
@@ -154,6 +154,8 @@ export function agentStarted(
|
|
|
154
154
|
* spawn queue, so its new slot is marked "eval", not "revived".
|
|
155
155
|
*/
|
|
156
156
|
evalWorker?: boolean;
|
|
157
|
+
/** omp's id for the agent's own session, so its bash calls can be traced to it. */
|
|
158
|
+
ompSessionId?: string;
|
|
157
159
|
now: number;
|
|
158
160
|
},
|
|
159
161
|
): { pool: string; used: number; cap: number } | null {
|
|
@@ -184,6 +186,7 @@ export function agentStarted(
|
|
|
184
186
|
if (slot) {
|
|
185
187
|
slot.agentId = a.agentId;
|
|
186
188
|
slot.state = "running";
|
|
189
|
+
if (a.ompSessionId !== undefined) slot.ompSessionId = a.ompSessionId;
|
|
187
190
|
} else {
|
|
188
191
|
slot = {
|
|
189
192
|
sessionId: a.sessionId,
|
|
@@ -194,6 +197,7 @@ export function agentStarted(
|
|
|
194
197
|
state: "running",
|
|
195
198
|
since: a.now,
|
|
196
199
|
unqueued: a.evalWorker ? "eval" : "revived",
|
|
200
|
+
...(a.ompSessionId === undefined ? {} : { ompSessionId: a.ompSessionId }),
|
|
197
201
|
};
|
|
198
202
|
board.slots.push(slot);
|
|
199
203
|
}
|
package/src/domain/commands.ts
CHANGED
|
@@ -54,27 +54,3 @@ export function decideCommand(rules: CommandRule[], command: string, runner: Run
|
|
|
54
54
|
function quote(word: string): string {
|
|
55
55
|
return `'${word.replaceAll("'", `'\\''`)}'`;
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* The command with the id of the agent running it exported first, so a queued job records whose it is
|
|
60
|
-
* (`omp-coord run` reads `OMP_COORD_AGENT`). An export, not a `VAR=… cmd` prefix, so it reaches every command of a
|
|
61
|
-
* compound line; each agent's next command sets it again.
|
|
62
|
-
*/
|
|
63
|
-
export function withAgent(agentId: string, command: string): string {
|
|
64
|
-
return `export OMP_COORD_AGENT=${quote(agentId)}; ${withoutAgentExports(command)}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** The command without the `export OMP_COORD_AGENT=…;` prefixes it starts with, if any. */
|
|
68
|
-
export function withoutAgentExports(command: string): string {
|
|
69
|
-
return command.replace(LEADING_AGENT_EXPORTS, "");
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* The `export OMP_COORD_AGENT=…;` prefixes already at the start of a command. The rewritten command is what the
|
|
74
|
-
* conversation shows, so an agent that copies its earlier calls sends them back; without stripping, each call would
|
|
75
|
-
* add one more. A value is single-quoted as `quote` writes it (`'\''` for a quote inside) or a bare word of
|
|
76
|
-
* shell-safe characters only, so a hand-typed value holding `\;`, `|`, `>` or a substitution is never mistaken for
|
|
77
|
-
* a prefix and nothing of the command itself is dropped. Only spaces and tabs may surround the assignment: in bash a
|
|
78
|
-
* newline ends a command, so an `export` alone on its line is a command of its own, not part of a prefix.
|
|
79
|
-
*/
|
|
80
|
-
const LEADING_AGENT_EXPORTS = /^(?:\s*export[ \t]+OMP_COORD_AGENT=(?:'(?:[^']|'\\'')*'|[\w.@%+,:/=-]+)[ \t]*;)+\s*/;
|
package/src/domain/model.ts
CHANGED
|
@@ -41,6 +41,11 @@ export interface SessionRecord {
|
|
|
41
41
|
configHash: string;
|
|
42
42
|
/** Port range handed to this session, when the scope hands out ports. */
|
|
43
43
|
ports: PortRange | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* omp's id for the main session's conversation (`PI_SESSION_ID` in its agents' bash calls). omp can change it
|
|
46
|
+
* within one process (`/new`, `/resume`, `/fork`), so the extension binds it again whenever it sees another.
|
|
47
|
+
*/
|
|
48
|
+
ompSessionId?: string;
|
|
44
49
|
}
|
|
45
50
|
|
|
46
51
|
export interface PortRange {
|
|
@@ -102,6 +107,8 @@ export interface AgentSlot {
|
|
|
102
107
|
* those outside the queue, as spawns of `invocationKind` "eval").
|
|
103
108
|
*/
|
|
104
109
|
unqueued?: "revived" | "eval";
|
|
110
|
+
/** omp's id for the agent's own session (`PI_SESSION_ID` in its bash calls), once its run started. */
|
|
111
|
+
ompSessionId?: string;
|
|
105
112
|
}
|
|
106
113
|
|
|
107
114
|
/** A session's place in line for a pool, after a refused spawn. */
|
package/src/domain/sessions.ts
CHANGED
|
@@ -11,11 +11,23 @@ const LABELS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
|
11
11
|
*/
|
|
12
12
|
export function registerSession(
|
|
13
13
|
board: Board,
|
|
14
|
-
s: {
|
|
14
|
+
s: {
|
|
15
|
+
id: string;
|
|
16
|
+
pid: number;
|
|
17
|
+
cwd: string;
|
|
18
|
+
socket: string;
|
|
19
|
+
configHash: string;
|
|
20
|
+
ompSessionId?: string;
|
|
21
|
+
now: number;
|
|
22
|
+
},
|
|
15
23
|
ports?: Pick<PortPolicy, "from" | "to" | "perSession">,
|
|
16
24
|
): string {
|
|
17
25
|
const existing = board.sessions.find(x => x.id === s.id);
|
|
18
|
-
if (existing)
|
|
26
|
+
if (existing) {
|
|
27
|
+
// an older omp-coord's save writes every session without it
|
|
28
|
+
if (s.ompSessionId !== undefined) existing.ompSessionId = s.ompSessionId;
|
|
29
|
+
return existing.label;
|
|
30
|
+
}
|
|
19
31
|
const taken = new Set(board.sessions.map(x => x.label));
|
|
20
32
|
const label = [...LABELS].find(l => !taken.has(l));
|
|
21
33
|
if (!label) throw new Error(`${LABELS.length} sessions are already registered`);
|
|
@@ -36,6 +48,7 @@ export function registerSession(
|
|
|
36
48
|
startedAt: s.now,
|
|
37
49
|
configHash: s.configHash,
|
|
38
50
|
ports: block,
|
|
51
|
+
...(s.ompSessionId === undefined ? {} : { ompSessionId: s.ompSessionId }),
|
|
39
52
|
});
|
|
40
53
|
emit(board, s.now, "session_joined", s.id, {
|
|
41
54
|
pid: s.pid,
|
|
@@ -45,6 +58,22 @@ export function registerSession(
|
|
|
45
58
|
return label;
|
|
46
59
|
}
|
|
47
60
|
|
|
61
|
+
/** Records omp's id for the main session's conversation, replacing the one it had. */
|
|
62
|
+
export function bindOmpSession(board: Board, sessionId: string, ompSessionId: string): void {
|
|
63
|
+
const session = board.sessions.find(s => s.id === sessionId);
|
|
64
|
+
if (session) session.ompSessionId = ompSessionId;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The agent whose bash call carries this omp session id (`PI_SESSION_ID`): `Main` for a session's main conversation,
|
|
69
|
+
* a running subagent's id for its own. Undefined for an id the board doesn't know, such as an agent whose run ended.
|
|
70
|
+
*/
|
|
71
|
+
export function agentOfOmpSession(board: Board, ompSessionId: string): string | undefined {
|
|
72
|
+
const slot = board.slots.find(s => s.ompSessionId === ompSessionId && s.agentId !== undefined);
|
|
73
|
+
if (slot) return slot.agentId;
|
|
74
|
+
return board.sessions.some(s => s.ompSessionId === ompSessionId) ? "Main" : undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
/**
|
|
49
78
|
* Removes a session that ended or died: its slots free, it leaves the line, messages to it are dropped, and its
|
|
50
79
|
* claims, ports and fair-share history go.
|