@intentius/chant-lexicon-fountain 0.72.2 → 0.72.4
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/acp/host.d.ts.map +1 -1
- package/dist/acp/serve.d.ts.map +1 -1
- package/dist/composites/steward.d.ts +7 -1
- package/dist/composites/steward.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/acp/gate-resolution.test.ts +81 -0
- package/src/acp/host.ts +22 -5
- package/src/acp/serve.ts +9 -0
- package/src/composites/steward.ts +7 -1
- package/src/op/runtime.test.ts +15 -3
package/dist/acp/host.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/acp/host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAClF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,uDAAuD;IACvD,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,4EAA4E;IAC5E,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,yCAAyC;AACzC,MAAM,WAAW,SAAS;IACxB,8CAA8C;IAC9C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxC,oDAAoD;IACpD,OAAO,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACrE,6FAA6F;IAC7F,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAChG,uFAAuF;IACvF,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1E;AAED,0EAA0E;AAC1E,eAAO,MAAM,YAAY,cAAc,CAAC;AAExC;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,
|
|
1
|
+
{"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/acp/host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAClF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,uDAAuD;IACvD,MAAM,CAAC,EAAE,QAAQ,CAAC;IAClB,4EAA4E;IAC5E,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,8CAA8C;IAC9C,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,yCAAyC;AACzC,MAAM,WAAW,SAAS;IACxB,8CAA8C;IAC9C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACxC,oDAAoD;IACpD,OAAO,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IACrE,6FAA6F;IAC7F,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAChG,uFAAuF;IACvF,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1E;AAED,0EAA0E;AAC1E,eAAO,MAAM,YAAY,cAAc,CAAC;AAExC;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAwDtD"}
|
package/dist/acp/serve.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/acp/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;
|
|
1
|
+
{"version":3,"file":"serve.d.ts","sourceRoot":"","sources":["../../src/acp/serve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AASH,wFAAwF;AACxF,wBAAsB,iBAAiB,CAAC,IAAI,GAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAqB/F"}
|
|
@@ -22,7 +22,13 @@
|
|
|
22
22
|
* `sandbox_mode: "persistent"` because the environment's checkout and tool
|
|
23
23
|
* cache have to survive a turn ending, `permission_policy: { default:
|
|
24
24
|
* "auto_allow" }` because there is nobody at the keyboard to answer a
|
|
25
|
-
* permission card
|
|
25
|
+
* permission card. Chant's own gates are still where a human belongs, and
|
|
26
|
+
* that is enforced rather than assumed: a gate resolved from this session
|
|
27
|
+
* would be answering a gate this session produced, which `recordGateApproval`
|
|
28
|
+
* refuses (chant#2384's origin rule, reached through chant#2400). So
|
|
29
|
+
* `auto_allow` decides ordinary tool calls and cannot clear a gate — the
|
|
30
|
+
* resolution has to arrive on a channel this agent is not on, normally
|
|
31
|
+
* `chant approve` at a shell, no `model`
|
|
26
32
|
* because an `acp` agent's model is whatever the command it launches decides
|
|
27
33
|
* to use, and no skills because a steward's competence is chant's op
|
|
28
34
|
* definitions rather than prose.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"steward.d.ts","sourceRoot":"","sources":["../../src/composites/steward.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"steward.d.ts","sourceRoot":"","sources":["../../src/composites/steward.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAChE,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAK5F;;;;;;;;;GASG;AACH,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,YAAY,CAAC,OAAO,UAAU,CAAC,CAAC;AAQnE,kFAAkF;AAClF,MAAM,WAAW,kBAAkB;IACjC,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAC;IACZ,mEAAmE;IACnE,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,WAAW;IAC1B,4DAA4D;IAC5D,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,WAAW,EAAE,YAAY,CAAC,OAAO,WAAW,CAAC,CAAC;IAC9C,8EAA8E;IAC9E,KAAK,CAAC,EAAE,YAAY,CAAC,OAAO,KAAK,CAAC,CAAC;IACnC;;;;OAIG;IACH,GAAG,EAAE,SAAS,EAAE,CAAC;IACjB,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B,uEAAuE;IACvE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,YAAY,CAAC,OAAO,KAAK,CAAC,CAAC;IAClC,QAAQ,EAAE,YAAY,CAAC,OAAO,QAAQ,CAAC,CAAC;IACxC,4DAA4D;IAC5D,SAAS,EAAE,YAAY,CAAC,OAAO,QAAQ,CAAC,EAAE,CAAC;IAC3C,OAAO,CAAC,EAAE,YAAY,CAAC,OAAO,OAAO,CAAC,CAAC;CACxC;AAED,mEAAmE;AACnE,eAAO,MAAM,uBAAuB,cAAc,CAAC;AAiBnD,iFAAiF;AACjF,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAE3D;AAED,iFAAiF;AACjF,wBAAgB,uBAAuB,IAAI,IAAI,CAG9C;AA8BD,wBAAgB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,gBAAgB,CAsF3D"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "22fc085ff69da74c44cb495ae9fdc594f95ec183191e8bef2b9884cfaf7d75da",
|
|
5
5
|
"meta.json": "ed2c507b18806e3eac6641d9b8fc85cd59401a86ab39ba39405869fab91ba729",
|
|
6
6
|
"types/index.d.ts": "242196b012731a2fb7b586159ab234bf33f63b1653e49fc625b1cff0b3f7f1f3",
|
|
7
7
|
"rules/ftn001-no-secret-literals.ts": "a417b194cd7039911cca9a0f14c49b042dfa6b1ee48517f7c2e22b16590a8599",
|
|
@@ -22,5 +22,5 @@
|
|
|
22
22
|
"skills/chant-fountain-ops.md": "206b9088be0c43807df32de39949888bd28eefd870a191743e051814c94b2d0c",
|
|
23
23
|
"skills/chant-fountain-locked-sandboxes.md": "de82f06cb3a08ba6bf3ae45fb9869e21d6da18b9ebe0fc769da8aebaceea7dd1"
|
|
24
24
|
},
|
|
25
|
-
"composite": "
|
|
25
|
+
"composite": "47bc6b0b254f3c5aa1b838d07c086731e1de903f915528ce0090ab9cb6c031fe"
|
|
26
26
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-fountain",
|
|
3
|
-
"version": "0.72.
|
|
3
|
+
"version": "0.72.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Fountain lexicon for chant — sandboxed agent environments, vaults, and agents as typed estate",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"bundle": "tsx src/package-cli.ts"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@intentius/chant": "^0.72.
|
|
53
|
+
"@intentius/chant": "^0.72.4",
|
|
54
54
|
"typescript": "^5.9.3",
|
|
55
55
|
"zod": "^4.3.6"
|
|
56
56
|
},
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* chant#2400 — an ACP session cannot clear a gate it produced.
|
|
7
|
+
*
|
|
8
|
+
* The `Steward` composite ships `permission_policy: { default: "auto_allow" }`,
|
|
9
|
+
* with a comment explaining that chant's own gates are where a human belongs.
|
|
10
|
+
* On the `--durable-requests` path that was not true. A gate became a
|
|
11
|
+
* `session/request_permission` (`./turn.ts`'s `gatedReply`), `auto_allow`
|
|
12
|
+
* answered it without anyone, and `AcpServer.resume` then wrote a real gate
|
|
13
|
+
* resolution and re-ran the command, which walked through the gate. The
|
|
14
|
+
* composite's doc described a separation its configuration removed, which is
|
|
15
|
+
* how it survived review: a reader finds an argument rather than a smell.
|
|
16
|
+
*
|
|
17
|
+
* chant#2384 did not close it, and the reason is the interesting part.
|
|
18
|
+
* `createChantHost`'s `resolveGate` called `appendGateResolution` DIRECTLY,
|
|
19
|
+
* writing a line that looked like an approval while skipping every check one
|
|
20
|
+
* carries: #2300's plan binding, so the resolution named no plan and
|
|
21
|
+
* authorised whatever the next run produced rather than what anyone read; the
|
|
22
|
+
* standing-pending-fact requirement, so it could answer a gate nothing had
|
|
23
|
+
* reached; and #2384's origin rule, which is the one that bites here.
|
|
24
|
+
*
|
|
25
|
+
* Routed through `recordGateApproval`, the same function `chant approve`
|
|
26
|
+
* calls, the rule applies: the pending fact and the resolution are both
|
|
27
|
+
* authored by the same ACP session, so it refuses. The property is now a
|
|
28
|
+
* consequence of the mechanism rather than of how a composite is configured.
|
|
29
|
+
*/
|
|
30
|
+
const HOST = fileURLToPath(new URL("./host.ts", import.meta.url));
|
|
31
|
+
const STEWARD = fileURLToPath(new URL("../composites/steward.ts", import.meta.url));
|
|
32
|
+
|
|
33
|
+
describe("an ACP gate resolution goes through the same door as chant approve (chant#2400)", () => {
|
|
34
|
+
const host = readFileSync(HOST, "utf8");
|
|
35
|
+
|
|
36
|
+
test("resolveGate calls recordGateApproval, not appendGateResolution", () => {
|
|
37
|
+
expect(host).toContain("recordGateApproval");
|
|
38
|
+
|
|
39
|
+
// The direct call is the defect. Writing the ledger line without the
|
|
40
|
+
// checks around it produces a record indistinguishable from an approval,
|
|
41
|
+
// which is worse than failing to write one.
|
|
42
|
+
//
|
|
43
|
+
// Matched on the import rather than the bare name, so the comment above
|
|
44
|
+
// `resolveGate` can go on explaining what it no longer does. A test that
|
|
45
|
+
// fails on its own subject being mentioned teaches people to stop
|
|
46
|
+
// mentioning it.
|
|
47
|
+
const importsLedgerWriter = /import\("@intentius\/chant\/lifecycle\/gate-ledger"\)/.test(host)
|
|
48
|
+
&& /\bappendGateResolution\s*[,}]/.test(host);
|
|
49
|
+
expect(
|
|
50
|
+
importsLedgerWriter,
|
|
51
|
+
"resolveGate must not append a resolution directly — that skips the plan binding, the " +
|
|
52
|
+
"standing-pending-fact requirement and the origin rule",
|
|
53
|
+
).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("it names the ACP channel, so the origin rule has something to compare", () => {
|
|
57
|
+
// Without an origin the rule cannot fire: `sameOriginRefusal` matches
|
|
58
|
+
// positively, so an unlabelled resolution is never refused.
|
|
59
|
+
expect(host).toMatch(/origin:\s*"acp"/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a refusal is surfaced rather than swallowed", () => {
|
|
63
|
+
// `recordGateApproval` reports failure by returning `{ ok: false }` and
|
|
64
|
+
// printing. A caller that ignored that would carry on and re-run the
|
|
65
|
+
// command, which is exactly the bypass this closes.
|
|
66
|
+
expect(host).toMatch(/outcome\.ok/);
|
|
67
|
+
expect(host).toContain("chant approve");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("the Steward's doc no longer claims a separation its configuration removes", () => {
|
|
71
|
+
const steward = readFileSync(STEWARD, "utf8");
|
|
72
|
+
|
|
73
|
+
// The configuration is unchanged and deliberately so: `auto_allow` is right
|
|
74
|
+
// for ordinary tool calls on a machine with nobody at the keyboard. What
|
|
75
|
+
// changed is that it can no longer clear a gate, and the doc has to say
|
|
76
|
+
// which of those it means.
|
|
77
|
+
expect(steward).toContain('permission_policy: { default: "auto_allow" }');
|
|
78
|
+
expect(steward).toMatch(/cannot clear a gate/);
|
|
79
|
+
expect(steward).toMatch(/chant#2384|origin rule/);
|
|
80
|
+
});
|
|
81
|
+
});
|
package/src/acp/host.ts
CHANGED
|
@@ -94,11 +94,28 @@ export function createChantHost(cwd: string): ChantHost {
|
|
|
94
94
|
},
|
|
95
95
|
|
|
96
96
|
async resolveGate(op, gate, resolvedBy) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
// chant#2400 — through `recordGateApproval`, the same function `chant
|
|
98
|
+
// approve` calls, rather than straight to `appendGateResolution`.
|
|
99
|
+
//
|
|
100
|
+
// Going direct wrote a line that looked like an approval and was missing
|
|
101
|
+
// every check one carries. It skipped #2300's plan binding, so the
|
|
102
|
+
// resolution named no plan and authorised whatever the next run produced
|
|
103
|
+
// rather than what anyone had read. It skipped the standing-pending-fact
|
|
104
|
+
// requirement, so it could answer a gate nothing had reached. And it
|
|
105
|
+
// skipped chant#2384's origin rule, which is the one that matters here:
|
|
106
|
+
// the pending fact and this resolution are both authored by the same ACP
|
|
107
|
+
// session, so the model that produced the gate was resolving it.
|
|
108
|
+
//
|
|
109
|
+
// With the rule applied, that case now refuses, which is the point. A
|
|
110
|
+
// Steward's gate reaches a person or it does not clear.
|
|
111
|
+
const { recordGateApproval } = await import("@intentius/chant/cli/handlers/operator");
|
|
112
|
+
const outcome = await recordGateApproval(op, gate, { actor: resolvedBy, origin: "acp" });
|
|
113
|
+
if (!outcome.ok) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`the resolution for gate "${gate}" on "${op}" was refused. A gate reached over ACP cannot ` +
|
|
116
|
+
`also be resolved over ACP — approve it with \`chant approve ${op} ${gate}\` at a shell.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
102
119
|
},
|
|
103
120
|
};
|
|
104
121
|
}
|
package/src/acp/serve.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { readFileSync } from "node:fs";
|
|
13
|
+
import { setGateOrigin } from "@intentius/chant/lifecycle/gate-origin";
|
|
13
14
|
import { dirname, join } from "node:path";
|
|
14
15
|
import { fileURLToPath } from "node:url";
|
|
15
16
|
import { AcpServer } from "./server";
|
|
@@ -17,6 +18,14 @@ import { streamTransport } from "./jsonrpc";
|
|
|
17
18
|
|
|
18
19
|
/** Serve until the client closes stdin, which is how it says the connection is over. */
|
|
19
20
|
export async function serveAcpOverStdio(opts: { durableRequests?: boolean } = {}): Promise<void> {
|
|
21
|
+
// chant#2384 — every prompt this session carries is authored by the model,
|
|
22
|
+
// and `approve` is a verb in chant's registry like any other, so nothing in
|
|
23
|
+
// the transport stops `chant run <op>` being followed by
|
|
24
|
+
// `chant approve <op> <gate>` as the next prompt. Declaring the channel is
|
|
25
|
+
// what makes the gate ledger able to tell that apart from a person at a
|
|
26
|
+
// shell; the same-origin rule then refuses it.
|
|
27
|
+
setGateOrigin("acp");
|
|
28
|
+
|
|
20
29
|
const server = new AcpServer({
|
|
21
30
|
durableRequests: opts.durableRequests ?? false,
|
|
22
31
|
version: lexiconVersion(),
|
|
@@ -22,7 +22,13 @@
|
|
|
22
22
|
* `sandbox_mode: "persistent"` because the environment's checkout and tool
|
|
23
23
|
* cache have to survive a turn ending, `permission_policy: { default:
|
|
24
24
|
* "auto_allow" }` because there is nobody at the keyboard to answer a
|
|
25
|
-
* permission card
|
|
25
|
+
* permission card. Chant's own gates are still where a human belongs, and
|
|
26
|
+
* that is enforced rather than assumed: a gate resolved from this session
|
|
27
|
+
* would be answering a gate this session produced, which `recordGateApproval`
|
|
28
|
+
* refuses (chant#2384's origin rule, reached through chant#2400). So
|
|
29
|
+
* `auto_allow` decides ordinary tool calls and cannot clear a gate — the
|
|
30
|
+
* resolution has to arrive on a channel this agent is not on, normally
|
|
31
|
+
* `chant approve` at a shell, no `model`
|
|
26
32
|
* because an `acp` agent's model is whatever the command it launches decides
|
|
27
33
|
* to use, and no skills because a steward's competence is chant's op
|
|
28
34
|
* definitions rather than prose.
|
package/src/op/runtime.test.ts
CHANGED
|
@@ -585,11 +585,23 @@ describe("tailConversation", () => {
|
|
|
585
585
|
json: { data: { id: "conv-1", status: "running", turn_count: 1 } },
|
|
586
586
|
},
|
|
587
587
|
});
|
|
588
|
-
// The turn thinks
|
|
589
|
-
// the polls were waiting through.
|
|
588
|
+
// The turn thinks until the poller has had more than one look at it, then
|
|
589
|
+
// finishes: several polls, then the event the polls were waiting through.
|
|
590
|
+
//
|
|
591
|
+
// The wait is on the poll count rather than on a fixed sleep. This used to
|
|
592
|
+
// sleep 25ms and assert that more than one 1ms poll had fit inside it,
|
|
593
|
+
// which is a wall-clock race the full suite loses every so often: a worker
|
|
594
|
+
// stalled past the sleep leaves the first event already due, exactly one
|
|
595
|
+
// poll lands, and `calls.length` is 1. Waiting on the number the assertion
|
|
596
|
+
// is about makes the same claim without timing it. The deadline keeps a
|
|
597
|
+
// genuine regression — a tail that stops polling — a loud failure here
|
|
598
|
+
// instead of a hang.
|
|
599
|
+
const deadline = Date.now() + 5_000;
|
|
590
600
|
const sse: FountainSse = () => ({
|
|
591
601
|
async *[Symbol.asyncIterator]() {
|
|
592
|
-
|
|
602
|
+
while (calls.length < 2 && Date.now() < deadline) {
|
|
603
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
604
|
+
}
|
|
593
605
|
yield sseEvent("1", { stream: "stdout", blocks: [{ kind: "text", body: JSON.stringify(RECORD) }] });
|
|
594
606
|
yield sseEvent("2", { stream: "stage", stage: "turn", state: "done" });
|
|
595
607
|
},
|