Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/builtins/commands.zig
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,10 @@ pub const top_level_flags = [_]TopLevelFlag{
.usage = "--resume-<id>",
.description = "Resume a session by exact ID",
},
.{
.usage = "--sessions-v2",
.description = "Use the experimental v2 session store, also set by FX_SESSIONS_V2=1",
},
.{
.usage = "-h, --help",
.description = "Display this help and exit",
Expand Down
11 changes: 10 additions & 1 deletion src/core/app/app_callbacks.zig
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,10 @@ pub fn Bindings(comptime App: type) type {
null
else
null,
.append_turn_piece = if (comptime @hasField(App, "session_persistence"))
if (app.session_persistence.v2 != null) agentAppendTurnPiece else null
else
null,
.propagate_grant = agentPropagateGrant,
.push_event = agentPushEvent,
.push_text = agentPushText,
Expand Down Expand Up @@ -383,7 +387,7 @@ pub fn Bindings(comptime App: type) type {
@hasField(@TypeOf(app.session_persistence), "writable"))
{
app.session.usage.configureCheckpointSink(
if (app.session_persistence.writable != null)
if (app.session_persistence.writable != null or app.session_persistence.v2 != null)
.{
.context = @ptrCast(app),
.allocator = app.alloc,
Expand Down Expand Up @@ -1210,6 +1214,11 @@ pub fn Bindings(comptime App: type) type {
try app_session_runtime.Runtime(App).clearRecoveryCheckpoint(app);
}

fn agentAppendTurnPiece(ctx: *anyopaque, progress: agent_runtime.TurnProgress) anyerror!void {
const app: *App = @ptrCast(@alignCast(ctx));
try app_session_runtime.Runtime(App).appendTurnPiece(app, progress);
}

fn agentPersistUsageCheckpoint(
ctx: *anyopaque,
snapshot: session_usage.Snapshot,
Expand Down
65 changes: 49 additions & 16 deletions src/core/app/app_entry_runtime.zig
Original file line number Diff line number Diff line change
Expand Up @@ -381,20 +381,30 @@ fn runInteractiveWithDeps(comptime App: type, comptime cooperative: bool, app: *
if (handoff_value) |value| {
var handoff = value;
defer handoff.deinit(alloc);
var argv = [_][]const u8{
request.executablePath(),
"resume",
handoff.session_id,
cli_surface.upgrade_relaunch_arg,
request.previousRevision() orelse "",
};
const argv_slice = if (request.previousRevision() == null) argv[0..4] else argv[0..5];
var argv: [6][]const u8 = undefined;
var argc: usize = 0;
argv[argc] = request.executablePath();
argc += 1;
// A v2 session resumes only with the flag that saved it.
if (handoff.sessions_v2) {
argv[argc] = cli_surface.sessions_v2_arg;
argc += 1;
}
for ([_][]const u8{ "resume", handoff.session_id, cli_surface.upgrade_relaunch_arg }) |arg| {
argv[argc] = arg;
argc += 1;
}
if (request.previousRevision()) |revision| {
argv[argc] = revision;
argc += 1;
}
const argv_slice = argv[0..argc];
const replace_err = deps.replace_process(
deps.replace_ctx,
io_mod.getIo(),
.{ .argv = argv_slice },
);
writeUpgradeRelaunchFailure(deps, replace_err, handoff.session_id);
writeUpgradeRelaunchFailure(deps, replace_err, handoff.session_id, handoff.sessions_v2);
} else {
writeStderr(
deps,
Expand All @@ -412,9 +422,10 @@ fn runInteractiveWithDeps(comptime App: type, comptime cooperative: bool, app: *
&message_buffer,
handoff.session_id,
resume_handoff_columns,
handoff.sessions_v2,
)
else
formatResumeHandoff(&message_buffer, handoff.session_id)) catch return .returned;
formatResumeHandoff(&message_buffer, handoff.session_id, handoff.sessions_v2)) catch return .returned;
deps.write_stdout(deps.stdout_ctx, message) catch {};
}
return .returned;
Expand All @@ -439,12 +450,13 @@ fn writeUpgradeRelaunchFailure(
deps: RunDeps,
err: std.process.ReplaceError,
session_id: []const u8,
sessions_v2: bool,
) void {
var buffer: [768]u8 = undefined;
const message = std.fmt.bufPrint(
&buffer,
"fx: upgrade installed, but relaunch failed: {s}\nContinue session with: fx --resume {s}\n",
.{ @errorName(err), session_id },
"fx: upgrade installed, but relaunch failed: {s}\nContinue session with: fx {s}--resume {s}\n",
.{ @errorName(err), if (sessions_v2) "--sessions-v2 " else "", session_id },
) catch "fx: upgrade installed, but relaunch failed; run `fx doctor`.\n";
writeStderr(deps, message);
}
Expand Down Expand Up @@ -515,11 +527,11 @@ fn writeRealStdout(_: ?*anyopaque, text: []const u8) !void {
try std.Io.File.stdout().writeStreamingAll(io_mod.getIo(), text);
}

fn formatResumeHandoff(buffer: []u8, session_id: []const u8) ![]const u8 {
fn formatResumeHandoff(buffer: []u8, session_id: []const u8, sessions_v2: bool) ![]const u8 {
return std.fmt.bufPrint(
buffer,
"Continue session with: fx --resume {s}\n",
.{session_id},
"Continue session with: fx {s}--resume {s}\n",
.{ if (sessions_v2) "--sessions-v2 " else "", session_id },
);
}

Expand Down Expand Up @@ -665,6 +677,7 @@ const TestCapture = struct {
record_stderr_event: bool = false,
record_stdout_event: bool = false,
resume_handoff_id: ?[]const u8 = null,
resume_handoff_sessions_v2: bool = false,
shutdown_failure: ?anyerror = null,
raise_sigint_during_deinit: bool = false,
upgrade_relaunch_path: ?[]const u8 = null,
Expand Down Expand Up @@ -805,7 +818,7 @@ const TestApp = struct {
self.deinit();
return .{ .failure = error.OutOfMemory };
};
break :blk .{ .session_id = session_id };
break :blk .{ .session_id = session_id, .sessions_v2 = active_capture.?.resume_handoff_sessions_v2 };
} else null;
if (active_capture.?.raise_sigint_during_deinit) {
_ = std.c.raise(std.posix.SIG.INT);
Expand Down Expand Up @@ -1030,6 +1043,26 @@ test "app entry bounds graceful-exit SIGINT suppression to handoff lifetime" {
try std.testing.expectEqual(@as(usize, 1), test_sigint_count.load(.seq_cst));
}

test "a v2 handoff relaunches and hints with --sessions-v2" {
const alloc = std.testing.allocator;
var capture = TestCapture.init(.{ .interactive = .{} });
defer capture.deinit();
capture.resume_handoff_id = "session-123";
capture.resume_handoff_sessions_v2 = true;
capture.upgrade_relaunch_path = "/tmp/fx-upgraded";

const outcome = try runWithDeps(TestApp, alloc, &.{}, testConfig(), capture.deps());

try std.testing.expectEqual(@as(u8, 1), outcome.exit);
try std.testing.expectEqual(@as(usize, 5), capture.replace_arg_count);
try std.testing.expectEqualStrings("/tmp/fx-upgraded", capture.replaceArg(0));
try std.testing.expectEqualStrings("--sessions-v2", capture.replaceArg(1));
try std.testing.expectEqualStrings("resume", capture.replaceArg(2));
try std.testing.expectEqualStrings("session-123", capture.replaceArg(3));
try std.testing.expectEqualStrings("--upgrade-relaunch", capture.replaceArg(4));
try std.testing.expect(std.mem.find(u8, capture.stderr.written(), "fx --sessions-v2 --resume session-123") != null);
}

test "app entry relaunches only after teardown with the validated handoff" {
const alloc = std.testing.allocator;
var capture = TestCapture.init(.{ .interactive = .{} });
Expand Down
Loading
Loading