const std = @import("std"); const builtin = @import("builtin"); const io_mod = @import("../core/shared/io.zig"); const debug_trace = @import("../core/shared/debug_trace.zig"); const mem_utils = @import("../core/shared/mem_utils.zig"); const jsonrpc = @import("jsonrpc.zig"); const acp_types = @import("types.zig"); const mcp_servers = @import("mcp_servers.zig"); const server = @import("server.zig"); const session_codec = @import("../core/session/session_codec.zig"); const session_display_metadata = @import("../core/session/session_display_metadata.zig"); const session_store = @import("../core/session/session_store.zig"); const legacy_background_migration = @import("../core/session/legacy_background_migration.zig"); const js_host_session_store = @import("../core/session/js_host_session_store.zig"); const session_runtime = @import("../core/session/session.zig"); const agent_execution_memory = @import("../core/agent/execution_memory.zig"); const tool_dispatch = @import("../core/tooling/tool_dispatch.zig"); const tool_call_presentation = @import("tool_call_presentation.zig"); const mcp_runtime = @import("../core/mcp/mcp_runtime.zig"); const mcp_contract = @import("../core/mcp/mcp_contract.zig"); const project_config = @import("../core/mcp/project_config.zig"); const workspace_config = @import("../core/mcp/workspace_config.zig"); const config_runtime = @import("../core/config/config_runtime.zig"); const model_catalog = @import("../core/gateway/model_catalog.zig"); const model_capabilities = @import("../core/config/model_capabilities.zig"); const provider_set = @import("../core/gateway/provider_set.zig"); const host = @import("../core/hosts/host.zig"); const host_target = @import("../core/hosts/target.zig"); const image_attachments = @import("../core/images/image_attachments.zig"); const credentials = @import("../core/auth/credentials.zig"); const model_provider = @import("../core/config/model_provider.zig"); const mode_registry = @import("../core/modes/mode_registry.zig"); const subagent_resume_admission = @import("../core/subagent/resume_admission.zig"); const types = @import("../core/shared/types.zig"); const context_contract = @import("../core/workspace/context_contract.zig"); const test_builtin_gateway = if (builtin.is_test) @import("../builtins/gateway.zig") else struct {}; const Allocator = std.mem.Allocator; const ErrorCode = jsonrpc.ErrorCode; const writeJsonStr = jsonrpc.writeJsonStr; pub fn handleNewLibfxSession( state: *server.ServerState, alloc: Allocator, msg: *jsonrpc.Message, ) !void { try server.releaseActiveSession(state); const session_id = try session_store.generateSessionId(alloc); var session_id_owned = true; defer if (session_id_owned) alloc.free(session_id); const model = try alloc.dupe(u8, state.selected_model); var model_owned = true; defer if (model_owned) alloc.free(model); var session_rt = session_runtime.SessionRuntime.initWithProviders( state.cfg.max_history_turns, state.cfg.provider_set.deferredUsageProviders(), ); var session_rt_owned = true; defer if (session_rt_owned) session_rt.deinit(alloc); state.active_session = .{ .session_id = session_id, .model = model, .provider = state.provider, .mode = state.cfg.mode_registry.default_mode_id, .workspace_root = state.workspace_root, .api_key = state.api_key, .credential_source = state.credential_source, .account_id = state.account_id, .agent_step_limit = state.agent_step_limit, .max_tool_result_bytes = state.max_tool_result_bytes, .fast_mode = state.fast_mode, .effort = state.effort, .first_call_tool_choice = state.first_call_tool_choice, .permission_mode = state.permission_mode, .permission_rules = state.permission_rules, .session_rt = session_rt, .cancel_flag = std.atomic.Value(bool).init(false), .pending_prompt_id = null, }; session_id_owned = false; model_owned = false; session_rt_owned = false; try writeNewSessionResponse(state, alloc, msg, session_id); } pub fn handleNewWasmSession(state: *server.ServerState, alloc: Allocator, msg: *jsonrpc.Message) !void { try server.releaseActiveSession(state); var durable = try freshAcpState(state, alloc); var durable_owned = true; defer if (durable_owned) durable.deinit(alloc); const session_id = try alloc.dupe(u8, durable.id); var session_id_owned = true; defer if (session_id_owned) alloc.free(session_id); const model = try alloc.dupe(u8, durable.preferences.model); var model_owned = true; defer if (model_owned) alloc.free(model); var session_rt = session_runtime.SessionRuntime.initWithProviders( state.cfg.max_history_turns, state.cfg.provider_set.deferredUsageProviders(), ); var session_rt_owned = true; defer if (session_rt_owned) session_rt.deinit(alloc); const revision = js_host_session_store.commit(alloc, durable, null) catch return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.internal_error, .message = "Failed to create session", }); var revision_owned = true; defer if (revision_owned) alloc.free(revision); state.active_session = .{ .session_id = session_id, .wasm_state = durable, .wasm_revision = revision, .model = model, .provider = durable.preferences.provider, .mode = state.cfg.mode_registry.default_mode_id, .workspace_root = state.workspace_root, .api_key = state.api_key, .credential_source = state.credential_source, .credential_refresh_after_ms = state.credential_refresh_after_ms, .account_id = state.account_id, .agent_step_limit = state.agent_step_limit, .max_tool_result_bytes = state.max_tool_result_bytes, .fast_mode = state.fast_mode, .effort = state.effort, .first_call_tool_choice = state.first_call_tool_choice, .permission_mode = state.permission_mode, .permission_rules = state.permission_rules, .session_rt = session_rt, .cancel_flag = std.atomic.Value(bool).init(false), .pending_prompt_id = null, }; durable_owned = false; revision_owned = false; session_id_owned = false; model_owned = false; session_rt_owned = false; try writeNewSessionResponse(state, alloc, msg, session_id); } pub fn commitWasmSessionLocked(alloc: Allocator, session: *server.ActiveSessionState) !void { const base = if (session.wasm_state) |*value| value else return error.SessionPersistenceUnavailable; var next = try base.dupe(alloc); var next_owned = true; defer if (next_owned) next.deinit(alloc); const history = try session.session_rt.snapshotHistory(alloc); types.freeHistoryTurnSlice(alloc, next.history); next.history = history; const permission_state = try session.session_rt.snapshotPermissionState(alloc); next.permission_state.deinit(alloc); next.permission_state = permission_state; next.context_history_start = 0; next.conversation_language = session.session_rt.languageSnapshot(); next.updated_at_ms = io_mod.milliTimestamp(); alloc.free(next.preferences.model); next.preferences.model = try alloc.dupe(u8, session.model); next.preferences.provider = session.provider; next.preferences.effort = session.effort; next.preferences.fast_mode = session.fast_mode; const usage = try session.session_rt.usage.snapshot(alloc); if (next.usage) |*old| old.deinit(alloc); next.usage = usage; const revision = try js_host_session_store.commit(alloc, next, session.wasm_revision); if (session.wasm_revision) |old| alloc.free(old); session.wasm_revision = revision; base.deinit(alloc); session.wasm_state = next; next_owned = false; } pub fn commitWasmSession(alloc: Allocator, session: *server.ActiveSessionState) !void { session.session_write_mutex.lockUncancelable(io_mod.getIo()); defer session.session_write_mutex.unlock(io_mod.getIo()); try commitWasmSessionLocked(alloc, session); } pub fn handleNewSession(state: *server.ServerState, alloc: Allocator, msg: *jsonrpc.Message) !void { var mcp_configs = mcp_servers.parse(alloc, msg.params_raw) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = mcp_servers.parseErrorMessage(err), }), }; defer mcp_configs.deinit(alloc); if (!state.cfg.allow_acp_mcp and mcp_configs.items.items.len > 0) { return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "MCP servers are unavailable in this runtime", }); } if (state.cfg.allow_acp_mcp) try appendProjectMcpConfigs(state, alloc, &mcp_configs); retireReducedActiveMcp(state, alloc, mcp_configs.items.items); var mcp_preparation = try mcp_servers.prepare( alloc, &mcp_configs, state.client_elicitation, server.legacyUrlCompletionSink(state), ); defer mcp_preparation.deinit(alloc); switch (mcp_preparation) { .ready => {}, .failed => |message| return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = message, }), } const session_mcp = mcp_preparation.takeRuntime(); var session_mcp_owned = true; defer if (session_mcp_owned) { if (session_mcp) |runtime| { runtime.deinit(); alloc.destroy(runtime); } }; var store = (if (state.cfg.home_override) |home| session_store.Store.initFromHome(alloc, home, state.workspace_root) else session_store.Store.init(alloc, state.workspace_root)) catch return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.internal_error, .message = "Session store not available", }); var store_owned = true; defer if (store_owned) store.deinit(alloc); var initial = try freshAcpState(state, alloc); defer initial.deinit(alloc); var writable = store.startWritableSessionWithOptions( alloc, initial, .{}, ) catch return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.internal_error, .message = "Failed to create session" }); var writable_owned = true; defer if (writable_owned) writable.deinit(alloc); const session_id = try alloc.dupe(u8, writable.active_id); var session_id_owned = true; defer if (session_id_owned) alloc.free(session_id); const model_copy = try alloc.dupe(u8, state.selected_model); var model_owned = true; defer if (model_owned) alloc.free(model_copy); const session_dir = try session_store.sessionDirPath(alloc, store.sessions_dir, writable.active_id); defer alloc.free(session_dir); var session_rt = session_runtime.SessionRuntime.initWithProviders( state.cfg.max_history_turns, state.cfg.provider_set.deferredUsageProviders(), ); var session_rt_owned = true; defer if (session_rt_owned) session_rt.deinit(alloc); _ = try session_rt.initializeProfileUsage(alloc, io_mod.getenv("HOME")); if (writable.state.usage) |usage| { try session_rt.usage.restore( alloc, usage, writable.state.created_at_ms, ); } else { session_rt.usage.restoreLegacyWallDuration( writable.state.created_at_ms, ); } writable.releaseHydrationHistory(alloc); session_rt.configureWebFetchArtifacts(alloc, session_dir); server.cancelAndReapActivePrompt(state); activateSession(state, store, .{ .session_id = session_id, .writable = writable, .model = model_copy, .provider = state.provider, .fast_mode = state.fast_mode, .effort = state.effort, .session_rt = session_rt, .mcp = session_mcp, }) catch { _ = store.discardPristineStartedSession(alloc, &writable); writable_owned = false; return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.internal_error, .message = "Failed to save active session", }); }; writable_owned = false; store_owned = false; session_id_owned = false; model_owned = false; session_rt_owned = false; session_mcp_owned = false; try writeNewSessionResponse(state, alloc, msg, session_id); } fn writeNewSessionResponse( state: *server.ServerState, alloc: Allocator, msg: *jsonrpc.Message, session_id: []const u8, ) !void { try server.refreshModelCatalogForOptions(state); var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"sessionId\":"); try writeJsonStr(session_id, &out.writer); try out.writer.writeAll(",\"configOptions\":["); if (comptime !host_target.is_wasm) { try writeProviderConfigOption(&out.writer, state.active_session.?.provider, state.configured_providers.definitions); try out.writer.writeAll(","); } try writeModelConfigOption( &out.writer, state.active_session.?.model, state.capability_resolver.catalogEntries(), ); try out.writer.writeAll(","); try writeModeConfigOption( &out.writer, state.cfg.mode_registry, state.cfg.mode_registry.default_mode_id, ); if (effortConfigState(state)) |config| { try out.writer.writeAll(","); try writeEffortConfigOption(&out.writer, config.efforts, config.current); } try out.writer.writeAll("],\"modes\":{\"currentModeId\":"); try writeJsonStr(state.cfg.mode_registry.default_mode_id, &out.writer); try out.writer.writeAll(",\"availableModes\":"); try writeModesArray(&out.writer, state.cfg.mode_registry); try out.writer.writeAll("}}"); try state.writer.writeResponse(alloc, msg.id, out.writer.buffered()); try sendAvailableCommands(state, alloc, session_id, "[]"); } pub fn handleLoadWasmSession(state: *server.ServerState, alloc: Allocator, msg: *jsonrpc.Message) !void { const params = msg.params_raw orelse return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Missing params", }); const parsed = std.json.parseFromSlice(std.json.Value, alloc, params, .{}) catch return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Invalid params" }); defer parsed.deinit(); const session_id = if (parsed.value == .object) if (parsed.value.object.get("sessionId")) |value| if (value == .string) value.string else return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Missing sessionId" }) else return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Missing sessionId" }) else return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Missing sessionId" }); if (state.active_session) |*active| { if (sameSessionId(active.session_id, session_id)) { for (active.session_rt.agent.history.items) |turn| try sendHistoryTurnAsUpdates(state, alloc, session_id, turn); try sendActiveSessionInfoUpdate(state, alloc); return writeLoadSessionResponse(state, alloc, msg, active.model); } } var loaded = (js_host_session_store.load(alloc, session_id) catch return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.internal_error, .message = "Session could not be loaded" })) orelse return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Session not found" }); var loaded_owned = true; defer if (loaded_owned) loaded.deinit(alloc); const sid_copy = try alloc.dupe(u8, loaded.state.id); var sid_owned = true; defer if (sid_owned) alloc.free(sid_copy); if (loaded.state.preferences.provider != .gateway) { return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_request, .message = "Subscription models are unavailable in this WASM runtime", }); } const model_copy = try alloc.dupe(u8, loaded.state.preferences.model); var model_owned = true; defer if (model_owned) alloc.free(model_copy); var session_rt = session_runtime.SessionRuntime.initWithProviders(state.cfg.max_history_turns, state.cfg.provider_set.deferredUsageProviders()); var session_rt_owned = true; defer if (session_rt_owned) session_rt.deinit(alloc); try session_rt.restoreWithPermissionState( alloc, loaded.state.conversation_language, loaded.state.history, loaded.state.permission_state, ); if (loaded.state.usage) |usage| try session_rt.usage.restore(alloc, usage, loaded.state.created_at_ms); try server.releaseActiveSession(state); state.active_session = .{ .session_id = sid_copy, .wasm_state = loaded.state, .wasm_revision = loaded.revision, .model = model_copy, .provider = loaded.state.preferences.provider, .mode = state.cfg.mode_registry.default_mode_id, .workspace_root = state.workspace_root, .api_key = state.api_key, .credential_source = state.credential_source, .credential_refresh_after_ms = state.credential_refresh_after_ms, .account_id = state.account_id, .agent_step_limit = state.agent_step_limit, .max_tool_result_bytes = state.max_tool_result_bytes, .fast_mode = loaded.state.preferences.fast_mode, .effort = loaded.state.preferences.effort, .first_call_tool_choice = state.first_call_tool_choice, .permission_mode = state.permission_mode, .permission_rules = state.permission_rules, .session_rt = session_rt, .cancel_flag = std.atomic.Value(bool).init(false), .pending_prompt_id = null, }; loaded_owned = false; sid_owned = false; model_owned = false; session_rt_owned = false; for (state.active_session.?.session_rt.agent.history.items) |turn| try sendHistoryTurnAsUpdates(state, alloc, session_id, turn); try sendActiveSessionInfoUpdate(state, alloc); try writeLoadSessionResponse(state, alloc, msg, state.active_session.?.model); } pub fn handleListWasmSessions(state: *server.ServerState, alloc: Allocator, msg: *jsonrpc.Message) !void { const entries = js_host_session_store.list(alloc) catch return state.writer.writeResponse(alloc, msg.id, "{\"sessions\":[]}"); defer { for (entries) |*entry| entry.deinit(alloc); alloc.free(entries); } var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"sessions\":["); for (entries, 0..) |entry, index| { if (index > 0) try out.writer.writeByte(','); try out.writer.writeAll("{\"sessionId\":"); try writeJsonStr(entry.id, &out.writer); try out.writer.writeAll(",\"cwd\":"); try writeJsonStr(state.workspace_root, &out.writer); try out.writer.writeAll(",\"updatedAt\":"); const iso = try formatIso8601(alloc, entry.updated_at_ms); defer alloc.free(iso); try writeJsonStr(iso, &out.writer); try out.writer.writeByte('}'); } try out.writer.writeAll("]}"); try state.writer.writeResponse(alloc, msg.id, out.writer.buffered()); } pub fn handleRemoveWasmSession(state: *server.ServerState, alloc: Allocator, msg: *jsonrpc.Message) !void { const params = msg.params_raw orelse return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Missing params" }); const parsed = std.json.parseFromSlice(std.json.Value, alloc, params, .{}) catch return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Invalid params" }); defer parsed.deinit(); const value = if (parsed.value == .object) parsed.value.object.get("sessionId") else null; const session_id = if (value) |id| if (id == .string) id.string else return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Missing sessionId" }) else return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Missing sessionId" }); js_host_session_store.remove(session_id) catch return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.internal_error, .message = "Failed to remove session" }); if (state.active_session) |*active| { if (sameSessionId(active.session_id, session_id)) try server.releaseActiveSession(state); } try state.writer.writeResponse(alloc, msg.id, "null"); } const RestoreKind = enum { load, reconnect, fn replaysHistory(self: RestoreKind) bool { return self == .load; } }; pub fn handleLoadSession(state: *server.ServerState, alloc: Allocator, msg: *jsonrpc.Message) !void { return handleRestoreSession(state, alloc, msg, .load); } pub fn handleResumeSession(state: *server.ServerState, alloc: Allocator, msg: *jsonrpc.Message) !void { return handleRestoreSession(state, alloc, msg, .reconnect); } fn handleRestoreSession( state: *server.ServerState, alloc: Allocator, msg: *jsonrpc.Message, kind: RestoreKind, ) !void { const params = msg.params_raw orelse return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Missing params", }); const parsed = std.json.parseFromSlice(std.json.Value, alloc, params, .{}) catch return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Invalid params" }); defer parsed.deinit(); const session_id = blk: { if (parsed.value == .object) { if (parsed.value.object.get("sessionId")) |v| { if (v == .string) break :blk v.string; } } return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Missing sessionId" }); }; var mcp_configs = switch (kind) { .load => mcp_servers.parse(alloc, msg.params_raw), .reconnect => mcp_servers.parseResume(alloc, msg.params_raw), } catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = mcp_servers.parseErrorMessage(err), }), }; defer mcp_configs.deinit(alloc); if (!state.cfg.allow_acp_mcp) { if (mcp_configs.items.items.len > 0) { return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "MCP servers are unavailable in this runtime", }); } } else { try appendProjectMcpConfigs(state, alloc, &mcp_configs); } retireReducedActiveMcp(state, alloc, mcp_configs.items.items); var mcp_preparation = try mcp_servers.prepare( alloc, &mcp_configs, state.client_elicitation, server.legacyUrlCompletionSink(state), ); defer mcp_preparation.deinit(alloc); switch (mcp_preparation) { .ready => {}, .failed => |message| return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = message, }), } const session_mcp = mcp_preparation.takeRuntime(); var session_mcp_owned = true; defer if (session_mcp_owned) { if (session_mcp) |runtime| { runtime.deinit(); alloc.destroy(runtime); } }; if (state.active_session) |*active| { if (sameSessionId(active.session_id, session_id)) { server.cancelAndReapActivePrompt(state); server.disableSubagentHost(state); state.subagent_authority_mutex.lockUncancelable(io_mod.getIo()); const previous_mcp = active.mcp; active.mcp = session_mcp; session_mcp_owned = false; server.applySessionMode( state.cfg.mode_registry, active, state.cfg.mode_registry.default_mode_id, ); state.subagent_authority_mutex.unlock(io_mod.getIo()); if (previous_mcp) |runtime| { runtime.retireAndWait(); runtime.deinit(); alloc.destroy(runtime); } server.enableSubagentHost(state); if (kind.replaysHistory()) { try sendActiveHistoryUpdates(state, alloc, session_id); } try sendPendingRecoveryUpdate( state, alloc, session_id, if (active.writable) |*writable| writable.state.recovery_checkpoint else null, ); try sendActiveSessionInfoUpdate(state, alloc); return writeLoadSessionResponse( state, alloc, msg, active.model, ); } } var store = (if (state.cfg.home_override) |home| session_store.Store.initFromHome(alloc, home, state.workspace_root) else session_store.Store.init(alloc, state.workspace_root)) catch return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.internal_error, .message = "Session store not available", }); var store_owned = true; defer if (store_owned) store.deinit(alloc); const seed_preferences = session_codec.DurableSessionPreferences{ .provider = state.provider, .model = state.configured_model, .effort = state.effort, .fast_mode = state.fast_mode, }; var writable = subagent_resume_admission.resumeForExternalPrompt( store, alloc, .{ .id = session_id }, state.workspace_root, .{ .seed_preferences = seed_preferences, .log = .{}, }, ) catch |err| return handleLoadFailure(state, alloc, msg, err); var writable_owned = true; defer if (writable_owned) writable.deinit(alloc); const sid_copy = try alloc.dupe(u8, writable.state.id); var sid_owned = true; defer if (sid_owned) alloc.free(sid_copy); const effective_provider = if (state.process_provider_override) state.provider else writable.state.preferences.provider; const effective_model = if (state.process_model_override or state.process_provider_override) state.selected_model else writable.state.preferences.model; var staged_credential = server.prepareCredentialForProvider(state, effective_provider) catch |err| { if (err != error.ProviderCredentialUnavailable) return err; return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_request, .message = if (effective_provider == .codex) credentials.missing_chatgpt_credential_message else if (effective_provider == .grok) credentials.missing_grok_credential_message else if (effective_provider == .configured) "Configured provider authentication is unavailable" else credentials.missing_credential_message, }); }; defer if (staged_credential) |*credential| credential.deinit(alloc); const model_copy = try alloc.dupe(u8, effective_model); var model_owned = true; defer if (model_owned) alloc.free(model_copy); var session_rt = session_runtime.SessionRuntime.initWithProviders( state.cfg.max_history_turns, state.cfg.provider_set.deferredUsageProviders(), ); var session_rt_owned = true; defer if (session_rt_owned) session_rt.deinit(alloc); _ = try session_rt.initializeProfileUsage(alloc, io_mod.getenv("HOME")); try session_rt.restoreWithPermissionState( alloc, writable.state.conversation_language, writable.state.history, writable.state.permission_state, ); if (writable.state.usage) |usage| { try session_rt.usage.restore( alloc, usage, writable.state.created_at_ms, ); } else { session_rt.usage.restoreLegacyWallDuration( writable.state.created_at_ms, ); } const session_dir = try session_store.sessionDirPath(alloc, store.sessions_dir, session_id); defer alloc.free(session_dir); writable.releaseHydrationHistory(alloc); session_rt.configureWebFetchArtifacts(alloc, session_dir); server.cancelAndReapActivePrompt(state); activateSession(state, store, .{ .session_id = sid_copy, .writable = writable, .model = model_copy, .provider = effective_provider, .credential = if (staged_credential) |*credential| credential else null, .fast_mode = writable.state.preferences.fast_mode, .effort = writable.state.preferences.effort, .session_rt = session_rt, .mcp = session_mcp, }) catch return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.internal_error, .message = "Failed to save active session", }); writable_owned = false; store_owned = false; sid_owned = false; model_owned = false; session_rt_owned = false; session_mcp_owned = false; if (kind.replaysHistory()) { try sendActiveHistoryUpdates(state, alloc, session_id); } try sendPendingRecoveryUpdate( state, alloc, session_id, state.active_session.?.writable.?.state.recovery_checkpoint, ); try sendActiveSessionInfoUpdate(state, alloc); try writeLoadSessionResponse( state, alloc, msg, state.active_session.?.model, ); } fn detachActiveMcpForAuthorityReduction( state: *server.ServerState, active: *server.ActiveSessionState, ) *mcp_runtime.McpRuntime { server.cancelAndReapActivePrompt(state); server.disableSubagentHost(state); state.subagent_authority_mutex.lockUncancelable(io_mod.getIo()); const previous = active.mcp.?; active.mcp = null; state.subagent_authority_mutex.unlock(io_mod.getIo()); return previous; } fn retireReducedActiveMcp( state: *server.ServerState, alloc: Allocator, next_configs: []const mcp_contract.McpServerConfig, ) void { const active = if (state.active_session) |*value| value else return; const runtime = active.mcp orelse return; if (!runtime.workspaceAuthorityReducedAgainstConfigs( next_configs, .acp_startup, )) return; const previous = detachActiveMcpForAuthorityReduction(state, active); previous.retireAndWait(); previous.deinit(); alloc.destroy(previous); server.enableSubagentHost(state); } fn appendProjectMcpConfigs( state: *server.ServerState, alloc: Allocator, configs: *mcp_servers.OwnedServerConfigs, ) !void { var choices = if (state.cfg.home_override) |home| config_runtime.loadProjectMcpChoicesFromHome(alloc, home, state.workspace_root) catch |err| { if (err == error.OutOfMemory) return error.OutOfMemory; debug_trace.logf("mcp", "ACP workspace MCP choices unavailable err={s}", .{@errorName(err)}); return; } else config_runtime.loadProjectMcpChoices(alloc, state.workspace_root) catch |err| { if (err == error.OutOfMemory) return error.OutOfMemory; debug_trace.logf("mcp", "ACP workspace MCP choices unavailable err={s}", .{@errorName(err)}); return; }; defer choices.deinit(alloc); var workspace = try workspace_config.load( alloc, state.workspace_root, .workspace, choices.choices, ); defer workspace.deinit(alloc); for (workspace.diagnostics.items) |diagnostic| { var name_buf: [256]u8 = undefined; var variable_buf: [160]u8 = undefined; debug_trace.logf( "mcp", "ACP workspace MCP config skipped cause={s} server={s} field={s} variable={s}", .{ @tagName(diagnostic.cause), if (diagnostic.server_name) |name| debug_trace.terminalPreview(name_buf[0..], name) else "none", if (diagnostic.environment_field) |field| @tagName(field) else "none", if (diagnostic.environment_variable) |name| debug_trace.terminalPreview(variable_buf[0..], name) else "none", }, ); } try project_config.appendWorkspaceAfterAcpPrimary( alloc, &configs.items, &workspace.configs, ); } fn sendPendingRecoveryUpdate( state: *server.ServerState, alloc: Allocator, session_id: []const u8, checkpoint: ?session_codec.RecoveryCheckpoint, ) !void { const recovery = checkpoint orelse return; try sendUserHistoryTurn(state, alloc, session_id, recovery.user); try sendExecutionHistory(state, alloc, session_id, recovery.execution); if (recovery.assistant_source.len > 0) { try sendAgentHistoryChunk(state, alloc, session_id, recovery.assistant_source); } const attempt = recovery.consumed_provider_attempts +| @intFromBool(recovery.outstanding_reservation); var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"sessionId\":"); try writeJsonStr(session_id, &out.writer); try out.writer.writeAll(",\"update\":"); try acp_types.writeModelRecoveryInfoUpdate(&out.writer, .{ .kind = .terminal_provider_error, .failed_attempt = attempt, .attempt_limit = recovery.max_provider_attempts, .cause = recovery.cause, .action = .paused, .required_action = if (recovery.tool_state == .uncertain) .inspect_uncertain_tool else .continue_later, .diagnostic = types.ModelFailureDiagnostic.forCause(recovery.cause), }, true); try out.writer.writeByte('}'); try state.writer.writeNotification( alloc, "session/update", out.writer.buffered(), ); } fn sameSessionId(active_session_id: []const u8, requested_session_id: []const u8) bool { return std.mem.eql(u8, active_session_id, requested_session_id); } fn writeLoadSessionResponse( state: *server.ServerState, alloc: Allocator, msg: *jsonrpc.Message, model: []const u8, ) !void { try server.refreshModelCatalogForOptions(state); var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"configOptions\":["); if (comptime !host_target.is_wasm) { try writeProviderConfigOption(&out.writer, state.active_session.?.provider, state.configured_providers.definitions); try out.writer.writeAll(","); } try writeModelConfigOption( &out.writer, model, state.capability_resolver.catalogEntries(), ); try out.writer.writeAll(","); try writeModeConfigOption( &out.writer, state.cfg.mode_registry, state.cfg.mode_registry.default_mode_id, ); if (effortConfigState(state)) |config| { try out.writer.writeAll(","); try writeEffortConfigOption(&out.writer, config.efforts, config.current); } try out.writer.writeAll("],\"modes\":{\"currentModeId\":"); try writeJsonStr(state.cfg.mode_registry.default_mode_id, &out.writer); try out.writer.writeAll(",\"availableModes\":"); try writeModesArray(&out.writer, state.cfg.mode_registry); try out.writer.writeAll("}}"); try state.writer.writeResponse(alloc, msg.id, out.writer.buffered()); } fn freshAcpState( state: *server.ServerState, alloc: Allocator, ) !session_codec.DurableSessionState { const now = io_mod.milliTimestamp(); const id = try session_store.generateSessionId(alloc); errdefer alloc.free(id); const origin = try alloc.dupe(u8, state.workspace_root); errdefer alloc.free(origin); const workspace = try alloc.dupe(u8, state.workspace_root); errdefer alloc.free(workspace); const model = try alloc.dupe(u8, state.configured_model); errdefer alloc.free(model); const history = try alloc.alloc(types.HistoryTurn, 0); errdefer alloc.free(history); return .{ .id = id, .origin_workspace_root = origin, .workspace_root = workspace, .created_at_ms = now, .updated_at_ms = now, .conversation_language = session_runtime.ConversationLanguage.default(), .preferences = .{ .provider = state.provider, .model = model, .effort = state.effort, .fast_mode = state.fast_mode, }, .history = history, .total_input_tokens = 0, .total_output_tokens = 0, }; } const SessionActivation = struct { session_id: []u8, writable: session_store.LoadedWritableSession, model: []u8, provider: model_provider.ProviderId, credential: ?*credentials.Credential = null, fast_mode: bool, effort: types.ReasoningEffort, session_rt: session_runtime.SessionRuntime, mcp: ?*mcp_runtime.McpRuntime, }; fn activateSession( state: *server.ServerState, store: session_store.Store, activation: SessionActivation, ) !void { try server.releaseActiveSession(state); if (activation.credential) |credential| server.adoptServerCredential(state, credential); state.active_session = .{ .session_id = activation.session_id, .store = store, .writable = activation.writable, .model = activation.model, .provider = activation.provider, .mode = state.cfg.mode_registry.default_mode_id, .workspace_root = state.workspace_root, .api_key = state.api_key, .credential_source = state.credential_source, .credential_refresh_after_ms = state.credential_refresh_after_ms, .account_id = state.account_id, .agent_step_limit = state.agent_step_limit, .max_tool_result_bytes = state.max_tool_result_bytes, .fast_mode = activation.fast_mode, .effort = activation.effort, .first_call_tool_choice = state.first_call_tool_choice, .permission_mode = state.permission_mode, .permission_rules = state.permission_rules, .session_rt = activation.session_rt, .mcp = activation.mcp, .cancel_flag = std.atomic.Value(bool).init(false), .pending_prompt_id = null, }; server.enableSubagentHost(state); state.active_session.?.session_rt.attachProfileUsagePublisher(state.alloc); if (state.cfg.provider_set.select(activation.provider).deferred_usage == null) { state.active_session.?.session_rt.usage.clearReconciliationCredential(); } else if (state.credential_source) |source| { state.active_session.?.session_rt.usage.replaceProviderReconciliationCredential( state.alloc, activation.provider, source, state.account_id, state.api_key, ); } else { state.active_session.?.session_rt.usage.clearReconciliationCredential(); } if (state.active_session.?.writable) |*writable| { if (writable.childCapability()) |capability| { _ = legacy_background_migration.migrate( state.alloc, capability, state.cfg.process_provider, ) catch |err| { debug_trace.logf( "session", "legacy process migration deferred session={s} err={s}", .{ writable.active_id, @errorName(err) }, ); }; } else |err| { debug_trace.logf( "session", "legacy process migration unavailable session={s} err={s}", .{ writable.active_id, @errorName(err) }, ); } } } fn handleLoadFailure( state: *server.ServerState, alloc: Allocator, msg: *jsonrpc.Message, err: anyerror, ) !void { debug_trace.logf( "acp", "session operation=load outcome=failed error={s}", .{@errorName(err)}, ); if (err == error.SessionWorkspaceRebindFailed) { return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.internal_error, .message = "Failed to persist session workspace rebind", }); } if (err == error.SessionBusy) { return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.internal_error, .message = "Session is busy", }); } if (err == error.InvalidSessionId) { return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Invalid session ID", }); } if (err == error.OneOffSessionNotResumable) { return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Subagent child sessions cannot be resumed directly", }); } if (err == error.InvalidSessionFormat or err == error.UnsupportedSessionSchema or err == error.LegacySessionTooLarge or err == error.LegacySessionReadResourceExhausted or err == error.SessionAuthorityBoundaryUnavailable or err == error.SessionAuthorityIntentCleanupPending or err == error.SessionPathUnsafe or err == error.DurablePathUnsafe) { return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.internal_error, .message = "Session could not be loaded", }); } return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Session not found", }); } pub fn handleListSessions(state: *server.ServerState, alloc: Allocator, msg: *jsonrpc.Message) !void { var params_arena = std.heap.ArenaAllocator.init(alloc); defer mem_utils.deinit_arena(params_arena); const params = parseListSessionsParams(params_arena.allocator(), msg) catch return state.writer.writeError(alloc, msg.id, .{ .code = ErrorCode.invalid_params, .message = "Invalid params", }); var store = (if (state.cfg.home_override) |home| session_store.Store.initReadOnlyFromHome(alloc, home, params.cwd orelse state.workspace_root) else session_store.Store.initReadOnly(alloc, params.cwd orelse state.workspace_root)) catch { try state.writer.writeResponse(alloc, msg.id, "{\"sessions\":[]}"); return; }; defer store.deinit(alloc); var page = subagent_resume_admission.listVisiblePage( store, alloc, if (params.cwd != null) .current_workspace else .all_workspaces, params.continuation, session_store.session_list_default_limit, ) catch { try state.writer.writeResponse(alloc, msg.id, "{\"sessions\":[]}"); return; }; defer page.deinit(alloc); var next_cursor_buf: [320]u8 = undefined; const next_cursor = if (page.has_more and page.summaries.items.len > 0) try std.fmt.bufPrint(&next_cursor_buf, "v1:{d}:{s}", .{ page.summaries.items[page.summaries.items.len - 1].updated_at_ms, page.summaries.items[page.summaries.items.len - 1].id, }) else null; var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"sessions\":["); var wrote_session = false; for (page.summaries.items) |summary| { const workspace_root = summary.workspace_root orelse { debug_trace.logf( "acp", "session operation=list outcome=omitted id={s} reason=workspace_unknown", .{summary.id}, ); continue; }; if (!std.fs.path.isAbsolute(workspace_root)) { debug_trace.logf( "acp", "session operation=list outcome=omitted id={s} reason=workspace_not_absolute", .{summary.id}, ); continue; } if (wrote_session) try out.writer.writeAll(","); wrote_session = true; try out.writer.writeAll("{\"sessionId\":"); try writeJsonStr(summary.id, &out.writer); try out.writer.writeAll(",\"cwd\":"); try writeJsonStr(workspace_root, &out.writer); if (summary.title) |title| { try out.writer.writeAll(",\"title\":"); try writeJsonStr(title, &out.writer); } try out.writer.writeAll(",\"updatedAt\":"); const iso = try formatIso8601(alloc, summary.updated_at_ms); defer alloc.free(iso); try writeJsonStr(iso, &out.writer); try out.writer.writeAll("}"); } try out.writer.writeAll("]"); if (next_cursor) |cursor| { try out.writer.writeAll(",\"nextCursor\":"); try writeJsonStr(cursor, &out.writer); } try out.writer.writeAll("}"); try state.writer.writeResponse(alloc, msg.id, out.writer.buffered()); } const ListSessionsParams = struct { cwd: ?[]const u8 = null, continuation: ?session_store.ResumableSessionContinuation = null, }; fn parseListSessionsParams( alloc: Allocator, msg: *jsonrpc.Message, ) !ListSessionsParams { const raw = msg.params_raw orelse return .{}; const parsed = std.json.parseFromSlice(std.json.Value, alloc, raw, .{}) catch return error.InvalidParams; if (parsed.value != .object) return error.InvalidParams; var result = ListSessionsParams{}; if (parsed.value.object.get("cwd")) |cwd| { if (cwd != .null) { if (cwd != .string or !std.fs.path.isAbsolute(cwd.string)) { return error.InvalidParams; } result.cwd = cwd.string; } } if (parsed.value.object.get("cursor")) |cursor| { if (cursor != .null) { if (cursor != .string) return error.InvalidParams; result.continuation = parseListCursor(cursor.string) catch return error.InvalidParams; } } return result; } fn parseListCursor(raw: []const u8) !session_store.ResumableSessionContinuation { if (raw.len == 0 or raw.len > 320) return error.InvalidParams; var fields = std.mem.splitScalar(u8, raw, ':'); if (!std.mem.eql(u8, fields.next() orelse return error.InvalidParams, "v1")) { return error.InvalidParams; } const updated_at_ms = std.fmt.parseInt( i64, fields.next() orelse return error.InvalidParams, 10, ) catch return error.InvalidParams; const id = fields.next() orelse return error.InvalidParams; if (updated_at_ms < 0 or fields.next() != null) return error.InvalidParams; session_store.validateSessionId(id) catch return error.InvalidParams; return .{ .updated_at_ms = updated_at_ms, .id = id }; } fn sendActiveHistoryUpdates(state: *server.ServerState, alloc: Allocator, session_id: []const u8) !void { const active = &state.active_session.?; if (active.store) |store| { const Visitor = struct { state: *server.ServerState, alloc: Allocator, session_id: []const u8, pub fn append(self: *@This(), turn: types.HistoryTurn) !void { try sendHistoryTurnAsUpdates(self.state, self.alloc, self.session_id, turn); } }; var visitor = Visitor{ .state = state, .alloc = alloc, .session_id = session_id }; return store.visitConversationHistory(alloc, session_id, &visitor); } for (active.session_rt.agent.history.items) |turn| { try sendHistoryTurnAsUpdates(state, alloc, session_id, turn); } } fn sendHistoryTurnAsUpdates(state: *server.ServerState, alloc: Allocator, session_id: []const u8, turn: types.HistoryTurn) !void { switch (turn) { .assistant => |assistant| try sendUserHistoryTurn(state, alloc, session_id, assistant.user), .interrupted => |interrupted| try sendUserHistoryTurn(state, alloc, session_id, interrupted.user), .compacted_summary => return, } switch (turn) { .assistant => |assistant| { try sendExecutionHistory(state, alloc, session_id, assistant.execution); try sendAgentHistoryChunk(state, alloc, session_id, assistant.assistant); }, .interrupted => |i| { try sendExecutionHistory(state, alloc, session_id, i.execution); if (i.assistant) |assistant| { if (assistant.len > 0) try sendAgentHistoryChunk(state, alloc, session_id, assistant); } try sendAgentHistoryChunk( state, alloc, session_id, session_runtime.interruptedTurnNotice(i).body, ); }, .compacted_summary => {}, } } fn sendUserHistoryTurn( state: *server.ServerState, alloc: Allocator, session_id: []const u8, user: types.UserTurn, ) !void { var message_id: acp_types.MessageIdBuffer = undefined; const stable_message_id = acp_types.generateMessageId(&message_id); if (user.text.len > 0) { try sendUserHistoryChunk(state, alloc, session_id, stable_message_id, user.text); } for (user.images) |attachment| { var snapshot = image_attachments.loadVerifiedSnapshot(alloc, attachment, .{}) catch |err| { if (err == error.OutOfMemory) return err; debug_trace.logf( "acp", "image history replay omitted id={d} err={s}", .{ attachment.id, @errorName(err) }, ); var unavailable: [96]u8 = undefined; const notice = try std.fmt.bufPrint( &unavailable, "Image #{d} unavailable", .{attachment.id}, ); try sendUserHistoryChunk(state, alloc, session_id, stable_message_id, notice); continue; }; defer snapshot.deinit(alloc); var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"sessionId\":"); try writeJsonStr(session_id, &out.writer); try out.writer.writeAll(",\"update\":"); try acp_types.writeUserImageChunk( &out.writer, stable_message_id, snapshot.media_type, snapshot.bytes, ); try out.writer.writeAll("}"); try state.writer.writeNotification(alloc, "session/update", out.writer.buffered()); } } fn sendUserHistoryChunk( state: *server.ServerState, alloc: Allocator, session_id: []const u8, message_id: []const u8, text: []const u8, ) !void { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"sessionId\":"); try writeJsonStr(session_id, &out.writer); try out.writer.writeAll(",\"update\":"); try acp_types.writeUserMessageChunk( &out.writer, message_id, text, ); try out.writer.writeAll("}"); try state.writer.writeNotification(alloc, "session/update", out.writer.buffered()); } fn sendExecutionHistory( state: *server.ServerState, alloc: Allocator, session_id: []const u8, execution: types.ExecutionMemory, ) !void { if (execution.isEmpty()) return; var arena_state = std.heap.ArenaAllocator.init(alloc); defer mem_utils.deinit_arena(arena_state); const frames = try planExecutionReplay( arena_state.allocator(), tool_call_presentation.activeToolRegistry(state), execution, ); for (frames) |frame| { switch (frame) { .assistant_text => |text| try sendAgentHistoryChunk(state, alloc, session_id, text), .tool_call => |call| try sendHistoryToolCall(state, alloc, session_id, call), } } } const ReplayToolCall = struct { id: []const u8, name: []const u8, title: []const u8, kind: acp_types.ToolCallKind, status: acp_types.ToolCallStatus, raw_input: ?std.json.Value, content_text: ?[]const u8, }; const ReplayFrame = union(enum) { assistant_text: []const u8, tool_call: ReplayToolCall, }; fn replayStatus(result: ?types.PersistedToolResult) acp_types.ToolCallStatus { const r = result orelse return .pending; return switch (r.status) { .success => .completed, .failure => .failed, }; } fn findToolResultIndex(results: []const types.PersistedToolResult, call_id: []const u8) ?usize { for (results, 0..) |result, i| { if (std.mem.eql(u8, result.tool_call_id, call_id)) return i; } return null; } fn planToolCallFrame( arena: Allocator, registry: tool_dispatch.Registry, call: types.ToolCall, result: ?types.PersistedToolResult, ) !ReplayToolCall { const name = tool_call_presentation.acpToolName(call.name); const title = tool_call_presentation.describeToolTitle(registry, arena, call) catch "Tool call"; const masked_arguments = try agent_execution_memory.redactToolArgumentsJson(arena, call.name, call.arguments_json); // Parsed with the arena and returned in the frame; no deinit, the caller's // arena frees it in bulk. const parsed: ?std.json.Parsed(std.json.Value) = std.json.parseFromSlice( std.json.Value, arena, masked_arguments, .{}, ) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, else => null, }; return .{ .id = call.id, .name = name, .title = title, .kind = tool_call_presentation.mapToolKind(name), .status = replayStatus(result), .raw_input = if (parsed) |p| p.value else null, // Result content follows the same unmasked tool-update content // contract as the live path; only arguments are masked for display // (redactToolArgumentsJson). .content_text = if (result) |r| if (r.output.len > 0) tool_call_presentation.toolUpdateContentText(r.status == .failure, r.output) else null else null, }; } /// Maps a persisted execution memory onto the same session/update frame /// sequence the live prompt path emits: interleaved assistant text plus one /// tool_call announcement (and final tool_call_update) per tool call. /// The returned frames borrow from `execution` and `arena`; the caller owns /// the slice and must free it with the arena. fn planExecutionReplay( arena: Allocator, registry: tool_dispatch.Registry, execution: types.ExecutionMemory, ) ![]ReplayFrame { var frames: std.ArrayList(ReplayFrame) = .empty; for (execution.tool_steps) |step| { if (step.assistant) |assistant| { if (assistant.len > 0) try frames.append(arena, .{ .assistant_text = assistant }); } const matched = try arena.alloc(bool, step.tool_results.len); @memset(matched, false); for (step.tool_calls) |call| { const result: ?types.PersistedToolResult = if (findToolResultIndex(step.tool_results, call.id)) |i| blk: { matched[i] = true; break :blk step.tool_results[i]; } else null; try frames.append(arena, .{ .tool_call = try planToolCallFrame(arena, registry, call, result) }); } // Results without a surviving call record (e.g. trimmed history) still // replay as completed frames so the transcript stays faithful. for (step.tool_results, 0..) |result, i| { if (matched[i]) continue; const call: types.ToolCall = .{ .id = result.tool_call_id, .name = result.tool_name, .arguments_json = "{}", }; try frames.append(arena, .{ .tool_call = try planToolCallFrame(arena, registry, call, result) }); } } return frames.toOwnedSlice(arena); } fn sendHistoryToolCall( state: *server.ServerState, alloc: Allocator, session_id: []const u8, call: ReplayToolCall, ) !void { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"sessionId\":"); try writeJsonStr(session_id, &out.writer); try out.writer.writeAll(",\"update\":"); try acp_types.writeToolCall(&out.writer, call.id, call.name, call.title, call.kind, .pending, call.raw_input); try out.writer.writeAll("}"); try state.writer.writeNotification(alloc, "session/update", out.writer.buffered()); if (call.status == .pending) return; out.clearRetainingCapacity(); try out.writer.writeAll("{\"sessionId\":"); try writeJsonStr(session_id, &out.writer); try out.writer.writeAll(",\"update\":"); try acp_types.writeToolCallUpdate(&out.writer, call.id, call.status, call.content_text); try out.writer.writeAll("}"); try state.writer.writeNotification(alloc, "session/update", out.writer.buffered()); } fn sendAgentHistoryChunk(state: *server.ServerState, alloc: Allocator, session_id: []const u8, text: []const u8) !void { var message_id: acp_types.MessageIdBuffer = undefined; var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"sessionId\":"); try writeJsonStr(session_id, &out.writer); try out.writer.writeAll(",\"update\":"); try acp_types.writeAgentMessageChunk( &out.writer, acp_types.generateMessageId(&message_id), text, ); try out.writer.writeAll("}"); try state.writer.writeNotification(alloc, "session/update", out.writer.buffered()); } fn sendAvailableCommands(state: *server.ServerState, alloc: Allocator, session_id: []const u8, commands_json: []const u8) !void { var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"sessionId\":"); try writeJsonStr(session_id, &out.writer); try out.writer.writeAll(",\"update\":"); try acp_types.writeAvailableCommandsUpdate(&out.writer, commands_json); try out.writer.writeAll("}"); try state.writer.writeNotification(alloc, "session/update", out.writer.buffered()); } pub fn sendActiveSessionInfoUpdate(state: *server.ServerState, alloc: Allocator) !void { const active = if (state.active_session) |*session| session else return; var metadata = try session_display_metadata.deriveFromHistory( alloc, active.session_rt.agent.history.items, ); defer metadata.deinit(alloc); if (active.writable) |*writable| { if (try writable.conversationTitle(alloc)) |title| { metadata.deinit(alloc); metadata = .{ .present = true, .title = title }; } } const updated_at_ms = if (active.writable) |*writable| writable.state.updated_at_ms else if (active.wasm_state) |durable| durable.updated_at_ms else io_mod.milliTimestamp(); const updated_at = try formatIso8601(alloc, @max(updated_at_ms, 0)); defer alloc.free(updated_at); var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"sessionId\":"); try writeJsonStr(active.session_id, &out.writer); try out.writer.writeAll(",\"update\":"); try acp_types.writeSessionInfoUpdate(&out.writer, metadata.title, updated_at); try out.writer.writeAll("}"); try state.writer.writeNotification(alloc, "session/update", out.writer.buffered()); } pub fn sendActiveSessionUsageUpdate(state: *server.ServerState, alloc: Allocator) !void { const active = if (state.active_session) |*session| session else return; const usage = active.session_rt.usage.liveContextSnapshot() orelse return; const provider_bundle = state.cfg.provider_set.select(active.provider); const capabilities = state.capability_resolver.available( active.model, provider_bundle.fallbackModelCapabilities(active.model), ); const context_window = capabilities.context_window orelse return; var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try out.writer.writeAll("{\"sessionId\":"); try writeJsonStr(active.session_id, &out.writer); try out.writer.writeAll(",\"update\":"); try acp_types.writeUsageUpdate( &out.writer, usage.used, context_window, usage.complete_cost, ); try out.writer.writeAll("}"); try state.writer.writeNotification(alloc, "session/update", out.writer.buffered()); } fn formatIso8601(alloc: Allocator, timestamp_ms: i64) ![]u8 { const epoch_secs: u64 = @intCast(@divTrunc(timestamp_ms, 1000)); const epoch = std.time.epoch.EpochSeconds{ .secs = epoch_secs }; const day = epoch.getDaySeconds(); const year_day = epoch.getEpochDay().calculateYearDay(); const month_day = year_day.calculateMonthDay(); return std.fmt.allocPrint(alloc, "{d:0>4}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}Z", .{ year_day.year, @intFromEnum(month_day.month), month_day.day_index + 1, day.getHoursIntoDay(), day.getMinutesIntoHour(), day.getSecondsIntoMinute(), }); } pub fn writeModelConfigOption( w: *std.Io.Writer, current: []const u8, catalog: ?[]const model_catalog.ModelCatalogEntry, ) !void { try w.writeAll("{\"id\":\"model\",\"name\":\"Model\",\"category\":\"model\",\"type\":\"select\",\"currentValue\":"); try writeJsonStr(current, w); try w.writeAll(",\"options\":["); const entries = catalog orelse &.{}; var wrote_current = false; for (entries, 0..) |entry, i| { const id = entry.id; if (i > 0) try w.writeAll(","); try w.writeAll("{\"value\":"); try writeJsonStr(id, w); try w.writeAll(",\"name\":"); try writeJsonStr(id, w); try w.writeAll("}"); if (std.mem.eql(u8, id, current)) wrote_current = true; } if (!wrote_current) { if (entries.len > 0) try w.writeAll(","); try w.writeAll("{\"value\":"); try writeJsonStr(current, w); try w.writeAll(",\"name\":"); try writeJsonStr(current, w); try w.writeAll("}"); } try w.writeAll("]}"); } pub fn writeProviderConfigOption( w: *std.Io.Writer, current: model_provider.ProviderId, definitions: []const @import("../core/config/configured_provider.zig").Definition, ) !void { try w.writeAll("{\"id\":\"provider\",\"name\":\"Provider\",\"category\":\"model\",\"type\":\"select\",\"currentValue\":"); try writeJsonStr(current.label(), w); try w.writeAll(",\"options\":[{\"value\":\"gateway\",\"name\":\"Vercel AI Gateway\"},{\"value\":\"codex\",\"name\":\"Codex subscription\"}"); if (comptime !host_target.is_wasm) { try w.writeAll(",{\"value\":\"grok\",\"name\":\"Grok subscription\"}"); for (definitions) |definition| { try w.writeAll(",{\"value\":"); try writeJsonStr(definition.id, w); try w.writeAll(",\"name\":"); try writeJsonStr(definition.id, w); try w.writeAll("}"); } } try w.writeAll("]}"); } pub fn writeModeConfigOption( w: *std.Io.Writer, registry: mode_registry.Registry, current: []const u8, ) !void { try w.writeAll("{\"id\":\"mode\",\"name\":\"Session Mode\",\"description\":\"Controls how the agent requests permission\",\"category\":\"mode\",\"type\":\"select\",\"currentValue\":"); try writeJsonStr(current, w); try w.writeAll(",\"options\":["); for (registry.modes, 0..) |mode, i| { if (i > 0) try w.writeAll(","); try w.writeAll("{\"value\":"); try writeJsonStr(mode.id, w); try w.writeAll(",\"name\":"); try writeJsonStr(mode.name, w); try w.writeAll(",\"description\":"); try writeJsonStr(mode.description, w); try w.writeAll(",\"permissionMode\":"); try writeJsonStr(@tagName(mode.permission_mode), w); try w.writeAll("}"); } try w.writeAll("]}"); } fn writeModesArray(w: *std.Io.Writer, registry: mode_registry.Registry) !void { try w.writeAll("["); for (registry.modes, 0..) |mode, i| { if (i > 0) try w.writeAll(","); try w.writeAll("{\"id\":"); try writeJsonStr(mode.id, w); try w.writeAll(",\"name\":"); try writeJsonStr(mode.name, w); try w.writeAll(",\"description\":"); try writeJsonStr(mode.description, w); try w.writeAll("}"); } try w.writeAll("]"); } pub const EffortConfigState = struct { efforts: model_capabilities.ReasoningEffortOptions, current: types.ReasoningEffort, }; /// Reasoning-effort selector state for the active session, or null when the /// active model advertises no effort options (matching the TUI, which hides /// the effort picker for those models). pub fn effortConfigState(state: *server.ServerState) ?EffortConfigState { const active = if (state.active_session) |*session| session else return null; const bundle = state.cfg.provider_set.select(active.provider); const capabilities = state.capability_resolver.available( active.model, bundle.fallbackModelCapabilities(active.model), ); if (capabilities.reasoning_efforts.len == 0) return null; return .{ .efforts = capabilities.reasoning_efforts, .current = active.effort }; } pub fn effortSupportedBy(efforts: model_capabilities.ReasoningEffortOptions, effort: types.ReasoningEffort) bool { if (effort == .auto) return true; for (efforts.slice()) |option| { if (option.eql(effort)) return true; } return false; } pub fn writeEffortConfigOption( w: *std.Io.Writer, efforts: model_capabilities.ReasoningEffortOptions, current: types.ReasoningEffort, ) !void { try w.writeAll("{\"id\":\"effort\",\"name\":\"Reasoning Effort\",\"description\":\"Controls how much the model thinks before responding\",\"category\":\"thought_level\",\"type\":\"select\",\"currentValue\":"); try writeJsonStr(current.label(), w); try w.writeAll(",\"options\":[{\"value\":\"auto\",\"name\":\"default\"}"); var current_listed = current == .auto; for (efforts.slice()) |effort| { if (effort.eql(current)) current_listed = true; try w.writeAll(",{\"value\":"); try writeJsonStr(effort.label(), w); try w.writeAll(",\"name\":"); try writeJsonStr(effort.displayLabel(), w); try w.writeAll("}"); } // A persisted effort the active model does not advertise still renders, so // the select never shows a value outside its own option list. if (!current_listed) { try w.writeAll(",{\"value\":"); try writeJsonStr(current.label(), w); try w.writeAll(",\"name\":"); try writeJsonStr(current.displayLabel(), w); try w.writeAll("}"); } try w.writeAll("]}"); } test "writeEffortConfigOption produces thought_level select with auto first" { const alloc = std.testing.allocator; var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); const efforts = model_capabilities.ReasoningEffortOptions.fromSlice(&.{ types.ReasoningEffort.literal("low"), types.ReasoningEffort.literal("high"), }); try writeEffortConfigOption(&out.writer, efforts, .literal("high")); const items = out.writer.buffered(); try std.testing.expect(std.mem.find(u8, items, "\"id\":\"effort\"") != null); try std.testing.expect(std.mem.find(u8, items, "\"category\":\"thought_level\"") != null); try std.testing.expect(std.mem.find(u8, items, "\"currentValue\":\"high\"") != null); const auto_index = std.mem.find(u8, items, "\"value\":\"auto\"").?; const low_index = std.mem.find(u8, items, "\"value\":\"low\"").?; try std.testing.expect(auto_index < low_index); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, items, .{}); defer parsed.deinit(); try std.testing.expectEqualStrings("select", parsed.value.object.get("type").?.string); } test "effortSupportedBy accepts auto and advertised names only" { const efforts = model_capabilities.ReasoningEffortOptions.fromSlice(&.{ types.ReasoningEffort.literal("low"), types.ReasoningEffort.literal("high"), }); try std.testing.expect(effortSupportedBy(efforts, .auto)); try std.testing.expect(effortSupportedBy(efforts, .literal("high"))); try std.testing.expect(!effortSupportedBy(efforts, .literal("max"))); } test "writeEffortConfigOption appends an unadvertised current effort" { const alloc = std.testing.allocator; var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); const efforts = model_capabilities.ReasoningEffortOptions.fromSlice(&.{ types.ReasoningEffort.literal("low"), }); try writeEffortConfigOption(&out.writer, efforts, .literal("max")); var parsed = try std.json.parseFromSlice(std.json.Value, alloc, out.writer.buffered(), .{}); defer parsed.deinit(); const options = parsed.value.object.get("options").?.array; try std.testing.expectEqual(@as(usize, 3), options.items.len); try std.testing.expectEqualStrings("max", options.items[2].object.get("value").?.string); } test "formatIso8601 produces valid format" { const alloc = std.testing.allocator; const result = try formatIso8601(alloc, 1700000000000); defer alloc.free(result); try std.testing.expect(result.len > 0); try std.testing.expect(std.mem.endsWith(u8, result, "Z")); try std.testing.expect(std.mem.find(u8, result, "T") != null); } test "formatIso8601 produces known timestamp" { const alloc = std.testing.allocator; const result = try formatIso8601(alloc, 0); defer alloc.free(result); try std.testing.expectEqualStrings("1970-01-01T00:00:00Z", result); } test "writeModelConfigOption produces valid json with single model fallback" { const alloc = std.testing.allocator; var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try writeModelConfigOption(&out.writer, "gpt-4", null); const items = out.writer.buffered(); try std.testing.expect(std.mem.find(u8, items, "\"id\":\"model\"") != null); try std.testing.expect(std.mem.find(u8, items, "\"currentValue\":\"gpt-4\"") != null); try std.testing.expect(std.mem.find(u8, items, "\"value\":\"gpt-4\"") != null); } test "writeModelConfigOption includes all cached model ids" { const alloc = std.testing.allocator; const entries = [_]model_catalog.ModelCatalogEntry{ .{ .id = @constCast("anthropic/claude-opus-4.6"), .model_type = @constCast("language") }, .{ .id = @constCast("openai/gpt-4o"), .model_type = @constCast("language") }, .{ .id = @constCast("xai/grok-3"), .model_type = @constCast("language") }, }; var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try writeModelConfigOption(&out.writer, "openai/gpt-4o", &entries); const items = out.writer.buffered(); try std.testing.expect(std.mem.find(u8, items, "\"currentValue\":\"openai/gpt-4o\"") != null); try std.testing.expect(std.mem.find(u8, items, "anthropic/claude-opus-4.6") != null); try std.testing.expect(std.mem.find(u8, items, "openai/gpt-4o") != null); try std.testing.expect(std.mem.find(u8, items, "xai/grok-3") != null); } test "writeModelConfigOption appends current model when not in cached list" { const alloc = std.testing.allocator; const entries = [_]model_catalog.ModelCatalogEntry{ .{ .id = @constCast("anthropic/claude-opus-4.6"), .model_type = @constCast("language") }, }; var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try writeModelConfigOption(&out.writer, "custom/my-model", &entries); const items = out.writer.buffered(); try std.testing.expect(std.mem.find(u8, items, "\"currentValue\":\"custom/my-model\"") != null); try std.testing.expect(std.mem.find(u8, items, "anthropic/claude-opus-4.6") != null); try std.testing.expect(std.mem.find(u8, items, "custom/my-model") != null); } test "writeModeConfigOption produces valid json with all modes" { const alloc = std.testing.allocator; var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try writeModeConfigOption(&out.writer, test_session_mode_registry, "inspect"); const items = out.writer.buffered(); try std.testing.expect(std.mem.find(u8, items, "\"id\":\"mode\"") != null); try std.testing.expect(std.mem.find(u8, items, "\"currentValue\":\"inspect\"") != null); try std.testing.expect(std.mem.find(u8, items, "\"review\"") != null); try std.testing.expect(std.mem.find(u8, items, "\"inspect\"") != null); try std.testing.expect(std.mem.find(u8, items, "\"permissionMode\":\"ask\"") != null); } test "writeModesArray preserves supplied registry order" { const alloc = std.testing.allocator; var out: std.Io.Writer.Allocating = .init(alloc); defer out.deinit(); try writeModesArray(&out.writer, test_session_mode_registry); const items = out.writer.buffered(); try std.testing.expect(items[0] == '['); try std.testing.expect(items[items.len - 1] == ']'); const review_index = std.mem.find(u8, items, "\"review\"") orelse return error.TestExpectedEqual; const inspect_index = std.mem.find(u8, items, "\"inspect\"") orelse return error.TestExpectedEqual; try std.testing.expect(review_index < inspect_index); } test "ACP load recognizes the retained active session exactly" { try std.testing.expect(sameSessionId( "release.2026.06", "release.2026.06", )); try std.testing.expect(!sameSessionId( "release.2026.06", "release.2026", )); } test "ACP history excludes typed summaries without filtering original user text" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(workspace); var capture = try tmp.dir.createFile(io_mod.getIo(), "history.jsonl", .{ .read = true }); defer capture.close(io_mod.getIo()); var state = try initAcpSessionTestState(arena, workspace, capture); defer state.deinit(); try sendHistoryTurnAsUpdates(&state, arena, "session-1", .{ .compacted_summary = .{ .summary = @constCast("internal summary"), .removed_turn_count = 1, .compaction_count = 1, } }); try std.testing.expectEqual(@as(u64, 0), try capture.length(io_mod.getIo())); const original = "Explain without hiding my question."; try sendHistoryTurnAsUpdates(&state, arena, "session-1", .{ .assistant = .{ .user = .{ .text = @constCast(original) }, .assistant = @constCast("original reply"), } }); var file = try tmp.dir.openFile(io_mod.getIo(), "history.jsonl", .{}); defer file.close(io_mod.getIo()); const captured = try io_mod.readFileToEnd(alloc, &file, 16 * 1024); defer alloc.free(captured); try std.testing.expect(std.mem.find(u8, captured, original) != null); try std.testing.expect(std.mem.find(u8, captured, "original reply") != null); try std.testing.expect(std.mem.find(u8, captured, "internal summary") == null); } test "ACP interrupted history replay hides model-only abort context" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(workspace); var capture = try tmp.dir.createFile( io_mod.getIo(), "acp-interrupted-history.jsonl", .{ .read = true }, ); defer capture.close(io_mod.getIo()); { var state = try initAcpSessionTestState(arena, workspace, capture); defer state.deinit(); var completed_tool_names = [_][]u8{@constCast("read_file")}; try sendHistoryTurnAsUpdates(&state, arena, "session-1", .{ .interrupted = .{ .user = .{ .text = @constCast("inspect the project") }, .completed_tool_names = completed_tool_names[0..], } }); try capture.sync(io_mod.getIo()); } var captured_file = try tmp.dir.openFile( io_mod.getIo(), "acp-interrupted-history.jsonl", .{}, ); defer captured_file.close(io_mod.getIo()); const captured = try io_mod.readFileToEnd(alloc, &captured_file, 16 * 1024); defer alloc.free(captured); try std.testing.expect(std.mem.find(u8, captured, "cancelled") != null); try std.testing.expect(std.mem.find(u8, captured, "Interrupted by user after completing") == null); try std.testing.expect(std.mem.find(u8, captured, "") == null); } fn readCaptured(capture_file: *std.Io.File, alloc: Allocator) ![]u8 { try capture_file.sync(io_mod.getIo()); return io_mod.readFileToEnd(alloc, capture_file, 1024 * 1024); } test "ACP history replay emits structured tool call frames" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(workspace); var capture = try tmp.dir.createFile(io_mod.getIo(), "history.jsonl", .{ .read = true }); defer capture.close(io_mod.getIo()); var state = try initAcpSessionTestState(arena, workspace, capture); defer state.deinit(); var calls = [_]types.ToolCall{ .{ .id = "call_read_1", .name = "read_file", .arguments_json = "{\"path\":\"README.md\"}" }, .{ .id = "call_write_1", .name = "write_file", .arguments_json = "{\"path\":\"out.txt\",\"content\":\"done\"}" }, }; var results = [_]types.PersistedToolResult{ .{ .tool_call_id = @constCast("call_read_1"), .tool_name = @constCast("read_file"), .status = .success, .output = @constCast("readme text"), .output_bytes = 27, .stored_output_bytes = 27, }, .{ .tool_call_id = @constCast("call_write_1"), .tool_name = @constCast("write_file"), .status = .failure, .output = @constCast("permission denied"), .output_bytes = 17, .stored_output_bytes = 17, }, }; var steps = [_]types.ToolExecutionStep{.{ .assistant = @constCast("Let me inspect those files."), .tool_calls = calls[0..], .tool_results = results[0..], }}; try sendHistoryTurnAsUpdates(&state, arena, "session-1", .{ .assistant = .{ .user = .{ .text = @constCast("read and write") }, .assistant = @constCast("All done."), .execution = .{ .tool_steps = steps[0..] }, } }); const captured = try readCaptured(&capture, alloc); defer alloc.free(captured); try std.testing.expect(std.mem.find(u8, captured, "Previous tool execution") == null); try std.testing.expect(std.mem.find(u8, captured, "Let me inspect those files.") != null); try std.testing.expect(std.mem.find(u8, captured, "All done.") != null); const announce_read = std.mem.find(u8, captured, "\"sessionUpdate\":\"tool_call\",\"toolCallId\":\"call_read_1\"").?; const announce_write = std.mem.find(u8, captured, "\"sessionUpdate\":\"tool_call\",\"toolCallId\":\"call_write_1\"").?; const finish_read = std.mem.find(u8, captured, "\"sessionUpdate\":\"tool_call_update\",\"toolCallId\":\"call_read_1\"").?; const finish_write = std.mem.find(u8, captured, "\"sessionUpdate\":\"tool_call_update\",\"toolCallId\":\"call_write_1\"").?; try std.testing.expect(announce_read < finish_read); try std.testing.expect(announce_write < finish_write); try std.testing.expect(announce_read < announce_write); try std.testing.expect(std.mem.find(u8, captured, "\"name\":\"read_file\"") != null); try std.testing.expect(std.mem.find(u8, captured, "\"kind\":\"read\"") != null); try std.testing.expect(std.mem.find(u8, captured, "\"rawInput\":{\"path\":\"README.md\"}") != null); try std.testing.expect(std.mem.find(u8, captured, "\"status\":\"completed\"") != null); try std.testing.expect(std.mem.find(u8, captured, "\"status\":\"failed\"") != null); try std.testing.expect(std.mem.find(u8, captured, "readme text") != null); try std.testing.expect(std.mem.find(u8, captured, "permission denied") != null); } test "ACP history replay leaves resultless tool calls pending" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(workspace); var capture = try tmp.dir.createFile(io_mod.getIo(), "history.jsonl", .{ .read = true }); defer capture.close(io_mod.getIo()); var state = try initAcpSessionTestState(arena, workspace, capture); defer state.deinit(); var calls = [_]types.ToolCall{ .{ .id = "call_orphan", .name = "read_file", .arguments_json = "{\"path\":\"a.txt\"}" }, }; var steps = [_]types.ToolExecutionStep{.{ .tool_calls = calls[0..] }}; try sendHistoryTurnAsUpdates(&state, arena, "session-1", .{ .interrupted = .{ .user = .{ .text = @constCast("inspect") }, .execution = .{ .tool_steps = steps[0..] }, } }); const captured = try readCaptured(&capture, alloc); defer alloc.free(captured); try std.testing.expect(std.mem.find(u8, captured, "\"sessionUpdate\":\"tool_call\",\"toolCallId\":\"call_orphan\"") != null); try std.testing.expect(std.mem.find(u8, captured, "\"sessionUpdate\":\"tool_call_update\"") == null); try std.testing.expect(std.mem.find(u8, captured, "Previous tool execution") == null); } test "ACP history replay redacts sensitive tool arguments" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "."); defer alloc.free(workspace); var capture = try tmp.dir.createFile(io_mod.getIo(), "history.jsonl", .{ .read = true }); defer capture.close(io_mod.getIo()); var state = try initAcpSessionTestState(arena, workspace, capture); defer state.deinit(); var calls = [_]types.ToolCall{ .{ .id = "call_secret", .name = "run_command", .arguments_json = "{\"command\":\"echo ok\",\"api_key\":\"sk-live-secret\"}" }, }; var results = [_]types.PersistedToolResult{.{ .tool_call_id = @constCast("call_secret"), .tool_name = @constCast("run_command"), .status = .success, .output = @constCast("ok"), .output_bytes = 2, .stored_output_bytes = 2, }}; var steps = [_]types.ToolExecutionStep{.{ .tool_calls = calls[0..], .tool_results = results[0..] }}; try sendHistoryTurnAsUpdates(&state, arena, "session-1", .{ .assistant = .{ .user = .{ .text = @constCast("run it") }, .assistant = @constCast("done"), .execution = .{ .tool_steps = steps[0..] }, } }); const captured = try readCaptured(&capture, alloc); defer alloc.free(captured); try std.testing.expect(std.mem.find(u8, captured, "sk-live-secret") == null); try std.testing.expect(std.mem.find(u8, captured, "[REDACTED]") != null); } test "execution replay plan interleaves assistant text and orphan results" { const alloc = std.testing.allocator; var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); var calls = [_]types.ToolCall{ .{ .id = "call_a", .name = "read_file", .arguments_json = "{\"path\":\"a.txt\"}" }, }; var results = [_]types.PersistedToolResult{ .{ .tool_call_id = @constCast("call_a"), .tool_name = @constCast("read_file"), .status = .success, .output = @constCast("alpha"), .output_bytes = 5, .stored_output_bytes = 5, }, .{ .tool_call_id = @constCast("call_orphaned"), .tool_name = @constCast("write_file"), .status = .failure, .output = @constCast("denied"), .output_bytes = 6, .stored_output_bytes = 6, }, }; var steps = [_]types.ToolExecutionStep{.{ .assistant = @constCast("working"), .tool_calls = calls[0..], .tool_results = results[0..], }}; const frames = try planExecutionReplay( arena, @import("../builtins/tools.zig").registry, .{ .tool_steps = steps[0..] }, ); try std.testing.expectEqual(@as(usize, 3), frames.len); try std.testing.expectEqualStrings("working", frames[0].assistant_text); try std.testing.expectEqualStrings("call_a", frames[1].tool_call.id); try std.testing.expectEqual(acp_types.ToolCallStatus.completed, frames[1].tool_call.status); try std.testing.expectEqual(acp_types.ToolCallKind.read, frames[1].tool_call.kind); try std.testing.expectEqualStrings("alpha", frames[1].tool_call.content_text.?); try std.testing.expectEqualStrings("call_orphaned", frames[2].tool_call.id); try std.testing.expectEqual(acp_types.ToolCallStatus.failed, frames[2].tool_call.status); try std.testing.expect(frames[2].tool_call.raw_input != null); } test "ACP load maps one-off child denial to invalid params" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const workspace = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "workspace"); defer alloc.free(workspace); var capture = try tmp.dir.createFile( io_mod.getIo(), "acp-one-off-error.jsonl", .{ .read = true }, ); defer capture.close(io_mod.getIo()); { var state = try initAcpSessionTestState(arena, workspace, capture); defer state.deinit(); var msg = jsonrpc.Message{ .id = .{ .integer = 1 }, .method = "session/load", }; try handleLoadFailure( &state, arena, &msg, error.OneOffSessionNotResumable, ); try std.testing.expect(state.active_session == null); try capture.sync(io_mod.getIo()); } var captured_file = try tmp.dir.openFile( io_mod.getIo(), "acp-one-off-error.jsonl", .{}, ); defer captured_file.close(io_mod.getIo()); const captured = try io_mod.readFileToEnd(alloc, &captured_file, 4096); defer alloc.free(captured); try std.testing.expect(std.mem.find(u8, captured, "\"code\":-32602") != null); try std.testing.expect(std.mem.find( u8, captured, "Subagent child sessions cannot be resumed directly", ) != null); } var acp_session_stable_test_environ: ?*std.process.Environ.Map = null; fn stableAcpSessionTestEnviron() !*const std.process.Environ.Map { if (acp_session_stable_test_environ) |map| return map; const alloc = std.heap.page_allocator; const map = try alloc.create(std.process.Environ.Map); map.* = std.process.Environ.Map.init(alloc); acp_session_stable_test_environ = map; return map; } const AcpSessionTestHome = struct { alloc: Allocator, map: std.process.Environ.Map, fn install(alloc: Allocator, home: []const u8) !*AcpSessionTestHome { _ = try stableAcpSessionTestEnviron(); const self = try alloc.create(AcpSessionTestHome); errdefer alloc.destroy(self); self.* = .{ .alloc = alloc, .map = std.process.Environ.Map.init(alloc), }; errdefer self.map.deinit(); try self.map.put("HOME", home); io_mod.setEnvironMap(&self.map); return self; } fn deinit(self: *AcpSessionTestHome) void { if (acp_session_stable_test_environ) |map| { io_mod.setEnvironMap(map); } self.map.deinit(); const alloc = self.alloc; alloc.destroy(self); } }; fn gatherNoopContextForTest(_: Allocator, _: context_contract.InitialContextInput) context_contract.ProviderError!context_contract.ProviderContext { return .{}; } fn appendNoopStaticContextForTest(_: context_contract.StaticContextInput, _: Allocator, _: *std.ArrayList(types.ChatMessage)) context_contract.ProviderError!void {} fn appendNoopTransientContextForTest(_: context_contract.TransientContextInput, _: Allocator, _: *std.ArrayList(types.ChatMessage)) context_contract.ProviderError!void {} const test_session_context_registry = context_contract.Registry{ .default_provider = .{ .id = "test.acp_session_context", .gather_project_context_fn = gatherNoopContextForTest, .select_applicable_project_context_fn = context_contract.selectNoApplicableProjectContext, .append_static_fn = appendNoopStaticContextForTest, .append_transient_fn = appendNoopTransientContextForTest, } }; const test_session_modes = [_]mode_registry.ModeSpec{ .{ .id = "review", .name = "Review", .description = "Review changes" }, .{ .id = "inspect", .name = "Inspect", .description = "Inspect a workspace" }, }; const test_session_mode_registry = mode_registry.Registry{ .default_mode_id = "inspect", .modes = test_session_modes[0..], }; fn acpSessionTestConfig() server.Config { return .{ .default_model = "test/model", .default_agent_step_limit = 8, .gateway_retry_count = 0, .gateway_chat_url = "http://127.0.0.1/unused", .gateway_models_path = "/v1/models", .gateway_provider = test_builtin_gateway.provider, .provider_set = provider_set.gateway_only(test_builtin_gateway.provider_bundle), .secret_store = host.unavailable_secret_store, .prompt_policy = .{ .system_prompt = "test" }, .ignored_list_entries = &.{}, .max_list_entries = 100, .max_read_file_bytes = 64 * 1024, .max_read_file_lines = 1000, .max_read_file_line_len = 4096, .max_command_output_bytes = 64 * 1024, .max_tool_result_bytes = 64 * 1024, .max_history_turns = 100, .context_registry = test_session_context_registry, .mode_registry = test_session_mode_registry, }; } fn initAcpSessionTestState( alloc: Allocator, workspace_root: []const u8, capture: std.Io.File, ) !server.ServerState { const workspace = try alloc.dupe(u8, workspace_root); errdefer alloc.free(workspace); const api_key = try alloc.dupe(u8, "test-key"); errdefer alloc.free(api_key); const selected_model = try alloc.dupe(u8, "test/model"); errdefer alloc.free(selected_model); const configured_model = try alloc.dupe(u8, "test/model"); errdefer alloc.free(configured_model); return .{ .alloc = alloc, .cfg = acpSessionTestConfig(), .writer = .{ .stdout = capture }, .workspace_root = workspace, .api_key = api_key, .credential_source = .ai_gateway_api_key, .selected_model = selected_model, .configured_model = configured_model, .agent_step_limit = 8, .max_tool_result_bytes = 64 * 1024, }; } test "ACP project MCP loading expands workspace environment templates" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); try tmp.dir.writeFile(io_mod.getIo(), .{ .sub_path = "workspace/.mcp.json", .data = \\{"mcpServers":{"expanded":{"command":"${ACP_MCP_COMMAND}","args":["${ACP_MCP_ARG:-fallback}"],"env":{"TOKEN":"${ACP_MCP_TOKEN}"}}}} , }); const home_path = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "home"); defer alloc.free(home_path); const workspace_path = try io_mod.dirRealpathAlloc( alloc, tmp.dir, "workspace", ); defer alloc.free(workspace_path); const test_home = try AcpSessionTestHome.install(alloc, home_path); defer test_home.deinit(); try test_home.map.put("ACP_MCP_COMMAND", "node"); try test_home.map.put("ACP_MCP_TOKEN", "secret-value"); var approved_names = [_][]u8{@constCast("expanded")}; var result = try workspace_config.load( alloc, workspace_path, .workspace, .{ .approved = &approved_names }, ); defer result.deinit(alloc); try std.testing.expectEqual(@as(usize, 1), result.configs.items.len); const config = result.configs.items[0]; try std.testing.expectEqualStrings("node", config.command.?); try std.testing.expectEqualStrings("fallback", config.args[0]); try std.testing.expectEqualStrings("secret-value", config.env[0].value); } test "ACP host-disabled new load and resume skip project MCP effects" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_path = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "home"); defer alloc.free(home_path); const workspace_path = try io_mod.dirRealpathAlloc( alloc, tmp.dir, "workspace", ); defer alloc.free(workspace_path); const marker_path = try std.fs.path.join( alloc, &.{ workspace_path, "project-mcp-launched" }, ); defer alloc.free(marker_path); const project_json = try std.fmt.allocPrint( alloc, "{{\"mcpServers\":{{\"fixture\":{{\"command\":\"/bin/sh\",\"args\":[\"-c\",\"printf launched > {s}\"]}},\"remote\":{{\"type\":\"http\",\"url\":\"http://127.0.0.1:1/mcp\",\"startup_timeout_ms\":5000}}}}}}", .{marker_path}, ); defer alloc.free(project_json); try tmp.dir.writeFile(io_mod.getIo(), .{ .sub_path = "workspace/.mcp.json", .data = project_json, }); try tmp.dir.writeFile(io_mod.getIo(), .{ .sub_path = "home/.fx/settings.json", .data = "{}", }); const test_home = try AcpSessionTestHome.install(alloc, home_path); defer test_home.deinit(); var capture = try tmp.dir.createFile( io_mod.getIo(), "acp-host-disabled.jsonl", .{ .read = true }, ); defer capture.close(io_mod.getIo()); var state = try initAcpSessionTestState(arena, workspace_path, capture); defer state.deinit(); state.cfg.allow_acp_mcp = false; var new_msg = jsonrpc.Message{ .id = .{ .integer = 1 }, .method = "session/new", .params_raw = "{\"mcpServers\":[]}", }; try handleNewSession(&state, arena, &new_msg); try std.testing.expect(state.active_session.?.mcp == null); const session_id = try alloc.dupe(u8, state.active_session.?.session_id); defer alloc.free(session_id); inline for (.{ .{ .method = "session/load", .handler = handleLoadSession }, .{ .method = "session/resume", .handler = handleResumeSession }, }, 0..) |restore, index| { const params = try std.fmt.allocPrint( arena, "{{\"sessionId\":\"{s}\",\"mcpServers\":[]}}", .{session_id}, ); var msg = jsonrpc.Message{ .id = .{ .integer = @intCast(index + 2) }, .method = restore.method, .params_raw = params, }; try restore.handler(&state, arena, &msg); try std.testing.expect(state.active_session.?.mcp == null); } const local_request = try std.fmt.allocPrint( arena, "{{\"name\":\"request-local\",\"command\":\"/bin/sh\",\"args\":[\"-c\",\"printf launched > {s}\"],\"env\":[]}}", .{marker_path}, ); const remote_request = "{\"type\":\"http\",\"name\":\"request-remote\",\"url\":\"http://127.0.0.1:1/mcp\",\"headers\":[]}"; inline for (.{ .{ .method = "session/new", .handler = handleNewSession, .server = local_request }, .{ .method = "session/load", .handler = handleLoadSession, .server = remote_request }, .{ .method = "session/resume", .handler = handleResumeSession, .server = local_request }, }, 0..) |request, index| { const params = if (std.mem.eql(u8, request.method, "session/new")) try std.fmt.allocPrint( arena, "{{\"mcpServers\":[{s}]}}", .{request.server}, ) else try std.fmt.allocPrint( arena, "{{\"sessionId\":\"{s}\",\"mcpServers\":[{s}]}}", .{ session_id, request.server }, ); var msg = jsonrpc.Message{ .id = .{ .integer = @intCast(index + 10) }, .method = request.method, .params_raw = params, }; try request.handler(&state, arena, &msg); try std.testing.expect(state.active_session.?.mcp == null); try std.testing.expectEqualStrings( session_id, state.active_session.?.session_id, ); } try std.testing.expectError( error.FileNotFound, tmp.dir.openFile(io_mod.getIo(), "workspace/project-mcp-launched", .{}), ); try capture.sync(io_mod.getIo()); var captured_file = try tmp.dir.openFile( io_mod.getIo(), "acp-host-disabled.jsonl", .{}, ); defer captured_file.close(io_mod.getIo()); const captured = try io_mod.readFileToEnd(alloc, &captured_file, 64 * 1024); defer alloc.free(captured); try std.testing.expectEqual( @as(usize, 3), std.mem.count(u8, captured, "MCP servers are unavailable in this runtime"), ); } test "ACP new and loaded sessions provide a writable subagent host" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_path = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "home"); defer alloc.free(home_path); const workspace_path = try io_mod.dirRealpathAlloc( alloc, tmp.dir, "workspace", ); defer alloc.free(workspace_path); const test_home = try AcpSessionTestHome.install(alloc, home_path); defer test_home.deinit(); var capture = try tmp.dir.createFile( io_mod.getIo(), "acp-output.jsonl", .{ .read = true }, ); defer capture.close(io_mod.getIo()); { var state = try initAcpSessionTestState(arena, workspace_path, capture); defer state.deinit(); var new_msg = jsonrpc.Message{ .id = .{ .integer = 1 }, .method = "session/new", .params_raw = "{\"mcpServers\":[]}", }; try handleNewSession(&state, arena, &new_msg); const new_active = &state.active_session.?; const new_writable = &new_active.writable.?; try std.testing.expectEqualStrings( test_session_mode_registry.default_mode_id, new_active.mode, ); server.applySessionMode( state.cfg.mode_registry, new_active, "review", ); try std.testing.expectEqualStrings("review", new_active.mode); try std.testing.expect(new_writable.state.usage != null); try std.testing.expect( new_active.session_rt.usage.generation_usage_providers.select(.gateway).?.lookup_fn == state.cfg.provider_set.deferredUsageProviders().select(.gateway).?.lookup_fn, ); io_mod.sleep(10 * std.time.ns_per_ms); var live_usage = try new_active.session_rt.usage.snapshot(alloc); defer live_usage.deinit(alloc); try std.testing.expect(live_usage.wall_duration_ms > 0); try std.testing.expect(state.subagent_store != null); try std.testing.expect(state.subagent_host != null); _ = try new_writable.appendEvent(arena, .{ .history_turn_committed = .{ .conversation_language = .literal("en"), .total_input_tokens = 0, .total_output_tokens = 0, .turn = .{ .assistant = .{ .user = .{ .text = @constCast("remember this") }, .assistant = @constCast("retained answer"), } }, } }, io_mod.milliTimestamp()); const session_id = try alloc.dupe(u8, new_active.session_id); defer alloc.free(session_id); try server.releaseActiveSession(&state); try std.testing.expect(state.active_session == null); try std.testing.expect(state.subagent_store == null); try std.testing.expect(state.subagent_host == null); var load_params: std.Io.Writer.Allocating = .init(arena); defer load_params.deinit(); try load_params.writer.writeAll("{\"sessionId\":"); try writeJsonStr(session_id, &load_params.writer); try load_params.writer.writeAll(",\"mcpServers\":[]}"); var load_msg = jsonrpc.Message{ .id = .{ .integer = 2 }, .method = "session/load", .params_raw = load_params.writer.buffered(), }; try handleLoadSession(&state, arena, &load_msg); const loaded_active = &state.active_session.?; const loaded_writable = &loaded_active.writable.?; try std.testing.expectEqual(@as(usize, 1), loaded_active.session_rt.historyLen()); try std.testing.expectEqual(@as(usize, 0), loaded_writable.state.history.len); try std.testing.expectEqualStrings( test_session_mode_registry.default_mode_id, loaded_active.mode, ); try std.testing.expect(loaded_writable.state.usage != null); try std.testing.expect(state.subagent_store != null); try std.testing.expect(state.subagent_host != null); try std.testing.expect( loaded_active.session_rt.usage.generation_usage_providers.select(.gateway).?.lookup_fn == state.cfg.provider_set.deferredUsageProviders().select(.gateway).?.lookup_fn, ); try capture.sync(io_mod.getIo()); } var captured_file = try tmp.dir.openFile( io_mod.getIo(), "acp-output.jsonl", .{}, ); defer captured_file.close(io_mod.getIo()); const captured = try io_mod.readFileToEnd( alloc, &captured_file, 64 * 1024, ); defer alloc.free(captured); try std.testing.expect(std.mem.find(u8, captured, "\"id\":1") != null); try std.testing.expect(std.mem.find(u8, captured, "\"id\":2") != null); } test "ACP same-session restore retires the replaced MCP runtime after active users drain" { const alloc = std.testing.allocator; var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); var arena_state = std.heap.ArenaAllocator.init(alloc); defer arena_state.deinit(); const arena = arena_state.allocator(); try tmp.dir.createDirPath(io_mod.getIo(), "home/.fx"); try tmp.dir.createDirPath(io_mod.getIo(), "workspace"); const home_path = try io_mod.dirRealpathAlloc(alloc, tmp.dir, "home"); defer alloc.free(home_path); const workspace_path = try io_mod.dirRealpathAlloc( alloc, tmp.dir, "workspace", ); defer alloc.free(workspace_path); const test_home = try AcpSessionTestHome.install(alloc, home_path); defer test_home.deinit(); var capture = try tmp.dir.createFile( io_mod.getIo(), "acp-replace-output.jsonl", .{ .read = true }, ); defer capture.close(io_mod.getIo()); var state = try initAcpSessionTestState(arena, workspace_path, capture); defer state.deinit(); var new_msg = jsonrpc.Message{ .id = .{ .integer = 1 }, .method = "session/new", .params_raw = "{\"mcpServers\":[]}", }; try handleNewSession(&state, arena, &new_msg); const runtime = try arena.create(mcp_runtime.McpRuntime); runtime.* = mcp_runtime.McpRuntime.init(arena); state.active_session.?.mcp = runtime; try std.testing.expect(runtime.acquireUse()); var load_params: std.Io.Writer.Allocating = .init(arena); defer load_params.deinit(); try load_params.writer.writeAll("{\"sessionId\":"); try writeJsonStr(state.active_session.?.session_id, &load_params.writer); try load_params.writer.writeAll(",\"mcpServers\":[]}"); var load_msg = jsonrpc.Message{ .id = .{ .integer = 2 }, .method = "session/load", .params_raw = load_params.writer.buffered(), }; const Restore = struct { state: *server.ServerState, alloc: Allocator, msg: *jsonrpc.Message, done: std.atomic.Value(bool) = .init(false), err: ?anyerror = null, fn run(self: *@This()) void { handleLoadSession(self.state, self.alloc, self.msg) catch |err| { self.err = err; }; self.done.store(true, .release); } }; var restore = Restore{ .state = &state, .alloc = arena, .msg = &load_msg, }; const restore_thread = try std.Thread.spawn(.{}, Restore.run, .{&restore}); var joined = false; defer if (!joined) restore_thread.join(); const observation_deadline = io_mod.milliTimestamp() + 5_000; while (!restore.done.load(.acquire) and !runtime.retiring.load(.acquire) and io_mod.milliTimestamp() < observation_deadline) { io_mod.sleep(std.time.ns_per_ms); } const retired_before_destroy = runtime.retiring.load(.acquire); const completed_while_leased = restore.done.load(.acquire); runtime.releaseUse(); restore_thread.join(); joined = true; if (restore.err) |err| return err; try std.testing.expect(retired_before_destroy); try std.testing.expect(!completed_while_leased); }