Commit Graph
100 Commits
Author SHA1 Message Date
WHCandCursor 814a31c9c8 feat: restore CI/release, prompt contracts, faster install (0.9.33)
Bring back Gitea CI + auto-publish; share release helpers; strengthen multi-turn
tool/job prompts with a unit contract; ccache/make help and --release bootstrap.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 19:56:42 +08:00
WHCandCursor f00aad9e3e feat: solo default, job completion reports, tools wave-1 (0.9.32)
Password Realm no longer auto-enables parliament (keeps task/worker tools);
finished jobs fan out to agent notes + TUI; raise PAR_MAX/grep caps and tag truncations.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 19:48:02 +08:00
WHCandCursor 5917c48bd0 feat: TUI completeness — identity, Seat Deck empty states, lean chrome (0.9.31)
Sanitize session titles, stamp CST on the topbar, fill work/sync/keys so
the sidebar never looks unfinished, drop duplicate ctx from the foot, and
tone down the scroll banner plus composer mode line.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 19:35:21 +08:00
WHCandCursor 2ccad623e4 fix: drop stranded topbar chair chip; blend chrome into panel (0.9.30)
Seat role already lives in the Seat Deck. Remove the lonely right-edge
chip and paint the session strip on panel ground so it no longer reads
as a raised bar with a hairline seam.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 19:18:55 +08:00
WHCandCursor 67b9d6683f fix: keep stream write-head on tip glyph, not next-line col0 (0.9.29)
Appending the scanning caret after trailing newlines parked a lonely
block at the left margin. Overlay the tip character instead and let the
decode crest sweep through the wake.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 19:15:01 +08:00
WHCandCursor 6d2e106e26 feat: decode-wave stream glitch — plasma tip, braille wake, caret (0.9.28)
Replace noisy glyph spam with a layered frontier: bright cyan plasma at
the tip, a traveling braille wake that ghosts the real text, a lock flash
as glyphs settle, and a scanning write-head caret.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 19:08:09 +08:00
WHCandCursor 1b0f14533a feat: adaptive drip buffer for stream reveal (0.9.27)
Network tokens fill md_live immediately; the UI reveals md_shown at a
non-linear rate (slow at the tip, accelerates with backlog) so glitch
and text stay readable without lagging far behind a fast model.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 19:05:20 +08:00
WHCandCursor 7d183d5bba fix: park cursor in composer; glitch stream frontier (0.9.26)
leaveok on chrome panes and always redraw input after side/log motion
so the caret no longer sticks on the Seat Deck version line. Live
assistant text briefly scrambles at the tip then settles to plaintext.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 19:02:05 +08:00
WHCandCursor 6e09f79666 feat: Seat Deck sidebar with parliament telemetry and motion (0.9.25)
Replace the OpenCode-style inspector with a seat/fleet/ctx/work telemetry
rail, default-on for wide sessions, driven by 80ms ASCII micro-motion.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 18:50:36 +08:00
WHCandCursor 00288108cd feat: TUI chrome refresh — steel theme, home card, session topbar (0.9.24)
Cooler brand palette, compact CST-agent home with tagline and framed
composer, session topbar (model + chair chip), sidebar off by default.
Screenshot pipeline in tools/tui_shot.py; docs/screenshots for before/after.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 18:27:06 +08:00
WHCandCursor c329db504d perf: accept thread + dial backoff; join/peers.json (0.9.23)
Run inbound peer handling on its own thread so outbound dials cannot starve
:9100. Failed peers cool down exponentially. Also land --join (member seat)
and realm/peers.json seed discovery for Tailscale meshes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 18:16:46 +08:00
WHCandCursor de2eaf87f7 fix: stop parliament accept starvation on outbound dial (0.9.22)
Peer dial used per-step read timeouts so a stuck handshake could block the
single-threaded daemon for tens of seconds; inbound hellos piled up unread
(CLOSE-WAIT). Use a wall-clock dial budget, drain accepts before each dial,
skip self, and only one side initiates idle polls.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 13:19:03 +08:00
WHCandCursor 2f96b050d6 perf: faster builds, leaner daemon, smoother install/run (0.9.20)
Reuse libcstcore.a for tests with -MMD incremental deps; idle-backoff the
parliament seat loop; defer curl init and cache settings; add systemd helper.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 11:41:16 +08:00
WHCandCursor 812e3ee265 feat: parliament seat collaboration loop (0.9.19)
Seats publish via parliament_note, sync pushes on write, member join without
stealing the chair, and worker tools are hard-gated when parliament is active.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 11:29:38 +08:00
WHCandCursor 1605d74f17 fix: sealed peer sync smoke + clearer approval denials (0.9.18)
CLI/Docker assert AEAD sync; status shows sealed channel; headless denials no longer pretend the user refused.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 11:18:04 +08:00
WHCandCursor 5900b53642 feat: seal peer sync with X25519 + XChaCha20-Poly1305 (0.9.17)
Encrypt parliament context sync after Realm mutual auth without pulling in OpenSSL.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 11:16:26 +08:00
WHCandCursor a3e115539e feat: /realm in TUI, daemon instance takeover (0.9.16)
Pidfile records instance+port; login restarts the service when the seat
changes. TUI /realm status|login|stop; prompts discourage filler chatter.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 11:00:34 +08:00
WHCandCursor e610d70c30 fix: interactive Realm password, recoverable worker budget (0.9.15)
Prompt for Realm password when CST_REALM_PASSWORD is unset; map worker
request-gate exhaustion to a clear budget error with partial progress;
hard-skip identical tool failures after 3 tries. Add docs/parliament.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 10:53:36 +08:00
WHCandCursor 9c6257fc96 fix: parliament UX, prompt alignment, execution nudges (0.9.14)
Inject shared parliament context into the agent environment, switch
prompts away from in-process workers when parliament is active, clear
login tickets on chair sync/new-project, and surface realm/chair/service
in /status. Tighten empty-reply handling and prefer-tool principles.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 10:22:31 +08:00
WHCandCursor be8d021480 fix: Alpine parliament Docker build (musl clean rebuild)
Ignore/clean host objects, add linux-headers, and include sys/time.h
so discover builds under musl. Docker chair↔member handshake now passes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-29 10:14:15 +08:00
WHCandCursor e853abac5e feat: parliament daemon, login gate, peer context sync (0.9.13)
Login claims the chair, writes a session ticket, and starts a background
service for discovery plus revision-based context sync. Password Realm
opens require --login (or CST_REALM_PASSWORD); --service/--service-stop
manage the resident agent.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 23:11:23 +08:00
WHCandCursor de9ee28773 feat: password Realm, single-chair login, shared parliament context (0.9.12)
Derive Realm keys from a user password (Argon2id, no private key on disk).
Login claims the sole chair with handover; peers share parliament context;
--new-project clears it. CLI peer/discover helpers and Docker alpine recipe
included; headless empty-answer retries raised for unattended runs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 23:06:51 +08:00
WHCandCursor 08ca613a0e feat: mutual Realm peer handshake over TCP (0.9.11)
Exchange enrolled certificates and Ed25519 challenge signatures so
same-Realm instances can authenticate each other after discovery.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 22:49:33 +08:00
WHCandCursor 5ee82f126d feat: same-Realm peer discovery via UDP and local registry (0.9.10)
Advertise instances with a multicast beacon and a config-dir registry so
same-machine and LAN peers that share a Realm fingerprint can find each
other without TOFU pairing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 22:48:04 +08:00
WHCandCursor dade4072cf feat: add Realm Ed25519 keys and instance enrollment (0.9.9)
Introduce a user Realm keypair (Monocypher) that signs instance
certificates so same-domain peers can later auto-discover and connect.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 22:43:54 +08:00
WHCandCursor acd6f1d998 fix: keep task completion, cancel, pause, and undo honest
Pending earlier-revision jobs no longer skip completion repair; late
cancels preserve real exits; begin clears pause; partial undo keeps
snapshots; permissions and worktree failure paths are hardened.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-28 22:09:47 +08:00
WHC d971cb02b0 feat: add v2 task control and asynchronous isolated workers 2026-10-20 14:46:57 +08:00
WHC 143dba203b feat: add persistent multi-model workers and orchestration budgets 2026-10-19 11:29:26 +08:00
WHC 235652604d feat: add Kimi Code and web APIs 2026-10-19 10:41:00 +08:00
WHC 60047ca5ce the session readers use getline(3), which a Windows CRT does not have
Building the Windows TUI by hand -- the first local build since the CI file
went away -- stopped in src/tools/session_tools.c: `getline` was an implicit
declaration there, which this GCC treats as an error. src/session.c had a
private fgetc() shim for exactly that; the session-list scan is newer and used
the POSIX call directly, and only the Windows build notices.

