sigit-code 1.6.2__tar.gz → 1.6.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. {sigit_code-1.6.2 → sigit_code-1.6.4}/.agents/AGENTS.md +33 -7
  2. {sigit_code-1.6.2/.claude → sigit_code-1.6.4/.agents}/skills/agent-client-protocol/SKILL.md +1 -0
  3. {sigit_code-1.6.2/.agents → sigit_code-1.6.4/.claude}/skills/agent-client-protocol/SKILL.md +1 -0
  4. sigit_code-1.6.4/.sigit/code-cloud-agent/config.toml +6 -0
  5. sigit_code-1.6.4/.sigit/code-review/config.toml +3 -0
  6. {sigit_code-1.6.2 → sigit_code-1.6.4}/AGENTS.md +33 -7
  7. {sigit_code-1.6.2 → sigit_code-1.6.4}/CHANGELOG.md +80 -0
  8. {sigit_code-1.6.2 → sigit_code-1.6.4}/CLAUDE.md +33 -7
  9. {sigit_code-1.6.2 → sigit_code-1.6.4}/Cargo.lock +1 -1
  10. {sigit_code-1.6.2 → sigit_code-1.6.4}/Cargo.toml +1 -1
  11. {sigit_code-1.6.2 → sigit_code-1.6.4}/PKG-INFO +1 -1
  12. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/backend.rs +64 -12
  13. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/inline_tool_calls.rs +262 -1
  14. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/main.rs +1026 -186
  15. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/tools.rs +52 -0
  16. sigit_code-1.6.4/tests/acp_concurrent_sessions.rs +343 -0
  17. sigit_code-1.6.4/tests/acp_logout.rs +179 -0
  18. {sigit_code-1.6.2 → sigit_code-1.6.4}/tests/acp_permissions.rs +138 -0
  19. {sigit_code-1.6.2 → sigit_code-1.6.4}/tests/acp_session_load.rs +170 -0
  20. {sigit_code-1.6.2 → sigit_code-1.6.4}/.agents/skills/ai-assisted-coding/SKILL.md +0 -0
  21. {sigit_code-1.6.2 → sigit_code-1.6.4}/.agents/skills/branding/SKILL.md +0 -0
  22. {sigit_code-1.6.2 → sigit_code-1.6.4}/.agents/skills/run-sigit/SKILL.md +0 -0
  23. {sigit_code-1.6.2 → sigit_code-1.6.4}/.agents/skills/run-sigit/driver.mjs +0 -0
  24. {sigit_code-1.6.2 → sigit_code-1.6.4}/.agents/skills/run-sigit/tui-smoke.sh +0 -0
  25. {sigit_code-1.6.2 → sigit_code-1.6.4}/.agents/skills/sigit-code-release/SKILL.md +0 -0
  26. {sigit_code-1.6.2 → sigit_code-1.6.4}/.agents/skills/tool-calling/SKILL.md +0 -0
  27. {sigit_code-1.6.2 → sigit_code-1.6.4}/.cargo/config.toml +0 -0
  28. {sigit_code-1.6.2 → sigit_code-1.6.4}/.claude/skills/ai-assisted-coding/SKILL.md +0 -0
  29. {sigit_code-1.6.2 → sigit_code-1.6.4}/.claude/skills/branding/SKILL.md +0 -0
  30. {sigit_code-1.6.2 → sigit_code-1.6.4}/.claude/skills/run-sigit/SKILL.md +0 -0
  31. {sigit_code-1.6.2 → sigit_code-1.6.4}/.claude/skills/run-sigit/driver.mjs +0 -0
  32. {sigit_code-1.6.2 → sigit_code-1.6.4}/.claude/skills/run-sigit/tui-smoke.sh +0 -0
  33. {sigit_code-1.6.2 → sigit_code-1.6.4}/.claude/skills/sigit-code-release/SKILL.md +0 -0
  34. {sigit_code-1.6.2 → sigit_code-1.6.4}/.claude/skills/tool-calling/SKILL.md +0 -0
  35. {sigit_code-1.6.2 → sigit_code-1.6.4}/.github/workflows/ci.yml +0 -0
  36. {sigit_code-1.6.2 → sigit_code-1.6.4}/.github/workflows/release-aur.yml +0 -0
  37. {sigit_code-1.6.2 → sigit_code-1.6.4}/.github/workflows/release-crates.yml +0 -0
  38. {sigit_code-1.6.2 → sigit_code-1.6.4}/.github/workflows/release-github.yml +0 -0
  39. {sigit_code-1.6.2 → sigit_code-1.6.4}/.github/workflows/release-homebrew.yml +0 -0
  40. {sigit_code-1.6.2 → sigit_code-1.6.4}/.github/workflows/release-npm.yml +0 -0
  41. {sigit_code-1.6.2 → sigit_code-1.6.4}/.github/workflows/release-nuget.yml +0 -0
  42. {sigit_code-1.6.2 → sigit_code-1.6.4}/.github/workflows/release-pypi.yml +0 -0
  43. {sigit_code-1.6.2 → sigit_code-1.6.4}/.github/workflows/release-scoop.yml +0 -0
  44. {sigit_code-1.6.2 → sigit_code-1.6.4}/.github/workflows/release-winget.yml +0 -0
  45. {sigit_code-1.6.2 → sigit_code-1.6.4}/.gitignore +0 -0
  46. {sigit_code-1.6.2 → sigit_code-1.6.4}/.nvmrc +0 -0
  47. {sigit_code-1.6.2 → sigit_code-1.6.4}/.sigit/factory/config.toml +0 -0
  48. {sigit_code-1.6.2 → sigit_code-1.6.4}/LICENSE +0 -0
  49. {sigit_code-1.6.2 → sigit_code-1.6.4}/README.md +0 -0
  50. {sigit_code-1.6.2 → sigit_code-1.6.4}/docs/headless.md +0 -0
  51. {sigit_code-1.6.2 → sigit_code-1.6.4}/docs/hooks.md +0 -0
  52. {sigit_code-1.6.2 → sigit_code-1.6.4}/docs/mcp.md +0 -0
  53. {sigit_code-1.6.2 → sigit_code-1.6.4}/examples/settings-with-hooks.toml +0 -0
  54. {sigit_code-1.6.2 → sigit_code-1.6.4}/examples/skills/README.md +0 -0
  55. {sigit_code-1.6.2 → sigit_code-1.6.4}/examples/skills/commit-message/SKILL.md +0 -0
  56. {sigit_code-1.6.2 → sigit_code-1.6.4}/npm/README.md.tmpl +0 -0
  57. {sigit_code-1.6.2 → sigit_code-1.6.4}/npm/package-compat.json.tmpl +0 -0
  58. {sigit_code-1.6.2 → sigit_code-1.6.4}/npm/package-main.json.tmpl +0 -0
  59. {sigit_code-1.6.2 → sigit_code-1.6.4}/npm/package.json.tmpl +0 -0
  60. {sigit_code-1.6.2 → sigit_code-1.6.4}/npm/scripts/render-main-package.cjs +0 -0
  61. {sigit_code-1.6.2 → sigit_code-1.6.4}/npm/scripts/render-platform-package.cjs +0 -0
  62. {sigit_code-1.6.2 → sigit_code-1.6.4}/npm/sigit/.gitignore +0 -0
  63. {sigit_code-1.6.2 → sigit_code-1.6.4}/npm/sigit/README.md +0 -0
  64. {sigit_code-1.6.2 → sigit_code-1.6.4}/npm/sigit/package.json +0 -0
  65. {sigit_code-1.6.2 → sigit_code-1.6.4}/npm/sigit/src/index.ts +0 -0
  66. {sigit_code-1.6.2 → sigit_code-1.6.4}/npm/sigit/tsconfig.json +0 -0
  67. {sigit_code-1.6.2 → sigit_code-1.6.4}/nuget/.gitignore +0 -0
  68. {sigit_code-1.6.2 → sigit_code-1.6.4}/nuget/sigit/Program.cs +0 -0
  69. {sigit_code-1.6.2 → sigit_code-1.6.4}/nuget/sigit/README.md +0 -0
  70. {sigit_code-1.6.2 → sigit_code-1.6.4}/nuget/sigit/SiGit.Code.csproj +0 -0
  71. {sigit_code-1.6.2 → sigit_code-1.6.4}/packaging/aur/PKGBUILD.in +0 -0
  72. {sigit_code-1.6.2 → sigit_code-1.6.4}/packaging/nfpm.yaml +0 -0
  73. {sigit_code-1.6.2 → sigit_code-1.6.4}/packaging/windows/check-static-crt.ps1 +0 -0
  74. {sigit_code-1.6.2 → sigit_code-1.6.4}/packaging/winget/getSigit.siGitCode.installer.yaml.in +0 -0
  75. {sigit_code-1.6.2 → sigit_code-1.6.4}/packaging/winget/getSigit.siGitCode.locale.en-US.yaml.in +0 -0
  76. {sigit_code-1.6.2 → sigit_code-1.6.4}/packaging/winget/getSigit.siGitCode.yaml.in +0 -0
  77. {sigit_code-1.6.2 → sigit_code-1.6.4}/pypi/README.md +0 -0
  78. {sigit_code-1.6.2 → sigit_code-1.6.4}/pypi/pyproject.toml +0 -0
  79. {sigit_code-1.6.2 → sigit_code-1.6.4}/pyproject.toml +0 -0
  80. {sigit_code-1.6.2 → sigit_code-1.6.4}/rust-toolchain.toml +0 -0
  81. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/account.rs +0 -0
  82. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/browser_auth.rs +0 -0
  83. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/chat.rs +0 -0
  84. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/client_fs.rs +0 -0
  85. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/commands.rs +0 -0
  86. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/credentials.rs +0 -0
  87. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/frontmatter.rs +0 -0
  88. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/headless.rs +0 -0
  89. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/hooks.rs +0 -0
  90. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/instructions.rs +0 -0
  91. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/mcp.rs +0 -0
  92. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/models.rs +0 -0
  93. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/permissions.rs +0 -0
  94. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/provider.rs +0 -0
  95. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/session_store.rs +0 -0
  96. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/settings.rs +0 -0
  97. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/setup.rs +0 -0
  98. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/skills.rs +0 -0
  99. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/subagents.rs +0 -0
  100. {sigit_code-1.6.2 → sigit_code-1.6.4}/src/workspace.rs +0 -0
  101. {sigit_code-1.6.2 → sigit_code-1.6.4}/tests/acp_client_fs.rs +0 -0
  102. {sigit_code-1.6.2 → sigit_code-1.6.4}/tests/acp_endpoint_errors.rs +0 -0
  103. {sigit_code-1.6.2 → sigit_code-1.6.4}/tests/acp_images.rs +0 -0
  104. {sigit_code-1.6.2 → sigit_code-1.6.4}/tests/acp_multi_root.rs +0 -0
  105. {sigit_code-1.6.2 → sigit_code-1.6.4}/tests/acp_session_list.rs +0 -0
  106. {sigit_code-1.6.2 → sigit_code-1.6.4}/tests/acp_stop_reasons.rs +0 -0
  107. {sigit_code-1.6.2 → sigit_code-1.6.4}/tests/acp_tool_stdin.rs +0 -0
  108. {sigit_code-1.6.2 → sigit_code-1.6.4}/tests/headless_mode.rs +0 -0
  109. {sigit_code-1.6.2 → sigit_code-1.6.4}/tests/version_flag.rs +0 -0
