const std = @import("std"); const client = @import("client.zig"); const contracts = @import("contracts.zig"); const debug_trace = @import("../shared/debug_trace.zig"); const operation = @import("operation.zig"); const shell_resolver = @import("shell_resolver.zig"); const protocol = @import("protocol.zig"); const Allocator = std.mem.Allocator; const max_pending = 32; pub const start_wait_ceiling_ms: u64 = 20_000; pub const OpenIntentAdmission = enum { accepted, occupied, }; pub const DeinitDisposition = enum { settled, abnormal, }; pub const Admission = struct { alloc: Allocator, profile_user: []const u8, durable_session_id: []const u8, workspace_root: []const u8, sandbox_backend: @FieldType(contracts.Principal, "sandbox_backend"), command: []const u8, }; const Pending = struct { correlation_id: contracts.CorrelationId, command: []u8, starting_pending: bool = true, completion: ?client.Completion = null, fn deinit(self: *Pending, alloc: Allocator) void { if (self.completion) |*completion| completion.deinit(); alloc.free(self.command); self.* = undefined; } }; pub const NoticePhase = enum { starting, final }; pub const Notice = union(enum) { starting: struct { correlation_id: contracts.CorrelationId, command: []const u8, }, running: struct { correlation_id: contracts.CorrelationId, command: []const u8, session_id: []const u8, }, failed: struct { correlation_id: contracts.CorrelationId, command: []const u8, code: contracts.StructuredErrorCode, }, pub fn correlationId(self: Notice) contracts.CorrelationId { return switch (self) { inline else => |value| value.correlation_id, }; } pub fn phase(self: Notice) NoticePhase { return switch (self) { .starting => .starting, .running, .failed => .final, }; } }; pub const Runtime = struct { pending: [max_pending]?Pending = @splat(null), len: usize = 0, open_intent: ?[]u8 = null, pub fn deinitSettled( self: *Runtime, alloc: Allocator, ) DeinitDisposition { if (self.len != 0) { self.deinitAbnormal(alloc, "graceful_exit_invariant"); return .abnormal; } self.deinitEmpty(alloc); return .settled; } pub fn deinitAbnormal( self: *Runtime, alloc: Allocator, reason: []const u8, ) void { for (self.pending[0..self.len]) |*entry| { if (entry.*) |*pending| { debug_trace.logf( "terminal", "direct pending dropped correlation={d} phase={s} reason={s}", .{ pending.correlation_id.value, pendingPhase(pending), reason, }, ); pending.deinit(alloc); entry.* = null; } } self.len = 0; self.deinitEmpty(alloc); } fn deinitEmpty(self: *Runtime, alloc: Allocator) void { if (self.open_intent) |session_id| alloc.free(session_id); self.* = .{}; } pub fn requestOpen( self: *Runtime, alloc: Allocator, session_id: []const u8, ) Allocator.Error!OpenIntentAdmission { if (self.open_intent != null) return .occupied; const owned = try alloc.dupe(u8, session_id); self.open_intent = owned; return .accepted; } pub fn pendingOpenIntent(self: *const Runtime) ?[]const u8 { return self.open_intent; } pub fn takeOpenIntent(self: *Runtime) ?[]u8 { const session_id = self.open_intent orelse return null; self.open_intent = null; return session_id; } pub fn admit( self: *Runtime, terminal_client: *client.Runtime, input: Admission, ) !contracts.CorrelationId { if (self.len == self.pending.len) return error.QueueFull; const command = try input.alloc.dupe(u8, input.command); errdefer input.alloc.free(command); var persistence = try operation.prepareStartPersistence(input.alloc, .{ .profile_user = input.profile_user, .durable_session_id = input.durable_session_id, .workspace_root = input.workspace_root, .cwd = input.workspace_root, .transport_role = .interactive, .sandbox_backend = input.sandbox_backend, .backend = .native, .actor = .human, .controls = .full(), .lifetime = .session, .direct_human_model_read_only = true, }); defer persistence.deinit(); const request = contracts.ActionRequest{ .start = .{ .cwd = input.workspace_root, .command = input.command, .shell = try shell_resolver.profileShell(input.alloc, null, .user), .backend = .native, .return_when = .started, .wait_ceiling_ms = start_wait_ceiling_ms, .persistence = persistence.view(), } }; try operation.validate(request); const correlation_id = terminal_client.nextCorrelationId(); try terminal_client.admit(input.alloc, correlation_id, request); self.pending[self.len] = .{ .correlation_id = correlation_id, .command = command, }; self.len += 1; return correlation_id; } pub fn nextNotice( self: *Runtime, terminal_client: *client.Runtime, ) ?Notice { for (self.pending[0..self.len]) |*entry| { if (entry.*) |*pending| { if (pending.starting_pending) return .{ .starting = .{ .correlation_id = pending.correlation_id, .command = pending.command, } }; if (pending.completion == null) { pending.completion = terminal_client.takeCompletionFor( pending.correlation_id, ) orelse continue; } if (finalNotice(pending)) |notice| return notice; } } return null; } pub fn acknowledgeNotice( self: *Runtime, alloc: Allocator, correlation_id: contracts.CorrelationId, phase: NoticePhase, ) void { for (self.pending[0..self.len], 0..) |entry, index| { const pending = entry.?; if (pending.correlation_id.value != correlation_id.value) continue; switch (phase) { .starting => { std.debug.assert(pending.starting_pending); self.pending[index].?.starting_pending = false; }, .final => { std.debug.assert(!pending.starting_pending); std.debug.assert(pending.completion != null); var removed = self.removeAt(index); removed.deinit(alloc); }, } return; } unreachable; } pub fn hasAcceptedPending(self: *const Runtime) bool { return self.len != 0; } pub fn pendingCount(self: *const Runtime) usize { return self.len; } fn removeAt(self: *Runtime, index: usize) Pending { const result = self.pending[index].?; var current = index; while (current + 1 < self.len) : (current += 1) { self.pending[current] = self.pending[current + 1]; } self.len -= 1; self.pending[self.len] = null; return result; } }; fn finalNotice(pending: *const Pending) ?Notice { const completion = &pending.completion.?; if (completion.kind != .response) return null; if (completion.frame) |*frame| { switch (frame.message().payload) { .response => |response| switch (response) { .success => |success| switch (success) { .start => |start| return .{ .running = .{ .correlation_id = pending.correlation_id, .command = pending.command, .session_id = start.session.session_id, } }, else => {}, }, .failure => |failure| return .{ .failed = .{ .correlation_id = pending.correlation_id, .command = pending.command, .code = failure.code, } }, }, else => {}, } } return null; } fn pendingPhase(pending: *const Pending) []const u8 { if (pending.starting_pending) return "starting_notice"; if (pending.completion == null) return "awaiting_host_result"; return if (finalNotice(pending) != null) "final_notice" else "indeterminate_completion"; } test "terminal open intent is first wins and reports occupied without mutation" { const alloc = std.testing.allocator; var runtime: Runtime = .{}; defer _ = runtime.deinitSettled(alloc); try std.testing.expectEqual( OpenIntentAdmission.accepted, try runtime.requestOpen(alloc, "terminal-a"), ); try std.testing.expectEqualStrings("terminal-a", runtime.pendingOpenIntent().?); try std.testing.expectEqual( OpenIntentAdmission.occupied, try runtime.requestOpen(alloc, "terminal-b"), ); try std.testing.expectEqualStrings("terminal-a", runtime.pendingOpenIntent().?); const taken = runtime.takeOpenIntent().?; defer alloc.free(taken); try std.testing.expectEqualStrings("terminal-a", taken); try std.testing.expect(runtime.pendingOpenIntent() == null); } test "terminal open intent allocation failure preserves exact ownership" { const alloc = std.testing.allocator; var runtime: Runtime = .{}; defer _ = runtime.deinitSettled(alloc); var failing = std.testing.FailingAllocator.init( alloc, .{ .fail_index = 0 }, ); try std.testing.expectError( error.OutOfMemory, runtime.requestOpen(failing.allocator(), "terminal-a"), ); try std.testing.expect(runtime.pendingOpenIntent() == null); try std.testing.expectEqual( OpenIntentAdmission.accepted, try runtime.requestOpen(alloc, "terminal-a"), ); const original = runtime.pendingOpenIntent().?; try std.testing.expectEqual( OpenIntentAdmission.occupied, try runtime.requestOpen(failing.allocator(), "terminal-b"), ); try std.testing.expectEqual(original.ptr, runtime.pendingOpenIntent().?.ptr); try std.testing.expectEqualStrings("terminal-a", runtime.pendingOpenIntent().?); } test "direct lifecycle retains indeterminate completion without fabricating failure" { const alloc = std.testing.allocator; var runtime: Runtime = .{}; defer runtime.deinitAbnormal(alloc, "test_cleanup"); var terminal_client: client.Runtime = .{}; defer terminal_client.deinit(); runtime.pending[0] = .{ .correlation_id = .{ .value = 7 }, .command = try alloc.dupe(u8, "zig build test"), }; runtime.len = 1; const first_start = runtime.nextNotice(&terminal_client).?; try std.testing.expectEqual(NoticePhase.starting, first_start.phase()); try std.testing.expectEqual(NoticePhase.starting, runtime.nextNotice(&terminal_client).?.phase()); runtime.acknowledgeNotice(alloc, first_start.correlationId(), first_start.phase()); try std.testing.expect(runtime.nextNotice(&terminal_client) == null); runtime.pending[0].?.completion = .{ .kind = .disconnected, .correlation_id = .{ .value = 7 }, }; try std.testing.expect(runtime.nextNotice(&terminal_client) == null); try std.testing.expect(runtime.hasAcceptedPending()); try std.testing.expectEqual( client.CompletionKind.disconnected, runtime.pending[0].?.completion.?.kind, ); } test "indeterminate completion does not starve a later authoritative result" { const alloc = std.testing.allocator; var runtime: Runtime = .{}; defer runtime.deinitAbnormal(alloc, "test_cleanup"); var terminal_client: client.Runtime = .{}; defer terminal_client.deinit(); var encoded = try protocol.encodeFrame( alloc, contracts.current_protocol_revision, 0, .{ .value = 8 }, .{ .response = .{ .success = .{ .start = .{ .session = .{ .session_id = "terminal-b", .lifecycle = .running, .attention = .{}, .backend = .native, .output_cursor = .{ .segment = 1, .offset = 0 }, .screen_recovery = .{ .unavailable = .missing }, }, .outcome = .started, } } } }, ); defer encoded.deinit(alloc); runtime.pending[0] = .{ .correlation_id = .{ .value = 7 }, .command = try alloc.dupe(u8, "indeterminate"), .starting_pending = false, .completion = .{ .kind = .disconnected, .correlation_id = .{ .value = 7 }, }, }; runtime.pending[1] = .{ .correlation_id = .{ .value = 8 }, .command = try alloc.dupe(u8, "authoritative"), .starting_pending = false, .completion = .{ .kind = .response, .correlation_id = .{ .value = 8 }, .frame = try protocol.decodeFrame(alloc, encoded.bytes), }, }; runtime.len = 2; const notice = runtime.nextNotice(&terminal_client).?; switch (notice) { .running => |running| { try std.testing.expectEqual(@as(u64, 8), running.correlation_id.value); try std.testing.expectEqualStrings("terminal-b", running.session_id); }, else => return error.TestUnexpectedResult, } runtime.acknowledgeNotice(alloc, notice.correlationId(), notice.phase()); try std.testing.expectEqual(@as(usize, 1), runtime.pendingCount()); try std.testing.expectEqual(@as(u64, 7), runtime.pending[0].?.correlation_id.value); try std.testing.expect(runtime.nextNotice(&terminal_client) == null); } test "abnormal direct cleanup releases accepted unresolved outcomes" { const alloc = std.testing.allocator; var runtime: Runtime = .{}; runtime.pending[0] = .{ .correlation_id = .{ .value = 8 }, .command = try alloc.dupe(u8, "unresolved"), .starting_pending = false, }; runtime.len = 1; runtime.deinitAbnormal(alloc, "input_closed"); try std.testing.expect(!runtime.hasAcceptedPending()); } test "settled direct cleanup distinguishes empty from abnormal" { const alloc = std.testing.allocator; var settled: Runtime = .{}; try std.testing.expectEqual( DeinitDisposition.settled, settled.deinitSettled(alloc), ); var runtime: Runtime = .{}; runtime.pending[0] = .{ .correlation_id = .{ .value = 9 }, .command = try alloc.dupe(u8, "unsettled"), .starting_pending = false, }; runtime.len = 1; try std.testing.expectEqual( DeinitDisposition.abnormal, runtime.deinitSettled(alloc), ); try std.testing.expect(!runtime.hasAcceptedPending()); }