The shim moves to os_compat as os_getline(): getline(3) itself on Unix, the
fgetc() loop on Windows, one prototype in the header so a caller reads the same
everywhere. src/session.c drops its copy and calls it; the scan does too (its
`ssize_t` becomes `long long`, which Windows has).

make check is green on Linux, the Windows cross build now finishes and
packages (dist/cst-agent-0.9.7-windows-x86_64.zip plus the NSIS setup), and the
static Linux tarball was rebuilt with it.
2026-10-11 21:54:32 +08:00
WHC b7034baec4 build: no CI; the packages are made here
The release files are built on this machine from now on, so the workflow that
did the same steps on a runner -- TinyCC build and unit tests, the static musl
Linux tarballs, the mingw Windows zip, the Gitea release upload -- is gone.
.gitea/ held nothing else.

Everything the workflow called stays and is runnable by hand exactly as it ran
there: scripts/ci_linux_toolchain.sh is what package_linux.sh uses to pick its
musl toolchain, scripts/ci_tcc.sh builds the second compiler the test job used,
scripts/ci_windows_cross.sh and scripts/bootstrap_windows.sh are the mingw
build, and scripts/release.sh is the upload (GITEA_TOKEN=... scripts/release.sh
FILE...).
2026-10-11 21:52:08 +08:00
WHC db8cdd7dd1 the back key: one C++ path, so a dialog closes too
A phone's back key did not close the settings dialog. A window-level QML
Shortcut never sees the key while a modal popup is up, and a desktop run says
so plainly: with the dialog open, Back left backLayer() at "settings", while
invokeMethod(win, "stepBack") closed it on the spot and Escape -- the popup's
own CloseOnEscape -- closed it too. So the key reaches the popup and never
reaches the shortcut map, which is why the drawer (no popup) closed fine and
hid the problem on a phone.

