Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions service/scripts/test-ptc-replay-bash-smoke.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,5 +519,37 @@ get_weather '{"city":"Rome"}'
);
}

{
// Regression: input past ARG_MAX (~128KB) must still replay from history,
// not get silently re-issued as a new pending call each run.
const bigExpr = '1+1;#' + 'x'.repeat(300_000);
const user = `
result=$(calculate '{"expression":"${bigExpr}"}')
echo "Result: $result"
`;
const r1 = runBash(assemble(user), {});
const p1 = extractPending(r1.stdout);
assert(r1.exitCode === 0, 'large_input: first run exit 0');
assert(p1.pending?.[0]?.call_id === 'call_001', 'large_input: first pending is call_001');
assert(
typeof p1.pending?.[0]?.input_hash === 'string' && p1.pending[0].input_hash.length === 64,
'large_input: pending carries a hash for the oversized input',
);

const history = {
call_001: {
result: 'ok-large',
tool_name: 'calculate',
input_hash: hashToolInput({ expression: bigExpr }),
received_at: 1,
},
};
const r2 = runBash(assemble(user), history);
const p2 = extractPending(r2.stdout);
assert(r2.exitCode === 0, 'large_input_replay: exit 0');
assert(p2.pending === null, 'large_input_replay: no pending re-emitted for the same oversized call');
assert(p2.stdout.includes('Result: "ok-large"'), 'large_input_replay: cached result used instead of re-invoking');
}

