agent-yes 1.290.7 → 1.291.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/dist/SUPPORTED_CLIS-BZP767UR.js +12 -0
- package/dist/{SUPPORTED_CLIS-hTRegnm5.js → SUPPORTED_CLIS-TiOdGi1h.js} +2 -2
- package/dist/{agentShare-C_LTEKEb.js → agentShare-D4N8StHO.js} +8 -8
- package/dist/{askCli-DR9vhdcV.js → askCli-DFsWcgpF.js} +3 -3
- package/dist/{browser-oqC3Ln2a.js → browser-wkzxw6lx.js} +3 -2
- package/dist/{callback-DAQ8wCtJ.js → callback-Bc2gnr_f.js} +3 -3
- package/dist/{callback-BEvLUUBR.js → callback-yCpK6Qpg.js} +7 -7
- package/dist/{channels-hOG3-R_3.js → channels-aYwLMbFV.js} +2 -2
- package/dist/cli.js +9 -9
- package/dist/{cmdPs-BAaLB4Pz.js → cmdPs-D5L3Eyh5.js} +7 -7
- package/dist/{cwdConflictWarn-BgXN56Jc.js → cwdConflictWarn-sQUONAye.js} +2 -2
- package/dist/{forkNested-CJ7lQQCe.js → forkNested-xtEeYpBQ.js} +3 -3
- package/dist/{globalPidIndex-DoaKEUkO.js → globalPidIndex-CRH9L9tn.js} +139 -12
- package/dist/globalPidIndex-DC93JhHQ.js +4 -0
- package/dist/index.js +5 -5
- package/dist/{messageLog-CVRSTLza.js → messageLog-Brx3w6zT.js} +2 -1
- package/dist/{notifyDaemon-Dnbe66Fl.js → notifyDaemon-CvUtRnAB.js} +7 -7
- package/dist/{parentLink-fdiuB13i.js → parentLink-BILJ32nA.js} +2 -2
- package/dist/{parentLink-bpDGuQi6.js → parentLink-CfbxykCv.js} +2 -2
- package/dist/{parentPingLoop-DWv-xJoi.js → parentPingLoop-CVOCiQp2.js} +4 -4
- package/dist/pidStore-Bpe8i_oU.js +6 -0
- package/dist/{pidStore-B73xnAeT.js → pidStore-DXWqzTSz.js} +2 -2
- package/dist/{remotes-DTXPW8U1.js → remotes-tmKwrr20.js} +3 -3
- package/dist/{remotes-C696AJzL.js → remotes-wPL0JtBs.js} +2 -2
- package/dist/{rustBinary-CZRPfa4y.js → rustBinary-C_EbyMTk.js} +2 -2
- package/dist/{rustBinary-jO6EN8LR.js → rustBinary-Clbk_K5c.js} +2 -2
- package/dist/{schedule-Hao0WqyJ.js → schedule-Ba_4wNVK.js} +7 -7
- package/dist/{serve-B2nisui8.js → serve-CJDQq_Jp.js} +46 -40
- package/dist/{setup-vCDTqyqR.js → setup-DwLAkbTC.js} +2 -2
- package/dist/share-Bmnd_qT0.js +4 -0
- package/dist/{share-DTlaFn6s.js → share-C9oGvjXT.js} +13 -4
- package/dist/spawnGate-BUEswFgo.js +5 -0
- package/dist/{spawnGate-B26UnzqP.js → spawnGate-YpHopveO.js} +2 -2
- package/dist/{subcommands-BpFdlCY0.js → subcommands-x8Pe7DrF.js} +6 -6
- package/dist/{subcommands-D6LBtVPM.js → subcommands-yWgVRM2r.js} +273 -45
- package/dist/{terminal-iGHEnc7Z.js → terminal-Dv73BTE-.js} +2 -2
- package/dist/terminal.js +1 -1
- package/dist/{todoCli-azqbjBtz.js → todoCli-DSQXAcwA.js} +3 -3
- package/dist/{todoIdentity-NEceTmEe.js → todoIdentity-CAmnkzzP.js} +2 -2
- package/dist/{trayApp-BdNlQCFx.js → trayApp-BRw8f0mT.js} +2 -2
- package/dist/trayApp-BjQ5V3WV.js +5 -0
- package/dist/{ts-oyuaLWWx.js → ts-C8PxhJZM.js} +7 -7
- package/dist/{versionChecker-CMjS88mL.js → versionChecker-CWPGASjD.js} +2 -2
- package/dist/webrtcLink-CnQf9pmu.js +73 -0
- package/dist/{webrtcRemote-L5NxhLLR.js → webrtcRemote-yvMYsij3.js} +2 -2
- package/dist/{widget-OvkqW2Z6.js → widget-DhBSNUey.js} +3 -3
- package/dist/widgets.js +1 -1
- package/dist/{ws-BeMouYAU.js → ws-CMX0pkti.js} +2 -2
- package/dist/{ws-qZy7Giub.js → ws-DZ9YAXlY.js} +7 -7
- package/lab/ui/console-logic.js +27 -4
- package/package.json +1 -1
- package/ts/crossTreeClaim.bun.spec.ts +76 -0
- package/ts/crossTreeClaim.spec.ts +76 -0
- package/ts/globalPidIndex.spec.ts +153 -0
- package/ts/globalPidIndex.ts +151 -10
- package/ts/messageLog.bun.spec.ts +33 -0
- package/ts/messageLog.spec.ts +33 -0
- package/ts/messageLog.ts +3 -0
- package/ts/rawInputDeclared.bun.spec.ts +68 -0
- package/ts/rawInputDeclared.spec.ts +68 -0
- package/ts/remoteHealth.bun.spec.ts +130 -0
- package/ts/remoteHealth.spec.ts +130 -0
- package/ts/remoteHealth.ts +149 -0
- package/ts/restartKillGuard.bun.spec.ts +40 -0
- package/ts/restartKillGuard.spec.ts +40 -0
- package/ts/senderAncestry.ts +45 -7
- package/ts/serve.ts +46 -20
- package/ts/share.bun.spec.ts +24 -2
- package/ts/share.spec.ts +24 -2
- package/ts/share.ts +37 -5
- package/ts/subcommands.ts +239 -19
- package/ts/terminal/browser.ts +5 -1
- package/ts/webrtcLink.bun.spec.ts +45 -1
- package/ts/webrtcLink.spec.ts +45 -1
- package/ts/webrtcLink.ts +63 -19
- package/dist/SUPPORTED_CLIS-CuvT3tKq.js +0 -12
- package/dist/globalPidIndex-yvWZXeWT.js +0 -4
- package/dist/pidStore-V51mbgZY.js +0 -6
- package/dist/share-BvU4I8BI.js +0 -4
- package/dist/spawnGate-C8MSRt7X.js +0 -5
- package/dist/trayApp-Br2T2Yrb.js +0 -5
- package/dist/webrtcLink-DLaJfTcB.js +0 -45
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { u as parseSecret } from "./e2e-z1xpwSAg.js";
|
|
2
|
+
|
|
3
|
+
//#region ts/webrtcLink.ts
|
|
4
|
+
const SIGNAL_SUBPROTOCOL = "ay-signal-1";
|
|
5
|
+
const DEFAULT_SIGHOST = "s.agent-yes.com";
|
|
6
|
+
function parseFragment(frag) {
|
|
7
|
+
if (/^\??[A-Za-z_]+=/.test(frag)) {
|
|
8
|
+
const q = new URLSearchParams(frag.replace(/^\?/, ""));
|
|
9
|
+
const room = q.get("room");
|
|
10
|
+
const token = q.get("s");
|
|
11
|
+
if (!room || !token) return null;
|
|
12
|
+
return {
|
|
13
|
+
room,
|
|
14
|
+
token,
|
|
15
|
+
host: q.get("sig") || DEFAULT_SIGHOST
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
const at = frag.split("@");
|
|
19
|
+
const seg = at[0];
|
|
20
|
+
const i = seg.indexOf(":");
|
|
21
|
+
if (i < 0) return null;
|
|
22
|
+
return {
|
|
23
|
+
room: seg.slice(0, i),
|
|
24
|
+
token: seg.slice(i + 1),
|
|
25
|
+
host: at[1] || DEFAULT_SIGHOST
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/** Parse a share link WITHOUT validating/stripping the secret's version marker. */
|
|
29
|
+
function parseRaw(link) {
|
|
30
|
+
const wr = /^webrtc:\/\/([^:]+):([^@]+)@(.+)$/.exec(link);
|
|
31
|
+
if (wr) return {
|
|
32
|
+
room: wr[1],
|
|
33
|
+
token: wr[2],
|
|
34
|
+
host: wr[3]
|
|
35
|
+
};
|
|
36
|
+
if (/^https?:\/\//.test(link) && link.includes("#")) return parseFragment(link.split("#")[1] ?? "");
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse a share link into { room, secret, signaling-host }. Accepts:
|
|
41
|
+
* webrtc://<room>:<token>@<host> (internal / legacy form)
|
|
42
|
+
* https://<anyhost>/room/#room=<id>&s=<token>[&sig=<host>]
|
|
43
|
+
* https://<anyhost>/w/#<room>:<token>[@<sighost>] (older positional form)
|
|
44
|
+
* Returns null if the string isn't a recognizable share link.
|
|
45
|
+
*/
|
|
46
|
+
function parseWebrtcLink(link) {
|
|
47
|
+
const raw = parseRaw(link);
|
|
48
|
+
if (!raw) return null;
|
|
49
|
+
const { s } = parseSecret(raw.token);
|
|
50
|
+
return {
|
|
51
|
+
room: raw.room,
|
|
52
|
+
s,
|
|
53
|
+
host: raw.host
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Normalize any accepted share-link spelling to the internal
|
|
58
|
+
* `webrtc://<room>:<token>@<host>` form that startShare/parseShareUrl consume.
|
|
59
|
+
* Returns the RAW token, so a legacy markerless room round-trips unchanged
|
|
60
|
+
* instead of being handed a marker it never had.
|
|
61
|
+
*/
|
|
62
|
+
function toWebrtcUrl(link) {
|
|
63
|
+
const raw = parseRaw(link);
|
|
64
|
+
return raw ? `webrtc://${raw.room}:${raw.token}@${raw.host}` : null;
|
|
65
|
+
}
|
|
66
|
+
/** True if `spec` looks like a WebRTC share link (vs. an http remote or alias). */
|
|
67
|
+
function isWebrtcSpec(spec) {
|
|
68
|
+
return spec.startsWith("webrtc://") || /^https?:\/\//.test(spec) && spec.includes("#");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
//#endregion
|
|
72
|
+
export { toWebrtcUrl as i, isWebrtcSpec as n, parseWebrtcLink as r, SIGNAL_SUBPROTOCOL as t };
|
|
73
|
+
//# sourceMappingURL=webrtcLink-CnQf9pmu.js.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as computeTranscriptHash, c as open, d as randomHex, f as seal, l as packEnvelope, n as FLAG_CONFIRM, o as deriveAuthToken, p as unpackEnvelope, s as deriveDirKeys } from "./e2e-z1xpwSAg.js";
|
|
2
|
-
import { n as isWebrtcSpec, r as parseWebrtcLink, t as SIGNAL_SUBPROTOCOL } from "./webrtcLink-
|
|
2
|
+
import { n as isWebrtcSpec, r as parseWebrtcLink, t as SIGNAL_SUBPROTOCOL } from "./webrtcLink-CnQf9pmu.js";
|
|
3
3
|
import { RTCPeerConnection } from "node-datachannel/polyfill";
|
|
4
4
|
|
|
5
5
|
//#region ts/signalingHint.ts
|
|
@@ -297,4 +297,4 @@ async function startWebrtcBridge(link) {
|
|
|
297
297
|
|
|
298
298
|
//#endregion
|
|
299
299
|
export { startWebrtcBridge };
|
|
300
|
-
//# sourceMappingURL=webrtcRemote-
|
|
300
|
+
//# sourceMappingURL=webrtcRemote-yvMYsij3.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-CJDQq_Jp.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-CJDQq_Jp.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-DhBSNUey.js.map
|
package/dist/widgets.js
CHANGED
|
@@ -2,7 +2,7 @@ import "./e2e-z1xpwSAg.js";
|
|
|
2
2
|
import "./channels-C3w3eAES.js";
|
|
3
3
|
import "./peer-BZyUyCt3.js";
|
|
4
4
|
import { t as AyChannel } from "./browser-XQnbPssj.js";
|
|
5
|
-
import { t as AyTerminal } from "./browser-
|
|
5
|
+
import { t as AyTerminal } from "./browser-wkzxw6lx.js";
|
|
6
6
|
import { AyWidget } from "./widget.js";
|
|
7
7
|
|
|
8
8
|
export { AyChannel, AyTerminal, AyWidget };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { F as listRecords } from "./subcommands-
|
|
1
|
+
import { F as listRecords } from "./subcommands-yWgVRM2r.js";
|
|
2
2
|
import { a as getProvisionRoot } from "./workspaceConfig-DTvPtr5j.js";
|
|
3
3
|
import { existsSync } from "fs";
|
|
4
4
|
import { lstat, opendir } from "fs/promises";
|
|
@@ -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-CMX0pkti.js.map
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import "./logger-CDIsZ-Pp.js";
|
|
2
2
|
import "./JsonlStore-BqF7AQ6E.js";
|
|
3
|
-
import "./globalPidIndex-
|
|
4
|
-
import "./pidStore-
|
|
5
|
-
import "./messageLog-
|
|
3
|
+
import "./globalPidIndex-CRH9L9tn.js";
|
|
4
|
+
import "./pidStore-DXWqzTSz.js";
|
|
5
|
+
import "./messageLog-Brx3w6zT.js";
|
|
6
6
|
import "./e2e-z1xpwSAg.js";
|
|
7
7
|
import "./configShared-DKTgiz7K.js";
|
|
8
|
-
import "./subcommands-
|
|
8
|
+
import "./subcommands-yWgVRM2r.js";
|
|
9
9
|
import "./notifyStore-2SWKGwZI.js";
|
|
10
|
-
import "./webrtcLink-
|
|
11
|
-
import "./remotes-
|
|
12
|
-
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-
|
|
10
|
+
import "./webrtcLink-CnQf9pmu.js";
|
|
11
|
+
import "./remotes-tmKwrr20.js";
|
|
12
|
+
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-CMX0pkti.js";
|
|
13
13
|
|
|
14
14
|
export { WS_JSON_SCHEMA, cmdWs, collectWorkspaces, isPathInside, loadProvision, walkWorkspaces, workspaceStatus };
|
package/lab/ui/console-logic.js
CHANGED
|
@@ -619,13 +619,33 @@ export function layeredRows(entries) {
|
|
|
619
619
|
// parent — which is what makes single-node layers vanish.
|
|
620
620
|
const roomNodes = [];
|
|
621
621
|
for (const [rid, re] of groupBy(entries, (e) => e._room || "")) {
|
|
622
|
-
|
|
622
|
+
// Group by SOURCE, not by the device label. A label is not an identity: two
|
|
623
|
+
// machines routinely report the same `user@host` — cloned images and
|
|
624
|
+
// container fleets are the normal provisioning case, and several daemons on
|
|
625
|
+
// one box report it too. Grouping by `_host` merged those into one peer
|
|
626
|
+
// group, so their agents landed in a single PID forest and could be shown
|
|
627
|
+
// parented to each other, and the layer then looked single-membered and was
|
|
628
|
+
// hidden entirely — two machines rendered as unlabelled sibling rows with
|
|
629
|
+
// nothing saying which box was which. `_src` is one machine by construction
|
|
630
|
+
// (`<room>/<peerId>` for a codehost peer, the room id for an ay-share room).
|
|
631
|
+
const peerGroups = groupBy(re, (e) => e._src || e._host || "");
|
|
623
632
|
const multiPeer = peerGroups.length > 1;
|
|
633
|
+
// Labels only have to be unique for READING; identity is `_src` above. When
|
|
634
|
+
// two sources really do share a label, suffix a short slice of the source so
|
|
635
|
+
// the operator can still tell the rows apart before sending a command to one.
|
|
636
|
+
const labelCounts = new Map();
|
|
637
|
+
for (const [, pe] of peerGroups) {
|
|
638
|
+
const l = pe[0]?._host || "";
|
|
639
|
+
if (l) labelCounts.set(l, (labelCounts.get(l) || 0) + 1);
|
|
640
|
+
}
|
|
624
641
|
const underRoom = [];
|
|
625
|
-
for (const [
|
|
642
|
+
for (const [src, pe] of peerGroups) {
|
|
626
643
|
const agentNodes = agentForestNodes(pe).map(toAgentNode);
|
|
627
|
-
|
|
628
|
-
|
|
644
|
+
const host = pe[0]?._host || "";
|
|
645
|
+
const label =
|
|
646
|
+
host && labelCounts.get(host) > 1 ? `${host} (${String(src).slice(-6)})` : host;
|
|
647
|
+
if (multiPeer && label)
|
|
648
|
+
underRoom.push({ kind: "peer", label, room: rid, host, src, children: agentNodes });
|
|
629
649
|
else underRoom.push(...agentNodes); // single/unlabelled peer hidden
|
|
630
650
|
}
|
|
631
651
|
if (multiRoom && rid) roomNodes.push({ kind: "room", label: rid, children: underRoom });
|
|
@@ -646,6 +666,9 @@ export function layeredRows(entries) {
|
|
|
646
666
|
entry: node.entry,
|
|
647
667
|
room: node.room, // peer headers: (room,host) keys the connection-type cache
|
|
648
668
|
host: node.host,
|
|
669
|
+
// The peer's source id. `host` can repeat across machines, so anything
|
|
670
|
+
// keying per-machine state off a header must use this, not (room,host).
|
|
671
|
+
src: node.src,
|
|
649
672
|
parentEntry: node.kind === "agent" ? parentAgent : null,
|
|
650
673
|
branch: railPrefix(ancestorsLast),
|
|
651
674
|
depth: ancestorsLast.length,
|
package/package.json
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { envelopeAttribution } from "./subcommands.ts";
|
|
3
|
+
import { senderLabel, type MessageRecord } from "./messageLog.ts";
|
|
4
|
+
|
|
5
|
+
const rec = (over: Partial<MessageRecord>): MessageRecord => ({
|
|
6
|
+
at: 1,
|
|
7
|
+
from: { pid: 20298, cli: "claude", cwd: "/x/acme/tree/dev", agent_id: "lane-dev" },
|
|
8
|
+
to: { pid: 2, cli: "claude", cwd: "/x/acme/tree/qa", agent_id: "lane-qa" },
|
|
9
|
+
body: "hi",
|
|
10
|
+
wrapped: true,
|
|
11
|
+
...over,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("a different tree claiming a lane is the contradiction", () => {
|
|
15
|
+
// Measured on real traffic before this change: the rule keyed on "is some
|
|
16
|
+
// OTHER registered lane my ancestor", and it ran inverted — LOUD on a lane's
|
|
17
|
+
// own nested processes (same tree, benign) and SILENT while another worktree
|
|
18
|
+
// sent two documents under this lane's name. The working directory is what
|
|
19
|
+
// actually separates them.
|
|
20
|
+
it("is LOUD in the envelope, because the receiving model reads only that", () => {
|
|
21
|
+
expect(envelopeAttribution("env-uncorroborated")).toContain("UNCORROBORATED");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("stays quiet for a lane's own detached process in its own tree", () => {
|
|
25
|
+
expect(envelopeAttribution("env-unverified")).toBe("");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("keeps the honest path silent", () => {
|
|
29
|
+
expect(envelopeAttribution("env")).toBe("");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders the distinction in a listing, where a human reads deliberately", () => {
|
|
33
|
+
expect(senderLabel(rec({ from_via: "env-uncorroborated" }))).toContain("UNCORROBORATED");
|
|
34
|
+
expect(senderLabel(rec({ from_via: "env-unverified" }))).toContain("unverified");
|
|
35
|
+
expect(senderLabel(rec({ from_via: "env" }))).toBe("claude #20298");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("still records what was measured about the real sender", () => {
|
|
39
|
+
// The audit that found the misattribution used exactly this: `from` said
|
|
40
|
+
// tree/dev, `sender_observed` said a process in tree/billings. Without the
|
|
41
|
+
// observation there is nothing to compare the claim against.
|
|
42
|
+
const r = rec({
|
|
43
|
+
from_via: "env-uncorroborated",
|
|
44
|
+
sender_observed: { user: "alice", host: "box", cwd: "/x/acme/tree/billings", pid: 47240 },
|
|
45
|
+
});
|
|
46
|
+
expect(r.sender_observed!.cwd).not.toBe(r.from!.cwd);
|
|
47
|
+
expect(senderLabel(r)).toContain("UNCORROBORATED");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("a sibling worktree of the same repo is the same lane", () => {
|
|
52
|
+
// Predicted by a lane before it fired, from its own fleet layout: lanes work
|
|
53
|
+
// in LINKED WORKTREES that are sibling directories, not children —
|
|
54
|
+
// ~/ws/org/_wt/feature-x alongside ~/ws/org/repo/tree/dev. A bare `ay send`
|
|
55
|
+
// from a shell there inherits AGENT_YES_PID honestly, and a path-containment
|
|
56
|
+
// check alone would accuse it.
|
|
57
|
+
//
|
|
58
|
+
// git answers it exactly: linked worktrees of one repository share a git
|
|
59
|
+
// common dir; a separate clone has its own. Measured on the reporting fleet —
|
|
60
|
+
// the sibling worktree resolved to the lane's own .git, and the worktree that
|
|
61
|
+
// had actually been misattributing resolved to a different one. A "same
|
|
62
|
+
// parent directory" heuristic would NOT separate them: both live under the
|
|
63
|
+
// same ancestor.
|
|
64
|
+
it("keeps the honest and the malign case distinguishable", () => {
|
|
65
|
+
// The property that matters, stated as the invariant rather than mocked:
|
|
66
|
+
// sharing a repo is what makes a foreign directory the same lane, and NOT
|
|
67
|
+
// sharing one is what makes it a different lane.
|
|
68
|
+
expect(envelopeAttribution("env-unverified")).toBe("");
|
|
69
|
+
expect(envelopeAttribution("env-uncorroborated")).toContain("UNCORROBORATED");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("renders the two outcomes differently in a listing", () => {
|
|
73
|
+
expect(senderLabel(rec({ from_via: "env-unverified" }))).toContain("unverified");
|
|
74
|
+
expect(senderLabel(rec({ from_via: "env-uncorroborated" }))).toContain("UNCORROBORATED");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { envelopeAttribution } from "./subcommands.ts";
|
|
3
|
+
import { senderLabel, type MessageRecord } from "./messageLog.ts";
|
|
4
|
+
|
|
5
|
+
const rec = (over: Partial<MessageRecord>): MessageRecord => ({
|
|
6
|
+
at: 1,
|
|
7
|
+
from: { pid: 20298, cli: "claude", cwd: "/x/acme/tree/dev", agent_id: "lane-dev" },
|
|
8
|
+
to: { pid: 2, cli: "claude", cwd: "/x/acme/tree/qa", agent_id: "lane-qa" },
|
|
9
|
+
body: "hi",
|
|
10
|
+
wrapped: true,
|
|
11
|
+
...over,
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("a different tree claiming a lane is the contradiction", () => {
|
|
15
|
+
// Measured on real traffic before this change: the rule keyed on "is some
|
|
16
|
+
// OTHER registered lane my ancestor", and it ran inverted — LOUD on a lane's
|
|
17
|
+
// own nested processes (same tree, benign) and SILENT while another worktree
|
|
18
|
+
// sent two documents under this lane's name. The working directory is what
|
|
19
|
+
// actually separates them.
|
|
20
|
+
it("is LOUD in the envelope, because the receiving model reads only that", () => {
|
|
21
|
+
expect(envelopeAttribution("env-uncorroborated")).toContain("UNCORROBORATED");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("stays quiet for a lane's own detached process in its own tree", () => {
|
|
25
|
+
expect(envelopeAttribution("env-unverified")).toBe("");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("keeps the honest path silent", () => {
|
|
29
|
+
expect(envelopeAttribution("env")).toBe("");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders the distinction in a listing, where a human reads deliberately", () => {
|
|
33
|
+
expect(senderLabel(rec({ from_via: "env-uncorroborated" }))).toContain("UNCORROBORATED");
|
|
34
|
+
expect(senderLabel(rec({ from_via: "env-unverified" }))).toContain("unverified");
|
|
35
|
+
expect(senderLabel(rec({ from_via: "env" }))).toBe("claude #20298");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("still records what was measured about the real sender", () => {
|
|
39
|
+
// The audit that found the misattribution used exactly this: `from` said
|
|
40
|
+
// tree/dev, `sender_observed` said a process in tree/billings. Without the
|
|
41
|
+
// observation there is nothing to compare the claim against.
|
|
42
|
+
const r = rec({
|
|
43
|
+
from_via: "env-uncorroborated",
|
|
44
|
+
sender_observed: { user: "alice", host: "box", cwd: "/x/acme/tree/billings", pid: 47240 },
|
|
45
|
+
});
|
|
46
|
+
expect(r.sender_observed!.cwd).not.toBe(r.from!.cwd);
|
|
47
|
+
expect(senderLabel(r)).toContain("UNCORROBORATED");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("a sibling worktree of the same repo is the same lane", () => {
|
|
52
|
+
// Predicted by a lane before it fired, from its own fleet layout: lanes work
|
|
53
|
+
// in LINKED WORKTREES that are sibling directories, not children —
|
|
54
|
+
// ~/ws/org/_wt/feature-x alongside ~/ws/org/repo/tree/dev. A bare `ay send`
|
|
55
|
+
// from a shell there inherits AGENT_YES_PID honestly, and a path-containment
|
|
56
|
+
// check alone would accuse it.
|
|
57
|
+
//
|
|
58
|
+
// git answers it exactly: linked worktrees of one repository share a git
|
|
59
|
+
// common dir; a separate clone has its own. Measured on the reporting fleet —
|
|
60
|
+
// the sibling worktree resolved to the lane's own .git, and the worktree that
|
|
61
|
+
// had actually been misattributing resolved to a different one. A "same
|
|
62
|
+
// parent directory" heuristic would NOT separate them: both live under the
|
|
63
|
+
// same ancestor.
|
|
64
|
+
it("keeps the honest and the malign case distinguishable", () => {
|
|
65
|
+
// The property that matters, stated as the invariant rather than mocked:
|
|
66
|
+
// sharing a repo is what makes a foreign directory the same lane, and NOT
|
|
67
|
+
// sharing one is what makes it a different lane.
|
|
68
|
+
expect(envelopeAttribution("env-unverified")).toBe("");
|
|
69
|
+
expect(envelopeAttribution("env-uncorroborated")).toContain("UNCORROBORATED");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("renders the two outcomes differently in a listing", () => {
|
|
73
|
+
expect(senderLabel(rec({ from_via: "env-unverified" }))).toContain("unverified");
|
|
74
|
+
expect(senderLabel(rec({ from_via: "env-uncorroborated" }))).toContain("UNCORROBORATED");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -389,6 +389,159 @@ describe("globalPidIndex", () => {
|
|
|
389
389
|
});
|
|
390
390
|
});
|
|
391
391
|
|
|
392
|
+
describe("sweepOrphanLogs", () => {
|
|
393
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
394
|
+
|
|
395
|
+
// The sweep always includes the caller's own cwd. Pin it inside the temp
|
|
396
|
+
// home so the suite can never reach the real repo's `.agent-yes/`.
|
|
397
|
+
beforeEach(() => {
|
|
398
|
+
vi.spyOn(process, "cwd").mockReturnValue(testHome);
|
|
399
|
+
});
|
|
400
|
+
afterEach(() => {
|
|
401
|
+
vi.restoreAllMocks();
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
/** Backdate a file so it falls outside the retention window. */
|
|
405
|
+
async function backdate(file: string, ageMs: number) {
|
|
406
|
+
const { utimes } = await import("fs/promises");
|
|
407
|
+
const when = new Date(Date.now() - ageMs);
|
|
408
|
+
await utimes(file, when, when);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
it("reclaims a dead pid's logs that no index record points at", async () => {
|
|
412
|
+
const mod = await loadModule();
|
|
413
|
+
const { mkdir, writeFile } = await import("fs/promises");
|
|
414
|
+
const { existsSync } = await import("fs");
|
|
415
|
+
|
|
416
|
+
// A project dir the index reaches only via a *sibling* record — the
|
|
417
|
+
// orphan's own record was already compacted away.
|
|
418
|
+
const projectCwd = path.join(testHome, "proj");
|
|
419
|
+
const dir = path.join(projectCwd, ".agent-yes");
|
|
420
|
+
await mkdir(dir, { recursive: true });
|
|
421
|
+
|
|
422
|
+
const orphanRaw = path.join(dir, "999999.raw.log");
|
|
423
|
+
const orphanDebug = path.join(dir, "999999.debug.log");
|
|
424
|
+
for (const f of [orphanRaw, orphanDebug]) {
|
|
425
|
+
await writeFile(f, "0123456789"); // 10 bytes each
|
|
426
|
+
await backdate(f, 30 * DAY);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// The only record in the index belongs to a different, live session.
|
|
430
|
+
const liveRaw = path.join(dir, `${process.pid}.raw.log`);
|
|
431
|
+
await writeFile(liveRaw, "x");
|
|
432
|
+
await backdate(liveRaw, 30 * DAY);
|
|
433
|
+
await mod.appendGlobalPid({
|
|
434
|
+
pid: process.pid,
|
|
435
|
+
cli: "claude",
|
|
436
|
+
prompt: null,
|
|
437
|
+
cwd: projectCwd,
|
|
438
|
+
log_file: liveRaw,
|
|
439
|
+
status: "active",
|
|
440
|
+
exit_code: null,
|
|
441
|
+
exit_reason: null,
|
|
442
|
+
started_at: Date.now(),
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const res = await mod.sweepOrphanLogs();
|
|
446
|
+
|
|
447
|
+
expect(res.removed.sort()).toEqual([orphanDebug, orphanRaw].sort());
|
|
448
|
+
expect(res.freedBytes).toBe(20);
|
|
449
|
+
expect(existsSync(orphanRaw)).toBe(false);
|
|
450
|
+
expect(existsSync(orphanDebug)).toBe(false);
|
|
451
|
+
expect(existsSync(liveRaw)).toBe(true); // pid still running
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("keeps logs inside the retention window and non-log files", async () => {
|
|
455
|
+
const mod = await loadModule();
|
|
456
|
+
const { mkdir, writeFile } = await import("fs/promises");
|
|
457
|
+
const { existsSync } = await import("fs");
|
|
458
|
+
|
|
459
|
+
const projectCwd = path.join(testHome, "proj");
|
|
460
|
+
const dir = path.join(projectCwd, ".agent-yes");
|
|
461
|
+
await mkdir(dir, { recursive: true });
|
|
462
|
+
|
|
463
|
+
const recent = path.join(dir, "999998.raw.log"); // dead pid, but fresh
|
|
464
|
+
await writeFile(recent, "x");
|
|
465
|
+
|
|
466
|
+
// Runtime state that shares the dir and must never be swept.
|
|
467
|
+
const inbox = path.join(dir, "inbox.jsonl");
|
|
468
|
+
const sqlite = path.join(dir, "pid.sqlite");
|
|
469
|
+
const records = path.join(dir, "pid-records.jsonl");
|
|
470
|
+
for (const f of [inbox, sqlite, records]) {
|
|
471
|
+
await writeFile(f, "x");
|
|
472
|
+
await backdate(f, 30 * DAY);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
await mod.appendGlobalPid({
|
|
476
|
+
pid: 999998,
|
|
477
|
+
cli: "claude",
|
|
478
|
+
prompt: null,
|
|
479
|
+
cwd: projectCwd,
|
|
480
|
+
log_file: recent,
|
|
481
|
+
status: "exited",
|
|
482
|
+
exit_code: 0,
|
|
483
|
+
exit_reason: null,
|
|
484
|
+
started_at: Date.now(),
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const res = await mod.sweepOrphanLogs();
|
|
488
|
+
|
|
489
|
+
expect(res.removed).toEqual([]);
|
|
490
|
+
for (const f of [recent, inbox, sqlite, records]) expect(existsSync(f)).toBe(true);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it("does not throw when the index is empty and the cwd has no .agent-yes", async () => {
|
|
494
|
+
const mod = await loadModule();
|
|
495
|
+
const res = await mod.sweepOrphanLogs();
|
|
496
|
+
expect(res.removed).toEqual([]);
|
|
497
|
+
expect(res.freedBytes).toBe(0);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
describe("gcLogs", () => {
|
|
502
|
+
beforeEach(() => {
|
|
503
|
+
vi.spyOn(process, "cwd").mockReturnValue(testHome);
|
|
504
|
+
});
|
|
505
|
+
afterEach(() => {
|
|
506
|
+
vi.restoreAllMocks();
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("counts a file once when both passes would claim it", async () => {
|
|
510
|
+
const mod = await loadModule();
|
|
511
|
+
const { mkdir, writeFile, utimes } = await import("fs/promises");
|
|
512
|
+
const { existsSync } = await import("fs");
|
|
513
|
+
|
|
514
|
+
const projectCwd = path.join(testHome, "proj");
|
|
515
|
+
const dir = path.join(projectCwd, ".agent-yes");
|
|
516
|
+
await mkdir(dir, { recursive: true });
|
|
517
|
+
|
|
518
|
+
// Dead + old in BOTH senses: the index still has its record (so the
|
|
519
|
+
// retention pass claims it) and its mtime is stale (so the sweep would
|
|
520
|
+
// too). It must be reported exactly once.
|
|
521
|
+
const raw = path.join(dir, "999999.raw.log");
|
|
522
|
+
await writeFile(raw, "01234"); // 5 bytes
|
|
523
|
+
const old = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
|
|
524
|
+
await utimes(raw, old, old);
|
|
525
|
+
await mod.appendGlobalPid({
|
|
526
|
+
pid: 999999,
|
|
527
|
+
cli: "claude",
|
|
528
|
+
prompt: null,
|
|
529
|
+
cwd: projectCwd,
|
|
530
|
+
log_file: raw,
|
|
531
|
+
status: "exited",
|
|
532
|
+
exit_code: 0,
|
|
533
|
+
exit_reason: null,
|
|
534
|
+
started_at: old.getTime(),
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
const res = await mod.gcLogs();
|
|
538
|
+
|
|
539
|
+
expect(res.removed).toEqual([raw]);
|
|
540
|
+
expect(res.freedBytes).toBe(5);
|
|
541
|
+
expect(existsSync(raw)).toBe(false);
|
|
542
|
+
});
|
|
543
|
+
});
|
|
544
|
+
|
|
392
545
|
it("skips corrupt lines without throwing", async () => {
|
|
393
546
|
const mod = await loadModule();
|
|
394
547
|
await mod.appendGlobalPid({
|