sigit-code 1.6.2__tar.gz → 1.6.3__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 (107) hide show
  1. {sigit_code-1.6.2 → sigit_code-1.6.3}/.agents/AGENTS.md +33 -7
  2. {sigit_code-1.6.2/.claude → sigit_code-1.6.3/.agents}/skills/agent-client-protocol/SKILL.md +1 -0
  3. {sigit_code-1.6.2/.agents → sigit_code-1.6.3/.claude}/skills/agent-client-protocol/SKILL.md +1 -0
  4. {sigit_code-1.6.2 → sigit_code-1.6.3}/AGENTS.md +33 -7
  5. {sigit_code-1.6.2 → sigit_code-1.6.3}/CHANGELOG.md +60 -0
  6. {sigit_code-1.6.2 → sigit_code-1.6.3}/CLAUDE.md +33 -7
  7. {sigit_code-1.6.2 → sigit_code-1.6.3}/Cargo.lock +1 -1
  8. {sigit_code-1.6.2 → sigit_code-1.6.3}/Cargo.toml +1 -1
  9. {sigit_code-1.6.2 → sigit_code-1.6.3}/PKG-INFO +1 -1
  10. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/backend.rs +64 -12
  11. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/main.rs +1024 -185
  12. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/tools.rs +52 -0
  13. sigit_code-1.6.3/tests/acp_concurrent_sessions.rs +343 -0
  14. sigit_code-1.6.3/tests/acp_logout.rs +179 -0
  15. {sigit_code-1.6.2 → sigit_code-1.6.3}/tests/acp_permissions.rs +138 -0
  16. {sigit_code-1.6.2 → sigit_code-1.6.3}/tests/acp_session_load.rs +170 -0
  17. {sigit_code-1.6.2 → sigit_code-1.6.3}/.agents/skills/ai-assisted-coding/SKILL.md +0 -0
  18. {sigit_code-1.6.2 → sigit_code-1.6.3}/.agents/skills/branding/SKILL.md +0 -0
  19. {sigit_code-1.6.2 → sigit_code-1.6.3}/.agents/skills/run-sigit/SKILL.md +0 -0
  20. {sigit_code-1.6.2 → sigit_code-1.6.3}/.agents/skills/run-sigit/driver.mjs +0 -0
  21. {sigit_code-1.6.2 → sigit_code-1.6.3}/.agents/skills/run-sigit/tui-smoke.sh +0 -0
  22. {sigit_code-1.6.2 → sigit_code-1.6.3}/.agents/skills/sigit-code-release/SKILL.md +0 -0
  23. {sigit_code-1.6.2 → sigit_code-1.6.3}/.agents/skills/tool-calling/SKILL.md +0 -0
  24. {sigit_code-1.6.2 → sigit_code-1.6.3}/.cargo/config.toml +0 -0
  25. {sigit_code-1.6.2 → sigit_code-1.6.3}/.claude/skills/ai-assisted-coding/SKILL.md +0 -0
  26. {sigit_code-1.6.2 → sigit_code-1.6.3}/.claude/skills/branding/SKILL.md +0 -0
  27. {sigit_code-1.6.2 → sigit_code-1.6.3}/.claude/skills/run-sigit/SKILL.md +0 -0
  28. {sigit_code-1.6.2 → sigit_code-1.6.3}/.claude/skills/run-sigit/driver.mjs +0 -0
  29. {sigit_code-1.6.2 → sigit_code-1.6.3}/.claude/skills/run-sigit/tui-smoke.sh +0 -0
  30. {sigit_code-1.6.2 → sigit_code-1.6.3}/.claude/skills/sigit-code-release/SKILL.md +0 -0
  31. {sigit_code-1.6.2 → sigit_code-1.6.3}/.claude/skills/tool-calling/SKILL.md +0 -0
  32. {sigit_code-1.6.2 → sigit_code-1.6.3}/.github/workflows/ci.yml +0 -0
  33. {sigit_code-1.6.2 → sigit_code-1.6.3}/.github/workflows/release-aur.yml +0 -0
  34. {sigit_code-1.6.2 → sigit_code-1.6.3}/.github/workflows/release-crates.yml +0 -0
  35. {sigit_code-1.6.2 → sigit_code-1.6.3}/.github/workflows/release-github.yml +0 -0
  36. {sigit_code-1.6.2 → sigit_code-1.6.3}/.github/workflows/release-homebrew.yml +0 -0
  37. {sigit_code-1.6.2 → sigit_code-1.6.3}/.github/workflows/release-npm.yml +0 -0
  38. {sigit_code-1.6.2 → sigit_code-1.6.3}/.github/workflows/release-nuget.yml +0 -0
  39. {sigit_code-1.6.2 → sigit_code-1.6.3}/.github/workflows/release-pypi.yml +0 -0
  40. {sigit_code-1.6.2 → sigit_code-1.6.3}/.github/workflows/release-scoop.yml +0 -0
  41. {sigit_code-1.6.2 → sigit_code-1.6.3}/.github/workflows/release-winget.yml +0 -0
  42. {sigit_code-1.6.2 → sigit_code-1.6.3}/.gitignore +0 -0
  43. {sigit_code-1.6.2 → sigit_code-1.6.3}/.nvmrc +0 -0
  44. {sigit_code-1.6.2 → sigit_code-1.6.3}/.sigit/factory/config.toml +0 -0
  45. {sigit_code-1.6.2 → sigit_code-1.6.3}/LICENSE +0 -0
  46. {sigit_code-1.6.2 → sigit_code-1.6.3}/README.md +0 -0
  47. {sigit_code-1.6.2 → sigit_code-1.6.3}/docs/headless.md +0 -0
  48. {sigit_code-1.6.2 → sigit_code-1.6.3}/docs/hooks.md +0 -0
  49. {sigit_code-1.6.2 → sigit_code-1.6.3}/docs/mcp.md +0 -0
  50. {sigit_code-1.6.2 → sigit_code-1.6.3}/examples/settings-with-hooks.toml +0 -0
  51. {sigit_code-1.6.2 → sigit_code-1.6.3}/examples/skills/README.md +0 -0
  52. {sigit_code-1.6.2 → sigit_code-1.6.3}/examples/skills/commit-message/SKILL.md +0 -0
  53. {sigit_code-1.6.2 → sigit_code-1.6.3}/npm/README.md.tmpl +0 -0
  54. {sigit_code-1.6.2 → sigit_code-1.6.3}/npm/package-compat.json.tmpl +0 -0
  55. {sigit_code-1.6.2 → sigit_code-1.6.3}/npm/package-main.json.tmpl +0 -0
  56. {sigit_code-1.6.2 → sigit_code-1.6.3}/npm/package.json.tmpl +0 -0
  57. {sigit_code-1.6.2 → sigit_code-1.6.3}/npm/scripts/render-main-package.cjs +0 -0
  58. {sigit_code-1.6.2 → sigit_code-1.6.3}/npm/scripts/render-platform-package.cjs +0 -0
  59. {sigit_code-1.6.2 → sigit_code-1.6.3}/npm/sigit/.gitignore +0 -0
  60. {sigit_code-1.6.2 → sigit_code-1.6.3}/npm/sigit/README.md +0 -0
  61. {sigit_code-1.6.2 → sigit_code-1.6.3}/npm/sigit/package.json +0 -0
  62. {sigit_code-1.6.2 → sigit_code-1.6.3}/npm/sigit/src/index.ts +0 -0
  63. {sigit_code-1.6.2 → sigit_code-1.6.3}/npm/sigit/tsconfig.json +0 -0
  64. {sigit_code-1.6.2 → sigit_code-1.6.3}/nuget/.gitignore +0 -0
  65. {sigit_code-1.6.2 → sigit_code-1.6.3}/nuget/sigit/Program.cs +0 -0
  66. {sigit_code-1.6.2 → sigit_code-1.6.3}/nuget/sigit/README.md +0 -0
  67. {sigit_code-1.6.2 → sigit_code-1.6.3}/nuget/sigit/SiGit.Code.csproj +0 -0
  68. {sigit_code-1.6.2 → sigit_code-1.6.3}/packaging/aur/PKGBUILD.in +0 -0
  69. {sigit_code-1.6.2 → sigit_code-1.6.3}/packaging/nfpm.yaml +0 -0
  70. {sigit_code-1.6.2 → sigit_code-1.6.3}/packaging/windows/check-static-crt.ps1 +0 -0
  71. {sigit_code-1.6.2 → sigit_code-1.6.3}/packaging/winget/getSigit.siGitCode.installer.yaml.in +0 -0
  72. {sigit_code-1.6.2 → sigit_code-1.6.3}/packaging/winget/getSigit.siGitCode.locale.en-US.yaml.in +0 -0
  73. {sigit_code-1.6.2 → sigit_code-1.6.3}/packaging/winget/getSigit.siGitCode.yaml.in +0 -0
  74. {sigit_code-1.6.2 → sigit_code-1.6.3}/pypi/README.md +0 -0
  75. {sigit_code-1.6.2 → sigit_code-1.6.3}/pypi/pyproject.toml +0 -0
  76. {sigit_code-1.6.2 → sigit_code-1.6.3}/pyproject.toml +0 -0
  77. {sigit_code-1.6.2 → sigit_code-1.6.3}/rust-toolchain.toml +0 -0
  78. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/account.rs +0 -0
  79. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/browser_auth.rs +0 -0
  80. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/chat.rs +0 -0
  81. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/client_fs.rs +0 -0
  82. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/commands.rs +0 -0
  83. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/credentials.rs +0 -0
  84. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/frontmatter.rs +0 -0
  85. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/headless.rs +0 -0
  86. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/hooks.rs +0 -0
  87. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/inline_tool_calls.rs +0 -0
  88. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/instructions.rs +0 -0
  89. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/mcp.rs +0 -0
  90. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/models.rs +0 -0
  91. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/permissions.rs +0 -0
  92. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/provider.rs +0 -0
  93. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/session_store.rs +0 -0
  94. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/settings.rs +0 -0
  95. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/setup.rs +0 -0
  96. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/skills.rs +0 -0
  97. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/subagents.rs +0 -0
  98. {sigit_code-1.6.2 → sigit_code-1.6.3}/src/workspace.rs +0 -0
  99. {sigit_code-1.6.2 → sigit_code-1.6.3}/tests/acp_client_fs.rs +0 -0
  100. {sigit_code-1.6.2 → sigit_code-1.6.3}/tests/acp_endpoint_errors.rs +0 -0
  101. {sigit_code-1.6.2 → sigit_code-1.6.3}/tests/acp_images.rs +0 -0
  102. {sigit_code-1.6.2 → sigit_code-1.6.3}/tests/acp_multi_root.rs +0 -0
  103. {sigit_code-1.6.2 → sigit_code-1.6.3}/tests/acp_session_list.rs +0 -0
  104. {sigit_code-1.6.2 → sigit_code-1.6.3}/tests/acp_stop_reasons.rs +0 -0
  105. {sigit_code-1.6.2 → sigit_code-1.6.3}/tests/acp_tool_stdin.rs +0 -0
  106. {sigit_code-1.6.2 → sigit_code-1.6.3}/tests/headless_mode.rs +0 -0
  107. {sigit_code-1.6.2 → sigit_code-1.6.3}/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).