A Keys handler on the window's root item is no help either: nothing reached
it, the focused TextField keeps the key. So the key is read in C++, on the
application object but only for this window's events -- a filter installed on
the window itself was never called once, while the same key closed the drawer
through the shortcut, hence the application object.

With a layer open, the window's own backLayer() names it and stepBack() closes
it, press and release both taken; with nothing open the key is passed on
untouched, which is what leaves the window on a phone and does nothing on a
desktop. Main.qml keeps only those two functions: the Shortcut and
backHasLayer are gone, --uitest installs the same handler main() does, and the
back checks now drive the code the phone runs instead of a shortcut only the
test had.

The last check asks the phone's own question and can end the run without the
closing line -- the activity leaving takes the process with it, and every line
is flushed as it is written.

Desktop: 106 ok, 0 FAIL. Phone: 104 ok, 0 FAIL (two clipboard checks are
skipped on Android), and "after the last back: window visible 0".
2026-10-11 21:44:33 +08:00
WHC 81aeee7d1e uitest: scroll a card into view before clicking it
Open and Share failed on the phone for a reason that was in the harness,
not in the app: the card was where isVisible() says yes, but the transcript
clips it away -- the run's own note says it found contentY at -14 with the
card's row below the visible part -- so the click (and the tap after it)
landed on whatever was at that point instead, and the header stayed empty.

The run now stops the view following its tail (ChatView.stickBottom, which
the replay sets), scrolls the card's row into view with the numbers in a
note, and only then clicks. ChatView gets an objectName for that.

