@nledford/omp-coord 0.2.1 → 0.2.3
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 +20 -1
- package/README.md +1 -1
- package/docs/commands.md +61 -1
- package/docs/configuration.md +1 -0
- package/docs/coordination.md +1 -1
- package/docs/event-log.md +2 -2
- package/docs/queues.md +2 -0
- package/docs/versioning.md +1 -0
- package/package.json +1 -1
- package/src/adapters/cli/cli.ts +45 -4
- package/src/adapters/omp/extension.ts +19 -4
- package/src/adapters/sqlite/store.ts +23 -2
- package/src/adapters/text/report.ts +43 -6
- package/src/app/job-runner.ts +2 -0
- package/src/config.ts +12 -0
- package/src/domain/commands.ts +9 -0
- package/src/domain/model.ts +3 -0
- package/src/domain/overview.ts +215 -0
- package/src/domain/queues.ts +3 -0
|
@@ -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.2.
|
|
10
|
+
"version": "0.2.3",
|
|
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,23 @@ Add each user-visible change under Unreleased as it lands, in one of: Added, Cha
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.2.3] - 2026-10-27
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `tagAgentCommands` (off by default): each queued job records the agent whose command queued it, and the queue lines in `/coord` and `omp-coord status` read `session A/WaveImport`; `status --json` has `agent` on each job and the `job_queued` event carries it. With it on, the extension starts every agent bash command with `export OMP_COORD_AGENT=<agent id>;`, which `omp-coord run` reads; leave it off if you use omp's `bash.patterns` rules or another extension that rewrites bash commands. A board made by an older omp-coord gains the new column when this version opens it; while any session of the scope still runs an older omp-coord, its saves drop the recorded agents (display only).
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- A queue line shows the job's key (for example `rebase-2204 ci`) instead of its command when the job has one; the command still shows when it doesn't, and `status --json` keeps both.
|
|
18
|
+
|
|
19
|
+
## [0.2.2] - 2026-10-27
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `omp-coord status --json` prints the board as one JSON object for scripts (scope, sessions, pools, line, agents, claims, queues with their running and waiting jobs, latest notes, load; times in epoch milliseconds). With a board that cannot be read it prints nothing to stdout and exits non-zero. The fields are documented in [docs/commands.md](docs/commands.md).
|
|
24
|
+
- A list of the running agents with their type, parent, pool, state and age: `/coord agents`, `omp-coord status --agents`, and `agents` in `status --json`.
|
|
25
|
+
|
|
9
26
|
## [0.2.1] - 2026-10-26
|
|
10
27
|
|
|
11
28
|
### Added
|
|
@@ -46,7 +63,9 @@ Add each user-visible change under Unreleased as it lands, in one of: Added, Cha
|
|
|
46
63
|
- An SQLite event log per scope, with retention.
|
|
47
64
|
- Packaging as an omp plugin, with a marketplace catalog; queued commands run on omp's own Bun.
|
|
48
65
|
|
|
49
|
-
[Unreleased]: https://github.com/nledford/omp-coord/compare/v0.2.
|
|
66
|
+
[Unreleased]: https://github.com/nledford/omp-coord/compare/v0.2.3...HEAD
|
|
67
|
+
[0.2.3]: https://github.com/nledford/omp-coord/compare/v0.2.2...v0.2.3
|
|
68
|
+
[0.2.2]: https://github.com/nledford/omp-coord/compare/v0.2.1...v0.2.2
|
|
50
69
|
[0.2.1]: https://github.com/nledford/omp-coord/compare/v0.2.0...v0.2.1
|
|
51
70
|
[0.2.0]: https://github.com/nledford/omp-coord/compare/v0.1.0...v0.2.0
|
|
52
71
|
[0.1.0]: https://github.com/nledford/omp-coord/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ Every omp session started in that folder or below now shares 6 agent slots, and
|
|
|
46
46
|
| [Agent slots](docs/agent-slots.md) | How caps, the line, holds and nested agents work. |
|
|
47
47
|
| [Queues](docs/queues.md) | `omp-coord run`, order, lanes, time limits, agents' commands. |
|
|
48
48
|
| [Working across sessions](docs/coordination.md) | Messages, claims, pause, the decision log, ports, crash recovery. |
|
|
49
|
-
| [Commands](docs/commands.md) | `/coord`, agent tools, CLI, environment variables. |
|
|
49
|
+
| [Commands](docs/commands.md) | `/coord`, agent tools, CLI (`status --json` for scripts), environment variables. |
|
|
50
50
|
| [Event log](docs/event-log.md) | The SQLite tables and events, with example queries. |
|
|
51
51
|
| [Versioning](docs/versioning.md) | What the version promises; cutting a release. |
|
|
52
52
|
|
package/docs/commands.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
| Command | Does |
|
|
6
6
|
|---|---|
|
|
7
7
|
| `/coord` | Sessions, slots, the line, claims, queues, the latest decisions; marks a paused scope and sessions reading a different config file. |
|
|
8
|
+
| `/coord agents` | The agents holding slots, one line each: session, id, type, parent (`Main` or an agent id), pool or `outside the caps`, state and age. Use it to see who runs, and what type each is, before messaging them. |
|
|
8
9
|
| `/coord send <letter\|B/agent\|all> <text>` | Message other sessions or one of their subagents. |
|
|
9
10
|
| `/coord pause [reason]`, `/coord resume` | Stop and restart new agents and queued jobs in every session. |
|
|
10
11
|
| `/coord claim <branch\|worktree\|pr> <target> [note]`, `/coord release <kind> <target>` | Claim or release. |
|
|
@@ -26,12 +27,70 @@
|
|
|
26
27
|
|
|
27
28
|
```sh
|
|
28
29
|
omp-coord run <queue> [--lane L] [--priority N] [--key K] [--owner SESSION] [--timeout 90m] -- <command> [args…]
|
|
29
|
-
omp-coord status | stats [days] | interrupted [hours] | notes [count] | note <text…>
|
|
30
|
+
omp-coord status [--agents | --json] | stats [days] | interrupted [hours] | notes [count] | note <text…>
|
|
30
31
|
omp-coord pause [reason…] | resume | doctor [--fix]
|
|
31
32
|
```
|
|
32
33
|
|
|
33
34
|
`run` is described under [queues](queues.md). The others act on the scope whose `.omp-coord.json` is found from the current folder.
|
|
34
35
|
|
|
36
|
+
### `status`
|
|
37
|
+
|
|
38
|
+
`omp-coord status` prints the same overview as `/coord`. `--agents` prints only the running agents, as `/coord agents` does:
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
Agents (3):
|
|
42
|
+
A Fixer task parent Main pool work running 5 min
|
|
43
|
+
A Fixer.Check reviewer parent Fixer outside the caps running 4 min
|
|
44
|
+
B Other task parent Main pool work running 2 min
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Each line is the session letter, the agent's id (`(no id yet)` for an approved spawn omp gave no key for), its type, its parent, its pool, its state (`running`, or `starting` until its first run is seen) and how long ago it took its slot.
|
|
48
|
+
|
|
49
|
+
`--json` prints the board as one JSON object on stdout, for scripts. It is built whole before anything is written: if the board cannot be read, nothing goes to stdout, the error goes to stderr, and the exit code is not `0` (`2` for a usage or configuration error, `1` otherwise). `--json` and `--agents` do not combine.
|
|
50
|
+
|
|
51
|
+
Every time is milliseconds since the epoch (`Date.now()`); a session is its letter, or `null` when there is none (a job started outside omp, or by a session that has ended). Fields are never omitted: a missing value is `null`, a list is `[]`. Removing or renaming a field is a breaking change ([versioning](versioning.md)); new fields may appear.
|
|
52
|
+
|
|
53
|
+
| Field | Meaning |
|
|
54
|
+
|---|---|
|
|
55
|
+
| `generatedAt` | When the object was built. |
|
|
56
|
+
| `scope` | `{ name, source }`: the scope and the config file it was read from. |
|
|
57
|
+
| `paused` | `null`, or `{ by, byId, reason, since }`: `by` is the pausing session's letter, `byId` its id; both are `null` for a pause from a terminal, and only `by` once that session has ended. |
|
|
58
|
+
| `load` | The load average the limits act on, or `null` without a fresh reading. |
|
|
59
|
+
| `sessions[]` | `letter`, `id`, `folder`, `pid`, `since`, `agents` (`{ pools: { <pool>: slots held }, outsideCaps }`), `ports` (`{ from, to }` or `null`), `differentConfig` (read another config file than this scope's). By letter. |
|
|
60
|
+
| `pools[]` | `name`, `used`, `cap` (now: lowered under load, `0` while paused), `configuredCap`. |
|
|
61
|
+
| `line[]` | Sessions waiting for a slot: `pool`, `session`, `position` (from 1), `since`, `heldUntil` (`null` unless a freed slot is held for it). |
|
|
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
|
+
| `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` when `tagAgentCommands` is off, for a job started outside an agent's bash call, and while any session of the scope runs an omp-coord older than 0.2.3, whose saves drop it), `state`, `queuedAt`, `startedAt` (`null` while waiting), `position` (from 1 while waiting, `null` while running). |
|
|
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
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"generatedAt": 1793000000000,
|
|
70
|
+
"scope": { "name": "demo", "source": "/work/demo/.omp-coord.json" },
|
|
71
|
+
"paused": null,
|
|
72
|
+
"load": 3.5,
|
|
73
|
+
"sessions": [
|
|
74
|
+
{ "letter": "A", "id": "7f3c…", "folder": "/work/demo", "pid": 4210, "since": 1792999000000,
|
|
75
|
+
"agents": { "pools": { "work": 1 }, "outsideCaps": 1 }, "ports": null, "differentConfig": false }
|
|
76
|
+
],
|
|
77
|
+
"pools": [{ "name": "work", "used": 1, "cap": 2, "configuredCap": 2 }],
|
|
78
|
+
"line": [],
|
|
79
|
+
"agents": [
|
|
80
|
+
{ "id": "Fixer", "type": "task", "parent": "Main", "session": "A", "pool": "work", "state": "running", "since": 1792999300000 },
|
|
81
|
+
{ "id": "Fixer.Check", "type": "reviewer", "parent": "Fixer", "session": "A", "pool": null, "state": "running", "since": 1792999400000 }
|
|
82
|
+
],
|
|
83
|
+
"claims": [],
|
|
84
|
+
"queues": [
|
|
85
|
+
{ "name": "heavy", "order": "fifo", "slots": 1, "configuredSlots": 1,
|
|
86
|
+
"running": [{ "id": "a1b2c3d4", "session": "A", "owner": "7f3c…", "lane": "normal", "priority": 0, "key": "demo ci",
|
|
87
|
+
"command": "cargo test", "state": "running", "queuedAt": 1792999500000, "startedAt": 1792999500000, "position": null }],
|
|
88
|
+
"waiting": [] }
|
|
89
|
+
],
|
|
90
|
+
"notes": []
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
35
94
|
Exit codes: `0` success, the command's own for `run`, `75` a job not run, `124` a job stopped by its time limit, `2` a usage or configuration error.
|
|
36
95
|
|
|
37
96
|
## Environment
|
|
@@ -43,4 +102,5 @@ Exit codes: `0` success, the command's own for `run`, `75` a job not run, `124`
|
|
|
43
102
|
| `OMP_COORD_SESSION` | the extension | The session that owns jobs started from its agents' commands. |
|
|
44
103
|
| `OMP_COORD_PORTS` | the extension | The session's port block, e.g. `18700-18799`, when `resources.ports` is set. |
|
|
45
104
|
| `OMP_COORD_SCRATCH` | the extension | The session's scratch folder, when `resources.scratch` is set. |
|
|
105
|
+
| `OMP_COORD_AGENT` | the extension, with `tagAgentCommands` | The agent whose command this is (`Main` for the main agent); `omp-coord run` records it on the job. |
|
|
46
106
|
| `OMP_COORD_JOB`, `OMP_COORD_QUEUE` | `omp-coord run` | The running job's id and queue, for nested runs. |
|
package/docs/configuration.md
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
| `queues.<name>.onDuplicate` | `"refuse"` | `"refuse"` or `"supersede"` a waiting job with the same key. |
|
|
22
22
|
| `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. |
|
|
23
23
|
| `queues.<name>.loadSlots` | `[]` | `[{ "above": 150, "slots": 1 }]`: fewer slots under load; `0` holds the queue. |
|
|
24
|
+
| `tagAgentCommands` | `false` | Start each agent bash command with `export OMP_COORD_AGENT=<agent id>;`, so each queued job records the agent that queued it (queue lines read `session A/<agent>`). Leave it off when you use omp's `bash.patterns` allow or deny rules (a rewritten command no longer matches them) or another extension that rewrites bash commands (the last rewrite wins). |
|
|
24
25
|
| `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. |
|
|
25
26
|
| `resources.scratch` | none | Root of the scratch folders: `<scratch>/<letter>` per session, `<scratch>/<letter>/<agent id>` per agent. Relative to the config file. |
|
|
26
27
|
| `history.retentionDays` | `30` | Events and decision-log notes older than this are deleted. |
|
package/docs/coordination.md
CHANGED
|
@@ -38,4 +38,4 @@ With `resources.ports`, each session gets its own block of ports, in `OMP_COORD_
|
|
|
38
38
|
|
|
39
39
|
`/coord interrupted` (or `omp-coord interrupted`, which first drops sessions whose process is gone) lists, per session that left, the agents that were mid-run and the folder the session ran in. omp keeps their transcripts; revive each from a session in that folder with `write agent://<id>`.
|
|
40
40
|
|
|
41
|
-
`/coord stats` sums waits, refusals, peaks and job outcomes per day; `omp-coord doctor` checks the scope's health, and `doctor --fix` drops sessions and jobs whose process is gone.
|
|
41
|
+
`/coord agents` (or `omp-coord status --agents`) lists who runs: each agent's session, id, type, parent, pool and age, so a message to `B/<agent id>` goes to the agent you mean. `/coord stats` sums waits, refusals, peaks and job outcomes per day; `omp-coord doctor` checks the scope's health, and `doctor --fix` drops sessions and jobs whose process is gone.
|
package/docs/event-log.md
CHANGED
|
@@ -14,7 +14,7 @@ These tables are rebuilt as sessions come and go; their layout is not part of th
|
|
|
14
14
|
| `slots` | Agent slots held: `session_id`, `agent_id`, `parent_id`, `agent_type`, `pool` (null outside the caps), `state` (`starting` or `running`), `since`. |
|
|
15
15
|
| `tickets` | The line: `session_id`, `pool`, `since`, `hold_until` (set while a slot is held for it), `notified`. |
|
|
16
16
|
| `messages` | Messages not yet delivered. |
|
|
17
|
-
| `jobs` | Queued and running build/test jobs: `id`, `queue`, `owner` (session id, or null outside omp), `lane`, `priority`, `key`, `command`, `pid`, `state` (`waiting`, `running`, `superseded`), `since`, `started_at`. |
|
|
17
|
+
| `jobs` | Queued and running build/test jobs: `id`, `queue`, `owner` (session id, or null outside omp), `lane`, `priority`, `key`, `command`, `agent` (null when not recorded), `pid`, `state` (`waiting`, `running`, `superseded`), `since`, `started_at`. |
|
|
18
18
|
| `claims` | `kind` (`branch`, `worktree`, `pr`), `target`, `session_id`, `agent_id`, `note`, `since`. |
|
|
19
19
|
| `pause` | One row while the scope is paused: `by_session` (null from a terminal), `reason`, `since`. |
|
|
20
20
|
| `agent_ports` | Port blocks agents took: `session_id`, `agent_id`, `from_port`, `to_port`. |
|
|
@@ -25,7 +25,7 @@ These tables are rebuilt as sessions come and go; their layout is not part of th
|
|
|
25
25
|
`events` is an append-only log: `at` (ms since epoch), `type`, `session_id` (null for jobs from outside omp), `label`, and `data` (JSON). Types:
|
|
26
26
|
|
|
27
27
|
- Sessions and agents: `session_joined`, `session_left`, `spawn_approved`, `spawn_refused` (with `position`, `used`, `cap`, or `nested` and `reason`), `run_started` (with `revived`), `run_ended` (with `ranMs`), `start_expired`, `hold_granted`, `hold_used` and `hold_expired` (with `waitedMs` since the first refusal), `message_sent` (recipients and length, never the text).
|
|
28
|
-
- Jobs: `job_queued` (with `queue`, `lane`, `priority`, `key`), `job_refused` (with `reason`), `job_started` (with `waitedMs`), `job_ended` (with `exitCode`, `ranMs`, and `timedOut` when the time limit stopped it), `job_withdrawn` (left the line before starting), `job_superseded`, `job_lost` (its runner died), `job_reprioritized`.
|
|
28
|
+
- Jobs: `job_queued` (with `queue`, `lane`, `priority`, `key`, and `agent` when recorded), `job_refused` (with `reason`), `job_started` (with `waitedMs`), `job_ended` (with `exitCode`, `ranMs`, and `timedOut` when the time limit stopped it), `job_withdrawn` (left the line before starting), `job_superseded`, `job_lost` (its runner died), `job_reprioritized`.
|
|
29
29
|
- Control: `paused` (with `reason`), `resumed` (with `pausedMs`), `claimed`, `released`, `claim_refused` (with `holder`), `note_added` (note number and length).
|
|
30
30
|
|
|
31
31
|
`notes` is the decision log: `id`, `at`, `label`, `author`, `text`.
|
package/docs/queues.md
CHANGED
|
@@ -49,4 +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
|
+
With `tagAgentCommands` on, the extension also starts every agent bash command with `export OMP_COORD_AGENT=<agent id>;`, and `omp-coord run` records that agent on the job: queue lines read `session A/WaveImport` instead of only `session A`. It is off by default because the rewritten command no longer matches omp's `bash.patterns` rules (they must match the whole command) and replaces any other extension's rewrite of the same call.
|
|
53
|
+
|
|
52
54
|
A command matching a `refuse` rule is refused with the rule's reason.
|
package/docs/versioning.md
CHANGED
|
@@ -8,6 +8,7 @@ The version covers:
|
|
|
8
8
|
|
|
9
9
|
- the `.omp-coord.json` fields, their defaults and what they accept ([configuration](configuration.md));
|
|
10
10
|
- the `omp-coord` subcommands, flags and exit codes (75, 124, 2) ([commands](commands.md));
|
|
11
|
+
- the fields of `omp-coord status --json` ([commands](commands.md#status));
|
|
11
12
|
- the agent tools (`coord_*`) and their parameters, and the `/coord` commands;
|
|
12
13
|
- the environment variables it sets or reads (`OMP_COORD_*`);
|
|
13
14
|
- the event types and fields in the event log, and the decision log ([event log](event-log.md));
|
package/package.json
CHANGED
package/src/adapters/cli/cli.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* terminals, or agents' bash calls (which the extension rewrites to go through here), and reports on the scope.
|
|
5
5
|
*
|
|
6
6
|
* omp-coord run <queue> [--lane L] [--priority N] [--key K] [--owner SESSION] [--timeout 90m] -- <command> [args…]
|
|
7
|
-
* omp-coord status | stats [days] | interrupted [hours] | notes [count] | note <text…>
|
|
7
|
+
* omp-coord status [--agents | --json] | stats [days] | interrupted [hours] | notes [count] | note <text…>
|
|
8
8
|
* omp-coord pause [reason…] | resume | doctor [--fix]
|
|
9
9
|
*
|
|
10
10
|
* `run` waits for a slot, runs the command with the terminal's stdin/stdout/stderr, and exits with its exit code.
|
|
@@ -22,11 +22,19 @@ import { Coordinator } from "../../app/coordinator.ts";
|
|
|
22
22
|
import { doctor } from "../../app/doctor.ts";
|
|
23
23
|
import { EXIT_TIMED_OUT, runJob } from "../../app/job-runner.ts";
|
|
24
24
|
import { type Config, ConfigError, findConfigFile, loadConfig } from "../../config.ts";
|
|
25
|
+
import { statusView } from "../../domain/overview.ts";
|
|
25
26
|
import { MAX_PRIORITY } from "../../domain/queues.ts";
|
|
26
27
|
import { osLoad } from "../os/load.ts";
|
|
27
28
|
import { groupRunning } from "../os/process-group.ts";
|
|
28
29
|
import { BoardStore, stateDir } from "../sqlite/store.ts";
|
|
29
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
formatAgents,
|
|
32
|
+
formatFindings,
|
|
33
|
+
formatInterrupted,
|
|
34
|
+
formatNotes,
|
|
35
|
+
formatStats,
|
|
36
|
+
formatStatus,
|
|
37
|
+
} from "../text/report.ts";
|
|
30
38
|
import { startBeacon, unixLiveness } from "../unix/beacon.ts";
|
|
31
39
|
|
|
32
40
|
const EXIT_USAGE = 2;
|
|
@@ -36,18 +44,49 @@ const DAY = 86_400_000;
|
|
|
36
44
|
|
|
37
45
|
const USAGE = `usage:
|
|
38
46
|
omp-coord run <queue> [--lane L] [--priority N] [--key K] [--owner SESSION] [--timeout 90m] -- <command…>
|
|
39
|
-
omp-coord status | stats [days] | interrupted [hours] | notes [count] | note <text…>
|
|
47
|
+
omp-coord status [--agents | --json] | stats [days] | interrupted [hours] | notes [count] | note <text…>
|
|
40
48
|
omp-coord pause [reason…] | resume | doctor [--fix]`;
|
|
41
49
|
|
|
42
50
|
class UsageError extends Error {}
|
|
43
51
|
|
|
52
|
+
/**
|
|
53
|
+
* `status` prints the board as text; `--agents` lists only the running agents; `--json` prints one JSON object. The
|
|
54
|
+
* output is built whole before anything is written, so a board that cannot be read leaves stdout empty.
|
|
55
|
+
*/
|
|
56
|
+
function status(args: string[]): Promise<number> {
|
|
57
|
+
let flags: ReturnType<typeof parseStatusFlags>;
|
|
58
|
+
try {
|
|
59
|
+
flags = parseStatusFlags(args);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw new UsageError(`omp-coord status: ${error instanceof Error ? error.message : String(error)}`);
|
|
62
|
+
}
|
|
63
|
+
const { agents, json } = flags.values;
|
|
64
|
+
if (flags.positionals.length > 0 || (agents && json))
|
|
65
|
+
throw new UsageError("omp-coord status: give no arguments, or one of --agents and --json");
|
|
66
|
+
return withScope(({ config, coordinator }) => {
|
|
67
|
+
const board = coordinator.board();
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
if (json) return print(JSON.stringify(statusView(board, config, now), null, 2));
|
|
70
|
+
return print(agents ? formatAgents(board, now) : formatStatus(board, config, "", now));
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function parseStatusFlags(args: string[]) {
|
|
75
|
+
return parseArgs({
|
|
76
|
+
args,
|
|
77
|
+
allowPositionals: true,
|
|
78
|
+
strict: true,
|
|
79
|
+
options: { agents: { type: "boolean" }, json: { type: "boolean" } },
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
44
83
|
async function main(argv: string[]): Promise<number> {
|
|
45
84
|
const [sub, ...rest] = argv;
|
|
46
85
|
switch (sub) {
|
|
47
86
|
case "run":
|
|
48
87
|
return run(rest);
|
|
49
88
|
case "status":
|
|
50
|
-
return
|
|
89
|
+
return status(rest);
|
|
51
90
|
case "stats":
|
|
52
91
|
return withScope(({ coordinator }) =>
|
|
53
92
|
print(formatStats(coordinator.stats(wholeNumber(rest[0], 7, "days") * DAY))),
|
|
@@ -210,6 +249,8 @@ async function run(args: string[]): Promise<number> {
|
|
|
210
249
|
id,
|
|
211
250
|
queue,
|
|
212
251
|
owner: values.owner || process.env.OMP_COORD_SESSION || null,
|
|
252
|
+
// set per command by the extension's bash hook
|
|
253
|
+
...(process.env.OMP_COORD_AGENT ? { agent: process.env.OMP_COORD_AGENT } : {}),
|
|
213
254
|
...(values.lane === undefined ? {} : { lane: values.lane }),
|
|
214
255
|
priority,
|
|
215
256
|
...(values.key === undefined ? {} : { key: values.key }),
|
|
@@ -16,11 +16,19 @@ import { join, resolve } from "node:path";
|
|
|
16
16
|
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
|
-
import { decideCommand } from "../../domain/commands.ts";
|
|
19
|
+
import { decideCommand, withAgent } from "../../domain/commands.ts";
|
|
20
20
|
import type { Claim } from "../../domain/model.ts";
|
|
21
21
|
import { osLoad } from "../os/load.ts";
|
|
22
22
|
import { BoardStore, stateDir } from "../sqlite/store.ts";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
clock,
|
|
25
|
+
formatAgents,
|
|
26
|
+
formatFooter,
|
|
27
|
+
formatInterrupted,
|
|
28
|
+
formatNotes,
|
|
29
|
+
formatStats,
|
|
30
|
+
formatStatus,
|
|
31
|
+
} from "../text/report.ts";
|
|
24
32
|
import { startBeacon, unixLiveness } from "../unix/beacon.ts";
|
|
25
33
|
import type { ExtensionAPI, ExtensionContext, Timer, ToolResult } from "./api.ts";
|
|
26
34
|
|
|
@@ -182,7 +190,12 @@ export default function ompCoord(pi: ExtensionAPI): void {
|
|
|
182
190
|
config: rt.config.source,
|
|
183
191
|
});
|
|
184
192
|
if (decision.kind === "refuse") return { block: true, reason: `omp-coord: ${decision.reason}` };
|
|
185
|
-
|
|
193
|
+
const run = decision.kind === "queue" ? decision.command : command;
|
|
194
|
+
// Every agent of a session shares the omp process's environment, so the agent's id can reach `omp-coord run` (and
|
|
195
|
+
// the queue lines) only through the command itself; opt-in (`tagAgentCommands`), since the rewrite defeats omp's
|
|
196
|
+
// bash.patterns rules and other extensions' rewrites. Off, the hook changes only queued commands, as before.
|
|
197
|
+
if (rt.config.tagAgentCommands) return { input: { ...event.input, command: withAgent(ctx.agent.id, run) } };
|
|
198
|
+
if (decision.kind === "queue") return { input: { ...event.input, command: run } };
|
|
186
199
|
return undefined;
|
|
187
200
|
});
|
|
188
201
|
|
|
@@ -334,7 +347,7 @@ export default function ompCoord(pi: ExtensionAPI): void {
|
|
|
334
347
|
|
|
335
348
|
pi.registerCommand("coord", {
|
|
336
349
|
description:
|
|
337
|
-
"Coordination with other omp sessions: `/coord` status; `send <letter|B/agent|all> <text>`; `pause [reason]`, `resume`; " +
|
|
350
|
+
"Coordination with other omp sessions: `/coord` status; `agents`; `send <letter|B/agent|all> <text>`; `pause [reason]`, `resume`; " +
|
|
338
351
|
"`claim <branch|worktree|pr> <target> [note]`, `release <kind> <target>`; `note <text>`, `notes [n]`; `stats [days]`; `interrupted [hours]`",
|
|
339
352
|
async handler(args, ctx) {
|
|
340
353
|
const rt = shared.runtime;
|
|
@@ -349,6 +362,8 @@ export default function ompCoord(pi: ExtensionAPI): void {
|
|
|
349
362
|
switch (sub) {
|
|
350
363
|
case "":
|
|
351
364
|
return ctx.ui.notify(statusText(rt), "info");
|
|
365
|
+
case "agents":
|
|
366
|
+
return ctx.ui.notify(formatAgents(rt.coordinator.board(), Date.now()), "info");
|
|
352
367
|
case "send": {
|
|
353
368
|
const m = /^(\S+)\s+([\s\S]+)$/.exec(tail);
|
|
354
369
|
return say(
|
|
@@ -77,6 +77,7 @@ CREATE TABLE jobs (
|
|
|
77
77
|
priority INTEGER NOT NULL,
|
|
78
78
|
key TEXT,
|
|
79
79
|
command TEXT NOT NULL,
|
|
80
|
+
agent TEXT,
|
|
80
81
|
socket TEXT NOT NULL,
|
|
81
82
|
pid INTEGER NOT NULL,
|
|
82
83
|
state TEXT NOT NULL CHECK (state IN ('waiting', 'running', 'superseded')),
|
|
@@ -229,7 +230,10 @@ export class BoardStore implements BoardRepository {
|
|
|
229
230
|
|
|
230
231
|
#migrate(): void {
|
|
231
232
|
const version = this.#db.query<{ user_version: number }, []>("PRAGMA user_version").get()?.user_version ?? 0;
|
|
232
|
-
if (version === SCHEMA_VERSION)
|
|
233
|
+
if (version === SCHEMA_VERSION) {
|
|
234
|
+
this.#addColumns();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
233
237
|
if (version !== 0) {
|
|
234
238
|
throw new Error(
|
|
235
239
|
`the board database has schema ${version}, but this omp-coord reads schema ${SCHEMA_VERSION}; update omp-coord`,
|
|
@@ -239,6 +243,20 @@ export class BoardStore implements BoardRepository {
|
|
|
239
243
|
this.#db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
|
|
240
244
|
}
|
|
241
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Nullable columns added after schema 1 was released, without a version bump: a board made by an older omp-coord
|
|
248
|
+
* gets them here, and an older omp-coord still running (an omp session not yet restarted) keeps reading and
|
|
249
|
+
* writing the board, ignoring them (its saves write null there, which readers show as unknown).
|
|
250
|
+
*/
|
|
251
|
+
#addColumns(): void {
|
|
252
|
+
const has = (table: string, column: string) =>
|
|
253
|
+
this.#db
|
|
254
|
+
.query<{ name: string }, []>(`PRAGMA table_info(${table})`)
|
|
255
|
+
.all()
|
|
256
|
+
.some(c => c.name === column);
|
|
257
|
+
if (!has("jobs", "agent")) this.#db.exec("ALTER TABLE jobs ADD COLUMN agent TEXT");
|
|
258
|
+
}
|
|
259
|
+
|
|
242
260
|
#maybePruneEvents(now: number): void {
|
|
243
261
|
const last = Number(
|
|
244
262
|
this.#db.query<{ value: string }, []>("SELECT value FROM meta WHERE key = 'events_pruned_at'").get()?.value ?? 0,
|
|
@@ -337,6 +355,7 @@ export class BoardStore implements BoardRepository {
|
|
|
337
355
|
priority: number;
|
|
338
356
|
key: string | null;
|
|
339
357
|
command: string;
|
|
358
|
+
agent: string | null;
|
|
340
359
|
socket: string;
|
|
341
360
|
pid: number;
|
|
342
361
|
state: "waiting" | "running" | "superseded";
|
|
@@ -354,6 +373,7 @@ export class BoardStore implements BoardRepository {
|
|
|
354
373
|
priority: r.priority,
|
|
355
374
|
key: r.key ?? undefined,
|
|
356
375
|
command: r.command,
|
|
376
|
+
agent: r.agent ?? undefined,
|
|
357
377
|
socket: r.socket,
|
|
358
378
|
pid: r.pid,
|
|
359
379
|
state: r.state,
|
|
@@ -455,7 +475,7 @@ export class BoardStore implements BoardRepository {
|
|
|
455
475
|
const sent = db.query("INSERT INTO sent (session_id, at) VALUES (?, ?)");
|
|
456
476
|
for (const x of board.sent) sent.run(x.sessionId, x.at);
|
|
457
477
|
const job = db.query(
|
|
458
|
-
"INSERT INTO jobs (id, queue, owner, lane, priority, key, command, socket, pid, state, since, started_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
478
|
+
"INSERT INTO jobs (id, queue, owner, lane, priority, key, command, agent, socket, pid, state, since, started_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
459
479
|
);
|
|
460
480
|
for (const j of board.jobs) {
|
|
461
481
|
job.run(
|
|
@@ -466,6 +486,7 @@ export class BoardStore implements BoardRepository {
|
|
|
466
486
|
j.priority,
|
|
467
487
|
j.key ?? null,
|
|
468
488
|
j.command,
|
|
489
|
+
j.agent ?? null,
|
|
469
490
|
j.socket,
|
|
470
491
|
j.pid,
|
|
471
492
|
j.state,
|
|
@@ -5,7 +5,8 @@ import { capOf, used } from "../../domain/agents.ts";
|
|
|
5
5
|
import { pauseReason } from "../../domain/control.ts";
|
|
6
6
|
import type { DayStats, InterruptedSession } from "../../domain/history.ts";
|
|
7
7
|
import { currentLoad } from "../../domain/limits.ts";
|
|
8
|
-
import { type AgentPolicy, type Board, labelOf, type Note } from "../../domain/model.ts";
|
|
8
|
+
import { type AgentPolicy, type Board, type Job, labelOf, type Note } from "../../domain/model.ts";
|
|
9
|
+
import { agentList, LATEST_NOTES } from "../../domain/overview.ts";
|
|
9
10
|
import { orderWaiting, type Queues, slotsOf } from "../../domain/queues.ts";
|
|
10
11
|
|
|
11
12
|
export function clock(ms: number): string {
|
|
@@ -14,6 +15,37 @@ export function clock(ms: number): string {
|
|
|
14
15
|
|
|
15
16
|
const minutes = (ms: number) => `${Math.round(ms / 60_000)} min`;
|
|
16
17
|
|
|
18
|
+
/** `45 s`, `12 min`, `2 h 5 min`. */
|
|
19
|
+
function age(ms: number): string {
|
|
20
|
+
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
21
|
+
if (seconds < 60) return `${seconds} s`;
|
|
22
|
+
const mins = Math.floor(seconds / 60);
|
|
23
|
+
return mins < 60 ? `${mins} min` : `${Math.floor(mins / 60)} h ${mins % 60} min`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The agents holding slots, one line each: session letter, id, type, parent, pool, state and how long ago the slot was
|
|
28
|
+
* taken. A starting agent's id is the spawn key omp gave, or `(no id yet)`.
|
|
29
|
+
*/
|
|
30
|
+
export function formatAgents(board: Board, now: number): string {
|
|
31
|
+
const agents = agentList(board);
|
|
32
|
+
if (agents.length === 0) return "No agents are running.";
|
|
33
|
+
const rows = agents.map(a => [
|
|
34
|
+
a.session ?? "?",
|
|
35
|
+
a.id ?? "(no id yet)",
|
|
36
|
+
a.type,
|
|
37
|
+
`parent ${a.parent}`,
|
|
38
|
+
a.pool === null ? "outside the caps" : `pool ${a.pool}`,
|
|
39
|
+
a.state,
|
|
40
|
+
age(now - a.since),
|
|
41
|
+
]);
|
|
42
|
+
const widths = rows[0]?.map((_, c) => Math.max(...rows.map(r => (r[c] as string).length))) ?? [];
|
|
43
|
+
return [
|
|
44
|
+
`Agents (${agents.length}):`,
|
|
45
|
+
...rows.map(r => ` ${r.map((cell, c) => (c === r.length - 1 ? cell : cell.padEnd(widths[c] ?? 0))).join(" ")}`),
|
|
46
|
+
].join("\n");
|
|
47
|
+
}
|
|
48
|
+
|
|
17
49
|
/** Everything on the board, from the view of session `me` ("" outside omp). */
|
|
18
50
|
export function formatStatus(board: Board, config: Config, me: string, now: number): string {
|
|
19
51
|
const policy = config.agents;
|
|
@@ -80,7 +112,7 @@ export function formatStatus(board: Board, config: Config, me: string, now: numb
|
|
|
80
112
|
const queueLines = formatQueues(board, config.queues, now);
|
|
81
113
|
if (queueLines.length) lines.push("Build and test queues:", ...queueLines);
|
|
82
114
|
|
|
83
|
-
const recent = board.notes.slice(-
|
|
115
|
+
const recent = board.notes.slice(-LATEST_NOTES);
|
|
84
116
|
if (recent.length)
|
|
85
117
|
lines.push(
|
|
86
118
|
"Latest decisions (all: notes):",
|
|
@@ -94,8 +126,13 @@ export function formatStatus(board: Board, config: Config, me: string, now: numb
|
|
|
94
126
|
/** One block per queue: running jobs, then waiting jobs in the order they will start. */
|
|
95
127
|
export function formatQueues(board: Board, queues: Queues, now: number): string[] {
|
|
96
128
|
const lines: string[] = [];
|
|
97
|
-
|
|
98
|
-
|
|
129
|
+
// whose job: the session, and the agent when recorded ("session A/WaveImport")
|
|
130
|
+
const who = (j: Job) =>
|
|
131
|
+
(j.owner === null ? "outside omp" : `session ${labelOf(board, j.owner) ?? "(ended)"}`) +
|
|
132
|
+
(j.agent ? `/${j.agent}` : "");
|
|
133
|
+
// what it is: the job's key ("<checkout> <recipe> <args>") reads better than the command's full path; the command
|
|
134
|
+
// when there is no key
|
|
135
|
+
const what = (j: Job) => j.key || j.command;
|
|
99
136
|
for (const [name, q] of Object.entries(queues)) {
|
|
100
137
|
const running = board.jobs.filter(j => j.queue === name && j.state === "running");
|
|
101
138
|
const waiting = orderWaiting(q, name, board, now);
|
|
@@ -103,14 +140,14 @@ export function formatQueues(board: Board, queues: Queues, now: number): string[
|
|
|
103
140
|
const lowered = slots < q.slots && !board.pause ? ` (lowered from ${q.slots} under load)` : "";
|
|
104
141
|
lines.push(` ${name} (${q.order}): ${running.length}/${slots} running${lowered}, ${waiting.length} waiting`);
|
|
105
142
|
for (const j of running)
|
|
106
|
-
lines.push(` running ${j.id} ${who(j
|
|
143
|
+
lines.push(` running ${j.id} ${who(j)} ${minutes(now - (j.startedAt ?? now))} ${what(j)}`);
|
|
107
144
|
for (const [i, j] of waiting.entries()) {
|
|
108
145
|
const extras = [
|
|
109
146
|
j.lane === q.lanes.at(-1) ? "" : `lane ${j.lane}`,
|
|
110
147
|
j.priority ? `priority ${j.priority}` : "",
|
|
111
148
|
].filter(Boolean);
|
|
112
149
|
lines.push(
|
|
113
|
-
` #${i + 1} ${j.id} ${who(j
|
|
150
|
+
` #${i + 1} ${j.id} ${who(j)} waited ${minutes(now - j.since)}${extras.length ? ` (${extras.join(", ")})` : ""} ${what(j)}`,
|
|
114
151
|
);
|
|
115
152
|
}
|
|
116
153
|
}
|
package/src/app/job-runner.ts
CHANGED
package/src/config.ts
CHANGED
|
@@ -26,6 +26,12 @@ 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;
|
|
29
35
|
/** Port ranges and scratch folders handed to sessions and their agents. */
|
|
30
36
|
resources: { ports: PortPolicy | undefined; scratch: string | undefined };
|
|
31
37
|
/** Retention of the event log and the decision log. */
|
|
@@ -185,6 +191,7 @@ export function parseConfig(raw: unknown, source: string, hash = "none"): Config
|
|
|
185
191
|
},
|
|
186
192
|
commands,
|
|
187
193
|
queues,
|
|
194
|
+
tagAgentCommands: flag(root.tagAgentCommands ?? false, "tagAgentCommands", fail),
|
|
188
195
|
resources: { ports, scratch },
|
|
189
196
|
history: {
|
|
190
197
|
maxAgeMs: count(history.retentionDays ?? 30, "history.retentionDays", fail) * 24 * 3_600_000,
|
|
@@ -205,6 +212,11 @@ function list(v: unknown, field: string, fail: Fail): unknown[] {
|
|
|
205
212
|
return v;
|
|
206
213
|
}
|
|
207
214
|
|
|
215
|
+
function flag(v: unknown, field: string, fail: Fail): boolean {
|
|
216
|
+
if (typeof v !== "boolean") fail(field, "must be true or false");
|
|
217
|
+
return v as boolean;
|
|
218
|
+
}
|
|
219
|
+
|
|
208
220
|
function str(v: unknown, field: string, fail: Fail): string {
|
|
209
221
|
if (typeof v !== "string" || v.trim() === "") return fail(field, "must be a non-empty string");
|
|
210
222
|
return v;
|
package/src/domain/commands.ts
CHANGED
|
@@ -54,3 +54,12 @@ 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)}; ${command}`;
|
|
65
|
+
}
|
package/src/domain/model.ts
CHANGED
|
@@ -158,6 +158,9 @@ export interface Job {
|
|
|
158
158
|
queue: string;
|
|
159
159
|
/** The omp session whose agent started it, or null (a terminal, a script). Fair share is per owner. */
|
|
160
160
|
owner: string | null;
|
|
161
|
+
/** omp's id of the agent whose command queued it ("Main" for the main agent), or undefined (outside omp, or an
|
|
162
|
+
* older omp-coord that did not record it). For display only. */
|
|
163
|
+
agent: string | undefined;
|
|
161
164
|
lane: string;
|
|
162
165
|
priority: number;
|
|
163
166
|
/** Jobs with the same key are duplicates (e.g. "<checkout> ci"). */
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The board as plain data for readers: `omp-coord status --json` prints a `StatusView`, and the text reports format
|
|
3
|
+
* its `agents`. Pure: callers pass `now` and the policies. Every time is milliseconds since the epoch; a session is
|
|
4
|
+
* named by its letter, or null when there is none (a job started outside omp, or by a session that has ended).
|
|
5
|
+
*
|
|
6
|
+
* The field names are part of the contract (docs/commands.md, docs/versioning.md): rename or remove one only as a
|
|
7
|
+
* breaking change.
|
|
8
|
+
*/
|
|
9
|
+
import { capOf, used } from "./agents.ts";
|
|
10
|
+
import { currentLoad } from "./limits.ts";
|
|
11
|
+
import { type AgentPolicy, type Board, labelOf } from "./model.ts";
|
|
12
|
+
import { orderWaiting, type Queues, slotsOf } from "./queues.ts";
|
|
13
|
+
|
|
14
|
+
/** What the view needs of the scope's configuration. `Config` satisfies it. */
|
|
15
|
+
export interface ScopeConfig {
|
|
16
|
+
scope: string;
|
|
17
|
+
/** The config file the scope was read from. */
|
|
18
|
+
source: string;
|
|
19
|
+
/** Hash of that file; sessions that read another one are flagged. */
|
|
20
|
+
hash: string;
|
|
21
|
+
agents: AgentPolicy;
|
|
22
|
+
queues: Queues;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** How many of the latest notes the view carries. */
|
|
26
|
+
export const LATEST_NOTES = 3;
|
|
27
|
+
|
|
28
|
+
export interface AgentView {
|
|
29
|
+
/** omp's agent id; null for an approved spawn whose first run has not started and that omp gave no key for. */
|
|
30
|
+
id: string | null;
|
|
31
|
+
type: string;
|
|
32
|
+
/** `Main` or the id of the subagent that started it. */
|
|
33
|
+
parent: string;
|
|
34
|
+
/** Letter of the session it runs in. */
|
|
35
|
+
session: string | null;
|
|
36
|
+
/** The pool holding its slot; null outside the caps (exempt types and nested reviewers). */
|
|
37
|
+
pool: string | null;
|
|
38
|
+
/** `starting`: approved, first run not seen yet. */
|
|
39
|
+
state: "starting" | "running";
|
|
40
|
+
/** When it took its slot. */
|
|
41
|
+
since: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface JobView {
|
|
45
|
+
id: string;
|
|
46
|
+
/** Letter of the session whose agent started it; null outside omp or once that session ended. */
|
|
47
|
+
session: string | null;
|
|
48
|
+
/** The owning session's id; null outside omp. */
|
|
49
|
+
owner: string | null;
|
|
50
|
+
lane: string;
|
|
51
|
+
priority: number;
|
|
52
|
+
key: string | null;
|
|
53
|
+
command: string;
|
|
54
|
+
/** omp's id of the agent whose command queued it ("Main" for the main agent); null outside omp or when not recorded. */
|
|
55
|
+
agent: string | null;
|
|
56
|
+
state: "running" | "waiting";
|
|
57
|
+
/** When it joined the line (a superseding job keeps the place of the one it replaced). */
|
|
58
|
+
queuedAt: number;
|
|
59
|
+
/** When it started; null while waiting. */
|
|
60
|
+
startedAt: number | null;
|
|
61
|
+
/** Place in line, from 1, in the order jobs will start; null while running. */
|
|
62
|
+
position: number | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface StatusView {
|
|
66
|
+
/** When the view was built. */
|
|
67
|
+
generatedAt: number;
|
|
68
|
+
scope: { name: string; source: string };
|
|
69
|
+
/** `by` is the pausing session's letter; null from a terminal, or once that session ended (`byId` tells which). */
|
|
70
|
+
paused: { by: string | null; byId: string | null; reason: string; since: number } | null;
|
|
71
|
+
/** The load average the limits act on; null without a fresh reading. */
|
|
72
|
+
load: number | null;
|
|
73
|
+
sessions: {
|
|
74
|
+
letter: string;
|
|
75
|
+
id: string;
|
|
76
|
+
folder: string;
|
|
77
|
+
pid: number;
|
|
78
|
+
since: number;
|
|
79
|
+
/** Slots held per configured pool, and agents outside the caps. */
|
|
80
|
+
agents: { pools: Record<string, number>; outsideCaps: number };
|
|
81
|
+
ports: { from: number; to: number } | null;
|
|
82
|
+
/** Whether it read a different config file than this scope's. */
|
|
83
|
+
differentConfig: boolean;
|
|
84
|
+
}[];
|
|
85
|
+
pools: { name: string; used: number; cap: number; configuredCap: number }[];
|
|
86
|
+
/** Sessions waiting for a slot, per pool, first in line first. */
|
|
87
|
+
line: { pool: string; session: string | null; position: number; since: number; heldUntil: number | null }[];
|
|
88
|
+
agents: AgentView[];
|
|
89
|
+
claims: {
|
|
90
|
+
kind: string;
|
|
91
|
+
target: string;
|
|
92
|
+
session: string | null;
|
|
93
|
+
agent: string;
|
|
94
|
+
note: string;
|
|
95
|
+
since: number;
|
|
96
|
+
}[];
|
|
97
|
+
queues: {
|
|
98
|
+
name: string;
|
|
99
|
+
order: string;
|
|
100
|
+
/** Slots now: lowered under load, 0 while paused. */
|
|
101
|
+
slots: number;
|
|
102
|
+
configuredSlots: number;
|
|
103
|
+
running: JobView[];
|
|
104
|
+
waiting: JobView[];
|
|
105
|
+
}[];
|
|
106
|
+
/** The latest notes, oldest first. `session` is the letter when the note was written, kept after that session ends. */
|
|
107
|
+
notes: { id: number; at: number; session: string | null; author: string; text: string }[];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Every agent holding a slot, by session letter then start. */
|
|
111
|
+
export function agentList(board: Board): AgentView[] {
|
|
112
|
+
return board.slots
|
|
113
|
+
.map(s => ({
|
|
114
|
+
id: s.agentId ?? null,
|
|
115
|
+
type: s.agentType,
|
|
116
|
+
parent: s.parentId,
|
|
117
|
+
session: labelOf(board, s.sessionId) ?? null,
|
|
118
|
+
pool: s.pool,
|
|
119
|
+
state: s.state,
|
|
120
|
+
since: s.since,
|
|
121
|
+
}))
|
|
122
|
+
.sort(
|
|
123
|
+
(a, b) =>
|
|
124
|
+
(a.session ?? "").localeCompare(b.session ?? "") || a.since - b.since || (a.id ?? "").localeCompare(b.id ?? ""),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function statusView(board: Board, config: ScopeConfig, now: number): StatusView {
|
|
129
|
+
const policy = config.agents;
|
|
130
|
+
const letter = (sessionId: string | null) => (sessionId === null ? null : (labelOf(board, sessionId) ?? null));
|
|
131
|
+
const pools = Object.keys(policy.pools);
|
|
132
|
+
|
|
133
|
+
const job = (j: Board["jobs"][number], position: number | null): JobView => ({
|
|
134
|
+
id: j.id,
|
|
135
|
+
session: letter(j.owner),
|
|
136
|
+
owner: j.owner,
|
|
137
|
+
lane: j.lane,
|
|
138
|
+
priority: j.priority,
|
|
139
|
+
key: j.key ?? null,
|
|
140
|
+
command: j.command,
|
|
141
|
+
agent: j.agent ?? null,
|
|
142
|
+
state: position === null ? "running" : "waiting",
|
|
143
|
+
queuedAt: j.since,
|
|
144
|
+
startedAt: j.startedAt ?? null,
|
|
145
|
+
position,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const paused = board.pause;
|
|
149
|
+
return {
|
|
150
|
+
generatedAt: now,
|
|
151
|
+
scope: { name: config.scope, source: config.source },
|
|
152
|
+
paused: paused ? { by: letter(paused.by), byId: paused.by, reason: paused.reason, since: paused.since } : null,
|
|
153
|
+
load: currentLoad(board, now) ?? null,
|
|
154
|
+
sessions: [...board.sessions]
|
|
155
|
+
.sort((a, b) => a.label.localeCompare(b.label))
|
|
156
|
+
.map(s => {
|
|
157
|
+
const held = board.slots.filter(x => x.sessionId === s.id);
|
|
158
|
+
return {
|
|
159
|
+
letter: s.label,
|
|
160
|
+
id: s.id,
|
|
161
|
+
folder: s.cwd,
|
|
162
|
+
pid: s.pid,
|
|
163
|
+
since: s.startedAt,
|
|
164
|
+
agents: {
|
|
165
|
+
pools: Object.fromEntries(pools.map(p => [p, held.filter(x => x.pool === p).length])),
|
|
166
|
+
outsideCaps: held.filter(x => x.pool === null).length,
|
|
167
|
+
},
|
|
168
|
+
ports: s.ports ? { from: s.ports.from, to: s.ports.to } : null,
|
|
169
|
+
differentConfig: s.configHash !== config.hash,
|
|
170
|
+
};
|
|
171
|
+
}),
|
|
172
|
+
pools: pools.map(name => ({
|
|
173
|
+
name,
|
|
174
|
+
used: used(board, name),
|
|
175
|
+
cap: capOf(board, policy, name, now),
|
|
176
|
+
configuredCap: policy.pools[name]?.cap ?? 0,
|
|
177
|
+
})),
|
|
178
|
+
line: pools.flatMap(pool =>
|
|
179
|
+
board.waitlist
|
|
180
|
+
.filter(t => t.pool === pool)
|
|
181
|
+
.sort((a, b) => a.since - b.since)
|
|
182
|
+
.map((t, i) => ({
|
|
183
|
+
pool,
|
|
184
|
+
session: letter(t.sessionId),
|
|
185
|
+
position: i + 1,
|
|
186
|
+
since: t.since,
|
|
187
|
+
heldUntil: t.holdUntil ?? null,
|
|
188
|
+
})),
|
|
189
|
+
),
|
|
190
|
+
agents: agentList(board),
|
|
191
|
+
claims: board.claims.map(c => ({
|
|
192
|
+
kind: c.kind,
|
|
193
|
+
target: c.target,
|
|
194
|
+
session: letter(c.sessionId),
|
|
195
|
+
agent: c.agentId,
|
|
196
|
+
note: c.note,
|
|
197
|
+
since: c.since,
|
|
198
|
+
})),
|
|
199
|
+
queues: Object.entries(config.queues).map(([name, q]) => ({
|
|
200
|
+
name,
|
|
201
|
+
order: q.order,
|
|
202
|
+
slots: slotsOf(board, q, now),
|
|
203
|
+
configuredSlots: q.slots,
|
|
204
|
+
running: board.jobs.filter(j => j.queue === name && j.state === "running").map(j => job(j, null)),
|
|
205
|
+
waiting: orderWaiting(q, name, board, now).map((j, i) => job(j, i + 1)),
|
|
206
|
+
})),
|
|
207
|
+
notes: board.notes.slice(-LATEST_NOTES).map(n => ({
|
|
208
|
+
id: n.id,
|
|
209
|
+
at: n.at,
|
|
210
|
+
session: n.label,
|
|
211
|
+
author: n.author,
|
|
212
|
+
text: n.text,
|
|
213
|
+
})),
|
|
214
|
+
};
|
|
215
|
+
}
|
package/src/domain/queues.ts
CHANGED
|
@@ -43,6 +43,7 @@ export interface JobRequest {
|
|
|
43
43
|
id: string;
|
|
44
44
|
queue: string;
|
|
45
45
|
owner: string | null;
|
|
46
|
+
agent?: string;
|
|
46
47
|
lane?: string;
|
|
47
48
|
priority: number;
|
|
48
49
|
key?: string;
|
|
@@ -81,6 +82,7 @@ export function enqueueJob(board: Board, queues: Queues, req: JobRequest): Enque
|
|
|
81
82
|
id: req.id,
|
|
82
83
|
queue: req.queue,
|
|
83
84
|
owner: req.owner,
|
|
85
|
+
agent: req.agent,
|
|
84
86
|
lane,
|
|
85
87
|
priority: req.priority,
|
|
86
88
|
key: req.key,
|
|
@@ -97,6 +99,7 @@ export function enqueueJob(board: Board, queues: Queues, req: JobRequest): Enque
|
|
|
97
99
|
lane,
|
|
98
100
|
priority: req.priority,
|
|
99
101
|
...(req.key === undefined ? {} : { key: req.key }),
|
|
102
|
+
...(req.agent === undefined ? {} : { agent: req.agent }),
|
|
100
103
|
});
|
|
101
104
|
schedule(board, queues, req.now);
|
|
102
105
|
return { ok: true };
|