@skydiveai/pi-extensions 0.1.254-beta.42 → 0.1.254-beta.44
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/index.mjs +155 -48
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -260,7 +260,7 @@ function createHealthHandler({ metadata }) {
|
|
|
260
260
|
const CAPABILITY_SOUL_NUDGE = "New capability gained — once the current task is done, if this changes what you can do for the user, record it in `soul.md` so it carries into future conversations rather than being rediscovered from scratch (then commit and push).";
|
|
261
261
|
//#endregion
|
|
262
262
|
//#region src/extensions/tool-call-summary.ts
|
|
263
|
-
const log$
|
|
263
|
+
const log$17 = logger.child({ module: "tool-call-summary-extension" });
|
|
264
264
|
/**
|
|
265
265
|
* The injected parameter name. Deliberately markdown-safe: the previous wire
|
|
266
266
|
* name (`__skydive_summary__`, now `TOOL_CALL_SUMMARY_FIELD_LEGACY`) carried
|
|
@@ -494,7 +494,7 @@ function withSummaryStrippedBeforeValidation(tool) {
|
|
|
494
494
|
try {
|
|
495
495
|
stripped = withoutInjectedSummary(args);
|
|
496
496
|
} catch (err) {
|
|
497
|
-
log$
|
|
497
|
+
log$17.error({
|
|
498
498
|
err,
|
|
499
499
|
event: "tool_call_summary_prepare_strip_failed",
|
|
500
500
|
toolName: tool.name
|
|
@@ -525,7 +525,7 @@ function stripInjectedSummary(pi, toolName, input) {
|
|
|
525
525
|
if (toolDeclaresSummaryParam(pi, toolName)) return;
|
|
526
526
|
for (const field of TOOL_CALL_SUMMARY_FIELDS) delete input[field];
|
|
527
527
|
} catch (err) {
|
|
528
|
-
log$
|
|
528
|
+
log$17.error({
|
|
529
529
|
err,
|
|
530
530
|
event: "tool_call_summary_strip_failed",
|
|
531
531
|
toolName
|
|
@@ -541,7 +541,7 @@ function buildInjectedPayload(pi, payload) {
|
|
|
541
541
|
try {
|
|
542
542
|
return injectToolCallSummary(payload, getStrictToolNames(pi));
|
|
543
543
|
} catch (err) {
|
|
544
|
-
log$
|
|
544
|
+
log$17.error({
|
|
545
545
|
err,
|
|
546
546
|
event: "tool_call_summary_injection_failed"
|
|
547
547
|
}, "tool_call_summary injection failed; passing payload through unchanged");
|
|
@@ -560,7 +560,7 @@ const toolCallSummaryExtension = (pi) => {
|
|
|
560
560
|
pi.registerTool(withSummaryStrippedBeforeValidation(createEditToolDefinition(ctx.cwd)));
|
|
561
561
|
editOverrideRegistered = true;
|
|
562
562
|
} catch (err) {
|
|
563
|
-
log$
|
|
563
|
+
log$17.error({
|
|
564
564
|
err,
|
|
565
565
|
event: "tool_call_summary_edit_override_failed"
|
|
566
566
|
}, "failed to register summary-tolerant edit tool; built-in remains active");
|
|
@@ -591,7 +591,7 @@ const toolCallSummaryExtension = (pi) => {
|
|
|
591
591
|
* or `ToolDefinition[]`. Files starting with `_` or `.` are skipped, so
|
|
592
592
|
* `tools/_example.ts` documents the shape without registering.
|
|
593
593
|
*/
|
|
594
|
-
const log$
|
|
594
|
+
const log$16 = logger.child({ module: "local-tools-extension" });
|
|
595
595
|
const TOOLS_DIRNAME = "tools";
|
|
596
596
|
const fileState = /* @__PURE__ */ new Map();
|
|
597
597
|
let pendingLocalToolsUpdate = null;
|
|
@@ -735,7 +735,7 @@ async function reconcileAndQueue({ pi, dir, reason }) {
|
|
|
735
735
|
dir
|
|
736
736
|
});
|
|
737
737
|
if (reason !== "session_start" && summaryHasChanges$1(summary)) pendingLocalToolsUpdate = summary;
|
|
738
|
-
log$
|
|
738
|
+
log$16.info({
|
|
739
739
|
event: "local_tools_reconcile",
|
|
740
740
|
reason,
|
|
741
741
|
total_tools: summary.totalTools,
|
|
@@ -757,7 +757,7 @@ const localToolsExtension = (pi) => {
|
|
|
757
757
|
reason: "session_start"
|
|
758
758
|
});
|
|
759
759
|
} catch (err) {
|
|
760
|
-
log$
|
|
760
|
+
log$16.error({
|
|
761
761
|
err,
|
|
762
762
|
event: "local_tools_reconcile_failed"
|
|
763
763
|
}, "local tools reconcile failed");
|
|
@@ -769,7 +769,7 @@ const localToolsExtension = (pi) => {
|
|
|
769
769
|
try {
|
|
770
770
|
current = await listToolFiles(dir);
|
|
771
771
|
} catch (err) {
|
|
772
|
-
log$
|
|
772
|
+
log$16.warn({
|
|
773
773
|
err,
|
|
774
774
|
event: "local_tools_listing_failed"
|
|
775
775
|
}, "tools/ listing failed");
|
|
@@ -791,7 +791,7 @@ const localToolsExtension = (pi) => {
|
|
|
791
791
|
reason: "auto_reload"
|
|
792
792
|
});
|
|
793
793
|
} catch (err) {
|
|
794
|
-
log$
|
|
794
|
+
log$16.error({
|
|
795
795
|
err,
|
|
796
796
|
event: "local_tools_auto_reload_failed"
|
|
797
797
|
}, "auto-reload after tools/ change failed");
|
|
@@ -1356,7 +1356,7 @@ async function loadMcpConfig(path) {
|
|
|
1356
1356
|
* Clients are keyed by JSON-stringified config and reused across
|
|
1357
1357
|
* reloads — only changed configs reconnect.
|
|
1358
1358
|
*/
|
|
1359
|
-
const log$
|
|
1359
|
+
const log$15 = logger.child({ module: "mcp-extension" });
|
|
1360
1360
|
async function closeConnected(connected) {
|
|
1361
1361
|
try {
|
|
1362
1362
|
await connected.client.close();
|
|
@@ -1928,7 +1928,7 @@ var McpExtension = class {
|
|
|
1928
1928
|
});
|
|
1929
1929
|
this.lastConfigMtimeMs = await readConfigMtimeMs(configPath);
|
|
1930
1930
|
if (reason !== "session_start" && summaryHasChanges(summary)) this.pendingMcpUpdate = summary;
|
|
1931
|
-
log$
|
|
1931
|
+
log$15.info({
|
|
1932
1932
|
event: "mcp_reconcile",
|
|
1933
1933
|
reason,
|
|
1934
1934
|
total_tools: summary.totalTools,
|
|
@@ -1955,7 +1955,7 @@ var McpExtension = class {
|
|
|
1955
1955
|
contextWindow: ctx.model?.contextWindow
|
|
1956
1956
|
});
|
|
1957
1957
|
} catch (err) {
|
|
1958
|
-
log$
|
|
1958
|
+
log$15.error({
|
|
1959
1959
|
err,
|
|
1960
1960
|
event: "mcp_reconcile_failed"
|
|
1961
1961
|
}, "MCP reconcile failed");
|
|
@@ -1969,7 +1969,7 @@ var McpExtension = class {
|
|
|
1969
1969
|
for (const legacy of renamed) {
|
|
1970
1970
|
if (this.loggedLegacyToolNames.has(legacy)) continue;
|
|
1971
1971
|
this.loggedLegacyToolNames.add(legacy);
|
|
1972
|
-
log$
|
|
1972
|
+
log$15.info({
|
|
1973
1973
|
event: "mcp_tool_legacy_name",
|
|
1974
1974
|
legacy_name: legacy,
|
|
1975
1975
|
tool_name: this.legacyToolNames.get(legacy)
|
|
@@ -1983,7 +1983,7 @@ var McpExtension = class {
|
|
|
1983
1983
|
try {
|
|
1984
1984
|
mtime = await readConfigMtimeMs(configPath);
|
|
1985
1985
|
} catch (err) {
|
|
1986
|
-
log$
|
|
1986
|
+
log$15.warn({
|
|
1987
1987
|
err,
|
|
1988
1988
|
event: "mcp_mtime_check_failed"
|
|
1989
1989
|
}, "mtime check on mcp.config.json failed");
|
|
@@ -1998,7 +1998,7 @@ var McpExtension = class {
|
|
|
1998
1998
|
contextWindow: ctx.model?.contextWindow
|
|
1999
1999
|
});
|
|
2000
2000
|
} catch (err) {
|
|
2001
|
-
log$
|
|
2001
|
+
log$15.error({
|
|
2002
2002
|
err,
|
|
2003
2003
|
event: "mcp_auto_reload_failed"
|
|
2004
2004
|
}, "auto-reload after mcp.config.json change failed");
|
|
@@ -2329,7 +2329,7 @@ const HEARTBEAT_THROTTLE_MS = 6e4;
|
|
|
2329
2329
|
const TOOL_HEARTBEAT_INTERVAL_MS = 5e3;
|
|
2330
2330
|
const MAX_TOOL_HEARTBEATS = 1440 * 60 * 1e3 / TOOL_HEARTBEAT_INTERVAL_MS;
|
|
2331
2331
|
const DAEMON_URL = "http://localhost:38994";
|
|
2332
|
-
const log$
|
|
2332
|
+
const log$14 = logger.child({ module: "platform-ext" });
|
|
2333
2333
|
function sandboxClient() {
|
|
2334
2334
|
const apiUrl = apiBaseUrl();
|
|
2335
2335
|
if (!apiUrl) return null;
|
|
@@ -2397,7 +2397,7 @@ async function fetchHarnessFlags() {
|
|
|
2397
2397
|
try {
|
|
2398
2398
|
const res = await client["feature-flags"].$get();
|
|
2399
2399
|
if (!res.ok) {
|
|
2400
|
-
log$
|
|
2400
|
+
log$14.debug({
|
|
2401
2401
|
status: res.status,
|
|
2402
2402
|
event: "feature_flags_fetch_failed"
|
|
2403
2403
|
}, "feature-flags fetch failed");
|
|
@@ -2409,7 +2409,7 @@ async function fetchHarnessFlags() {
|
|
|
2409
2409
|
closingText: body.closingText ?? null
|
|
2410
2410
|
};
|
|
2411
2411
|
} catch (err) {
|
|
2412
|
-
log$
|
|
2412
|
+
log$14.debug({
|
|
2413
2413
|
err,
|
|
2414
2414
|
event: "feature_flags_fetch_error"
|
|
2415
2415
|
}, "feature-flags request errored");
|
|
@@ -2420,7 +2420,7 @@ function postHeartbeat({ messageId }) {
|
|
|
2420
2420
|
const client = sandboxClient();
|
|
2421
2421
|
if (!client) return;
|
|
2422
2422
|
client.heartbeat.$post({ json: { messageId } }).catch((err) => {
|
|
2423
|
-
log$
|
|
2423
|
+
log$14.debug({
|
|
2424
2424
|
err,
|
|
2425
2425
|
event: "heartbeat_failed"
|
|
2426
2426
|
}, "heartbeat failed");
|
|
@@ -2432,7 +2432,7 @@ async function resolveConversationFromApi(messageId) {
|
|
|
2432
2432
|
try {
|
|
2433
2433
|
const res = await client["message-conversation"].$get({ query: { messageId } });
|
|
2434
2434
|
if (!res.ok) {
|
|
2435
|
-
log$
|
|
2435
|
+
log$14.warn({
|
|
2436
2436
|
status: res.status,
|
|
2437
2437
|
messageId,
|
|
2438
2438
|
event: "resolve_conversation_failed"
|
|
@@ -2441,7 +2441,7 @@ async function resolveConversationFromApi(messageId) {
|
|
|
2441
2441
|
}
|
|
2442
2442
|
return (await res.json()).conversationId ?? null;
|
|
2443
2443
|
} catch (err) {
|
|
2444
|
-
log$
|
|
2444
|
+
log$14.warn({
|
|
2445
2445
|
err,
|
|
2446
2446
|
messageId,
|
|
2447
2447
|
event: "resolve_conversation_error"
|
|
@@ -2466,13 +2466,13 @@ async function putBackgroundTaskJournalSpec({ messageId, spec }) {
|
|
|
2466
2466
|
messageId,
|
|
2467
2467
|
spec
|
|
2468
2468
|
} });
|
|
2469
|
-
if (!res.ok) log$
|
|
2469
|
+
if (!res.ok) log$14.warn({
|
|
2470
2470
|
status: res.status,
|
|
2471
2471
|
taskId: spec.id,
|
|
2472
2472
|
event: "bg_journal_put_failed"
|
|
2473
2473
|
}, "bg-task journal PUT failed");
|
|
2474
2474
|
} catch (err) {
|
|
2475
|
-
log$
|
|
2475
|
+
log$14.warn({
|
|
2476
2476
|
err,
|
|
2477
2477
|
taskId: spec.id,
|
|
2478
2478
|
event: "bg_journal_put_failed"
|
|
@@ -2488,7 +2488,7 @@ async function deleteBackgroundTaskJournalSpec({ messageId, taskId }) {
|
|
|
2488
2488
|
taskId
|
|
2489
2489
|
} });
|
|
2490
2490
|
} catch (err) {
|
|
2491
|
-
log$
|
|
2491
|
+
log$14.debug({
|
|
2492
2492
|
err,
|
|
2493
2493
|
taskId,
|
|
2494
2494
|
event: "bg_journal_delete_failed"
|
|
@@ -2503,7 +2503,7 @@ async function listBackgroundTaskJournalSpecs({ messageId }) {
|
|
|
2503
2503
|
if (!res.ok) return [];
|
|
2504
2504
|
return (await res.json()).specs ?? [];
|
|
2505
2505
|
} catch (err) {
|
|
2506
|
-
log$
|
|
2506
|
+
log$14.debug({
|
|
2507
2507
|
err,
|
|
2508
2508
|
event: "bg_journal_list_failed"
|
|
2509
2509
|
}, "bg-task journal GET failed");
|
|
@@ -2517,7 +2517,7 @@ function postBackgroundTasksSnapshot({ messageId, tasks }) {
|
|
|
2517
2517
|
messageId,
|
|
2518
2518
|
tasks
|
|
2519
2519
|
} }).catch((err) => {
|
|
2520
|
-
log$
|
|
2520
|
+
log$14.debug({
|
|
2521
2521
|
err,
|
|
2522
2522
|
event: "bg_tasks_snapshot_failed"
|
|
2523
2523
|
}, "bg-tasks snapshot publish failed");
|
|
@@ -2615,7 +2615,7 @@ function createToolHeartbeat({ messageId }) {
|
|
|
2615
2615
|
}
|
|
2616
2616
|
heartbeatCount++;
|
|
2617
2617
|
if (heartbeatCount > MAX_TOOL_HEARTBEATS) {
|
|
2618
|
-
log$
|
|
2618
|
+
log$14.warn({
|
|
2619
2619
|
heartbeatCount,
|
|
2620
2620
|
activeToolCalls: [...activeToolCalls]
|
|
2621
2621
|
}, "tool heartbeat max reached, stopping");
|
|
@@ -2646,7 +2646,7 @@ function postToDaemon(path, body) {
|
|
|
2646
2646
|
headers: { "content-type": "application/json" },
|
|
2647
2647
|
body: JSON.stringify(body)
|
|
2648
2648
|
}).catch((err) => {
|
|
2649
|
-
log$
|
|
2649
|
+
log$14.debug({
|
|
2650
2650
|
err,
|
|
2651
2651
|
path,
|
|
2652
2652
|
event: "daemon_post_failed"
|
|
@@ -2655,7 +2655,7 @@ function postToDaemon(path, body) {
|
|
|
2655
2655
|
}
|
|
2656
2656
|
function createPlatformExtensions({ sessionId, channelContext }) {
|
|
2657
2657
|
return (pi) => {
|
|
2658
|
-
log$
|
|
2658
|
+
log$14.info({
|
|
2659
2659
|
sessionId,
|
|
2660
2660
|
hasChannelContext: Boolean(channelContext)
|
|
2661
2661
|
}, "platform extension initialized");
|
|
@@ -2704,7 +2704,7 @@ function createPlatformExtensions({ sessionId, channelContext }) {
|
|
|
2704
2704
|
});
|
|
2705
2705
|
});
|
|
2706
2706
|
pi.on("agent_end", () => {
|
|
2707
|
-
log$
|
|
2707
|
+
log$14.info({ sessionId }, "session ending");
|
|
2708
2708
|
postToDaemon("/session/end", { sessionId });
|
|
2709
2709
|
});
|
|
2710
2710
|
};
|
|
@@ -2731,7 +2731,7 @@ function createPlatformExtensions({ sessionId, channelContext }) {
|
|
|
2731
2731
|
* read on the hot path before every LLM call), it falls back to the default
|
|
2732
2732
|
* for that knob and logs once.
|
|
2733
2733
|
*/
|
|
2734
|
-
const log$
|
|
2734
|
+
const log$13 = logger.child({ module: "context-management-config" });
|
|
2735
2735
|
const DEFAULT_CONTEXT_MANAGEMENT_CONFIG = {
|
|
2736
2736
|
enabled: false,
|
|
2737
2737
|
perResultMaxBytes: 16 * 1024,
|
|
@@ -2776,7 +2776,7 @@ function resolveContextManagementConfig(env = process.env) {
|
|
|
2776
2776
|
clearAtLeastTokens: env.SKYDIVE_CTX_CLEAR_AT_LEAST_TOKENS
|
|
2777
2777
|
});
|
|
2778
2778
|
if (!parsed.success) {
|
|
2779
|
-
log$
|
|
2779
|
+
log$13.warn({
|
|
2780
2780
|
event: "context_management_config_invalid",
|
|
2781
2781
|
err: parsed.error
|
|
2782
2782
|
}, "falling back to default context-management config");
|
|
@@ -2860,7 +2860,7 @@ function hasFlagSource(env = process.env) {
|
|
|
2860
2860
|
* alive and an indeterminate result (no api url / transient failure) leaves the
|
|
2861
2861
|
* last-known values untouched so a blip can't silently flip behavior.
|
|
2862
2862
|
*/
|
|
2863
|
-
const log$
|
|
2863
|
+
const log$12 = logger.child({ module: "feature-flags-poll" });
|
|
2864
2864
|
const FLAG_POLL_INTERVAL_MS = 6e4;
|
|
2865
2865
|
let contextManagement = null;
|
|
2866
2866
|
let closingText = null;
|
|
@@ -2900,7 +2900,7 @@ function apply(name, next) {
|
|
|
2900
2900
|
if (next !== prev) for (const cb of subscribers[name]) try {
|
|
2901
2901
|
cb(next);
|
|
2902
2902
|
} catch (err) {
|
|
2903
|
-
log$
|
|
2903
|
+
log$12.warn({
|
|
2904
2904
|
err,
|
|
2905
2905
|
flag: name
|
|
2906
2906
|
}, "flag subscriber threw");
|
|
@@ -2913,7 +2913,7 @@ async function pollOnce() {
|
|
|
2913
2913
|
apply("contextManagement", flags.contextManagement ?? null);
|
|
2914
2914
|
apply("closingText", flags.closingText ?? null);
|
|
2915
2915
|
} catch (err) {
|
|
2916
|
-
log$
|
|
2916
|
+
log$12.debug({ err }, "feature-flag poll threw");
|
|
2917
2917
|
}
|
|
2918
2918
|
}
|
|
2919
2919
|
/**
|
|
@@ -3166,7 +3166,7 @@ const bashDefaultTimeoutExtension = (pi) => {
|
|
|
3166
3166
|
* turn over an observability feature.
|
|
3167
3167
|
*/
|
|
3168
3168
|
const execFileAsync = promisify(execFile);
|
|
3169
|
-
const log$
|
|
3169
|
+
const log$11 = logger.child({ module: "resource-pressure-warning" });
|
|
3170
3170
|
const POLL_INTERVAL_MS = 1e4;
|
|
3171
3171
|
function envOverride(name) {
|
|
3172
3172
|
for (const prefix of ["SKYDIVE_", "ANYONE_"]) {
|
|
@@ -3363,7 +3363,7 @@ const resourcePressureWarningExtension = (pi) => {
|
|
|
3363
3363
|
warnedDiskThisTurn = true;
|
|
3364
3364
|
}
|
|
3365
3365
|
if (trigger === null) return;
|
|
3366
|
-
log$
|
|
3366
|
+
log$11.warn({
|
|
3367
3367
|
trigger,
|
|
3368
3368
|
mem,
|
|
3369
3369
|
cpuPct,
|
|
@@ -3401,7 +3401,7 @@ const resourcePressureWarningExtension = (pi) => {
|
|
|
3401
3401
|
if (!timer) {
|
|
3402
3402
|
timer = setInterval(() => {
|
|
3403
3403
|
checkOnce().catch((err) => {
|
|
3404
|
-
log$
|
|
3404
|
+
log$11.error({ err }, "resource pressure check failed");
|
|
3405
3405
|
});
|
|
3406
3406
|
}, POLL_INTERVAL_MS);
|
|
3407
3407
|
timer.unref?.();
|
|
@@ -3417,7 +3417,7 @@ const resourcePressureWarningExtension = (pi) => {
|
|
|
3417
3417
|
};
|
|
3418
3418
|
//#endregion
|
|
3419
3419
|
//#region src/extensions/disk-guard.ts
|
|
3420
|
-
const log$
|
|
3420
|
+
const log$10 = logger.child({ module: "disk-guard" });
|
|
3421
3421
|
/**
|
|
3422
3422
|
* In-band bypass. The guard is a safety net, not a jail: when the agent knows
|
|
3423
3423
|
* a flagged command is genuinely safe (writing to a different mount, a tiny
|
|
@@ -3429,7 +3429,7 @@ const log$9 = logger.child({ module: "disk-guard" });
|
|
|
3429
3429
|
*/
|
|
3430
3430
|
const BYPASS_MARKER$1 = /#\s*disk-guard:\s*allow\b/i;
|
|
3431
3431
|
/** The exact marker text the block message tells the agent to append. */
|
|
3432
|
-
const BYPASS_HINT$
|
|
3432
|
+
const BYPASS_HINT$2 = "# disk-guard: allow";
|
|
3433
3433
|
/**
|
|
3434
3434
|
* Harness-level kill switch: set DISK_GUARD_DISABLE=1 to turn the guard off
|
|
3435
3435
|
* entirely. This is the "I own my harness, let me opt out" knob — an agent
|
|
@@ -3439,7 +3439,7 @@ const BYPASS_HINT$1 = "# disk-guard: allow";
|
|
|
3439
3439
|
* first; the SKYDIVE_/ANYONE_ prefixes are accepted too for consistency with
|
|
3440
3440
|
* the other env overrides. Empty/unset/"0"/"false" leave the guard on.
|
|
3441
3441
|
*/
|
|
3442
|
-
function guardDisabledByEnv$
|
|
3442
|
+
function guardDisabledByEnv$2() {
|
|
3443
3443
|
for (const name of [
|
|
3444
3444
|
"DISK_GUARD_DISABLE",
|
|
3445
3445
|
"SKYDIVE_DISK_GUARD_DISABLE",
|
|
@@ -3451,7 +3451,7 @@ function guardDisabledByEnv$1() {
|
|
|
3451
3451
|
return false;
|
|
3452
3452
|
}
|
|
3453
3453
|
/** True when the command carries the in-band bypass marker. */
|
|
3454
|
-
function hasBypassMarker$
|
|
3454
|
+
function hasBypassMarker$2(command) {
|
|
3455
3455
|
return BYPASS_MARKER$1.test(command);
|
|
3456
3456
|
}
|
|
3457
3457
|
/**
|
|
@@ -3540,24 +3540,24 @@ function isSpaceHungryCommand(command) {
|
|
|
3540
3540
|
function shouldBlockForDisk(command, diskPct) {
|
|
3541
3541
|
if (diskPct === null) return false;
|
|
3542
3542
|
if (diskPct < 95) return false;
|
|
3543
|
-
if (hasBypassMarker$
|
|
3543
|
+
if (hasBypassMarker$2(command)) return false;
|
|
3544
3544
|
return isSpaceHungryCommand(command);
|
|
3545
3545
|
}
|
|
3546
3546
|
/** The agent-facing explanation returned as the blocked tool result. */
|
|
3547
3547
|
function diskBlockReason(command, diskPct) {
|
|
3548
|
-
return `Blocked: the sandbox disk is ${diskPct}% full and this command (\`${command.trim().slice(0, 120)}\`) writes a large amount, so it would fail partway with ENOSPC and leave a corrupt result. Reclaim space FIRST, then retry. Free ONLY what THIS conversation created — scratch/build output you wrote this run, downloads you're done with, and worktrees/branches whose work you've already committed and pushed (\`git worktree remove\`, \`yarn cache clean\`, delete your own scratch). Do NOT blindly wipe /tmp or delete a clone/worktree you don't recognize — other conversations share this box. Check headroom with \`df -h /\` and \`du -sh ~/workspace/* 2>/dev/null\`. If you genuinely can't free enough, stop and tell the user you're blocked on disk rather than retrying the write. If you're certain this command is safe anyway (writes elsewhere, tiny bounded size, delete-then-write), force it through by appending \` ${BYPASS_HINT$
|
|
3548
|
+
return `Blocked: the sandbox disk is ${diskPct}% full and this command (\`${command.trim().slice(0, 120)}\`) writes a large amount, so it would fail partway with ENOSPC and leave a corrupt result. Reclaim space FIRST, then retry. Free ONLY what THIS conversation created — scratch/build output you wrote this run, downloads you're done with, and worktrees/branches whose work you've already committed and pushed (\`git worktree remove\`, \`yarn cache clean\`, delete your own scratch). Do NOT blindly wipe /tmp or delete a clone/worktree you don't recognize — other conversations share this box. Check headroom with \`df -h /\` and \`du -sh ~/workspace/* 2>/dev/null\`. If you genuinely can't free enough, stop and tell the user you're blocked on disk rather than retrying the write. If you're certain this command is safe anyway (writes elsewhere, tiny bounded size, delete-then-write), force it through by appending \` ${BYPASS_HINT$2}\` to the command.`;
|
|
3549
3549
|
}
|
|
3550
3550
|
const diskGuardExtension = (pi) => {
|
|
3551
3551
|
pi.on("tool_call", async (event) => {
|
|
3552
3552
|
if (event.toolName !== "bash") return;
|
|
3553
|
-
if (guardDisabledByEnv$
|
|
3553
|
+
if (guardDisabledByEnv$2()) return;
|
|
3554
3554
|
const command = event.input.command;
|
|
3555
3555
|
if (typeof command !== "string" || command.length === 0) return;
|
|
3556
|
-
if (hasBypassMarker$
|
|
3556
|
+
if (hasBypassMarker$2(command)) return;
|
|
3557
3557
|
if (!isSpaceHungryCommand(command)) return;
|
|
3558
3558
|
const diskPct = await readDiskUsePct();
|
|
3559
3559
|
if (!shouldBlockForDisk(command, diskPct)) return;
|
|
3560
|
-
log$
|
|
3560
|
+
log$10.warn({
|
|
3561
3561
|
diskPct,
|
|
3562
3562
|
command: command.slice(0, 200)
|
|
3563
3563
|
}, "blocked space-hungry bash command on near-full disk");
|
|
@@ -3568,6 +3568,112 @@ const diskGuardExtension = (pi) => {
|
|
|
3568
3568
|
});
|
|
3569
3569
|
};
|
|
3570
3570
|
//#endregion
|
|
3571
|
+
//#region src/extensions/noop-loop-guard.ts
|
|
3572
|
+
const log$9 = logger.child({ module: "noop-loop-guard" });
|
|
3573
|
+
/** Bare shell no-op: `true` or `:` (case-insensitive). */
|
|
3574
|
+
const BARE_NOOP_RE = /^(true|:)\s*$/i;
|
|
3575
|
+
/**
|
|
3576
|
+
* Shell metacharacters that make an `echo` argument more than a literal:
|
|
3577
|
+
* variables, command substitution, redirection, pipes. A matching argument
|
|
3578
|
+
* means real (or at least unknown) work, so it is never a no-op.
|
|
3579
|
+
*/
|
|
3580
|
+
const ECHO_ARG_METACHARS = /[$`><|&(){}]/;
|
|
3581
|
+
/** Longest `echo` argument still treated as a pure no-op. */
|
|
3582
|
+
const ECHO_ARG_MAX_LEN = 64;
|
|
3583
|
+
/** Marker that forces a single command through the guard. */
|
|
3584
|
+
const BYPASS_HINT$1 = "noop-guard: allow";
|
|
3585
|
+
/** Env vars that turn the guard off for the process. */
|
|
3586
|
+
const KILL_SWITCH_KEYS = ["NOOP_LOOP_GUARD_DISABLE", "SKYDIVE_NOOP_LOOP_GUARD_DISABLE"];
|
|
3587
|
+
function guardDisabledByEnv$1() {
|
|
3588
|
+
return KILL_SWITCH_KEYS.some((key) => {
|
|
3589
|
+
const value = process.env[key];
|
|
3590
|
+
return value !== void 0 && value !== "" && value !== "0";
|
|
3591
|
+
});
|
|
3592
|
+
}
|
|
3593
|
+
function hasBypassMarker$1(command) {
|
|
3594
|
+
return command.toLowerCase().includes(BYPASS_HINT$1);
|
|
3595
|
+
}
|
|
3596
|
+
/**
|
|
3597
|
+
* True when one `;`/`&&`/newline-separated segment is a pure no-op: a bare
|
|
3598
|
+
* `true`/`:`, a comment-only segment, or a bare/pure-`echo` call.
|
|
3599
|
+
*/
|
|
3600
|
+
function segmentIsNoop(segment) {
|
|
3601
|
+
let s = segment.trim();
|
|
3602
|
+
if (s.length === 0) return true;
|
|
3603
|
+
if (s.startsWith("#")) return true;
|
|
3604
|
+
const hash = s.indexOf("#");
|
|
3605
|
+
if (hash > 0) s = s.slice(0, hash).trim();
|
|
3606
|
+
if (s.length === 0) return true;
|
|
3607
|
+
if (BARE_NOOP_RE.test(s)) return true;
|
|
3608
|
+
return isEchoNoop(s);
|
|
3609
|
+
}
|
|
3610
|
+
/**
|
|
3611
|
+
* True when the command is an `echo` that only prints a short literal: no
|
|
3612
|
+
* shell metacharacters, argument at most ECHO_ARG_MAX_LEN characters. Bare
|
|
3613
|
+
* `echo` (no argument) is also a no-op. Anything with `$`, backticks,
|
|
3614
|
+
* redirection, or pipes is real work and never matches.
|
|
3615
|
+
*/
|
|
3616
|
+
function isEchoNoop(command) {
|
|
3617
|
+
const m = /^echo\s*(.*)$/.exec(command.trim());
|
|
3618
|
+
if (!m || m[1] === void 0) return false;
|
|
3619
|
+
let arg = m[1].trim();
|
|
3620
|
+
if (arg.endsWith(";")) arg = arg.slice(0, -1).trim();
|
|
3621
|
+
if (arg.length === 0) return true;
|
|
3622
|
+
if (arg.length > ECHO_ARG_MAX_LEN) return false;
|
|
3623
|
+
const quoted = /^(?:"([^"]*)"|'([^']*)')$/.exec(arg);
|
|
3624
|
+
if (quoted) arg = quoted[1] ?? quoted[2] ?? "";
|
|
3625
|
+
return arg.length <= ECHO_ARG_MAX_LEN && !ECHO_ARG_METACHARS.test(arg);
|
|
3626
|
+
}
|
|
3627
|
+
/**
|
|
3628
|
+
* True when the whole command is a shell no-op: only bare `true`/`:`,
|
|
3629
|
+
* pure `echo` calls, and comments, joined by `;`, `&&`, or newlines. Commands
|
|
3630
|
+
* that merely CONTAIN a no-op (`git foo || true`) are real work and never
|
|
3631
|
+
* match — `||` is deliberately not a no-op separator, because the left side
|
|
3632
|
+
* of `||` runs.
|
|
3633
|
+
*/
|
|
3634
|
+
function isBareNoop(command) {
|
|
3635
|
+
const trimmed = command.trim();
|
|
3636
|
+
if (trimmed.length === 0) return false;
|
|
3637
|
+
return trimmed.split(/;|&&|\n/).every(segmentIsNoop);
|
|
3638
|
+
}
|
|
3639
|
+
function noopBlockReason(streak, aborted) {
|
|
3640
|
+
const stop = "No-op loop: a bare `true`/`:` or a pure `echo` does nothing; it only costs a model round trip. If you are holding silence (e.g. right after `platform channel suppress-reply`) or waiting on something, the right move is to END THE TURN: emitting nothing IS the silence, and it costs zero commands. If a real process needs the wait, use `bg_run` or schedule a wake (`platform automation create --at <time>`).";
|
|
3641
|
+
const escalate = aborted ? ` This run has now fired ${streak} consecutive no-ops; the harness is aborting it.` : ` ${Math.max(25 - streak, 0)} more before the harness aborts the run.`;
|
|
3642
|
+
const bypass = ` If this specific command genuinely needs to run, append \` # ${BYPASS_HINT$1}\`.`;
|
|
3643
|
+
return stop + escalate + bypass;
|
|
3644
|
+
}
|
|
3645
|
+
/** Count one bash command against the streak; returns the new streak. */
|
|
3646
|
+
function recordNoopCommand(state, command) {
|
|
3647
|
+
if (!isBareNoop(command) || hasBypassMarker$1(command)) {
|
|
3648
|
+
state.streak = 0;
|
|
3649
|
+
return 0;
|
|
3650
|
+
}
|
|
3651
|
+
state.streak += 1;
|
|
3652
|
+
return state.streak;
|
|
3653
|
+
}
|
|
3654
|
+
const noopLoopGuardExtension = (pi) => {
|
|
3655
|
+
const state = { streak: 0 };
|
|
3656
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
3657
|
+
if (event.toolName !== "bash") return;
|
|
3658
|
+
if (guardDisabledByEnv$1()) return;
|
|
3659
|
+
const command = event.input.command;
|
|
3660
|
+
if (typeof command !== "string" || command.length === 0) return;
|
|
3661
|
+
const streak = recordNoopCommand(state, command);
|
|
3662
|
+
if (streak < 3) return;
|
|
3663
|
+
const aborted = streak >= 25;
|
|
3664
|
+
log$9.warn({
|
|
3665
|
+
streak,
|
|
3666
|
+
aborted,
|
|
3667
|
+
toolCallId: event.toolCallId
|
|
3668
|
+
}, "no-op loop guard fired");
|
|
3669
|
+
if (aborted) ctx.abort();
|
|
3670
|
+
return {
|
|
3671
|
+
block: true,
|
|
3672
|
+
reason: noopBlockReason(streak, aborted)
|
|
3673
|
+
};
|
|
3674
|
+
});
|
|
3675
|
+
};
|
|
3676
|
+
//#endregion
|
|
3571
3677
|
//#region src/extensions/sleep-guard.ts
|
|
3572
3678
|
/**
|
|
3573
3679
|
* Foreground sleep guard: block `sleep` commands that hold the turn open.
|
|
@@ -5941,6 +6047,7 @@ const all = [
|
|
|
5941
6047
|
toolCallEnvExtension,
|
|
5942
6048
|
bashDefaultTimeoutExtension,
|
|
5943
6049
|
diskGuardExtension,
|
|
6050
|
+
noopLoopGuardExtension,
|
|
5944
6051
|
sleepGuardExtension,
|
|
5945
6052
|
toolCallSummaryExtension
|
|
5946
6053
|
];
|