@@ -113,9 +113,20 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
113
113
  - **`src/main.rs`** — entry point, mode dispatch, the full ACP `Agent` impl (session lifecycle:
114
114
  new/load/fork/prompt/cancel, config options, slash-command advertisement), and the `SYSTEM_PROMPT`.
115
115
  ACP session state owns its roots, conversation, and selected model even though the process has
116
- one live backend; activating a thread parks and restores all three. Unknown session ids are
117
- rejected instead of silently borrowing the active thread's cwd. Prompt cancellation is routed
118
- outside `turn_lock`, which lets a client cancel the turn currently holding that lock. The
116
+ one working directory and one live backend slot; activating a thread parks and restores all
117
+ three. A thread on an HTTP backend keeps a backend of its own (`InferenceBackend::fresh`,
118
+ `SessionState::remote`), so its turn can wait on the endpoint while another thread is
119
+ installed. Unknown session ids are rejected instead of silently borrowing the active thread's
120
+ cwd. Prompt cancellation takes no lock, which lets a client cancel a turn whatever it holds.
121
+ `session/load` and `session/resume` share `restore_session`; the only difference is that
122
+ resume must not replay the history as `session/update`. `session/close` is the one place a
123
+ `SessionState` is dropped. It runs in two halves: `begin_close` signals the session's turn
124
+ from the dispatch loop (the turn holds the session's lock, so the signal cannot wait for it) and
125
+ marks the id in `closing_sessions`, which makes `handle_prompt` cancel a prompt that was
126
+ still queued; `handle_close_session` then runs under the session and workspace locks and
127
+ removes the state, the permission grants and the background commands
128
+ (`tools::kill_session_tasks`). It leaves `session_store` alone, so a closed thread still lists
129
+ and reopens. The
119
130
  `SYSTEM_PROMPT` bakes in smbCloud-specific context the agent should use when the repo is clearly
120
131
  smbCloud, and stay general otherwise.
121
132
  - **`src/backend.rs`** — the `InferenceBackend` trait and neutral types (`ToolSpec`, `ToolCall`,
@@ -268,10 +279,21 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
268
279
  announced, because it sets the announced status: a call that will ask starts `pending`, the
269
280
  permission request carries that call's own id, and an `in_progress` update follows approval.
270
281
  That update also puts the card's title back, since the permission request overwrites it
271
- with the full arguments. Note: ACP turn-affecting handlers run in `cx.spawn`ed tasks
272
- serialized by `SiGitAgent::turn_lock` so the dispatch loop can route the client's permission
273
- answer mid-turn — don't move them back inline, and don't await client requests from inline
274
- handlers (deadlock).
282
+ with the full arguments. The Manual/Auto/Plan choice reaches an ACP client twice: as the
283
+ `sigit-permission-mode` config option, and as session `modes` with `session/set_mode`, which
284
+ the spec is retiring but some clients still render instead. Both are built from
285
+ `PERMISSION_MODES` in `main.rs` and read the same state, so whatever changes the mode has to
286
+ refresh both (`ConfigOptionUpdate` and `send_current_mode`). Note: ACP turn-affecting handlers run in `cx.spawn`ed tasks
287
+ so the dispatch loop can route the client's permission answer mid-turn — don't move them back
288
+ inline, and don't await client requests from inline handlers (deadlock). Two locks order them.
289
+ A per-session lock (`SiGitAgent::session_lock`) is held for a whole request, so requests on one
290
+ thread keep their order. `SiGitAgent::workspace_lock` guards what a session installs
291
+ process-wide (cwd, workspace roots, session MCP servers, the live backend slot, the engine's
292
+ conversation); lifecycle and config handlers hold it throughout, and `handle_prompt` holds it
293
+ through a `WorkspaceHold` that it releases while waiting on an HTTP endpoint or a permission
294
+ answer and retakes (reinstalling its session via `resume_workspace`) before any tool runs. An
295
+ on-device turn never releases it. Take the session lock first, never the other way round. Tool
296
+ execution, subagents included, still runs one session at a time.
275
297
  - **`src/instructions.rs`** — project instruction files, the always-on counterpart to skills.
276
298
  Reads `AGENTS.md` (the cross-tool [agents.md](https://agents.md) standard) and `CLAUDE.md`,
277
299
  walking from the session cwd up to the repo root (nearest ancestor with `.git`, never above it),
@@ -337,6 +359,10 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
337
359
  those read env vars once at init.
338
360
  - **`src/account.rs`** — siGit Code Cloud auth (`/login`, `/logout`, `/whoami`); authenticates
339
361
  against the account API and stores a session token. Performs no console I/O.
362
+ ACP `logout` (the editor's sign out button, shown only because `handle_initialize` advertises
363
+ `agentCapabilities.auth.logout`) and `/logout` both go through `SiGitAgent::sign_out` in
364
+ `main.rs`. The request names no session, so `leave_cloud` moves the parked threads off their
365
+ cloud tier along with the live one and each of them gets a fresh model picker.
340
366
  - **`src/browser_auth.rs`** — browser sign-in, the path the editor's "Sign in to siGit Code"
341
367
  button takes. sigit is a public OAuth client (client id `sigit-code-cli`, no secret, PKCE
342
368
  S256) against the authorization server at `$SIGIT_API_URL/oauth`. Two ways the code gets
@@ -205,6 +205,7 @@ the connection and the agent anymore. Don't reintroduce the mpsc forwarder patte
205
205
  | `ListSessionsRequest` | `handle_list_sessions` | the editor's "Import Threads" picker; gated by `SessionListCapabilities` |
206
206
  | `PromptRequest` | `handle_prompt` | the turn: parse blocks → slash commands or tool-calling loop |
207
207
  | `SetSessionConfigOptionRequest` | `handle_set_session_config_option` | the Zed model picker — switches/downloads models |
208
+ | `SetSessionModeRequest` | `handle_set_session_mode` | legacy mode selector; same Manual/Auto/Plan state as the Permissions config option |
208
209
  | `CancelNotification` | `handle_cancel` | notification, no response |
209
210
 
210
211
  Everything else is left to the SDK default (method not found).
@@ -205,6 +205,7 @@ the connection and the agent anymore. Don't reintroduce the mpsc forwarder patte
205
205
  | `ListSessionsRequest` | `handle_list_sessions` | the editor's "Import Threads" picker; gated by `SessionListCapabilities` |
206
206
  | `PromptRequest` | `handle_prompt` | the turn: parse blocks → slash commands or tool-calling loop |
207
207
  | `SetSessionConfigOptionRequest` | `handle_set_session_config_option` | the Zed model picker — switches/downloads models |
208
+ | `SetSessionModeRequest` | `handle_set_session_mode` | legacy mode selector; same Manual/Auto/Plan state as the Permissions config option |
208
209
  | `CancelNotification` | `handle_cancel` | notification, no response |
209
210
 
210
211
  Everything else is left to the SDK default (method not found).
@@ -0,0 +1,6 @@
1
+ [pull_request]
2
+ base = "development"
3
+ branch_prefix = "feature"
4
+
5
+ [agent]
6
+ max_tool_rounds = 60
@@ -0,0 +1,3 @@
1
+ [review.reviewer_skill]
2
+ name = "sigit-code-review-inner-loop"
3
+ version = "1.0.0"
@@ -113,9 +113,20 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
113
113
  - **`src/main.rs`** — entry point, mode dispatch, the full ACP `Agent` impl (session lifecycle:
114
114
  new/load/fork/prompt/cancel, config options, slash-command advertisement), and the `SYSTEM_PROMPT`.
115
115
  ACP session state owns its roots, conversation, and selected model even though the process has
116
- one live backend; activating a thread parks and restores all three. Unknown session ids are
117
- rejected instead of silently borrowing the active thread's cwd. Prompt cancellation is routed
118
- outside `turn_lock`, which lets a client cancel the turn currently holding that lock. The
116
+ one working directory and one live backend slot; activating a thread parks and restores all
117
+ three. A thread on an HTTP backend keeps a backend of its own (`InferenceBackend::fresh`,
118
+ `SessionState::remote`), so its turn can wait on the endpoint while another thread is
119
+ installed. Unknown session ids are rejected instead of silently borrowing the active thread's
120
+ cwd. Prompt cancellation takes no lock, which lets a client cancel a turn whatever it holds.
121
+ `session/load` and `session/resume` share `restore_session`; the only difference is that
122
+ resume must not replay the history as `session/update`. `session/close` is the one place a
123
+ `SessionState` is dropped. It runs in two halves: `begin_close` signals the session's turn
124
+ from the dispatch loop (the turn holds the session's lock, so the signal cannot wait for it) and
125
+ marks the id in `closing_sessions`, which makes `handle_prompt` cancel a prompt that was
126
+ still queued; `handle_close_session` then runs under the session and workspace locks and
127
+ removes the state, the permission grants and the background commands
128
+ (`tools::kill_session_tasks`). It leaves `session_store` alone, so a closed thread still lists
129
+ and reopens. The
119
130
  `SYSTEM_PROMPT` bakes in smbCloud-specific context the agent should use when the repo is clearly
120
131
  smbCloud, and stay general otherwise.
121
132
  - **`src/backend.rs`** — the `InferenceBackend` trait and neutral types (`ToolSpec`, `ToolCall`,
@@ -268,10 +279,21 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
268
279
  announced, because it sets the announced status: a call that will ask starts `pending`, the
269
280
  permission request carries that call's own id, and an `in_progress` update follows approval.
270
281
  That update also puts the card's title back, since the permission request overwrites it
271
- with the full arguments. Note: ACP turn-affecting handlers run in `cx.spawn`ed tasks
272
- serialized by `SiGitAgent::turn_lock` so the dispatch loop can route the client's permission
273
- answer mid-turn — don't move them back inline, and don't await client requests from inline
274
- handlers (deadlock).
282
+ with the full arguments. The Manual/Auto/Plan choice reaches an ACP client twice: as the
283
+ `sigit-permission-mode` config option, and as session `modes` with `session/set_mode`, which
284
+ the spec is retiring but some clients still render instead. Both are built from
285
+ `PERMISSION_MODES` in `main.rs` and read the same state, so whatever changes the mode has to
286
+ refresh both (`ConfigOptionUpdate` and `send_current_mode`). Note: ACP turn-affecting handlers run in `cx.spawn`ed tasks
287
+ so the dispatch loop can route the client's permission answer mid-turn — don't move them back
288
+ inline, and don't await client requests from inline handlers (deadlock). Two locks order them.
289
+ A per-session lock (`SiGitAgent::session_lock`) is held for a whole request, so requests on one
290
+ thread keep their order. `SiGitAgent::workspace_lock` guards what a session installs
291
+ process-wide (cwd, workspace roots, session MCP servers, the live backend slot, the engine's
292
+ conversation); lifecycle and config handlers hold it throughout, and `handle_prompt` holds it
293
+ through a `WorkspaceHold` that it releases while waiting on an HTTP endpoint or a permission
294
+ answer and retakes (reinstalling its session via `resume_workspace`) before any tool runs. An
295
+ on-device turn never releases it. Take the session lock first, never the other way round. Tool
296
+ execution, subagents included, still runs one session at a time.
275
297
  - **`src/instructions.rs`** — project instruction files, the always-on counterpart to skills.
276
298
  Reads `AGENTS.md` (the cross-tool [agents.md](https://agents.md) standard) and `CLAUDE.md`,
277
299
  walking from the session cwd up to the repo root (nearest ancestor with `.git`, never above it),
@@ -337,6 +359,10 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
337
359
  those read env vars once at init.
338
360
  - **`src/account.rs`** — siGit Code Cloud auth (`/login`, `/logout`, `/whoami`); authenticates
339
361
  against the account API and stores a session token. Performs no console I/O.
362
+ ACP `logout` (the editor's sign out button, shown only because `handle_initialize` advertises
363
+ `agentCapabilities.auth.logout`) and `/logout` both go through `SiGitAgent::sign_out` in
364
+ `main.rs`. The request names no session, so `leave_cloud` moves the parked threads off their
365
+ cloud tier along with the live one and each of them gets a fresh model picker.
340
366
  - **`src/browser_auth.rs`** — browser sign-in, the path the editor's "Sign in to siGit Code"
341
367
  button takes. sigit is a public OAuth client (client id `sigit-code-cli`, no secret, PKCE
342
368
  S256) against the authorization server at `$SIGIT_API_URL/oauth`. Two ways the code gets
@@ -1,5 +1,85 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.4
4
+
5
+ ### Added
6
+
7
+ - **DeepSeek DSML tool calls written into the reply text are recovered.** A
8
+ DeepSeek-family endpoint can render a tool call as literal
9
+ `<|DSML|tool_calls>` text instead of the structured `tool_calls` field,
10
+ which streamed the tag into the editor and ended the turn with nothing run.
11
+ The inline tool-call scanner that already recovered the GLM XML and Kimi K3
12
+ shapes now recovers the DSML shape too, including a marker split across
13
+ stream chunks. A block that fails to parse is still kept out of the reply
14
+ and the history, and the model is told the call did not run (#181).
15
+
16
+ ### Fixed
17
+
18
+ - **The round-cap stop message no longer carries a stray run of spaces.**
19
+ When the tool-round cap ended a turn, the closing message could collapse a
20
+ space run into the wrong spot, which the editor then rendered verbatim
21
+ (#177).
22
+
23
+ ## 1.6.3
24
+
25
+ ### Added
26
+
27
+ - **A client can resume a session and close one.** siGit Code now advertises
28
+ `sessionCapabilities.resume` and `sessionCapabilities.close`.
29
+ `session/resume` restores a saved thread the way `session/load` does, but
30
+ sends none of it back: it is for a client that still has the thread on
31
+ screen, after an agent restart for example. `session/close` cancels the
32
+ session's running turn and drops what the process held for it, which is its
33
+ state, its permission grants and plan mode, the MCP servers the client named
34
+ for it, and the background commands it started. The saved history is left
35
+ alone, so a closed thread still lists and reopens (#149).
36
+
37
+ - **The permission modes are also offered as ACP session modes.** Manual, Auto
38
+ and Plan were only reachable as a config option, which is what the protocol
39
+ now recommends and what Zed reads. A client that draws the older mode
40
+ selector saw nothing. `session/new`, `session/load` and `session/fork` now
41
+ return the same three in `modes`, `session/set_mode` switches between them,
42
+ and a change made any other way (the config option, `/plan`, `/clear`,
43
+ `/reload`) is announced with `current_mode_update`. A client that reads
44
+ config options ignores `modes`, so nothing changes there (#148).
45
+
46
+ - **An editor can sign out of siGit Code.** The `initialize` reply carried an
47
+ empty `auth` object, so a client drew no sign out button, and typing
48
+ `/logout` was the only way out of an account. The reply now advertises
49
+ `agentCapabilities.auth.logout`, and a `logout` request ends the account
50
+ session through the method `/logout` calls. The request names no session,
51
+ so it moves every open thread that used the account, not only the live one:
52
+ a thread parked on a cloud tier goes on-device with the live thread instead
53
+ of failing to restore its tier on its next prompt, and every thread that
54
+ moved gets a fresh model picker. Local inference is switched back on if it
55
+ was off, so a thread opened after signing out asks for no cloud tier
56
+ (#150).
57
+
58
+ ### Changed
59
+
60
+ - **Two threads can run a turn at the same time.** An editor runs one siGit
61
+ Code process for every thread it has open, and a single lock ordered every
62
+ turn-affecting request in it, so a second prompt waited for the first
63
+ thread's whole turn and two prompts never overlapped. The lock held what a
64
+ session installs process-wide — the working directory, the workspace roots,
65
+ the MCP servers the client named for it, the live backend — none of which a
66
+ turn touches while it waits on an HTTP endpoint, which is most of its time.
67
+ A session on an HTTP backend now keeps a backend of its own, so its
68
+ conversation is no longer swapped in and out of a shared one, and a turn
69
+ releases the process-wide lock while it waits on the endpoint or on a
70
+ permission answer and takes it back, reinstalling its session, before any
71
+ tool runs. On-device turns are unchanged, since the engine keeps one
72
+ conversation, and tool execution still runs one session at a time (#176).
73
+
74
+ ### Fixed
75
+
76
+ - **A prompt whose inference failed is kept through a model switch.** A turn
77
+ that ended in an endpoint error stayed in the client's thread, but the
78
+ carryover into a new model dropped its trailing user message, and the error
79
+ path never saved the session. Only a cancelled prompt should be taken back
80
+ out, so cancellation now removes it from the live history, and the error
81
+ path persists the session (#124).
82
+
3
83
  ## 1.6.2
4
84
 
5
85
  ### Added
@@ -113,9 +113,20 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
113
113
  - **`src/main.rs`** — entry point, mode dispatch, the full ACP `Agent` impl (session lifecycle:
114
114
  new/load/fork/prompt/cancel, config options, slash-command advertisement), and the `SYSTEM_PROMPT`.
115
115
  ACP session state owns its roots, conversation, and selected model even though the process has
116
- one live backend; activating a thread parks and restores all three. Unknown session ids are
117
- rejected instead of silently borrowing the active thread's cwd. Prompt cancellation is routed
118
- outside `turn_lock`, which lets a client cancel the turn currently holding that lock. The
116
+ one working directory and one live backend slot; activating a thread parks and restores all
117
+ three. A thread on an HTTP backend keeps a backend of its own (`InferenceBackend::fresh`,
118
+ `SessionState::remote`), so its turn can wait on the endpoint while another thread is
119
+ installed. Unknown session ids are rejected instead of silently borrowing the active thread's
120
+ cwd. Prompt cancellation takes no lock, which lets a client cancel a turn whatever it holds.
121
+ `session/load` and `session/resume` share `restore_session`; the only difference is that
122
+ resume must not replay the history as `session/update`. `session/close` is the one place a
123
+ `SessionState` is dropped. It runs in two halves: `begin_close` signals the session's turn
124
+ from the dispatch loop (the turn holds the session's lock, so the signal cannot wait for it) and
125
+ marks the id in `closing_sessions`, which makes `handle_prompt` cancel a prompt that was
126
+ still queued; `handle_close_session` then runs under the session and workspace locks and
127
+ removes the state, the permission grants and the background commands
128
+ (`tools::kill_session_tasks`). It leaves `session_store` alone, so a closed thread still lists
129
+ and reopens. The
119
130
  `SYSTEM_PROMPT` bakes in smbCloud-specific context the agent should use when the repo is clearly
120
131
  smbCloud, and stay general otherwise.
121
132
  - **`src/backend.rs`** — the `InferenceBackend` trait and neutral types (`ToolSpec`, `ToolCall`,
@@ -268,10 +279,21 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
268
279
  announced, because it sets the announced status: a call that will ask starts `pending`, the
269
280
  permission request carries that call's own id, and an `in_progress` update follows approval.
270
281
  That update also puts the card's title back, since the permission request overwrites it
271
- with the full arguments. Note: ACP turn-affecting handlers run in `cx.spawn`ed tasks
272
- serialized by `SiGitAgent::turn_lock` so the dispatch loop can route the client's permission
273
- answer mid-turn — don't move them back inline, and don't await client requests from inline
274
- handlers (deadlock).
282
+ with the full arguments. The Manual/Auto/Plan choice reaches an ACP client twice: as the
283
+ `sigit-permission-mode` config option, and as session `modes` with `session/set_mode`, which
284
+ the spec is retiring but some clients still render instead. Both are built from
285
+ `PERMISSION_MODES` in `main.rs` and read the same state, so whatever changes the mode has to
286
+ refresh both (`ConfigOptionUpdate` and `send_current_mode`). Note: ACP turn-affecting handlers run in `cx.spawn`ed tasks
287
+ so the dispatch loop can route the client's permission answer mid-turn — don't move them back
288
+ inline, and don't await client requests from inline handlers (deadlock). Two locks order them.
289
+ A per-session lock (`SiGitAgent::session_lock`) is held for a whole request, so requests on one
290
+ thread keep their order. `SiGitAgent::workspace_lock` guards what a session installs
291
+ process-wide (cwd, workspace roots, session MCP servers, the live backend slot, the engine's
292
+ conversation); lifecycle and config handlers hold it throughout, and `handle_prompt` holds it
293
+ through a `WorkspaceHold` that it releases while waiting on an HTTP endpoint or a permission
294
+ answer and retakes (reinstalling its session via `resume_workspace`) before any tool runs. An
295
+ on-device turn never releases it. Take the session lock first, never the other way round. Tool
296
+ execution, subagents included, still runs one session at a time.
275
297
  - **`src/instructions.rs`** — project instruction files, the always-on counterpart to skills.
276
298
  Reads `AGENTS.md` (the cross-tool [agents.md](https://agents.md) standard) and `CLAUDE.md`,
277
299
  walking from the session cwd up to the repo root (nearest ancestor with `.git`, never above it),
@@ -337,6 +359,10 @@ feeds results back. Neither the loop nor ACP/TUI surfaces depend on a concrete b
337
359
  those read env vars once at init.
338
360
  - **`src/account.rs`** — siGit Code Cloud auth (`/login`, `/logout`, `/whoami`); authenticates
339
361
  against the account API and stores a session token. Performs no console I/O.
362
+ ACP `logout` (the editor's sign out button, shown only because `handle_initialize` advertises
363
+ `agentCapabilities.auth.logout`) and `/logout` both go through `SiGitAgent::sign_out` in
364
+ `main.rs`. The request names no session, so `leave_cloud` moves the parked threads off their
365
+ cloud tier along with the live one and each of them gets a fresh model picker.
340
366
  - **`src/browser_auth.rs`** — browser sign-in, the path the editor's "Sign in to siGit Code"
341
367
  button takes. sigit is a public OAuth client (client id `sigit-code-cli`, no secret, PKCE
342
368
  S256) against the authorization server at `$SIGIT_API_URL/oauth`. Two ways the code gets
@@ -5778,7 +5778,7 @@ checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
5778
5778
 
5779
5779
  [[package]]
5780
5780
  name = "sigit"
5781
- version = "1.6.2"
5781
+ version = "1.6.4"
5782
5782
  dependencies = [
5783
5783
  "agent-client-protocol",
5784
5784
  "anyhow",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "sigit"
3
- version = "1.6.2"
3
+ version = "1.6.4"
4
4
  edition = "2024"
5
5
  description = "siGit Code — ACP-compatible AI coding agent. Sí, git."
6
6
  documentation = "https://github.com/getsigit/sigit"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sigit-code
3
- Version: 1.6.2
3
+ Version: 1.6.4
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -445,6 +445,15 @@ pub trait InferenceBackend: Send + Sync {
445
445
  /// local model while requests actually go to the cloud.
446
446
  fn is_remote(&self) -> bool;
447
447
 
448
+ /// A backend for the same endpoint and model with an empty conversation,
449
+ /// or `None` when the conversation cannot be separated from the backend
450
+ /// (on-device, the engine holds it). An editor keeps several threads open
451
+ /// in one process; this is what lets each of them own its history instead
452
+ /// of taking turns on a shared one.
453
+ fn fresh(&self) -> Option<Arc<dyn InferenceBackend>> {
454
+ None
455
+ }
456
+
448
457
  /// A serializable snapshot of the conversation history, one JSON object per
449
458
  /// message (`{"role": ..., "content": ...}` at minimum). The snapshot is
450
459
  /// what the session store persists; it includes any seeded system message
@@ -1554,6 +1563,19 @@ impl InferenceBackend for OpenAiBackend {
1554
1563
  true
1555
1564
  }
1556
1565
 
1566
+ fn fresh(&self) -> Option<Arc<dyn InferenceBackend>> {
1567
+ Some(Arc::new(Self {
1568
+ base_url: self.base_url.clone(),
1569
+ api_key: self.api_key.clone(),
1570
+ model: self.model.clone(),
1571
+ // The client is a handle on one connection pool; sharing it keeps
1572
+ // connections to the endpoint warm across sessions.
1573
+ http: self.http.clone(),
1574
+ history: Mutex::new(Vec::new()),
1575
+ accepts_images: self.accepts_images,
1576
+ }))
1577
+ }
1578
+
1557
1579
  async fn history_snapshot(&self) -> Vec<serde_json::Value> {
1558
1580
  self.history.lock().await.clone()
1559
1581
  }
@@ -1944,18 +1966,25 @@ pub fn carryover_history(snapshot: Vec<serde_json::Value>) -> Vec<serde_json::Va
1944
1966
  }
1945
1967
  }
1946
1968
 
1947
- // Session switches happen only after the serialized turn has ended. A
1948
- // trailing user message therefore belongs to a cancelled/failed inference
1949
- // request with no answer; carrying it would make the next activation feed
1950
- // the model an orphaned prompt that the client considers cancelled.
1951
- while carried
1969
+ carried
1970
+ }
1971
+
1972
+ /// Take a cancelled prompt's user message back out of the live history.
1973
+ ///
1974
+ /// The client drops a cancelled prompt from its thread, so the model must not
1975
+ /// keep answering it. Only the first inference round can cancel with the user
1976
+ /// message as the last entry; later rounds end on tool or assistant output.
1977
+ /// A prompt that fails with an error is not taken back: the client keeps it,
1978
+ /// so the history keeps it too and a model switch carries it over.
1979
+ pub async fn forget_trailing_user_message(backend: &dyn InferenceBackend) {
1980
+ let mut history = backend.history_snapshot().await;
1981
+ if history
1952
1982
  .last()
1953
1983
  .is_some_and(|message| message["role"] == "user")
1954
1984
  {
1955
- carried.pop();
1985
+ history.pop();
1986
+ backend.restore_history(history).await;
1956
1987
  }
1957
-
1958
- carried
1959
1988
  }
1960
1989
 
1961
1990
  /// Replay `carried` (from [`carryover_history`]) into `backend`, on top of the
@@ -2161,16 +2190,39 @@ mod tests {
2161
2190
  }
2162
2191
 
2163
2192
  #[test]
2164
- fn carryover_drops_a_cancelled_trailing_user_message() {
2193
+ fn carryover_keeps_a_trailing_user_message_from_a_failed_turn() {
2194
+ // The client keeps a prompt whose inference errored, so a switch must
2195
+ // not drop it (issue #124).
2165
2196
  let carried = carryover_history(vec![
2166
2197
  serde_json::json!({ "role": "system", "content": "prompt" }),
2167
2198
  serde_json::json!({ "role": "user", "content": "completed question" }),
2168
2199
  serde_json::json!({ "role": "assistant", "content": "completed answer" }),
2169
- serde_json::json!({ "role": "user", "content": "cancelled question" }),
2200
+ serde_json::json!({ "role": "user", "content": "failed question" }),
2170
2201
  ]);
2171
2202
 
2172
- assert_eq!(carried.len(), 2, "{carried:#?}");
2173
- assert_eq!(carried.last().unwrap()["content"], "completed answer");
2203
+ assert_eq!(carried.len(), 3, "{carried:#?}");
2204
+ assert_eq!(carried.last().unwrap()["content"], "failed question");
2205
+ }
2206
+
2207
+ #[tokio::test]
2208
+ async fn forget_trailing_user_message_drops_only_a_cancelled_prompt() {
2209
+ let backend = OpenAiBackend::new("http://localhost", "", "m", None);
2210
+ backend
2211
+ .restore_history(vec![
2212
+ serde_json::json!({ "role": "user", "content": "completed question" }),
2213
+ serde_json::json!({ "role": "assistant", "content": "completed answer" }),
2214
+ serde_json::json!({ "role": "user", "content": "cancelled question" }),
2215
+ ])
2216
+ .await;
2217
+
2218
+ forget_trailing_user_message(&backend).await;
2219
+
2220
+ let history = backend.history_snapshot().await;
2221
+ assert_eq!(history.len(), 2, "{history:#?}");
2222
+ assert_eq!(history[1]["content"], "completed answer");
2223
+
2224
+ forget_trailing_user_message(&backend).await;
2225
+ assert_eq!(backend.history_snapshot().await.len(), 2);
2174
2226
  }
2175
2227
 
2176
2228
  #[test]