@desplega.ai/agent-swarm 2.5.3 → 2.5.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-adapter-gr2fcyyj.js → acp-adapter-ek38cwsv.js} +4 -4
- package/dist/{actions-wv766ghf.js → actions-92mepq03.js} +17 -17
- package/dist/{amp-adapter-speb906t.js → amp-adapter-9gaxg015.js} +2 -2
- package/dist/{app-ag29n14f.js → app-ctn8gsgx.js} +3 -3
- package/dist/{assistant-3x8bz3eg.js → assistant-zyr5kz47.js} +19 -19
- package/dist/{boot-reembed-jsmfrpzf.js → boot-reembed-dwzfttdr.js} +1 -1
- package/dist/{boot-reembed-5b3d2eea.js → boot-reembed-w75maga2.js} +1 -1
- package/dist/{boot-scrub-sweep-s73gerxy.js → boot-scrub-sweep-e9kq43kf.js} +3 -3
- package/dist/{boot-seeds-5zmax2wr.js → boot-seeds-x28v3fhb.js} +4 -4
- package/dist/{claude-adapter-hswdnm3t.js → claude-adapter-f0afqnne.js} +4 -4
- package/dist/{claude-managed-adapter-cvbzj1h6.js → claude-managed-adapter-x81a7r65.js} +3 -3
- package/dist/{claude-managed-setup-3k8m3hhc.js → claude-managed-setup-bqzwjd9x.js} +1 -1
- package/dist/{claude-sdk-session-fkbyk66x.js → claude-sdk-session-7b7whkaq.js} +9307 -9254
- package/dist/{cli-a5ebc95n.js → cli-0shx9j93.js} +16 -13
- package/dist/{cli-vwn81jbs.js → cli-19202kew.js} +3 -3
- package/dist/{cli-svrj71pg.js → cli-1r5ad7j5.js} +7 -7
- package/dist/{cli-hhxjnjzs.js → cli-25n3mv5d.js} +8 -6
- package/dist/cli-2655k16q.js +140 -0
- package/dist/{cli-hcpj2xqz.js → cli-277c31dw.js} +1 -1
- package/dist/{cli-35gtk1p6.js → cli-4b2z6n0m.js} +29 -7
- package/dist/{cli-40jsnrjm.js → cli-5bnsc0e1.js} +7 -5
- package/dist/{cli-fzcn3d87.js → cli-621ce7kw.js} +1 -1
- package/dist/{cli-s49hpn37.js → cli-64mh9mf3.js} +19 -5
- package/dist/{cli-e8jyq7d8.js → cli-6a1xg1p7.js} +6 -2
- package/dist/{cli-p9rafdrb.js → cli-7p4jxgtt.js} +4 -4
- package/dist/{cli-w1zwctj2.js → cli-9j129brq.js} +9 -3
- package/dist/{cli-ce0ak9ms.js → cli-abs8ndtr.js} +11 -11
- package/dist/{cli-aecdcr77.js → cli-asq5ydm4.js} +123 -50
- package/dist/{cli-n45mr7zh.js → cli-bvpgr9ny.js} +2 -2
- package/dist/{cli-210xd1f4.js → cli-c61mbdds.js} +3 -3
- package/dist/{cli-e5bxnjps.js → cli-c952c7ac.js} +5 -5
- package/dist/{cli-p859hcc8.js → cli-dew6x255.js} +1 -1
- package/dist/{cli-tx00c0h6.js → cli-dqygeeh5.js} +17 -1
- package/dist/{cli-gpv60278.js → cli-e42vss7b.js} +2 -2
- package/dist/{cli-s53vp709.js → cli-eftzyh9y.js} +1 -1
- package/dist/{cli-3ngdk5te.js → cli-ethgy7ha.js} +1 -1
- package/dist/{cli-99xgw26r.js → cli-ff1tm9n9.js} +1 -1
- package/dist/{cli-ewm3pwbq.js → cli-g9vd9ece.js} +37 -12
- package/dist/{cli-jjznk3f7.js → cli-gbfxp48k.js} +62 -28
- package/dist/{cli-z21whrrg.js → cli-gn5af0x5.js} +14 -14
- package/dist/{cli-1fc85mvt.js → cli-gwhjmh8g.js} +4 -4
- package/dist/{cli-avdvgcay.js → cli-hh5we352.js} +24 -5
- package/dist/{cli-0ebey9hg.js → cli-j6rktc91.js} +1 -1
- package/dist/{cli-kxawjrxg.js → cli-jb30dzvk.js} +332 -36
- package/dist/{cli-25j01dpj.js → cli-jbnn4vxb.js} +3 -3
- package/dist/{cli-qzme5949.js → cli-k4tjsdx9.js} +44 -39
- package/dist/{cli-8s5wx3nz.js → cli-m4ecdxvz.js} +40 -13
- package/dist/{cli-ta8bd7th.js → cli-pzatvw94.js} +1 -1
- package/dist/{cli-p6zs9h4y.js → cli-rr7zx19j.js} +767 -249
- package/dist/{cli-502eqv93.js → cli-s682zgqz.js} +1 -1
- package/dist/{cli-fsdprwrg.js → cli-sbwsxvtd.js} +1 -1
- package/dist/{cli-0xk505e8.js → cli-sxgtf5fw.js} +1 -1
- package/dist/{cli-egjggehm.js → cli-tsma7p8v.js} +1 -1
- package/dist/{cli-qgf8emrn.js → cli-wghjkr0e.js} +5 -5
- package/dist/{cli-p0rzjkya.js → cli-wx2nj19m.js} +29 -10
- package/dist/{cli-e2ar4bp1.js → cli-x0f2v69c.js} +111 -42
- package/dist/{cli-y561h5a9.js → cli-xtrz6bzs.js} +5 -5
- package/dist/{cli-gpsz7g88.js → cli-xzfq4at3.js} +7 -7
- package/dist/cli.js +18 -18
- package/dist/{codex-adapter-n8ka66vf.js → codex-adapter-wch12q99.js} +2 -2
- package/dist/{codex-hook-e2he7aca.js → codex-hook-brpnycn5.js} +1 -1
- package/dist/{codex-session-runner-fsebts37.js → codex-session-runner-41pfcr95.js} +2 -2
- package/dist/{commands-t04rt2pc.js → commands-9849dkj6.js} +2 -2
- package/dist/{controller-rgna04r2.js → controller-ja0sd95p.js} +27 -1
- package/dist/{controllers-connect-bqm1rtft.js → controllers-connect-xp05dj6x.js} +16 -16
- package/dist/{cursor-adapter-swr2k95y.js → cursor-adapter-623qxqew.js} +1 -2
- package/dist/{db-a4m7haw0.js → db-47vj40dy.js} +3 -3
- package/dist/{db-24ck09xy.js → db-w759qmx0.js} +2 -2
- package/dist/{dispatcher-nwf9ss0q.js → dispatcher-7b8e4z2j.js} +15 -15
- package/dist/{docker-nfy2gc8t.js → docker-s84hjs91.js} +2 -2
- package/dist/{dsh-adapter-kpdnhjst.js → dsh-adapter-2ezw3xea.js} +1 -2
- package/dist/{external-channel-j1e2p34p.js → external-channel-e2s028da.js} +2 -2
- package/dist/{grok-adapter-qtjz0whk.js → grok-adapter-ys7gaqee.js} +5 -5
- package/dist/{handlers-19xr6dnz.js → handlers-wy1dr6ja.js} +18 -18
- package/dist/{heartbeat-vtesda5d.js → heartbeat-w3mntghj.js} +17 -17
- package/dist/{hook-8wqaszda.js → hook-3jjywnpb.js} +9 -3
- package/dist/{hook-b2hhj6rd.js → hook-g7kvv6x9.js} +3 -2
- package/dist/{http-mxcqhhcs.js → http-tkp7d4kb.js} +267 -75
- package/dist/{index-srg522m6.js → index-1ef1myq2.js} +15 -15
- package/dist/{index-1r8wfpzs.js → index-3n8m9ea2.js} +1 -1
- package/dist/{index-kw2mmat7.js → index-nmm79qgr.js} +17 -17
- package/dist/{index-n1vbvta9.js → index-q7m26kvs.js} +24 -22
- package/dist/{index-spwxjgay.js → index-xdaahthb.js} +15 -15
- package/dist/{keepalive-dsd8rtgx.js → keepalive-v4ta8wgg.js} +3 -3
- package/dist/{lead-zyerk1x2.js → lead-sawnyqxk.js} +33 -33
- package/dist/{maintenance-jqmqwnet.js → maintenance-84n1ex21.js} +1 -1
- package/dist/{oauth-refresh-sweep-q2c3bnvm.js → oauth-refresh-sweep-vjnnandw.js} +2 -2
- package/dist/{onboard-w4yd7s8t.js → onboard-sfmpe4np.js} +2 -2
- package/dist/{opencode-adapter-9tppvxar.js → opencode-adapter-jmpst4q4.js} +3 -3
- package/dist/{otel-impl-f36byzbd.js → otel-impl-v6cc0zve.js} +1 -1
- package/dist/{pi-mono-adapter-tqsge1ay.js → pi-mono-adapter-fk8fkcf6.js} +33 -6
- package/dist/{pricing-refresh-xvak5p4v.js → pricing-refresh-fsk1e1zc.js} +3 -3
- package/dist/{rbac-roles-gfn30fb8.js → rbac-roles-prmkdnkw.js} +2 -2
- package/dist/{rbac-roles-682k2mt5.js → rbac-roles-zxmxqa39.js} +1 -1
- package/dist/{render-v2-hgytz8nn.js → render-v2-pjwxvnkw.js} +3 -3
- package/dist/{resume-2dvq348g.js → resume-frdtgfa0.js} +8 -6
- package/dist/{session-access-7f21ekq9.js → session-access-fpez4y5q.js} +4 -4
- package/dist/{setup-epffxtbk.js → setup-mq6m3m8q.js} +1 -1
- package/dist/{work-objects-xz1a5rqk.js → work-objects-kfahhjsq.js} +18 -18
- package/dist/{worker-42t8emjt.js → worker-v5e5rxj3.js} +33 -33
- package/openapi.json +68 -3
- package/package.json +5 -5
- package/plugin/commands/create-pr.md +2 -2
- package/plugin/pi-skills/create-pr/SKILL.md +2 -2
- package/src/be/db/approvals.ts +48 -0
- package/src/be/db/tasks/write.ts +15 -5
- package/src/be/db/workflow-idempotency.ts +183 -0
- package/src/be/db/workflow-runs.ts +318 -7
- package/src/be/db.ts +67 -17
- package/src/be/migrations/242_workflow_idempotency_keys.sql +40 -0
- package/src/be/sealed-json.ts +1 -0
- package/src/be/seed-scripts/catalog/compound-insights.inline.ts +2 -2
- package/src/be/seed-scripts/catalog/compound-insights.ts +2 -2
- package/src/be/seed-skills/bundled-files.generated.json +1 -1
- package/src/be/sql-connections.ts +7 -1
- package/src/be/swarm-config-guard.ts +2 -0
- package/src/be/users.ts +9 -0
- package/src/commands/controller.ts +33 -0
- package/src/commands/runner.ts +6 -2
- package/src/extensions/apply-task-create.ts +57 -11
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +25 -0
- package/src/extensions/dispatcher.ts +4 -0
- package/src/extensions/post-bridge.ts +8 -0
- package/src/hooks/context-bound-note.ts +196 -0
- package/src/hooks/hook.ts +12 -0
- package/src/http/config.ts +48 -16
- package/src/http/idempotency-key.ts +117 -0
- package/src/http/workflows.ts +129 -24
- package/src/prompts/context-bound-note.ts +48 -0
- package/src/prompts/session-templates.ts +25 -1
- package/src/providers/amp-adapter.ts +30 -8
- package/src/providers/claude-adapter.ts +29 -4
- package/src/providers/claude-managed-adapter.ts +26 -0
- package/src/providers/claude-sdk-session.ts +10 -0
- package/src/providers/claude-session-events.ts +13 -0
- package/src/providers/codex-adapter.ts +12 -0
- package/src/providers/opencode-adapter.ts +18 -0
- package/src/providers/pi-mono-adapter.ts +35 -0
- package/src/runtime-providers/docker.ts +6 -2
- package/src/scripts-runtime/types/swarm-extension.d.ts +35 -0
- package/src/tests/amp-adapter.test.ts +97 -0
- package/src/tests/approval-requests.test.ts +10 -7
- package/src/tests/approval-sweeps.test.ts +12 -6
- package/src/tests/base-prompt.test.ts +1 -1
- package/src/tests/claude-hook-output.test.ts +79 -1
- package/src/tests/claude-managed-adapter.test.ts +275 -0
- package/src/tests/codex-adapter.test.ts +163 -0
- package/src/tests/context-bound-note.test.ts +269 -0
- package/src/tests/extensions-pre-task.test.ts +273 -0
- package/src/tests/fetch-resolved-env.test.ts +17 -0
- package/src/tests/fixtures/extensions/record-blocks/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/record-blocks/manifest.json +7 -0
- package/src/tests/fixtures/fake-amp.ts +22 -8
- package/src/tests/fixtures/held-template-render.ts +42 -0
- package/src/tests/fixtures/workflow-idempotency-trigger.child.ts +73 -0
- package/src/tests/openapi-route-headers.test.ts +2 -0
- package/src/tests/opencode-adapter.test.ts +134 -0
- package/src/tests/pi-mono-adapter.test.ts +105 -0
- package/src/tests/prompt-template-session.test.ts +4 -3
- package/src/tests/runner-push-delivery.test.ts +25 -0
- package/src/tests/runtime-controller-liveness.test.ts +133 -0
- package/src/tests/runtime-providers-docker.test.ts +2 -0
- package/src/tests/script-connections.test.ts +3 -0
- package/src/tests/script-run-config-scope.test.ts +91 -3
- package/src/tests/session-logs.test.ts +70 -0
- package/src/tests/sessions-tree.test.ts +37 -0
- package/src/tests/workflow-cascade-cancel.test.ts +1169 -0
- package/src/tests/workflow-foreach-supersede-retry.test.ts +488 -0
- package/src/tests/workflow-idempotency.test.ts +915 -0
- package/src/tools/workflows/cancel-workflow-run.ts +1 -1
- package/src/utils/context-window.ts +8 -6
- package/src/workflows/approval-cancel.ts +15 -13
- package/src/workflows/approval-notifications.ts +5 -1
- package/src/workflows/checkpoint.ts +6 -3
- package/src/workflows/engine.ts +30 -8
- package/src/workflows/executors/foreach.ts +2 -2
- package/src/workflows/foreach-driver.ts +10 -3
- package/src/workflows/foreach-join.ts +29 -6
- package/src/workflows/recovery.ts +6 -1
- package/src/workflows/resume.ts +168 -56
- package/src/workflows/retry-poller.ts +49 -3
- package/src/workflows/task-step-routing.ts +29 -7
- package/templates/ai-toolbox.manifest.json +8 -8
- package/templates/community/open-prompt-showreel/PLAYBOOK.md +5 -6
- package/templates/community/open-prompt-showreel/lead-prompt.md +1 -1
- package/templates/community/open-prompt-showreel/skills/open-prompt-showreel/SKILL.md +4 -4
- package/templates/skills/agent-browser/SKILL.md +4 -4
- package/templates/skills/agent-browser/content.md +4 -4
- package/templates/skills/code-quality/config.json +1 -1
- package/templates/skills/code-quality/content.md +2 -2
- package/templates/skills/delegate-work/content.md +29 -25
- package/templates/skills/delegate-work/files/scripts/codex-exec.sh +5 -5
- package/templates/skills/qa/content.md +2 -2
- package/templates/skills/researching/content.md +2 -0
- package/templates/skills/work-on-task/SKILL.md +1 -1
- package/templates/skills/work-on-task/content.md +1 -1
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
import { existsSync as nodeExistsSync } from "node:fs";
|
|
32
32
|
import { join } from "node:path";
|
|
33
33
|
import { buildRatingsFromLlm, fetchRetrievalsForTask, postRatings } from "../be/memory/raters/llm";
|
|
34
|
+
import { contextBoundNoteThreshold, createContextBoundNote } from "../hooks/context-bound-note";
|
|
34
35
|
import { getApiKey } from "../utils/api-key";
|
|
35
36
|
import {
|
|
36
37
|
CONTEXT_FORMULA,
|
|
@@ -995,6 +996,8 @@ export class CodexSession implements ProviderSession {
|
|
|
995
996
|
* summary path which materially relies on this ordering for testability.
|
|
996
997
|
*/
|
|
997
998
|
private pendingResult: ProviderResult | null = null;
|
|
999
|
+
/** Fed each main-thread request's prompt size; steers the one-time context-bound note. */
|
|
1000
|
+
private readonly observeContextBound: (contextTokens: number | undefined) => Promise<void>;
|
|
998
1001
|
|
|
999
1002
|
constructor(
|
|
1000
1003
|
thread: CodexThread,
|
|
@@ -1015,6 +1018,13 @@ export class CodexSession implements ProviderSession {
|
|
|
1015
1018
|
skillsDir ?? process.env.CODEX_SKILLS_DIR ?? join(codexUserHome(), ".codex", "skills");
|
|
1016
1019
|
this.summarizeDeps = summarizeDeps;
|
|
1017
1020
|
this.logFileHandle = Bun.file(config.logFile).writer();
|
|
1021
|
+
this.observeContextBound = createContextBoundNote({
|
|
1022
|
+
threshold: contextBoundNoteThreshold(config.env ?? process.env),
|
|
1023
|
+
model: () => this.resolvedModel,
|
|
1024
|
+
// `turn/steer` only joins the active turn, so a late note never starts one.
|
|
1025
|
+
canDeliver: () => this.acceptsSteering && !this.settled && !this.aborted,
|
|
1026
|
+
inject: (note) => this.deliverSteering({ mode: "steer", text: note }),
|
|
1027
|
+
});
|
|
1018
1028
|
// Mirrors the resolution `buildCodexConfig` already applied when
|
|
1019
1029
|
// constructing `mergedConfig` — cheap, pure, and keeps the applied-level
|
|
1020
1030
|
// feedback co-located with `settle()` rather than threading an extra
|
|
@@ -1425,6 +1435,8 @@ export class CodexSession implements ProviderSession {
|
|
|
1425
1435
|
break;
|
|
1426
1436
|
case "context.updated":
|
|
1427
1437
|
this.emitContextUsage(event.usage, event.contextWindow ?? this.contextWindow);
|
|
1438
|
+
// Codex input tokens already include the cached read and write subsets.
|
|
1439
|
+
void this.observeContextBound(event.usage.input_tokens);
|
|
1428
1440
|
break;
|
|
1429
1441
|
case "error": {
|
|
1430
1442
|
const { message } = this.formatTerminalError(event.message, event.codexErrorInfo);
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
providerKeyVars,
|
|
22
22
|
} from "@desplega/model-catalog";
|
|
23
23
|
import type { OpenCode, OpenCodeEvent } from "@opencode/sdk";
|
|
24
|
+
import { contextBoundNoteThreshold, createContextBoundNote } from "../hooks/context-bound-note";
|
|
24
25
|
import {
|
|
25
26
|
CONTEXT_FORMULA,
|
|
26
27
|
clampContextPercent,
|
|
@@ -534,6 +535,8 @@ export class OpencodeSession implements ProviderSession {
|
|
|
534
535
|
// Assistant text by message, in part order; the runner keeps the last emit.
|
|
535
536
|
private textByMessage = new Map<string, string[]>();
|
|
536
537
|
private transcript: string[] = [];
|
|
538
|
+
/** Fed each step's prompt size; steers the one-time context-bound note. */
|
|
539
|
+
private readonly observeContextBound: (contextTokens: number | undefined) => Promise<void>;
|
|
537
540
|
|
|
538
541
|
constructor(
|
|
539
542
|
private readonly host: OpenCode.Interface,
|
|
@@ -547,6 +550,20 @@ export class OpencodeSession implements ProviderSession {
|
|
|
547
550
|
this.completionPromise = new Promise<ProviderResult>((resolve) => {
|
|
548
551
|
this.completionResolve = resolve;
|
|
549
552
|
});
|
|
553
|
+
this.observeContextBound = createContextBoundNote({
|
|
554
|
+
threshold: contextBoundNoteThreshold(swarm.env ?? process.env),
|
|
555
|
+
model: () => this.model,
|
|
556
|
+
canDeliver: () => !this.completed && !this.aborted,
|
|
557
|
+
// A steer is promoted at the next step boundary mid-work; `resume: false`
|
|
558
|
+
// leaves an idle session idle. Same route as OpenCode's own Plan reminder.
|
|
559
|
+
inject: (note) =>
|
|
560
|
+
host.sessions.synthetic({
|
|
561
|
+
sessionID: _sessionId,
|
|
562
|
+
text: note,
|
|
563
|
+
delivery: "steer",
|
|
564
|
+
resume: false,
|
|
565
|
+
}),
|
|
566
|
+
});
|
|
550
567
|
}
|
|
551
568
|
|
|
552
569
|
get sessionId(): string {
|
|
@@ -685,6 +702,7 @@ export class OpencodeSession implements ProviderSession {
|
|
|
685
702
|
// Per-step usage drives the dashboard's context gauge.
|
|
686
703
|
const t = ev.data.tokens;
|
|
687
704
|
const contextUsed = t.input + t.cache.read + t.cache.write + t.output;
|
|
705
|
+
void this.observeContextBound(t.input + t.cache.read + t.cache.write);
|
|
688
706
|
const contextTotal = getContextWindowSize(this.model || "default");
|
|
689
707
|
if (contextTotal > 0 && contextUsed > 0) {
|
|
690
708
|
this.emit({
|
|
@@ -61,6 +61,7 @@ import { type TSchema, Type } from "typebox";
|
|
|
61
61
|
import "./pi-codemode-runtime";
|
|
62
62
|
// Registers the reprompt template in the code registry (the runner loads it too).
|
|
63
63
|
import "../commands/templates";
|
|
64
|
+
import { contextBoundNoteThreshold, createContextBoundNote } from "../hooks/context-bound-note";
|
|
64
65
|
import { resolveTemplateAsync } from "../prompts/resolver";
|
|
65
66
|
import { CORE_TOOLS } from "../tools/tool-config";
|
|
66
67
|
import { classifyAwsSdkError } from "../utils/aws-error-classifier";
|
|
@@ -907,6 +908,17 @@ function safeLabel(value: unknown): string {
|
|
|
907
908
|
return typeof value === "string" && /^[A-Za-z0-9_-]{1,32}$/.test(value) ? value : "other";
|
|
908
909
|
}
|
|
909
910
|
|
|
911
|
+
/** Prompt size of the request behind an assistant message: input + cache read + cache write. */
|
|
912
|
+
function requestContextTokens(message: unknown): number | undefined {
|
|
913
|
+
const usage = (
|
|
914
|
+
message as { usage?: { input?: unknown; cacheRead?: unknown; cacheWrite?: unknown } }
|
|
915
|
+
)?.usage;
|
|
916
|
+
if (typeof usage?.input !== "number") return undefined;
|
|
917
|
+
const cacheRead = typeof usage.cacheRead === "number" ? usage.cacheRead : 0;
|
|
918
|
+
const cacheWrite = typeof usage.cacheWrite === "number" ? usage.cacheWrite : 0;
|
|
919
|
+
return usage.input + cacheRead + cacheWrite;
|
|
920
|
+
}
|
|
921
|
+
|
|
910
922
|
function describeAssistantTurn(message: unknown): AssistantTurnShape {
|
|
911
923
|
const msg = (message ?? {}) as {
|
|
912
924
|
content?: unknown;
|
|
@@ -1008,6 +1020,14 @@ export class PiMonoSession implements ProviderSession {
|
|
|
1008
1020
|
private abortRequested = false;
|
|
1009
1021
|
/** Removes this session's own models.json once the session ends. */
|
|
1010
1022
|
private disposeModels: () => Promise<void>;
|
|
1023
|
+
/** Fed each assistant request's prompt size; steers the one-time context-bound note. */
|
|
1024
|
+
private readonly observeContextBound: (contextTokens: number | undefined) => Promise<void>;
|
|
1025
|
+
/**
|
|
1026
|
+
* From a tool-call reply until its turn ends. Pi reads steering after the
|
|
1027
|
+
* turn's tool results; a steer queued later waits for the next reply, and
|
|
1028
|
+
* when that is the final one it starts another turn.
|
|
1029
|
+
*/
|
|
1030
|
+
private toolTurnOpen = false;
|
|
1011
1031
|
|
|
1012
1032
|
constructor(
|
|
1013
1033
|
agentSession: AgentSession,
|
|
@@ -1024,6 +1044,12 @@ export class PiMonoSession implements ProviderSession {
|
|
|
1024
1044
|
this.logFileHandle = Bun.file(config.logFile).writer();
|
|
1025
1045
|
this._sessionId = agentSession.sessionId;
|
|
1026
1046
|
this.sessionStartedAt = Date.now();
|
|
1047
|
+
this.observeContextBound = createContextBoundNote({
|
|
1048
|
+
threshold: contextBoundNoteThreshold(config.env ?? process.env),
|
|
1049
|
+
model: () => this.reportedModel(),
|
|
1050
|
+
canDeliver: () => this.toolTurnOpen && !this.sessionEnded && !this.abortRequested,
|
|
1051
|
+
inject: (note) => this.deliverSteering({ mode: "steer", text: note }),
|
|
1052
|
+
});
|
|
1027
1053
|
|
|
1028
1054
|
// Emit session_init immediately
|
|
1029
1055
|
const piVersion = readPkgVersion("@earendil-works/pi-coding-agent");
|
|
@@ -1098,6 +1124,12 @@ export class PiMonoSession implements ProviderSession {
|
|
|
1098
1124
|
}
|
|
1099
1125
|
// A successful assistant turn means any prior error has recovered.
|
|
1100
1126
|
this.terminalError = null;
|
|
1127
|
+
// Only on tool-call turns: a steer after the final reply would start
|
|
1128
|
+
// another turn.
|
|
1129
|
+
if (turn.hasToolCall) {
|
|
1130
|
+
this.toolTurnOpen = true;
|
|
1131
|
+
void this.observeContextBound(requestContextTokens(event.message));
|
|
1132
|
+
}
|
|
1101
1133
|
if (!turn.hasText && !turn.hasToolCall) {
|
|
1102
1134
|
// Nothing else records these turns: no text and no tool call leaves
|
|
1103
1135
|
// no trace in session logs. Block types only, never content.
|
|
@@ -1152,6 +1184,9 @@ export class PiMonoSession implements ProviderSession {
|
|
|
1152
1184
|
}
|
|
1153
1185
|
break;
|
|
1154
1186
|
}
|
|
1187
|
+
case "turn_end":
|
|
1188
|
+
this.toolTurnOpen = false;
|
|
1189
|
+
break;
|
|
1155
1190
|
case "tool_execution_start": {
|
|
1156
1191
|
const status = (event.args as { status?: unknown } | null)?.status;
|
|
1157
1192
|
if (
|
|
@@ -177,7 +177,9 @@ export class DockerRuntimeProvider implements RuntimeProvider {
|
|
|
177
177
|
try {
|
|
178
178
|
await dockerApi(target, "POST", `/containers/${created.Id}/start`);
|
|
179
179
|
} catch (err) {
|
|
180
|
-
await dockerApi(target, "DELETE", `/containers/${created.Id}?force=true`).catch(
|
|
180
|
+
await dockerApi(target, "DELETE", `/containers/${created.Id}?force=true&v=true`).catch(
|
|
181
|
+
() => {},
|
|
182
|
+
);
|
|
181
183
|
throw err;
|
|
182
184
|
}
|
|
183
185
|
return { kind: "docker", externalId: created.Id };
|
|
@@ -188,7 +190,9 @@ export class DockerRuntimeProvider implements RuntimeProvider {
|
|
|
188
190
|
|
|
189
191
|
async terminate(target: RuntimeTargetConfig, handle: RuntimeHandle): Promise<void> {
|
|
190
192
|
try {
|
|
191
|
-
|
|
193
|
+
// v=true also removes the container's anonymous volumes (the worker image declares
|
|
194
|
+
// VOLUME /logs); named volumes are never removed by it.
|
|
195
|
+
await dockerApi(target, "DELETE", `/containers/${handle.externalId}?force=true&v=true`);
|
|
192
196
|
} catch (err) {
|
|
193
197
|
if (!(err instanceof DockerApiError) || err.statusCode !== 404) throw err;
|
|
194
198
|
}
|
|
@@ -402,6 +402,36 @@ declare module "swarm-extension" {
|
|
|
402
402
|
origin: TaskCreateOrigin;
|
|
403
403
|
requestInfo?: RequestInfo;
|
|
404
404
|
}
|
|
405
|
+
/** A pre.task.create hook blocked a task before it was created. */
|
|
406
|
+
export interface TaskBlockedEvent {
|
|
407
|
+
/** The extension that blocked, or whose model/agent rewrite was rejected. */
|
|
408
|
+
extension: {
|
|
409
|
+
id: string;
|
|
410
|
+
name: string;
|
|
411
|
+
};
|
|
412
|
+
/** The scrubbed block reason. */
|
|
413
|
+
reason: string;
|
|
414
|
+
origin: TaskCreateOrigin;
|
|
415
|
+
source?: AgentTask["source"];
|
|
416
|
+
requester: {
|
|
417
|
+
userId?: string;
|
|
418
|
+
agentId?: string;
|
|
419
|
+
slackUserId?: string;
|
|
420
|
+
};
|
|
421
|
+
context?: {
|
|
422
|
+
contextKey?: string;
|
|
423
|
+
scheduleId?: string;
|
|
424
|
+
workflowRunId?: string;
|
|
425
|
+
workflowRunStepId?: string;
|
|
426
|
+
parentTaskId?: string;
|
|
427
|
+
vcsRepo?: string;
|
|
428
|
+
vcsNumber?: number;
|
|
429
|
+
slackChannelId?: string;
|
|
430
|
+
slackThreadTs?: string;
|
|
431
|
+
};
|
|
432
|
+
/** The original task description, scrubbed and cut to 500 characters. */
|
|
433
|
+
description: string;
|
|
434
|
+
}
|
|
405
435
|
export type TaskCreateModify = Partial<
|
|
406
436
|
Pick<
|
|
407
437
|
CreateTaskOptions,
|
|
@@ -704,6 +734,11 @@ declare module "swarm-extension" {
|
|
|
704
734
|
modify: never;
|
|
705
735
|
result: void;
|
|
706
736
|
};
|
|
737
|
+
"post.task.blocked": {
|
|
738
|
+
event: TaskBlockedEvent;
|
|
739
|
+
modify: never;
|
|
740
|
+
result: void;
|
|
741
|
+
};
|
|
707
742
|
"post.task.completed": {
|
|
708
743
|
event: TaskCompletedEvent;
|
|
709
744
|
modify: never;
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
validateProviderCredentials,
|
|
9
9
|
} from "../commands/provider-credentials";
|
|
10
10
|
import { recomputeSessionCost } from "../http/session-cost-recompute";
|
|
11
|
+
import { renderContextBoundNote } from "../prompts/context-bound-note";
|
|
11
12
|
import { createProviderAdapter } from "../providers";
|
|
12
13
|
import {
|
|
13
14
|
AMP_PACKAGE,
|
|
@@ -31,6 +32,7 @@ import { getModelAwareCredentialVars } from "../utils/credentials";
|
|
|
31
32
|
import { resolveHarnessProvider } from "../utils/harness-provider";
|
|
32
33
|
import { clearVolatileSecretsForTesting } from "../utils/secret-scrubber";
|
|
33
34
|
import { ampPricingLookup } from "./amp-pricing-helpers";
|
|
35
|
+
import { holdTemplateRender } from "./fixtures/held-template-render";
|
|
34
36
|
|
|
35
37
|
const KEY = "sgamp_test_credential_value";
|
|
36
38
|
|
|
@@ -769,6 +771,101 @@ describe("amp session", () => {
|
|
|
769
771
|
}, 20_000);
|
|
770
772
|
});
|
|
771
773
|
|
|
774
|
+
describe("amp context-bound note", () => {
|
|
775
|
+
// Each fake request is 4 input + 200 cache read + 1000 cache write = 1204 tokens.
|
|
776
|
+
async function toolResults(
|
|
777
|
+
mode: string,
|
|
778
|
+
env: Record<string, string>,
|
|
779
|
+
overrides: Partial<ProviderSessionConfig> = {},
|
|
780
|
+
) {
|
|
781
|
+
const { config } = await fixture(mode, overrides, env);
|
|
782
|
+
const { events, result } = await runToCompletion(config);
|
|
783
|
+
const results = events.flatMap((e) => (e.type === "tool_end" ? [e.result] : []));
|
|
784
|
+
return { results, result };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
test("queues the note into the first tool result at the threshold, then never again", async () => {
|
|
788
|
+
const { results, result } = await toolResults("steer-twice", {
|
|
789
|
+
CONTEXT_BOUND_NOTE_TOKENS: "1204",
|
|
790
|
+
});
|
|
791
|
+
expect(results).toEqual([`2\n\n${await renderContextBoundNote(1204)}`, "2"]);
|
|
792
|
+
// Carried inside the turn: no extra turn replays it.
|
|
793
|
+
expect(result).toMatchObject({ isError: false, output: "DONE1" });
|
|
794
|
+
expect(result.cost?.numTurns).toBe(1);
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
test("a pinned model gets the window share; a mode names no model and gets none", async () => {
|
|
798
|
+
const pin = "anthropic/claude-sonnet-4-6";
|
|
799
|
+
const pinned = await toolResults(
|
|
800
|
+
"steer-twice",
|
|
801
|
+
{ CONTEXT_BOUND_NOTE_TOKENS: "1204" },
|
|
802
|
+
{ model: pin },
|
|
803
|
+
);
|
|
804
|
+
const note = await renderContextBoundNote(1204, pin);
|
|
805
|
+
expect(note).toContain("of the 1,000,000-token window)");
|
|
806
|
+
expect(pinned.results).toEqual([`2\n\n${note}`, "2"]);
|
|
807
|
+
const mode = await toolResults("steer-twice", { CONTEXT_BOUND_NOTE_TOKENS: "1204" });
|
|
808
|
+
expect(mode.results[0]).not.toContain("token window");
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
test("no-op below the threshold and when disabled at 0", async () => {
|
|
812
|
+
for (const threshold of ["1205", "0"]) {
|
|
813
|
+
const { results } = await toolResults("steer-twice", {
|
|
814
|
+
CONTEXT_BOUND_NOTE_TOKENS: threshold,
|
|
815
|
+
});
|
|
816
|
+
expect(results).toEqual(["2", "2"]);
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test("fails open on missing or corrupt request usage", async () => {
|
|
821
|
+
for (const usage of ["missing", "corrupt"]) {
|
|
822
|
+
const { results, result } = await toolResults("steer", {
|
|
823
|
+
CONTEXT_BOUND_NOTE_TOKENS: "1",
|
|
824
|
+
AMP_TEST_REQUEST_USAGE: usage,
|
|
825
|
+
});
|
|
826
|
+
expect(results).toEqual(["2"]);
|
|
827
|
+
expect(result).toMatchObject({ isError: false, output: "DONE1" });
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
test("a final reply with no tool call never queues it, so no new turn starts", async () => {
|
|
832
|
+
const { results, result } = await toolResults("steer-no-tool", {
|
|
833
|
+
CONTEXT_BOUND_NOTE_TOKENS: "1",
|
|
834
|
+
});
|
|
835
|
+
expect(results).toEqual([]);
|
|
836
|
+
expect(result).toMatchObject({ isError: false, output: "DONE1" });
|
|
837
|
+
expect(result.cost?.numTurns).toBe(1);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
test("a note rendered after the last tool result is dropped, not replayed as a new turn", async () => {
|
|
841
|
+
const held = holdTemplateRender();
|
|
842
|
+
try {
|
|
843
|
+
const { config } = await fixture(
|
|
844
|
+
"steer",
|
|
845
|
+
{},
|
|
846
|
+
{
|
|
847
|
+
CONTEXT_BOUND_NOTE_TOKENS: "1204",
|
|
848
|
+
AMP_TEST_POST_RESULT_DELAY_MS: "150",
|
|
849
|
+
},
|
|
850
|
+
);
|
|
851
|
+
const session = await new AmpAdapter().createSession(config);
|
|
852
|
+
const results: unknown[] = [];
|
|
853
|
+
session.onEvent((event) => {
|
|
854
|
+
if (event.type !== "tool_end") return;
|
|
855
|
+
results.push(event.result);
|
|
856
|
+
// Ready while the final reply is pending.
|
|
857
|
+
held.release();
|
|
858
|
+
});
|
|
859
|
+
const result = await session.waitForCompletion();
|
|
860
|
+
expect(results).toEqual(["2"]);
|
|
861
|
+
expect(result).toMatchObject({ isError: false, output: "DONE1" });
|
|
862
|
+
expect(result.cost?.numTurns).toBe(1);
|
|
863
|
+
} finally {
|
|
864
|
+
held.stop();
|
|
865
|
+
}
|
|
866
|
+
});
|
|
867
|
+
});
|
|
868
|
+
|
|
772
869
|
describe("amp live test", () => {
|
|
773
870
|
const env = async (mode: string) => {
|
|
774
871
|
const { config } = await fixture(mode);
|
|
@@ -418,7 +418,7 @@ describe("Approval Requests", () => {
|
|
|
418
418
|
expect(result).toBeNull();
|
|
419
419
|
});
|
|
420
420
|
|
|
421
|
-
test("does not resolve a workflow approval after its run
|
|
421
|
+
test("does not resolve a workflow approval after its run is cancelled", async () => {
|
|
422
422
|
const workflow = await createWorkflow({
|
|
423
423
|
name: `approval-cancel-${crypto.randomUUID()}`,
|
|
424
424
|
definition: { nodes: [] },
|
|
@@ -432,11 +432,12 @@ describe("Approval Requests", () => {
|
|
|
432
432
|
nodeId: "approval",
|
|
433
433
|
nodeType: "human-in-the-loop",
|
|
434
434
|
});
|
|
435
|
-
|
|
436
|
-
|
|
435
|
+
// Asked while the run was live; the run was cancelled after, leaving
|
|
436
|
+
// the request pending (a cancelled step's trigger would close it).
|
|
437
437
|
const approval = await createApprovalRequest(
|
|
438
438
|
makeApprovalData({ workflowRunId: runId, workflowRunStepId: stepId }),
|
|
439
439
|
);
|
|
440
|
+
await updateWorkflowRun(runId, { status: "cancelled" });
|
|
440
441
|
|
|
441
442
|
const result = await resolveApprovalRequest(
|
|
442
443
|
approval.id,
|
|
@@ -1864,15 +1865,17 @@ describe("Approval Requests", () => {
|
|
|
1864
1865
|
|
|
1865
1866
|
test("cancel on a waiting run cancels the run, its step, and its task", async () => {
|
|
1866
1867
|
const { runId, stepId, approvalId } = await makeRun("waiting");
|
|
1868
|
+
// Linked directly: the engine binds a step's task while the step is
|
|
1869
|
+
// still running, and this step is already parked.
|
|
1867
1870
|
const task = await createTaskExtended("step task", {
|
|
1868
1871
|
agentId: owner.id,
|
|
1869
1872
|
source: "mcp",
|
|
1870
1873
|
workflowRunId: runId,
|
|
1871
|
-
workflowRunStepId: stepId,
|
|
1872
1874
|
});
|
|
1873
|
-
await getDbClient().run(
|
|
1874
|
-
|
|
1875
|
-
|
|
1875
|
+
await getDbClient().run(
|
|
1876
|
+
"UPDATE agent_tasks SET status = 'in_progress', workflowRunStepId = ? WHERE id = ?",
|
|
1877
|
+
[stepId, task.id],
|
|
1878
|
+
);
|
|
1876
1879
|
|
|
1877
1880
|
const res = await post(
|
|
1878
1881
|
`/api/approval-requests/${approvalId}/cancel`,
|
|
@@ -223,15 +223,17 @@ describe("approval sweeps", () => {
|
|
|
223
223
|
|
|
224
224
|
test("cancels the waiting run, its step, and its linked task", async () => {
|
|
225
225
|
const { runId, stepId } = await makeRun("waiting");
|
|
226
|
+
// Linked directly: the engine binds a step's task while the step is
|
|
227
|
+
// still running, and this step is already parked.
|
|
226
228
|
const task = await createTaskExtended("step task", {
|
|
227
229
|
agentId,
|
|
228
230
|
source: "mcp",
|
|
229
231
|
workflowRunId: runId,
|
|
230
|
-
workflowRunStepId: stepId,
|
|
231
232
|
});
|
|
232
|
-
await getDbClient().run(
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
await getDbClient().run(
|
|
234
|
+
"UPDATE agent_tasks SET status = 'in_progress', workflowRunStepId = ? WHERE id = ?",
|
|
235
|
+
[stepId, task.id],
|
|
236
|
+
);
|
|
235
237
|
const id = await makeRequest({ ageDays: 8, workflowRunId: runId, workflowRunStepId: stepId });
|
|
236
238
|
|
|
237
239
|
const result = await autoCancelStaleApprovalRequests();
|
|
@@ -300,8 +302,10 @@ describe("approval sweeps", () => {
|
|
|
300
302
|
});
|
|
301
303
|
|
|
302
304
|
test("leaves a failed run as it is", async () => {
|
|
303
|
-
|
|
305
|
+
// The request was asked while the run was live; the run failed after.
|
|
306
|
+
const { runId, stepId } = await makeRun("waiting");
|
|
304
307
|
const id = await makeRequest({ ageDays: 8, workflowRunId: runId, workflowRunStepId: stepId });
|
|
308
|
+
await updateWorkflowRun(runId, { status: "failed" });
|
|
305
309
|
|
|
306
310
|
const result = await autoCancelStaleApprovalRequests();
|
|
307
311
|
|
|
@@ -415,13 +419,15 @@ describe("approval sweeps", () => {
|
|
|
415
419
|
});
|
|
416
420
|
|
|
417
421
|
test("times out a request on a completed run", async () => {
|
|
418
|
-
|
|
422
|
+
// The request was asked while the run was live; the run completed after.
|
|
423
|
+
const { runId, stepId } = await makeRun("waiting");
|
|
419
424
|
const id = await makeRequest({
|
|
420
425
|
ageDays: 0,
|
|
421
426
|
expiresInMs: -60_000,
|
|
422
427
|
workflowRunId: runId,
|
|
423
428
|
workflowRunStepId: stepId,
|
|
424
429
|
});
|
|
430
|
+
await updateWorkflowRun(runId, { status: "completed" });
|
|
425
431
|
await timeoutExpiredApprovalRequests();
|
|
426
432
|
expect((await getApprovalRequestById(id))!.status).toBe("timeout");
|
|
427
433
|
});
|
|
@@ -490,7 +490,7 @@ describe("getBasePrompt: outputs section", () => {
|
|
|
490
490
|
expect(result).toContain("## Outputs");
|
|
491
491
|
expect(result).toContain(AGENT_FS_LINE);
|
|
492
492
|
expect(result).toContain(
|
|
493
|
-
|
|
493
|
+
`Give people the live app link \`\${AGENT_FS_LIVE_URL}/file/~/<org>/<drive>/<path>\`. Make an \`agent-fs share-create\` link only when the requester asks for one or the reader has no agent-fs login.`,
|
|
494
494
|
);
|
|
495
495
|
expect(result).not.toContain(NO_AGENT_FS_LINE);
|
|
496
496
|
});
|
|
@@ -82,7 +82,11 @@ afterAll(async () => {
|
|
|
82
82
|
await rm(projectDir, { recursive: true, force: true });
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
-
async function runHook(
|
|
85
|
+
async function runHook(
|
|
86
|
+
event: Record<string, unknown>,
|
|
87
|
+
taskId: string,
|
|
88
|
+
extraEnv: Record<string, string> = {},
|
|
89
|
+
) {
|
|
86
90
|
const taskFile = join(projectDir, `task-${taskId}.json`);
|
|
87
91
|
await Bun.write(
|
|
88
92
|
taskFile,
|
|
@@ -92,6 +96,8 @@ async function runHook(event: Record<string, unknown>, taskId: string) {
|
|
|
92
96
|
...process.env,
|
|
93
97
|
CLAUDE_PROJECT_DIR: projectDir,
|
|
94
98
|
TASK_FILE: taskFile,
|
|
99
|
+
CONTEXT_BOUND_NOTE_TOKENS: undefined,
|
|
100
|
+
...extraEnv,
|
|
95
101
|
};
|
|
96
102
|
return runChild(["bun", HOOK_PATH], {
|
|
97
103
|
cwd: projectDir,
|
|
@@ -107,6 +113,9 @@ function parseSoleJson(stdout: string): Record<string, unknown> {
|
|
|
107
113
|
return JSON.parse(trimmed) as Record<string, unknown>;
|
|
108
114
|
}
|
|
109
115
|
|
|
116
|
+
/** The note is off unless CONTEXT_BOUND_NOTE_TOKENS is set. */
|
|
117
|
+
const noteOn = { CONTEXT_BOUND_NOTE_TOKENS: "200000" };
|
|
118
|
+
|
|
110
119
|
const bashTouch = {
|
|
111
120
|
tool_name: "Bash",
|
|
112
121
|
tool_input: { command: "touch /tmp/probe" },
|
|
@@ -230,4 +239,73 @@ describe("claude hook stdout", () => {
|
|
|
230
239
|
},
|
|
231
240
|
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
232
241
|
);
|
|
242
|
+
|
|
243
|
+
test(
|
|
244
|
+
"worker context-bound note fires once as PostToolUse additionalContext",
|
|
245
|
+
async () => {
|
|
246
|
+
Object.assign(state, { isLead: false, cancelledTaskId: null, shouldBlockPolling: false });
|
|
247
|
+
const taskId = crypto.randomUUID();
|
|
248
|
+
await Bun.write(
|
|
249
|
+
join(projectDir, `task-${taskId}.json.context.json`),
|
|
250
|
+
JSON.stringify({ contextTokens: 250_000, model: "claude-opus-5-5" }),
|
|
251
|
+
);
|
|
252
|
+
const event = { hook_event_name: "PostToolUse", ...bashTouch, tool_response: {} };
|
|
253
|
+
|
|
254
|
+
const first = await runHook(event, taskId, noteOn);
|
|
255
|
+
const out = parseSoleJson(first.stdout) as { hookSpecificOutput: Record<string, string> };
|
|
256
|
+
expect(out.hookSpecificOutput.hookEventName).toBe("PostToolUse");
|
|
257
|
+
expect(out.hookSpecificOutput.additionalContext).toStartWith(
|
|
258
|
+
"Context is 250,000 tokens (25% of the 1,000,000-token window). Keep this task bounded",
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const second = await runHook(event, taskId, noteOn);
|
|
262
|
+
expect(second.exitCode).toBe(0);
|
|
263
|
+
expect(second.stdout.trim()).toBe("");
|
|
264
|
+
},
|
|
265
|
+
CHILD_PROCESS_TEST_BUDGET_MS * 2,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
test(
|
|
269
|
+
"a subagent tool call does not consume the context-bound note",
|
|
270
|
+
async () => {
|
|
271
|
+
Object.assign(state, { isLead: false, cancelledTaskId: null, shouldBlockPolling: false });
|
|
272
|
+
const taskId = crypto.randomUUID();
|
|
273
|
+
await Bun.write(
|
|
274
|
+
join(projectDir, `task-${taskId}.json.context.json`),
|
|
275
|
+
JSON.stringify({ contextTokens: 250_000, model: "made-up-model" }),
|
|
276
|
+
);
|
|
277
|
+
const event = { hook_event_name: "PostToolUse", ...bashTouch, tool_response: {} };
|
|
278
|
+
|
|
279
|
+
const child = await runHook({ ...event, agent_id: "child" }, taskId, noteOn);
|
|
280
|
+
expect(child.exitCode).toBe(0);
|
|
281
|
+
expect(child.stdout.trim()).toBe("");
|
|
282
|
+
|
|
283
|
+
// Unknown model: no window share rather than a percent of the 200k fallback.
|
|
284
|
+
const main = await runHook(event, taskId, noteOn);
|
|
285
|
+
const out = parseSoleJson(main.stdout) as { hookSpecificOutput: Record<string, string> };
|
|
286
|
+
expect(out.hookSpecificOutput.additionalContext).toStartWith(
|
|
287
|
+
"Context is 250,000 tokens. Keep this task bounded",
|
|
288
|
+
);
|
|
289
|
+
},
|
|
290
|
+
CHILD_PROCESS_TEST_BUDGET_MS * 2,
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
test(
|
|
294
|
+
"the context-bound note is off when CONTEXT_BOUND_NOTE_TOKENS is unset",
|
|
295
|
+
async () => {
|
|
296
|
+
Object.assign(state, { isLead: false, cancelledTaskId: null, shouldBlockPolling: false });
|
|
297
|
+
const taskId = crypto.randomUUID();
|
|
298
|
+
await Bun.write(
|
|
299
|
+
join(projectDir, `task-${taskId}.json.context.json`),
|
|
300
|
+
JSON.stringify({ contextTokens: 900_000, model: "claude-opus-5-5" }),
|
|
301
|
+
);
|
|
302
|
+
const result = await runHook(
|
|
303
|
+
{ hook_event_name: "PostToolUse", ...bashTouch, tool_response: {} },
|
|
304
|
+
taskId,
|
|
305
|
+
);
|
|
306
|
+
expect(result.exitCode).toBe(0);
|
|
307
|
+
expect(result.stdout.trim()).toBe("");
|
|
308
|
+
},
|
|
309
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
310
|
+
);
|
|
233
311
|
});
|