@hegemonart/get-design-done 1.26.0 → 1.27.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +50 -0
- package/README.md +10 -8
- package/SKILL.md +3 -0
- package/agents/README.md +29 -0
- package/package.json +1 -1
- package/reference/peer-cli-capabilities.md +151 -0
- package/reference/peer-protocols.md +266 -0
- package/reference/registry.json +14 -0
- package/reference/runtime-models.md +3 -3
- package/scripts/lib/bandit-router.cjs +214 -7
- package/scripts/lib/budget-enforcer.cjs +69 -1
- package/scripts/lib/event-stream/index.ts +14 -1
- package/scripts/lib/event-stream/types.ts +125 -1
- package/scripts/lib/install/runtimes.cjs +58 -0
- package/scripts/lib/peer-cli/acp-client.cjs +375 -0
- package/scripts/lib/peer-cli/adapters/codex.cjs +101 -0
- package/scripts/lib/peer-cli/adapters/copilot.cjs +79 -0
- package/scripts/lib/peer-cli/adapters/cursor.cjs +78 -0
- package/scripts/lib/peer-cli/adapters/gemini.cjs +81 -0
- package/scripts/lib/peer-cli/adapters/qwen.cjs +72 -0
- package/scripts/lib/peer-cli/asp-client.cjs +587 -0
- package/scripts/lib/peer-cli/broker-lifecycle.cjs +406 -0
- package/scripts/lib/peer-cli/registry.cjs +434 -0
- package/scripts/lib/peer-cli/spawn-cmd.cjs +149 -0
- package/scripts/lib/runtime-detect.cjs +1 -1
- package/scripts/lib/session-runner/index.ts +215 -0
- package/scripts/lib/session-runner/types.ts +60 -0
- package/scripts/validate-frontmatter.ts +159 -1
- package/skills/peer-cli-add/SKILL.md +170 -0
- package/skills/peer-cli-customize/SKILL.md +110 -0
- package/skills/peers/SKILL.md +101 -0
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// scripts/lib/peer-cli/broker-lifecycle.cjs
|
|
2
|
+
//
|
|
3
|
+
// Plan 27-03 — long-lived broker session per (peer, workspace).
|
|
4
|
+
//
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// WHY A BROKER, NOT PER-CALL SPAWN — Phase 27 D-03
|
|
7
|
+
// ============================================================================
|
|
8
|
+
//
|
|
9
|
+
// Cold-spawning an ACP/ASP peer-CLI session for every delegated agent call
|
|
10
|
+
// re-runs the JSON-RPC `initialize` handshake every time. On a real cycle
|
|
11
|
+
// with N delegated calls (gemini-research, codex-execute, cursor-debug, ...),
|
|
12
|
+
// that's N handshakes × ~200-800ms each = a measurable latency tax that
|
|
13
|
+
// erases most of the cost arbitrage win.
|
|
14
|
+
//
|
|
15
|
+
// Instead, we keep ONE long-lived peer-CLI process per `(peer, workspace)`
|
|
16
|
+
// tuple — the "broker" — and route delegated calls to it via:
|
|
17
|
+
//
|
|
18
|
+
// - POSIX: Unix domain socket at
|
|
19
|
+
// `~/.gdd/peer-brokers/<peer>-<workspace-hash>.sock`
|
|
20
|
+
// - Windows: named pipe at
|
|
21
|
+
// `\\.\pipe\gdd-peer-broker-<peer>-<workspace-hash>`
|
|
22
|
+
//
|
|
23
|
+
// Brokers live BETWEEN gdd cycles. Closing this client's connection does
|
|
24
|
+
// NOT shut down the broker — multiple cycles re-attach to the same broker
|
|
25
|
+
// process. The broker is reaped by an external lifecycle manager (TBD in
|
|
26
|
+
// Plan 27-06 integration) or by an idle-timeout the broker itself enforces.
|
|
27
|
+
//
|
|
28
|
+
// This module is the CLIENT-SIDE surface only. It connects, sends JSON-RPC
|
|
29
|
+
// frames, awaits replies, and disconnects. It does NOT implement the broker
|
|
30
|
+
// server itself — that's a separate concern (likely a small persistent
|
|
31
|
+
// process started by the registry on first dispatch). For tests we mock
|
|
32
|
+
// the underlying transport entirely; real broker spawn/reap is exercised
|
|
33
|
+
// in Plan 27-06's integration harness.
|
|
34
|
+
//
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// CONTRACT
|
|
37
|
+
// ============================================================================
|
|
38
|
+
//
|
|
39
|
+
// const broker = createBroker({
|
|
40
|
+
// peer: 'gemini', // peer ID; matches adapters/<peer>.cjs
|
|
41
|
+
// workspace: '/repo', // absolute repo path
|
|
42
|
+
// transport: 'acp', // 'acp' or 'asp'
|
|
43
|
+
// });
|
|
44
|
+
// await broker.connect();
|
|
45
|
+
// broker.send({ id: 1, method: 'initialize', params: {...} });
|
|
46
|
+
// const reply = await broker.receive(5000); // ms timeout
|
|
47
|
+
// await broker.close();
|
|
48
|
+
//
|
|
49
|
+
// Properties:
|
|
50
|
+
// - send() is non-blocking; replies arrive via receive()
|
|
51
|
+
// - receive() resolves with the next pending reply OR rejects on timeout
|
|
52
|
+
// - send() throws BrokerBusyError when the in-flight queue exceeds
|
|
53
|
+
// MAX_PENDING (D-03 backpressure: 100)
|
|
54
|
+
// - close() releases this client's transport handle but does NOT signal
|
|
55
|
+
// the broker process to terminate
|
|
56
|
+
//
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// BACKPRESSURE
|
|
59
|
+
// ============================================================================
|
|
60
|
+
//
|
|
61
|
+
// Each client tracks its own pending-request queue. If the broker is slow
|
|
62
|
+
// (e.g., a long-running peer LLM call) and the caller fires sends faster
|
|
63
|
+
// than the broker drains, send() throws `BrokerBusyError` once the queue
|
|
64
|
+
// hits 100 in-flight. This is intentional: a stuck broker should surface
|
|
65
|
+
// as "your peer is wedged" rather than silently inflating memory until the
|
|
66
|
+
// process OOMs. The session-runner (Plan 27-06) catches BrokerBusyError
|
|
67
|
+
// and falls back to local Anthropic SDK on this dispatch — same fallback
|
|
68
|
+
// path as peer-absent / peer-error per D-07.
|
|
69
|
+
|
|
70
|
+
'use strict';
|
|
71
|
+
|
|
72
|
+
const crypto = require('node:crypto');
|
|
73
|
+
const net = require('node:net');
|
|
74
|
+
const path = require('node:path');
|
|
75
|
+
const os = require('node:os');
|
|
76
|
+
|
|
77
|
+
const MAX_PENDING = 100;
|
|
78
|
+
const DEFAULT_RECEIVE_TIMEOUT_MS = 30_000;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Thrown by `send()` when this client's pending-request queue is at the
|
|
82
|
+
* `MAX_PENDING` ceiling. Session-runner catches this and falls back.
|
|
83
|
+
*/
|
|
84
|
+
class BrokerBusyError extends Error {
|
|
85
|
+
constructor(message) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.name = 'BrokerBusyError';
|
|
88
|
+
this.code = 'EBROKERBUSY';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Thrown by `receive()` when no reply arrives within the timeout window.
|
|
94
|
+
*/
|
|
95
|
+
class BrokerTimeoutError extends Error {
|
|
96
|
+
constructor(message) {
|
|
97
|
+
super(message);
|
|
98
|
+
this.name = 'BrokerTimeoutError';
|
|
99
|
+
this.code = 'EBROKERTIMEOUT';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Compute the platform-appropriate broker endpoint for a (peer, workspace).
|
|
105
|
+
*
|
|
106
|
+
* POSIX → `~/.gdd/peer-brokers/<peer>-<hash>.sock`
|
|
107
|
+
* Windows → `\\.\pipe\gdd-peer-broker-<peer>-<hash>`
|
|
108
|
+
*
|
|
109
|
+
* The workspace hash is a short SHA-256 prefix of the absolute workspace
|
|
110
|
+
* path; we don't include the literal path in the socket name because:
|
|
111
|
+
* - sockets have a length limit (~104 bytes on macOS) that long repo
|
|
112
|
+
* paths blow through, and
|
|
113
|
+
* - paths contain `/` and other chars that make filenames awkward.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} args
|
|
116
|
+
* @param {string} args.peer e.g. 'gemini'
|
|
117
|
+
* @param {string} args.workspace absolute path to the workspace root
|
|
118
|
+
* @param {NodeJS.Platform} [args.platform] override; tests inject 'win32'
|
|
119
|
+
* @returns {string} endpoint path or named-pipe address
|
|
120
|
+
*/
|
|
121
|
+
function brokerEndpoint({ peer, workspace, platform } = {}) {
|
|
122
|
+
if (typeof peer !== 'string' || peer.length === 0) {
|
|
123
|
+
throw new TypeError('brokerEndpoint: peer must be a non-empty string');
|
|
124
|
+
}
|
|
125
|
+
if (typeof workspace !== 'string' || workspace.length === 0) {
|
|
126
|
+
throw new TypeError('brokerEndpoint: workspace must be a non-empty string');
|
|
127
|
+
}
|
|
128
|
+
const plat = platform || process.platform;
|
|
129
|
+
const hash = crypto
|
|
130
|
+
.createHash('sha256')
|
|
131
|
+
.update(workspace)
|
|
132
|
+
.digest('hex')
|
|
133
|
+
.slice(0, 12);
|
|
134
|
+
|
|
135
|
+
if (plat === 'win32') {
|
|
136
|
+
return `\\\\.\\pipe\\gdd-peer-broker-${peer}-${hash}`;
|
|
137
|
+
}
|
|
138
|
+
return path.join(os.homedir(), '.gdd', 'peer-brokers', `${peer}-${hash}.sock`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @typedef {object} BrokerOptions
|
|
143
|
+
* @property {string} peer peer-CLI ID (e.g. 'gemini', 'codex')
|
|
144
|
+
* @property {string} workspace absolute workspace path
|
|
145
|
+
* @property {'acp'|'asp'} transport
|
|
146
|
+
* @property {string} [endpoint] override the computed endpoint (test/escape hatch)
|
|
147
|
+
* @property {(endpoint: string) => any} [connectFn] test injection: must return
|
|
148
|
+
* an object with `.write(line: string) → boolean`, `.end()`, and EventEmitter
|
|
149
|
+
* semantics for `data` (Buffer chunks) + `error` + `close`. Default: `net.createConnection`.
|
|
150
|
+
* @property {NodeJS.Platform} [platform] override for endpoint computation
|
|
151
|
+
* @property {number} [maxPending] override MAX_PENDING (test only)
|
|
152
|
+
*/
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @typedef {object} BrokerHandle
|
|
156
|
+
* @property {() => Promise<void>} connect
|
|
157
|
+
* @property {(message: object) => void} send
|
|
158
|
+
* @property {(timeoutMs?: number) => Promise<object>} receive
|
|
159
|
+
* @property {() => Promise<void>} close
|
|
160
|
+
* @property {() => number} pendingCount current in-flight send count
|
|
161
|
+
* @property {string} endpoint
|
|
162
|
+
*/
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create a client handle for a peer broker. Idempotent connect — calling
|
|
166
|
+
* `.connect()` twice on the same handle is a no-op after the first.
|
|
167
|
+
*
|
|
168
|
+
* @param {BrokerOptions} opts
|
|
169
|
+
* @returns {BrokerHandle}
|
|
170
|
+
*/
|
|
171
|
+
function createBroker(opts) {
|
|
172
|
+
if (!opts || typeof opts !== 'object') {
|
|
173
|
+
throw new TypeError('createBroker: opts object required');
|
|
174
|
+
}
|
|
175
|
+
const { peer, workspace, transport } = opts;
|
|
176
|
+
if (transport !== 'acp' && transport !== 'asp') {
|
|
177
|
+
throw new TypeError(
|
|
178
|
+
`createBroker: transport must be 'acp' or 'asp', got ${JSON.stringify(transport)}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const endpoint =
|
|
183
|
+
opts.endpoint ||
|
|
184
|
+
brokerEndpoint({ peer, workspace, platform: opts.platform });
|
|
185
|
+
const connectFn = opts.connectFn || ((ep) => net.createConnection(ep));
|
|
186
|
+
const maxPending =
|
|
187
|
+
Number.isFinite(opts.maxPending) && opts.maxPending > 0
|
|
188
|
+
? opts.maxPending
|
|
189
|
+
: MAX_PENDING;
|
|
190
|
+
|
|
191
|
+
/** @type {any} the underlying socket / fake transport */
|
|
192
|
+
let socket = null;
|
|
193
|
+
let connected = false;
|
|
194
|
+
let connecting = null;
|
|
195
|
+
let closed = false;
|
|
196
|
+
|
|
197
|
+
// Inbound replies that have been parsed but not yet handed to a receive() caller.
|
|
198
|
+
/** @type {object[]} */
|
|
199
|
+
const inbox = [];
|
|
200
|
+
// receive() callers waiting for the next reply. FIFO.
|
|
201
|
+
/** @type {Array<{resolve: (m: object) => void, reject: (e: Error) => void, timer: NodeJS.Timeout | null}>} */
|
|
202
|
+
const waiters = [];
|
|
203
|
+
// Send-side accounting: count of outgoing requests that have not yet been
|
|
204
|
+
// matched by a corresponding inbound reply. We use this for backpressure
|
|
205
|
+
// — when this hits maxPending, send() throws BrokerBusyError.
|
|
206
|
+
let pending = 0;
|
|
207
|
+
|
|
208
|
+
// Line-buffer for newline-delimited JSON. ACP and ASP both frame messages
|
|
209
|
+
// with a single `\n` separator; partial chunks are common because TCP /
|
|
210
|
+
// domain-socket reads can split frames anywhere.
|
|
211
|
+
let lineBuf = '';
|
|
212
|
+
|
|
213
|
+
function deliver(message) {
|
|
214
|
+
// Decrement pending: every inbound message that matches a sent request
|
|
215
|
+
// frees one slot. We don't try to correlate request IDs here — that's
|
|
216
|
+
// the protocol layer's job (acp-client / asp-client). At the broker
|
|
217
|
+
// level we just count round-trips so backpressure is meaningful.
|
|
218
|
+
if (pending > 0) pending -= 1;
|
|
219
|
+
|
|
220
|
+
const next = waiters.shift();
|
|
221
|
+
if (next) {
|
|
222
|
+
if (next.timer) clearTimeout(next.timer);
|
|
223
|
+
next.resolve(message);
|
|
224
|
+
} else {
|
|
225
|
+
inbox.push(message);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function fail(err) {
|
|
230
|
+
// Reject all pending receivers; future send() calls fail because the
|
|
231
|
+
// socket is gone.
|
|
232
|
+
while (waiters.length > 0) {
|
|
233
|
+
const w = waiters.shift();
|
|
234
|
+
if (w.timer) clearTimeout(w.timer);
|
|
235
|
+
w.reject(err);
|
|
236
|
+
}
|
|
237
|
+
connected = false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function onData(chunk) {
|
|
241
|
+
lineBuf += typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
242
|
+
let nl;
|
|
243
|
+
while ((nl = lineBuf.indexOf('\n')) >= 0) {
|
|
244
|
+
const line = lineBuf.slice(0, nl).trim();
|
|
245
|
+
lineBuf = lineBuf.slice(nl + 1);
|
|
246
|
+
if (line.length === 0) continue;
|
|
247
|
+
let parsed;
|
|
248
|
+
try {
|
|
249
|
+
parsed = JSON.parse(line);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
// Malformed frame from the broker — surface as an error to anyone
|
|
252
|
+
// waiting; flush the rest of the buffer to avoid wedging on a
|
|
253
|
+
// poison frame.
|
|
254
|
+
fail(new Error(`broker: malformed JSON frame: ${e.message}`));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
deliver(parsed);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function attachSocket(sock) {
|
|
262
|
+
socket = sock;
|
|
263
|
+
sock.on('data', onData);
|
|
264
|
+
sock.on('error', (e) => fail(e));
|
|
265
|
+
sock.on('close', () => {
|
|
266
|
+
// Treat unexpected close as an error for any in-flight callers, but
|
|
267
|
+
// don't synthesize an error if the consumer initiated close().
|
|
268
|
+
if (!closed) {
|
|
269
|
+
fail(new Error('broker: connection closed unexpectedly'));
|
|
270
|
+
}
|
|
271
|
+
connected = false;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function connect() {
|
|
276
|
+
if (connected) return;
|
|
277
|
+
if (connecting) return connecting;
|
|
278
|
+
if (closed) {
|
|
279
|
+
throw new Error('broker: cannot reconnect a closed handle');
|
|
280
|
+
}
|
|
281
|
+
connecting = new Promise((resolve, reject) => {
|
|
282
|
+
let sock;
|
|
283
|
+
try {
|
|
284
|
+
sock = connectFn(endpoint);
|
|
285
|
+
} catch (e) {
|
|
286
|
+
reject(e);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
// If the underlying transport supports a 'connect' / 'ready' event,
|
|
290
|
+
// wait for it. Otherwise (e.g., test fakes that are synchronously
|
|
291
|
+
// ready) fall through to immediate resolve.
|
|
292
|
+
let settled = false;
|
|
293
|
+
const finish = (err) => {
|
|
294
|
+
if (settled) return;
|
|
295
|
+
settled = true;
|
|
296
|
+
if (err) {
|
|
297
|
+
reject(err);
|
|
298
|
+
} else {
|
|
299
|
+
attachSocket(sock);
|
|
300
|
+
connected = true;
|
|
301
|
+
resolve();
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
// Most net.Socket / mock transports support .once('connect').
|
|
305
|
+
if (typeof sock.once === 'function') {
|
|
306
|
+
sock.once('connect', () => finish(null));
|
|
307
|
+
sock.once('error', (e) => finish(e));
|
|
308
|
+
} else {
|
|
309
|
+
// Synchronous fake: assume already connected.
|
|
310
|
+
queueMicrotask(() => finish(null));
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
try {
|
|
314
|
+
await connecting;
|
|
315
|
+
} finally {
|
|
316
|
+
connecting = null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function send(message) {
|
|
321
|
+
if (closed) {
|
|
322
|
+
throw new Error('broker: send on closed handle');
|
|
323
|
+
}
|
|
324
|
+
if (!connected) {
|
|
325
|
+
throw new Error('broker: send before connect');
|
|
326
|
+
}
|
|
327
|
+
if (pending >= maxPending) {
|
|
328
|
+
throw new BrokerBusyError(
|
|
329
|
+
`broker: ${pending} pending requests at MAX_PENDING=${maxPending}`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
const line = JSON.stringify(message) + '\n';
|
|
333
|
+
pending += 1;
|
|
334
|
+
// We deliberately do not await drain() here — line-delimited JSON-RPC
|
|
335
|
+
// is small (typically < 4KB per frame); the kernel buffer absorbs it.
|
|
336
|
+
// If a future workload changes that, switch to a write-with-drain queue.
|
|
337
|
+
socket.write(line);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function receive(timeoutMs) {
|
|
341
|
+
if (closed) {
|
|
342
|
+
return Promise.reject(new Error('broker: receive on closed handle'));
|
|
343
|
+
}
|
|
344
|
+
// Fast path: a reply arrived before anyone was waiting.
|
|
345
|
+
if (inbox.length > 0) {
|
|
346
|
+
return Promise.resolve(inbox.shift());
|
|
347
|
+
}
|
|
348
|
+
const ms = Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
349
|
+
? timeoutMs
|
|
350
|
+
: DEFAULT_RECEIVE_TIMEOUT_MS;
|
|
351
|
+
return new Promise((resolve, reject) => {
|
|
352
|
+
const waiter = { resolve, reject, timer: null };
|
|
353
|
+
waiter.timer = setTimeout(() => {
|
|
354
|
+
// Remove from queue so a late-arriving reply doesn't resolve a
|
|
355
|
+
// caller that already gave up.
|
|
356
|
+
const idx = waiters.indexOf(waiter);
|
|
357
|
+
if (idx >= 0) waiters.splice(idx, 1);
|
|
358
|
+
reject(
|
|
359
|
+
new BrokerTimeoutError(`broker: receive timeout after ${ms}ms`),
|
|
360
|
+
);
|
|
361
|
+
}, ms);
|
|
362
|
+
// Allow the process to exit even if a receive() is outstanding —
|
|
363
|
+
// matches the behavior of net.Socket which is also unref-friendly.
|
|
364
|
+
if (waiter.timer && typeof waiter.timer.unref === 'function') {
|
|
365
|
+
waiter.timer.unref();
|
|
366
|
+
}
|
|
367
|
+
waiters.push(waiter);
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function close() {
|
|
372
|
+
if (closed) return;
|
|
373
|
+
closed = true;
|
|
374
|
+
// Reject any outstanding waiters — the consumer is going away.
|
|
375
|
+
while (waiters.length > 0) {
|
|
376
|
+
const w = waiters.shift();
|
|
377
|
+
if (w.timer) clearTimeout(w.timer);
|
|
378
|
+
w.reject(new Error('broker: handle closed'));
|
|
379
|
+
}
|
|
380
|
+
if (socket && typeof socket.end === 'function') {
|
|
381
|
+
try {
|
|
382
|
+
socket.end();
|
|
383
|
+
} catch {
|
|
384
|
+
// Best-effort close — broker may already be gone.
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
connected = false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
connect,
|
|
392
|
+
send,
|
|
393
|
+
receive,
|
|
394
|
+
close,
|
|
395
|
+
pendingCount: () => pending,
|
|
396
|
+
endpoint,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
module.exports = {
|
|
401
|
+
createBroker,
|
|
402
|
+
brokerEndpoint,
|
|
403
|
+
BrokerBusyError,
|
|
404
|
+
BrokerTimeoutError,
|
|
405
|
+
MAX_PENDING,
|
|
406
|
+
};
|