harnery 0.45.2 → 0.46.1
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/dist/commander.d.ts +13 -0
- package/dist/commander.d.ts.map +1 -1
- package/dist/commands/admission.d.ts.map +1 -1
- package/dist/commands/admission.js +214 -27
- package/dist/commands/agents.d.ts +4 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +102 -25
- package/dist/commands/browse.d.ts +3 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +172 -8
- package/dist/commands/fetch.js +1 -0
- package/dist/commands/ledger-v3.d.ts.map +1 -1
- package/dist/commands/ledger-v3.js +8 -2
- package/dist/commands/presence.d.ts.map +1 -1
- package/dist/commands/presence.js +40 -0
- package/dist/commands/qa-run.d.ts.map +1 -1
- package/dist/commands/qa-run.js +4 -3
- package/dist/commands/review-pack.d.ts.map +1 -1
- package/dist/commands/review-pack.js +33 -6
- package/dist/core/agents/index.d.ts +1 -0
- package/dist/core/agents/index.d.ts.map +1 -1
- package/dist/core/agents/index.js +1 -0
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +11 -4
- package/dist/core/agents/render/session-context.d.ts +0 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +1 -6
- package/dist/core/agents/rules/claim-conflict.d.ts.map +1 -1
- package/dist/core/agents/rules/claim-conflict.js +36 -16
- package/dist/core/agents/rules/stop-hook.d.ts +6 -0
- package/dist/core/agents/rules/stop-hook.d.ts.map +1 -1
- package/dist/core/agents/rules/stop-hook.js +20 -1
- package/dist/core/agents/session-events.d.ts.map +1 -1
- package/dist/core/agents/session-events.js +12 -4
- package/dist/core/agents/state/heartbeat-reader.d.ts +3 -0
- package/dist/core/agents/state/heartbeat-reader.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.d.ts +3 -1
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +6 -2
- package/dist/core/agents/state/parent-reference-contract.d.ts +16 -0
- package/dist/core/agents/state/parent-reference-contract.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference-contract.js +1 -0
- package/dist/core/agents/state/parent-reference.d.ts +18 -0
- package/dist/core/agents/state/parent-reference.d.ts.map +1 -0
- package/dist/core/agents/state/parent-reference.js +119 -0
- package/dist/core/config.d.ts +39 -0
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +63 -0
- package/dist/core/diagnostics/path-identity.d.ts +1 -1
- package/dist/core/diagnostics/path-identity.d.ts.map +1 -1
- package/dist/core/diagnostics/path-identity.js +51 -6
- package/dist/core/events/v3/canonical.js +43 -14
- package/dist/core/events/v3/index.d.ts +1 -1
- package/dist/core/events/v3/index.d.ts.map +1 -1
- package/dist/core/events/v3/index.js +1 -1
- package/dist/core/events/v3/producers/recorder.d.ts +34 -0
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +117 -13
- package/dist/core/events/v3/reader.d.ts.map +1 -1
- package/dist/core/events/v3/reader.js +53 -13
- package/dist/core/events/v3/validate.d.ts.map +1 -1
- package/dist/core/events/v3/validate.js +36 -1
- package/dist/core/hooks/adapter/native-parent.d.ts +23 -0
- package/dist/core/hooks/adapter/native-parent.d.ts.map +1 -0
- package/dist/core/hooks/adapter/native-parent.js +91 -0
- package/dist/core/hooks/cli.js +103 -5
- package/dist/core/hooks/resolve/owner.d.ts +0 -11
- package/dist/core/hooks/resolve/owner.d.ts.map +1 -1
- package/dist/core/hooks/resolve/owner.js +0 -23
- package/dist/core/hooks/resolve/transcript.d.ts +12 -0
- package/dist/core/hooks/resolve/transcript.d.ts.map +1 -1
- package/dist/core/hooks/resolve/transcript.js +54 -0
- package/dist/core/hooks/shell-admission.d.ts +84 -0
- package/dist/core/hooks/shell-admission.d.ts.map +1 -0
- package/dist/core/hooks/shell-admission.js +622 -0
- package/dist/core/hooks/shell-commit-tree.d.ts +2 -0
- package/dist/core/hooks/shell-commit-tree.d.ts.map +1 -0
- package/dist/core/hooks/shell-commit-tree.js +104 -0
- package/dist/core/inbox/contract.d.ts +12 -0
- package/dist/core/inbox/contract.d.ts.map +1 -1
- package/dist/core/inbox/contract.js +16 -0
- package/dist/core/inbox/peer-messages.d.ts +41 -16
- package/dist/core/inbox/peer-messages.d.ts.map +1 -1
- package/dist/core/inbox/peer-messages.js +372 -15
- package/dist/core/inbox/service.d.ts +8 -1
- package/dist/core/inbox/service.d.ts.map +1 -1
- package/dist/core/inbox/service.js +83 -3
- package/dist/core/inbox/targets.d.ts +17 -3
- package/dist/core/inbox/targets.d.ts.map +1 -1
- package/dist/core/inbox/targets.js +77 -2
- package/dist/core/presence/git.d.ts +18 -12
- package/dist/core/presence/git.d.ts.map +1 -1
- package/dist/core/presence/git.js +32 -38
- package/dist/core/presence/index.d.ts +2 -2
- package/dist/core/presence/index.d.ts.map +1 -1
- package/dist/core/presence/index.js +8 -6
- package/dist/core/presence/mail.d.ts +133 -0
- package/dist/core/presence/mail.d.ts.map +1 -0
- package/dist/core/presence/mail.js +464 -0
- package/dist/core/semantic/contract.d.ts +5 -5
- package/dist/core/storage/modes.d.ts +4 -0
- package/dist/core/storage/modes.d.ts.map +1 -1
- package/dist/core/storage/modes.js +4 -0
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +5 -4
- package/dist/lib/admission-history.d.ts +96 -0
- package/dist/lib/admission-history.d.ts.map +1 -0
- package/dist/lib/admission-history.js +196 -0
- package/dist/lib/admission.d.ts +56 -7
- package/dist/lib/admission.d.ts.map +1 -1
- package/dist/lib/admission.js +161 -19
- package/dist/lib/browser/client.d.ts +18 -0
- package/dist/lib/browser/client.d.ts.map +1 -1
- package/dist/lib/browser/client.js +100 -12
- package/dist/lib/browser/content-checks.d.ts +3 -1
- package/dist/lib/browser/content-checks.d.ts.map +1 -1
- package/dist/lib/browser/content-checks.js +10 -2
- package/dist/lib/browser/critique.d.ts +32 -0
- package/dist/lib/browser/critique.d.ts.map +1 -1
- package/dist/lib/browser/critique.js +72 -3
- package/dist/lib/browser/geometry.d.ts +2 -0
- package/dist/lib/browser/geometry.d.ts.map +1 -1
- package/dist/lib/browser/geometry.js +61 -15
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/index.js +1 -1
- package/dist/lib/browser/page-review-contracts.d.ts +6 -1
- package/dist/lib/browser/page-review-contracts.d.ts.map +1 -1
- package/dist/lib/browser/page-review-contracts.js +6 -1
- package/dist/lib/browser/page-review-judge.d.ts +3 -1
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +41 -11
- package/dist/lib/browser/page-review-pack.d.ts +3 -0
- package/dist/lib/browser/page-review-pack.d.ts.map +1 -1
- package/dist/lib/browser/page-review-pack.js +5 -2
- package/dist/lib/browser/qa-run-contracts.d.ts +11 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run-contracts.js +11 -4
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +73 -16
- package/dist/lib/browser/runts.d.ts +4 -2
- package/dist/lib/browser/runts.d.ts.map +1 -1
- package/dist/lib/browser/runts.js +84 -33
- package/dist/lib/browser/windows-chrome.d.ts +14 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -1
- package/dist/lib/browser/windows-chrome.js +42 -0
- package/dist/lib/browser/windows-login-sync.d.ts +110 -0
- package/dist/lib/browser/windows-login-sync.d.ts.map +1 -0
- package/dist/lib/browser/windows-login-sync.js +171 -0
- package/dist/lib/cookies/client.d.ts +30 -5
- package/dist/lib/cookies/client.d.ts.map +1 -1
- package/dist/lib/cookies/client.js +55 -10
- package/dist/lib/cookies/index.d.ts +1 -1
- package/dist/lib/cookies/index.d.ts.map +1 -1
- package/dist/lib/cookies/index.js +1 -1
- package/dist/lib/docs-metadata-sync.d.ts.map +1 -1
- package/dist/lib/docs-metadata-sync.js +41 -2
- package/dist/lib/durable-job.d.ts +8 -0
- package/dist/lib/durable-job.d.ts.map +1 -1
- package/dist/lib/durable-job.js +10 -0
- package/dist/lib/http/client.d.ts +9 -8
- package/dist/lib/http/client.d.ts.map +1 -1
- package/dist/lib/http/client.js +44 -2
- package/dist/lib/instructions/templates.js +4 -4
- package/package.json +2 -2
- package/src/commander.ts +13 -0
- package/src/commands/admission.ts +272 -29
- package/src/commands/agents.ts +132 -27
- package/src/commands/browse.ts +208 -6
- package/src/commands/fetch.ts +1 -0
- package/src/commands/ledger-v3.ts +8 -2
- package/src/commands/presence.ts +49 -0
- package/src/commands/qa-run.ts +4 -3
- package/src/commands/review-pack.ts +41 -5
- package/src/core/agents/index.ts +6 -0
- package/src/core/agents/live-authority-v3.ts +11 -4
- package/src/core/agents/render/session-context.ts +1 -7
- package/src/core/agents/rules/claim-conflict.ts +50 -18
- package/src/core/agents/rules/stop-hook.ts +28 -1
- package/src/core/agents/session-events.ts +18 -3
- package/src/core/agents/state/heartbeat-reader.ts +3 -0
- package/src/core/agents/state/live-coordination-view.ts +13 -6
- package/src/core/agents/state/parent-reference-contract.ts +14 -0
- package/src/core/agents/state/parent-reference.ts +179 -0
- package/src/core/config.ts +85 -0
- package/src/core/diagnostics/path-identity.ts +52 -6
- package/src/core/events/v3/canonical.ts +46 -13
- package/src/core/events/v3/index.ts +3 -0
- package/src/core/events/v3/producers/recorder.ts +151 -13
- package/src/core/events/v3/reader.ts +55 -11
- package/src/core/events/v3/validate.ts +42 -1
- package/src/core/hooks/adapter/native-parent.ts +109 -0
- package/src/core/hooks/cli.ts +110 -4
- package/src/core/hooks/resolve/owner.ts +0 -23
- package/src/core/hooks/resolve/transcript.ts +61 -0
- package/src/core/hooks/shell-admission.ts +634 -0
- package/src/core/hooks/shell-commit-tree.ts +102 -0
- package/src/core/inbox/contract.ts +18 -0
- package/src/core/inbox/peer-messages.ts +501 -15
- package/src/core/inbox/service.ts +108 -3
- package/src/core/inbox/targets.ts +94 -3
- package/src/core/presence/git.ts +45 -37
- package/src/core/presence/index.ts +16 -8
- package/src/core/presence/mail.ts +620 -0
- package/src/core/storage/modes.ts +6 -0
- package/src/core/storage/segments.ts +5 -4
- package/src/lib/admission-history.ts +267 -0
- package/src/lib/admission.ts +217 -19
- package/src/lib/browser/client.ts +147 -35
- package/src/lib/browser/content-checks.ts +14 -3
- package/src/lib/browser/critique.ts +113 -3
- package/src/lib/browser/geometry.ts +63 -14
- package/src/lib/browser/index.ts +5 -0
- package/src/lib/browser/page-review-contracts.ts +6 -1
- package/src/lib/browser/page-review-judge.ts +58 -13
- package/src/lib/browser/page-review-pack.ts +13 -2
- package/src/lib/browser/qa-run-contracts.ts +30 -4
- package/src/lib/browser/qa-run.ts +78 -17
- package/src/lib/browser/runts.ts +85 -37
- package/src/lib/browser/windows-chrome.ts +50 -0
- package/src/lib/browser/windows-login-sync.ts +275 -0
- package/src/lib/cookies/client.ts +62 -10
- package/src/lib/cookies/index.ts +3 -0
- package/src/lib/docs-metadata-sync.ts +44 -2
- package/src/lib/durable-job.ts +15 -0
- package/src/lib/http/client.ts +63 -1
- package/src/lib/instructions/templates.ts +4 -4
|
@@ -44,6 +44,15 @@ export interface FetchOptions<TResponseType extends FetchResponseType = "text">
|
|
|
44
44
|
extraCookies?: ExtraCookies;
|
|
45
45
|
/** Response body representation. Default `text`; use `bytes` for lossless file output. */
|
|
46
46
|
responseType?: TResponseType;
|
|
47
|
+
/**
|
|
48
|
+
* With `redirect: "follow"`, follow redirects in this client instead of the
|
|
49
|
+
* runtime's fetch. Every hop then gets its own cookie header, `extraHeaders`
|
|
50
|
+
* call, and Set-Cookie persistence, so a caller that inspects those can stop
|
|
51
|
+
* a chain between hops. Credential headers are dropped on a cross-origin hop.
|
|
52
|
+
* Default: on whenever a `jar` is passed (host-only cookies must follow the
|
|
53
|
+
* jar's exact-host rule on every hop), off without one.
|
|
54
|
+
*/
|
|
55
|
+
clientRedirects?: boolean;
|
|
47
56
|
}
|
|
48
57
|
type FetchBody<TResponseType extends FetchResponseType> = TResponseType extends "bytes" ? Uint8Array : string;
|
|
49
58
|
export interface FetchResult<TBody = string> {
|
|
@@ -55,14 +64,6 @@ export interface FetchResult<TBody = string> {
|
|
|
55
64
|
/** Number of cookies persisted back into the jar (0 if no jar passed). */
|
|
56
65
|
cookiesSaved: number;
|
|
57
66
|
}
|
|
58
|
-
/**
|
|
59
|
-
* Fetch a URL with optional cookie-jar attach + persist.
|
|
60
|
-
*
|
|
61
|
-
* Returns the response body as text by default (callers handle JSON parsing).
|
|
62
|
-
* Pass `responseType: "bytes"` when the caller must preserve the body exactly.
|
|
63
|
-
* Streams aren't supported; this is a CLI helper, not a streaming HTTP
|
|
64
|
-
* client. Use Bun's `fetch` directly for streaming workloads.
|
|
65
|
-
*/
|
|
66
67
|
export declare function fetchWithJar<TResponseType extends FetchResponseType = "text">(url: string, opts?: FetchOptions<TResponseType>): Promise<FetchResult<FetchBody<TResponseType>>>;
|
|
67
68
|
export {};
|
|
68
69
|
//# sourceMappingURL=client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/http/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,SAAS,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/lib/http/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,SAAS,EAEd,KAAK,YAAY,EAElB,MAAM,qBAAqB,CAAC;AAE7B;;;;;;;;;GASG;AAEH,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC;AAEjD,MAAM,WAAW,YAAY,CAAC,aAAa,SAAS,iBAAiB,GAAG,MAAM;IAC5E,gCAAgC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACvB,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC;;;;;OAKG;IACH,GAAG,CAAC,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,8CAA8C;IAC9C,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,0FAA0F;IAC1F,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,KAAK,SAAS,CAAC,aAAa,SAAS,iBAAiB,IAAI,aAAa,SAAS,OAAO,GACnF,UAAU,GACV,MAAM,CAAC;AAEX,MAAM,WAAW,WAAW,CAAC,KAAK,GAAG,MAAM;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,KAAK,CAAC;IACZ,0EAA0E;IAC1E,YAAY,EAAE,MAAM,CAAC;CACtB;AAaD,wBAAsB,YAAY,CAAC,aAAa,SAAS,iBAAiB,GAAG,MAAM,EACjF,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,YAAY,CAAC,aAAa,CAAM,GACrC,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC,aAAa,CAAC,CAAC,CAAC,CAsChD"}
|
package/dist/lib/http/client.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { applyExtraCookies, } from "../cookies/index.js";
|
|
1
|
+
import { applyExtraCookies, domainMatches, violatesCookiePrefix, } from "../cookies/index.js";
|
|
2
2
|
/**
|
|
3
3
|
* Fetch a URL with optional cookie-jar attach + persist.
|
|
4
4
|
*
|
|
@@ -7,7 +7,43 @@ import { applyExtraCookies, } from "../cookies/index.js";
|
|
|
7
7
|
* Streams aren't supported; this is a CLI helper, not a streaming HTTP
|
|
8
8
|
* client. Use Bun's `fetch` directly for streaming workloads.
|
|
9
9
|
*/
|
|
10
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
11
|
+
const MAX_CLIENT_REDIRECTS = 20;
|
|
10
12
|
export async function fetchWithJar(url, opts = {}) {
|
|
13
|
+
// With a jar, redirects are followed here so every hop gets its own Cookie
|
|
14
|
+
// header from the jar (exact-host for host-only cookies) and its own
|
|
15
|
+
// Set-Cookie persistence; the runtime would otherwise carry or drop the
|
|
16
|
+
// first hop's header on its own rules. Pass `clientRedirects: false` to opt out.
|
|
17
|
+
const clientRedirects = opts.clientRedirects ?? Boolean(opts.jar);
|
|
18
|
+
if (!clientRedirects || (opts.redirect ?? "follow") !== "follow")
|
|
19
|
+
return fetchOnce(url, opts);
|
|
20
|
+
let current = url;
|
|
21
|
+
let method = opts.method ?? "GET";
|
|
22
|
+
let body = opts.body;
|
|
23
|
+
let headers = opts.headers;
|
|
24
|
+
let cookiesSaved = 0;
|
|
25
|
+
for (let hop = 0;; hop++) {
|
|
26
|
+
const result = await fetchOnce(current, { ...opts, method, body, headers, redirect: "manual" });
|
|
27
|
+
cookiesSaved += result.cookiesSaved;
|
|
28
|
+
const location = result.headers.location;
|
|
29
|
+
if (!REDIRECT_STATUSES.has(result.status) || !location)
|
|
30
|
+
return { ...result, cookiesSaved };
|
|
31
|
+
if (hop >= MAX_CLIENT_REDIRECTS) {
|
|
32
|
+
throw new Error(`Too many redirects (more than ${MAX_CLIENT_REDIRECTS}) starting at ${url}`);
|
|
33
|
+
}
|
|
34
|
+
const next = new URL(location, current);
|
|
35
|
+
if (result.status === 303 ||
|
|
36
|
+
((result.status === 301 || result.status === 302) && method !== "GET" && method !== "HEAD")) {
|
|
37
|
+
method = "GET";
|
|
38
|
+
body = undefined;
|
|
39
|
+
}
|
|
40
|
+
if (next.origin !== new URL(current).origin && headers) {
|
|
41
|
+
headers = Object.fromEntries(Object.entries(headers).filter(([name]) => !["authorization", "cookie"].includes(name.toLowerCase())));
|
|
42
|
+
}
|
|
43
|
+
current = next.href;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async function fetchOnce(url, opts = {}) {
|
|
11
47
|
const headers = { ...(opts.headers ?? {}) };
|
|
12
48
|
if (opts.jar) {
|
|
13
49
|
applyExtraCookies(url, opts.jar, opts.extraCookies);
|
|
@@ -145,7 +181,10 @@ function parseSetCookie(raw, defaultDomain) {
|
|
|
145
181
|
for (const attr of parts.slice(1)) {
|
|
146
182
|
const lower = attr.toLowerCase();
|
|
147
183
|
if (lower.startsWith("domain=")) {
|
|
148
|
-
const d = attr.slice(7).trim();
|
|
184
|
+
const d = attr.slice(7).trim().toLowerCase();
|
|
185
|
+
// A server may only scope a cookie to its own host or a parent domain.
|
|
186
|
+
if (!d || !domainMatches(d, defaultDomain))
|
|
187
|
+
return null;
|
|
149
188
|
cookie.domain = d.startsWith(".") ? d : `.${d}`;
|
|
150
189
|
}
|
|
151
190
|
else if (lower.startsWith("path=")) {
|
|
@@ -176,5 +215,8 @@ function parseSetCookie(raw, defaultDomain) {
|
|
|
176
215
|
cookie.sameSite = v.charAt(0).toUpperCase() + v.slice(1).toLowerCase();
|
|
177
216
|
}
|
|
178
217
|
}
|
|
218
|
+
// A cookie that breaks its name prefix is dropped, as a browser would.
|
|
219
|
+
if (violatesCookiePrefix(cookie))
|
|
220
|
+
return null;
|
|
179
221
|
return cookie;
|
|
180
222
|
}
|
|
@@ -111,10 +111,10 @@ carries a reason instead of \`(no intent)\`; the [tool-intent
|
|
|
111
111
|
guide](https://harnery.com/guides/tool-intent/) owns the details.
|
|
112
112
|
|
|
113
113
|
**Messaging another agent.** \`${b} agents ping <name> "<message>"\` reaches an
|
|
114
|
-
agent by name
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
agent by name, running or not, here or on another machine sharing this origin:
|
|
115
|
+
a live agent sees it on their next prompt (after a fetch, if remote), and a
|
|
116
|
+
dormant name holds it until that session next starts. Send it as your only step;
|
|
117
|
+
do not check who is live first or hunt elsewhere. Only an unused name is refused.
|
|
118
118
|
|
|
119
119
|
**Journal.** \`${b} journal add <category> "<text>"\` (category = ${journalCats})
|
|
120
120
|
leaves breadcrumbs that survive context compaction;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harnery",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.1",
|
|
4
4
|
"description": "Multi-agent coordination for AI coding agents - Claude Code, Cursor, and Codex.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ryan Kelly",
|
|
@@ -252,7 +252,7 @@
|
|
|
252
252
|
"access": "public",
|
|
253
253
|
"provenance": true
|
|
254
254
|
},
|
|
255
|
-
"packageManager": "bun@1.4.
|
|
255
|
+
"packageManager": "bun@1.4.4",
|
|
256
256
|
"scripts": {
|
|
257
257
|
"build": "npm run clean && tsc -p tsconfig.build.json && node scripts/rewrite-dts-extensions.mjs",
|
|
258
258
|
"build:openclaw-plugin": "mkdir -p openclaw-plugin/dist && bun build openclaw-plugin/src/index.ts openclaw-plugin/src/record-worker.ts --target=node --format=esm --external openclaw --outdir openclaw-plugin/dist --entry-naming '[name].js' && cp openclaw-plugin/package.json openclaw-plugin/openclaw.plugin.json openclaw-plugin/dist/",
|
package/src/commander.ts
CHANGED
|
@@ -130,6 +130,19 @@ export interface HarneryProgramContext {
|
|
|
130
130
|
* harn standalone skips the callback entirely.
|
|
131
131
|
*/
|
|
132
132
|
extraCookies?: import("./lib/cookies/index.ts").ExtraCookies;
|
|
133
|
+
/**
|
|
134
|
+
* When it returns true for a run, `fetch` follows redirects itself so the
|
|
135
|
+
* cookie header, `extraHeaders`, and Set-Cookie persistence apply to every
|
|
136
|
+
* hop instead of only the first request.
|
|
137
|
+
*/
|
|
138
|
+
clientRedirects?: () => boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Called once per `browse` run. Returns the guard that inspects that
|
|
141
|
+
* session's live cookies before each request, after each response, and at
|
|
142
|
+
* close (throwing stops the session and fails the run), or undefined for no
|
|
143
|
+
* guard.
|
|
144
|
+
*/
|
|
145
|
+
cookieGuard?: () => import("./lib/browser/client.ts").CookieGuard | undefined;
|
|
133
146
|
/**
|
|
134
147
|
* Host-injected vision-model call for `browse --check-critique`. harnery
|
|
135
148
|
* ships no model client or API key; a consumer wires this to its own
|
|
@@ -15,7 +15,7 @@ import { closeSync, openSync } from "node:fs";
|
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { type Command, Option } from "commander";
|
|
17
17
|
import type { EmitContext } from "../commander.ts";
|
|
18
|
-
import { resolveBinName } from "../core/config.ts";
|
|
18
|
+
import { readAdmissionConfig, resolveBinName } from "../core/config.ts";
|
|
19
19
|
import {
|
|
20
20
|
type AdmissionEntry,
|
|
21
21
|
type AdmissionStatus,
|
|
@@ -24,7 +24,16 @@ import {
|
|
|
24
24
|
admissionBaseDir,
|
|
25
25
|
admissionStatus,
|
|
26
26
|
listAdmissionResources,
|
|
27
|
+
waitForMemoryPressure,
|
|
27
28
|
} from "../lib/admission.ts";
|
|
29
|
+
import {
|
|
30
|
+
type AdmissionHistoryGroup,
|
|
31
|
+
type AdmissionHistoryRecord,
|
|
32
|
+
type AdmissionReleaseResult,
|
|
33
|
+
readAdmissionHistory,
|
|
34
|
+
resolveAdmissionHistoryDir,
|
|
35
|
+
summarizeAdmissionHistory,
|
|
36
|
+
} from "../lib/admission-history.ts";
|
|
28
37
|
import {
|
|
29
38
|
classifyJob,
|
|
30
39
|
createJobDir,
|
|
@@ -53,6 +62,7 @@ interface AdmissionRunOpts {
|
|
|
53
62
|
capacity?: string;
|
|
54
63
|
timeout?: string;
|
|
55
64
|
label?: string;
|
|
65
|
+
projectRoot?: string;
|
|
56
66
|
detach?: boolean;
|
|
57
67
|
json?: boolean;
|
|
58
68
|
}
|
|
@@ -67,6 +77,15 @@ interface AdmissionWaitOpts {
|
|
|
67
77
|
json?: boolean;
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
interface AdmissionHistoryOpts {
|
|
81
|
+
resource?: string;
|
|
82
|
+
since?: string;
|
|
83
|
+
label?: string;
|
|
84
|
+
list?: boolean;
|
|
85
|
+
limit?: string;
|
|
86
|
+
json?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
interface AdmissionJobsOpts {
|
|
71
90
|
json?: boolean;
|
|
72
91
|
limit?: string;
|
|
@@ -86,6 +105,95 @@ export function jobsBaseDir(): string {
|
|
|
86
105
|
return coordEnv("JOBS_DIR") ?? `${admissionBaseDir()}-jobs`;
|
|
87
106
|
}
|
|
88
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Resources this process tree already holds, set on every child that
|
|
110
|
+
* `admission run` starts. A nested run on one of them runs directly.
|
|
111
|
+
*/
|
|
112
|
+
const HELD_ENV = "HARNERY_ADMISSION_HELD";
|
|
113
|
+
|
|
114
|
+
function heldResources(): string[] {
|
|
115
|
+
return (coordEnv("ADMISSION_HELD") ?? "")
|
|
116
|
+
.split(",")
|
|
117
|
+
.map((name) => name.trim())
|
|
118
|
+
.filter(Boolean);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Run a command with inherited stdio and report how it ended. Never throws. */
|
|
122
|
+
async function runChild(
|
|
123
|
+
emit: EmitContext,
|
|
124
|
+
commandArgs: string[],
|
|
125
|
+
env: NodeJS.ProcessEnv,
|
|
126
|
+
): Promise<AdmissionReleaseResult> {
|
|
127
|
+
const [executable, ...childArgs] = commandArgs;
|
|
128
|
+
try {
|
|
129
|
+
const outcome = await new Promise<{ code: number | null; signal: NodeJS.Signals | null }>(
|
|
130
|
+
(resolvePromise, rejectPromise) => {
|
|
131
|
+
const child = spawn(executable as string, childArgs, {
|
|
132
|
+
stdio: "inherit",
|
|
133
|
+
shell: false,
|
|
134
|
+
env,
|
|
135
|
+
});
|
|
136
|
+
child.on("error", rejectPromise);
|
|
137
|
+
child.on("exit", (code, signal) => resolvePromise({ code, signal }));
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
if (outcome.signal !== null) {
|
|
141
|
+
emit.error({
|
|
142
|
+
code: "admission_child_signal",
|
|
143
|
+
message: `${executable} was terminated by signal ${outcome.signal}`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return { exitCode: outcome.code, signal: outcome.signal };
|
|
147
|
+
} catch (err: unknown) {
|
|
148
|
+
emit.error({
|
|
149
|
+
code: "admission_spawn_error",
|
|
150
|
+
message: `cannot run ${executable}: ${describeError(err)}`,
|
|
151
|
+
});
|
|
152
|
+
return { exitCode: 1 };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Parse "30m", "24h", or "7d" into milliseconds; undefined means invalid. */
|
|
157
|
+
function parseLookback(raw: string): number | undefined {
|
|
158
|
+
const match = /^(\d+)([mhd])$/.exec(raw.trim());
|
|
159
|
+
if (!match) return undefined;
|
|
160
|
+
const amount = Number(match[1]);
|
|
161
|
+
const unitMs = match[2] === "m" ? 60_000 : match[2] === "h" ? 3_600_000 : 86_400_000;
|
|
162
|
+
return amount > 0 ? amount * unitMs : undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function describeHistoryGroup(group: AdmissionHistoryGroup): string {
|
|
166
|
+
const outcomes = Object.entries(group.outcomes)
|
|
167
|
+
.filter(([outcome]) => outcome !== "released")
|
|
168
|
+
.map(([outcome, count]) => `${count} ${outcome}`);
|
|
169
|
+
const hold =
|
|
170
|
+
group.hold_ms !== null
|
|
171
|
+
? `held p50 ${formatJobAge(group.hold_ms.p50)}, p90 ${formatJobAge(group.hold_ms.p90)}`
|
|
172
|
+
: "no completed holds";
|
|
173
|
+
return (
|
|
174
|
+
` ${group.label || "(no label)"}: ${group.count} run(s), ${group.queued} queued; ` +
|
|
175
|
+
`waited p50 ${formatJobAge(group.wait_ms.p50)}, p90 ${formatJobAge(group.wait_ms.p90)}, ` +
|
|
176
|
+
`max ${formatJobAge(group.wait_ms.max)}, total ${formatJobAge(group.wait_ms.total)}; ${hold}` +
|
|
177
|
+
(outcomes.length > 0 ? `; ${outcomes.join(", ")}` : "")
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function describeHistoryRecord(record: AdmissionHistoryRecord): string {
|
|
182
|
+
const bits = [
|
|
183
|
+
record.outcome,
|
|
184
|
+
`waited ${formatJobAge(record.wait_ms)}`,
|
|
185
|
+
...(record.hold_ms !== undefined ? [`held ${formatJobAge(record.hold_ms)}`] : []),
|
|
186
|
+
...(record.exit_code !== undefined && record.exit_code !== null
|
|
187
|
+
? [`exit ${record.exit_code}`]
|
|
188
|
+
: []),
|
|
189
|
+
...(record.signal ? [`signal ${record.signal}`] : []),
|
|
190
|
+
];
|
|
191
|
+
const behind = record.waited_behind?.length
|
|
192
|
+
? ` (behind: ${record.waited_behind.join(", ")})`
|
|
193
|
+
: "";
|
|
194
|
+
return ` ${record.ended_at} ${record.resource} ${record.label || "(no label)"}: ${bits.join(", ")}${behind}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
89
197
|
/** Parse an integer flag with a bounded range; undefined means invalid. */
|
|
90
198
|
function parseBoundedInt(
|
|
91
199
|
raw: string | undefined,
|
|
@@ -231,6 +339,11 @@ export function registerAdmissionCommand(program: Command, emit: EmitContext): v
|
|
|
231
339
|
"--label <text>",
|
|
232
340
|
"Holder description shown in status listings (default: the command itself).",
|
|
233
341
|
)
|
|
342
|
+
.option(
|
|
343
|
+
"--project-root <dir>",
|
|
344
|
+
"Read admission settings from this project's .harnery/config.jsonc instead of " +
|
|
345
|
+
"the nearest one (for callers inside a nested checkout).",
|
|
346
|
+
)
|
|
234
347
|
.option(
|
|
235
348
|
"--detach",
|
|
236
349
|
"Launch the command as a durable job: a detached supervisor holds the slot, " +
|
|
@@ -248,7 +361,8 @@ export function registerAdmissionCommand(program: Command, emit: EmitContext): v
|
|
|
248
361
|
"launch failure. The job's own outcome comes from admission wait.",
|
|
249
362
|
)
|
|
250
363
|
.action(async (commandArgs: string[], opts: AdmissionRunOpts) => {
|
|
251
|
-
const
|
|
364
|
+
const resourceConfig = readAdmissionConfig(opts.projectRoot)?.resources[opts.resource];
|
|
365
|
+
const capacity = parseBoundedInt(opts.capacity, resourceConfig?.capacity ?? 1, 1, 32);
|
|
252
366
|
if (capacity === undefined) {
|
|
253
367
|
emit.error({
|
|
254
368
|
code: "admission_usage",
|
|
@@ -257,7 +371,12 @@ export function registerAdmissionCommand(program: Command, emit: EmitContext): v
|
|
|
257
371
|
process.exitCode = 1;
|
|
258
372
|
return;
|
|
259
373
|
}
|
|
260
|
-
const timeoutMinutes = parseBoundedInt(
|
|
374
|
+
const timeoutMinutes = parseBoundedInt(
|
|
375
|
+
opts.timeout,
|
|
376
|
+
resourceConfig?.timeout_minutes ?? 60,
|
|
377
|
+
1,
|
|
378
|
+
1440,
|
|
379
|
+
);
|
|
261
380
|
if (timeoutMinutes === undefined) {
|
|
262
381
|
emit.error({
|
|
263
382
|
code: "admission_usage",
|
|
@@ -271,6 +390,16 @@ export function registerAdmissionCommand(program: Command, emit: EmitContext): v
|
|
|
271
390
|
opts.label ??
|
|
272
391
|
(joined.length > LABEL_MAX_CHARS ? `${joined.slice(0, LABEL_MAX_CHARS - 3)}...` : joined);
|
|
273
392
|
|
|
393
|
+
// A nested run on a resource this process tree already holds would wait
|
|
394
|
+
// on its own parent forever, and HARNERY_ADMISSION_SKIP=1 opts one
|
|
395
|
+
// command out on purpose. Both run the command directly.
|
|
396
|
+
const held = heldResources();
|
|
397
|
+
if (coordEnv("ADMISSION_SKIP") === "1" || held.includes(opts.resource)) {
|
|
398
|
+
const result = await runChild(emit, commandArgs, process.env);
|
|
399
|
+
process.exitCode = result.exitCode ?? 1;
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
|
|
274
403
|
if (opts.detach) {
|
|
275
404
|
launchDetachedJob(emit, {
|
|
276
405
|
commandArgs,
|
|
@@ -279,10 +408,29 @@ export function registerAdmissionCommand(program: Command, emit: EmitContext): v
|
|
|
279
408
|
label,
|
|
280
409
|
timeoutMinutes,
|
|
281
410
|
json: opts.json === true,
|
|
411
|
+
...(resourceConfig?.defer_on_pressure
|
|
412
|
+
? { deferOnPressure: resourceConfig.defer_on_pressure }
|
|
413
|
+
: {}),
|
|
282
414
|
});
|
|
283
415
|
return;
|
|
284
416
|
}
|
|
285
417
|
|
|
418
|
+
let pressureWaitMs = 0;
|
|
419
|
+
const pressure = resourceConfig?.defer_on_pressure;
|
|
420
|
+
if (pressure) {
|
|
421
|
+
pressureWaitMs = await waitForMemoryPressure({
|
|
422
|
+
threshold: pressure.psi_full_avg10,
|
|
423
|
+
maxWaitMs: pressure.max_wait_seconds * 1000,
|
|
424
|
+
onWait: (event) =>
|
|
425
|
+
emit.log(
|
|
426
|
+
event.gaveUp
|
|
427
|
+
? `memory pressure still ${event.pressure} after ${formatJobAge(event.waitedMs)}; starting anyway: ${label}`
|
|
428
|
+
: `memory pressure ${event.pressure} > ${pressure.psi_full_avg10}; waiting before: ${label}`,
|
|
429
|
+
"info",
|
|
430
|
+
),
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
286
434
|
const dir = admissionBaseDir();
|
|
287
435
|
let lastWaitMessage = "";
|
|
288
436
|
let handle: Awaited<ReturnType<typeof acquireAdmission>>;
|
|
@@ -292,6 +440,7 @@ export function registerAdmissionCommand(program: Command, emit: EmitContext): v
|
|
|
292
440
|
{
|
|
293
441
|
label,
|
|
294
442
|
timeoutMs: timeoutMinutes * 60_000,
|
|
443
|
+
...(pressureWaitMs > 0 ? { pressureWaitMs } : {}),
|
|
295
444
|
onWait: (info) => {
|
|
296
445
|
const holders = info.holders.map((holder) => holder.label).join(", ") || "none";
|
|
297
446
|
const message =
|
|
@@ -317,36 +466,103 @@ export function registerAdmissionCommand(program: Command, emit: EmitContext): v
|
|
|
317
466
|
throw err;
|
|
318
467
|
}
|
|
319
468
|
|
|
320
|
-
const
|
|
469
|
+
const childEnv = {
|
|
470
|
+
...process.env,
|
|
471
|
+
[HELD_ENV]: [...held, opts.resource].join(","),
|
|
472
|
+
};
|
|
473
|
+
let result: AdmissionReleaseResult = {};
|
|
321
474
|
try {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
475
|
+
result = await runChild(emit, commandArgs, childEnv);
|
|
476
|
+
process.exitCode = result.signal ? 1 : (result.exitCode ?? 1);
|
|
477
|
+
} finally {
|
|
478
|
+
handle.release(result);
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
// ----------------------------------------------------------------- history
|
|
483
|
+
admission
|
|
484
|
+
.command("history")
|
|
485
|
+
.description(
|
|
486
|
+
"Report finished slot use: how long work queued for each resource, what it " +
|
|
487
|
+
"waited behind, how long holders kept their slots, and which tickets timed " +
|
|
488
|
+
"out or were abandoned by a process that died. Grouped by resource and " +
|
|
489
|
+
"label, most total wait first; --list shows individual records.",
|
|
490
|
+
)
|
|
491
|
+
.option("--resource <name>", "Report one resource instead of all of them.")
|
|
492
|
+
.option("--since <window>", "Look back this far: 30m, 24h, 7d (default 24h).")
|
|
493
|
+
.option("--label <text>", "Keep records whose label contains this text.")
|
|
494
|
+
.option("--list", "List individual records, newest first, after the summary.")
|
|
495
|
+
.option("--limit <n>", "With --list, how many records to show (1-500; default 20).")
|
|
496
|
+
.option("--json", "Emit { history_dir, since, groups: [...], records: [...] } as JSON.")
|
|
497
|
+
.addHelpText(
|
|
498
|
+
"after",
|
|
499
|
+
"\nHistory is written to $XDG_STATE_HOME/harnery/admission-history (default " +
|
|
500
|
+
"~/.local/state/harnery/admission-history); HARNERY_ADMISSION_HISTORY_DIR " +
|
|
501
|
+
"moves it and HARNERY_ADMISSION_HISTORY=off stops recording." +
|
|
502
|
+
"\nExit codes: 0 always (reporting only) · 1 usage error.",
|
|
503
|
+
)
|
|
504
|
+
.action((opts: AdmissionHistoryOpts) => {
|
|
505
|
+
const lookbackMs = parseLookback(opts.since ?? "24h");
|
|
506
|
+
if (lookbackMs === undefined) {
|
|
507
|
+
emit.error({
|
|
508
|
+
code: "admission_usage",
|
|
509
|
+
message: "--since must look like 30m, 24h, or 7d",
|
|
332
510
|
});
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
process.exitCode = 1;
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
process.exitCode = outcome.code ?? 1;
|
|
342
|
-
} catch (err: unknown) {
|
|
511
|
+
process.exitCode = 1;
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
const limit = parseBoundedInt(opts.limit, 20, 1, 500);
|
|
515
|
+
if (limit === undefined) {
|
|
343
516
|
emit.error({
|
|
344
|
-
code: "
|
|
345
|
-
message:
|
|
517
|
+
code: "admission_usage",
|
|
518
|
+
message: "--limit must be an integer between 1 and 500",
|
|
346
519
|
});
|
|
347
520
|
process.exitCode = 1;
|
|
348
|
-
|
|
349
|
-
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const historyDir = resolveAdmissionHistoryDir(admissionBaseDir());
|
|
524
|
+
const sinceMs = Date.now() - lookbackMs;
|
|
525
|
+
const records = readAdmissionHistory(historyDir, {
|
|
526
|
+
...(opts.resource !== undefined ? { resource: opts.resource } : {}),
|
|
527
|
+
...(opts.label !== undefined ? { labelContains: opts.label } : {}),
|
|
528
|
+
sinceMs,
|
|
529
|
+
});
|
|
530
|
+
const groups = summarizeAdmissionHistory(records);
|
|
531
|
+
const newest = [...records].reverse().slice(0, limit);
|
|
532
|
+
if (opts.json) {
|
|
533
|
+
emit.data({
|
|
534
|
+
history_dir: historyDir,
|
|
535
|
+
since: new Date(sinceMs).toISOString(),
|
|
536
|
+
groups,
|
|
537
|
+
records: opts.list ? newest : [],
|
|
538
|
+
});
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (historyDir === null) {
|
|
542
|
+
emit.log("admission history recording is off (HARNERY_ADMISSION_HISTORY=off)", "info");
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
if (records.length === 0) {
|
|
546
|
+
emit.log(`no admission history since ${new Date(sinceMs).toISOString()}`, "info");
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
let resource = "";
|
|
550
|
+
for (const group of groups.sort((a, b) => a.resource.localeCompare(b.resource))) {
|
|
551
|
+
if (group.resource !== resource) {
|
|
552
|
+
resource = group.resource;
|
|
553
|
+
const inResource = groups.filter((candidate) => candidate.resource === resource);
|
|
554
|
+
const total = inResource.reduce((sum, candidate) => sum + candidate.wait_ms.total, 0);
|
|
555
|
+
emit.log(
|
|
556
|
+
`${resource}: ${inResource.reduce((sum, candidate) => sum + candidate.count, 0)} ` +
|
|
557
|
+
`run(s), ${formatJobAge(total)} total queue wait`,
|
|
558
|
+
"info",
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
emit.log(describeHistoryGroup(group), "info");
|
|
562
|
+
}
|
|
563
|
+
if (opts.list) {
|
|
564
|
+
emit.log(`latest ${newest.length} record(s):`, "info");
|
|
565
|
+
for (const record of newest) emit.log(describeHistoryRecord(record), "info");
|
|
350
566
|
}
|
|
351
567
|
});
|
|
352
568
|
|
|
@@ -480,6 +696,7 @@ interface LaunchOptions {
|
|
|
480
696
|
label: string;
|
|
481
697
|
timeoutMinutes: number;
|
|
482
698
|
json: boolean;
|
|
699
|
+
deferOnPressure?: { psi_full_avg10: number; max_wait_seconds: number };
|
|
483
700
|
}
|
|
484
701
|
|
|
485
702
|
function launchDetachedJob(emit: EmitContext, options: LaunchOptions): void {
|
|
@@ -507,6 +724,7 @@ function launchDetachedJob(emit: EmitContext, options: LaunchOptions): void {
|
|
|
507
724
|
argv: options.commandArgs,
|
|
508
725
|
cwd: process.cwd(),
|
|
509
726
|
created_at: nowIso(),
|
|
727
|
+
...(options.deferOnPressure ? { defer_on_pressure: options.deferOnPressure } : {}),
|
|
510
728
|
};
|
|
511
729
|
writeJobDocument(jobDir, document);
|
|
512
730
|
logFd = openSync(join(jobDir, DURABLE_JOB_LOG_FILENAME), "a");
|
|
@@ -623,6 +841,25 @@ export async function superviseJob(emit: EmitContext, opts: AdmissionSuperviseOp
|
|
|
623
841
|
});
|
|
624
842
|
};
|
|
625
843
|
|
|
844
|
+
// Defer before queueing, holding no slot, exactly as a foreground run does.
|
|
845
|
+
let pressureWaitMs = 0;
|
|
846
|
+
const pressure = document.defer_on_pressure;
|
|
847
|
+
if (pressure) {
|
|
848
|
+
pressureWaitMs = await waitForMemoryPressure({
|
|
849
|
+
threshold: pressure.psi_full_avg10,
|
|
850
|
+
maxWaitMs: pressure.max_wait_seconds * 1000,
|
|
851
|
+
onWait: (event) => {
|
|
852
|
+
writeJobStatus(jobDir, { ...status, updated_at: nowIso() });
|
|
853
|
+
emit.log(
|
|
854
|
+
event.gaveUp
|
|
855
|
+
? `memory pressure still ${event.pressure} after ${formatJobAge(event.waitedMs)}; queueing anyway`
|
|
856
|
+
: `memory pressure ${event.pressure} > ${pressure.psi_full_avg10}; waiting before queueing`,
|
|
857
|
+
"info",
|
|
858
|
+
);
|
|
859
|
+
},
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
626
863
|
let handle: Awaited<ReturnType<typeof acquireAdmission>>;
|
|
627
864
|
try {
|
|
628
865
|
handle = await acquireAdmission(
|
|
@@ -634,6 +871,7 @@ export async function superviseJob(emit: EmitContext, opts: AdmissionSuperviseOp
|
|
|
634
871
|
{
|
|
635
872
|
label: document.label,
|
|
636
873
|
timeoutMs: timeoutMinutes * 60_000,
|
|
874
|
+
...(pressureWaitMs > 0 ? { pressureWaitMs } : {}),
|
|
637
875
|
onWait: () => {
|
|
638
876
|
writeJobStatus(jobDir, { ...status, updated_at: nowIso() });
|
|
639
877
|
},
|
|
@@ -668,6 +906,7 @@ export async function superviseJob(emit: EmitContext, opts: AdmissionSuperviseOp
|
|
|
668
906
|
heartbeat.unref?.();
|
|
669
907
|
|
|
670
908
|
const [executable, ...childArgs] = document.argv;
|
|
909
|
+
const result: AdmissionReleaseResult = {};
|
|
671
910
|
let logFd: number | undefined;
|
|
672
911
|
try {
|
|
673
912
|
logFd = openSync(join(jobDir, DURABLE_JOB_LOG_FILENAME), "a");
|
|
@@ -676,12 +915,15 @@ export async function superviseJob(emit: EmitContext, opts: AdmissionSuperviseOp
|
|
|
676
915
|
const child = spawn(executable as string, childArgs, {
|
|
677
916
|
stdio: ["ignore", logFd as number, logFd as number],
|
|
678
917
|
shell: false,
|
|
918
|
+
env: { ...process.env, [HELD_ENV]: [...heldResources(), document.resource].join(",") },
|
|
679
919
|
...(document.cwd ? { cwd: document.cwd } : {}),
|
|
680
920
|
});
|
|
681
921
|
child.on("error", rejectPromise);
|
|
682
922
|
child.on("exit", (code, signal) => resolvePromise({ code, signal }));
|
|
683
923
|
},
|
|
684
924
|
);
|
|
925
|
+
result.exitCode = outcome.code;
|
|
926
|
+
result.signal = outcome.signal;
|
|
685
927
|
finish({ exitCode: outcome.code, signal: outcome.signal });
|
|
686
928
|
process.exitCode = outcome.signal !== null ? 1 : (outcome.code ?? 1);
|
|
687
929
|
} catch (err: unknown) {
|
|
@@ -690,10 +932,11 @@ export async function superviseJob(emit: EmitContext, opts: AdmissionSuperviseOp
|
|
|
690
932
|
message: `cannot run ${executable}: ${describeError(err)}`,
|
|
691
933
|
});
|
|
692
934
|
finish({ exitCode: 1, signal: null });
|
|
935
|
+
result.exitCode = 1;
|
|
693
936
|
process.exitCode = 1;
|
|
694
937
|
} finally {
|
|
695
938
|
clearInterval(heartbeat);
|
|
696
939
|
if (logFd !== undefined) closeSync(logFd);
|
|
697
|
-
handle.release();
|
|
940
|
+
handle.release(result);
|
|
698
941
|
}
|
|
699
942
|
}
|