Skip to content
Closed
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
2 changes: 1 addition & 1 deletion sdk/py/harness_sdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from typing import Iterator, Optional

MODELS = ["MiniMax-M2.5", "MiniMax-M2.7", "MiniMax-M3", "accounts/fireworks/models/deepseek-v4-flash", "accounts/fireworks/models/deepseek-v4-pro", "accounts/fireworks/models/glm-5p2", "accounts/fireworks/models/gpt-oss-120b", "accounts/fireworks/models/kimi-k2p6", "accounts/fireworks/models/kimi-k2p7-code", "accounts/fireworks/models/minimax-m3", "accounts/fireworks/models/qwen3p7-plus", "claude-fable-5", "claude-haiku-4-5", "claude-opus-4-5", "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", "claude-opus-4.8", "claude-opus-5", "claude-sonnet-4-5", "claude-sonnet-4-6", "claude-sonnet-4.6", "claude-sonnet-5", "deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro", "fugu", "fugu-ultra", "fugu-ultra-20260615", "glm-4.5", "glm-4.7", "glm-5", "glm-5.2", "gpt-5-codex", "gpt-5.2", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-pro", "gpt-5.5", "gpt-5.6", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "grok-4.3", "grok-build", "k3", "kilo-auto/small", "kimi-for-coding", "kimi-for-coding-highspeed", "kimi-k2.6", "kimi-latest", "lmstudio", "mimo-v2-flash", "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2.5-pro-ultraspeed", "minimax-m3", "mistral-medium-latest", "mlx-community/Qwen3.6-27B-OptiQ-4bit", "openai/gpt-oss-120b"]
TOOLS = ["bash", "bash_output", "bash_kill", "read_file", "edit_file", "write_file", "webfetch", "skill", "codedb", "todo_write", "todo_read", "eval", "note_constraint", "ask_user", "attempt_completion", "load_tool_schemas", "clock_sleep", "subagent", "workflow", "agent_output", "learn_candidate", "peer_message", "imagegen"]
TOOLS = ["bash", "bash_output", "bash_kill", "read_file", "edit_file", "write_file", "webfetch", "skill", "codedb", "todo_write", "todo_read", "eval", "note_constraint", "ask_user", "attempt_completion", "load_tool_schemas", "clock_sleep", "subagent", "workflow", "agent_output", "learn_candidate", "peer_message", "imagegen", "view_image"]
PROVIDERS = ["anthropic", "codegraff", "deepseek", "openai", "minimax", "xiaomi", "kilo", "groq", "mistral", "kimi", "moonshot", "xai", "zai", "fugu", "fireworks", "mlx", "lmstudio", "codex"]


Expand Down
4 changes: 2 additions & 2 deletions sdk/ts/harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import type { Readable, Writable } from "node:stream";
export const HARNESS_VERSION = "0.12";

export type ModelName = "MiniMax-M2.5" | "MiniMax-M2.7" | "MiniMax-M3" | "accounts/fireworks/models/deepseek-v4-flash" | "accounts/fireworks/models/deepseek-v4-pro" | "accounts/fireworks/models/glm-5p2" | "accounts/fireworks/models/gpt-oss-120b" | "accounts/fireworks/models/kimi-k2p6" | "accounts/fireworks/models/kimi-k2p7-code" | "accounts/fireworks/models/minimax-m3" | "accounts/fireworks/models/qwen3p7-plus" | "claude-fable-5" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6" | "claude-opus-4-7" | "claude-opus-4-8" | "claude-opus-4.8" | "claude-opus-5" | "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-sonnet-4.6" | "claude-sonnet-5" | "deepseek-chat" | "deepseek-reasoner" | "deepseek-v4-flash" | "deepseek-v4-pro" | "fugu" | "fugu-ultra" | "fugu-ultra-20260615" | "glm-4.5" | "glm-4.7" | "glm-5" | "glm-5.2" | "gpt-5-codex" | "gpt-5.2" | "gpt-5.3-codex-spark" | "gpt-5.4" | "gpt-5.4-mini" | "gpt-5.4-pro" | "gpt-5.5" | "gpt-5.6" | "gpt-5.6-luna" | "gpt-5.6-sol" | "gpt-5.6-terra" | "grok-4.3" | "grok-build" | "k3" | "kilo-auto/small" | "kimi-for-coding" | "kimi-for-coding-highspeed" | "kimi-k2.6" | "kimi-latest" | "lmstudio" | "mimo-v2-flash" | "mimo-v2.5" | "mimo-v2.5-pro" | "mimo-v2.5-pro-ultraspeed" | "minimax-m3" | "mistral-medium-latest" | "mlx-community/Qwen3.6-27B-OptiQ-4bit" | "openai/gpt-oss-120b" | (string & {});
export type ToolName = "bash" | "bash_output" | "bash_kill" | "read_file" | "edit_file" | "write_file" | "webfetch" | "skill" | "codedb" | "todo_write" | "todo_read" | "eval" | "note_constraint" | "ask_user" | "attempt_completion" | "load_tool_schemas" | "clock_sleep" | "subagent" | "workflow" | "agent_output" | "learn_candidate" | "peer_message" | "imagegen";
export type ToolName = "bash" | "bash_output" | "bash_kill" | "read_file" | "edit_file" | "write_file" | "webfetch" | "skill" | "codedb" | "todo_write" | "todo_read" | "eval" | "note_constraint" | "ask_user" | "attempt_completion" | "load_tool_schemas" | "clock_sleep" | "subagent" | "workflow" | "agent_output" | "learn_candidate" | "peer_message" | "imagegen" | "view_image";
export type ProviderId = "anthropic" | "codegraff" | "deepseek" | "openai" | "minimax" | "xiaomi" | "kilo" | "groq" | "mistral" | "kimi" | "moonshot" | "xai" | "zai" | "fugu" | "fireworks" | "mlx" | "lmstudio" | "codex";

/** Events streamed by `harness --json` (one JSON object per stdout line), each
Expand Down Expand Up @@ -661,4 +661,4 @@ export async function* runAgent(opts: RunAgentOptions): AsyncGenerator<Event> {
}

export const MODELS: ModelName[] = ["MiniMax-M2.5", "MiniMax-M2.7", "MiniMax-M3", "accounts/fireworks/models/deepseek-v4-flash", "accounts/fireworks/models/deepseek-v4-pro", "accounts/fireworks/models/glm-5p2", "accounts/fireworks/models/gpt-oss-120b", "accounts/fireworks/models/kimi-k2p6", "accounts/fireworks/models/kimi-k2p7-code", "accounts/fireworks/models/minimax-m3", "accounts/fireworks/models/qwen3p7-plus", "claude-fable-5", "claude-haiku-4-5", "claude-opus-4-5", "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", "claude-opus-4.8", "claude-opus-5", "claude-sonnet-4-5", "claude-sonnet-4-6", "claude-sonnet-4.6", "claude-sonnet-5", "deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro", "fugu", "fugu-ultra", "fugu-ultra-20260615", "glm-4.5", "glm-4.7", "glm-5", "glm-5.2", "gpt-5-codex", "gpt-5.2", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-pro", "gpt-5.5", "gpt-5.6", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "grok-4.3", "grok-build", "k3", "kilo-auto/small", "kimi-for-coding", "kimi-for-coding-highspeed", "kimi-k2.6", "kimi-latest", "lmstudio", "mimo-v2-flash", "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2.5-pro-ultraspeed", "minimax-m3", "mistral-medium-latest", "mlx-community/Qwen3.6-27B-OptiQ-4bit", "openai/gpt-oss-120b"];
export const TOOLS: ToolName[] = ["bash", "bash_output", "bash_kill", "read_file", "edit_file", "write_file", "webfetch", "skill", "codedb", "todo_write", "todo_read", "eval", "note_constraint", "ask_user", "attempt_completion", "load_tool_schemas", "clock_sleep", "subagent", "workflow", "agent_output", "learn_candidate", "peer_message", "imagegen"];
export const TOOLS: ToolName[] = ["bash", "bash_output", "bash_kill", "read_file", "edit_file", "write_file", "webfetch", "skill", "codedb", "todo_write", "todo_read", "eval", "note_constraint", "ask_user", "attempt_completion", "load_tool_schemas", "clock_sleep", "subagent", "workflow", "agent_output", "learn_candidate", "peer_message", "imagegen", "view_image"];
4 changes: 2 additions & 2 deletions sdk/ts/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
export const HARNESS_VERSION = "0.12";

export type ModelName = "MiniMax-M2.5" | "MiniMax-M2.7" | "MiniMax-M3" | "accounts/fireworks/models/deepseek-v4-flash" | "accounts/fireworks/models/deepseek-v4-pro" | "accounts/fireworks/models/glm-5p2" | "accounts/fireworks/models/gpt-oss-120b" | "accounts/fireworks/models/kimi-k2p6" | "accounts/fireworks/models/kimi-k2p7-code" | "accounts/fireworks/models/minimax-m3" | "accounts/fireworks/models/qwen3p7-plus" | "claude-fable-5" | "claude-haiku-4-5" | "claude-opus-4-5" | "claude-opus-4-6" | "claude-opus-4-7" | "claude-opus-4-8" | "claude-opus-4.8" | "claude-opus-5" | "claude-sonnet-4-5" | "claude-sonnet-4-6" | "claude-sonnet-4.6" | "claude-sonnet-5" | "deepseek-chat" | "deepseek-reasoner" | "deepseek-v4-flash" | "deepseek-v4-pro" | "fugu" | "fugu-ultra" | "fugu-ultra-20260615" | "glm-4.5" | "glm-4.7" | "glm-5" | "glm-5.2" | "gpt-5-codex" | "gpt-5.2" | "gpt-5.3-codex-spark" | "gpt-5.4" | "gpt-5.4-mini" | "gpt-5.4-pro" | "gpt-5.5" | "gpt-5.6" | "gpt-5.6-luna" | "gpt-5.6-sol" | "gpt-5.6-terra" | "grok-4.3" | "grok-build" | "k3" | "kilo-auto/small" | "kimi-for-coding" | "kimi-for-coding-highspeed" | "kimi-k2.6" | "kimi-latest" | "lmstudio" | "mimo-v2-flash" | "mimo-v2.5" | "mimo-v2.5-pro" | "mimo-v2.5-pro-ultraspeed" | "minimax-m3" | "mistral-medium-latest" | "mlx-community/Qwen3.6-27B-OptiQ-4bit" | "openai/gpt-oss-120b" | (string & {});
export type ToolName = "bash" | "bash_output" | "bash_kill" | "read_file" | "edit_file" | "write_file" | "webfetch" | "skill" | "codedb" | "todo_write" | "todo_read" | "eval" | "note_constraint" | "ask_user" | "attempt_completion" | "load_tool_schemas" | "clock_sleep" | "subagent" | "workflow" | "agent_output" | "learn_candidate" | "peer_message" | "imagegen";
export type ToolName = "bash" | "bash_output" | "bash_kill" | "read_file" | "edit_file" | "write_file" | "webfetch" | "skill" | "codedb" | "todo_write" | "todo_read" | "eval" | "note_constraint" | "ask_user" | "attempt_completion" | "load_tool_schemas" | "clock_sleep" | "subagent" | "workflow" | "agent_output" | "learn_candidate" | "peer_message" | "imagegen" | "view_image";
export type ProviderId = "anthropic" | "codegraff" | "deepseek" | "openai" | "minimax" | "xiaomi" | "kilo" | "groq" | "mistral" | "kimi" | "moonshot" | "xai" | "zai" | "fugu" | "fireworks" | "mlx" | "lmstudio" | "codex";

/** Events streamed by the bridge (same `--json` contract as the stdio SDK).
Expand Down Expand Up @@ -508,4 +508,4 @@ export async function* runAgentRemote(opts: RunAgentRemoteOptions): AsyncGenerat
}

export const MODELS: ModelName[] = ["MiniMax-M2.5", "MiniMax-M2.7", "MiniMax-M3", "accounts/fireworks/models/deepseek-v4-flash", "accounts/fireworks/models/deepseek-v4-pro", "accounts/fireworks/models/glm-5p2", "accounts/fireworks/models/gpt-oss-120b", "accounts/fireworks/models/kimi-k2p6", "accounts/fireworks/models/kimi-k2p7-code", "accounts/fireworks/models/minimax-m3", "accounts/fireworks/models/qwen3p7-plus", "claude-fable-5", "claude-haiku-4-5", "claude-opus-4-5", "claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8", "claude-opus-4.8", "claude-opus-5", "claude-sonnet-4-5", "claude-sonnet-4-6", "claude-sonnet-4.6", "claude-sonnet-5", "deepseek-chat", "deepseek-reasoner", "deepseek-v4-flash", "deepseek-v4-pro", "fugu", "fugu-ultra", "fugu-ultra-20260615", "glm-4.5", "glm-4.7", "glm-5", "glm-5.2", "gpt-5-codex", "gpt-5.2", "gpt-5.3-codex-spark", "gpt-5.4", "gpt-5.4-mini", "gpt-5.4-pro", "gpt-5.5", "gpt-5.6", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "grok-4.3", "grok-build", "k3", "kilo-auto/small", "kimi-for-coding", "kimi-for-coding-highspeed", "kimi-k2.6", "kimi-latest", "lmstudio", "mimo-v2-flash", "mimo-v2.5", "mimo-v2.5-pro", "mimo-v2.5-pro-ultraspeed", "minimax-m3", "mistral-medium-latest", "mlx-community/Qwen3.6-27B-OptiQ-4bit", "openai/gpt-oss-120b"];
export const TOOLS: ToolName[] = ["bash", "bash_output", "bash_kill", "read_file", "edit_file", "write_file", "webfetch", "skill", "codedb", "todo_write", "todo_read", "eval", "note_constraint", "ask_user", "attempt_completion", "load_tool_schemas", "clock_sleep", "subagent", "workflow", "agent_output", "learn_candidate", "peer_message", "imagegen"];
export const TOOLS: ToolName[] = ["bash", "bash_output", "bash_kill", "read_file", "edit_file", "write_file", "webfetch", "skill", "codedb", "todo_write", "todo_read", "eval", "note_constraint", "ask_user", "attempt_completion", "load_tool_schemas", "clock_sleep", "subagent", "workflow", "agent_output", "learn_candidate", "peer_message", "imagegen", "view_image"];
3 changes: 3 additions & 0 deletions src/agent_request_policy.zig
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ pub fn refreshLoginKeyBeforeSend(self: *Agent) void {
// Only adopt on a real CHANGE: an unchanged credential must not churn the
// session arena, nor hand the WS transport latch back on every request.
if (std.mem.eql(u8, fresh.key, self.provider.api_key)) return;
// #404: a failed persist left the live token only in memory. Do not
// replace it with an older on-disk credential.
if (oauth.persistFailed() and !std.mem.eql(u8, fresh.key, self.provider.api_key)) return;
adoptFreshAuth(self, fresh);
}

Expand Down
1 change: 1 addition & 0 deletions src/agent_tools.zig
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ pub fn runTools(self: *Agent, calls: []const ToolCall) ![]ExecResult {
.subagent_cross_provider = self.subagent_cross_provider,
.registry = if (self.sub) null else self.registry,
.from_sub = self.sub,
.out = self.out,
.has_eval = self.eval_cmd != null,
.approvals = self.approvals,
.tracer = self.tracer,
Expand Down
23 changes: 22 additions & 1 deletion src/credential_store.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ pub const private_dir: Io.File.Permissions = if (Io.File.Permissions.has_executa
/// `private_file` for anything holding a secret; pass `.default_file` to keep
/// the ordinary umask-governed behaviour of a plain `createFile`.
pub fn replaceFile(io: Io, dir: Io.Dir, sub_path: []const u8, bytes: []const u8, permissions: Io.File.Permissions) !void {
var atomic = try dir.createFileAtomic(io, sub_path, .{ .permissions = permissions, .replace = true });
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
const dest = if (dir.readLink(io, sub_path, &path_buf)) |n| path_buf[0..n] else |_| sub_path;
var atomic = try dir.createFileAtomic(io, dest, .{ .permissions = permissions, .replace = true });
defer atomic.deinit(io);
if (Io.File.Permissions.has_executable_bit) {
if (permissions != .default_file) {
Expand Down Expand Up @@ -196,3 +198,22 @@ test "writeOAuth: the credential file is 0600 inside 0700 directories" {
try std.testing.expectEqualStrings("refresh-1", parsed.object.get("refresh_token").?.string);
try std.testing.expectEqual(@as(i64, 1234), parsed.object.get("expires_at").?.integer);
}

test "#405: replaceFile writes through a symlink instead of replacing it" {
if (@import("builtin").os.tag == .windows) return;
const io = std.testing.io;
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
var real_buf: [std.fs.max_path_bytes]u8 = undefined;
const base = real_buf[0..try tmp.dir.realPath(io, &real_buf)];
const target = try std.fmt.allocPrint(std.testing.allocator, "{s}/real.json", .{base});
defer std.testing.allocator.free(target);
const link = try std.fmt.allocPrint(std.testing.allocator, "{s}/settings.json", .{base});
defer std.testing.allocator.free(link);
try Io.Dir.cwd().writeFile(io, .{ .sub_path = target, .data = "old\n" });
try Io.Dir.cwd().symLink(io, target, link, .{});
try replaceFile(io, Io.Dir.cwd(), link, "new\n", .default_file);
const through = try Io.Dir.cwd().readFileAlloc(io, target, std.testing.allocator, .limited(64));
defer std.testing.allocator.free(through);
try std.testing.expectEqualStrings("new\n", through);
}
20 changes: 10 additions & 10 deletions src/exec.zig
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const noSymlinkEscape = approvals_mod.noSymlinkEscape;
const jobs = @import("jobs.zig");
const runCapped = jobs.runCapped;
const runCappedWithOptions = jobs.runCappedWithOptions;
const exec_bash_stream = @import("exec_bash_stream.zig");
const toolRunOptions = jobs.toolRunOptions; // #266/#198: own the child's process group
const spawnJob = jobs.spawnJob;
const jobOutput = jobs.jobOutput;
Expand All @@ -76,7 +77,8 @@ const no_local_tools = @import("no_local_tools.zig"); // #330: the hard --no-loc
const native_fold = @import("native_fold.zig"); // folded native power tools: layer-2 refusal until load_tool_schemas unfolds
const vision = @import("vision.zig"); // read_file stages images like MCP image results (#249)
const input_util = @import("input_util.zig");
const imagegen = @import("imagegen.zig"); // #352: the codex-gated image tool (advertising lives in schema.zig/tool_gates.zig)
const imagegen = @import("imagegen.zig");
const view_image = @import("view_image.zig");

/// Wall-clock ceiling for one *subagent* bash command. Subagents run on pool
/// threads with no TTY, so there is no Esc to kill a runaway command — without
Expand Down Expand Up @@ -325,13 +327,11 @@ fn execToolInner(ctx: ToolCtx, call: ToolCall) !ToolOutput {
}
const sh = shellArgv(cmd);
const deadline: u64 = if (ctx.from_sub) subagent_bash_deadline_ms else 0;
// #276 P0-1: a worktree-isolated agent's bash calls run pinned to its
// own worktree — via std.process.Child.Cwd, per spawn, never a
// process-wide chdir — so parallel siblings never share a cwd.
// #266/#198: toolRunOptions also gives the command its own process
// group, so an Esc cancel or the deadline kills what it spawned (ssh,
// xcodebuild) instead of leaving it running against a dead turn.
const run = try runCappedWithOptions(gpa, io, &sh, bash_stdout_cap, bash_stderr_cap, deadline, toolRunOptions(ctx.agent_cwd));
// Isolated cwd + process group.
var opts = toolRunOptions(ctx.agent_cwd);
var stream = exec_bash_stream.Ctx{ .io = io, .w = ctx.out };
exec_bash_stream.attach(&opts, !ctx.from_sub, &stream);
const run = try runCappedWithOptions(gpa, io, &sh, bash_stdout_cap, bash_stderr_cap, deadline, opts);
defer gpa.free(run.stdout);
defer gpa.free(run.stderr);

Expand Down Expand Up @@ -545,9 +545,8 @@ fn execToolInner(ctx: ToolCtx, call: ToolCall) !ToolOutput {
// Loads one SKILL.md body (or lists them). Rescans on every call, so a
// skill written this session is loadable without a restart.
if (std.mem.eql(u8, call.name, "skill")) return skill_docs.execSkill(gpa, io, input);
// #352: codex-gated. execImagegen answers a call that was never advertised
// (an unavailable session) with the same honest error it gives the model.
if (std.mem.eql(u8, call.name, imagegen.tool_name)) return imagegen.execImagegen(ctx, input);
if (std.mem.eql(u8, call.name, view_image.name)) return view_image.exec(ctx, input);
if (std.mem.eql(u8, call.name, "subagent")) return execSubagent(ctx, input);
if (std.mem.eql(u8, call.name, "workflow")) return execWorkflow(ctx, input);
if (std.mem.eql(u8, call.name, "agent_output")) {
Expand Down Expand Up @@ -594,6 +593,7 @@ test "internal learning respects the parent privacy ceiling" {
}

test { // main.zig is at the 600-line cap; exec.zig is these modules' importer, so the compiled-in references live here (the reach check diffs the test binary, not which file holds the line)
_ = exec_bash_stream;
_ = @import("codedbpro_report.zig");
_ = @import("tool_balance.zig");
}
60 changes: 60 additions & 0 deletions src/exec_bash_stream.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//! Live foreground-bash output (#472). runCapped already polls pipes every
//! 200ms; this file is the UI half — write each new chunk so a long build
//! does not look like a hang.

const std = @import("std");
const Io = std.Io;

const main_mod = @import("main.zig");
const jobs = @import("jobs.zig");

pub const Ctx = struct {
io: Io,
w: ?*Io.Writer,
};

pub fn attach(opts: *jobs.CappedRunOptions, live: bool, stream: *Ctx) void {
if (!live) return;
opts.stream = &emit;
opts.stream_ctx = stream;
}

pub fn emit(ctx: ?*anyopaque, which: u8, chunk: []const u8) void {
if (chunk.len == 0) return;
const c: *const Ctx = @ptrCast(@alignCast(ctx orelse return));
if (main_mod.json_mode) {
const w = main_mod.g_out orelse return;
main_mod.g_gui_mu.lockUncancelable(c.io);
defer main_mod.g_gui_mu.unlock(c.io);
w.writeAll("{\"type\":\"tool_output\",\"name\":\"bash\",\"stream\":\"") catch return;
w.writeAll(if (which == 0) "stdout" else "stderr") catch return;
w.writeAll("\",\"text\":") catch return;
writeJsonString(w, chunk) catch return;
w.writeAll("}\n") catch return;
w.flush() catch {};
return;
}
const w = c.w orelse main_mod.g_out orelse return;
w.writeAll(chunk) catch return;
w.flush() catch {};
}

fn writeJsonString(w: *Io.Writer, s: []const u8) !void {
try w.writeByte('"');
for (s) |c| switch (c) {
'"' => try w.writeAll("\\\""),
'\\' => try w.writeAll("\\\\"),
'\n' => try w.writeAll("\\n"),
'\r' => try w.writeAll("\\r"),
'\t' => try w.writeAll("\\t"),
else => if (c < 0x20) try w.print("\\u{d:0>4}", .{c}) else try w.writeByte(c),
};
try w.writeByte('"');
}

test "#472: writeJsonString escapes a newline" {
var buf: [64]u8 = undefined;
var w: Io.Writer = .fixed(&buf);
try writeJsonString(&w, "a\nb");
try std.testing.expectEqualStrings("\"a\\nb\"", w.buffered());
}
Loading
Loading