The checks also say what the card hands the engine (the name the store
keeps the file under, from the strip's own model) and what the card itself
answers -- which is what turned "it did nothing" into this answer:

  note the card is out of the viewport (contentY -14, card at 433 of 615):
       scrolling to it
  ok   Open hands the file to the system (opened report.md (test mode);
       cards hold 'img-...png, art-....md', on screen, card says '')
  ok   Share names the file (shared report.md (test mode); card says '')

The whole device run is green now: 97 ok, 0 FAIL (it was 88/17 when this
session's work started, and every one of those was real).
2026-10-11 21:14:54 +08:00
WHC a99f78134a uitest: no clipboard on Android, and say which event got through
The two clipboard-image checks failed on the phone for a reason that is not
a defect: Android's clipboard carries text and URIs, not images -- setImage
does nothing there -- so they are skipped on Android with a note saying so,
instead of reporting a phone that is broken because it is a phone.

Open and Share still fail on the device, and now say how far they got:
neither the synthesized mouse click nor a touch tap leaves a line on the
card's buttons, while the same click reaches the header and the sidebar.
The engine's own path is fine (openArtifact() called directly answers as it
should), and the only way for the header to stay empty is openArtifact()'s
early return for a name it cannot resolve -- so what is left to look at is
the name the card hands over (the model's `rel`) and whether the card is
inside the visible part of the scrolled transcript at that moment, since
QQuickItem::isVisible() does not account for clipping.

Device run after this: 95 ok, 2 FAIL (was 17 before this session's fixes).
2026-10-11 21:05:21 +08:00
WHC 8e5bdd0c9d the header is a Rectangle, and the phone puts it where it is
A Controls ToolBar hands its children to a style-owned contentItem, and on
the NX789J that item sat 35 px below the bar itself: the header was at y 0
h 42 while its own gear was at y 56 -- drawn on top of the sidebar's first
lines, and clicked there too, so the settings dialog could not be opened
from the gear at all (eleven checks that follow it failed with it). A
desktop never showed it: same style (Basic), same numbers, gear at y 21.

A plain Rectangle has no such indirection -- its children are where it is.
Nothing else changed; the background and the border line are the same.

--uitest prints what it sees at that point now (style, window, header,
gear, safe area), which is how this was found:

  note style Basic, window 1280x853, header y 0 h 42, gear centre y 21

Before: gear centre y 56, `FAIL the gear opens the settings dialog`, 17
failures on the device. After: 95 ok, 4 FAIL (Open/Share and the two
clipboard-image checks, which Android has no answer for).
2026-10-11 21:00:11 +08:00
WHC f8030ec201 a device run can be looked at: --shots, and Qt's messages in the log
The settings dialog check failed only on the phone, with no QML error and
nothing to read: this build's Qt messages never reach logcat (verified: a
plain qInfo from the app is not in the buffer). Two ways in, then:

- --uitest installs a message handler that writes Qt's own warnings into
  uitest.log, next to the checks. What the phone did say, it says there.
- --uitest --shots saves window grabs at the interesting points into the
  workspace. A device run cannot be handed CST_APP_SHOT (an app started
  from adb gets no environment), so what a desktop run asks for with an
  environment variable has to be an argument here. main.cpp takes it, so
  `--uitest --workspace DIR --shots` works from adb.

What they said about the phone (NX789J, 1280x853):

  ok ... the gear is reachable, centre 1191,56 of 1280x853
  ok ... and openSettings() opens it when called directly

so the button is there and the handler works: the synthesized click is what
does not open the dialog. The run's own picture of that moment shows why --
the header is drawn over the sidebar, so the click lands on what is drawn
above the gear. On a desktop the same window has the header above the
sidebar and the click lands. Not fixed yet (nor the Open/Share checks on the
device, which are the other two failures that are not about the dialog).

The settings checks now go on either way, so a phone run reports the dialog
instead of eleven lines of "not there".
2026-10-11 20:53:16 +08:00
WHC a34c74e453 the tool card counts, and a device run leaves a log
A tool that takes minutes -- a build, a wide search, a command nobody can
predict -- read as a window that had hung: the card said "running" and
nothing else, forever. The chip now counts from the point the card starts
running (from five seconds on, before that it is noise), which is where a
person looks when a call seems stuck; the status line and the Stop button
were already there. A card that appears already running (a replayed
transcript) starts its clock too.

--uitest: two checks for it (a card four minutes eighteen in says
"running · 4:18"; a finished one stops counting).

And the run now writes uitest.log in its workspace, every line and the
verdict. That is the only way to read a run *on a phone*: stdout reaches
nobody there, an app started from adb cannot be handed CST_APP_SHOT or any
other environment (so the shots a desktop run can take are out), and logcat
does not carry this build's Qt messages at all -- checked: a plain
`qInfo` from the app is not in the buffer.

First device run (NX789J, 105 checks): 88 ok, 17 FAIL, every failure
pre-existing and unrelated to this work -- the settings dialog does not
open from the gear (so nothing in it is reachable: the five dialog checks,
the two provider-form ones and the four clicks that follow), Open/Share
leave no status line on the device, and the clipboard-image checks have no
clipboard. All the md_to_pdf and card checks pass on the phone, including
"black ink on paper (darkest pixel 0)" from 4c69ef3.
2026-10-11 20:41:58 +08:00
WHC 9d7ad9433e android: the arguments arrive as a String[], not an ArrayList
The hook added in da04913 asked for getStringArrayListExtra, but `am
start --esa applicationArguments "--uitest,--workspace,/sdcard/x"`
stores a String[] -- that call answers null for an array, so the window
still started with no arguments at all. Ask for the array first, then
the list (Qt's own loader, putStringArrayListExtra), then the single
comma-separated string.

With this the uitest runs from adb on the NX789J: the fixtures and
selftest.pdf land in --workspace, which is how a device round trip is
possible without touch injection (adb cannot tap this window).
2026-10-11 20:35:41 +08:00
WHC 4c69ef39ab pdf: text that names no colour comes out black, not the palette
A QTextDocument takes its default text colour from the application
palette, and markdown names no colour at all: in a window running a dark
palette -- which is what a phone in dark mode hands Qt -- every plain
paragraph was laid out #eeeeee on a white page. Printed, that is a blank
document; on screen it reads as "the font is oddly faint". The first
PDFs off the phone were exactly that.

- PdfTools: paperDocument() sets the default stylesheet (body colour
  #000000) before setHtml, and writePdf() lays that out. A colour the
  document does name still wins, so an .html handed to the tool keeps its
  own -- checked with a #cc0000 document under both palettes.
- --uitest: draws that document on a white image under a dark palette and
  looks for ink (238 before the fix, 0 after).

Verified on the NX789J: md_to_pdf on the device now writes CID TrueType
NotoSansCJKHK-Regular/-Bold text whose page renders at min=0.
2026-10-11 20:35:41 +08:00
WHC da04913071 android: read the activity's own arguments, since Qt does not deliver them
`am start -n <pkg>/org.qtproject.qt.android.bindings.QtActivity --esa
applicationArguments "--workspace,/sdcard/x"` left the window on its default
workspace on the NX789J (the plain-string extra too), so the extra is read
from the intent directly. That is what makes `cst-agent-app --uitest
--workspace DIR` reachable on a device with no keyboard -- and adb cannot
inject touch into this window either (tap, touchscreen tap, mouse tap and
motionevent all do nothing; key events do work), so it is the only way in.

Everything degrades to "no arguments": an invalid object, a missing method or
a context that is not an activity all come back empty.
2026-10-11 20:22:50 +08:00
WHC 7b2e7a3dbd fix: the window's approval switch never reached the engine
needs_confirm() (src/agent_loop.c) decides whether a tool asks by reading
cfg->confirm_shell / confirm_write / confirm_net; the approval mode only
relaxes those. The CLI fills them in (main.c: parse_confirm), the TUI and the
WebUI do too -- the Qt window never did: AgentCore::open() memset the whole
config and cst_agent_create() keeps what it is given, so every tool ran
without asking while the sidebar said "ask". That is why nothing was ever
asked on the phone (or on a desktop).

- AgentCore: one mapping, applyApproval(), used by open() and setApproval();
  the mode is remembered like the TUI's /approval and read back on start
  (else "ask", the only mode where the window's consent dialog is reached).
- --uitest: two checks that the mode reaches the engine as those flags.

Version 0.9.7 (local).
2026-10-11 20:22:50 +08:00
WHC 4e990c8e35 docs/android.md: what a turn does when nobody answers, and md_to_pdf
The SAF picker is not 'still to come' any more (v0.9.6 did it): the list keeps
the back gesture and the icon glyphs. In its place, what v0.9.7 changed on the
phone -- a consent question refused in the background and bounded in the
foreground, the bounded GUI hop, the phase in the header, and the window's own
md_to_pdf.
2026-10-11 20:04:24 +08:00
WHC dae51e06c7 fix the session-list scan's line count, and pin both new paths with tests
The rewritten cst_agent_store_info() counted a transcript line that ran past a
whole 64 KiB read block a second time, when the newline ending its tail
arrived: a session with one very long line (a tool that read a file back)
reported one message too many -- exactly what the sidebar shows the person.

- src/agent_store.c: the tail's newline ends a line that was already counted.
- tests/test_store.c: test_info_scan() holds the scan down -- a 70 KiB line is
  one line, the first user message comes from the head of the file, and a user
  line past the window is not called the first one.
- tests/test_tools_ext.c: a byte budget stops the session_search walk, says so
  in the result, and never reports a match it did not reach; the same query
  without a budget finds it.

make check and make asan are green.
2026-10-11 20:03:39 +08:00
WHC 042d7f6999 v0.9.7: a turn that cannot hang, a session list that reads nothing twice, and PDFs
- consent: on_permission / on_ask_user wait at most two / five minutes, and in
  the background (a phone off screen, a window that is gone) the answer is
  refused at once with a reason -- a dialog nobody can see blocked the worker
  for good, which is what "the app hangs while the agent works" looked like.
- finishTurn() no longer joins the thread it is called from; the event loop
  reaps the Turn instead.
- AndroidTools: the hop to the GUI thread pings first and gives up after 3 s
  instead of waiting forever on a busy GUI thread.
- the session list stopped reading every transcript: cst_agent_store_info()
  counts newlines in 64 KiB blocks (first_user only within the first 256 KiB),
  and AgentCore::sessions() reuses a row while (mtime, size) is unchanged.
- session_search gets a budget (max_bytes 32 MB, max_seconds 5 s), skips lines
  over 64 KiB, and says when the budget stopped the walk.
- the header shows the phase ("shell: make -j8"), so a long turn no longer
  looks like a stuck one.
- md_to_pdf: the front end's own Qt tool (owner app:qt, every platform):
  markdown or a workspace .md/.html becomes an A4 PDF with 15 mm margins,
  paginated by hand, written into the workspace and delivered as a file card.
  No PrintSupport, so the phone gets it too.
- --uitest runs md_to_pdf through the registry with Chinese text and checks the
  bytes are a PDF; app/CMakeLists.txt no longer drops src/web_md.c from the
  standalone engine build, which the window needs to render markdown.

Version 0.9.7.
2026-10-11 19:26:36 +08:00
WHC 2ffe74b37c desktop: the window installs itself (launcher entry, icon, default.nix)
app/linux/cst-agent-app.{desktop,svg} plus one CMake component ("app") in
app/CMakeLists.txt: `cmake --install --component app` puts the binary in bin/,
the entry in share/applications/ and the icon in hicolor/scalable/apps/. The
entry's file name stays the app_id Qt sets (QGuiApplication::applicationName),
or the launcher draws a generic icon; only the front end installs, the CLI
keeps coming from scripts/bootstrap_linux.sh.

default.nix is that install for nix users: nixpkgs' Qt 6 (qtwayland for a
Wayland session, qtsvg for Breeze's SVG icons), wrapQtAppsHook so the plugins
and QML modules resolve outside a nix-shell and survive nix-collect-garbage,
a filtered src (third_party/ and the build directories are gigabytes this
build never reads), and the version read from include/cst_version.h. doCheck
runs ctest minus test_sandbox, whose Landlock assertion cannot hold inside the
nix build sandbox (the other 40 pass there; make check runs all 41).

Verified: nix-build, nix profile add --file ., then --selftest and --uitest in
a clean environment (no nix-shell, no XDG_* from the session).
2026-10-11 18:44:31 +08:00
WHC 580d533656 v0.9.6: documents, downloads, PDFs and the phone's own files
Four tools and a document writer, plus the storage half of the phone: until
now the picker's answer (a content:// URI) reached the engine as a path, and
it said "cannot read" -- the app had no way to hand a phone's file over.

- md_to_html (new, src/tools/web.c + cst_web_md_document()): markdown in, one
  self-contained HTML document out -- doctype, utf-8, viewport, escaped
  title, a small stylesheet; no script, no external request. It is written
  into the workspace and delivered as a file card (for_user, so it never
  enters a request), the way deliver_file does. The default name comes from
  the source file's stem or, for pasted markdown, from the title with what a
  path cannot carry turned into '-'. It has a preview, so an approval prompt
  says "render x -> y.html" before anything is written.
- download (src/tools/web.c): an http(s) URL into a file in the workspace,
  streamed with a 128 MB ceiling; a redirect chain, the same CA handling and
  the same refusals as web_fetch, but the bytes stay on disk (a half file is
  removed on any failure, including HTTP >= 400). The name comes from the URL
  when the caller names only a directory.
- read_pdf (src/tools/fs_tools.c): the text layer and page count of a PDF,
  which read_file refuses because the bytes are binary -- and a PDF is
  exactly what arrives from a phone. Same extractor an attachment uses, its
  own 4 MiB input ceiling, 256 KiB of text by default with a note when cut.
- session_search (new, src/tools/session_tools.c): every stored session --
  this one and all the ones before it -- searched for a substring, reporting
  the session's id, message count and title, then the matching lines with
  their number and role. Case-insensitive by default, capped at 20 matches.
- device_info (same file, perm_name "device"): version, system/arch/CPU
  count, workspace, sandbox, model and permissions, config/temp/program
  directories, pid -- so the model stops guessing where it runs.
- Tools 3 and 4 come with tests/test_tools_ext.c (also in both build files):
  the document is a whole page, a script tag and a javascript: link do not
  survive, a hostile title stays text and cannot write outside the workspace,
  the preview names the file, read_pdf/download refuse what they must (no
  network in a test), session_search finds a fixture session and says so when
  nothing matches, device_info names the workspace. README's tool table grew
  five rows.

- Android storage: the picker (SAF) answers with a content:// URI, and the
  engine opens files by path, so the app now copies what it points at into
  its own directory first -- app/android/src/xyz/whcself/cstagent/
  AttachFile.java, called through a new app::materializeAttachment()
  (ToolProvider.h: one API for every platform; a desktop strips file:// and
  refuses content:// with a reason). The copy needs no permission at all: the
  grant travels with the URI.
- "All files access" (MANAGE_EXTERNAL_STORAGE) is the other half: it is what
  lets the engine read a path in shared storage that a task names rather than
  picks. Android has no runtime dialog for it, so main.cpp opens the one
  Settings screen once (AttachFile.java's requestAllFilesAccess, with the
  all-apps screen as a fallback) and never insists: without it everything
  works, one picked file at a time. On the NX789J the screen came up, the
  app-op reads back "allow", and a relaunch no longer asks.
- Composer.qml hands a content:// URI through untouched (it is not a path,
  and decoding it would corrupt it); AgentCore::attachFile() turns whatever
  the picker gave into a path first.

Desktop: build clean, make check 36/36 (the new test included, also under
ASan), --uitest all green, --selftest and cst-app-selftest OK.
The APK builds and installs (86 MB, release-signed) with the new Java class
in its dex.

Version 0.9.6.
2026-10-11 18:33:41 +08:00
WHC 08008a5a4c v0.9.5: a session owns a directory, and the store hands out uuids
A session's transcript lived beside its attachments, both named after an id
that was a timestamp. Two things fell out of that: on a workspace two
machines share, a timestamp is not an id (the same second on both is enough
to hand out the same name), and "delete this conversation" had to chase the
jsonl, the title and a directory in a shared attachments/ tree, guessing what
belonged to whom. A phone's app data has nobody to tidy it by hand.

Now one session is one directory:

  <workspace>/.agent/sessions/
    <uuid>/session.jsonl     the transcript
    <uuid>/title             the window title, one line
    <uuid>/attachments/      images and delivered files (img-…, art-…)
    <uuid>/sub-1.jsonl       an agent role's own transcript
    CURRENT                  the id the next run opens

- src/agent_store.c: new_id() makes a uuid v4 (16 bytes from /dev/urandom,
  clock+pid only as a fallback), dir_for_id/flat_for_id/nested_for_id/
  shape_of_id decide which shape an id has, so every reader takes either.
  An id is a directory name, so it is checked as one (no "/", no "..",
  nothing starting with ".", not CURRENT or attachments). path_for_id() keeps
  the shape a session already has and makes the directory for a new id, so
  nothing had to change in the callers. store_list() lists the directories
  that hold a transcript plus the flat files, newest first. delete() is one
  rm_tree of the session's directory, plus the old shared attachments/<id>
  and CURRENT when it pointed there.
- cst_agent_store_migrate() (--migrate-store, or plain cst-agent for a
  session): moves a flat store in — <id>.jsonl to <id>/session.jsonl,
  <id>.title in, the shared attachments/<id>/ to <id>/attachments/. rename
  first, copy when the filesystem says no, and counts what it moved, so a
  second run is a no-op that says "nothing to move". On the real workspace:
  6 sessions moved, the shared directory gone, --list-sessions reads them
  back unchanged, a second run moved 0.
- cst_agent_store_id_of_path(): the id of a session path. This is the bug the
  whole change had been hiding: cst_agent_session_id() took the id from the
  file name, and the new file name is session.jsonl -- every session would
  have called itself "session" (the sidebar, CURRENT, undo, an agent role's
  directory, and the metrics file, which now also goes through the helper).
- src/attach.c: a session that owns its directory keeps its payloads inside
  it (<id>/attachments/); only the flat layout needs the shared directory and
  the id stem. Attachments behave exactly as before otherwise.
- tests/test_store.c: uuid shape and uniqueness, a small buffer refused, a
  new session's path and directory, write/list/info/title read back, the
  payload landing in <id>/attachments/, delete taking the directory with it;
  then an old-layout store: readable before the move, moved byte for byte,
  its payload readable through the new path, listed once, a second migration
  a no-op, deletion covering the migrated payload. Also in the Makefile and
  CMakeLists test lists.
- app/src/ui/UiTest.cpp: the image directory check wanted "/attachments/",
  which the new layout has no trailing slash for.

Desktop: build clean, make check 35/35, --uitest all green, --selftest and
cst-app-selftest OK, and --selftest opens a session of the migrated workspace.

Version 0.9.5.
2026-10-11 17:39:00 +08:00
WHC 7299f39245 v0.9.4: a turn keeps running when the app is not on screen
A phone freezes an app it cannot see, and the front end had nothing holding
it: a turn that took minutes -- a long shell command, a provider that thinks,
a build -- stopped quietly the moment the user pressed home, and the answer
was only there if the window happened to come back. The engine's own /attach
flow made this visible: an APK turn survived, a pocket did not.

- app/android/src/xyz/whcself/cstagent/CstService.java: a foreground service
  started with an explicit component (nothing outside the app can reach it,
  no intent filter) for as long as a turn runs. Being a foreground service
  makes the process one Android neither freezes nor kills; a partial wake
  lock (30 min ceiling) keeps the CPU awake with the screen off. Its ongoing
  notification says what is running -- "CST-agent is working" plus the first
  line of the prompt -- on the same cst-agent channel the notification_send
  tool uses, and taps back into the app. A phone that refuses the foreground
  start (Android 12+ does, from the background) loses the protection, not the
  turn: the failure is caught and the service stops itself.
- Manifest: FOREGROUND_SERVICE, FOREGROUND_SERVICE_DATA_SYNC (API 34+ wants
  the typed one) and WAKE_LOCK, plus the <service> with
  foregroundServiceType="dataSync". The first build of this failed with
  "Expected '>', but got ' '" -- a manifest comment cannot contain "--".
- Front end: app::holdForeground(text) / app::releaseForeground() in
  ToolProvider.h, one call for every platform -- Android starts and stops the
  service through JNI, a desktop has nothing to hold (its window is the app).
  AgentCore::send() calls it when the turn starts (firstLine() of the prompt
  is what the notification says) and finishTurn() releases it, so the
  notification exists exactly as long as the work does. CST_APP_NO_SERVICE=1
  keeps a test run from touching the phone.
- On the NX789J, with the app sent home and the screen off: the service read
  back isForeground=true with its notification, the wake lock cst-agent:turn
  was taken at the start of the turn and dropped at its end, run_shell
  "sleep 60" finished with the screen dark, and the service was gone after.

Desktop: build clean, --uitest all green, --selftest and cst-app-selftest OK
(the hold is a no-op there, so nothing about the Qt side changed).

Version 0.9.4.
2026-10-11 16:51:39 +08:00
WHC d4f65b17cf v0.9.3: notification_send posts on the phone, and Java helpers have a home
The tool was the last Android one that never worked on the NX789J: it would
fail with "Notification.Builder.build() failed: no exception detail" and the
shade stayed empty. The detail was the whole story -- Qt clears a Java
exception before C++ gets to look at it, so a build() that throws and a
build() that returns null look exactly the same from here, and the only way
to catch the difference is to let Java catch it.

- app/android/src/xyz/whcself/cstagent/Notifier.java: the builder, the
  PendingIntent, build() and notify() in one try/catch that returns "" or
  the throwable's class and message. androiddeployqt copies app/android/ with
  the rest of the package source and Qt's Gradle template puts its src/ on
  java.srcDirs, so a class dropped there needs no build-script change (the
  first one is this; the foreground service of the next step goes there too).
- AndroidTools.cpp keeps what it can see from C++ (the context, the channel
  on API 26+, the small icon looked up by name, the launch intent) and calls
  Notifier.post(); the result line now carries the Java failure when there is
  one. The javaErrorText() helper of the previous attempt is gone: reading a
  pending exception from C++ is exactly what does not work here.
- On the device: the tool answers "notification sent", the system holds the
  record (channel cst-agent, importance 3, the framework small icon, the tap
  intent back into the app). Earlier attempts in the same session are in the
  transcript next to it: four failures from the old path, then this one.

Docs: docs/android.md gets the Java-helper mechanism (and why the tool has to
be wrapped in Java at all) and no longer lists notification_send as missing;
app/README.md's platform table and the Android note say where a Java class
goes.

Version 0.9.3.
2026-10-11 16:40:51 +08:00
WHC 60cbce3c5c v0.9.2: files the agent delivers come back as cards, and the saved key is used
A turn could write a report and say "here it is" -- and there was nothing to
open. The engine had no way to hand a finished file to the person, and the
front end had no way to show one. This release adds the whole path: a tool to
deliver, a store that keeps the copy, a card in the transcript, and open /
share through the platform. It also fixes two bugs the phone exposed while it
was being tried there.

deliver_file (engine)
- cst_attachment_t.for_user marks a copy that belongs to the person, not to
  the model; cst_attach_import_artifact() stores any bytes up to
  CST_ARTIFACT_MAX_BYTES (64 MB) with the mime taken from the name's
  extension. Attachments are unchanged (content decides the type, 16 MB).
- It is a read-policy engine tool: path resolves inside the workspace, a
  directory is refused, title (optional) is the display name.
- The wire is what keeps it out of the conversation: parts_add_attachments()
  skips for_user entries without even a "not sent" note, and a turn that only
  delivered a file no longer invents an empty user message. tests/test_artifacts.c
  pins that down end to end, including a session-file round trip.

The card (Qt front end)
- filesText() replaced imagesText(): one line per file -- kind, stored name,
  name, origin, mime, bytes, pixels -- decoded by ImageStrip.qml. A for_user
  entry is a file card (extension badge, name, type and size, Open, Share);
  a picture stays a thumbnail and opens on tap. Both shapes are read, so
  sessions written by an older version still draw.
- AgentCore::openArtifact() / shareArtifact() accept a name only when one of
  the session's own messages carries it, and hand the display name to the
  platform layer for the status line.
- Android opens through the app's FileProvider (content:// + ACTION_VIEW,
  resolveActivity() checked, read permission granted) and shares through the
  share sheet; a desktop uses QDesktopServices / the clipboard.
  CST_APP_NO_OPEN=1 makes --uitest run without opening anything.

Two bugs the device found
- The session writer had its own copy of the attachment JSON and forgot
  `for_user`: the file survived the file, but came back as an attachment --
  no card, and put back in front of the model. It now uses the one writer
  (cst_attachments_to_json()), with the round trip covered by a test.
- engineToolImages() looked the card up with findTool(), which only finds a
  card that is still running -- and the engine reports the result *before*
  the files, so the card was already complete and the files were dropped.
  A live turn therefore showed no card while a replay of the same session
  did. It now matches on the call id.
- A tap that could not work said nothing: ImageStrip.qml's errorText was
  never drawn. The card now carries the reason ("no app on the phone opens
  report.md").

Also
- The saved provider key reaches the engine on every start: AgentCore::open()
  was handed an empty key and the engine never resolves credentials itself
  (only the CLI did), so every start asked for it again. It resolves the same
  order main.c uses (saved key, provider environment, CST_API_KEY) and wipes
  the copy it made; --selftest checks it against a scratch credentials file.
- Android: notification_send reads the Java exception text into its result,
  and keeps failing on the NX789J -- the build() returns null and Qt clears
  the exception before C++ can read it, so the call has to be wrapped in Java
  next (docs/android.md says so).
- --uitest grew the delivered-file case (card, Open, Share, a forged name
  refused), the live order of result-then-files, and the startup row count.

Version 0.9.2.
2026-10-11 16:34:47 +08:00