feat(local-model): the launch refusal hook, and an honest name for a rebuilt log line

- `LocalModelService.launchRefusal(endpointId)`: what a session launch asks
  before it starts on the managed endpoint. It answers the error that
  refuses the launch (the runtime child is not alive, another program holds
  the port, the runtime keeps stopping, the model's digest changed) or
  nothing. New error code `runtime-not-running` with its sentence, for a
  runtime the person stopped: only a refused session start says it.
- The fake runtime's MacBook log line is rebuilt from the two values the
  measurement notes quote, so it is `INFERENCE_COMPUTE_LINE_MACBOOK` now.
  Only the Mac mini line is a verbatim capture and keeps `REAL_`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-11-06 20:23:58 +01:00
1 parent 771072c36b
commit 84c1017487
4 files changed
+24 -7

No files matched your search

+11
View File
@@ -84,6 +84,7 @@ import type {
LocalModelConflict,
LocalModelDeveloperTools,
LocalModelErrorCode,
LocalModelInstallError,
LocalModelInstallRequest,
LocalModelInstallState,
LocalModelInstallStep,
@@ -458,6 +459,16 @@ export interface LocalModelServiceDeps {
export interface LocalModelService {
/** The `install` object of the GET. Never throws. */
view(): Promise<LocalModelInstallView>;
/**
* Whether a session may be started on the saved endpoint `endpointId` right now. `undefined`:
* yes, which is also the answer for every endpoint that is not this feature's. Otherwise the
* error that refuses the launch, in this order: `runtime-not-running` (the runtime child
* Codeman started is not alive, so whatever answers on the port is not ours),
* `port-in-use`, `runtime-stopped`, `model-changed` (the digest is read again and is not the
* stored one). The session launch path asks this before it starts a session with the
* managed endpoint, and `boot()` asks it once. Never throws.
*/
launchRefusal(endpointId: string): Promise<LocalModelInstallError | undefined>;
/** What the GET passes to `recommend()`: the stored measured total and the runtime download that is still needed. */
recommendOptions(): Promise<RecommendOptions>;
/** `POST /api/local-model/install`. Answers once the job is STARTED; the job runs on in the server. */
+6
View File
@@ -116,6 +116,8 @@ export const LOCAL_MODEL_INSTALL_STEPS: readonly LocalModelInstallStep[] = [
* | `runtime-unpack-failed` | `/usr/bin/tar` failed |
* | `runtime-start-failed` | the runtime was started and never answered `/api/version` |
* | `runtime-stopped` | installed: the runtime crashed and was restarted 3 times, then given up |
* | `runtime-not-running` | installed: the runtime is not running (the person stopped it). Only a |
* | | refused session start says it; the view shows no error for a stopped runtime |
* | `port-in-use` | another program listens on the stored port, or no port of the span is free |
* | `disk-full` | a write failed for lack of space |
* | `pull-failed` | the pull still ends on an error line or a cut stream after its retries |
@@ -137,6 +139,7 @@ export type LocalModelErrorCode =
| 'runtime-unpack-failed'
| 'runtime-start-failed'
| 'runtime-stopped'
| 'runtime-not-running'
| 'port-in-use'
| 'disk-full'
| 'pull-failed'
@@ -218,6 +221,7 @@ export const LOCAL_MODEL_INSTALL_LABELS = {
errRuntimeUnpackFailed: 'The runtime could not be unpacked on this Mac.',
errRuntimeStartFailed: 'The runtime did not start on this Mac.',
errRuntimeStopped: 'The runtime keeps stopping, so Codeman no longer restarts it.',
errRuntimeNotRunning: 'The model on this Mac is not running. Start it first.',
errPortInUse: 'Port {port} is in use by another program.',
errDiskFull: 'The disk is full. Free up space, then try again.',
errPullFailed: 'The download keeps stopping. The network may be damaging data. Try again or use another network.',
@@ -259,6 +263,7 @@ export const LOCAL_MODEL_ERROR_LABELS: Record<LocalModelErrorCode, LocalModelIns
'runtime-unpack-failed': LOCAL_MODEL_INSTALL_LABELS.errRuntimeUnpackFailed,
'runtime-start-failed': LOCAL_MODEL_INSTALL_LABELS.errRuntimeStartFailed,
'runtime-stopped': LOCAL_MODEL_INSTALL_LABELS.errRuntimeStopped,
'runtime-not-running': LOCAL_MODEL_INSTALL_LABELS.errRuntimeNotRunning,
'port-in-use': LOCAL_MODEL_INSTALL_LABELS.errPortInUse,
'disk-full': LOCAL_MODEL_INSTALL_LABELS.errDiskFull,
'pull-failed': LOCAL_MODEL_INSTALL_LABELS.errPullFailed,
@@ -282,6 +287,7 @@ export const LOCAL_MODEL_ERROR_NEXT: Record<LocalModelErrorCode, LocalModelNextS
'runtime-unpack-failed': 'retry',
'runtime-start-failed': 'retry',
'runtime-stopped': 'start',
'runtime-not-running': 'start',
'port-in-use': 'other-port',
'disk-full': 'retry',
'pull-failed': 'retry',
+5 -5
View File
@@ -67,12 +67,12 @@ export const REAL_INFERENCE_COMPUTE_LINE_MAC_MINI =
'time=2026-11-06T13:21:49.885+01:00 level=INFO source=types.go:32 msg="inference compute" id=0 filter_id=0 library=Metal compute=0.0 name=MTL0 description="Apple M4" libdirs="" driver=0.0 pci_id="" type=iGPU total="11.8 GiB" available="11.8 GiB"';
/**
* The same line as the 48 GB MacBook (Apple M5 Pro) printed it on 2026-11-06. The measurement
* notes quote `total="37.4 GiB" available="37.4 GiB"`; the rest of the line is the Mac mini's
* with the chip name and a time filled in. `parseInferenceComputeTotal()` must read
* 40,157,944,217 bytes from it.
* The same line for the 48 GB MacBook (Apple M5 Pro), RECONSTRUCTED: the measurement notes of
* 2026-11-06 quote only `total="37.4 GiB" available="37.4 GiB"` from it, so the rest is the
* Mac mini's line with the chip name and a time filled in (hence no `REAL_` in the name).
* `parseInferenceComputeTotal()` must read 40,157,944,217 bytes from it.
*/
export const REAL_INFERENCE_COMPUTE_LINE_MACBOOK =
export const INFERENCE_COMPUTE_LINE_MACBOOK =
'time=2026-11-06T15:04:12.371+01:00 level=INFO source=types.go:32 msg="inference compute" id=0 filter_id=0 library=Metal compute=0.0 name=MTL0 description="Apple M5 Pro" libdirs="" driver=0.0 pci_id="" type=iGPU total="37.4 GiB" available="37.4 GiB"';
/** 37.4 GiB in bytes, rounded down: what the MacBook line stands for. */
+2 -2
View File
@@ -23,9 +23,9 @@ import {
fakeRuntimeLog,
FakeRuntimeChild,
freeLoopbackPort,
INFERENCE_COMPUTE_LINE_MACBOOK,
MACBOOK_MEASURED_TOTAL_BYTES,
REAL_INFERENCE_COMPUTE_LINE_MAC_MINI,
REAL_INFERENCE_COMPUTE_LINE_MACBOOK,
REAL_PULL_ERROR_LINE,
startFakeOllama,
type FakeOllama,
@@ -364,7 +364,7 @@ describe('the fake runtime: the archive', () => {
describe('the fake runtime: the child process', () => {
it('holds the real log lines with the device total', () => {
expect(REAL_INFERENCE_COMPUTE_LINE_MAC_MINI).toContain('total="11.8 GiB" available="11.8 GiB"');
expect(REAL_INFERENCE_COMPUTE_LINE_MACBOOK).toContain('total="37.4 GiB" available="37.4 GiB"');
expect(INFERENCE_COMPUTE_LINE_MACBOOK).toContain('total="37.4 GiB" available="37.4 GiB"');
expect(MACBOOK_MEASURED_TOTAL_BYTES).toBe(Math.floor(37.4 * 1024 ** 3));
expect(fakeRuntimeLog('37.4 GiB')[2]).toContain('msg="inference compute"');
});