Something went wrong. Try again.
My version of https://github.com/vercel-labs/fx
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679const 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 <context_handoff> 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, "<turn_aborted>") == 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("<content>readme text</content>"), .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);}