@nledford/omp-coord 0.4.1 → 0.4.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 +9 -1
- package/docs/commands.md +1 -1
- package/docs/event-log.md +1 -1
- package/docs/queues.md +2 -1
- package/package.json +1 -1
- package/src/adapters/cli/cli.ts +11 -3
- package/src/adapters/sqlite/store.ts +6 -1
- package/src/adapters/text/report.ts +1 -1
- package/src/app/coordinator.ts +1 -1
- package/src/app/job-runner.ts +2 -0
- package/src/domain/history.ts +39 -1
- package/src/domain/model.ts +2 -0
- package/src/domain/queues.ts +30 -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.4.
|
|
10
|
+
"version": "0.4.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,13 @@ Add each user-visible change under Unreleased as it lands, in one of: Added, Cha
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.2] - 2026-11-02
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `omp-coord run --meta k=v` (repeatable, at most 8; the key matches `[a-z][a-z0-9_-]{0,31}`, the value is at most 200 characters on one line) tags a job. The tags go into the `job_queued` and `job_ended` events as `meta`, and `job_ended` now also carries the job's `key`. A script can detect support with `omp-coord --help 2>&1 | grep -q -- '--meta'`. The jobs table gains a nullable `meta` column (no schema version bump).
|
|
14
|
+
- `omp-coord stats` and `/coord stats` add `; of the failures: N repeat an unchanged failure, M intended` to a queue's line when a failed job of that day had tags: a repeat is a failed job whose `fp` tag equals that of an earlier failed job in the same queue, with no successful job of that `fp` in between; intended is `intent=failfirst`.
|
|
15
|
+
|
|
9
16
|
## [0.4.1] - 2026-10-30
|
|
10
17
|
|
|
11
18
|
### Added
|
|
@@ -113,7 +120,8 @@ Add each user-visible change under Unreleased as it lands, in one of: Added, Cha
|
|
|
113
120
|
- An SQLite event log per scope, with retention.
|
|
114
121
|
- Packaging as an omp plugin, with a marketplace catalog; queued commands run on omp's own Bun.
|
|
115
122
|
|
|
116
|
-
[Unreleased]: https://github.com/nledford/omp-coord/compare/v0.4.
|
|
123
|
+
[Unreleased]: https://github.com/nledford/omp-coord/compare/v0.4.2...HEAD
|
|
124
|
+
[0.4.2]: https://github.com/nledford/omp-coord/compare/v0.4.1...v0.4.2
|
|
117
125
|
[0.4.1]: https://github.com/nledford/omp-coord/compare/v0.4.0...v0.4.1
|
|
118
126
|
[0.4.0]: https://github.com/nledford/omp-coord/compare/v0.3.2...v0.4.0
|
|
119
127
|
[0.3.2]: https://github.com/nledford/omp-coord/compare/v0.3.1...v0.3.2
|
package/docs/commands.md
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
## `omp-coord`
|
|
27
27
|
|
|
28
28
|
```sh
|
|
29
|
-
omp-coord run <queue> [--lane L] [--priority N] [--key K] [--owner SESSION] [--timeout 90m] -- <command> [args…]
|
|
29
|
+
omp-coord run <queue> [--lane L] [--priority N] [--key K] [--owner SESSION] [--timeout 90m] [--meta k=v]... -- <command> [args…]
|
|
30
30
|
omp-coord status [--agents | --json] | stats [days] | interrupted [hours] | notes [count] | note <text…>
|
|
31
31
|
omp-coord pause [reason…] | resume | doctor [--fix]
|
|
32
32
|
```
|
package/docs/event-log.md
CHANGED
|
@@ -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`, 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`.
|
|
28
|
+
- Jobs: `job_queued` (with `queue`, `lane`, `priority`, `key`, and `agent` and `meta` when recorded), `job_refused` (with `reason`), `job_started` (with `waitedMs`), `job_ended` (with `exitCode`, `ranMs`, `key` and `meta` when the job had them, 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`), `write_refused` (a file-tool write or a state-changing `git` command refused in a protected checkout: `agentId`, `tool` (`bash` for git), `paths` as the agent wrote them, empty for git), `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
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
`omp-coord run <queue> -- <command>` waits for one of the queue's slots, shared by everything on this machine in the same scope, then runs the command. `just` recipes, scripts and terminals call it; agents' matching bash commands are rewritten to go through it. Queues are set under `queues` in the [configuration](configuration.md).
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
omp-coord run <queue> [--lane L] [--priority N] [--key K] [--owner SESSION] [--timeout 90m] -- <command> [args…]
|
|
6
|
+
omp-coord run <queue> [--lane L] [--priority N] [--key K] [--owner SESSION] [--timeout 90m] [--meta k=v]... -- <command> [args…]
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
## Running a job
|
|
@@ -14,6 +14,7 @@ The command runs in its own process group and session, so a time limit stops eve
|
|
|
14
14
|
|
|
15
15
|
- **Exit codes**: the command's own, `75` when the job was not run (a refused or superseded duplicate), `124` when its time limit stopped it, `2` for a usage or configuration error.
|
|
16
16
|
- **Owner**: jobs belong to the omp session in `OMP_COORD_SESSION` (set by the extension, so commands an agent runs inherit it) or `--owner`; otherwise to no session ("outside omp").
|
|
17
|
+
- **Tags**: `--meta k=v` (repeatable, at most 8; a repeated key: the last wins) records tags on the job's `job_queued` and `job_ended` events. A key matches `[a-z][a-z0-9_-]{0,31}`; a value is at most 200 characters on one line. Scripts can tag a job with a fingerprint of the code state (`fp`) and mark a deliberate failure (`intent=failfirst`); `omp-coord stats` then adds `; of the failures: N repeat an unchanged failure, M intended` to the queue's line. A repeat is a failed job whose `fp` equals that of an earlier failed job in the same queue inside the stats window, with no successful job of that `fp` in between. Detect support with `omp-coord --help 2>&1 | grep -q -- '--meta'`.
|
|
17
18
|
- **Duplicates**: jobs with the same `--key` (for example `"<checkout> ci"`) are duplicates. One that duplicates a running job is refused; one that duplicates a waiting job is refused or, with `onDuplicate: "supersede"`, replaces it in its place in line.
|
|
18
19
|
|
|
19
20
|
## Order
|
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 when a rule queues them), and
|
|
5
5
|
* reports on the scope.
|
|
6
6
|
*
|
|
7
|
-
* omp-coord run <queue> [--lane L] [--priority N] [--key K] [--owner SESSION] [--timeout 90m] -- <command> [args…]
|
|
7
|
+
* omp-coord run <queue> [--lane L] [--priority N] [--key K] [--owner SESSION] [--timeout 90m] [--meta k=v]... -- <command> [args…]
|
|
8
8
|
* omp-coord status [--agents | --json] | stats [days] | interrupted [hours] | notes [count] | note <text…>
|
|
9
9
|
* omp-coord pause [reason…] | resume | doctor [--fix]
|
|
10
10
|
*
|
|
@@ -24,7 +24,7 @@ import { doctor } from "../../app/doctor.ts";
|
|
|
24
24
|
import { EXIT_TIMED_OUT, runJob } from "../../app/job-runner.ts";
|
|
25
25
|
import { type Config, ConfigError, findConfigFile, loadConfig } from "../../config.ts";
|
|
26
26
|
import { statusView } from "../../domain/overview.ts";
|
|
27
|
-
import { MAX_PRIORITY } from "../../domain/queues.ts";
|
|
27
|
+
import { MAX_PRIORITY, parseMeta } from "../../domain/queues.ts";
|
|
28
28
|
import { agentOfOmpSession } from "../../domain/sessions.ts";
|
|
29
29
|
import { osLoad } from "../os/load.ts";
|
|
30
30
|
import { groupRunning } from "../os/process-group.ts";
|
|
@@ -45,7 +45,7 @@ const KILL_GRACE_MS = 10_000;
|
|
|
45
45
|
const DAY = 86_400_000;
|
|
46
46
|
|
|
47
47
|
const USAGE = `usage:
|
|
48
|
-
omp-coord run <queue> [--lane L] [--priority N] [--key K] [--owner SESSION] [--timeout 90m] -- <command…>
|
|
48
|
+
omp-coord run <queue> [--lane L] [--priority N] [--key K] [--owner SESSION] [--timeout 90m] [--meta k=v]... -- <command…>
|
|
49
49
|
omp-coord status [--agents | --json] | stats [days] | interrupted [hours] | notes [count] | note <text…>
|
|
50
50
|
omp-coord pause [reason…] | resume | doctor [--fix]`;
|
|
51
51
|
|
|
@@ -206,6 +206,12 @@ async function run(args: string[]): Promise<number> {
|
|
|
206
206
|
if (!Number.isSafeInteger(priority) || Math.abs(priority) > MAX_PRIORITY) {
|
|
207
207
|
throw new UsageError(`omp-coord run: --priority must be a whole number from -${MAX_PRIORITY} to ${MAX_PRIORITY}`);
|
|
208
208
|
}
|
|
209
|
+
let meta: ReturnType<typeof parseMeta>;
|
|
210
|
+
try {
|
|
211
|
+
meta = parseMeta(values.meta ?? []);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
throw new UsageError(`omp-coord run: ${error instanceof Error ? error.message : String(error)}`);
|
|
214
|
+
}
|
|
209
215
|
const timeoutMs = values.timeout === undefined ? undefined : parseDuration(values.timeout);
|
|
210
216
|
const command = args.slice(split + 1);
|
|
211
217
|
|
|
@@ -258,6 +264,7 @@ async function run(args: string[]): Promise<number> {
|
|
|
258
264
|
...(values.lane === undefined ? {} : { lane: values.lane }),
|
|
259
265
|
priority,
|
|
260
266
|
...(values.key === undefined ? {} : { key: values.key }),
|
|
267
|
+
...(meta === undefined ? {} : { meta }),
|
|
261
268
|
command: command.join(" "),
|
|
262
269
|
pid: process.pid,
|
|
263
270
|
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
@@ -293,6 +300,7 @@ function parseRunFlags(args: string[]) {
|
|
|
293
300
|
key: { type: "string" },
|
|
294
301
|
owner: { type: "string" },
|
|
295
302
|
timeout: { type: "string" },
|
|
303
|
+
meta: { type: "string", multiple: true },
|
|
296
304
|
},
|
|
297
305
|
});
|
|
298
306
|
}
|
|
@@ -81,6 +81,7 @@ CREATE TABLE jobs (
|
|
|
81
81
|
key TEXT,
|
|
82
82
|
command TEXT NOT NULL,
|
|
83
83
|
agent TEXT,
|
|
84
|
+
meta TEXT,
|
|
84
85
|
socket TEXT NOT NULL,
|
|
85
86
|
pid INTEGER NOT NULL,
|
|
86
87
|
state TEXT NOT NULL CHECK (state IN ('waiting', 'running', 'superseded')),
|
|
@@ -258,6 +259,7 @@ export class BoardStore implements BoardRepository {
|
|
|
258
259
|
.all()
|
|
259
260
|
.some(c => c.name === column);
|
|
260
261
|
if (!has("jobs", "agent")) this.#db.exec("ALTER TABLE jobs ADD COLUMN agent TEXT");
|
|
262
|
+
if (!has("jobs", "meta")) this.#db.exec("ALTER TABLE jobs ADD COLUMN meta TEXT");
|
|
261
263
|
if (!has("slots", "unqueued"))
|
|
262
264
|
this.#db.exec("ALTER TABLE slots ADD COLUMN unqueued TEXT CHECK (unqueued IN ('revived', 'eval'))");
|
|
263
265
|
if (!has("sessions", "omp_session")) this.#db.exec("ALTER TABLE sessions ADD COLUMN omp_session TEXT");
|
|
@@ -369,6 +371,7 @@ export class BoardStore implements BoardRepository {
|
|
|
369
371
|
key: string | null;
|
|
370
372
|
command: string;
|
|
371
373
|
agent: string | null;
|
|
374
|
+
meta: string | null;
|
|
372
375
|
socket: string;
|
|
373
376
|
pid: number;
|
|
374
377
|
state: "waiting" | "running" | "superseded";
|
|
@@ -387,6 +390,7 @@ export class BoardStore implements BoardRepository {
|
|
|
387
390
|
key: r.key ?? undefined,
|
|
388
391
|
command: r.command,
|
|
389
392
|
agent: r.agent ?? undefined,
|
|
393
|
+
meta: r.meta === null ? undefined : (JSON.parse(r.meta) as Record<string, string>),
|
|
390
394
|
socket: r.socket,
|
|
391
395
|
pid: r.pid,
|
|
392
396
|
state: r.state,
|
|
@@ -499,7 +503,7 @@ export class BoardStore implements BoardRepository {
|
|
|
499
503
|
const sent = db.query("INSERT INTO sent (session_id, at) VALUES (?, ?)");
|
|
500
504
|
for (const x of board.sent) sent.run(x.sessionId, x.at);
|
|
501
505
|
const job = db.query(
|
|
502
|
-
"INSERT INTO jobs (id, queue, owner, lane, priority, key, command, agent, socket, pid, state, since, started_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
506
|
+
"INSERT INTO jobs (id, queue, owner, lane, priority, key, command, agent, meta, socket, pid, state, since, started_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
503
507
|
);
|
|
504
508
|
for (const j of board.jobs) {
|
|
505
509
|
job.run(
|
|
@@ -511,6 +515,7 @@ export class BoardStore implements BoardRepository {
|
|
|
511
515
|
j.key ?? null,
|
|
512
516
|
j.command,
|
|
513
517
|
j.agent ?? null,
|
|
518
|
+
j.meta === undefined ? null : JSON.stringify(j.meta),
|
|
514
519
|
j.socket,
|
|
515
520
|
j.pid,
|
|
516
521
|
j.state,
|
|
@@ -212,7 +212,7 @@ export function formatStats(days: DayStats[]): string {
|
|
|
212
212
|
);
|
|
213
213
|
for (const [q, s] of Object.entries(d.queues)) {
|
|
214
214
|
lines.push(
|
|
215
|
-
` ${q}: ${s.started} started, wait avg ${minutes(s.avgWaitMs)} / max ${minutes(s.maxWaitMs)}; ${s.ended} ended, ${s.failed} failed${s.timedOut ? ` (${s.timedOut} timed out)` : ""}`,
|
|
215
|
+
` ${q}: ${s.started} started, wait avg ${minutes(s.avgWaitMs)} / max ${minutes(s.maxWaitMs)}; ${s.ended} ended, ${s.failed} failed${s.timedOut ? ` (${s.timedOut} timed out)` : ""}${s.ofFailures ? `; of the failures: ${s.ofFailures.repeats} repeat an unchanged failure, ${s.ofFailures.intended} intended` : ""}`,
|
|
216
216
|
);
|
|
217
217
|
}
|
|
218
218
|
}
|
package/src/app/coordinator.ts
CHANGED
|
@@ -161,7 +161,7 @@ export class Coordinator {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
stats(sinceMs: number): DayStats[] {
|
|
164
|
-
return stats(this.repo.events(this.clock.now() - sinceMs)
|
|
164
|
+
return stats(this.repo.events(), this.clock.now() - sinceMs).days;
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
requestSpawn(req: Omit<SpawnRequest, "now">): SpawnDecision {
|
package/src/app/job-runner.ts
CHANGED
package/src/domain/history.ts
CHANGED
|
@@ -67,6 +67,8 @@ export interface QueueDay {
|
|
|
67
67
|
/** Ended with a nonzero exit code, a signal, or a timeout. */
|
|
68
68
|
failed: number;
|
|
69
69
|
timedOut: number;
|
|
70
|
+
/** Present when any failed job of the day carried `--meta` tags. */
|
|
71
|
+
ofFailures?: { repeats: number; intended: number };
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export interface DayStats {
|
|
@@ -80,7 +82,16 @@ export interface DayStats {
|
|
|
80
82
|
queues: Record<string, QueueDay>;
|
|
81
83
|
}
|
|
82
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Per queue and day: failures that repeat an unchanged failure (a failed job with `meta.fp` equal to that of an
|
|
87
|
+
* earlier failed job of the same queue in the window, with no successful job of that `fp` in between) and failures
|
|
88
|
+
* marked `meta.intent == "failfirst"`.
|
|
89
|
+
*/
|
|
83
90
|
export function stats(events: BoardEvent[], since: number): { days: DayStats[] } {
|
|
91
|
+
// Per queue: fingerprints whose latest outcome was a failure.
|
|
92
|
+
const failing = new Map<string, Set<string>>();
|
|
93
|
+
// Tags seen at queue time, for a job_ended that lost them (an older omp-coord still running rewrote the job without them).
|
|
94
|
+
const queuedMeta = new Map<string, unknown>();
|
|
84
95
|
const days = new Map<string, DayStats & { waits: Record<string, number[]> }>();
|
|
85
96
|
const dayOf = (at: number) => {
|
|
86
97
|
const d = new Date(at);
|
|
@@ -96,6 +107,11 @@ export function stats(events: BoardEvent[], since: number): { days: DayStats[] }
|
|
|
96
107
|
(s.queues[q] ??= { started: 0, avgWaitMs: 0, maxWaitMs: 0, ended: 0, failed: 0, timedOut: 0 });
|
|
97
108
|
|
|
98
109
|
for (const e of events) {
|
|
110
|
+
// Tags are indexed from the whole log, so a job queued before the window still explains its end inside it.
|
|
111
|
+
if (e.type === "job_queued") {
|
|
112
|
+
if (e.data.meta === undefined) queuedMeta.delete(String(e.data.job));
|
|
113
|
+
else queuedMeta.set(String(e.data.job), e.data.meta);
|
|
114
|
+
}
|
|
99
115
|
if (e.at < since) continue;
|
|
100
116
|
const s = dayOf(e.at);
|
|
101
117
|
const n = (k: string) => (typeof e.data[k] === "number" ? (e.data[k] as number) : 0);
|
|
@@ -117,8 +133,30 @@ export function stats(events: BoardEvent[], since: number): { days: DayStats[] }
|
|
|
117
133
|
if (e.type === "job_ended") {
|
|
118
134
|
const q = queueOf(s, String(e.data.queue));
|
|
119
135
|
q.ended++;
|
|
120
|
-
|
|
136
|
+
const failed = e.data.exitCode !== 0 || e.data.timedOut === true;
|
|
137
|
+
if (failed) q.failed++;
|
|
121
138
|
if (e.data.timedOut === true) q.timedOut++;
|
|
139
|
+
const jobId = String(e.data.job);
|
|
140
|
+
const meta = e.data.meta ?? queuedMeta.get(jobId);
|
|
141
|
+
queuedMeta.delete(jobId);
|
|
142
|
+
if (typeof meta === "object" && meta !== null) {
|
|
143
|
+
const tags = meta as Record<string, unknown>;
|
|
144
|
+
const fp = typeof tags.fp === "string" ? tags.fp : undefined;
|
|
145
|
+
const queueName = String(e.data.queue);
|
|
146
|
+
const set = failing.get(queueName) ?? new Set<string>();
|
|
147
|
+
failing.set(queueName, set);
|
|
148
|
+
if (failed) {
|
|
149
|
+
const counts = q.ofFailures ?? { repeats: 0, intended: 0 };
|
|
150
|
+
q.ofFailures = counts;
|
|
151
|
+
if (fp !== undefined) {
|
|
152
|
+
if (set.has(fp)) counts.repeats++;
|
|
153
|
+
set.add(fp);
|
|
154
|
+
}
|
|
155
|
+
if (tags.intent === "failfirst") counts.intended++;
|
|
156
|
+
} else if (fp !== undefined) {
|
|
157
|
+
set.delete(fp);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
122
160
|
}
|
|
123
161
|
}
|
|
124
162
|
return {
|
package/src/domain/model.ts
CHANGED
|
@@ -181,6 +181,8 @@ export interface Job {
|
|
|
181
181
|
priority: number;
|
|
182
182
|
/** Jobs with the same key are duplicates (e.g. "<checkout> ci"). */
|
|
183
183
|
key: string | undefined;
|
|
184
|
+
/** Tags the caller gave (`--meta k=v`), kept in the event log; undefined when none. */
|
|
185
|
+
meta?: Record<string, string> | undefined;
|
|
184
186
|
/** What runs, for display. */
|
|
185
187
|
command: string;
|
|
186
188
|
/** The runner's liveness socket and process id. */
|
package/src/domain/queues.ts
CHANGED
|
@@ -47,12 +47,38 @@ export interface JobRequest {
|
|
|
47
47
|
lane?: string;
|
|
48
48
|
priority: number;
|
|
49
49
|
key?: string;
|
|
50
|
+
meta?: Record<string, string>;
|
|
50
51
|
command: string;
|
|
51
52
|
socket: string;
|
|
52
53
|
pid: number;
|
|
53
54
|
now: number;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
export const MAX_META = 8;
|
|
58
|
+
const META_KEY = /^[a-z][a-z0-9_-]{0,31}$/;
|
|
59
|
+
const MAX_META_VALUE = 200;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parses repeated `--meta k=v` values into tags (a repeated key: the last wins). Throws an Error naming the problem:
|
|
63
|
+
* more than `MAX_META` pairs, no `=`, a key outside `[a-z][a-z0-9_-]{0,31}`, a value over 200 characters or with a
|
|
64
|
+
* newline. Returns undefined for no pairs.
|
|
65
|
+
*/
|
|
66
|
+
export function parseMeta(pairs: string[]): Record<string, string> | undefined {
|
|
67
|
+
if (pairs.length > MAX_META) throw new Error(`--meta may be given at most ${MAX_META} times`);
|
|
68
|
+
const meta: Record<string, string> = {};
|
|
69
|
+
for (const pair of pairs) {
|
|
70
|
+
const eq = pair.indexOf("=");
|
|
71
|
+
if (eq < 0) throw new Error(`--meta takes k=v, not "${pair}"`);
|
|
72
|
+
const k = pair.slice(0, eq);
|
|
73
|
+
const v = pair.slice(eq + 1);
|
|
74
|
+
if (!META_KEY.test(k)) throw new Error(`--meta key "${k}" must match [a-z][a-z0-9_-]{0,31}`);
|
|
75
|
+
if (v.length > MAX_META_VALUE) throw new Error(`--meta ${k}: the value is over ${MAX_META_VALUE} characters`);
|
|
76
|
+
if (/[\r\n]/.test(v)) throw new Error(`--meta ${k}: the value must not contain a newline`);
|
|
77
|
+
meta[k] = v;
|
|
78
|
+
}
|
|
79
|
+
return Object.keys(meta).length ? meta : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
56
82
|
export type EnqueueResult = { ok: true } | { ok: false; reason: string };
|
|
57
83
|
|
|
58
84
|
/** Puts a job in line (or starts it at once when a slot is free). */
|
|
@@ -86,6 +112,7 @@ export function enqueueJob(board: Board, queues: Queues, req: JobRequest): Enque
|
|
|
86
112
|
lane,
|
|
87
113
|
priority: req.priority,
|
|
88
114
|
key: req.key,
|
|
115
|
+
meta: req.meta,
|
|
89
116
|
command: req.command,
|
|
90
117
|
socket: req.socket,
|
|
91
118
|
pid: req.pid,
|
|
@@ -100,6 +127,7 @@ export function enqueueJob(board: Board, queues: Queues, req: JobRequest): Enque
|
|
|
100
127
|
priority: req.priority,
|
|
101
128
|
...(req.key === undefined ? {} : { key: req.key }),
|
|
102
129
|
...(req.agent === undefined ? {} : { agent: req.agent }),
|
|
130
|
+
...(req.meta === undefined ? {} : { meta: req.meta }),
|
|
103
131
|
});
|
|
104
132
|
schedule(board, queues, req.now);
|
|
105
133
|
return { ok: true };
|
|
@@ -128,6 +156,8 @@ export function jobEnded(
|
|
|
128
156
|
queue: job.queue,
|
|
129
157
|
exitCode: j.exitCode,
|
|
130
158
|
ranMs: j.now - (job.startedAt ?? j.now),
|
|
159
|
+
...(job.key === undefined ? {} : { key: job.key }),
|
|
160
|
+
...(job.meta === undefined ? {} : { meta: job.meta }),
|
|
131
161
|
...(j.timedOut ? { timedOut: true } : {}),
|
|
132
162
|
});
|
|
133
163
|
} else if (job.state === "waiting") {
|