console.log(`\n${passed} passed, ${failed} failed`);
if (failed > 0) process.exit(1);
3 changes: 3 additions & 0 deletions service/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,9 @@ export const env = {
EGRESS_GATEWAY_FILE_SERVER_URL: process.env.EGRESS_GATEWAY_FILE_SERVER_URL ?? process.env.FILE_SERVER_URL ?? 'http://localhost:3000',
EGRESS_GATEWAY_TOOL_CALL_SERVER_URL: process.env.EGRESS_GATEWAY_TOOL_CALL_SERVER_URL ?? process.env.TOOL_CALL_SERVER_URL ?? 'http://localhost:3033',
EGRESS_GATEWAY_MAX_TOOL_CALL_BYTES: Number(process.env.EGRESS_GATEWAY_MAX_TOOL_CALL_BYTES) || 1024 * 1024,
// Per-entry / aggregate caps for PTC tool results persisted in `tool_history:` (see replay-state.ts).
PTC_MAX_TOOL_RESULT_BYTES: Number(process.env.PTC_MAX_TOOL_RESULT_BYTES) || 5_000_000,
PTC_MAX_TOOL_HISTORY_TOTAL_BYTES: Number(process.env.PTC_MAX_TOOL_HISTORY_TOTAL_BYTES) || 40_000_000,
Comment thread
kenzaelk98 marked this conversation as resolved.
Outdated
EGRESS_GATEWAY_MAX_FILE_BYTES: Number(process.env.EGRESS_GATEWAY_MAX_FILE_BYTES ?? process.env.SANDBOX_MAX_FILE_SIZE) || 10_000_000,
EGRESS_GATEWAY_MAX_PATH_LENGTH: Number(process.env.EGRESS_GATEWAY_MAX_PATH_LENGTH ?? process.env.SANDBOX_MAX_PATH_LENGTH) || 256,
EGRESS_GATEWAY_MAX_NESTING_DEPTH: Number(process.env.EGRESS_GATEWAY_MAX_NESTING_DEPTH ?? process.env.SANDBOX_MAX_NESTING_DEPTH) || 10,
Expand Down
34 changes: 23 additions & 11 deletions service/src/preamble-bash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,18 +417,20 @@ _ptc_next_call_id() {

_ptc_history_matches_by_signature() {
local _ptc_name="$1"
local _ptc_input="$2"
local _ptc_input_file="$2"
local _ptc_input_hash="$3"
local _ptc_call_site="$4"
if [ ! -r "$_PTC_HISTORY_PATH" ]; then
return 0
fi
# Path, not inline: large input can exceed ARG_MAX via --argjson.
jq -c \\
--arg nm "$_ptc_name" \\
--arg site "$_ptc_call_site" \\
--arg hash "$_ptc_input_hash" \\
--argjson inp "$_ptc_input" \\
'to_entries
--slurpfile inp_arr "$_ptc_input_file" \\
'($inp_arr[0]) as $inp
| to_entries
| map(select((.value | type) == "object"
and .value.tool_name == $nm
and ((.value.input_hash == $hash) or (.value.input == $inp))))
Expand Down Expand Up @@ -474,13 +476,15 @@ _ptc_print_history_entry() {
_ptc_history_entry_matches_current_call() {
local _ptc_entry="$1"
local _ptc_name="$2"
local _ptc_input="$3"
local _ptc_input_file="$3"
local _ptc_input_hash="$4"
# Path, same ARG_MAX reason as above.
printf '%s' "$_ptc_entry" | jq -e \\
--arg nm "$_ptc_name" \\
--arg hash "$_ptc_input_hash" \\
--argjson inp "$_ptc_input" \\
'if type != "object" then true
--slurpfile inp_arr "$_ptc_input_file" \\
'($inp_arr[0]) as $inp
| if type != "object" then true
elif (has("tool_name") and .tool_name != $nm) then false
elif (has("input_hash") or has("input")) then
((.input_hash == $hash) or (.input == $inp))
Expand All @@ -505,8 +509,13 @@ _ptc_call_tool() {
exit 1
fi

# Large input can exceed ARG_MAX via --argjson; write once, reuse path below.
local _ptc_input_tmp
_ptc_input_tmp="$(mktemp -t _ptc_input.XXXXXX 2>/dev/null || mktemp /tmp/_ptc_input.XXXXXX)"
printf '%s' "$_ptc_input" > "$_ptc_input_tmp"

local _ptc_matches
_ptc_matches=$(_ptc_history_matches_by_signature "$_ptc_name" "$_ptc_input" "$_ptc_input_hash" "$_ptc_call_site")
_ptc_matches=$(_ptc_history_matches_by_signature "$_ptc_name" "$_ptc_input_tmp" "$_ptc_input_hash" "$_ptc_call_site")

if ! _ptc_acquire_lock; then
exit 1
Expand All @@ -522,6 +531,7 @@ _ptc_call_tool() {
printf '%s\\n' "$_ptc_matched_call_id" >> "$_PTC_CONSUMED_FILE"
_ptc_mark_counter_at_least "$_ptc_matched_call_id"
_ptc_release_lock
rm -f "$_ptc_input_tmp"
_ptc_print_history_entry "$_ptc_matched_entry"
return $?
fi
Expand All @@ -538,26 +548,28 @@ _ptc_call_tool() {
if [ -z "$_ptc_entry" ] || [ "$_ptc_entry" = "null" ]; then
break
fi
if _ptc_history_entry_matches_current_call "$_ptc_entry" "$_ptc_name" "$_ptc_input" "$_ptc_input_hash"; then
if _ptc_history_entry_matches_current_call "$_ptc_entry" "$_ptc_name" "$_ptc_input_tmp" "$_ptc_input_hash"; then
printf '%s\\n' "$_ptc_call_id" >> "$_PTC_CONSUMED_FILE"
_ptc_release_lock
rm -f "$_ptc_input_tmp"
_ptc_print_history_entry "$_ptc_entry"
return $?
fi
done

if ! jq -c -n \\
if ! printf '%s' "$_ptc_input" | jq -c -n \\
--arg cid "$_ptc_call_id" \\
--arg nm "$_ptc_name" \\
--arg hash "$_ptc_input_hash" \\
--arg site "$_ptc_call_site" \\
--argjson inp "$_ptc_input" \\
'{call_id:$cid,tool_name:$nm,input:$inp,input_hash:$hash,call_site:$site}' >> "$_PTC_PENDING_FILE"; then
'{call_id:$cid,tool_name:$nm,input:input,input_hash:$hash,call_site:$site}' >> "$_PTC_PENDING_FILE"; then
Comment thread
kenzaelk98 marked this conversation as resolved.
_ptc_write_error "failed to serialize pending tool call for $_ptc_name"
_ptc_release_lock
rm -f "$_ptc_input_tmp"
exit 1
fi
_ptc_release_lock
rm -f "$_ptc_input_tmp"
exit 0
}

Expand Down
4 changes: 2 additions & 2 deletions service/src/service/replay-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ export const REPLAY_LOCK_TTL_MS = Math.max(10 * 60 * 1000, env.JOB_TIMEOUT * 2 +
* Redis hash and the `_ptc_history.json` injected into the sandbox bounded
* regardless of how pathological a single tool result is. Scaled
* proportionally with `MAX_EXECUTION_STATE_BYTES` (ratio 1:2 vs exec_state). */
export const MAX_TOOL_RESULT_BYTES = 5_000_000;
export const MAX_TOOL_RESULT_BYTES = env.PTC_MAX_TOOL_RESULT_BYTES;

/** Aggregate cap across ALL results persisted for a single execution. Scaled
* proportionally with `MAX_EXECUTION_STATE_BYTES` (ratio 4:1 vs exec_state)
* so a long replay flow can accumulate ~8 saturating tool results before
* being asked to break work into a fresh execution. */
export const MAX_TOOL_HISTORY_TOTAL_BYTES = 40_000_000;
export const MAX_TOOL_HISTORY_TOTAL_BYTES = env.PTC_MAX_TOOL_HISTORY_TOTAL_BYTES;

/** Maximum number of keys `scanKeys` will return in a single call. The janitor
* runs every `STALE_CLEANUP_INTERVAL` and processes whatever this yields; if
Expand Down