github-router 0.3.150 → 0.3.152
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/browser-ext/manifest.json +1 -1
- package/dist/engine-CvY9mR3b.js +6 -0
- package/dist/main.js +58 -22
- package/dist/main.js.map +1 -1
- package/dist/{peer-mcp-personas-D_HyWhUb.js → peer-mcp-personas-BQVOxB1i.js} +902 -59
- package/dist/peer-mcp-personas-BQVOxB1i.js.map +1 -0
- package/package.json +1 -1
- package/dist/engine-Dtu1TDpQ.js +0 -6
- package/dist/peer-mcp-personas-D_HyWhUb.js.map +0 -1
|
@@ -1176,35 +1176,172 @@ var ArtifactError = class extends Error {
|
|
|
1176
1176
|
this.detail = args.detail;
|
|
1177
1177
|
}
|
|
1178
1178
|
};
|
|
1179
|
+
const AWAIT_DEFAULT_TIMEOUT_MS = 25e3;
|
|
1180
|
+
const AWAIT_ABORT_MARGIN_MS = 5e3;
|
|
1181
|
+
const DEFAULT_RETRY_BASE_MS = 250;
|
|
1182
|
+
const TRANSIENT_RETRIES = 2;
|
|
1183
|
+
const DEFAULT_RETRYABLE_CODES = new Set(["UNREACHABLE", "TIMEOUT"]);
|
|
1184
|
+
const AWAIT_RETRYABLE_CODES = new Set(["UNREACHABLE"]);
|
|
1179
1185
|
var ArtifactClient = class {
|
|
1180
1186
|
baseUrl;
|
|
1181
1187
|
token;
|
|
1182
1188
|
sessionId;
|
|
1183
1189
|
fetchFn;
|
|
1184
1190
|
insecureTLS;
|
|
1191
|
+
retryBaseMs;
|
|
1185
1192
|
constructor(options) {
|
|
1186
1193
|
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
1187
1194
|
this.token = options.token;
|
|
1188
1195
|
this.sessionId = options.sessionId;
|
|
1189
1196
|
this.fetchFn = options.fetchFn ?? globalThis.fetch.bind(globalThis);
|
|
1190
1197
|
this.insecureTLS = options.insecureTLS ?? false;
|
|
1198
|
+
this.retryBaseMs = options.retryBaseMs ?? DEFAULT_RETRY_BASE_MS;
|
|
1191
1199
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1200
|
+
/**
|
|
1201
|
+
* Open (or replace) the review for this session. `mode` is advisory metadata
|
|
1202
|
+
* forwarded to the server (forward-compat: ignored if unknown); the actual
|
|
1203
|
+
* interactivity comes from the served HTML's `data-aod-*` markup. Retried on a
|
|
1204
|
+
* transient failure with a STABLE idempotency key so a retry-after-success does
|
|
1205
|
+
* not double-open.
|
|
1206
|
+
*/
|
|
1207
|
+
open(file, opts = {}) {
|
|
1208
|
+
const idempotencyKey = opts.idempotencyKey ?? randomUUID();
|
|
1209
|
+
const body = { file };
|
|
1210
|
+
if (opts.mode) body.mode = opts.mode;
|
|
1211
|
+
return this.withRetry(() => this.requestOnce({
|
|
1212
|
+
method: "POST",
|
|
1213
|
+
pathname: this.path("/open"),
|
|
1214
|
+
body,
|
|
1215
|
+
signal: opts.signal,
|
|
1216
|
+
idempotencyKey
|
|
1217
|
+
}), TRANSIENT_RETRIES, opts.signal);
|
|
1194
1218
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1219
|
+
/**
|
|
1220
|
+
* Replace the current review's content. Exactly one of `file` | `html` (the
|
|
1221
|
+
* caller-facing tool enforces that). `html` is written by the SERVER to the
|
|
1222
|
+
* review's existing sandboxed file, so `html` with no existing review is an
|
|
1223
|
+
* INVALID_REQUEST from the server. Retried with a stable idempotency key.
|
|
1224
|
+
*/
|
|
1225
|
+
update(opts) {
|
|
1226
|
+
const idempotencyKey = opts.idempotencyKey ?? randomUUID();
|
|
1227
|
+
const body = {};
|
|
1228
|
+
if (opts.file !== void 0) body.file = opts.file;
|
|
1229
|
+
if (opts.html !== void 0) body.html = opts.html;
|
|
1230
|
+
return this.withRetry(() => this.requestOnce({
|
|
1231
|
+
method: "POST",
|
|
1232
|
+
pathname: this.path("/update"),
|
|
1233
|
+
body,
|
|
1234
|
+
signal: opts.signal,
|
|
1235
|
+
idempotencyKey,
|
|
1236
|
+
allowEmptyJson: true
|
|
1237
|
+
}), TRANSIENT_RETRIES, opts.signal);
|
|
1238
|
+
}
|
|
1239
|
+
/** Force a reload from disk (no content change). Single-shot (idempotent, cheap). */
|
|
1240
|
+
refresh(signal) {
|
|
1241
|
+
return this.requestOnce({
|
|
1242
|
+
method: "POST",
|
|
1243
|
+
pathname: this.path("/refresh"),
|
|
1244
|
+
signal,
|
|
1245
|
+
allowEmptyJson: true
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
/** Hide the panel UI while keeping the review alive. Single-shot; server idempotent. */
|
|
1249
|
+
dismiss(signal) {
|
|
1250
|
+
return this.requestOnce({
|
|
1251
|
+
method: "POST",
|
|
1252
|
+
pathname: this.path("/dismiss"),
|
|
1253
|
+
signal,
|
|
1254
|
+
allowEmptyJson: true
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Typed drain (contract v2.2 §1/§2). Long-holds up to the server cap, returns
|
|
1259
|
+
* events with `id > cursor` plus the new high-water `cursor`. Idempotent by
|
|
1260
|
+
* (cursor, event.id): re-calling with the same cursor replays the same window
|
|
1261
|
+
* from the server's bounded buffer, so a lost cursor (compaction) is
|
|
1262
|
+
* recoverable. Retried on a transient network failure (the cursor makes retry
|
|
1263
|
+
* safe — no double-consumption).
|
|
1264
|
+
*/
|
|
1265
|
+
awaitEvents(opts = {}) {
|
|
1266
|
+
const serverTimeoutMs = typeof opts.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : AWAIT_DEFAULT_TIMEOUT_MS;
|
|
1267
|
+
const clientTimeoutMs = serverTimeoutMs + AWAIT_ABORT_MARGIN_MS;
|
|
1268
|
+
return this.withRetry(() => this.requestOnce({
|
|
1269
|
+
method: "GET",
|
|
1270
|
+
pathname: this.path("/await"),
|
|
1271
|
+
query: {
|
|
1272
|
+
cursor: opts.cursor,
|
|
1273
|
+
timeoutMs: String(serverTimeoutMs)
|
|
1274
|
+
},
|
|
1275
|
+
signal: opts.signal,
|
|
1276
|
+
timeoutMsHint: clientTimeoutMs
|
|
1277
|
+
}), TRANSIENT_RETRIES, opts.signal, AWAIT_RETRYABLE_CODES);
|
|
1197
1278
|
}
|
|
1279
|
+
/** Free-text agent->human reply. SINGLE-SHOT (not retried): a retry-after-success
|
|
1280
|
+
* would render a duplicate chat bubble server-side. */
|
|
1198
1281
|
agentReply(text, signal) {
|
|
1199
|
-
return this.
|
|
1282
|
+
return this.requestOnce({
|
|
1283
|
+
method: "POST",
|
|
1284
|
+
pathname: this.path("/agent-reply"),
|
|
1285
|
+
body: { text },
|
|
1286
|
+
signal,
|
|
1287
|
+
allowEmptyJson: true
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* End the review. Retried with a stable idempotency key; a NOT_FOUND (the
|
|
1292
|
+
* review is already ended — whether by a prior call or by a retry landing after
|
|
1293
|
+
* the first attempt succeeded) is mapped to a successful `{ ok, status:"ended" }`
|
|
1294
|
+
* rather than surfaced as a 404 (contract v2.2 §1.1).
|
|
1295
|
+
*/
|
|
1296
|
+
async end(signal) {
|
|
1297
|
+
const idempotencyKey = randomUUID();
|
|
1298
|
+
try {
|
|
1299
|
+
return await this.withRetry(() => this.requestOnce({
|
|
1300
|
+
method: "POST",
|
|
1301
|
+
pathname: this.path("/end"),
|
|
1302
|
+
signal,
|
|
1303
|
+
idempotencyKey,
|
|
1304
|
+
allowEmptyJson: true
|
|
1305
|
+
}), TRANSIENT_RETRIES, signal);
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
if (err instanceof ArtifactError && err.code === "NOT_FOUND") return {
|
|
1308
|
+
ok: true,
|
|
1309
|
+
status: "ended"
|
|
1310
|
+
};
|
|
1311
|
+
throw err;
|
|
1312
|
+
}
|
|
1200
1313
|
}
|
|
1201
|
-
|
|
1202
|
-
|
|
1314
|
+
/**
|
|
1315
|
+
* FROZEN legacy long-poll (contract v2.2 §1). Old payload shape,
|
|
1316
|
+
* comment-equivalent only. New agents use `awaitEvents`. Single request per
|
|
1317
|
+
* call (the caller-facing tool owns the bounded re-poll budget).
|
|
1318
|
+
*/
|
|
1319
|
+
poll(timeoutMsHint, signal) {
|
|
1320
|
+
return this.requestOnce({
|
|
1321
|
+
method: "GET",
|
|
1322
|
+
pathname: this.path("/poll"),
|
|
1323
|
+
signal,
|
|
1324
|
+
timeoutMsHint
|
|
1325
|
+
});
|
|
1203
1326
|
}
|
|
1204
|
-
|
|
1327
|
+
path(suffix) {
|
|
1328
|
+
return `/api/artifact/${encodeURIComponent(this.sessionId)}${suffix}`;
|
|
1329
|
+
}
|
|
1330
|
+
async withRetry(fn, retries, signal, retryableCodes = DEFAULT_RETRYABLE_CODES) {
|
|
1331
|
+
let attempt = 0;
|
|
1332
|
+
for (;;) try {
|
|
1333
|
+
return await fn();
|
|
1334
|
+
} catch (err) {
|
|
1335
|
+
attempt += 1;
|
|
1336
|
+
if (!(err instanceof ArtifactError && err.retryable && retryableCodes.has(err.code)) || attempt > retries || signal?.aborted) throw err;
|
|
1337
|
+
const base = this.retryBaseMs;
|
|
1338
|
+
await sleep$1(base <= 0 ? 0 : Math.round(base * 2 ** (attempt - 1) * (.5 + Math.random() * .5)), signal);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
async requestOnce(o) {
|
|
1205
1342
|
let url;
|
|
1206
1343
|
try {
|
|
1207
|
-
url = new URL(pathname, `${this.baseUrl}/`);
|
|
1344
|
+
url = new URL(o.pathname, `${this.baseUrl}/`);
|
|
1208
1345
|
} catch (err) {
|
|
1209
1346
|
throw new ArtifactError({
|
|
1210
1347
|
code: "UNREACHABLE",
|
|
@@ -1213,48 +1350,73 @@ var ArtifactClient = class {
|
|
|
1213
1350
|
detail: err
|
|
1214
1351
|
});
|
|
1215
1352
|
}
|
|
1216
|
-
|
|
1217
|
-
|
|
1353
|
+
if (o.query) {
|
|
1354
|
+
for (const [key, value] of Object.entries(o.query)) if (value !== void 0) url.searchParams.set(key, value);
|
|
1355
|
+
}
|
|
1356
|
+
const timeout = combineSignalAndTimeout(o.signal, o.timeoutMsHint);
|
|
1218
1357
|
try {
|
|
1358
|
+
const headers = { Authorization: `Bearer ${this.token}` };
|
|
1359
|
+
if (o.body !== void 0) headers["Content-Type"] = "application/json";
|
|
1360
|
+
if (o.idempotencyKey) headers["Idempotency-Key"] = o.idempotencyKey;
|
|
1219
1361
|
const init = {
|
|
1220
|
-
method,
|
|
1221
|
-
headers
|
|
1222
|
-
|
|
1223
|
-
...body === void 0 ? {} : { "Content-Type": "application/json" }
|
|
1224
|
-
},
|
|
1225
|
-
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
1362
|
+
method: o.method,
|
|
1363
|
+
headers,
|
|
1364
|
+
body: o.body === void 0 ? void 0 : JSON.stringify(o.body),
|
|
1226
1365
|
redirect: "error",
|
|
1227
1366
|
signal: timeout.signal
|
|
1228
1367
|
};
|
|
1229
1368
|
if (this.insecureTLS) applyInsecureTls(init);
|
|
1230
|
-
response = await this.fetchFn(url.toString(), init);
|
|
1369
|
+
const response = await this.fetchFn(url.toString(), init);
|
|
1370
|
+
if (!response.ok) throw await mapHttpError$1(response);
|
|
1371
|
+
let text;
|
|
1372
|
+
try {
|
|
1373
|
+
text = await response.text();
|
|
1374
|
+
} catch (err) {
|
|
1375
|
+
if (isAbortLike$2(err)) throw mapNetworkError$1(err);
|
|
1376
|
+
throw new ArtifactError({
|
|
1377
|
+
code: "INVALID_RESPONSE",
|
|
1378
|
+
message: "artifact API response body could not be read",
|
|
1379
|
+
retryable: false,
|
|
1380
|
+
detail: err
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
if (!text && o.allowEmptyJson) return {};
|
|
1384
|
+
try {
|
|
1385
|
+
return JSON.parse(text);
|
|
1386
|
+
} catch (err) {
|
|
1387
|
+
throw new ArtifactError({
|
|
1388
|
+
code: "INVALID_RESPONSE",
|
|
1389
|
+
message: "artifact API returned a non-JSON response",
|
|
1390
|
+
retryable: false,
|
|
1391
|
+
detail: err
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1231
1394
|
} catch (err) {
|
|
1395
|
+
if (err instanceof ArtifactError) throw err;
|
|
1232
1396
|
throw mapNetworkError$1(err);
|
|
1233
1397
|
} finally {
|
|
1234
1398
|
timeout.cleanup();
|
|
1235
1399
|
}
|
|
1236
|
-
if (!response.ok) throw await mapHttpError$1(response);
|
|
1237
|
-
const text = await response.text().catch((err) => {
|
|
1238
|
-
throw new ArtifactError({
|
|
1239
|
-
code: "INVALID_RESPONSE",
|
|
1240
|
-
message: "artifact API response body could not be read",
|
|
1241
|
-
retryable: false,
|
|
1242
|
-
detail: err
|
|
1243
|
-
});
|
|
1244
|
-
});
|
|
1245
|
-
if (!text && allowEmptyJson) return {};
|
|
1246
|
-
try {
|
|
1247
|
-
return JSON.parse(text);
|
|
1248
|
-
} catch (err) {
|
|
1249
|
-
throw new ArtifactError({
|
|
1250
|
-
code: "INVALID_RESPONSE",
|
|
1251
|
-
message: "artifact API returned a non-JSON response",
|
|
1252
|
-
retryable: false,
|
|
1253
|
-
detail: err
|
|
1254
|
-
});
|
|
1255
|
-
}
|
|
1256
1400
|
}
|
|
1257
1401
|
};
|
|
1402
|
+
function sleep$1(ms, signal) {
|
|
1403
|
+
if (ms <= 0) return Promise.resolve();
|
|
1404
|
+
return new Promise((resolve, reject) => {
|
|
1405
|
+
const onAbort = () => {
|
|
1406
|
+
clearTimeout(timer);
|
|
1407
|
+
reject(new DOMException("retry backoff aborted", "AbortError"));
|
|
1408
|
+
};
|
|
1409
|
+
const timer = setTimeout(() => {
|
|
1410
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1411
|
+
resolve();
|
|
1412
|
+
}, ms);
|
|
1413
|
+
if (signal?.aborted) {
|
|
1414
|
+
onAbort();
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1258
1420
|
function combineSignalAndTimeout(signal, timeoutMsHint) {
|
|
1259
1421
|
const timeoutMs = typeof timeoutMsHint === "number" && Number.isFinite(timeoutMsHint) && timeoutMsHint > 0 ? timeoutMsHint : void 0;
|
|
1260
1422
|
if (timeoutMs === void 0) return {
|
|
@@ -1311,6 +1473,13 @@ async function mapHttpError$1(response) {
|
|
|
1311
1473
|
status: response.status,
|
|
1312
1474
|
detail
|
|
1313
1475
|
});
|
|
1476
|
+
if (response.status === 400 && detailToCode(detail)?.toUpperCase() === "INVALID_REQUEST") return new ArtifactError({
|
|
1477
|
+
code: "INVALID_REQUEST",
|
|
1478
|
+
message: `artifact API rejected the request (400)${suffix}`,
|
|
1479
|
+
retryable: false,
|
|
1480
|
+
status: response.status,
|
|
1481
|
+
detail
|
|
1482
|
+
});
|
|
1314
1483
|
return new ArtifactError({
|
|
1315
1484
|
code: "UPSTREAM_ERROR",
|
|
1316
1485
|
message: `artifact API returned HTTP ${response.status}${suffix}`,
|
|
@@ -1355,6 +1524,18 @@ function detailToMessage$1(detail) {
|
|
|
1355
1524
|
}
|
|
1356
1525
|
if (typeof record.message === "string") return record.message;
|
|
1357
1526
|
}
|
|
1527
|
+
/** Extract a machine `code` from an upstream error body (`{error:{code}}` or
|
|
1528
|
+
* `{code}`) so a tagged 400 can be classified (INVALID_REQUEST). */
|
|
1529
|
+
function detailToCode(detail) {
|
|
1530
|
+
if (typeof detail !== "object" || detail === null) return void 0;
|
|
1531
|
+
const record = detail;
|
|
1532
|
+
const error = record.error;
|
|
1533
|
+
if (typeof error === "object" && error !== null) {
|
|
1534
|
+
const code = error.code;
|
|
1535
|
+
if (typeof code === "string") return code;
|
|
1536
|
+
}
|
|
1537
|
+
if (typeof record.code === "string") return record.code;
|
|
1538
|
+
}
|
|
1358
1539
|
function isAbortLike$2(err) {
|
|
1359
1540
|
return err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
1360
1541
|
}
|
|
@@ -1383,27 +1564,86 @@ function tool(toolNameHttp, description, inputSchema, handler) {
|
|
|
1383
1564
|
};
|
|
1384
1565
|
}
|
|
1385
1566
|
const ARTIFACT_TOOLS = Object.freeze([
|
|
1386
|
-
tool("artifact_open", "Open a workspace file in ai-or-die's Artifact review panel for human review. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$2({
|
|
1567
|
+
tool("artifact_open", "Open a workspace file in ai-or-die's Artifact review panel for human review. Pass mode:\"interactive\" when the HTML carries data-aod-* action controls. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$2({
|
|
1568
|
+
file: stringProp$2("Workspace-relative or absolute file path to show in the Artifact panel."),
|
|
1569
|
+
mode: enumProp(["static", "interactive"], "Advisory. \"interactive\" signals the HTML contains data-aod-* action controls the panel should wire; \"static\" (default) is a read-and-annotate artifact.")
|
|
1570
|
+
}, ["file"]), async (args, signal) => {
|
|
1387
1571
|
const env = readArtifactEnv();
|
|
1388
1572
|
if (!env) return missingEnvResult();
|
|
1389
1573
|
const file = requiredString$2(args, "file");
|
|
1574
|
+
const mode = optionalEnum(args, "mode", ["static", "interactive"]);
|
|
1575
|
+
const response = await clientFromEnv(env).open(file, {
|
|
1576
|
+
mode,
|
|
1577
|
+
signal
|
|
1578
|
+
});
|
|
1390
1579
|
return ok$2({
|
|
1391
|
-
viewUrl:
|
|
1392
|
-
|
|
1580
|
+
viewUrl: response.viewUrl,
|
|
1581
|
+
sessionId: response.sessionId,
|
|
1582
|
+
key: response.key,
|
|
1583
|
+
next_step: "Tell the user to review at the Artifact panel, then call artifact_await to receive their feedback."
|
|
1393
1584
|
});
|
|
1394
1585
|
}),
|
|
1395
|
-
tool("
|
|
1586
|
+
tool("artifact_update", "Replace the current Artifact review's content in place. Provide EXACTLY ONE of file (a workspace file path) or html (raw HTML the server writes to the review's sandboxed file). html requires an already-open review. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$2({
|
|
1587
|
+
file: stringProp$2("Workspace-relative or absolute file path to become the review's new content."),
|
|
1588
|
+
html: stringProp$2("Raw HTML to write into the review's existing sandboxed file, then reload."),
|
|
1589
|
+
idempotencyKey: stringProp$2("Optional stable key so a retried update is de-duplicated by the server.")
|
|
1590
|
+
}, []), async (args, signal) => {
|
|
1396
1591
|
const env = readArtifactEnv();
|
|
1397
1592
|
if (!env) return missingEnvResult();
|
|
1398
|
-
|
|
1593
|
+
const file = optionalString$2(args, "file");
|
|
1594
|
+
const html = optionalString$2(args, "html");
|
|
1595
|
+
if (file === void 0 === (html === void 0)) throw new ArtifactToolInputError("INVALID_ARGUMENT", "artifact_update requires EXACTLY ONE of arguments.file or arguments.html");
|
|
1596
|
+
const idempotencyKey = optionalString$2(args, "idempotencyKey");
|
|
1597
|
+
return ok$2({
|
|
1598
|
+
...await clientFromEnv(env).update({
|
|
1599
|
+
file,
|
|
1600
|
+
html,
|
|
1601
|
+
idempotencyKey,
|
|
1602
|
+
signal
|
|
1603
|
+
}),
|
|
1604
|
+
ok: true,
|
|
1605
|
+
next_step: "The panel now shows the updated content. Call artifact_await for further feedback."
|
|
1606
|
+
});
|
|
1607
|
+
}),
|
|
1608
|
+
tool("artifact_refresh", "Force the ai-or-die Artifact panel to reload the current artifact from disk (no content change). Only works inside an ai-or-die tab-backed Claude session.", objectSchema$2({}, []), async (_args, signal) => {
|
|
1609
|
+
const env = readArtifactEnv();
|
|
1610
|
+
if (!env) return missingEnvResult();
|
|
1611
|
+
return ok$2({
|
|
1612
|
+
...await clientFromEnv(env).refresh(signal),
|
|
1613
|
+
ok: true,
|
|
1614
|
+
next_step: "The panel reloaded the artifact. Call artifact_await for feedback."
|
|
1615
|
+
});
|
|
1616
|
+
}),
|
|
1617
|
+
tool("artifact_await", "Wait for the human's next Artifact review events (typed drain: comments AND structured action-button/checkbox events) and return them with a cursor. Pass the returned cursor on the next call to receive only newer events. Supersedes artifact_poll. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$2({
|
|
1618
|
+
cursor: stringProp$2("High-water cursor from the previous artifact_await response. Omit on the first call."),
|
|
1619
|
+
timeoutMs: numberProp$2("Optional server long-hold budget in ms (default ~25000).")
|
|
1620
|
+
}, []), async (args, signal) => {
|
|
1621
|
+
const env = readArtifactEnv();
|
|
1622
|
+
if (!env) return missingEnvResult();
|
|
1623
|
+
const cursor = optionalString$2(args, "cursor");
|
|
1624
|
+
const timeoutMs = optionalNumber$2(args, "timeoutMs");
|
|
1625
|
+
return ok$2(formatAwaitResponse(await clientFromEnv(env).awaitEvents({
|
|
1626
|
+
cursor,
|
|
1627
|
+
timeoutMs,
|
|
1628
|
+
signal
|
|
1629
|
+
})));
|
|
1630
|
+
}),
|
|
1631
|
+
tool("artifact_dismiss", "Hide the ai-or-die Artifact panel UI while keeping the review alive (queued feedback preserved, channel open, re-openable). Only works inside an ai-or-die tab-backed Claude session.", objectSchema$2({}, []), async (_args, signal) => {
|
|
1632
|
+
const env = readArtifactEnv();
|
|
1633
|
+
if (!env) return missingEnvResult();
|
|
1634
|
+
return ok$2({
|
|
1635
|
+
...await clientFromEnv(env).dismiss(signal),
|
|
1636
|
+
ok: true,
|
|
1637
|
+
next_step: "The panel is hidden but the review is still live. Re-open the artifact or call artifact_await when ready."
|
|
1638
|
+
});
|
|
1399
1639
|
}),
|
|
1400
1640
|
tool("artifact_reply", "Send the agent's reply back to the ai-or-die Artifact review panel after applying or responding to human feedback. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$2({ text: stringProp$2("Agent reply text to deliver to the human Artifact review panel.") }, ["text"]), async (args, signal) => {
|
|
1401
1641
|
const env = readArtifactEnv();
|
|
1402
1642
|
if (!env) return missingEnvResult();
|
|
1403
1643
|
const text = requiredString$2(args, "text");
|
|
1404
1644
|
return ok$2({
|
|
1405
|
-
ok: true,
|
|
1406
1645
|
...await clientFromEnv(env).agentReply(text, signal),
|
|
1646
|
+
ok: true,
|
|
1407
1647
|
next_step: "Wait for further human review, or continue if the review loop is complete."
|
|
1408
1648
|
});
|
|
1409
1649
|
}),
|
|
@@ -1411,10 +1651,15 @@ const ARTIFACT_TOOLS = Object.freeze([
|
|
|
1411
1651
|
const env = readArtifactEnv();
|
|
1412
1652
|
if (!env) return missingEnvResult();
|
|
1413
1653
|
return ok$2({
|
|
1414
|
-
ok: true,
|
|
1415
1654
|
...await clientFromEnv(env).end(signal),
|
|
1655
|
+
ok: true,
|
|
1416
1656
|
next_step: "Artifact review loop ended."
|
|
1417
1657
|
});
|
|
1658
|
+
}),
|
|
1659
|
+
tool("artifact_poll", "FROZEN legacy alias for artifact_await (old payload, human comments only, no structured actions). New agents should call artifact_await instead. Only works inside an ai-or-die tab-backed Claude session.", objectSchema$2({ timeoutMs: numberProp$2("Optional per-call budget hint in ms (advisory).") }, []), async (_args, signal) => {
|
|
1660
|
+
const env = readArtifactEnv();
|
|
1661
|
+
if (!env) return missingEnvResult();
|
|
1662
|
+
return ok$2(formatPollResponse(await pollUntilReady(clientFromEnv(env), signal)));
|
|
1418
1663
|
})
|
|
1419
1664
|
]);
|
|
1420
1665
|
function readArtifactEnv() {
|
|
@@ -1489,6 +1734,30 @@ function formatPollResponse(response) {
|
|
|
1489
1734
|
next_step: response.next_step ?? defaultPollNextStep(response.status)
|
|
1490
1735
|
});
|
|
1491
1736
|
}
|
|
1737
|
+
/**
|
|
1738
|
+
* Shape the typed drain for the model: pass events through verbatim (unknown
|
|
1739
|
+
* `kind`s preserved — the model ignores what it does not understand), echo the
|
|
1740
|
+
* cursor to thread into the next call, and pick a next_step from the events.
|
|
1741
|
+
*/
|
|
1742
|
+
function formatAwaitResponse(response) {
|
|
1743
|
+
const events$1 = Array.isArray(response.events) ? response.events : [];
|
|
1744
|
+
const status = typeof response.status === "string" ? response.status : void 0;
|
|
1745
|
+
return definedObject$1({
|
|
1746
|
+
events: events$1,
|
|
1747
|
+
status,
|
|
1748
|
+
cursor: response.cursor,
|
|
1749
|
+
next_step: awaitNextStep(status ?? "", events$1)
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
function awaitNextStep(status, events$1) {
|
|
1753
|
+
if ((status ?? "").toLowerCase() === "ended") return "The review has ended. No further feedback will arrive.";
|
|
1754
|
+
if (events$1.length === 0) return "No feedback yet. Call artifact_await again, passing the returned cursor.";
|
|
1755
|
+
const hasAction = events$1.some((e) => e.kind === "action");
|
|
1756
|
+
const hasComment = events$1.some((e) => e.kind === "comment");
|
|
1757
|
+
if (hasAction && hasComment) return "Act on the action events (buttons/checkboxes) and the comments, reply with artifact_reply, then call artifact_await with the returned cursor.";
|
|
1758
|
+
if (hasAction) return "The human triggered action controls. Act on them, optionally artifact_reply, then call artifact_await with the returned cursor.";
|
|
1759
|
+
return "Apply the human comments, call artifact_reply with a concise summary, then artifact_await with the returned cursor.";
|
|
1760
|
+
}
|
|
1492
1761
|
function defaultPollNextStep(status) {
|
|
1493
1762
|
return isWaitingStatus(status) ? "No human feedback is ready yet. Call artifact_poll again." : "Apply the human Artifact review feedback, then call artifact_reply with a concise summary.";
|
|
1494
1763
|
}
|
|
@@ -1501,6 +1770,24 @@ function requiredString$2(args, key) {
|
|
|
1501
1770
|
if (typeof value !== "string" || value.trim() === "") throw new ArtifactToolInputError("INVALID_ARGUMENT", `arguments.${key} is required and must be a non-empty string`);
|
|
1502
1771
|
return value;
|
|
1503
1772
|
}
|
|
1773
|
+
function optionalString$2(args, key) {
|
|
1774
|
+
const value = args[key];
|
|
1775
|
+
if (value === void 0 || value === null) return void 0;
|
|
1776
|
+
if (typeof value !== "string" || value.trim() === "") throw new ArtifactToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a non-empty string when provided`);
|
|
1777
|
+
return value;
|
|
1778
|
+
}
|
|
1779
|
+
function optionalNumber$2(args, key) {
|
|
1780
|
+
const value = args[key];
|
|
1781
|
+
if (value === void 0 || value === null) return void 0;
|
|
1782
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) throw new ArtifactToolInputError("INVALID_ARGUMENT", `arguments.${key} must be a positive number when provided`);
|
|
1783
|
+
return value;
|
|
1784
|
+
}
|
|
1785
|
+
function optionalEnum(args, key, allowed) {
|
|
1786
|
+
const value = optionalString$2(args, key);
|
|
1787
|
+
if (value === void 0) return void 0;
|
|
1788
|
+
if (!allowed.includes(value)) throw new ArtifactToolInputError("INVALID_ARGUMENT", `arguments.${key} must be one of ${allowed.join(", ")}`);
|
|
1789
|
+
return value;
|
|
1790
|
+
}
|
|
1504
1791
|
var ArtifactToolInputError = class extends Error {
|
|
1505
1792
|
code;
|
|
1506
1793
|
constructor(code, message) {
|
|
@@ -1565,6 +1852,19 @@ function stringProp$2(description) {
|
|
|
1565
1852
|
description
|
|
1566
1853
|
};
|
|
1567
1854
|
}
|
|
1855
|
+
function numberProp$2(description) {
|
|
1856
|
+
return {
|
|
1857
|
+
type: "number",
|
|
1858
|
+
description
|
|
1859
|
+
};
|
|
1860
|
+
}
|
|
1861
|
+
function enumProp(values, description) {
|
|
1862
|
+
return {
|
|
1863
|
+
type: "string",
|
|
1864
|
+
enum: [...values],
|
|
1865
|
+
description
|
|
1866
|
+
};
|
|
1867
|
+
}
|
|
1568
1868
|
|
|
1569
1869
|
//#endregion
|
|
1570
1870
|
//#region src/lib/fleet/mesh-egress-agent.ts
|
|
@@ -2673,6 +2973,464 @@ var MergedFleetRegistry = class {
|
|
|
2673
2973
|
}
|
|
2674
2974
|
};
|
|
2675
2975
|
|
|
2976
|
+
//#endregion
|
|
2977
|
+
//#region src/lib/fleet/driver.ts
|
|
2978
|
+
/** ai-or-die named keys (raw:false maps these; never a literal "\r"). C4. */
|
|
2979
|
+
const SUBMIT_KEY = "enter";
|
|
2980
|
+
const INTERRUPT_KEY = "ctrl-c";
|
|
2981
|
+
/** The control events that RELIABLY mark a turn boundary (transcript-derived). */
|
|
2982
|
+
const TURN_SETTLE_KINDS = ["turn_ended", "waiting_input"];
|
|
2983
|
+
const DEFAULT_READY_POLL_MS = 500;
|
|
2984
|
+
const DEFAULT_TURN_POLL_MS = 25e3;
|
|
2985
|
+
const DEFAULT_PRIME_TIMEOUT_MS = 0;
|
|
2986
|
+
const DEFAULT_IDLE_WAIT_MS = 2e3;
|
|
2987
|
+
const DEFAULT_RECOVER_MS = 15e3;
|
|
2988
|
+
const DEFAULT_TAIL_LINES = 200;
|
|
2989
|
+
function realSleep(ms) {
|
|
2990
|
+
if (ms <= 0) return Promise.resolve();
|
|
2991
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2992
|
+
}
|
|
2993
|
+
/** Map a named op to the ai-or-die named key. `submit` = Enter, `interrupt` = Ctrl-C.
|
|
2994
|
+
* Callers send the returned key with raw:false so ai-or-die interprets the NAMED
|
|
2995
|
+
* key — never a literal control byte. */
|
|
2996
|
+
function mapNamedKeyOp(op) {
|
|
2997
|
+
switch (op) {
|
|
2998
|
+
case "submit": return SUBMIT_KEY;
|
|
2999
|
+
case "interrupt": return INTERRUPT_KEY;
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
function isNamedKeyOp(value) {
|
|
3003
|
+
return value === "submit" || value === "interrupt";
|
|
3004
|
+
}
|
|
3005
|
+
/** Read `awaiting.kind` off a status defensively (the shape is `{ kind, ... }`). */
|
|
3006
|
+
function readAwaitingKind(status) {
|
|
3007
|
+
const awaiting = status?.awaiting;
|
|
3008
|
+
if (awaiting && typeof awaiting === "object") {
|
|
3009
|
+
const kind = awaiting.kind;
|
|
3010
|
+
if (typeof kind === "string" && kind.trim() !== "") return kind;
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
/**
|
|
3014
|
+
* Classify whether a free-text message may be submitted to a session RIGHT NOW.
|
|
3015
|
+
* Ready only when the session is idle or explicitly awaiting the next message.
|
|
3016
|
+
* A pending non-message prompt (plan_approval / choice_question / tool_approval /
|
|
3017
|
+
* trust_prompt) is `awaiting_other` — the caller should use `respond`, not a raw
|
|
3018
|
+
* message. `busy` / `terminal` are hard refusals; `unknown` carries no positive
|
|
3019
|
+
* evidence of a busy composer (the caller fails OPEN and lets the send surface its
|
|
3020
|
+
* own transport result).
|
|
3021
|
+
*/
|
|
3022
|
+
function classifyMessageReadiness(status) {
|
|
3023
|
+
const interactionState = typeof status?.interactionState === "string" ? status.interactionState : void 0;
|
|
3024
|
+
const awaitingKind = readAwaitingKind(status);
|
|
3025
|
+
if (interactionState === "busy") return {
|
|
3026
|
+
ready: false,
|
|
3027
|
+
reason: "busy",
|
|
3028
|
+
interactionState,
|
|
3029
|
+
awaitingKind
|
|
3030
|
+
};
|
|
3031
|
+
if (interactionState === "exited" || interactionState === "crashed") return {
|
|
3032
|
+
ready: false,
|
|
3033
|
+
reason: "terminal",
|
|
3034
|
+
interactionState
|
|
3035
|
+
};
|
|
3036
|
+
if (awaitingKind !== void 0 && awaitingKind !== "next_message") return {
|
|
3037
|
+
ready: false,
|
|
3038
|
+
reason: "awaiting_other",
|
|
3039
|
+
interactionState,
|
|
3040
|
+
awaitingKind
|
|
3041
|
+
};
|
|
3042
|
+
if (interactionState === "idle") return {
|
|
3043
|
+
ready: true,
|
|
3044
|
+
reason: "idle",
|
|
3045
|
+
interactionState
|
|
3046
|
+
};
|
|
3047
|
+
if (awaitingKind === "next_message" || interactionState === "waiting_input") return {
|
|
3048
|
+
ready: true,
|
|
3049
|
+
reason: "awaiting_message",
|
|
3050
|
+
interactionState,
|
|
3051
|
+
awaitingKind
|
|
3052
|
+
};
|
|
3053
|
+
return {
|
|
3054
|
+
ready: false,
|
|
3055
|
+
reason: "unknown",
|
|
3056
|
+
interactionState,
|
|
3057
|
+
awaitingKind
|
|
3058
|
+
};
|
|
3059
|
+
}
|
|
3060
|
+
/** A refusal reason with POSITIVE evidence the composer must not be typed into. */
|
|
3061
|
+
function isHardNotReady(reason) {
|
|
3062
|
+
return reason === "busy" || reason === "awaiting_other" || reason === "terminal";
|
|
3063
|
+
}
|
|
3064
|
+
/**
|
|
3065
|
+
* Poll `/status` until the session is ready for a message or the wait budget is
|
|
3066
|
+
* spent. A status probe that THROWS fails OPEN (reports `unknown` and returns) so a
|
|
3067
|
+
* transient status hiccup never wedges a legitimate send — the send itself carries
|
|
3068
|
+
* ai-or-die's own submission signal.
|
|
3069
|
+
*/
|
|
3070
|
+
async function waitForMessageReady(client, localId, options = {}) {
|
|
3071
|
+
const now = options.now ?? Date.now;
|
|
3072
|
+
const sleep$2 = options.sleep ?? realSleep;
|
|
3073
|
+
const waitMs = Math.max(0, options.waitMs ?? 0);
|
|
3074
|
+
const pollMs = Math.max(1, options.pollMs ?? DEFAULT_READY_POLL_MS);
|
|
3075
|
+
const deadline = now() + waitMs;
|
|
3076
|
+
let last = {
|
|
3077
|
+
ready: false,
|
|
3078
|
+
reason: "unknown"
|
|
3079
|
+
};
|
|
3080
|
+
for (;;) {
|
|
3081
|
+
let status;
|
|
3082
|
+
try {
|
|
3083
|
+
status = (await client.status(localId, options.signal)).status;
|
|
3084
|
+
} catch {
|
|
3085
|
+
return {
|
|
3086
|
+
ready: false,
|
|
3087
|
+
readiness: {
|
|
3088
|
+
ready: false,
|
|
3089
|
+
reason: "unknown"
|
|
3090
|
+
},
|
|
3091
|
+
statusError: true
|
|
3092
|
+
};
|
|
3093
|
+
}
|
|
3094
|
+
last = classifyMessageReadiness(status);
|
|
3095
|
+
if (last.ready) return {
|
|
3096
|
+
ready: true,
|
|
3097
|
+
readiness: last
|
|
3098
|
+
};
|
|
3099
|
+
const remaining = deadline - now();
|
|
3100
|
+
if (remaining <= 0) return {
|
|
3101
|
+
ready: false,
|
|
3102
|
+
readiness: last
|
|
3103
|
+
};
|
|
3104
|
+
await sleep$2(Math.min(pollMs, remaining));
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
/**
|
|
3108
|
+
* Classify a batch of already-stamped events per session for the `await_turn`
|
|
3109
|
+
* summary. `turn_ended` -> completed, `waiting_input` -> awaiting_input (both
|
|
3110
|
+
* reliable / transcript-derived); a bare `became_idle` is surfaced as `idle_flicker`
|
|
3111
|
+
* with reliable:false so a caller NEVER mistakes the PTY heuristic for completion.
|
|
3112
|
+
* `became_busy` and other kinds are ignored (not a settle signal).
|
|
3113
|
+
*/
|
|
3114
|
+
function classifyTurnEvents(events$1) {
|
|
3115
|
+
const rank = {
|
|
3116
|
+
completed: 3,
|
|
3117
|
+
awaiting_input: 2,
|
|
3118
|
+
idle_flicker: 1
|
|
3119
|
+
};
|
|
3120
|
+
const best = /* @__PURE__ */ new Map();
|
|
3121
|
+
for (const event of events$1) {
|
|
3122
|
+
const sessionId = typeof event.sessionId === "string" ? event.sessionId : void 0;
|
|
3123
|
+
const kind = typeof event.kind === "string" ? event.kind : void 0;
|
|
3124
|
+
if (sessionId === void 0 || kind === void 0) continue;
|
|
3125
|
+
let status;
|
|
3126
|
+
if (kind === "turn_ended") status = "completed";
|
|
3127
|
+
else if (kind === "waiting_input") status = "awaiting_input";
|
|
3128
|
+
else if (kind === "became_idle") status = "idle_flicker";
|
|
3129
|
+
if (status === void 0) continue;
|
|
3130
|
+
const prior = best.get(sessionId);
|
|
3131
|
+
if (prior === void 0 || rank[status] > rank[prior]) best.set(sessionId, status);
|
|
3132
|
+
}
|
|
3133
|
+
return [...best.entries()].map(([sessionId, status]) => ({
|
|
3134
|
+
sessionId,
|
|
3135
|
+
status,
|
|
3136
|
+
reliable: status !== "idle_flicker"
|
|
3137
|
+
}));
|
|
3138
|
+
}
|
|
3139
|
+
function pickSettleEvent(events$1, localId) {
|
|
3140
|
+
let awaiting;
|
|
3141
|
+
for (const event of events$1) {
|
|
3142
|
+
if (event.sessionId !== localId) continue;
|
|
3143
|
+
if (event.kind === "turn_ended") return event;
|
|
3144
|
+
if (event.kind === "waiting_input" && awaiting === void 0) awaiting = event;
|
|
3145
|
+
}
|
|
3146
|
+
return awaiting;
|
|
3147
|
+
}
|
|
3148
|
+
/** Client-side backoff after a transient waitEvents failure so the loop cannot
|
|
3149
|
+
* hot-spin (hammer the control plane) when polls fail fast (network down). */
|
|
3150
|
+
const TURN_POLL_ERROR_BACKOFF_MS = 250;
|
|
3151
|
+
/** Attempts to obtain a starting cursor before a drive send (see driveTask step 2). */
|
|
3152
|
+
const PRIME_CURSOR_ATTEMPTS = 3;
|
|
3153
|
+
/**
|
|
3154
|
+
* Obtain a starting `/events` cursor before sending, retrying a bounded number of
|
|
3155
|
+
* times. A cursor is what prevents a stale prior-turn `turn_ended` from satisfying
|
|
3156
|
+
* the post-send wait; a cursorless request may replay history. Retries are cheap
|
|
3157
|
+
* (a zero/short poll). Returns undefined only if every attempt fails (rare) or the
|
|
3158
|
+
* signal aborts — the caller then proceeds best-effort.
|
|
3159
|
+
*/
|
|
3160
|
+
async function primeTurnCursor(client, localId, timeoutMs, signal) {
|
|
3161
|
+
for (let attempt = 0; attempt < PRIME_CURSOR_ATTEMPTS; attempt++) {
|
|
3162
|
+
if (signal?.aborted) return void 0;
|
|
3163
|
+
try {
|
|
3164
|
+
return (await client.waitEvents({
|
|
3165
|
+
sessionIds: [localId],
|
|
3166
|
+
kinds: [...TURN_SETTLE_KINDS],
|
|
3167
|
+
timeoutMs
|
|
3168
|
+
}, signal)).cursor;
|
|
3169
|
+
} catch {}
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
/**
|
|
3173
|
+
* Wait until the CURRENT turn actually ends (`turn_ended`) or the session is
|
|
3174
|
+
* awaiting input (`waiting_input`), long-polling `/events` filtered to those kinds
|
|
3175
|
+
* and advancing the server cursor across windows. Returns `{settled:false,
|
|
3176
|
+
* reason:"timeout"}` when neither fires within `timeoutMs`. It NEVER settles on
|
|
3177
|
+
* `became_idle`. Prime a cursor BEFORE the send (a zero/short poll) and pass it in
|
|
3178
|
+
* so a stale prior-turn event cannot satisfy the wait.
|
|
3179
|
+
*/
|
|
3180
|
+
async function waitForTurnSettled(client, localId, options) {
|
|
3181
|
+
const now = options.now ?? Date.now;
|
|
3182
|
+
const sleep$2 = options.sleep ?? realSleep;
|
|
3183
|
+
const pollTimeoutMs = Math.max(1, options.pollTimeoutMs ?? DEFAULT_TURN_POLL_MS);
|
|
3184
|
+
const budget = Number.isFinite(options.timeoutMs) ? Math.max(0, options.timeoutMs) : 0;
|
|
3185
|
+
const deadline = now() + budget;
|
|
3186
|
+
let cursor = options.cursor;
|
|
3187
|
+
do {
|
|
3188
|
+
if (options.signal?.aborted) return {
|
|
3189
|
+
settled: false,
|
|
3190
|
+
reason: "aborted",
|
|
3191
|
+
cursor
|
|
3192
|
+
};
|
|
3193
|
+
const remaining = deadline - now();
|
|
3194
|
+
const poll = remaining <= 0 ? 0 : Math.min(pollTimeoutMs, remaining);
|
|
3195
|
+
let response;
|
|
3196
|
+
try {
|
|
3197
|
+
response = await client.waitEvents({
|
|
3198
|
+
sessionIds: [localId],
|
|
3199
|
+
kinds: [...TURN_SETTLE_KINDS],
|
|
3200
|
+
timeoutMs: poll,
|
|
3201
|
+
cursor
|
|
3202
|
+
}, options.signal);
|
|
3203
|
+
} catch {
|
|
3204
|
+
if (options.signal?.aborted) return {
|
|
3205
|
+
settled: false,
|
|
3206
|
+
reason: "aborted",
|
|
3207
|
+
cursor
|
|
3208
|
+
};
|
|
3209
|
+
if (now() >= deadline) return {
|
|
3210
|
+
settled: false,
|
|
3211
|
+
reason: "timeout",
|
|
3212
|
+
cursor
|
|
3213
|
+
};
|
|
3214
|
+
await sleep$2(Math.min(TURN_POLL_ERROR_BACKOFF_MS, Math.max(0, deadline - now())));
|
|
3215
|
+
continue;
|
|
3216
|
+
}
|
|
3217
|
+
cursor = response.cursor;
|
|
3218
|
+
const hit = pickSettleEvent(response.events, localId);
|
|
3219
|
+
if (hit) return {
|
|
3220
|
+
settled: true,
|
|
3221
|
+
reason: hit.kind === "turn_ended" ? "turn_ended" : "waiting_input",
|
|
3222
|
+
cursor,
|
|
3223
|
+
event: hit
|
|
3224
|
+
};
|
|
3225
|
+
} while (now() < deadline);
|
|
3226
|
+
return {
|
|
3227
|
+
settled: false,
|
|
3228
|
+
reason: "timeout",
|
|
3229
|
+
cursor
|
|
3230
|
+
};
|
|
3231
|
+
}
|
|
3232
|
+
const OPERATOR_REPORT_HEADER = "=== OPERATOR REPORT ===";
|
|
3233
|
+
const OPERATOR_REPORT_FOOTER = "=== END OPERATOR REPORT ===";
|
|
3234
|
+
const OPERATOR_REPORT_LABELS = [
|
|
3235
|
+
"REPORT_ID",
|
|
3236
|
+
"STATE",
|
|
3237
|
+
"SUMMARY",
|
|
3238
|
+
"ASK",
|
|
3239
|
+
"ARTIFACT"
|
|
3240
|
+
];
|
|
3241
|
+
/**
|
|
3242
|
+
* Parse the LAST OPERATOR REPORT trailer in a transcript tail into typed fields.
|
|
3243
|
+
* Robust to: no trailer (returns state:"unknown", found:false), a missing footer,
|
|
3244
|
+
* multi-line field values (a value runs until the next known label or the footer),
|
|
3245
|
+
* and case-insensitive labels. Lines before the first label are ignored.
|
|
3246
|
+
*/
|
|
3247
|
+
function parseOperatorReport(text) {
|
|
3248
|
+
const raw = text ?? "";
|
|
3249
|
+
const headerIdx = raw.lastIndexOf(OPERATOR_REPORT_HEADER);
|
|
3250
|
+
if (headerIdx === -1) return {
|
|
3251
|
+
state: "unknown",
|
|
3252
|
+
raw,
|
|
3253
|
+
found: false
|
|
3254
|
+
};
|
|
3255
|
+
let block = raw.slice(headerIdx + 23);
|
|
3256
|
+
const footerIdx = block.indexOf(OPERATOR_REPORT_FOOTER);
|
|
3257
|
+
if (footerIdx !== -1) block = block.slice(0, footerIdx);
|
|
3258
|
+
const values = {};
|
|
3259
|
+
let current;
|
|
3260
|
+
for (const line of block.split(/\r?\n/)) {
|
|
3261
|
+
const match = /^\s*([A-Za-z_]+)\s*:\s*(.*)$/.exec(line);
|
|
3262
|
+
const label = match?.[1]?.toUpperCase();
|
|
3263
|
+
if (match && label && OPERATOR_REPORT_LABELS.includes(label)) {
|
|
3264
|
+
current = label;
|
|
3265
|
+
values[current] = [match[2] ?? ""];
|
|
3266
|
+
} else if (current) values[current].push(line);
|
|
3267
|
+
}
|
|
3268
|
+
const join$1 = (key) => {
|
|
3269
|
+
const parts = values[key];
|
|
3270
|
+
if (parts === void 0) return void 0;
|
|
3271
|
+
const joined = parts.join("\n").trim();
|
|
3272
|
+
if (joined === "") return void 0;
|
|
3273
|
+
if (/^<[^>\n]*\s[^>\n]*>$/.test(joined)) return void 0;
|
|
3274
|
+
return joined;
|
|
3275
|
+
};
|
|
3276
|
+
return {
|
|
3277
|
+
state: join$1("STATE") ?? "unknown",
|
|
3278
|
+
summary: join$1("SUMMARY"),
|
|
3279
|
+
ask: join$1("ASK"),
|
|
3280
|
+
artifact: join$1("ARTIFACT"),
|
|
3281
|
+
reportId: join$1("REPORT_ID"),
|
|
3282
|
+
raw,
|
|
3283
|
+
found: true
|
|
3284
|
+
};
|
|
3285
|
+
}
|
|
3286
|
+
/** The trailer instruction `drive_task` appends when `expectReport` is on, so a
|
|
3287
|
+
* driven session ends its turn with a parseable {@link parseOperatorReport} block.
|
|
3288
|
+
* When `reportId` is supplied it is embedded as a `REPORT_ID` line the session must
|
|
3289
|
+
* copy verbatim, so the driver can confirm the parsed report is for THIS turn and not
|
|
3290
|
+
* a stale prior-turn trailer still inside the transcript tail window. */
|
|
3291
|
+
function operatorReportInstruction(reportId) {
|
|
3292
|
+
const lines = [
|
|
3293
|
+
"",
|
|
3294
|
+
reportId !== void 0 ? "When you have completely finished this task, end your FINAL message with EXACTLY this trailer. Copy the REPORT_ID line verbatim and fill in each other field:" : "When you have completely finished this task, end your FINAL message with EXACTLY this trailer, filling in each field:",
|
|
3295
|
+
OPERATOR_REPORT_HEADER
|
|
3296
|
+
];
|
|
3297
|
+
if (reportId !== void 0) lines.push(`REPORT_ID: ${reportId}`);
|
|
3298
|
+
lines.push("STATE: <done | blocked | needs_input | in_progress>", "SUMMARY: <1-3 sentence summary of what you did>", "ASK: <what you need from the operator, or 'none'>", "ARTIFACT: <path or URL to the primary artifact, or 'none'>", OPERATOR_REPORT_FOOTER);
|
|
3299
|
+
return lines.join("\n");
|
|
3300
|
+
}
|
|
3301
|
+
function notReadyState(reason) {
|
|
3302
|
+
switch (reason) {
|
|
3303
|
+
case "busy": return "busy";
|
|
3304
|
+
case "awaiting_other": return "awaiting_other";
|
|
3305
|
+
case "terminal": return "dead";
|
|
3306
|
+
default: return "unknown";
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
async function readTail(client, localId, lines, signal) {
|
|
3310
|
+
try {
|
|
3311
|
+
const response = await client.readSession(localId, lines, signal);
|
|
3312
|
+
return typeof response.text === "string" ? response.text : "";
|
|
3313
|
+
} catch {
|
|
3314
|
+
return "";
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
/**
|
|
3318
|
+
* Drive one prompt on a session to completion and return the parsed operator report.
|
|
3319
|
+
* Flow: ensure the composer is idle (C1) -> send + surface whether the bytes reached
|
|
3320
|
+
* the composer (C2 `submitted`; a delivered-but-unconfirmed send still proceeds, since
|
|
3321
|
+
* the turn wait + timeout recovery covers the "nothing landed" case) -> wait for
|
|
3322
|
+
* `turn_ended`/`waiting_input` (C2) -> read the transcript tail -> parse the operator
|
|
3323
|
+
* report trailer -> if the turn did not settle in `timeoutMs`, AUTO-RECOVER via a
|
|
3324
|
+
* Ctrl-C interrupt (C4) rather than blocking (~10 min stop-hook hang), then re-wait
|
|
3325
|
+
* briefly and re-read. Robust to a busy session, a missing trailer (state:"unknown"),
|
|
3326
|
+
* and a hung stop hook.
|
|
3327
|
+
*/
|
|
3328
|
+
async function driveTask(deps) {
|
|
3329
|
+
const { client, localId, prompt, timeoutMs, expectReport, idempotencyKey, interruptKey, reportId, signal } = deps;
|
|
3330
|
+
const now = deps.now ?? Date.now;
|
|
3331
|
+
const sleep$2 = deps.sleep ?? realSleep;
|
|
3332
|
+
const tailLines = deps.tailLines ?? DEFAULT_TAIL_LINES;
|
|
3333
|
+
const pollTimeoutMs = deps.pollTimeoutMs;
|
|
3334
|
+
const readyResult = await waitForMessageReady(client, localId, {
|
|
3335
|
+
waitMs: deps.idleWaitMs ?? DEFAULT_IDLE_WAIT_MS,
|
|
3336
|
+
now,
|
|
3337
|
+
sleep: sleep$2,
|
|
3338
|
+
signal
|
|
3339
|
+
});
|
|
3340
|
+
if (!readyResult.ready && isHardNotReady(readyResult.readiness.reason)) return {
|
|
3341
|
+
submitted: false,
|
|
3342
|
+
delivered: false,
|
|
3343
|
+
settled: "timeout",
|
|
3344
|
+
state: notReadyState(readyResult.readiness.reason),
|
|
3345
|
+
raw: "",
|
|
3346
|
+
reportFound: false,
|
|
3347
|
+
interrupted: false,
|
|
3348
|
+
recovered: false,
|
|
3349
|
+
notReady: true,
|
|
3350
|
+
readiness: readyResult.readiness,
|
|
3351
|
+
error: "not_ready"
|
|
3352
|
+
};
|
|
3353
|
+
let cursor = await primeTurnCursor(client, localId, deps.primeTimeoutMs ?? DEFAULT_PRIME_TIMEOUT_MS, signal);
|
|
3354
|
+
const cursorPrimed = cursor !== void 0;
|
|
3355
|
+
const message = expectReport ? `${prompt}\n${operatorReportInstruction(reportId)}` : prompt;
|
|
3356
|
+
const send = await client.sendMessage(localId, {
|
|
3357
|
+
message,
|
|
3358
|
+
idempotencyKey,
|
|
3359
|
+
awaitMs: 0
|
|
3360
|
+
}, signal);
|
|
3361
|
+
const submitted = send.submission?.status === "submitted";
|
|
3362
|
+
if (send.delivered === false || send.delivery?.status === "failed" || send.delivery?.status === "error") return {
|
|
3363
|
+
submitted: false,
|
|
3364
|
+
delivered: false,
|
|
3365
|
+
settled: "timeout",
|
|
3366
|
+
state: "send_failed",
|
|
3367
|
+
raw: "",
|
|
3368
|
+
reportFound: false,
|
|
3369
|
+
interrupted: false,
|
|
3370
|
+
recovered: false,
|
|
3371
|
+
cursorPrimed,
|
|
3372
|
+
sendConfirmation: send.confirmation,
|
|
3373
|
+
error: "delivery_failed"
|
|
3374
|
+
};
|
|
3375
|
+
let settle = await waitForTurnSettled(client, localId, {
|
|
3376
|
+
timeoutMs,
|
|
3377
|
+
pollTimeoutMs,
|
|
3378
|
+
cursor,
|
|
3379
|
+
now,
|
|
3380
|
+
sleep: sleep$2,
|
|
3381
|
+
signal
|
|
3382
|
+
});
|
|
3383
|
+
cursor = settle.cursor;
|
|
3384
|
+
const isCurrentReport = (r) => expectReport && r.found && r.reportId !== void 0 && r.reportId === reportId;
|
|
3385
|
+
let tail = signal?.aborted ? "" : await readTail(client, localId, tailLines, signal);
|
|
3386
|
+
let report = parseOperatorReport(tail);
|
|
3387
|
+
let interrupted = false;
|
|
3388
|
+
let recovered = false;
|
|
3389
|
+
const aborted = settle.reason === "aborted" || signal?.aborted === true;
|
|
3390
|
+
if (!settle.settled && !aborted) {
|
|
3391
|
+
interrupted = true;
|
|
3392
|
+
await client.sendKeys(localId, {
|
|
3393
|
+
keys: INTERRUPT_KEY,
|
|
3394
|
+
idempotencyKey: interruptKey,
|
|
3395
|
+
raw: false
|
|
3396
|
+
}, signal).catch(() => {});
|
|
3397
|
+
const recovery = await waitForTurnSettled(client, localId, {
|
|
3398
|
+
timeoutMs: deps.recoverTimeoutMs ?? DEFAULT_RECOVER_MS,
|
|
3399
|
+
pollTimeoutMs,
|
|
3400
|
+
cursor,
|
|
3401
|
+
now,
|
|
3402
|
+
sleep: sleep$2,
|
|
3403
|
+
signal
|
|
3404
|
+
});
|
|
3405
|
+
recovered = recovery.settled;
|
|
3406
|
+
if (recovery.settled) settle = recovery;
|
|
3407
|
+
const recoveredTail = await readTail(client, localId, tailLines, signal);
|
|
3408
|
+
const recoveredReport = parseOperatorReport(recoveredTail);
|
|
3409
|
+
if (recoveredTail !== "" && (isCurrentReport(recoveredReport) || !isCurrentReport(report))) {
|
|
3410
|
+
tail = recoveredTail;
|
|
3411
|
+
report = recoveredReport;
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
const settledReason = settle.settled ? settle.reason : settle.reason === "aborted" || signal?.aborted === true ? "aborted" : "timeout";
|
|
3415
|
+
const settleDerivedState = settledReason === "waiting_input" ? "awaiting_input" : settledReason === "aborted" ? "aborted" : settledReason === "timeout" ? "timeout" : "unknown";
|
|
3416
|
+
const reportCurrent = isCurrentReport(report);
|
|
3417
|
+
return {
|
|
3418
|
+
submitted,
|
|
3419
|
+
delivered: true,
|
|
3420
|
+
settled: settledReason,
|
|
3421
|
+
state: settledReason === "waiting_input" ? "awaiting_input" : reportCurrent && report.state !== "unknown" ? report.state : settleDerivedState,
|
|
3422
|
+
summary: reportCurrent ? report.summary : void 0,
|
|
3423
|
+
ask: reportCurrent ? report.ask : void 0,
|
|
3424
|
+
artifact: reportCurrent ? report.artifact : void 0,
|
|
3425
|
+
raw: tail,
|
|
3426
|
+
reportFound: reportCurrent,
|
|
3427
|
+
interrupted,
|
|
3428
|
+
recovered,
|
|
3429
|
+
cursorPrimed,
|
|
3430
|
+
sendConfirmation: send.confirmation
|
|
3431
|
+
};
|
|
3432
|
+
}
|
|
3433
|
+
|
|
2676
3434
|
//#endregion
|
|
2677
3435
|
//#region src/lib/fleet/tools.ts
|
|
2678
3436
|
const FLEET_GROUP = "fleet";
|
|
@@ -2681,6 +3439,7 @@ const INSTANCE_PROBE_CACHE_TTL_MS = 5e3;
|
|
|
2681
3439
|
const CAPABILITIES_CACHE_TTL_MS = 6e4;
|
|
2682
3440
|
const AWAIT_TURN_DEFAULT_TIMEOUT_MS = 3e4;
|
|
2683
3441
|
const AWAIT_TURN_TIMEOUT_SLACK_MS = 5e3;
|
|
3442
|
+
const DRIVE_TASK_DEFAULT_TIMEOUT_MS = 12e4;
|
|
2684
3443
|
const LIST_INSTANCES_FANOUT_CONCURRENCY = 16;
|
|
2685
3444
|
const AWAIT_TURN_FANOUT_CONCURRENCY = 256;
|
|
2686
3445
|
const INSTANCE_PROBE_RATE_LIMIT_MAX_RETRIES = 1;
|
|
@@ -2874,22 +3633,47 @@ function createFleetTools(options = {}) {
|
|
|
2874
3633
|
sessionId: globalId
|
|
2875
3634
|
});
|
|
2876
3635
|
}),
|
|
2877
|
-
tool$1("send_message", "Send a message to a fleet session.
|
|
3636
|
+
tool$1("send_message", "Send a message to a fleet session. By DEFAULT it first checks the session is idle / awaiting the next message and REFUSES (structured notReady, isError) rather than blind-type into a busy composer or a pending prompt — set requireIdle:false to force the legacy unconditional send, or waitForIdleMs to wait briefly for idle first. isError reflects DELIVERY: true when the message was not delivered (transport/precondition failure) OR refused as notReady. A delivered message whose confirmation did not arrive within awaitMs is NOT an error — it returns delivered:true with confirmationPending/confirmationTimedOut, because a long turn legitimately outruns awaitMs. The additive `submitted` field is true only when ai-or-die's submission sub-status proves the message reached the composer. Recommended pattern: send with awaitMs:0 for a fast delivery ack, then call await_turn (filtered to this sessionId) to observe the session's actual turn completion. The idempotencyKey makes a retried send safe (a retry never re-types the message).", objectSchema$1({
|
|
2878
3637
|
sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
|
|
2879
3638
|
instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2880
3639
|
message: stringProp$1("Message text to deliver to the session."),
|
|
2881
3640
|
idempotencyKey: stringProp$1("Optional caller idempotency key; AUTO-GENERATED when omitted, so you normally never pass it. Supply your OWN stable key only when you will retry the SAME send and need the upstream to dedupe it."),
|
|
2882
|
-
awaitMs: numberProp$1("Optional best-effort confirmation wait (ms) — NOT a deadline. Prefer awaitMs:0 plus await_turn; a turn that outruns awaitMs returns confirmationPending, not an error.")
|
|
3641
|
+
awaitMs: numberProp$1("Optional best-effort confirmation wait (ms) — NOT a deadline. Prefer awaitMs:0 plus await_turn; a turn that outruns awaitMs returns confirmationPending, not an error."),
|
|
3642
|
+
requireIdle: booleanProp("Default true: check status and refuse a busy/awaiting-prompt/dead session with a structured notReady result. Set false to force an unconditional send (unsafe: may type into a busy composer)."),
|
|
3643
|
+
waitForIdleMs: numberProp$1("When requireIdle, wait up to this many ms for the session to become idle before deciding (default 0 = decide immediately).")
|
|
2883
3644
|
}, ["sessionId", "message"]), async (args, signal) => {
|
|
2884
3645
|
const { instance, localId, globalId } = await resolveSession(args);
|
|
2885
3646
|
const awaitMs = optionalNumber$1(args, "awaitMs");
|
|
2886
|
-
const
|
|
3647
|
+
const requireIdle = optionalBoolean(args, "requireIdle") ?? true;
|
|
3648
|
+
const client = clientFor(instance);
|
|
3649
|
+
if (requireIdle) {
|
|
3650
|
+
const readyResult = await waitForMessageReady(client, localId, {
|
|
3651
|
+
waitMs: optionalNumber$1(args, "waitForIdleMs") ?? 0,
|
|
3652
|
+
signal
|
|
3653
|
+
});
|
|
3654
|
+
if (!readyResult.ready && isHardNotReady(readyResult.readiness.reason)) {
|
|
3655
|
+
const advise = readyResult.readiness.reason === "awaiting_other" ? " The session is awaiting a prompt — use `respond`, not a free-text message." : readyResult.readiness.reason === "terminal" ? " The session has exited." : " Wait for it to go idle (await_turn) or set requireIdle:false to force.";
|
|
3656
|
+
return jsonResult$1({
|
|
3657
|
+
resolvedInstance: publicInstance(instance),
|
|
3658
|
+
sessionId: globalId,
|
|
3659
|
+
delivered: false,
|
|
3660
|
+
submitted: false,
|
|
3661
|
+
notReady: true,
|
|
3662
|
+
reason: readyResult.readiness.reason,
|
|
3663
|
+
interactionState: readyResult.readiness.interactionState,
|
|
3664
|
+
awaitingKind: readyResult.readiness.awaitingKind,
|
|
3665
|
+
message: `not sent: session is not ready for a message (${readyResult.readiness.reason}).${advise}`
|
|
3666
|
+
}, true);
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
const response = await client.sendMessage(localId, {
|
|
2887
3670
|
message: requiredString$1(args, "message"),
|
|
2888
3671
|
idempotencyKey: optionalString$1(args, "idempotencyKey") ?? randomUUID(),
|
|
2889
3672
|
...awaitMs === void 0 ? {} : { awaitMs }
|
|
2890
3673
|
}, signal);
|
|
2891
3674
|
const delivered = !(response.delivered === false || response.delivery?.status === "failed" || response.delivery?.status === "error");
|
|
2892
3675
|
const confirmed = delivered && response.confirmed === true;
|
|
3676
|
+
const submitted = delivered && response.submission?.status === "submitted";
|
|
2893
3677
|
const confirmationTimedOut = delivered && !confirmed && (awaitMs !== void 0 && awaitMs > 0 || response.confirmationTimedOut === true);
|
|
2894
3678
|
const isError = !delivered;
|
|
2895
3679
|
return jsonResult$1({
|
|
@@ -2898,6 +3682,7 @@ function createFleetTools(options = {}) {
|
|
|
2898
3682
|
...response,
|
|
2899
3683
|
delivered,
|
|
2900
3684
|
confirmed,
|
|
3685
|
+
submitted,
|
|
2901
3686
|
...confirmationTimedOut ? {
|
|
2902
3687
|
confirmationPending: true,
|
|
2903
3688
|
confirmationTimedOut: true
|
|
@@ -2905,23 +3690,41 @@ function createFleetTools(options = {}) {
|
|
|
2905
3690
|
...isError ? { message: "message was not delivered to the session by the upstream instance" } : confirmationTimedOut ? { message: "delivered; turn completion not confirmed in the await window. Use await_turn filtered to this sessionId to observe completion (the idempotencyKey makes a retried send safe)." } : {}
|
|
2906
3691
|
}, isError);
|
|
2907
3692
|
}),
|
|
2908
|
-
tool$1("send_keys", "Send key input to a fleet session.", objectSchema$1({
|
|
3693
|
+
tool$1("send_keys", "Send key input to a fleet session. Prefer the higher-level `op`: 'submit' presses Enter and 'interrupt' sends Ctrl-C, each mapped to ai-or-die's NAMED key (never a literal control byte like \"\\r\"). Use `keys` only for literal input; `raw` is strictly for literal bytes. Provide exactly one of `op` or `keys`.", objectSchema$1({
|
|
2909
3694
|
sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
|
|
2910
3695
|
instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
2911
|
-
|
|
3696
|
+
op: stringProp$1("Higher-level named op: 'submit' (Enter) or 'interrupt' (Ctrl-C). Mapped to the ai-or-die named key with raw off. Do NOT also pass keys."),
|
|
3697
|
+
keys: stringProp$1("Literal key sequence to send. Provide instead of op."),
|
|
2912
3698
|
idempotencyKey: stringProp$1("Optional caller idempotency key; auto-generated when omitted."),
|
|
2913
|
-
raw: booleanProp("Pass keys through as raw
|
|
2914
|
-
}, ["sessionId"
|
|
3699
|
+
raw: booleanProp("Pass keys through as raw literal bytes when the instance supports it. Ignored when op is set.")
|
|
3700
|
+
}, ["sessionId"]), async (args, signal) => {
|
|
2915
3701
|
const { instance, localId, globalId } = await resolveSession(args);
|
|
2916
|
-
const
|
|
3702
|
+
const op = optionalString$1(args, "op");
|
|
3703
|
+
const literalKeys = optionalString$1(args, "keys");
|
|
3704
|
+
if (op !== void 0 && literalKeys !== void 0) throw new FleetToolInputError("INVALID_ARGUMENT", "provide either arguments.op or arguments.keys, not both");
|
|
3705
|
+
if (op === void 0 && literalKeys === void 0) throw new FleetToolInputError("INVALID_ARGUMENT", "one of arguments.op or arguments.keys is required");
|
|
3706
|
+
let keys;
|
|
3707
|
+
let raw;
|
|
3708
|
+
if (op !== void 0) {
|
|
3709
|
+
if (!isNamedKeyOp(op)) throw new FleetToolInputError("INVALID_ARGUMENT", `arguments.op must be 'submit' or 'interrupt' (got ${JSON.stringify(op)})`);
|
|
3710
|
+
keys = mapNamedKeyOp(op);
|
|
3711
|
+
raw = false;
|
|
3712
|
+
} else {
|
|
3713
|
+
keys = literalKeys;
|
|
3714
|
+
raw = optionalBoolean(args, "raw");
|
|
3715
|
+
}
|
|
2917
3716
|
const response = await clientFor(instance).sendKeys(localId, {
|
|
2918
|
-
keys
|
|
3717
|
+
keys,
|
|
2919
3718
|
idempotencyKey: optionalString$1(args, "idempotencyKey") ?? randomUUID(),
|
|
2920
3719
|
...raw === void 0 ? {} : { raw }
|
|
2921
3720
|
}, signal);
|
|
2922
3721
|
return ok$1({
|
|
2923
3722
|
resolvedInstance: publicInstance(instance),
|
|
2924
3723
|
sessionId: globalId,
|
|
3724
|
+
...op === void 0 ? {} : {
|
|
3725
|
+
op,
|
|
3726
|
+
mappedKeys: keys
|
|
3727
|
+
},
|
|
2925
3728
|
...response
|
|
2926
3729
|
});
|
|
2927
3730
|
}),
|
|
@@ -2956,15 +3759,18 @@ function createFleetTools(options = {}) {
|
|
|
2956
3759
|
start: booleanProp("Whether the remote instance should start the session immediately."),
|
|
2957
3760
|
readyTimeoutMs: numberProp$1("F17: bounded ms to wait for the agent to become driveable before returning. The response carries ready/bound/blocker."),
|
|
2958
3761
|
permissionMode: stringProp$1("F10 (claude only): permission mode the launched agent starts in — one of plan | acceptEdits | default | bypassPermissions. Rejected with BAD_REQUEST if unknown or if agentArgs also sets it."),
|
|
2959
|
-
agentArgs: arrayProp("F10 (claude only): extra launcher args appended after the github-router prefix. Must NOT include --permission-mode or --dangerously-skip-permissions (use permissionMode) — rejected with BAD_REQUEST.")
|
|
3762
|
+
agentArgs: arrayProp("F10 (claude only): extra launcher args appended after the github-router prefix. Must NOT include --permission-mode or --dangerously-skip-permissions (use permissionMode) — rejected with BAD_REQUEST."),
|
|
3763
|
+
disableStopGate: booleanProp("C3 (claude only): disable the structural Stop-gate on the launched session by injecting --no-stop-gate into agentArgs, so a driven session's turn-end never hangs on a blocking Stop hook. Requires a remote github-router that understands the flag (uses the agent_args capability).")
|
|
2960
3764
|
}, ["instance", "agent"]), async (args, signal) => {
|
|
2961
3765
|
const instance = await resolve(requiredString$1(args, "instance"));
|
|
2962
3766
|
const agent = requiredString$1(args, "agent");
|
|
2963
3767
|
const idempotencyKey = optionalString$1(args, "idempotencyKey") ?? randomUUID();
|
|
2964
3768
|
const permissionMode = optionalString$1(args, "permissionMode");
|
|
2965
|
-
const
|
|
3769
|
+
const disableStopGate = optionalBoolean(args, "disableStopGate") === true;
|
|
3770
|
+
const requestedAgentArgs = optionalStringArray(args, "agentArgs");
|
|
3771
|
+
const agentArgs = disableStopGate ? [...requestedAgentArgs ?? [], "--no-stop-gate"] : requestedAgentArgs;
|
|
2966
3772
|
if (permissionMode !== void 0) await assertCapability(instance, "permission_mode", "permissionMode", signal);
|
|
2967
|
-
if (agentArgs !== void 0) await assertCapability(instance, "agent_args", "agentArgs", signal);
|
|
3773
|
+
if (agentArgs !== void 0) await assertCapability(instance, "agent_args", disableStopGate ? "disableStopGate" : "agentArgs", signal);
|
|
2968
3774
|
const response = await clientFor(instance).createSession(definedObject({
|
|
2969
3775
|
agent,
|
|
2970
3776
|
name: optionalString$1(args, "name"),
|
|
@@ -3052,10 +3858,12 @@ function createFleetTools(options = {}) {
|
|
|
3052
3858
|
instance: publicInstance(instance),
|
|
3053
3859
|
...gap
|
|
3054
3860
|
})));
|
|
3861
|
+
const settled = classifyTurnEvents(events$1);
|
|
3055
3862
|
return ok$1({
|
|
3056
3863
|
resolvedInstances: target.instances.map(publicInstance),
|
|
3057
3864
|
events: events$1,
|
|
3058
3865
|
gaps,
|
|
3866
|
+
...settled.length > 0 ? { settled } : {},
|
|
3059
3867
|
cursors: responses.map(({ instance, response }) => ({
|
|
3060
3868
|
instance: publicInstance(instance),
|
|
3061
3869
|
cursor: response.cursor
|
|
@@ -3064,6 +3872,31 @@ function createFleetTools(options = {}) {
|
|
|
3064
3872
|
...errors.length > 0 ? { errors } : {}
|
|
3065
3873
|
});
|
|
3066
3874
|
}),
|
|
3875
|
+
tool$1("drive_task", "Drive one prompt on a session to completion and return the parsed operator report. Composes the reliable path: ensure the composer is idle (else return a structured busy/not-ready result), send and surface whether the message reached the composer (submitted; a delivered-but-unconfirmed send still proceeds), wait for the RELIABLE turn boundary (turn_ended / waiting_input — never the became_idle flicker), read the transcript tail, and parse the OPERATOR REPORT trailer into {state, summary, ask, artifact, raw}. A per-call REPORT_ID nonce is embedded in the trailer instruction and the parsed report is trusted ONLY when it echoes that nonce, so a stale prior-turn trailer left in the tail can never be returned as this turn's result. A reliable waiting_input outranks the model's self-reported STATE (a still-blocked session is never reported as done). If the turn does not end within timeoutMs (e.g. a blocking Stop hook that would otherwise hang ~10 min), it AUTO-RECOVERS with a Ctrl-C interrupt rather than blocking, then re-waits briefly and re-reads; a caller ABORT is distinct (settled:'aborted', state:'aborted') and never injects a Ctrl-C. Read `state` TOGETHER with `settled`/`interrupted`/`recovered`: state:'done' with settled:'timeout' + interrupted:true means the model reported done but the turn had to be interrupted to recover, so treat it as needs-verification rather than a clean completion; `submitted` is a best-effort positive signal that CAN be false even on a successful turn. Robust to a busy session (state:'busy'), a missing/stale/placeholder trailer (state falls back to the settle-derived value, reportFound:false), and a hung hook (interrupted:true, recovered:true/false). By default it appends the trailer instruction so the driven session emits a parseable report; set expectReport:false to send the prompt verbatim (a trailer left in the tail is then never trusted).", objectSchema$1({
|
|
3876
|
+
sessionId: stringProp$1("Global session id in the form instanceId:localSessionId."),
|
|
3877
|
+
instance: stringProp$1("Optional instance id/label; when supplied it must agree with sessionId."),
|
|
3878
|
+
prompt: stringProp$1("The task/prompt to drive on the session."),
|
|
3879
|
+
timeoutMs: numberProp$1(`Ms to wait for the turn to end before auto-recovering via interrupt (default ${DRIVE_TASK_DEFAULT_TIMEOUT_MS}). Set generously — exceeding it triggers a Ctrl-C recovery.`),
|
|
3880
|
+
expectReport: booleanProp("Default true: append the OPERATOR REPORT trailer instruction so the driven session ends its turn with a parseable {state,summary,ask,artifact}. Set false to send the prompt verbatim.")
|
|
3881
|
+
}, ["sessionId", "prompt"]), async (args, signal) => {
|
|
3882
|
+
const { instance, localId, globalId } = await resolveSession(args);
|
|
3883
|
+
const result = await driveTask({
|
|
3884
|
+
client: clientFor(instance),
|
|
3885
|
+
localId,
|
|
3886
|
+
prompt: requiredString$1(args, "prompt"),
|
|
3887
|
+
timeoutMs: optionalNumber$1(args, "timeoutMs") ?? DRIVE_TASK_DEFAULT_TIMEOUT_MS,
|
|
3888
|
+
expectReport: optionalBoolean(args, "expectReport") ?? true,
|
|
3889
|
+
idempotencyKey: randomUUID(),
|
|
3890
|
+
interruptKey: randomUUID(),
|
|
3891
|
+
reportId: randomUUID(),
|
|
3892
|
+
signal
|
|
3893
|
+
});
|
|
3894
|
+
return jsonResult$1({
|
|
3895
|
+
resolvedInstance: publicInstance(instance),
|
|
3896
|
+
sessionId: globalId,
|
|
3897
|
+
...result
|
|
3898
|
+
}, result.error !== void 0);
|
|
3899
|
+
}),
|
|
3067
3900
|
tool$1("read_file", "Read a file from one fleet instance via its existing /api/files/content endpoint.", objectSchema$1({
|
|
3068
3901
|
instance: stringProp$1("Instance id or label. Defaults to the registry default, or the sole instance."),
|
|
3069
3902
|
path: stringProp$1("Remote file path to read.")
|
|
@@ -24305,6 +25138,16 @@ function stopGateId(env = process.env) {
|
|
|
24305
25138
|
const v = (env.GH_ROUTER_STOP_GATE_ID ?? "").trim();
|
|
24306
25139
|
return v.length > 0 ? v : "default-ci";
|
|
24307
25140
|
}
|
|
25141
|
+
/**
|
|
25142
|
+
* C3: whether the structural Stop-gate is disabled for THIS launch. True when the
|
|
25143
|
+
* `GH_ROUTER_DISABLE_STOP_GATE` env is set OR the `--no-stop-gate` launcher flag was
|
|
25144
|
+
* passed (`args["no-stop-gate"] === true`). A DRIVEN session sets the flag so a
|
|
25145
|
+
* blocking Stop hook never hangs its turn-end (~10 min) waiting on the fleet driver.
|
|
25146
|
+
* Pure so it is unit-testable without the live launch path.
|
|
25147
|
+
*/
|
|
25148
|
+
function stopGateDisabled(args, env = process.env) {
|
|
25149
|
+
return parseBoolEnv(env.GH_ROUTER_DISABLE_STOP_GATE) === true || args["no-stop-gate"] === true;
|
|
25150
|
+
}
|
|
24308
25151
|
/** True when a settings `Stop` entry already registers `command` (so the merge
|
|
24309
25152
|
* is idempotent across re-launches). */
|
|
24310
25153
|
function entryHasCommand(entry, command) {
|
|
@@ -26346,5 +27189,5 @@ async function runStandInToolCall(args, signal) {
|
|
|
26346
27189
|
}
|
|
26347
27190
|
|
|
26348
27191
|
//#endregion
|
|
26349
|
-
export {
|
|
26350
|
-
//# sourceMappingURL=peer-mcp-personas-
|
|
27192
|
+
export { logStreamError as $, copilotBaseUrl as $t, BROWSE_DEFAULT_MODEL as A, UPSTREAM_FETCH_TIMEOUT_MS as At, toolbeltEnabled as B, cacheCopilotVersion as Bt, repoFingerprint as C, ArtifactClient as Ct, trustRepo as D, DEFAULT_CODEX_MODEL as Dt, stopReviewStateDir as E, DEFAULT_CLAUDE_MODEL_FALLBACKS as Et, appendPlanReminder as F, withInstallLock as Ft, searchWeb as G, resolveCodexModel as Gt, vscodeRipgrepPath as H, cacheVSCodeVersion as Ht, runWorkerAgent as I, setupCopilotToken as It, buildAdvisorStream as J, getModels as Jt, ADVISOR_INTERNAL_TOOL_NAME as K, resolveModel as Kt, withNoOutputRetry as L, setupGitHubAgentToken as Lt, IMPLEMENT_DEFAULT_MODEL as M, generateRandomPort as Mt, PLAN_DEFAULT_MODEL as N, pickClaudeDefault as Nt, resolveSealedGate as O, DEFAULT_CODEX_MODEL_FALLBACKS as Ot, REVIEW_DEFAULT_MODEL as P, getPackageVersion as Pt, isControllerClosedError as Q, GITHUB_API_BASE_URL as Qt, availableToolCommands as R, setupGitHubToken as Rt, isSubagentContext as S, shouldUseInsecureTls as St, stopGateEnabledForRepo as T, toolbeltPathOverride as Tt, TOOLBELT_TOOLS$1 as U, filterBetaHeader as Ut, toolbeltSkipSet as V, cacheModels as Vt, assetFor as W, isNullish as Wt, isAdvisorRequested as X, HTTPError as Xt, injectAdvisorTool as Y, fetchWithTransientRetry as Yt, buildOpenAIErrorEvent as Z, forwardError as Zt, stopReviewEnabled as _, provisionBrowserAssets as _t, buildPeerAwarenessSnippet as a, browserToolsEnabled as at, fileLastPromptStore as b, extractTarGzMember as bt, buildSessionBindHookCommand as c, workerToolsEnabled as ct, decideStopHook as d, getTokenCount as dt, copilotHeaders as en, readIteratorWithTimeout as et, fileBlockBudget as f, createResponses as ft, stopGateId as g, parseJsonOrDiagnose as gt, stopGateDisabled as h, readResponseBodyCapped as ht, buildAgentPrompt as i, agentToolsEnabled as it, DEFAULT_MODEL as j, UPSTREAM_INACTIVITY_TIMEOUT_MS as jt, liveExec as k, DEFAULT_PORT as kt, buildStopHookCommand as l, countTokens as lt, launchBaselineKey as m, MAX_RESPONSE_BODY_BYTES as mt, MCP_GROUPS as n, state as nn, handleMcpDelete as nt, personasFor as o, fleetToolsEnabled as ot, injectStopHookIntoSettingsFile as p, createChatCompletions as pt, ADVISOR_TOOL_INSTRUCTIONS as q, sleep as qt, assertMcpToolSurfaceConsistent as r, handleMcpPost as rt, buildArtifactOpenHookCommand as s, standInToolEnabled as st, GROUP_META as t, githubHeaders as tn, relayAnthropicStream as tt, captureLaunchBaseline as u, createMessages as ut, fileBaselineStore as v, hasSupportedBrowserInstalled as vt, repoRoot as w, collapsePathKeys as wt, fileReviewDebounce as x, extractZipMember as xt, fileFindingsStore as y, provisionAndIndexColbert as yt, buildToolbeltAwareness as z, tryRefreshAndRetry as zt };
|
|
27193
|
+
//# sourceMappingURL=peer-mcp-personas-BQVOxB1i.js.map
|