Skip to content
Open
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
5 changes: 5 additions & 0 deletions src/kimi_cli/web/runner/process.py
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,11 @@ async def _encode_uploaded_files(self) -> AsyncGenerator[ContentPart]:
# Mark files as sent
for file in files:
self._sent_files.add(file.name)
with contextlib.suppress(Exception):
sent_marker.write_text(
json.dumps(sorted(self._sent_files), ensure_ascii=False),
encoding="utf-8",
Comment on lines +507 to +509
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Persist upload marker only after prompt send succeeds

Writing .sent here commits uploads as delivered before send_message() actually forwards the prompt to the worker (stdin.write/drain happens later). If the worker exits or the pipe write fails in that window, the prompt never reaches the model, but the persisted marker survives restart and causes those files to be skipped on retry, silently dropping user uploads. This regression is specific to restart/error paths introduced by persisting the marker in _encode_uploaded_files.

Useful? React with 👍 / 👎.

)

async def _handle_in_message(self, message: JSONRPCInMessage) -> str | None:
"""Handle inbound message to worker, encoding uploaded files."""
Expand Down
37 changes: 37 additions & 0 deletions tests/web/test_uploaded_files.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Tests for web runner upload handling."""

from __future__ import annotations

import json
from types import SimpleNamespace
from uuid import uuid4

import pytest
from kosong.message import TextPart

from kimi_cli.web.runner import process as process_module
from kimi_cli.web.runner.process import SessionProcess


@pytest.mark.asyncio
async def test_uploaded_files_marker_survives_process_restart(
tmp_path, monkeypatch: pytest.MonkeyPatch
) -> None:
session_dir = tmp_path / "session"
uploads = session_dir / "uploads"
uploads.mkdir(parents=True)
(uploads / "note.txt").write_text("hello", encoding="utf-8")

session = SimpleNamespace(kimi_cli_session=SimpleNamespace(dir=session_dir))
monkeypatch.setattr(process_module, "load_session_by_id", lambda _session_id: session)

first_process = SessionProcess(uuid4())
first_parts = [part async for part in first_process._encode_uploaded_files()]

assert any(isinstance(part, TextPart) and "note.txt" in part.text for part in first_parts)
assert json.loads((uploads / ".sent").read_text(encoding="utf-8")) == ["note.txt"]

restarted_process = SessionProcess(uuid4())
restarted_parts = [part async for part in restarted_process._encode_uploaded_files()]

assert restarted_parts == []
Loading