diff options
| author | t <t@tjp.lol> | 2026-06-13 23:45:59 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-15 15:08:32 -0600 |
| commit | 02b4c7a35ac0b714bc045d54fb4bb45d1ce4e490 (patch) | |
| tree | 134196a82dd6fdd55b735be4c0cf38428c85038d /src/lua_runtime.zig | |
| parent | 71643a5d69ffc40882c9fcde3cc8a3bcf02d7396 (diff) | |
Add Codex Responses support and session debugging
Teach provider config and auth resolution about the Codex Responses
dialect, including user-facing `style = "openai_responses"` with
`dialect = "codex"`. Serialize and parse Responses traffic with
provider-specific reasoning replay, assistant phase metadata, and robust
function-call assembly keyed by `output_index` so streamed tool inputs
survive proxy quirks and empty terminal payloads.
Also persist thinking origins and message metadata across sessions, add
the Anthropic interleaved-thinking header switch, write per-session
debug logs, and improve the TUI and scripts for inspecting tool output
and session costs.
Diffstat (limited to 'src/lua_runtime.zig')
| -rw-r--r-- | src/lua_runtime.zig | 140 |
1 files changed, 140 insertions, 0 deletions
diff --git a/src/lua_runtime.zig b/src/lua_runtime.zig index 2574fae..35247bb 100644 --- a/src/lua_runtime.zig +++ b/src/lua_runtime.zig @@ -87,6 +87,15 @@ pub const LuaRuntime = struct { /// calls to drive libuv to completion. `0` until /// `installScheduler` runs. uv_run_ref: c_int = 0, + /// Registry ref to `require("luv").update_time`. libuv caches the + /// loop's notion of "now" and only refreshes it while `uv.run` + /// executes; the loop is idle between batches (and during a slow + /// model's streaming), so its clock goes stale. The scheduler calls + /// this at the start of each batch so handlers that arm timers + /// (e.g. the shell tool's timeout) compute deadlines against the + /// real current time, not a frozen one. `0` until + /// `installScheduler` runs. + uv_update_time_ref: c_int = 0, /// Pointer to the in-flight batch, valid only for the duration of /// one `invoke_batch` call. The `panto._record_result` C function @@ -220,6 +229,7 @@ pub const LuaRuntime = struct { try installRecordResult(self); try installWrapperClosure(self); try cacheUvRun(self); + try cacheUvUpdateTime(self); } /// Tear down the runtime: free every owned string, unref every @@ -244,6 +254,9 @@ pub const LuaRuntime = struct { if (self.uv_run_ref != 0) { c.luaL_unref(self.L, lua_bridge.LUA_REGISTRYINDEX, self.uv_run_ref); } + if (self.uv_update_time_ref != 0) { + c.luaL_unref(self.L, lua_bridge.LUA_REGISTRYINDEX, self.uv_update_time_ref); + } // Free the UI event bridge (Lua handler/component refs + caches) // BEFORE closing the state, since it unrefs registry entries. @@ -724,6 +737,12 @@ fn runBatch( return; } + // Refresh libuv's cached loop clock before any handler arms a timer. + // The loop sits idle between batches (and while a slow model streams), + // so its notion of "now" is stale; a timeout armed against it would + // otherwise fire the instant `uv.run` refreshes time below. + refreshLoopClock(self); + var slots = try allocator.alloc(Slot, calls.len); defer allocator.free(slots); for (slots) |*s| s.* = .{}; @@ -1156,6 +1175,45 @@ fn cacheUvRun(self: *LuaRuntime) !void { self.uv_run_ref = c.luaL_ref(L, lua_bridge.LUA_REGISTRYINDEX); } +/// Cache `require("luv").update_time` so the scheduler can refresh the +/// loop clock at the start of each batch. See `uv_update_time_ref`. +fn cacheUvUpdateTime(self: *LuaRuntime) !void { + const L = self.L; + const snippet = + \\local uv = require("luv") + \\return uv.update_time + ; + if (c.luaL_loadstring(L, snippet) != 0) { + logTopAsError(L, "panto-lua: failed to compile luv.update_time lookup"); + c.lua_settop(L, c.lua_gettop(L) - 1); + return RuntimeError.LuaInitFailed; + } + if (c.lua_pcallk(L, 0, 1, 0, 0, null) != 0) { + logTopAsError(L, "panto-lua: require('luv') failed (was the bootstrap successful?)"); + c.lua_settop(L, c.lua_gettop(L) - 1); + return RuntimeError.LuaInitFailed; + } + if (c.lua_type(L, -1) != lua_bridge.T_FUNCTION) { + c.lua_settop(L, c.lua_gettop(L) - 1); + return RuntimeError.LuaInitFailed; + } + self.uv_update_time_ref = c.luaL_ref(L, lua_bridge.LUA_REGISTRYINDEX); +} + +/// Refresh libuv's cached loop clock (`uv.update_time()`). Best-effort: +/// a failure here only means timeout deadlines may be computed against a +/// slightly stale clock, so we log and continue rather than failing the +/// batch. +fn refreshLoopClock(self: *LuaRuntime) void { + if (self.uv_update_time_ref == 0) return; + const L = self.L; + _ = c.lua_rawgeti(L, lua_bridge.LUA_REGISTRYINDEX, @intCast(self.uv_update_time_ref)); + if (c.lua_pcallk(L, 0, 0, 0, 0, null) != 0) { + logTopAsError(L, "panto-lua: uv.update_time failed"); + c.lua_settop(L, c.lua_gettop(L) - 1); + } +} + // --------------------------------------------------------------------------- // Internals // --------------------------------------------------------------------------- @@ -1778,6 +1836,88 @@ test "scheduler: two real std.shell calls in one batch do not deadlock" { try testing.expect(std.mem.indexOf(u8, okText(results[1]), "second-done") != null); } +test "scheduler: shell timeout is measured from dispatch, not a stale loop clock" { + // Regression: libuv caches the loop's "now" and only refreshes it while + // `uv.run` runs. The loop is idle between batches, so a tool that arms a + // timeout timer against the stale clock would have it fire the instant + // `uv.run` refreshes time. We force staleness with `uv.sleep` (a blocking + // sleep that does NOT run the loop), then dispatch a 1s-timeout shell call + // and assert it does not spuriously time out. + var rt = try LuaRuntime.create(testing.allocator); + defer rt.deinit(); + const luarocks_rt = bootstrapRealRuntime(rt) catch |e| return e; + defer luarocks_rt.deinit(); + + const tools_dir = try findAgentToolsDir(); + defer testing.allocator.free(tools_dir); + const shell_path = try std.fs.path.join(testing.allocator, &.{ tools_dir, "shell.lua" }); + defer testing.allocator.free(shell_path); + try rt.loadTool(shell_path, tools_dir); + + var src = rt.toolSource(); + + // Prime the loop clock with one run. + { + const warm = [_]panto.ToolCall{.{ .tool_name = "std.shell", .input = "{\"command\":\"true\"}" }}; + var wres: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }}; + try src.vtable.invoke_batch(src.ctx, &warm, &wres, testing.allocator); + freeResults(&wres); + } + + // Advance real time ~1.2s WITHOUT running the loop, so its cached clock + // goes stale relative to the wall clock by more than the timeout below. + { + const snippet = "require('luv').sleep(1200)"; + if (c.luaL_loadstring(rt.L, snippet) != 0) return error.SleepSnippetFailed; + if (c.lua_pcallk(rt.L, 0, 0, 0, 0, null) != 0) return error.SleepSnippetFailed; + } + + const calls = [_]panto.ToolCall{ + .{ .tool_name = "std.shell", .input = "{\"command\":\"echo prompt-done\",\"timeout\":1}" }, + }; + var results: [1]panto.ToolCallResult = .{.{ .err = error.SourceDroppedCall }}; + try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator); + defer freeResults(&results); + + try testing.expect(std.mem.indexOf(u8, okText(results[0]), "prompt-done") != null); + try testing.expect(std.mem.indexOf(u8, okText(results[0]), "timed out") == null); +} + +test "scheduler: three real std.shell calls with explicit timeout all succeed" { + var rt = try LuaRuntime.create(testing.allocator); + defer rt.deinit(); + const luarocks_rt = bootstrapRealRuntime(rt) catch |e| return e; + defer luarocks_rt.deinit(); + + const tools_dir = try findAgentToolsDir(); + defer testing.allocator.free(tools_dir); + const shell_path = try std.fs.path.join(testing.allocator, &.{ tools_dir, "shell.lua" }); + defer testing.allocator.free(shell_path); + try rt.loadTool(shell_path, tools_dir); + + var src = rt.toolSource(); + const calls = [_]panto.ToolCall{ + .{ .tool_name = "std.shell", .input = "{\"command\":\"echo alpha-done\",\"timeout\":30}" }, + .{ .tool_name = "std.shell", .input = "{\"command\":\"echo bravo-done\",\"timeout\":30}" }, + .{ .tool_name = "std.shell", .input = "{\"command\":\"echo charlie-done\",\"timeout\":30}" }, + }; + var results: [3]panto.ToolCallResult = .{ + .{ .err = error.SourceDroppedCall }, + .{ .err = error.SourceDroppedCall }, + .{ .err = error.SourceDroppedCall }, + }; + try src.vtable.invoke_batch(src.ctx, &calls, &results, testing.allocator); + defer freeResults(&results); + + try testing.expect(std.mem.indexOf(u8, okText(results[0]), "alpha-done") != null); + try testing.expect(std.mem.indexOf(u8, okText(results[1]), "bravo-done") != null); + try testing.expect(std.mem.indexOf(u8, okText(results[2]), "charlie-done") != null); + // None should have hit the timeout path. + for (results) |r| { + try testing.expect(std.mem.indexOf(u8, okText(r), "timed out") == null); + } +} + // Reproduction: two REAL `std.read` calls in one batch. read.lua uses // only synchronous `uv.fs_*` calls and never yields, so both should // complete during dispatch without entering the drive loop. |
