@xemahq/opencode-xema-plugin 0.1.13 → 0.2.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/README.md +33 -5
- package/dist/.source-stamp.json +2 -2
- package/dist/hooks/subagent-fanout-ceiling.d.ts +39 -0
- package/dist/hooks/subagent-fanout-ceiling.d.ts.map +1 -0
- package/dist/hooks/subagent-fanout-ceiling.js +188 -0
- package/dist/hooks/subagent-fanout-ceiling.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -2
- package/dist/index.js.map +1 -1
- package/dist/opencode-types.d.ts +14 -0
- package/dist/opencode-types.d.ts.map +1 -1
- package/dist/opencode-types.js.map +1 -1
- package/package.json +3 -3
- package/plugin.mjs +224 -6
package/README.md
CHANGED
|
@@ -28,16 +28,44 @@
|
|
|
28
28
|
|
|
29
29
|
## Overview
|
|
30
30
|
|
|
31
|
-
An Opencode plugin that registers Xema-namespaced runtime tools
|
|
32
|
-
system-prompt overlay hook
|
|
33
|
-
registers its tools unconditionally and
|
|
34
|
-
tool-execution time, where each tool reads the
|
|
35
|
-
worker filesystem and rejects calls the invoking
|
|
31
|
+
An Opencode plugin that registers Xema-namespaced runtime tools, a dynamic
|
|
32
|
+
system-prompt overlay hook, and the sub-agent fan-out ceiling. The plugin is
|
|
33
|
+
evaluated once at agent startup; it registers its tools unconditionally and
|
|
34
|
+
defers per-role authorization to tool-execution time, where each tool reads the
|
|
35
|
+
invocation context from the worker filesystem and rejects calls the invoking
|
|
36
|
+
role is not permitted to make.
|
|
36
37
|
|
|
37
38
|
## When to use it
|
|
38
39
|
|
|
39
40
|
- Use it to add the Xema runtime tools and prompt overlay to an Opencode agent
|
|
40
41
|
runtime.
|
|
42
|
+
- Use it to bound how many sub-agents one worker may run at once.
|
|
43
|
+
|
|
44
|
+
## The sub-agent fan-out ceiling
|
|
45
|
+
|
|
46
|
+
Opencode bounds how DEEP delegation may nest (`subagent_depth`). Nothing bounds
|
|
47
|
+
how WIDE a turn may go, so at the platform's declared depth an unbounded
|
|
48
|
+
branching factor is an unbounded process — every `task` call is forked with no
|
|
49
|
+
pool, no semaphore and no queue.
|
|
50
|
+
|
|
51
|
+
This plugin adds the missing pool cap. It is BACKPRESSURE, not a refusal: when
|
|
52
|
+
the worker is already running its ceiling of sub-agents, the next `task` call
|
|
53
|
+
WAITS for a slot instead of being denied. An agent that cannot spawn a helper
|
|
54
|
+
would produce a wrong answer; one that waits produces a slower right one.
|
|
55
|
+
|
|
56
|
+
Configuration exists to TIGHTEN, never to enable:
|
|
57
|
+
|
|
58
|
+
| | |
|
|
59
|
+
|---|---|
|
|
60
|
+
| Default | 8 concurrent `task` calls per worker |
|
|
61
|
+
| Tighten with | `XEMA_SUBAGENT_FANOUT_LIMIT` (1..8) |
|
|
62
|
+
| Unset or empty | the platform default — never zero, never "off" |
|
|
63
|
+
| Above the default | refused at boot, with the reason |
|
|
64
|
+
|
|
65
|
+
Every degradation is logged: a task that waits says so and for how long, and
|
|
66
|
+
both leak backstops (a `session.idle` sweep and a per-permit TTL) name what they
|
|
67
|
+
reclaimed. A ceiling that silently stops binding is indistinguishable from
|
|
68
|
+
working software until somebody asks.
|
|
41
69
|
|
|
42
70
|
## Installation
|
|
43
71
|
|
package/dist/.source-stamp.json
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare const TASK_TOOL_ID = "task";
|
|
2
|
+
export declare const DEFAULT_SUBAGENT_FANOUT_LIMIT = 8;
|
|
3
|
+
export declare const DEFAULT_FANOUT_WAIT_TIMEOUT_MS: number;
|
|
4
|
+
export declare const DEFAULT_PERMIT_TTL_MS: number;
|
|
5
|
+
export declare const FANOUT_LIMIT_ENV = "XEMA_SUBAGENT_FANOUT_LIMIT";
|
|
6
|
+
export declare function resolveSubagentFanoutLimit(env?: Readonly<Record<string, string | undefined>>): number;
|
|
7
|
+
export interface FanoutCeilingStats {
|
|
8
|
+
readonly limit: number;
|
|
9
|
+
readonly inFlight: number;
|
|
10
|
+
readonly waiting: number;
|
|
11
|
+
readonly waitedCount: number;
|
|
12
|
+
readonly timedOutCount: number;
|
|
13
|
+
readonly sweptCount: number;
|
|
14
|
+
readonly ttlReclaimedCount: number;
|
|
15
|
+
readonly armedTimers: number;
|
|
16
|
+
}
|
|
17
|
+
export interface SubagentFanoutCeiling {
|
|
18
|
+
readonly before: (input: {
|
|
19
|
+
tool: string;
|
|
20
|
+
sessionID: string;
|
|
21
|
+
callID: string;
|
|
22
|
+
}) => Promise<void>;
|
|
23
|
+
readonly after: (input: {
|
|
24
|
+
tool: string;
|
|
25
|
+
sessionID: string;
|
|
26
|
+
callID: string;
|
|
27
|
+
}) => void;
|
|
28
|
+
readonly onSessionIdle: (sessionId: string) => void;
|
|
29
|
+
readonly dispose: () => void;
|
|
30
|
+
readonly stats: () => FanoutCeilingStats;
|
|
31
|
+
}
|
|
32
|
+
export interface FanoutCeilingOptions {
|
|
33
|
+
readonly limit: number;
|
|
34
|
+
readonly waitTimeoutMs?: number;
|
|
35
|
+
readonly permitTtlMs?: number;
|
|
36
|
+
readonly now?: () => number;
|
|
37
|
+
}
|
|
38
|
+
export declare function createSubagentFanoutCeiling(options: FanoutCeilingOptions): SubagentFanoutCeiling;
|
|
39
|
+
//# sourceMappingURL=subagent-fanout-ceiling.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subagent-fanout-ceiling.d.ts","sourceRoot":"","sources":["../../src/hooks/subagent-fanout-ceiling.ts"],"names":[],"mappings":"AA0FA,eAAO,MAAM,YAAY,SAAS,CAAC;AA6CnC,eAAO,MAAM,6BAA6B,IAAI,CAAC;AAW/C,eAAO,MAAM,8BAA8B,QAAiB,CAAC;AAU7D,eAAO,MAAM,qBAAqB,QAAiB,CAAC;AAGpD,eAAO,MAAM,gBAAgB,+BAA+B,CAAC;AAyB7D,wBAAgB,0BAA0B,CACxC,GAAG,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAe,GAC9D,MAAM,CA8BR;AAoBD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAEzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAE/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IAiBnC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,qBAAqB;IAEpC,QAAQ,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE;QACvB,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAEpB,QAAQ,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE;QACtB,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,IAAI,CAAC;IAEX,QAAQ,CAAC,aAAa,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAgBpD,QAAQ,CAAC,OAAO,EAAE,MAAM,IAAI,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,MAAM,kBAAkB,CAAC;CAC1C;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAE9B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC7B;AAOD,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,oBAAoB,GAC5B,qBAAqB,CAgPvB"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { info, warn, debug } from '../logger.js';
|
|
2
|
+
export const TASK_TOOL_ID = 'task';
|
|
3
|
+
export const DEFAULT_SUBAGENT_FANOUT_LIMIT = 8;
|
|
4
|
+
export const DEFAULT_FANOUT_WAIT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
5
|
+
export const DEFAULT_PERMIT_TTL_MS = 30 * 60 * 1000;
|
|
6
|
+
export const FANOUT_LIMIT_ENV = 'XEMA_SUBAGENT_FANOUT_LIMIT';
|
|
7
|
+
export function resolveSubagentFanoutLimit(env = process.env) {
|
|
8
|
+
const declared = env[FANOUT_LIMIT_ENV];
|
|
9
|
+
if (declared === undefined || declared.trim() === '') {
|
|
10
|
+
return DEFAULT_SUBAGENT_FANOUT_LIMIT;
|
|
11
|
+
}
|
|
12
|
+
const parsed = Number(declared.trim());
|
|
13
|
+
if (!Number.isInteger(parsed)) {
|
|
14
|
+
throw new Error(`${FANOUT_LIMIT_ENV}="${declared}" is not an integer. Expected a whole number of ` +
|
|
15
|
+
`concurrently executing sub-agent tasks between 1 and ${DEFAULT_SUBAGENT_FANOUT_LIMIT}.`);
|
|
16
|
+
}
|
|
17
|
+
if (parsed < 1) {
|
|
18
|
+
throw new Error(`${FANOUT_LIMIT_ENV}="${declared}" is below 1. A ceiling of 0 would park EVERY delegation ` +
|
|
19
|
+
`until it timed out, turning a resource bound into an outage. Remove the variable to ` +
|
|
20
|
+
`accept the platform default (${DEFAULT_SUBAGENT_FANOUT_LIMIT}).`);
|
|
21
|
+
}
|
|
22
|
+
if (parsed > DEFAULT_SUBAGENT_FANOUT_LIMIT) {
|
|
23
|
+
throw new Error(`${FANOUT_LIMIT_ENV}="${declared}" is above the platform default ` +
|
|
24
|
+
`(${DEFAULT_SUBAGENT_FANOUT_LIMIT}). Configuration may only TIGHTEN this ceiling. The ` +
|
|
25
|
+
`default is a stated platform decision; a wider one moves ` +
|
|
26
|
+
`DEFAULT_SUBAGENT_FANOUT_LIMIT, where the grounds are recorded.`);
|
|
27
|
+
}
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
export function createSubagentFanoutCeiling(options) {
|
|
31
|
+
const limit = options.limit;
|
|
32
|
+
const waitTimeoutMs = options.waitTimeoutMs ?? DEFAULT_FANOUT_WAIT_TIMEOUT_MS;
|
|
33
|
+
const permitTtlMs = options.permitTtlMs ?? DEFAULT_PERMIT_TTL_MS;
|
|
34
|
+
const now = options.now ?? (() => Date.now());
|
|
35
|
+
const held = new Map();
|
|
36
|
+
const waiters = [];
|
|
37
|
+
let waitedCount = 0;
|
|
38
|
+
let timedOutCount = 0;
|
|
39
|
+
let sweptCount = 0;
|
|
40
|
+
let ttlReclaimedCount = 0;
|
|
41
|
+
const armed = new Set();
|
|
42
|
+
function arm(fn, ms, keepProcessAlive) {
|
|
43
|
+
const timer = setTimeout(() => {
|
|
44
|
+
armed.delete(timer);
|
|
45
|
+
fn();
|
|
46
|
+
}, ms);
|
|
47
|
+
if (!keepProcessAlive)
|
|
48
|
+
timer.unref?.();
|
|
49
|
+
armed.add(timer);
|
|
50
|
+
return timer;
|
|
51
|
+
}
|
|
52
|
+
function disarm(timer) {
|
|
53
|
+
if (timer === undefined)
|
|
54
|
+
return;
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
armed.delete(timer);
|
|
57
|
+
}
|
|
58
|
+
function take(callId, sessionId) {
|
|
59
|
+
const ttlTimer = arm(() => {
|
|
60
|
+
if (!held.has(callId))
|
|
61
|
+
return;
|
|
62
|
+
ttlReclaimedCount += 1;
|
|
63
|
+
warn(`fanout-ceiling: permit TTL expired for call=${callId} session=${sessionId} after ` +
|
|
64
|
+
`${permitTtlMs}ms — force-releasing. A task held a slot longer than any healthy ` +
|
|
65
|
+
`sub-agent turn; the pool would otherwise have shrunk permanently.`);
|
|
66
|
+
release(callId, 'ttl');
|
|
67
|
+
}, permitTtlMs, false);
|
|
68
|
+
held.set(callId, { sessionId, acquiredAt: now(), ttlTimer });
|
|
69
|
+
}
|
|
70
|
+
function release(callId, reason) {
|
|
71
|
+
const permit = held.get(callId);
|
|
72
|
+
if (permit === undefined)
|
|
73
|
+
return;
|
|
74
|
+
disarm(permit.ttlTimer);
|
|
75
|
+
held.delete(callId);
|
|
76
|
+
if (reason !== 'completed') {
|
|
77
|
+
debug(`fanout-ceiling: reclaimed call=${callId} via ${reason} ` +
|
|
78
|
+
`(held ${now() - permit.acquiredAt}ms)`);
|
|
79
|
+
}
|
|
80
|
+
drain();
|
|
81
|
+
}
|
|
82
|
+
function drain() {
|
|
83
|
+
while (waiters.length > 0 && held.size < limit) {
|
|
84
|
+
const next = waiters.shift();
|
|
85
|
+
if (next === undefined)
|
|
86
|
+
return;
|
|
87
|
+
disarm(next.timer);
|
|
88
|
+
const waitedMs = now() - next.since;
|
|
89
|
+
waitedCount += 1;
|
|
90
|
+
take(next.callId, next.sessionId);
|
|
91
|
+
info(`fanout-ceiling: admitted call=${next.callId} session=${next.sessionId} after waiting ` +
|
|
92
|
+
`${waitedMs}ms (limit=${limit}, in-flight=${held.size}, still waiting=${waiters.length})`);
|
|
93
|
+
next.grant();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async function before(input) {
|
|
97
|
+
if (input.tool !== TASK_TOOL_ID)
|
|
98
|
+
return;
|
|
99
|
+
const { callID: callId, sessionID: sessionId } = input;
|
|
100
|
+
if (held.size < limit) {
|
|
101
|
+
take(callId, sessionId);
|
|
102
|
+
debug(`fanout-ceiling: admitted call=${callId} immediately ` +
|
|
103
|
+
`(in-flight=${held.size}/${limit})`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const since = now();
|
|
107
|
+
warn(`fanout-ceiling: sub-agent fan-out at ceiling — call=${callId} session=${sessionId} is ` +
|
|
108
|
+
`WAITING (limit=${limit}, in-flight=${held.size}, already waiting=${waiters.length}). ` +
|
|
109
|
+
`This is backpressure, not a refusal: the task will start when a slot frees.`);
|
|
110
|
+
await new Promise((resolve) => {
|
|
111
|
+
const waiter = {
|
|
112
|
+
callId,
|
|
113
|
+
sessionId,
|
|
114
|
+
since,
|
|
115
|
+
grant: resolve,
|
|
116
|
+
timer: undefined,
|
|
117
|
+
};
|
|
118
|
+
waiter.timer = arm(() => {
|
|
119
|
+
const idx = waiters.indexOf(waiter);
|
|
120
|
+
if (idx === -1)
|
|
121
|
+
return;
|
|
122
|
+
waiters.splice(idx, 1);
|
|
123
|
+
timedOutCount += 1;
|
|
124
|
+
warn(`fanout-ceiling: call=${callId} session=${sessionId} waited ${waitTimeoutMs}ms and is ` +
|
|
125
|
+
`being ADMITTED OVER the ceiling (limit=${limit}, in-flight=${held.size}). The pool ` +
|
|
126
|
+
`did not free a slot in that window — suspect a leaked permit or a stuck sub-agent.`);
|
|
127
|
+
take(callId, sessionId);
|
|
128
|
+
resolve();
|
|
129
|
+
}, waitTimeoutMs, true);
|
|
130
|
+
waiters.push(waiter);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function after(input) {
|
|
134
|
+
if (input.tool !== TASK_TOOL_ID)
|
|
135
|
+
return;
|
|
136
|
+
release(input.callID, 'completed');
|
|
137
|
+
}
|
|
138
|
+
function onSessionIdle(sessionId) {
|
|
139
|
+
const stale = [];
|
|
140
|
+
for (const [callId, permit] of held) {
|
|
141
|
+
if (permit.sessionId === sessionId)
|
|
142
|
+
stale.push(callId);
|
|
143
|
+
}
|
|
144
|
+
if (stale.length === 0)
|
|
145
|
+
return;
|
|
146
|
+
sweptCount += stale.length;
|
|
147
|
+
warn(`fanout-ceiling: session=${sessionId} went idle still holding ${stale.length} permit(s) ` +
|
|
148
|
+
`[${stale.join(', ')}] — reclaiming. These tasks ended without a tool.execute.after, ` +
|
|
149
|
+
`which upstream does not fire on the failure path.`);
|
|
150
|
+
for (const callId of stale)
|
|
151
|
+
release(callId, 'session-idle');
|
|
152
|
+
}
|
|
153
|
+
function dispose() {
|
|
154
|
+
const parked = waiters.length;
|
|
155
|
+
const outstanding = held.size;
|
|
156
|
+
while (waiters.length > 0) {
|
|
157
|
+
const w = waiters.shift();
|
|
158
|
+
if (w === undefined)
|
|
159
|
+
break;
|
|
160
|
+
disarm(w.timer);
|
|
161
|
+
w.grant();
|
|
162
|
+
}
|
|
163
|
+
for (const permit of held.values())
|
|
164
|
+
disarm(permit.ttlTimer);
|
|
165
|
+
held.clear();
|
|
166
|
+
if (parked > 0 || outstanding > 0) {
|
|
167
|
+
info(`fanout-ceiling: disposed — released ${parked} parked task(s) and disarmed ` +
|
|
168
|
+
`${outstanding} permit timer(s).`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
before,
|
|
173
|
+
after,
|
|
174
|
+
onSessionIdle,
|
|
175
|
+
dispose,
|
|
176
|
+
stats: () => ({
|
|
177
|
+
limit,
|
|
178
|
+
inFlight: held.size,
|
|
179
|
+
waiting: waiters.length,
|
|
180
|
+
waitedCount,
|
|
181
|
+
timedOutCount,
|
|
182
|
+
sweptCount,
|
|
183
|
+
ttlReclaimedCount,
|
|
184
|
+
armedTimers: armed.size,
|
|
185
|
+
}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=subagent-fanout-ceiling.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subagent-fanout-ceiling.js","sourceRoot":"","sources":["../../src/hooks/subagent-fanout-ceiling.ts"],"names":[],"mappings":"AAuFA,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,cAAc,CAAC;AAGjD,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC;AA6CnC,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,CAAC;AAW/C,MAAM,CAAC,MAAM,8BAA8B,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAU7D,MAAM,CAAC,MAAM,qBAAqB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAGpD,MAAM,CAAC,MAAM,gBAAgB,GAAG,4BAA4B,CAAC;AAyB7D,MAAM,UAAU,0BAA0B,CACxC,MAAoD,OAAO,CAAC,GAAG;IAE/D,MAAM,QAAQ,GAAG,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACvC,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,OAAO,6BAA6B,CAAC;IACvC,CAAC;IAGD,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACvC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,GAAG,gBAAgB,KAAK,QAAQ,kDAAkD;YAChF,wDAAwD,6BAA6B,GAAG,CAC3F,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,GAAG,CAAC,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,GAAG,gBAAgB,KAAK,QAAQ,2DAA2D;YACzF,sFAAsF;YACtF,gCAAgC,6BAA6B,IAAI,CACpE,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,GAAG,6BAA6B,EAAE,CAAC;QAC3C,MAAM,IAAI,KAAK,CACb,GAAG,gBAAgB,KAAK,QAAQ,kCAAkC;YAChE,IAAI,6BAA6B,sDAAsD;YACvF,2DAA2D;YAC3D,gEAAgE,CACnE,CAAC;IACJ,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAiGD,MAAM,UAAU,2BAA2B,CACzC,OAA6B;IAE7B,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;IAC5B,MAAM,aAAa,GAAG,OAAO,CAAC,aAAa,IAAI,8BAA8B,CAAC;IAC9E,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,qBAAqB,CAAC;IACjE,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAE9C,MAAM,IAAI,GAAG,IAAI,GAAG,EAAsB,CAAC;IAC3C,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,IAAI,aAAa,GAAG,CAAC,CAAC;IACtB,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,IAAI,iBAAiB,GAAG,CAAC,CAAC;IAc1B,MAAM,KAAK,GAAG,IAAI,GAAG,EAAiC,CAAC;IAUvD,SAAS,GAAG,CACV,EAAc,EACd,EAAU,EACV,gBAAyB;QAEzB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACpB,EAAE,EAAE,CAAC;QACP,CAAC,EAAE,EAAE,CAAC,CAAC;QACP,IAAI,CAAC,gBAAgB;YAAE,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;QACvC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACjB,OAAO,KAAK,CAAC;IACf,CAAC;IAED,SAAS,MAAM,CAAC,KAAgD;QAC9D,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAChC,YAAY,CAAC,KAAK,CAAC,CAAC;QACpB,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACtB,CAAC;IAED,SAAS,IAAI,CAAC,MAAc,EAAE,SAAiB;QAC7C,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,EAAE;YACxB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC;gBAAE,OAAO;YAC9B,iBAAiB,IAAI,CAAC,CAAC;YACvB,IAAI,CACF,+CAA+C,MAAM,YAAY,SAAS,SAAS;gBACjF,GAAG,WAAW,mEAAmE;gBACjF,mEAAmE,CACtE,CAAC;YACF,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QACzB,CAAC,EAAE,WAAW,EAAE,KAAK,CAAC,CAAC;QAOvB,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC/D,CAAC;IAED,SAAS,OAAO,CAAC,MAAc,EAAE,MAAqB;QACpD,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAChC,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO;QACjC,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QACxB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACpB,IAAI,MAAM,KAAK,WAAW,EAAE,CAAC;YAC3B,KAAK,CACH,kCAAkC,MAAM,QAAQ,MAAM,GAAG;gBACvD,SAAS,GAAG,EAAE,GAAG,MAAM,CAAC,UAAU,KAAK,CAC1C,CAAC;QACJ,CAAC;QACD,KAAK,EAAE,CAAC;IACV,CAAC;IAED,SAAS,KAAK;QACZ,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,IAAI,CAAC,IAAI,GAAG,KAAK,EAAE,CAAC;YAC/C,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;YAC7B,IAAI,IAAI,KAAK,SAAS;gBAAE,OAAO;YAC/B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACnB,MAAM,QAAQ,GAAG,GAAG,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC;YACpC,WAAW,IAAI,CAAC,CAAC;YACjB,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YAClC,IAAI,CACF,iCAAiC,IAAI,CAAC,MAAM,YAAY,IAAI,CAAC,SAAS,iBAAiB;gBACrF,GAAG,QAAQ,aAAa,KAAK,eAAe,IAAI,CAAC,IAAI,mBAAmB,OAAO,CAAC,MAAM,GAAG,CAC5F,CAAC;YACF,IAAI,CAAC,KAAK,EAAE,CAAC;QACf,CAAC;IACH,CAAC;IAED,KAAK,UAAU,MAAM,CAAC,KAIrB;QACC,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY;YAAE,OAAO;QACxC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,KAAK,CAAC;QAEvD,IAAI,IAAI,CAAC,IAAI,GAAG,KAAK,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;YACxB,KAAK,CACH,iCAAiC,MAAM,eAAe;gBACpD,cAAc,IAAI,CAAC,IAAI,IAAI,KAAK,GAAG,CACtC,CAAC;YACF,OAAO;QACT,CAAC;QAID,MAAM,KAAK,GAAG,GAAG,EAAE,CAAC;QACpB,IAAI,CACF,uDAAuD,MAAM,YAAY,SAAS,MAAM;YACtF,kBAAkB,KAAK,eAAe,IAAI,CAAC,IAAI,qBAAqB,OAAO,CAAC,MAAM,KAAK;YACvF,6EAA6E,CAChF,CAAC;QAEF,MAAM,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YAClC,MAAM,MAAM,GAAW;gBACrB,MAAM;gBACN,SAAS;gBACT,KAAK;gBACL,KAAK,EAAE,OAAO;gBACd,KAAK,EAAE,SAAS;aACjB,CAAC;YACF,MAAM,CAAC,KAAK,GAAG,GAAG,CAAC,GAAG,EAAE;gBACtB,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;gBACpC,IAAI,GAAG,KAAK,CAAC,CAAC;oBAAE,OAAO;gBACvB,OAAO,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;gBACvB,aAAa,IAAI,CAAC,CAAC;gBAKnB,IAAI,CACF,wBAAwB,MAAM,YAAY,SAAS,WAAW,aAAa,YAAY;oBACrF,0CAA0C,KAAK,eAAe,IAAI,CAAC,IAAI,cAAc;oBACrF,oFAAoF,CACvF,CAAC;gBACF,IAAI,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;gBACxB,OAAO,EAAE,CAAC;YACZ,CAAC,EAAE,aAAa,EAAE,IAAI,CAAC,CAAC;YAgBxB,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACvB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,SAAS,KAAK,CAAC,KAId;QACC,IAAI,KAAK,CAAC,IAAI,KAAK,YAAY;YAAE,OAAO;QACxC,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACrC,CAAC;IAED,SAAS,aAAa,CAAC,SAAiB;QAKtC,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACpC,IAAI,MAAM,CAAC,SAAS,KAAK,SAAS;gBAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QACzD,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC/B,UAAU,IAAI,KAAK,CAAC,MAAM,CAAC;QAC3B,IAAI,CACF,2BAA2B,SAAS,4BAA4B,KAAK,CAAC,MAAM,aAAa;YACvF,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,kEAAkE;YACtF,mDAAmD,CACtD,CAAC;QACF,KAAK,MAAM,MAAM,IAAI,KAAK;YAAE,OAAO,CAAC,MAAM,EAAE,cAAc,CAAC,CAAC;IAC9D,CAAC;IAED,SAAS,OAAO;QACd,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;QAC9B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC;QAE9B,OAAO,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC1B,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC;YAC1B,IAAI,CAAC,KAAK,SAAS;gBAAE,MAAM;YAC3B,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;YAChB,CAAC,CAAC,KAAK,EAAE,CAAC;QACZ,CAAC;QAED,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,MAAM,EAAE;YAAE,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;QAC5D,IAAI,CAAC,KAAK,EAAE,CAAC;QACb,IAAI,MAAM,GAAG,CAAC,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;YAClC,IAAI,CACF,uCAAuC,MAAM,+BAA+B;gBAC1E,GAAG,WAAW,mBAAmB,CACpC,CAAC;QACJ,CAAC;IACH,CAAC;IAED,OAAO;QACL,MAAM;QACN,KAAK;QACL,aAAa;QACb,OAAO;QACP,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC;YACZ,KAAK;YACL,QAAQ,EAAE,IAAI,CAAC,IAAI;YACnB,OAAO,EAAE,OAAO,CAAC,MAAM;YACvB,WAAW;YACX,aAAa;YACb,UAAU;YACV,iBAAiB;YACjB,WAAW,EAAE,KAAK,CAAC,IAAI;SACxB,CAAC;KACH,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Plugin } from './opencode-types.js';
|
|
|
2
2
|
export declare const XemaPlugin: Plugin;
|
|
3
3
|
export declare function __resetBootCacheForTests(): void;
|
|
4
4
|
export default XemaPlugin;
|
|
5
|
+
export { createSubagentFanoutCeiling, resolveSubagentFanoutLimit, DEFAULT_SUBAGENT_FANOUT_LIMIT, DEFAULT_FANOUT_WAIT_TIMEOUT_MS, DEFAULT_PERMIT_TTL_MS, FANOUT_LIMIT_ENV, TASK_TOOL_ID, type SubagentFanoutCeiling, type FanoutCeilingOptions, type FanoutCeilingStats, } from './hooks/subagent-fanout-ceiling.js';
|
|
5
6
|
export { readInvocationContext } from './context.js';
|
|
6
7
|
export type { InvocationContext, InvocationRole } from './context.js';
|
|
7
8
|
export * from './errors.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkEA,OAAO,KAAK,EAIV,MAAM,EAEP,MAAM,qBAAqB,CAAC;AAuB7B,eAAO,MAAM,UAAU,EAAE,MAgBxB,CAAC;AAgFF,wBAAgB,wBAAwB,IAAI,IAAI,CAE/C;AAyBD,eAAe,UAAU,CAAC;AAG1B,OAAO,EACL,2BAA2B,EAC3B,0BAA0B,EAC1B,6BAA6B,EAC7B,8BAA8B,EAC9B,qBAAqB,EACrB,gBAAgB,EAChB,YAAY,EACZ,KAAK,qBAAqB,EAC1B,KAAK,oBAAoB,EACzB,KAAK,kBAAkB,GACxB,MAAM,oCAAoC,CAAC;AAC5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,YAAY,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACtE,cAAc,aAAa,CAAC;AAC5B,OAAO,EACL,gBAAgB,EAChB,uBAAuB,EACvB,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,KAAK,YAAY,GAClB,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { buildSessionIdleAutocommit } from './hooks/session-idle-autocommit.js';
|
|
4
|
+
import { createSubagentFanoutCeiling, resolveSubagentFanoutLimit, } from './hooks/subagent-fanout-ceiling.js';
|
|
4
5
|
import { buildSystemPromptOverlay } from './hooks/system-prompt-overlay.js';
|
|
5
|
-
import { info, isDebug } from './logger.js';
|
|
6
|
+
import { info, isDebug, warn } from './logger.js';
|
|
6
7
|
import { buildXemaMemoryGetTool, buildXemaMemorySetTool, } from './tools/xema-memory.js';
|
|
7
8
|
const bootCache = new Map();
|
|
8
9
|
export const XemaPlugin = async (ctx) => {
|
|
@@ -29,10 +30,40 @@ async function doBoot(directory) {
|
|
|
29
30
|
info(`boot: directory=${directory} tools=${toolNames.length} [${toolNames.join(', ')}] ` +
|
|
30
31
|
`context.json=${contextPresentAtBoot ? 'present' : 'absent'} ` +
|
|
31
32
|
`debug=${isDebug() ? 'on' : 'off'}`);
|
|
33
|
+
const fanoutLimit = resolveSubagentFanoutLimit();
|
|
34
|
+
const fanout = createSubagentFanoutCeiling({ limit: fanoutLimit });
|
|
35
|
+
const autocommit = buildSessionIdleAutocommit(directory);
|
|
36
|
+
info(`boot: subagent fan-out ceiling active (limit=${fanoutLimit} concurrent task calls ` +
|
|
37
|
+
`per worker; tighten with XEMA_SUBAGENT_FANOUT_LIMIT)`);
|
|
32
38
|
const hooks = {
|
|
33
39
|
tool: tools,
|
|
34
40
|
'experimental.chat.system.transform': buildSystemPromptOverlay(directory),
|
|
35
|
-
|
|
41
|
+
'tool.execute.before': async (input) => {
|
|
42
|
+
await fanout.before(input);
|
|
43
|
+
},
|
|
44
|
+
'tool.execute.after': (input) => {
|
|
45
|
+
fanout.after(input);
|
|
46
|
+
},
|
|
47
|
+
dispose: async () => {
|
|
48
|
+
fanout.dispose();
|
|
49
|
+
},
|
|
50
|
+
event: async (input) => {
|
|
51
|
+
const evt = input.event;
|
|
52
|
+
if (evt.type === 'session.idle') {
|
|
53
|
+
const sessionId = evt.properties
|
|
54
|
+
?.sessionID;
|
|
55
|
+
if (sessionId) {
|
|
56
|
+
try {
|
|
57
|
+
fanout.onSessionIdle(sessionId);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
61
|
+
warn(`fanout-ceiling: session-idle sweep failed: ${message}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
await autocommit(input);
|
|
66
|
+
},
|
|
36
67
|
};
|
|
37
68
|
return hooks;
|
|
38
69
|
}
|
|
@@ -46,6 +77,7 @@ function buildAllTools(workspaceDir) {
|
|
|
46
77
|
};
|
|
47
78
|
}
|
|
48
79
|
export default XemaPlugin;
|
|
80
|
+
export { createSubagentFanoutCeiling, resolveSubagentFanoutLimit, DEFAULT_SUBAGENT_FANOUT_LIMIT, DEFAULT_FANOUT_WAIT_TIMEOUT_MS, DEFAULT_PERMIT_TTL_MS, FANOUT_LIMIT_ENV, TASK_TOOL_ID, } from './hooks/subagent-fanout-ceiling.js';
|
|
49
81
|
export { readInvocationContext } from './context.js';
|
|
50
82
|
export * from './errors.js';
|
|
51
83
|
export { loadSessionState, loadSessionStateForTool, ensureRoleAllowed, ContextMissingError, RoleNotAllowedError, } from './session-state.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/opencode-types.d.ts
CHANGED
|
@@ -37,8 +37,22 @@ export type OpencodeEvent = OpencodeSessionIdleEvent | {
|
|
|
37
37
|
type: string;
|
|
38
38
|
properties?: Record<string, unknown>;
|
|
39
39
|
};
|
|
40
|
+
export interface ToolExecuteHookInput {
|
|
41
|
+
tool: string;
|
|
42
|
+
sessionID: string;
|
|
43
|
+
callID: string;
|
|
44
|
+
}
|
|
40
45
|
export interface Hooks {
|
|
41
46
|
tool?: Record<string, ToolDefinition>;
|
|
47
|
+
dispose?: () => Promise<void>;
|
|
48
|
+
'tool.execute.before'?: (input: ToolExecuteHookInput, output: {
|
|
49
|
+
args: unknown;
|
|
50
|
+
}) => Promise<void> | void;
|
|
51
|
+
'tool.execute.after'?: (input: ToolExecuteHookInput, output: {
|
|
52
|
+
title: string;
|
|
53
|
+
output: string;
|
|
54
|
+
metadata: unknown;
|
|
55
|
+
}) => Promise<void> | void;
|
|
42
56
|
'experimental.chat.system.transform'?: (input: ExperimentalChatSystemTransformInput, output: ExperimentalChatSystemTransformOutput) => Promise<void> | void;
|
|
43
57
|
event?: (input: {
|
|
44
58
|
event: OpencodeEvent;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opencode-types.d.ts","sourceRoot":"","sources":["../src/opencode-types.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAE7B,UAAU,WAAW;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,WAAW,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,GAAG,IAAI,CAAC;CAC/E;AAED,MAAM,WAAW,cAAc,CAAC,IAAI,SAAS,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,WAAW;IACxE,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,IAAI,CAAC;IACX,OAAO,CACL,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,EAChC,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,CAAC;CACpB;AAQD,MAAM,WAAW,qBAAqB;IACpC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oCAAoC;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,qBAAqB,CAAC;CAC9B;AAED,MAAM,WAAW,qCAAqC;IACpD,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAUD,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,cAAc,CAAC;IACrB,UAAU,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CACnC,CAAC;AAEF,MAAM,MAAM,aAAa,GACrB,wBAAwB,GACxB;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"opencode-types.d.ts","sourceRoot":"","sources":["../src/opencode-types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"opencode-types.js","sourceRoot":"","sources":["../src/opencode-types.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"opencode-types.js","sourceRoot":"","sources":["../src/opencode-types.ts"],"names":[],"mappings":"AAoJA,MAAM,UAAU,IAAI,CAClB,KAA2B;IAE3B,OAAO,KAAK,CAAC;AACf,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xemahq/opencode-xema-plugin",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Opencode plugin providing Xema-namespaced runtime tools (xema_memory_*)
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Opencode plugin providing Xema-namespaced runtime tools (xema_memory_*), the dynamic system-prompt overlay hook, and the sub-agent fan-out ceiling.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Neuralchowder Inc. <developer@xema.dev> (https://xema.dev)",
|
|
7
7
|
"homepage": "https://xema.dev",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"esbuild": "^0.25.4",
|
|
34
34
|
"typescript": "5.9.3",
|
|
35
35
|
"zod": "^4.3.6",
|
|
36
|
-
"@xemahq/kernel-contracts": "^
|
|
36
|
+
"@xemahq/kernel-contracts": "^33.5.0"
|
|
37
37
|
},
|
|
38
38
|
"exports": {
|
|
39
39
|
".": {
|
package/plugin.mjs
CHANGED
|
@@ -14866,6 +14866,185 @@ function sleep(ms) {
|
|
|
14866
14866
|
});
|
|
14867
14867
|
}
|
|
14868
14868
|
|
|
14869
|
+
// src/hooks/subagent-fanout-ceiling.ts
|
|
14870
|
+
var TASK_TOOL_ID = "task";
|
|
14871
|
+
var DEFAULT_SUBAGENT_FANOUT_LIMIT = 8;
|
|
14872
|
+
var DEFAULT_FANOUT_WAIT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
14873
|
+
var DEFAULT_PERMIT_TTL_MS = 30 * 60 * 1e3;
|
|
14874
|
+
var FANOUT_LIMIT_ENV = "XEMA_SUBAGENT_FANOUT_LIMIT";
|
|
14875
|
+
function resolveSubagentFanoutLimit(env = process.env) {
|
|
14876
|
+
const declared = env[FANOUT_LIMIT_ENV];
|
|
14877
|
+
if (declared === void 0 || declared.trim() === "") {
|
|
14878
|
+
return DEFAULT_SUBAGENT_FANOUT_LIMIT;
|
|
14879
|
+
}
|
|
14880
|
+
const parsed = Number(declared.trim());
|
|
14881
|
+
if (!Number.isInteger(parsed)) {
|
|
14882
|
+
throw new Error(
|
|
14883
|
+
`${FANOUT_LIMIT_ENV}="${declared}" is not an integer. Expected a whole number of concurrently executing sub-agent tasks between 1 and ${DEFAULT_SUBAGENT_FANOUT_LIMIT}.`
|
|
14884
|
+
);
|
|
14885
|
+
}
|
|
14886
|
+
if (parsed < 1) {
|
|
14887
|
+
throw new Error(
|
|
14888
|
+
`${FANOUT_LIMIT_ENV}="${declared}" is below 1. A ceiling of 0 would park EVERY delegation until it timed out, turning a resource bound into an outage. Remove the variable to accept the platform default (${DEFAULT_SUBAGENT_FANOUT_LIMIT}).`
|
|
14889
|
+
);
|
|
14890
|
+
}
|
|
14891
|
+
if (parsed > DEFAULT_SUBAGENT_FANOUT_LIMIT) {
|
|
14892
|
+
throw new Error(
|
|
14893
|
+
`${FANOUT_LIMIT_ENV}="${declared}" is above the platform default (${DEFAULT_SUBAGENT_FANOUT_LIMIT}). Configuration may only TIGHTEN this ceiling. The default is a stated platform decision; a wider one moves DEFAULT_SUBAGENT_FANOUT_LIMIT, where the grounds are recorded.`
|
|
14894
|
+
);
|
|
14895
|
+
}
|
|
14896
|
+
return parsed;
|
|
14897
|
+
}
|
|
14898
|
+
function createSubagentFanoutCeiling(options) {
|
|
14899
|
+
const limit = options.limit;
|
|
14900
|
+
const waitTimeoutMs = options.waitTimeoutMs ?? DEFAULT_FANOUT_WAIT_TIMEOUT_MS;
|
|
14901
|
+
const permitTtlMs = options.permitTtlMs ?? DEFAULT_PERMIT_TTL_MS;
|
|
14902
|
+
const now = options.now ?? (() => Date.now());
|
|
14903
|
+
const held = /* @__PURE__ */ new Map();
|
|
14904
|
+
const waiters = [];
|
|
14905
|
+
let waitedCount = 0;
|
|
14906
|
+
let timedOutCount = 0;
|
|
14907
|
+
let sweptCount = 0;
|
|
14908
|
+
let ttlReclaimedCount = 0;
|
|
14909
|
+
const armed = /* @__PURE__ */ new Set();
|
|
14910
|
+
function arm(fn, ms, keepProcessAlive) {
|
|
14911
|
+
const timer = setTimeout(() => {
|
|
14912
|
+
armed.delete(timer);
|
|
14913
|
+
fn();
|
|
14914
|
+
}, ms);
|
|
14915
|
+
if (!keepProcessAlive) timer.unref?.();
|
|
14916
|
+
armed.add(timer);
|
|
14917
|
+
return timer;
|
|
14918
|
+
}
|
|
14919
|
+
function disarm(timer) {
|
|
14920
|
+
if (timer === void 0) return;
|
|
14921
|
+
clearTimeout(timer);
|
|
14922
|
+
armed.delete(timer);
|
|
14923
|
+
}
|
|
14924
|
+
function take(callId, sessionId) {
|
|
14925
|
+
const ttlTimer = arm(() => {
|
|
14926
|
+
if (!held.has(callId)) return;
|
|
14927
|
+
ttlReclaimedCount += 1;
|
|
14928
|
+
warn(
|
|
14929
|
+
`fanout-ceiling: permit TTL expired for call=${callId} session=${sessionId} after ${permitTtlMs}ms \u2014 force-releasing. A task held a slot longer than any healthy sub-agent turn; the pool would otherwise have shrunk permanently.`
|
|
14930
|
+
);
|
|
14931
|
+
release(callId, "ttl");
|
|
14932
|
+
}, permitTtlMs, false);
|
|
14933
|
+
held.set(callId, { sessionId, acquiredAt: now(), ttlTimer });
|
|
14934
|
+
}
|
|
14935
|
+
function release(callId, reason) {
|
|
14936
|
+
const permit = held.get(callId);
|
|
14937
|
+
if (permit === void 0) return;
|
|
14938
|
+
disarm(permit.ttlTimer);
|
|
14939
|
+
held.delete(callId);
|
|
14940
|
+
if (reason !== "completed") {
|
|
14941
|
+
debug(
|
|
14942
|
+
`fanout-ceiling: reclaimed call=${callId} via ${reason} (held ${now() - permit.acquiredAt}ms)`
|
|
14943
|
+
);
|
|
14944
|
+
}
|
|
14945
|
+
drain();
|
|
14946
|
+
}
|
|
14947
|
+
function drain() {
|
|
14948
|
+
while (waiters.length > 0 && held.size < limit) {
|
|
14949
|
+
const next = waiters.shift();
|
|
14950
|
+
if (next === void 0) return;
|
|
14951
|
+
disarm(next.timer);
|
|
14952
|
+
const waitedMs = now() - next.since;
|
|
14953
|
+
waitedCount += 1;
|
|
14954
|
+
take(next.callId, next.sessionId);
|
|
14955
|
+
info(
|
|
14956
|
+
`fanout-ceiling: admitted call=${next.callId} session=${next.sessionId} after waiting ${waitedMs}ms (limit=${limit}, in-flight=${held.size}, still waiting=${waiters.length})`
|
|
14957
|
+
);
|
|
14958
|
+
next.grant();
|
|
14959
|
+
}
|
|
14960
|
+
}
|
|
14961
|
+
async function before(input) {
|
|
14962
|
+
if (input.tool !== TASK_TOOL_ID) return;
|
|
14963
|
+
const { callID: callId, sessionID: sessionId } = input;
|
|
14964
|
+
if (held.size < limit) {
|
|
14965
|
+
take(callId, sessionId);
|
|
14966
|
+
debug(
|
|
14967
|
+
`fanout-ceiling: admitted call=${callId} immediately (in-flight=${held.size}/${limit})`
|
|
14968
|
+
);
|
|
14969
|
+
return;
|
|
14970
|
+
}
|
|
14971
|
+
const since = now();
|
|
14972
|
+
warn(
|
|
14973
|
+
`fanout-ceiling: sub-agent fan-out at ceiling \u2014 call=${callId} session=${sessionId} is WAITING (limit=${limit}, in-flight=${held.size}, already waiting=${waiters.length}). This is backpressure, not a refusal: the task will start when a slot frees.`
|
|
14974
|
+
);
|
|
14975
|
+
await new Promise((resolve) => {
|
|
14976
|
+
const waiter = {
|
|
14977
|
+
callId,
|
|
14978
|
+
sessionId,
|
|
14979
|
+
since,
|
|
14980
|
+
grant: resolve,
|
|
14981
|
+
timer: void 0
|
|
14982
|
+
};
|
|
14983
|
+
waiter.timer = arm(() => {
|
|
14984
|
+
const idx = waiters.indexOf(waiter);
|
|
14985
|
+
if (idx === -1) return;
|
|
14986
|
+
waiters.splice(idx, 1);
|
|
14987
|
+
timedOutCount += 1;
|
|
14988
|
+
warn(
|
|
14989
|
+
`fanout-ceiling: call=${callId} session=${sessionId} waited ${waitTimeoutMs}ms and is being ADMITTED OVER the ceiling (limit=${limit}, in-flight=${held.size}). The pool did not free a slot in that window \u2014 suspect a leaked permit or a stuck sub-agent.`
|
|
14990
|
+
);
|
|
14991
|
+
take(callId, sessionId);
|
|
14992
|
+
resolve();
|
|
14993
|
+
}, waitTimeoutMs, true);
|
|
14994
|
+
waiters.push(waiter);
|
|
14995
|
+
});
|
|
14996
|
+
}
|
|
14997
|
+
function after(input) {
|
|
14998
|
+
if (input.tool !== TASK_TOOL_ID) return;
|
|
14999
|
+
release(input.callID, "completed");
|
|
15000
|
+
}
|
|
15001
|
+
function onSessionIdle(sessionId) {
|
|
15002
|
+
const stale = [];
|
|
15003
|
+
for (const [callId, permit] of held) {
|
|
15004
|
+
if (permit.sessionId === sessionId) stale.push(callId);
|
|
15005
|
+
}
|
|
15006
|
+
if (stale.length === 0) return;
|
|
15007
|
+
sweptCount += stale.length;
|
|
15008
|
+
warn(
|
|
15009
|
+
`fanout-ceiling: session=${sessionId} went idle still holding ${stale.length} permit(s) [${stale.join(", ")}] \u2014 reclaiming. These tasks ended without a tool.execute.after, which upstream does not fire on the failure path.`
|
|
15010
|
+
);
|
|
15011
|
+
for (const callId of stale) release(callId, "session-idle");
|
|
15012
|
+
}
|
|
15013
|
+
function dispose() {
|
|
15014
|
+
const parked = waiters.length;
|
|
15015
|
+
const outstanding = held.size;
|
|
15016
|
+
while (waiters.length > 0) {
|
|
15017
|
+
const w = waiters.shift();
|
|
15018
|
+
if (w === void 0) break;
|
|
15019
|
+
disarm(w.timer);
|
|
15020
|
+
w.grant();
|
|
15021
|
+
}
|
|
15022
|
+
for (const permit of held.values()) disarm(permit.ttlTimer);
|
|
15023
|
+
held.clear();
|
|
15024
|
+
if (parked > 0 || outstanding > 0) {
|
|
15025
|
+
info(
|
|
15026
|
+
`fanout-ceiling: disposed \u2014 released ${parked} parked task(s) and disarmed ${outstanding} permit timer(s).`
|
|
15027
|
+
);
|
|
15028
|
+
}
|
|
15029
|
+
}
|
|
15030
|
+
return {
|
|
15031
|
+
before,
|
|
15032
|
+
after,
|
|
15033
|
+
onSessionIdle,
|
|
15034
|
+
dispose,
|
|
15035
|
+
stats: () => ({
|
|
15036
|
+
limit,
|
|
15037
|
+
inFlight: held.size,
|
|
15038
|
+
waiting: waiters.length,
|
|
15039
|
+
waitedCount,
|
|
15040
|
+
timedOutCount,
|
|
15041
|
+
sweptCount,
|
|
15042
|
+
ttlReclaimedCount,
|
|
15043
|
+
armedTimers: armed.size
|
|
15044
|
+
})
|
|
15045
|
+
};
|
|
15046
|
+
}
|
|
15047
|
+
|
|
14869
15048
|
// src/hooks/system-prompt-overlay.ts
|
|
14870
15049
|
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
14871
15050
|
import { join as join4 } from "node:path";
|
|
@@ -15252,15 +15431,54 @@ async function doBoot(directory) {
|
|
|
15252
15431
|
info(
|
|
15253
15432
|
`boot: directory=${directory} tools=${toolNames.length} [${toolNames.join(", ")}] context.json=${contextPresentAtBoot ? "present" : "absent"} debug=${isDebug() ? "on" : "off"}`
|
|
15254
15433
|
);
|
|
15434
|
+
const fanoutLimit = resolveSubagentFanoutLimit();
|
|
15435
|
+
const fanout = createSubagentFanoutCeiling({ limit: fanoutLimit });
|
|
15436
|
+
const autocommit = buildSessionIdleAutocommit(directory);
|
|
15437
|
+
info(
|
|
15438
|
+
`boot: subagent fan-out ceiling active (limit=${fanoutLimit} concurrent task calls per worker; tighten with XEMA_SUBAGENT_FANOUT_LIMIT)`
|
|
15439
|
+
);
|
|
15255
15440
|
const hooks = {
|
|
15256
15441
|
tool: tools,
|
|
15257
15442
|
"experimental.chat.system.transform": buildSystemPromptOverlay(directory),
|
|
15258
|
-
//
|
|
15259
|
-
//
|
|
15260
|
-
//
|
|
15261
|
-
//
|
|
15262
|
-
|
|
15263
|
-
|
|
15443
|
+
// Backpressure on sub-agent fan-out. Upstream awaits this hook immediately
|
|
15444
|
+
// before the tool runs, so parking here parks the `task` call BEFORE the
|
|
15445
|
+
// child session exists — a pool cap that makes the N+1th spawn wait, not a
|
|
15446
|
+
// counter that aborts a run already paid for.
|
|
15447
|
+
"tool.execute.before": async (input) => {
|
|
15448
|
+
await fanout.before(input);
|
|
15449
|
+
},
|
|
15450
|
+
"tool.execute.after": (input) => {
|
|
15451
|
+
fanout.after(input);
|
|
15452
|
+
},
|
|
15453
|
+
// Shutdown. Disarms the ceiling's timers and lets any parked `task` call
|
|
15454
|
+
// through, so a torn-down worker leaves nothing armed and nothing pending.
|
|
15455
|
+
dispose: async () => {
|
|
15456
|
+
fanout.dispose();
|
|
15457
|
+
},
|
|
15458
|
+
// TWO consumers of `session.idle` now, so the channel fans out. opencode
|
|
15459
|
+
// exposes session lifecycle only as a discriminated event union, not as
|
|
15460
|
+
// dedicated hooks, and `Hooks.event` is a single function — so composition
|
|
15461
|
+
// has to happen here.
|
|
15462
|
+
//
|
|
15463
|
+
// The order matters and the isolation matters more: the fan-out sweep is a
|
|
15464
|
+
// LEAK BACKSTOP, so it must run even if auto-commit fails. Auto-commit
|
|
15465
|
+
// already swallows its own errors by hard invariant, but relying on that
|
|
15466
|
+
// from the outside would make this correct only by someone else's promise.
|
|
15467
|
+
event: async (input) => {
|
|
15468
|
+
const evt = input.event;
|
|
15469
|
+
if (evt.type === "session.idle") {
|
|
15470
|
+
const sessionId = evt.properties?.sessionID;
|
|
15471
|
+
if (sessionId) {
|
|
15472
|
+
try {
|
|
15473
|
+
fanout.onSessionIdle(sessionId);
|
|
15474
|
+
} catch (err) {
|
|
15475
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
15476
|
+
warn(`fanout-ceiling: session-idle sweep failed: ${message}`);
|
|
15477
|
+
}
|
|
15478
|
+
}
|
|
15479
|
+
}
|
|
15480
|
+
await autocommit(input);
|
|
15481
|
+
}
|
|
15264
15482
|
};
|
|
15265
15483
|
return hooks;
|
|
15266
15484
|
}
|