agent-yes 1.288.0 → 1.289.0
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/default.config.yaml +106 -0
- package/dist/{SUPPORTED_CLIS-BHowMgMY.js → SUPPORTED_CLIS-BlgVWZrM.js} +2 -2
- package/dist/{SUPPORTED_CLIS-ChhJyq4h.js → SUPPORTED_CLIS-C65PuIs6.js} +3 -3
- package/dist/{agentShare-T3RskvDT.js → agentShare-Cm15-dfZ.js} +3 -3
- package/dist/{callback-CuohbCJA.js → callback-Bbplrsbv.js} +3 -3
- package/dist/{callback-DeaETuHt.js → callback-Dwnkeqpz.js} +3 -3
- package/dist/cli.js +11 -11
- package/dist/cmdPs-BYBZQ0Vc.js +331 -0
- package/dist/{cwdConflictWarn-DIPD6HH5.js → cwdConflictWarn-BgXN56Jc.js} +1 -1
- package/dist/{cwdPassthroughHint-C0tfZlgZ.js → cwdPassthroughHint-CDEGVteh.js} +1 -1
- package/dist/{expose-Ds9aUFsq.js → expose-BlddYYx9.js} +1 -1
- package/dist/{forkNested-Dt5XP8ZH.js → forkNested-CJ7lQQCe.js} +1 -1
- package/dist/{hist-DSYaJEd3.js → hist-DZxq7NMA.js} +1 -1
- package/dist/index.js +2 -2
- package/dist/{initMsg-CeF4QNOM.js → initMsg-DoltWTZj.js} +1 -1
- package/dist/{initMsg-DejbYYjX.js → initMsg-axyQAPLK.js} +1 -1
- package/dist/{nodeRuntime-_zeWDISM.js → nodeRuntime-CipjIwfO.js} +1 -1
- package/dist/{notifyDaemon-BORqPeAl.js → notifyDaemon-Q3FF-WEZ.js} +47 -39
- package/dist/{openBrowser-BNP8xByu.js → openBrowser-THOzOGNm.js} +1 -1
- package/dist/{parentLink-Bv8ihRcu.js → parentLink-bpDGuQi6.js} +1 -1
- package/dist/{parentLink-HWunrH1s.js → parentLink-fdiuB13i.js} +1 -1
- package/dist/{parentPingLoop-COePRYr9.js → parentPingLoop-Dc7co5D7.js} +4 -4
- package/dist/{cmdPs-Bm4JG7hi.js → procStats-CyD8h5nx.js} +2 -329
- package/dist/{remotes-b6reVXzo.js → remotes-BjsglpjL.js} +2 -2
- package/dist/{remotes-BlHTR9ow.js → remotes-CAqtL_Ib.js} +1 -1
- package/dist/{rustBinary-BmXzqj5z.js → rustBinary-B57oRftk.js} +2 -2
- package/dist/{rustBinary-D12k7iOD.js → rustBinary-Dq0AytZ7.js} +2 -2
- package/dist/{schedule-DLoUmyJh.js → schedule-pim0Izhs.js} +6 -6
- package/dist/{serve-DaDlOtss.js → serve-CMsvjqPU.js} +174 -27
- package/dist/{setup-Cqy8nRrq.js → setup-BmxYINCx.js} +4 -4
- package/dist/{spawnGate-B2hA_2NQ.js → spawnGate-B26UnzqP.js} +2 -2
- package/dist/{spawnGate-FLwTAPW6.js → spawnGate-C8MSRt7X.js} +1 -1
- package/dist/{subcommands-DKTa3E6O.js → subcommands-CJBV06rg.js} +2 -2
- package/dist/{subcommands-BfiOyfz7.js → subcommands-CWDqhKQe.js} +21 -21
- package/dist/{systemPathLink-Dl2gJqeg.js → systemPathLink-BVdk48Vz.js} +1 -1
- package/dist/{terminal-C0tnGnrZ.js → terminal-CcmBzB3G.js} +2 -2
- package/dist/{todoCli-MWrejmdM.js → todoCli-azqbjBtz.js} +1 -1
- package/dist/{tray-BhPVpP_r.js → tray-CIt9mCKZ.js} +1 -1
- package/dist/trayApp-BovAZs7m.js +5 -0
- package/dist/{trayApp-DvL9qW6V.js → trayApp-DuZBkGl_.js} +2 -2
- package/dist/{ts-DJLp2_bv.js → ts-CSp4mJ7Z.js} +23 -9
- package/dist/{versionChecker-CkO7wYyV.js → versionChecker-CWCN4vwm.js} +2 -2
- package/dist/{webrtcRemote-H9Hok0_R.js → webrtcRemote-Bevc5ogS.js} +1 -1
- package/dist/{widget-CJqpScCG.js → widget-D2cCC8nz.js} +3 -3
- package/dist/{workspaceConfig-D6QndOCz.js → workspaceConfig-DTvPtr5j.js} +1 -1
- package/dist/{ws-DGENGNrr.js → ws-B5Qb2c-1.js} +3 -3
- package/dist/{ws-DzCKvzWD.js → ws-CyO9r4jx.js} +3 -3
- package/package.json +1 -1
- package/ts/agentNice.spec.ts +42 -2
- package/ts/index.ts +5 -4
- package/ts/notifyDaemon.spec.ts +16 -0
- package/ts/notifyDaemon.ts +22 -0
- package/ts/serve.ts +14 -0
- package/ts/serveSampler.spec.ts +390 -0
- package/ts/serveSampler.ts +218 -0
- package/dist/trayApp-BrAMlCY7.js +0 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as __esmMin, r as __exportAll } from "./chunk-cZBX9HZv.js";
|
|
2
2
|
import { n as logger, t as addTransport } from "./logger-CDIsZ-Pp.js";
|
|
3
|
-
import { i as getInstalledPackage } from "./versionChecker-
|
|
3
|
+
import { i as getInstalledPackage } from "./versionChecker-CWCN4vwm.js";
|
|
4
4
|
import { t as agentYesHome } from "./agentYesHome-BNtn19J4.js";
|
|
5
5
|
import { u as removeControlCharacters } from "./todoAutomation-CbVVS4re.js";
|
|
6
6
|
import { i as shouldUseLock, r as releaseLock, t as acquireLock } from "./runningLock-BOIPD_Vs.js";
|
|
@@ -248,6 +248,20 @@ async function getSessionForCwd(cwd) {
|
|
|
248
248
|
* Extract session ID from codex output
|
|
249
249
|
* Session IDs are UUIDs in the format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
250
250
|
*/
|
|
251
|
+
/**
|
|
252
|
+
* True for the codex CLI and every codex-derived variant (`codex-ds`,
|
|
253
|
+
* `codex-ds-direct`, …) — they all run the same `codex` binary and print the
|
|
254
|
+
* same session-id banner, so the session capture/resume path must treat them
|
|
255
|
+
* alike. Keying those sites on an exact `=== "codex"` made `--continue` on a
|
|
256
|
+
* variant silently start a fresh session instead of resuming.
|
|
257
|
+
*
|
|
258
|
+
* The per-cwd session store is therefore shared across codex variants. That is
|
|
259
|
+
* fine: resuming re-applies the launching CLI's own `defaultArgs`, so a session
|
|
260
|
+
* resumed via `codex-ds` still talks to DeepSeek.
|
|
261
|
+
*/
|
|
262
|
+
function isCodexFamily(cli) {
|
|
263
|
+
return cli === "codex" || cli.startsWith("codex-");
|
|
264
|
+
}
|
|
251
265
|
function extractSessionId(output) {
|
|
252
266
|
const match = output.match(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i);
|
|
253
267
|
return match ? match[0] : null;
|
|
@@ -1599,10 +1613,10 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
|
|
|
1599
1613
|
})();
|
|
1600
1614
|
const originalPrompt = prompt;
|
|
1601
1615
|
if (prompt && inheritedParentPid) try {
|
|
1602
|
-
const { resolveSpawner } = await import("./parentLink-
|
|
1616
|
+
const { resolveSpawner } = await import("./parentLink-fdiuB13i.js");
|
|
1603
1617
|
const spawner = await resolveSpawner(inheritedParentPid);
|
|
1604
1618
|
if (spawner) {
|
|
1605
|
-
const { buildInitMsg } = await import("./initMsg-
|
|
1619
|
+
const { buildInitMsg } = await import("./initMsg-DoltWTZj.js");
|
|
1606
1620
|
const { formatIdentity } = await import("./identity-DFr3dfTO.js");
|
|
1607
1621
|
const identity = formatIdentity({
|
|
1608
1622
|
cwd: spawner.cwd,
|
|
@@ -1666,7 +1680,7 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
|
|
|
1666
1680
|
prompt = prompt ? `[${hint}]\n\n${prompt}` : hint;
|
|
1667
1681
|
}
|
|
1668
1682
|
} catch {}
|
|
1669
|
-
if (resume) if (cli
|
|
1683
|
+
if (resume) if (isCodexFamily(cli) && resume) {
|
|
1670
1684
|
const storedSessionId = await getSessionForCwd(workingDir);
|
|
1671
1685
|
if (storedSessionId) {
|
|
1672
1686
|
cliArgs = [
|
|
@@ -1905,7 +1919,7 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
|
|
|
1905
1919
|
logger.warn(`[pidStore] Failed to update status for PID ${exitedPid}:`, error);
|
|
1906
1920
|
}
|
|
1907
1921
|
let restoreArgs = conf.restoreArgs;
|
|
1908
|
-
if (cli
|
|
1922
|
+
if (isCodexFamily(cli)) {
|
|
1909
1923
|
const storedSessionId = await getSessionForCwd(workingDir);
|
|
1910
1924
|
if (storedSessionId) {
|
|
1911
1925
|
restoreArgs = ["resume", storedSessionId];
|
|
@@ -2057,7 +2071,7 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
|
|
|
2057
2071
|
await exitAgent();
|
|
2058
2072
|
break;
|
|
2059
2073
|
}
|
|
2060
|
-
if (cli
|
|
2074
|
+
if (isCodexFamily(cli)) {
|
|
2061
2075
|
const sessionId = extractSessionId(line);
|
|
2062
2076
|
if (sessionId) {
|
|
2063
2077
|
logger.debug(`heartbeat|session|captured session ID: ${sessionId}`);
|
|
@@ -2071,7 +2085,7 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
|
|
|
2071
2085
|
}, 100);
|
|
2072
2086
|
const cleanupHeartbeat = () => clearInterval(heartbeatInterval);
|
|
2073
2087
|
shell.onExit(cleanupHeartbeat);
|
|
2074
|
-
const { startParentPingLoop } = await import("./parentPingLoop-
|
|
2088
|
+
const { startParentPingLoop } = await import("./parentPingLoop-Dc7co5D7.js");
|
|
2075
2089
|
const parentPingLoop = startParentPingLoop({
|
|
2076
2090
|
parentPid,
|
|
2077
2091
|
selfWrapperPid: process.pid,
|
|
@@ -2283,7 +2297,7 @@ async function agentYes({ cli, cliArgs = [], skipPermissions = false, prompt, ro
|
|
|
2283
2297
|
ctx.isFatal = true;
|
|
2284
2298
|
await exitAgent();
|
|
2285
2299
|
}
|
|
2286
|
-
if (cli
|
|
2300
|
+
if (isCodexFamily(cli)) {
|
|
2287
2301
|
const sessionId = extractSessionId(line);
|
|
2288
2302
|
if (sessionId) {
|
|
2289
2303
|
logger.debug(`session|captured session ID: ${sessionId}`);
|
|
@@ -2334,4 +2348,4 @@ function sleep(ms) {
|
|
|
2334
2348
|
|
|
2335
2349
|
//#endregion
|
|
2336
2350
|
export { hasTodoStore as a, permissionBadge as c, answerAsk as i, AgentContext as l, agentYes as n, listAsks as o, config as r, listAsksForProject as s, CLIS_CONFIG as t, CLAUDE_SESSION_PIN_ENV as u };
|
|
2337
|
-
//# sourceMappingURL=ts-
|
|
2351
|
+
//# sourceMappingURL=ts-CSp4mJ7Z.js.map
|
|
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
|
|
|
7
7
|
|
|
8
8
|
//#region package.json
|
|
9
9
|
var name = "agent-yes";
|
|
10
|
-
var version = "1.
|
|
10
|
+
var version = "1.289.0";
|
|
11
11
|
|
|
12
12
|
//#endregion
|
|
13
13
|
//#region ts/versionChecker.ts
|
|
@@ -215,4 +215,4 @@ async function displayVersion() {
|
|
|
215
215
|
|
|
216
216
|
//#endregion
|
|
217
217
|
export { versionString as a, getInstalledPackage as i, compareVersions as n, displayVersion as r, checkAndAutoUpdate as t };
|
|
218
|
-
//# sourceMappingURL=versionChecker-
|
|
218
|
+
//# sourceMappingURL=versionChecker-CWCN4vwm.js.map
|
|
@@ -28,7 +28,7 @@ function parseFlags(args, known) {
|
|
|
28
28
|
}
|
|
29
29
|
/** Resolve the local daemon base URL + token (flags override discovery). */
|
|
30
30
|
async function daemonTarget(flags) {
|
|
31
|
-
const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-
|
|
31
|
+
const { resolveDaemonHttpBase, loadTokenReadOnly } = await import("./serve-CMsvjqPU.js");
|
|
32
32
|
const base = typeof flags.base === "string" ? flags.base : await resolveDaemonHttpBase();
|
|
33
33
|
if (!base) throw new Error("no running ay serve daemon found — start `ay serve` or pass --base <url> (e.g. http://127.0.0.1:PORT)");
|
|
34
34
|
const token = typeof flags.token === "string" ? flags.token : await loadTokenReadOnly() ?? "";
|
|
@@ -186,7 +186,7 @@ async function cmdMint(args) {
|
|
|
186
186
|
const bad = caps.filter((c) => !KNOWN_CAPS.has(c));
|
|
187
187
|
if (bad.length) throw new Error(`unknown cap(s): ${bad.join(", ")} (valid: ${[...KNOWN_CAPS].join(", ")})`);
|
|
188
188
|
const ttlSec = parseTtlSec(typeof flags.ttl === "string" ? flags.ttl : "15m");
|
|
189
|
-
const { mintScopedTermToken } = await import("./serve-
|
|
189
|
+
const { mintScopedTermToken } = await import("./serve-CMsvjqPU.js");
|
|
190
190
|
const r = await mintScopedTermToken(target, {
|
|
191
191
|
ttlSec,
|
|
192
192
|
caps
|
|
@@ -202,4 +202,4 @@ async function cmdMint(args) {
|
|
|
202
202
|
|
|
203
203
|
//#endregion
|
|
204
204
|
export { cmdMint, cmdWidget };
|
|
205
|
-
//# sourceMappingURL=widget-
|
|
205
|
+
//# sourceMappingURL=widget-D2cCC8nz.js.map
|
|
@@ -215,4 +215,4 @@ function allowsSkipPermissions() {
|
|
|
215
215
|
|
|
216
216
|
//#endregion
|
|
217
217
|
export { getProvisionRoot as a, getWorkspaceRoot as c, isProvisionAllowed as d, resolveSpawnCwd as f, getProvisionHook as i, hasProvisionHook as l, getMaxAgents as n, getSpawnHook as o, setWorkspaceRoot as p, getMinFreeMb as r, getSpawnWaitMs as s, allowsSkipPermissions as t, hasSpawnHook as u };
|
|
218
|
-
//# sourceMappingURL=workspaceConfig-
|
|
218
|
+
//# sourceMappingURL=workspaceConfig-DTvPtr5j.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as listRecords } from "./subcommands-
|
|
2
|
-
import { a as getProvisionRoot } from "./workspaceConfig-
|
|
1
|
+
import { D as listRecords } from "./subcommands-CWDqhKQe.js";
|
|
2
|
+
import { a as getProvisionRoot } from "./workspaceConfig-DTvPtr5j.js";
|
|
3
3
|
import { existsSync } from "fs";
|
|
4
4
|
import { lstat, opendir } from "fs/promises";
|
|
5
5
|
import path from "path";
|
|
@@ -424,4 +424,4 @@ async function cmdWs(args) {
|
|
|
424
424
|
|
|
425
425
|
//#endregion
|
|
426
426
|
export { isPathInside as a, walkWorkspaces as c, importProvisionModule as i, workspaceStatus as l, cmdWs as n, loadProvision as o, collectWorkspaces as r, resolveOperand as s, WS_JSON_SCHEMA as t };
|
|
427
|
-
//# sourceMappingURL=ws-
|
|
427
|
+
//# sourceMappingURL=ws-B5Qb2c-1.js.map
|
|
@@ -3,10 +3,10 @@ import "./globalPidIndex-DoaKEUkO.js";
|
|
|
3
3
|
import "./messageLog-DdjBNqgt.js";
|
|
4
4
|
import "./e2e-z1xpwSAg.js";
|
|
5
5
|
import "./configShared-DKTgiz7K.js";
|
|
6
|
-
import "./subcommands-
|
|
6
|
+
import "./subcommands-CWDqhKQe.js";
|
|
7
7
|
import "./notifyStore-2SWKGwZI.js";
|
|
8
8
|
import "./webrtcLink-DLaJfTcB.js";
|
|
9
|
-
import "./remotes-
|
|
10
|
-
import { a as isPathInside, c as walkWorkspaces, i as importProvisionModule, l as workspaceStatus, n as cmdWs, o as loadProvision, r as collectWorkspaces, s as resolveOperand, t as WS_JSON_SCHEMA } from "./ws-
|
|
9
|
+
import "./remotes-BjsglpjL.js";
|
|
10
|
+
import { a as isPathInside, c as walkWorkspaces, i as importProvisionModule, l as workspaceStatus, n as cmdWs, o as loadProvision, r as collectWorkspaces, s as resolveOperand, t as WS_JSON_SCHEMA } from "./ws-B5Qb2c-1.js";
|
|
11
11
|
|
|
12
12
|
export { WS_JSON_SCHEMA, cmdWs, collectWorkspaces, isPathInside, loadProvision, walkWorkspaces, workspaceStatus };
|
package/package.json
CHANGED
package/ts/agentNice.spec.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// applyAgentNice reaches setPriority through a dynamic `await import("os")`, so
|
|
4
|
+
// the module has to be mocked rather than spied — a namespace object's exports
|
|
5
|
+
// are frozen and a spy on them never takes effect.
|
|
6
|
+
const setPriority = vi.fn();
|
|
7
|
+
vi.mock("os", async (importActual) => {
|
|
8
|
+
const actual = await importActual<typeof import("os")>();
|
|
9
|
+
return { ...actual, default: { ...actual, setPriority }, setPriority };
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
import { agentNiceValue, applyAgentNice } from "./agentNice.ts";
|
|
3
13
|
|
|
4
14
|
describe("agentNice.agentNiceValue", () => {
|
|
5
15
|
it("defaults to 5 when unset or empty", () => {
|
|
@@ -22,3 +32,33 @@ describe("agentNice.agentNiceValue", () => {
|
|
|
22
32
|
expect(agentNiceValue({ AGENT_YES_AGENT_NICE: "3.9" })).toBe(3); // truncates
|
|
23
33
|
});
|
|
24
34
|
});
|
|
35
|
+
|
|
36
|
+
describe("agentNice.applyAgentNice", () => {
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
setPriority.mockReset();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("deprioritizes the spawned pid to the configured nice", async () => {
|
|
42
|
+
await applyAgentNice(4242, { AGENT_YES_AGENT_NICE: "7" });
|
|
43
|
+
|
|
44
|
+
expect(setPriority).toHaveBeenCalledWith(4242, 7);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("does nothing when nice is 0 (explicitly disabled)", async () => {
|
|
48
|
+
await applyAgentNice(4242, { AGENT_YES_AGENT_NICE: "0" });
|
|
49
|
+
|
|
50
|
+
// 0 means "leave the agent at the default priority" — calling setPriority(0)
|
|
51
|
+
// anyway would be a no-op syscall, but skipping it keeps the disabled path
|
|
52
|
+
// free of a platform call that can throw.
|
|
53
|
+
expect(setPriority).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("swallows a setPriority failure — a scheduling hint must never break a spawn", async () => {
|
|
57
|
+
setPriority.mockImplementation(() => {
|
|
58
|
+
// What an unprivileged container or a pid that already exited looks like.
|
|
59
|
+
throw new Error("EPERM: operation not permitted");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
await expect(applyAgentNice(4242, { AGENT_YES_AGENT_NICE: "5" })).resolves.toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
});
|
package/ts/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { agentYesHome } from "./agentYesHome.ts";
|
|
|
10
10
|
import {
|
|
11
11
|
extractSessionId,
|
|
12
12
|
getSessionForCwd,
|
|
13
|
+
isCodexFamily,
|
|
13
14
|
storeSessionForCwd,
|
|
14
15
|
} from "./resume/codexSessionManager.ts";
|
|
15
16
|
import pty, { ptyPackage } from "./pty.ts";
|
|
@@ -435,7 +436,7 @@ export default async function agentYes({
|
|
|
435
436
|
|
|
436
437
|
// Handle --continue flag for codex session restoration
|
|
437
438
|
if (resume) {
|
|
438
|
-
if (cli
|
|
439
|
+
if (isCodexFamily(cli) && resume) {
|
|
439
440
|
// Try to get stored session for this directory
|
|
440
441
|
const storedSessionId = await getSessionForCwd(workingDir);
|
|
441
442
|
if (storedSessionId) {
|
|
@@ -827,7 +828,7 @@ export default async function agentYes({
|
|
|
827
828
|
|
|
828
829
|
// For codex, try to use stored session ID for this directory
|
|
829
830
|
let restoreArgs = conf.restoreArgs;
|
|
830
|
-
if (cli
|
|
831
|
+
if (isCodexFamily(cli)) {
|
|
831
832
|
const storedSessionId = await getSessionForCwd(workingDir);
|
|
832
833
|
if (storedSessionId) {
|
|
833
834
|
// Use specific session ID instead of --last
|
|
@@ -1060,7 +1061,7 @@ export default async function agentYes({
|
|
|
1060
1061
|
}
|
|
1061
1062
|
|
|
1062
1063
|
// session ID capture for codex
|
|
1063
|
-
if (cli
|
|
1064
|
+
if (isCodexFamily(cli)) {
|
|
1064
1065
|
const sessionId = extractSessionId(line);
|
|
1065
1066
|
if (sessionId) {
|
|
1066
1067
|
logger.debug(`heartbeat|session|captured session ID: ${sessionId}`);
|
|
@@ -1447,7 +1448,7 @@ export default async function agentYes({
|
|
|
1447
1448
|
}
|
|
1448
1449
|
|
|
1449
1450
|
// session ID capture for codex
|
|
1450
|
-
if (cli
|
|
1451
|
+
if (isCodexFamily(cli)) {
|
|
1451
1452
|
const sessionId = extractSessionId(line);
|
|
1452
1453
|
if (sessionId) {
|
|
1453
1454
|
logger.debug(`session|captured session ID: ${sessionId}`);
|
package/ts/notifyDaemon.spec.ts
CHANGED
|
@@ -72,6 +72,22 @@ describe("notifyd singleton lock", () => {
|
|
|
72
72
|
expect(await acquireDaemonLock((pid) => pid === 424242)).toBe(true);
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
it("GIVES UP (bounded, not a 15ms busy-wait) when the owner is OUR OWN pid, live and fresh", async () => {
|
|
76
|
+
// The pathological spin: pid reuse hands us the SAME pid a prior daemon held.
|
|
77
|
+
// With no os_start to prove the disagreement, the decline check skips
|
|
78
|
+
// (owner.pid === process.pid) AND the shared steal decision refuses (pid is
|
|
79
|
+
// "self" → not dead; ts fresh → not stale). Before the wait bound this looped
|
|
80
|
+
// at 15ms forever; now it must decline within the budget instead of spinning.
|
|
81
|
+
await mkdir(daemonLockDir(), { recursive: true });
|
|
82
|
+
await writeFile(
|
|
83
|
+
daemonLockOwnerPath(),
|
|
84
|
+
JSON.stringify({ pid: process.pid, started_at: 1, ts: Date.now() }),
|
|
85
|
+
);
|
|
86
|
+
const t0 = Date.now();
|
|
87
|
+
expect(await acquireDaemonLock(() => true, 80)).toBe(false); // bounded decline
|
|
88
|
+
expect(Date.now() - t0).toBeLessThan(2000);
|
|
89
|
+
});
|
|
90
|
+
|
|
75
91
|
it("eventually steals a torn owner, but only AFTER the grace (no double daemon)", async () => {
|
|
76
92
|
// A torn owner is also the mkdir→writeOwner window of a concurrent daemon
|
|
77
93
|
// start, so it is respected within the grace (~1s) and stolen only past it —
|
package/ts/notifyDaemon.ts
CHANGED
|
@@ -258,6 +258,18 @@ export interface DaemonOptions {
|
|
|
258
258
|
// the pid+started_at re-check before SIGTERM).
|
|
259
259
|
const OWNER_TTL_MS = 30_000;
|
|
260
260
|
|
|
261
|
+
// Hard upper bound on how long a contender may wait for the singleton lock. The
|
|
262
|
+
// decline and steal predicates are SUPPOSED to be exhaustive, but they are not
|
|
263
|
+
// complements: a valid owner whose pid equals OUR OWN pid (pid reuse — a prior
|
|
264
|
+
// daemon died and the OS handed us the same pid, with no os_start recorded to
|
|
265
|
+
// prove the disagreement) is skipped by the decline check (`owner.pid !==
|
|
266
|
+
// process.pid`) yet also refused by the shared steal decision (its pid is "self",
|
|
267
|
+
// so not dead; its ts is fresh, so not stale). Without a bound that residue spins
|
|
268
|
+
// at 15ms forever, burning a whole core. Bounded well above the torn-owner grace
|
|
269
|
+
// (~1s) and well below the heartbeat TTL (30s), so a live holder is never waited
|
|
270
|
+
// out to staleness — we decline promptly instead and the next watch poll retries.
|
|
271
|
+
const LOCK_WAIT_BUDGET_MS = 5000;
|
|
272
|
+
|
|
261
273
|
export interface DaemonIdentity {
|
|
262
274
|
pid: number;
|
|
263
275
|
started_at: number;
|
|
@@ -333,11 +345,21 @@ async function writeOwner(): Promise<boolean> {
|
|
|
333
345
|
*/
|
|
334
346
|
export async function acquireDaemonLock(
|
|
335
347
|
isAlive: (pid: number) => boolean = isPidAlive,
|
|
348
|
+
waitBudgetMs: number = LOCK_WAIT_BUDGET_MS,
|
|
336
349
|
): Promise<boolean> {
|
|
337
350
|
// The lock dir lives under notify/ — ensure that exists first, else mkdir of
|
|
338
351
|
// the lock throws ENOENT which must NOT be mistaken for "someone holds it".
|
|
339
352
|
await mkdir(notifyDir(), { recursive: true }).catch(() => {});
|
|
353
|
+
const deadline = Date.now() + waitBudgetMs;
|
|
340
354
|
for (;;) {
|
|
355
|
+
// Never busy-wait without bound. Every iteration must end in acquire,
|
|
356
|
+
// decline, or steal; if the identity is so split that NEITHER decline nor
|
|
357
|
+
// steal applies (see LOCK_WAIT_BUDGET_MS), give up and decline rather than
|
|
358
|
+
// spin at 15ms indefinitely — the next watch poll re-ensures the daemon.
|
|
359
|
+
if (Date.now() >= deadline) {
|
|
360
|
+
logger.warn("[notifyd] gave up acquiring the daemon lock — unverifiable live holder");
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
341
363
|
try {
|
|
342
364
|
await mkdir(daemonLockDir(), { recursive: false });
|
|
343
365
|
daemonStartedAt = Date.now(); // fixed for this daemon's lifetime
|
package/ts/serve.ts
CHANGED
|
@@ -58,6 +58,7 @@ import { isCallbackRevoked, loadCallbackSecretReadOnly } from "./callback.ts";
|
|
|
58
58
|
import { CLAUDE_SESSION_PIN_ENV } from "./sessionEnv.ts";
|
|
59
59
|
import { MAX_CALLBACK_MSG_BYTES, frameVisitorMessage, verifyCapability } from "./callbackCore.ts";
|
|
60
60
|
import { isTerminalReply } from "./terminalReply.ts";
|
|
61
|
+
import * as serveSampler from "./serveSampler.ts";
|
|
61
62
|
import { withIpcLock } from "./ipcLock.ts";
|
|
62
63
|
import { removeControlCharacters } from "./removeControlCharacters.ts";
|
|
63
64
|
import { parseStatusText } from "./statusText.ts";
|
|
@@ -2251,6 +2252,10 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
2251
2252
|
: await logBadges(r.log_file).then(async (b) =>
|
|
2252
2253
|
(await isUserTyping(r.pid)) ? [...b, TYPING_BADGE.id] : b,
|
|
2253
2254
|
),
|
|
2255
|
+
// Per-agent resource window (CPU-seconds/RSS/procs) from the background
|
|
2256
|
+
// sampler. Null until the sampler's first pass lands; the console degrades
|
|
2257
|
+
// to a transparent backdrop.
|
|
2258
|
+
res: serveSampler.resField(r.pid),
|
|
2254
2259
|
};
|
|
2255
2260
|
};
|
|
2256
2261
|
|
|
@@ -3095,6 +3100,7 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
3095
3100
|
loadavg: loadavg(),
|
|
3096
3101
|
mem: { total: totalmem(), free: freemem() },
|
|
3097
3102
|
uptime: uptime(),
|
|
3103
|
+
resources: serveSampler.resourcesField(),
|
|
3098
3104
|
caps: {
|
|
3099
3105
|
send: true, // the console can always drive agent stdin
|
|
3100
3106
|
kill: true,
|
|
@@ -4982,6 +4988,14 @@ export async function cmdServe(rest: string[]): Promise<number> {
|
|
|
4982
4988
|
}
|
|
4983
4989
|
}
|
|
4984
4990
|
|
|
4991
|
+
// Start the per-agent resource sampler (left-panel heatmap data). Prime the
|
|
4992
|
+
// first bucket immediately so the first /api/ls already carries `res`. The
|
|
4993
|
+
// sampler reads the live wrapper pids through listRecords (same registry the
|
|
4994
|
+
// /api/ls handler consults), so both agree on the fleet.
|
|
4995
|
+
serveSampler.startSampler(async () =>
|
|
4996
|
+
(await listRecords(undefined, defaultOpts({ all: true }))).map((r) => r.pid),
|
|
4997
|
+
);
|
|
4998
|
+
|
|
4985
4999
|
// server is null only when we degraded to WebRTC-only above (port wedged);
|
|
4986
5000
|
// skip the HTTP connection banner since nothing is listening on the port.
|
|
4987
5001
|
if (server) {
|