@@ -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,65 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.6.3
4
+
5
+ ### Added
6
+
7
+ - **A client can resume a session and close one.** siGit Code now advertises
8
+ `sessionCapabilities.resume` and `sessionCapabilities.close`.
9
+ `session/resume` restores a saved thread the way `session/load` does, but
10
+ sends none of it back: it is for a client that still has the thread on
11
+ screen, after an agent restart for example. `session/close` cancels the
12
+ session's running turn and drops what the process held for it, which is its
13
+ state, its permission grants and plan mode, the MCP servers the client named
14
+ for it, and the background commands it started. The saved history is left
15
+ alone, so a closed thread still lists and reopens (#149).
16
+
17
+ - **The permission modes are also offered as ACP session modes.** Manual, Auto
18
+ and Plan were only reachable as a config option, which is what the protocol
19
+ now recommends and what Zed reads. A client that draws the older mode
20
+ selector saw nothing. `session/new`, `session/load` and `session/fork` now
21
+ return the same three in `modes`, `session/set_mode` switches between them,
22
+ and a change made any other way (the config option, `/plan`, `/clear`,
23
+ `/reload`) is announced with `current_mode_update`. A client that reads
24
+ config options ignores `modes`, so nothing changes there (#148).
25
+
26
+ - **An editor can sign out of siGit Code.** The `initialize` reply carried an
27
+ empty `auth` object, so a client drew no sign out button, and typing
28
+ `/logout` was the only way out of an account. The reply now advertises
29
+ `agentCapabilities.auth.logout`, and a `logout` request ends the account
30
+ session through the method `/logout` calls. The request names no session,
31
+ so it moves every open thread that used the account, not only the live one:
32
+ a thread parked on a cloud tier goes on-device with the live thread instead
33
+ of failing to restore its tier on its next prompt, and every thread that
34
+ moved gets a fresh model picker. Local inference is switched back on if it
35
+ was off, so a thread opened after signing out asks for no cloud tier
36
+ (#150).
37
+
38
+ ### Changed
39
+
40
+ - **Two threads can run a turn at the same time.** An editor runs one siGit
41
+ Code process for every thread it has open, and a single lock ordered every
42
+ turn-affecting request in it, so a second prompt waited for the first
43
+ thread's whole turn and two prompts never overlapped. The lock held what a
44
+ session installs process-wide — the working directory, the workspace roots,
45
+ the MCP servers the client named for it, the live backend — none of which a
46
+ turn touches while it waits on an HTTP endpoint, which is most of its time.
47
+ A session on an HTTP backend now keeps a backend of its own, so its
48
+ conversation is no longer swapped in and out of a shared one, and a turn
49
+ releases the process-wide lock while it waits on the endpoint or on a
50
+ permission answer and takes it back, reinstalling its session, before any
51
+ tool runs. On-device turns are unchanged, since the engine keeps one
52
+ conversation, and tool execution still runs one session at a time (#176).
53
+
54
+ ### Fixed
55
+
56
+ - **A prompt whose inference failed is kept through a model switch.** A turn
57
+ that ended in an endpoint error stayed in the client's thread, but the
58
+ carryover into a new model dropped its trailing user message, and the error
59
+ path never saved the session. Only a cancelled prompt should be taken back
60
+ out, so cancellation now removes it from the live history, and the error
61
+ path persists the session (#124).
62
+
3
63
  ## 1.6.2
4
64
 
5
65
  ### 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.3"
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.3"
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.3
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]