Feature/tensoragent ios - #200
Conversation
… Darwin-aware managed layer - TensorSharp.GGML.Native: TENSORSHARP_GGML_NATIVE_ENABLE_METAL / _IOS CMake options, Metal-only code gated on TSG_GGML_USE_METAL (the MPSGraph conv path was gated on __APPLE__, which the CPU-only simulator slice does not satisfy), build-ios.sh cross-builds device (Metal, embedded shader sources) + simulator (CPU) slices into build-ios/GgmlOps.xcframework and validates both slices. - Backends.GGML: Metal is an Apple-platform backend (iOS/iPadOS/Mac Catalyst), the DllImport resolver maps GgmlOps to the main program image on iOS (static link), GgmlMemoryPool uses page-aligned mmap on every Darwin platform. - Runtime/Models: no whole-file prefault on iOS (jetsam), madvise hints on iOS. - csproj: the native desktop build/copy targets are skipped for the ios TFM. - eng/fetch-python-ios.sh fetches the CPython iOS distribution TensorAgent embeds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…sation store, settings, Python bundle staging - Catalog: Gemma 4 E2B/E4B (dense), Qwen3.5 9B, Qwen3.8 27B (2-bit, experimental), Gemma 4 26B-A4B and Qwen3.6 35B-A3B MoE (16 GB devices), Qwen-Image-Edit 2511 - sizes and SHA-256 read from the Hugging Face tree API, gated by device memory tier. - ResumableDownloader: Range resumption of the kept .part, streaming SHA-256 (prefix re-hashed on resume), retries with backoff, structured progress; ModelStore manages one folder per entry with companion files beside the weights. - ConversationStore persists the Web UI's own message shape so a session can be handed back to the page unchanged; SettingsStore for the permission toggles. - scripts/prepare-python.sh stages CPython 3.13 + numpy/Pillow (BeeWare wheels) + pure packages into the .fwork/framework layout iOS can load. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ming, multipart uploads, per-launch token) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… can run in-process Package D of TensorAgent (iOS). Every launch in TensorSharp.AgentHost — the shell tool, background jobs, package installs, syntax checks, API probes and skill scripts — now goes through one seam, IShellBackend, instead of reaching ConfinedProcess/SpawnedProcess directly. ProcessShellBackend is the desktop implementation and the default everywhere: it writes the wrapper script, builds the ConfinedLaunch exactly as ShellRunner.RunIn did, launches under the detected OS sandbox, and rewrites the wrapper-script path/line numbers on the way out. Desktop results are unchanged; the existing shell, skill-script and drift-guard suites pass as before. What a host that cannot start processes (iOS) plugs into: - ShellLaunch (command line + ShellSession, or argv) carries every host decision; IShellJob mirrors ConfinedJob so background jobs are held the same way. - ShellRunner takes an IShellBackend plus optional ISkillSandbox/ShellProgram/ ISyntaxVerifier/IApiProbe/IPackageInstaller (interfaces extracted from SyntaxCheck, ApiProbe and PackageInstaller; the concrete classes stay the defaults). CanRun, NetworkConfinementGuaranteed, UnavailableReason and the result's "Not confined on this host" line are all judged from the backend's sandbox, so an in-process backend with honest capabilities runs under Sandbox=Required without --code-exec-unconfined. - ShellSession.Load()/Save()/MarkEnvironmentReset() persist cwd and env from the host side, parsing the shapes `export -p` emits and writing through the same PosixEnvFilter the wrapper pipes through, atomically (rename replaces a planted symlink rather than following it). CurrentDirectory/CurrentDirectoryLabel/ TakeEnvironmentWasReset work identically for both backends. - ShellProgram.InProcess(); CodeEnvironment.Configure()/Reset() override the PATH probes (AvailableTools, interpreter resolution, Python version, WhichCache, CodeDiagnostics install prefix) so the declaration describes an embedded runtime. - SkillScriptRunnerOptions.Backend routes RunConfined through the seam as an argv launch; the second copy of the launch sequence is gone. - iOS guards: PosixSpawn.IsSupported false on iOS/tvOS; SpawnedProcess.TryStart answers "this platform cannot start programs"; SkillPathGuard.PathComparison is Ordinal on iOS; ShellSession's Darwin open(2) flags apply on iOS/Catalyst; the libSystem.Native fstat import falls back to libc fstat with the pinned Darwin stat64 layout; InProcessSandbox (Name "in-process") is the iOS candidate in SkillSandboxFactory (writes/home/process tree confined, network NOT claimed). Tests: FakeShellBackendTests + SkillScriptRunnerBackendTests (a recording fake backend driving ShellRunner and SkillScriptRunner), ShellSessionStateSafetyTests additions for Load/Save/parsers/reset marker/symlink write/Darwin fstat layout. docs/agent_skills.md gains a "Launch backends" subsection. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… builds and runs for iOS Package C of TensorAgent. Every native-library media call - OpenCvSharp video frames and MP4 writing, the ffmpeg tier, Magick.NET image decode/encode/resize - now goes through TensorSharp.Models.Media.MediaCodecs, whose default managed provider covers PNG/JPEG/BMP/GIF decode (StbImageSharp plus the existing hand-written PNG decoder), PNG encode, bilinear/Lanczos resize and WAV/MP3/OGG audio, and whose desktop provider (compiled only off iOS, registered by a module initializer) keeps today's behaviour byte-for-byte. Video decode/encode without a provider refuse with a message naming what is missing rather than falling back silently. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rp.Server Package B of TensorAgent. The chat stack the Web UI drives - ModelService, the generation pipeline, sessions, the skills request plan and streaming loop, every request parser, the SSE frame builders, sampling defaults, upload policy - moves verbatim into a new net10.0 class library that does not reference ASP.NET Core, so the iOS app, the CLI and the server all run ONE implementation. - Namespaces are unchanged (TensorSharp.Server.*), so the 48 coupled test files and the server's own call sites compile untouched. - BackendCatalog splits: the pure descriptor table, Canonicalize and ToBackendValue move; the Cuda/MLX/GGML availability probes stay behind in the server as BackendCatalogProbes. ModelLifecycleService takes an injectable tensor-parallel group factory instead of referencing TensorSharp.Distributed. - New WebUiChatService and SkillsService hold what WebUiAdapter and SkillsAdapter did minus the transport: preflight failures throw WebUiRequestRejectedException and the chat/image-edit/video streams are IAsyncEnumerable<object> of the exact frame objects, so the adapters shrink to HttpContext plumbing and the wire contract stays byte-identical. - ModelService gains UnloadModel() and a scheduler-config override so a phone can free the model on a memory warning and size the KV pool itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
iOS forbids Process.Start, so the agent host's ProcessShellBackend has
nothing to launch there. This is the replacement: a shell that parses and
runs the command itself, inside the app.
ShellSyntax/ShellExec cover the language a coding model actually writes --
pipelines, && || ; and subshells, redirections including heredocs, globs,
parameter expansion, command substitution, arithmetic, for/while/if/case,
and functions. ShellTest implements test/[/[[ ]] with the POSIX 1/2/3
argument rules. The builtin table covers the coreutils a model reaches for
(ls cat grep sed awk find sort cut tr head tail wc diff xargs tar unzip and
the rest), with awk being a real interpreter rather than a pattern match.
Every path a command touches is resolved through ConfinedPaths, so a write
outside WorkRoot, a symlink pointing out of it, and an archive member with
a ../ in its name are all refused rather than followed. Network commands
and script interpreters check ExecutionPolicy first, and say which switch
the user has to flip rather than failing silently.
63 tests pass. Six bugs the tests caught and this commit fixes:
- `[ -n x ]` was a usage error: -n was missing from the unary operator set
- `head -20` and `tail -5` ignored the count and printed ten lines
- `sed -i 's/a/b/' f` ate its own script, because -i was parsed as taking
a separate argument; GNU never does, and BSD's `-i ''` is now handled
- awk read `toupper($1)` as a variable next to a group, because the call
test looked at the space before the name instead of before the paren
- awk's printf appended a newline that printf must not add
- unzip and tar let a `../` member escape the extraction directory; the
sandbox still contained it, but both now refuse it outright
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The IShellBackend seam already existed; this is the implementation below it
for a platform with no Process.Start. A ShellLaunch goes to InProcessShell
instead of to a wrapper script under Seatbelt, and comes back as the same
ConfinedResult the host reads everywhere else, so the ledger, the artifact
capture and every sentence the model sees are unchanged.
The launch's permissions become an ExecutionPolicy: WriteDirectory is the
work root, ReadOnlyDirectory and ReadablePaths are the readable roots, and
AllowNetwork carries straight across. ExecutionPolicy gains WritablePaths
so the host can still grant one exact file outside the work root without
granting its directory, and AllowLoopbackPort so the egress-proxy shape
survives the seam.
The sandbox reports what it actually enforces -- writes, network and home
reads confined by path resolution rather than by the kernel, and a bounded
process tree only because there are no processes. What it cannot do is
preempt: a builtin already inside a long call runs past its deadline, and
the doc comment says so rather than implying a kill.
Three defects the seam tests caught:
- a per-call workdir was persisted, so `workdir` moved the whole
conversation instead of one command
- the saved directory used the resolved spelling (/private/var on macOS)
which the session validates as outside its own workspace and discards,
so `cd` never persisted; it is now mapped back to the caller's spelling
- the saved exports were written but never read back, so `export` and
`source .venv/bin/activate` did nothing on the next call
78 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The page in the WebView is TensorSharp.Server's index.html byte for byte,
so the API under it has to answer the same paths with the same payloads
and the same event frames. WebUiRoutes binds all of them onto the shared
WebUiChatService and SkillsService: chat, models, sessions, uploads,
image edit, video generation and the skills management surface. What
changed is only the transport, HttpListener instead of minimal APIs,
because iOS has no ASP.NET Core runtime pack.
MapAgent adds what only a phone needs, under its own /api/agent prefix so
the shared surface stays exactly shared: the built-in catalog with each
entry's install state, a resumable download as an event stream, the saved
conversations, and the sandbox switches.
Two contract bugs fixed:
- the event-stream response applied its headers before pulling a frame,
so a refused chat request (no model loaded, unknown session) arrived
as an empty 200 instead of a 400 with the reason. The first frame is
now pulled before any header goes out, the way the desktop adapter
does it, and the chat service's rejection is translated into the
transport's own status-carrying exception.
- the catalog serialized its family and kind enums as numbers, which
the UI cannot read; they go out as names.
93 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
AgentAppHost is what Program.cs is for the desktop server: it builds the whole object graph from a directory and a settings file. The model service and its sessions, the skills registry, the code runner over the in-process shell backend, and the loopback server with the Web UI in front of it. It lives in the platform-neutral project on purpose. An iOS app cannot be unit-tested from a terminal, so the wiring is only ever checked if it can be started, driven over real HTTP and torn down on a development machine, which is what AgentAppHostTests does. Model weights go under the cache root and conversations under the data root, because a 6 GB file that can be downloaded again must not be pushed into the user's iCloud backup while a transcript, which cannot be recovered any other way, must. ConversationRecorder solves the problem the Web UI creates by holding its history only in the page: on a phone the app is killed constantly, so the transcript is written host-side instead. The engine session and the saved conversation are different things with different lifetimes -- a resumed chat always gets a fresh session, because the KV cache did not survive -- so /api/sessions binds the two and hands the saved messages back for the page to re-render. Metal is the default backend and the first one offered; the CPU entry stays for the simulator, which has no usable GPU. 107 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The MAUI head now hosts the real thing instead of a canned stub. What is left in the iOS project is only what cannot be tested off-device: which directory the system gives for backed-up data versus re-fetchable files, how much memory the device has, and where the bundle put the Web UI. CPython is linked as a dynamic framework with its standard library staged into the bundle, chosen per slice, so the interpreter's P/Invokes resolve through the main program handle the same way GgmlOps does. Verified in the simulator. The app launches, the loopback server binds, GgmlOps resolves from the main program image, and the Web UI creates a session through the real route: TensorAgent loopback server listening on http://127.0.0.1:51086 engine probe: TSGgml_CanInitializeBackend(Cpu)=True, (Metal)=False; TSGgml_* resolved from the main program image Created session via /api/sessions: 0bd64ef68245... Metal is correctly absent in the simulator slice and the probe says so rather than pretending. Driving /api from the Mac returns the catalog with all eight entries and their install state, the models payload with Metal offered first, and the conversation the page just started. Two inconsistencies the run exposed: the conversation summary serialized PascalCase while every other payload is camelCase, and skill scripts were left off by a build-time default instead of following the user's own code execution switch. 107 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A skill on a phone is a different proposition from a skill on a
workstation. There is no pip at run time, no browser to drive, no child
process to spawn, and the only interpreter is the CPython staged into the
bundle. A skill whose instructions are excellent but whose scripts import
something that is not there fails on the user's first attempt, which is
worse than not offering it.
scripts/verify-skills.py decides by inspection rather than by optimism.
For each skill it parses every script, resolves each import against the
staged standard library, the staged site packages and the skill's own
files, and refuses anything reaching for a capability iOS does not have.
Twelve of nineteen pass and are bundled; skills/verdicts.json records
every verdict with its reason.
The seven that do not, and what blocks each:
docx, pptx, xlsx lxml and defusedxml are not in the bundled runtime,
and their validators shell out to LibreOffice
pdf pdfplumber is missing and pdf2image shells out to
poppler, which cannot be bundled
skill-creator subprocess and webbrowser
webapp-testing playwright needs a browser engine
mcp-builder an MCP server needs a process and a socket
Two flaws in the checker itself, found by reading its own output: it
tested availability before unavailability, so subprocess and webbrowser
passed because they ARE in the standard library -- importable is not
usable -- and it only recognised a sibling package with an __init__.py,
missing the implicit namespace packages these skills actually use.
Verified in the simulator: the running app reports "12 skills" and lists
all twelve through /api/skills.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three pages the Web UI cannot have, because they are about a device rather than a server: the model catalog with per-entry size, licence and install state and a resumable download; the saved chats; and the sandbox switches, written as what they control rather than as feature names. The chat page gains the row a browser cannot offer -- photo, camera, video, file and dictation. Attachments upload through the same /api/upload the paperclip uses and are handed to the page's own attachment list, so one picked natively and one picked in the page are the same thing by the time a message is sent. Dictation goes onto Apple's recogniser directly rather than through a toolkit package, and asks for on-device recognition: a chat app that runs its model locally must not ship the user's microphone to a server. The companion script was written but never served. index.html is TensorSharp.Server's byte for byte and must stay that way, so it is now embedded in the assembly and appended as one script tag at request time. That is what makes session resume, native attachments and dictated text work at all -- and what lets the app replace the empty state's "start TensorSharp.Server with --model" with instructions that make sense on a phone. Verified in the simulator: the model list renders all eight entries with their sizes and licences, and the chat's empty state now points at the Models page. TENSORAGENT_START_PAGE lets the harness screenshot a page other than the chat, since simctl cannot tap. Also includes the embedded CPython runtime's own corrections, found by its author testing rather than reading: PyConfig carried a field the shipping 3.13 header does not have, a layout sentinel expected the Python-config default instead of the isolated one, and the wheel extractor wrote a member before refusing the archive. 207 tests pass, 12 skipped (the live-interpreter tests, which need TENSORAGENT_PYTHON_ROOT). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The iOS media provider, over ImageIO/CoreGraphics/AVFoundation, so a
multi-modal model can read what a phone actually produces. HEIC is the
reason it exists: it is the default camera format and the managed decoder
cannot read it at all. Orientation goes through the shared ExifOrientation
so all three providers agree byte for byte, and the two pieces most likely
to hide a wrong constant -- premultiplied-alpha recovery and the display
matrix -- live outside the iOS guard so a desktop test can execute them.
The embedded CPython and JavaScriptCore engines are now discovered by the
host rather than passed in as null, so the shell can dispatch python3 and
node. Both report their own availability with a reason, and the engine
line says what is missing before a model tries to use it.
A startup self-test runs representative commands through the real backend
and logs what each did, because the failures that matter here are not
compile errors: an interpreter that links but cannot find its standard
library produces an app that starts perfectly and fails on first use. On
the simulator all eleven checks pass:
shell, files, awk; python 3.13.14 with its stdlib, numpy and Pillow;
node and node -p; a write outside the workspace refused; the network
refused.
Four defects it found, none of which a build would have:
- the 77 compiled extension modules never reached the bundle. iOS
refuses to dlopen a Mach-O outside a signed framework, which is why
each .so ships as its own framework with a .fwork placeholder where
it used to be; the placeholders were staged and the frameworks were
not, so every `import binascii` failed and most of the standard
library went with it. They are embedded, not linked: Python resolves
them by dlopen, and force-loading 76 dylibs would cost every launch.
- the audit hook banned ctypes.dlopen outright. On this platform that
does not stop an attacker, it stops `import numpy`. The rule is now
about which library: inside the app's own bundle is code that shipped
signed with the app; everything else is refused as before, on the
resolved path so a symlink cannot walk out. dlsym and call_function
stay refused, so a handle cannot be used to reach anything.
- dlopen(NULL) was refused with it. That returns a handle to the image
already running and loads nothing; numpy and Pillow both ask for one
while probing. Allowed for that reason and no other.
- `node -p` printed nothing, because it was mapped to -e and the
completion value was discarded -- a command that looks to a model
like it succeeded and produced no output.
Also narrows two project assertions that used Assert.Single on the whole
csproj and broke as soon as the head bundled more than one thing.
TensorAgent: 219 pass, 12 skipped. Repo: 3055 pass, 1 pre-existing
load-sensitive benchmark that passes in isolation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t found
An end-to-end suite that loads a real model and drives the real API, so
the one question the hermetic tests cannot answer gets answered: does a
person typing into this app get a useful conversation back.
Measured with Gemma 4 E4B Q8_0 on CPU, four turns in one session:
turn 1 prompt 2812 reused 0 ( 0.0%)
turn 2 prompt 2932 reused 2908 (99.2%)
turn 3 prompt 3019 reused 2996 (99.2%)
turn 4 prompt 3105 reused 3082 (99.3%)
Only the new message and the previous answer are processed each turn, and
the model recalls facts from every earlier turn. Starting a new chat drops
reuse to zero; rewriting an earlier turn invalidates from the point the
histories diverge and the model then answers from the rewritten history,
not the old one.
Three real defects the suite found, none of which any unit test would:
- Shutting down released the engine before it stopped serving. A request
in flight is usually inside the model, so freeing it first unmapped
weights that native compute threads were still reading, and the
process died with a segmentation fault in an unrelated-looking kernel.
Stopping a generation partway hit this every time. The server is now
closed first and waits for in-flight requests to actually finish.
- A client that walked away never stopped the work being done for it.
HttpListener has no disconnect event, so a failed write is how it is
learned -- and it has to be learned, or the Stop button aborts the
fetch while the model generates into nothing for the rest of its
budget, holding the session and draining the battery.
- An answer was only written down when the NEXT request carried it in
its history, so a user who asked a question, read the answer and
switched away lost exactly the answer they were reading. The turn is
now closed when it ends, reassembled from the same frames the page
reads, and an aborted turn is saved too because the partial answer is
what the user is looking at.
Verified across an app restart, which is the phone's real multi-turn
story: the transcript survives, the resumed chat re-prefills once (reuse
0, correctly, since the KV cache did not survive), the model still knows
what it was told before the restart, and the next turn is back to 99.5%
reuse.
223 pass, 21 skipped without weights or a live interpreter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
verify-sim.sh still asserted the demo stub's behaviour: a fixed
max-tokens, a canned SSE round trip, an engine route that moved. It now
checks what the app actually promises -- Metal offered first, a session
that binds a conversation, a catalog covering both families and all three
architectures, the sandbox defaulting to code-on/network-off, the
companion script served, and every startup self-test line.
Two things it caught in the process:
- The page was being read into a string and written back, which drops a
byte-order mark and normalises the encoding. The served file was
therefore no longer the Server's, which defeats the whole reason
there is no second copy of index.html to keep in step. The script tag
is now spliced into the bytes.
- The script presented the launch token as a bearer header the server
does not accept. It uses the cookie the WebView uses; adding a header
purely for a test script would widen the surface for a convenience.
Also portable to the bash 3.2 macOS ships, and the byte-identity property
is now pinned by a unit test rather than only on device.
Every simulator check passes: the page, the token gate, the engine line
(python 3.13.14, JavaScriptCore, 12 skills), the catalog, the sandbox
defaults, eleven self-test checks, and the media probe's HEIC decode,
EXIF orientation, MP4 round trip and audio read.
224 pass, 21 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The top-level README gains a highlight, DEVELOPMENT.md gains the project structure, and TensorAgent/README.md is rewritten from the spike notes it still carried into a description of the app that now exists: what it does, how to build and run it, how it differs from the desktop and why, which skills are bundled and what blocks the rest, and the measured KV cache reuse across a four-turn conversation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five settings were written to disk and shown in Settings while nothing read
them. A switch that appears to control something and controls nothing is
worse than no switch, so each is now either honoured or gone:
- "Show reasoning by default" and the default skill selection are
properties of the page's own controls, so the companion script applies
them at load. A resumed conversation overrides them afterwards, which
is the right precedence: what the user last did in THIS chat beats
what they chose as a default.
- "Keep the screen awake" is held for exactly the stretch the model is
working. The page had no event for that, so the script wraps its send
and reports both edges; on iOS the screen sleeping suspends the app
and stops a generation partway.
- "Download over cellular" is enforced before a five-to-ten gigabyte
download starts, and says which setting to change.
- "Ask before running" is removed. Enforcing it means holding a tool
call open for a user decision mid-stream, which is real work; leaving
the switch there implying it happens is not honest.
Two more things the simulator run exposed:
- Every launch left an empty chat behind, because the page creates a
session on load and a session binds a conversation. Twenty launches,
twenty "Chat Sep 2, 07:38" rows pushing the real ones off the screen.
A new session now reuses the untouched conversation instead of adding
another, and an empty one is not listed at all.
- The page was told Metal was available everywhere. The simulator's
slice of the engine has no Metal, so the default backend did not
exist and the user was one tap from a load that fails. The app now
offers what its probe found, and the simulator check asserts that
property rather than assuming Metal.
Also adds an event-stream heartbeat. A disconnect is only visible when a
write fails, and during a long prefill there is nothing to write for
minutes -- so Stop appeared to work while the model kept going. A comment
line every five seconds fixes that; the page's reader ignores any line
that is not `data:`.
226 pass, 21 skipped. All simulator checks pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pt example The Metal line said it is always the default; it is the default where the build has it, and the simulator's slice does not. The skills checker's usage example pointed at one machine's checkout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sing it Stopping the server ends the HTTP requests, which was not enough. A generation those requests started is still inside a graph compute on the engine's own threads -- cancellation is delivered between tokens, and a token can take a while on a CPU. Releasing the model at that moment unmaps weights those threads are reading and the process dies with a segmentation fault in whichever kernel happened to be running. The engine reports what it is processing, so shutdown now waits for that to reach zero before the model is released, capped so a wedged request cannot stop the app from closing. Found by the aborted-generation test regressing when the event-stream heartbeat was added: the heartbeat notices a disconnect during a long prefill, which made the teardown happen earlier and exposed the race the earlier ordering fix had only made less likely. Also fixes the heartbeat itself, which would have spun on cancellation -- a cancelled Task.Delay completes immediately, so looping on it wrote keep-alives as fast as the socket allowed. It now waits on the pending frame with its own timeout instead. 226 pass, 21 skipped; the aborted-generation test passes in 26 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Loading a model on ggml_metal and letting the process end aborted it:
ggml-metal-device.m:962: GGML_ASSERT([rsets->data count] == 0) failed
ggml-metal's device is a C++ static whose destructor asserts that every
residency set has been handed back, and it runs from __cxa_finalize, after
the last managed code. Two independent leaks kept it dirty:
- ModelBase.Dispose released the weight and KV wrappers but never the
graph scratch -- the reuse gallocr and the per-graph compute buffer,
769 MiB and 256 MiB here, each carrying an MTLResidencySet. The two
diffusion pipelines release that scratch by hand between stages
precisely because nothing else does; a model that only ever loaded and
unloaded had no such call anywhere. It is also close to a gigabyte a
phone does not get back when the user switches models.
- The app never called GgmlBasicOps.Shutdown() at exit, which the Server
and CLI both do, so quitting with a model still loaded aborted on the
weight buffers instead.
A third bug surfaced on the way: TSGgml_ReleaseReuseComputeBuffers freed
the scratch without draining deferred GPU work, so under async compute the
last graph's command buffer was still reading what had just been freed.
ExternalProjects/ggml is a build-time clone that resets on every build, so
none of this could be fixed upstream even if it belonged there.
Two review corrections on top of the diagnosis:
- The justifying comment claimed every other caller pairs
ClearOffloadableState with releasing the scratch, citing the two
pipelines. They do not -- they release the scratch alone. Corrected to
what is true.
- The process-exit net was installed from AgentAppHost's constructor,
which handed it to every test that builds a host. A net that catches
an undisposed engine also hides one, and that is how this leak went
unnoticed. The app installs it deliberately at startup; tests get
none, so a leak still aborts loudly.
Verified: the original repro passes and exits cleanly twice running, and
in 8 s rather than 81 s, because the tests now load on Metal instead of
CPU. MetalLifetimeTests measures device allocation directly and fails with
1023 MiB left over when the fix is removed. TensorAgent 226 pass;
InferenceWeb 3054 pass with two timing benchmarks that pass in isolation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The shell has 102 builtins and had no git. A coding agent wants one: to see what it changed, to put a file back after a bad edit, and to checkpoint before trying something. LibGit2Sharp would mean cross-compiling native libgit2 for iOS, so this is pure C# against the on-disk format instead. init, add, commit, status, diff, log, show, checkout --, restore, rm, mv, plus rev-parse, cat-file, hash-object, ls-files, branch, tag and config, which share the same machinery. Reflogs are written, so real git reflog works on the result. Packfiles are read -- .idx v1 and v2, both delta encodings, the copy/insert stream, with base memoisation. That was the decision worth making deliberately: git self-packs after a few thousand objects and any directory copied from elsewhere arrives packed, so a loose-only reader would report an EMPTY HISTORY rather than an error. Writing packs is not implemented; real git reads loose objects natively. Real git validated it. `git fsck --strict` is clean on a repository this wrote, `git log` and `git ls-files --stage` agree on every object name, so the SHA-1 naming, tree sort order, index checksum and commit encoding are all right. The reverse direction works too, and after `git gc --aggressive --prune=now` left zero loose objects the builtin still walked the whole history and printed delta-compressed blobs byte-identically. One bug found while writing the symlink test, and it was not git's: ConfinedPaths.Resolve deliberately follows symlinks, which is right for confinement and wrong for `git add link.txt` -- it committed the target's bytes under the link's name, losing the link and silently inlining a file from outside the work tree. GitFileGate.ResolveLeaf confines by the parent directory and leaves the last component unfollowed, which is git's lstat. Verified that this is not an escape: the leaf is only ever lstat'd, deleted, or deleted-then-replaced, and every read that follows a link is guarded by a LinkTarget check first. Network subcommands are refused by name with the reason, exit 128. So are merge, rebase, reset and the rest that rewrite the working tree: a known subcommand says it is unimplemented, a typo still gets git's own "'stauts' is not a git command". 41 tests, all executing, none skipped; the four real-git cross-checks run the system binary and skip with a message where it is absent. Suite: 267 pass, 23 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…right
Two capabilities the app could not do, built from what the bundled
interpreter actually has rather than from what the published skills want.
Research: fetch a page as readable text, search through a configured
endpoint or a documented HTML fallback, and crawl several pages into a
dossier -- stdlib only, no API key, and every path degrading with a
message when the network switch is off. Every network path was verified
against the real internet: curl, wget, urllib, node's fetch, the host
allow-list in all three, and a PyPI wheel install that the next Python run
imports out of the session's package root.
Documents: PDF via reportlab, workbooks via openpyxl, and .docx/.pptx
written as OOXML by hand with zipfile and xml.etree, because lxml has no
iOS wheel on PyPI or the BeeWare index and no recipe in mobile-forge --
so python-docx and python-pptx cannot be bundled at all. Verified by
reading the output back with the real lxml-backed python-pptx and
python-docx: a deck comes back as 3 slides at the right 16:9 EMU with a
real table, and a document as Title/Subtitle/Heading 2/Normal/List
Paragraph with a real table. No Office application has opened them.
Four defects fixed, three of which produced wrong numbers silently:
- A "=B2*C2" string inside a spec's `rows` IS a formula -- openpyxl
writes it to the file as one -- but only the separate `formulas` array
was computed and cached. So the cell went in with no cached value, the
run reported success, and analyze_table.py then read every revenue as
0. Reproduced (sum(Revenue)=0 across three regions), fixed, and now
reports 25.0/12.0/10.5 totalling 47.5.
- IF did not short-circuit. The evaluator computes as it parses, so
=IF(B2=0,0,C2/B2) -- the canonical divide-by-zero guard, the reason
anyone writes IF -- evaluated the division first and refused the whole
formula. An untaken branch's failure is now carried, not raised.
- AVERAGE/MEDIAN/MIN/MAX/STDEV/PRODUCT over a range with no numbers
returned a hardcoded 0.0, which was then cached into the sheet as a
real number no reader could tell from a measured one. They refuse now,
which puts the cell in uncomputable_formulas where it belongs.
- A spec with invented keys (`sections` instead of `blocks`) parsed,
validated and produced a file containing only its title, exit 0. All
four writers now name the unknown keys and list the accepted ones.
Also: the research skill mapped every PermissionError to "turn Network
on", including the allow-list refusal -- sending a model to a switch that
was already on. The two refusals now give opposite, correct advice.
And the allow-list itself was unreachable: three runtimes checked
ExecutionPolicy.NetworkHosts and nothing in the app could set it, so it
was always empty and IsHostAllowed short-circuited to true. It is a
setting now, threaded to the policy, with a test that fails if it stops
biting. WheelInstaller's fourth, differently-worded refusal for the same
condition is unified onto the shared sentence.
verdicts.json is regenerated by its own tool rather than hand-edited:
14 of 14 bundled skills pass.
305 pass, 45 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ed them Two catalog entries advertised a device tier they could never run on. Gemma 4 E2B claimed 6 GB and needs about 6.6 GB resident; E4B UD-Q4_K_XL claimed 8 GB and needs about 6.8. A 6 GB iPhone would have been shown a five gigabyte download and then been killed opening it -- the worst thing this catalog can do, because the user pays for it twice, once in data and once in the wait. The budget test only ever checked the 12 GB tier, so neither was caught. It now applies the same rule to every entry against the tier it advertises, and a second test walks every real device size and asserts that whatever ForDevice hands back fits the device that asked. Both entries move to 12 GB. The README's model table was written from memory rather than from the catalog and disagreed with it on three rows and every download size. It is now generated from the same numbers, with the Hugging Face repository each file comes from. 306 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…an edit costs Answering two questions with measurements rather than estimates. **Does image editing work end to end?** Yes, and it always did once the three silent bugs were fixed: /uploads is routed, the vision projector is named so the loader's scan can find it, and the Lightning LoRA reaches TS_QWEN_IMAGE_LORA. A real edit runs on Metal, the LoRA engages (720 pairs, 4 steps, cfg 1), and the result decodes to the size it claims. **Does it fit a phone?** Not proven, and the honest numbers are worse than the catalog assumed. A traced edit shows 1.82 GB through VAE encode, 2.99 GB during text encode -- so the text encoder IS freed as advertised -- then 23.2 GB the instant the DiT loads, held for the whole denoise. The tier moves from 12 GB to 16. Two engine changes came out of it. CPU offload was gated to GgmlCuda, so on Metal it could never engage whatever the environment said -- and Metal is what a phone runs. It is enabled there now: the path works, the edit completes, and the pipeline reports streaming as designed. The residency budget was sized from what the device says is free (recommendedMaxWorkingSetSize minus currentAllocatedSize). On a phone those are not the numbers that matter: iOS kills an app at its jetsam allowance, a fraction of what the GPU calls free, so sizing from `free` means deciding the weights fit right up until the process dies. TS_QWEN_IMAGE_RESIDENT_MB caps it, and TensorAgent publishes an output-area cap per device alongside the companion paths -- halving the area took a denoise from 111 s to 50 s. The new test reports peak Metal allocation and asserts the tier ONLY when it measured the catalog's own quantisation; with any other DiT it prints the number and skips, saying which file it had and which it needed. The figure is currentAllocatedSize, an upper bound on what jetsam counts, so under budget proves life and over budget is a warning -- and the test says so rather than implying more. What is still unproven: the catalog's own Q2_K DiT has not been measured (it is 5.8 GB smaller than the file on this machine), and nothing has run on a physical device. 306 pass; QwenImage engine tests 21 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One edit with the Q2_K DiT -- the smallest quantisation unsloth publishes at 7.47 GB -- allocates 16.0 GB on Metal. A 16 GB iPhone grants an app about 11.3 GB before jetsam. Halving the output area saves ~1.2 GB and the CPU-offload path does not lower allocation on unified memory, so neither closes a 4.7 GB gap. The entry now sits at a 24 GB tier, which no phone reports, so ForDevice never offers it; the Notes carry the measurement rather than the previous "loaded one at a time to fit", which was true about the loading and wrong about the fitting. The memory test keeps both claims: the advertised tier has to hold, and 17.5 GB is a regression ceiling on the number itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two reports from the phone.
THE MICROPHONE. Holding the talk button said there was no permission. The
Info.plist keys were all present and the request order was right; two other
things were wrong. Both TaskCompletionSources completed WITHOUT
RunContinuationsAsynchronously, so everything after the await -- including
starting the AVAudioSession -- continued on whichever thread iOS delivered the
callback on rather than the main one. And a permission the user has already
refused cannot be asked for again: iOS shows its prompt once and afterwards
returns the stored "no" silently, so the button failed forever with nothing to
do about it. Denied is now distinguished from not-yet-asked, named for what it
is ("Settings › TensorAgent › Microphone"), and carried to the page with a
marker so the refusal comes with an Open Settings button instead of
instructions.
THE UX THAT DID NOT CHANGE. It had, but only half: the page was the new one
while a NATIVE row above it still carried the status and Chats / Models /
Settings chips. Two rows of chrome stacked on a 6.9-inch screen is the one
thing the redesign exists to avoid, and it is why the app still looked like the
old one. The native row is gone. Navigation lives in the page now -- a single ☰
in the same row as the activity dot, the model and New, opening a sheet with
Chats, Models, Settings and About -- and the app routes it through OnPageEvent,
accepting only the routes the shell actually registers so a page cannot
navigate somewhere there is no page for.
Verified on the phone: it serves the new page (16753 bytes) with menu, bar,
busy, model, new, plus, hold, lang, think and voice present, the four routes in
the nav sheet, and zero mentions of TensorSharp.
313 passed, 57 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"The research skill never successfully works." It works from a laptop and cannot work from a device, and the asymmetry is why every test of it passed. _ssl.framework ships in the app bundle, so Python CAN speak TLS on iOS. What is not there is a certificate authority bundle. OpenSSL looks for a trust store at a path compiled into it, which does not exist inside an app bundle, so every https:// fails CERTIFICATE_VERIFY_FAILED -- while a development machine's interpreter quietly uses the system store and succeeds. The skill's own search endpoint is https. So is nearly everything else it would fetch. certifi is now staged beside the other packages for both slices (240216 bytes of authorities), and the bootstrap points OpenSSL at it before anything imports ssl. Finding where it was being lost took three wrong guesses, and the answer is worth recording: the bootstrap set SSL_CERT_FILE on the real environment, and every run then REPLACED os.environ with a plain dict built from the request (PythonBootstrap, "a plain dict, not the process environment") -- deliberately, so a script cannot mutate the app's environment or leak it to a child. The variable was set and then dropped a moment later. The CA path is now carried across that swap as the one exception, because it is a property of the interpreter rather than of the caller, and a caller who forgot it would get a verification failure on every request with nothing pointing at the cause. A caller that sets it explicitly still wins. TheInterpreterHasACertificateStoreAndCanVerifyTls asserts the bundle is found AND that a default SSL context actually loads authorities from it, so a staged file that OpenSSL ignores fails the test rather than passing it. A drift guard did its job on the way: DocumentSkillTests refused the new staging entry until certifi was registered in ModuleOfPackage. 313 passed, 58 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ible trace
Five things, all reported from the phone.
VOICE HANDS BACK. Releasing the talk button now leaves voice mode, puts the
transcription in the text box and focuses it with the caret at the end.
Speaking is how a message STARTS; reading it back, fixing a word and pressing
send is how it finishes, and staying in voice mode hid the very text the user
needed to check.
A VISIBLE TRACE. The desktop page shows a live activity block and DELETES it
when the step finishes, which suits a wide screen someone is watching. On a
phone the useful thing is the opposite: a short line per step that STAYS, so a
user who looked away can see it read a skill, ran a script and edited a file.
Live status while running, one finished line with its duration after -- the
tool vocabulary is taken from the Server's own activityLabel so the words match
what the desktop says.
SKILLS CAN BE ADDED AND REMOVED. POST /api/skills/from-url installs from a
link, which is how a skill is actually shared -- someone sends a URL and a
phone has nowhere convenient to put a file first. The body may name one archive
or a plain-text list of them, one per line, because a collection is the other
way skills travel; a list reports both halves, so eight of ten landing says so
and names the two that did not. Everything downloaded goes through the same
Install path as an upload, so the path guard on every entry, the
decompressed-size budget and the entry cap all still apply -- a URL is a
different way to arrive, not a different level of trust. Size is capped while
reading rather than trusting Content-Length, which the server chooses.
READING A SKILL. Tapping one opens a sheet with the whole description, the
switch and Remove. The list still clamps to two lines on purpose; what changed
is that the rest is now one tap away instead of unreachable.
claude-api is gone from the built-in list.
One trap worth recording: deleting a skill from the source tree does NOT remove
it from the app bundle. BundleResource copies incrementally and never deletes,
so the build kept shipping claude-api and the simulator kept listing 14 skills
after the directory was gone. The stale output has to be cleared.
Verified on BOTH targets. Physical iPhone 17 Pro Max:
engine: sh (in-process), python 3.13.14, node (JavaScriptCore),
installs enabled · code execution on · network on · 13 skills
skills 13, claude-api absent, from-url rejects a non-http scheme with 400,
and the page carries the skill, add-skill, language, hold-to-talk and menu
surfaces.
Simulator: identical, on a clean install.
313 passed, 58 skipped.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removing a skill only ever worked for one it had installed itself. Most of this
app's skills ship INSIDE the bundle, which is read-only and is rewritten by
every install of the app, so SkillRegistry.Remove refused them outright:
"'brand-guidelines' was discovered under <bundle>/skills and is not managed by
TensorSharp; remove it from that directory instead." There is no "that
directory" on a phone. Deleting a built-in skill was impossible, and any
delete that had appeared to work would have been undone by the next launch.
The registry now keeps a record of what the user removed. Removing an INSTALLED
skill still deletes its directory; removing one that lives in a read-only root
writes the id down instead, and discovery skips it from then on -- including
after the app is reinstalled and the bundle puts the files back. The record
lives in the data root, so it survives updates the way conversations do.
Installing a skill CLEARS its entry first, because installing is the user
asking for it back, and without that a reinstall would succeed while the skill
stayed invisible.
Configured only by the app. The desktop server passes no record file and keeps
the old refusal, which is right for a process whose skill directory an operator
manages with a shell.
Verified on BOTH targets, including the part that matters -- that it does not
come back:
simulator: 13 skills -> delete brand-guidelines -> 12 -> RELAUNCH -> 12
iPhone: 13 skills -> delete brand-guidelines -> 12 -> RELAUNCH -> 12,
engine line agrees ("... network on · 12 skills"),
and reinstalling the same zip restores it -> 13.
314 passed in TensorAgent, 405 in InferenceWeb's skill suite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…that stops copying Two things the engine was leaving on the table on Apple silicon, both of them conservatism rather than a missing capability. A block-quantized K/V cache was admitted by the fused whole-model graphs only on CUDA, so asking for q8_0 anywhere else silently dropped to the per-op path: decode 3.7 tok/s against 75.6 on f16, with the engine saying so itself. Nothing was missing. ggml-metal instantiates flash_attn_ext_q8_0 and _q4_0 for exactly the same dk/dv set as f16, the cache write has kernel_cpy_f32_q8_0, and the native graphs were already dtype-generic. Measured after, ggml_metal, 3 interleaved reps: Qwen3.6-35B-A3B decodes 75.5 tok/s at q8_0 against 76.0 at f16, and a token costs 22.4 KiB instead of 41.8. A two-needle recall test at 7,490 tokens returns both planted values at f16, q8_0 and q4_0. Gemma 4 does not get it. Its fused paths are fine, but its sliding-window layers use a CIRCULAR cache whose three managed helpers are float-only, and the 26B-A4B MoE reaches them on an ordinary prompt: enabling it crashed with "Requires a Float32 tensor, but found Q8_0" out of CopyToCacheCircular the moment a user typed. It now declines at load and uses f16, which is what the guard was for before it was bypassed. Separately, load-time fusion copies. GGUF stores tensors alphabetically, so the pairs worth fusing are almost never adjacent and TryCreateConcatenatedView falls through to a memcpy into fresh anonymous memory - 1.53 GiB on Qwen3.5-9B Q8_0, 6.65 GiB on gpt-oss-20b, against llama.cpp's zero, because it never synthesizes a weight the file does not contain. On iOS that is charged against jetsam, so the copy is now declined where a separate-weights path is verified to exist (SeparateQkv, SupportsSplitGateUpFfn, the recurrent pack's four sources). Peak footprint 2,137 MB -> 570 MB, bit-identical output. gpt-oss keeps the copy: its per-expert gate_up has no split path and there is nothing to fall back to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A Release device build shipped with no engine at all, and said so only if you
looked: ggmlCpuAvailable=false, ggmlMetalAvailable=false, "Unsupported
architecture: gemma4. Registered: .", and no model would load. Debug was fine,
which is why this only ever broke the shipping configuration.
Two steps fight each other. The link exports what the resolver needs, because
NativeLibrary.GetMainProgramHandle() reaches the statically linked engine by
dlsym:
xcrun clang++ ... -force_load libGgmlOpsMerged.a
-Wl,-exported_symbol,_TSGgml_*
and Release then runs, separately,
xcrun strip -i -s <obj>/mtouch-symbols.list <executable>
where `strip -s file` keeps ONLY the symbols named in that file. Nothing put
TSGgml_ in it, so they were stripped straight back out after being correctly
exported.
GgmlExportedSymbols.targets names all 248 of them as ReferenceNativeSymbol, which
is the SDK's own way to say "keep this": the items are written into that very list
and also become -u link roots. No leading underscore, because the SDK emits
-u%(Identity) and the linker adds its own. 587 exports, 248 TSGgml, 78 MB, and the
app comes up on Metal.
Not MtouchNoSymbolStrip=true. That also restores the exports but keeps every
symbol, the executable goes 78 MB to 190 MB, and the app dies at launch with
SIGABRT inside load_aot_module before any managed code runs.
One trap worth knowing: iOS link results are cached under obj/macos/<Config> and
deleting the .app does not invalidate them. The same csproj produced either a
78 MB or a 190 MB binary depending on leftover state, which looks exactly like a
nondeterministic crash. Clean obj before comparing two link configurations.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
TensorAgent was killed by jetsam running Qwen3.5 9B on a 12 GB iPhone, and the cause was that its context budget never reached the engine. Every catalog entry carries a ContextLength, and its only reader was the JSON the page renders. AppSettings.ContextLength, documented as the user's override, was read by nothing at all. KvCacheDtypeConfig.ConfigureFromEnvironment() was called by the server and the CLI but never by the MAUI head. So the engine used the GGUF's own number - 262,144 for Qwen3.5 - and a pasted document grew the KV cache until the process died. Measured on ggml_metal with a 24,696-token prompt: 5,679 MB of physical footprint unbounded, 943 MB with the budget applied. EngineMemoryPolicy applies it in UseModel, which is the single funnel every load arrives at, and before RepointHostedModel because the engine reads these when the model is constructed. Setting MAX_CONTEXT also switches the cache from growing on demand to reserving its window once, which is the behaviour to want next to a jetsam limit: a budget that is wrong is discovered at load, where it can be reported, instead of arriving as geometric growth mid-conversation. The catalog changes with it. Qwen entries take a q8_0 cache now that the fused graphs read one, which is 22.4 KiB/token against f16's 41.8 at decode parity, and their windows grow to 32768 - or 16384 for the dense 27B, whose 64 attention layers cost 68 KiB/token even quantized. Gemma stays on f16 and 8192; it refuses a block-quantized cache. Both mixture-of-experts entries drop to the 12 GB tier: the test helper that gated them at 16 assumed "Metal wires the mmap'd weights, so resident memory is roughly the GGUF", and measurement says otherwise - an 11,272 MB model peaks at 1,211 MB resident / 1,090 MB footprint. That estimate is now two numbers, because they answer to different limits: anonymous memory to the jetsam budget, weights to the device's RAM, past which every token faults from flash. Gemma 4 12B is new. The reply-length limit reaches 256K by doubling rungs rather than a thousand steps of 256, and says plainly that the context is the real ceiling, because it is: the generation reserve is trimmed to what the window leaves after the prompt. os_proc_available_memory() is wired in too. The app never asked how much memory it had left, and a jetsam kill writes no stack and no message of its own, so the last thing the app said about its own budget is the only evidence there is. On the device it reports 6.40 GB, against the 8.5 GB the tests assume. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two silences a user hits and cannot explain.
The first: "here is your PDF", a link, and "error: not found" when you tap it. The
runner hands the page /api/code/artifacts/{runId}/{path} and the app mapped no
such route - TensorSharp.Server has had the endpoint all along, the phone did not -
so every generated document, PDF and PPTX alike, was unreachable. MapCodeArtifacts
serves them the same defensive way the server does: confinement re-checked by the
store rather than trusted from the route, always an attachment, never a content
type a WebView might execute.
That is only half of it, because a WebView cannot save an attachment either: with
no download delegate, tapping one does nothing at all. The link is intercepted and
handed to the share sheet instead, which is what "save it to my phone" means -
Save to Files, Mail, AirDrop. Resolving through the store rather than fetching the
URL keeps the confinement check in one place and skips a loopback round trip for a
file already on disk.
The second: a turn that ran your code, streamed plenty doing it, and then ended
without writing an answer. There was already a message for a turn cut off before
producing anything, but it keys on tokenCount, which counts every streamed piece -
thinking and tool calls included. So that turn had a healthy tokenCount, truncated
was false, and the page showed the step that ran followed by nothing at all.
Whether ANSWER text was ever produced is now tracked separately, and its absence
is said out loud.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…DF, and a switch for all of it The research skill required a URL, which is the one thing a person asking a research question does not have. It discovers sources itself now - discover.py finds them, research.py reads and extracts, analyze.py summarises - and search.py, which only fetched what you already named, is gone. What a page says is treated as what a stranger wrote: quoted, weighed, cited, never followed as instruction. "Turn this photo into a PDF" is one command rather than a program someone has to write: make_pdf.py --image, each picture a page of its own, scaled to fit with its proportions kept. Skills can be switched off entirely. When the toggle is off no skill is offered, discovered or run. Three things that had to be fixed for any of it to work end to end. skills_run went through a child process, which is fatal on iOS, so ICodeRunner now carries the in-process Backend the interpreter already uses. tempfile.tempdir leaked between runs, so a second session's workbook was written into the first session's scratch directory. And the live-Python test classes raced each other over one process-wide interpreter: they share a collection now, and one DllImport resolver serves both CPython and JavaScriptCore instead of the two racing for the single slot, where the loser could not bind its imports for the life of the process. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ves the screen Reopening a saved chat lost everything it carried. Images, audio, video and files were rendered from a payload the history never kept, so switching back showed the words and nothing else. The page stores each attachment with the message now - url, name, kind - and rebuilds the img/audio/video/link on the way back in, and the whole history is sent rather than a trimmed copy. ChatMessage carries AttachmentPaths and AttachmentNames so the server side can resolve them, and every attachment is staged for the interpreter rather than only the ones it guessed at. A DOM harness runs the real page in JavaScriptCore, so the behaviours that broke are held by tests that fail against the old page. A generation belongs to the app, not to the HTTP request that started it. On a phone the reader goes away constantly - another screen, another app, a display that dimmed - and each of those used to throw away an answer halfway through. ChatTurnManager owns the turn, the page re-attaches to it, and a background assertion keeps it alive long enough to finish. Model downloads got the same treatment: they outlive the screen that started them, resume from their .part, and the Models list finds the job again when it reopens. Uploads get names that cannot collide or escape, multipart parsing is its own tested unit, and the shell reports a missing command as a missing command instead of a stack trace. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # TensorSharp.AgentHost/Skills/SkillSandbox.cs # TensorSharp.Backends.GGML/GgmlNative.cs
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e8f546718c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return; | ||
| try | ||
| { | ||
| IReadOnlyList<string> resumed = _downloads.ResumeInterrupted(ModelCatalog.Find); |
There was a problem hiding this comment.
Honor the cellular policy before auto-resuming downloads
When a Wi-Fi download fails while the app is suspended and the user returns on a cellular-only connection, this foreground callback restarts the multi-gigabyte transfer unconditionally. The normal Models-page entry points check AllowCellularDownloads and DeviceState.IsOnCellularOnly(), but this path has neither the settings store nor the connectivity check, so the default-off cellular policy can be bypassed and incur substantial data usage.
Useful? React with 👍 / 👎.
| CatalogFileRole.Projector, | ||
| CatalogFileRole.Lora, | ||
| CatalogFileRole.TextEncoder, | ||
| CatalogFileRole.Vae, | ||
| CatalogFileRole.VisionProjector, |
There was a problem hiding this comment.
Include draft heads in optional downloads
When DownloadOptionalFiles is enabled for either Gemma catalog entry, ModelStore.DownloadAsync includes optional files only when their role appears in this array, but CatalogFileRole.Draft is absent. Consequently the setting documented as downloading optional projector/draft files never fetches either cataloged MTP GGUF, while the job still reports completion.
Useful? React with 👍 / 👎.
| // notice is inert. | ||
| Options.RepointSamplingDefaults(SamplingDefaultsFor(model)); | ||
|
|
||
| Options.RepointHostedModel(weights, projector); |
There was a problem hiding this comment.
Configure an installed draft head before loading the model
When a catalog draft GGUF is present, UseModel resolves only the weights and projector and never publishes the draft path or enables speculation before calling ModelService.LoadModel. ModelLifecycleService discovers separate heads solely through TS_SPEC_DRAFT_MODEL/its legacy equivalent, so the advertised optional Gemma draft remains unused even after it is downloaded or otherwise placed in the model directory.
Useful? React with 👍 / 👎.
main split TensorSharp.Server into a library plus a TensorSharp.Server.Host
application; this branch had split it into a host-neutral TensorSharp.Chat
plus TensorSharp.Server. The two splits are compatible — Chat < Server <
Server.Host — so the resolutions keep this branch's layering and apply main's
visibility widening on top:
BackendCatalog keep Chat's delegate-based catalog (the probes stay in
the Server, which alone may link Cuda/MLX); take main's
public class + Canonicalize, which Server.Host's
StartupBanner calls.
UploadContentPolicy keep Chat's ASP.NET-free content-type table. main made
the class public only for BuildStaticFileOptions, which
now lives in the Server's UploadStaticFiles, so the
policy goes back to internal and UploadStaticFiles is
the public one.
SkillsAdapter, using-block only; keep this branch's TensorSharp.Chat
WebUiAdapter imports. main's internal -> public merged cleanly.
InferenceWeb.Tests reference Chat, Server and Server.Host.
Continuing main's split, the three Server-side composition types Program.cs and
StartupBanner now reach across an assembly boundary are public:
BackendCatalogProbes, UploadStaticFiles and DistributedTensorParallel.
main's rename left the runnable project behind in several places; the server's
entry point is TensorSharp.Server.Host and TensorSharp.Server no longer has a
Main, so `dotnet run --project TensorSharp.Server` now errors. Repointed the
release-binaries and test-matrix workflows, the engine_comparison harness
(which launches the server by DLL path), and 35 doc/website commands.
Verified: solution builds; InferenceWeb.Tests 3252 passed / 0 failed;
TensorAgent.Tests 523 passed / 0 failed; the merged server boots, serves the
Web UI from its new home and answers the Ollama and OpenAI endpoints.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
eng/verify-packages.ps1 compares each packed nuspec's internal dependencies
against a hard-coded list, and two real dependencies were missing from it:
TensorSharp.Runtime.Logging added when logging was factored into its own
assembly (PR #196), missing on main ever since
TensorSharp.Chat added by the host-neutral chat split
So the gate in publish-nuget.yml failed for both TensorSharp.Server and
TensorSharp.Cli. Worse than the gate failing: neither dependency was in the
publish set at all, so a released TensorSharp.Server would have carried two
references that do not exist on nuget.org.
Both projects — and TensorSharp.Server.Host, also packable and also unlisted —
were authored with a package Description and PackageTags, and nothing in the
repo uses the script's EmbeddedAssemblies mechanism, so they ship as their own
packages rather than being folded into their consumers.
Dependency sets are the ones actually packed, not the ones inferred from the
csproj graph. Verified by running the gate: 13 packages verified, exit 0, and
every internal dependency is itself published (AdvUtils.dll stays embedded in
TensorSharp.Tensors).
Also drops a stale reference to a README package table that no longer exists.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Engine comparison — TensorSharp vs llama.cpp (PR smoke)No report artifact was produced — the benchmark failed before generating results (see the workflow logs). |
No description provided.