loki-mode 7.31.0 → 7.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -113,15 +113,22 @@ _ml_python() {
113
113
  # `--check-sdk` probe, which runs the exact loader the server uses and exits 0
114
114
  # only when FastMCP loaded.
115
115
  #
116
- # Critical: we set PYTHONPATH to the install root and DO NOT cd into it, so the
117
- # probe exercises the SAME module resolution as the real launch (which preserves
118
- # the user's cwd). The redirect of stdin from /dev/null is insurance: if the
119
- # pip SDK's own `mcp.server` were ever reached, its stub starts a stdio receive
120
- # loop; the EOF makes it exit instead of hanging.
116
+ # Critical: we probe with the SAME FILE-EXEC form the launch uses
117
+ # (`"$root/mcp/server.py"`, NOT `-m mcp.server`), with PYTHONPATH set to the
118
+ # install root and WITHOUT cd-ing into it, so the probe exercises byte-identical
119
+ # module resolution to the real launch (which preserves the user's cwd). This
120
+ # matters because `-m mcp.server` puts the user's cwd at sys.path[0] AHEAD of
121
+ # PYTHONPATH=$root, so a cwd that happens to contain a regular `mcp/` python
122
+ # package would shadow Loki's server during the probe (false SDK-missing) while
123
+ # the file-exec launch -- immune to cwd shadowing -- would succeed. Probing by
124
+ # file path keeps probe and launch resolving the IDENTICAL module. The redirect
125
+ # of stdin from /dev/null is insurance: if the pip SDK's own `mcp.server` were
126
+ # ever reached, its stub starts a stdio receive loop; the EOF makes it exit
127
+ # instead of hanging.
121
128
  _ml_sdk_importable() {
122
129
  local py="$1" root="$2"
123
130
  PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" \
124
- "$py" -m mcp.server --check-sdk </dev/null >/dev/null 2>&1
131
+ "$py" "$root/mcp/server.py" --check-sdk </dev/null >/dev/null 2>&1
125
132
  }
126
133
 
127
134
  # _ml_print_manual <root> <venv>: print the honest manual install commands.
@@ -135,7 +142,7 @@ _ml_print_manual() {
135
142
  printf 'Install the MCP server dependencies manually:\n' >&2
136
143
  printf " python3 -m venv '%s'\n" "$venv" >&2
137
144
  printf " '%s/bin/pip' install -r '%s/mcp/requirements.txt'\n" "$venv" "$root" >&2
138
- printf " PYTHONPATH='%s' '%s/bin/python' -m mcp.server\n" "$root" "$venv" >&2
145
+ printf " PYTHONPATH='%s' '%s/bin/python' '%s/mcp/server.py'\n" "$root" "$venv" "$root" >&2
139
146
  }
140
147
 
141
148
  _ml_help() {
@@ -189,7 +196,8 @@ EOF
189
196
  # mcp_launch_main: dispatcher invoked by cmd_mcp() (autonomy/loki) or directly.
190
197
  mcp_launch_main() {
191
198
  # Split argv into launcher-owned flags (consumed here) and server argv
192
- # (forwarded verbatim to `python -m mcp.server`). The server's argparse only
199
+ # (forwarded verbatim to the file-exec launch `python "$root/mcp/server.py"`).
200
+ # The server's argparse only
193
201
  # accepts --transport/--port/--check-sdk; forwarding a launcher flag like
194
202
  # --yes would make it abort with exit 2, so launcher flags MUST be stripped.
195
203
  # A bare `--` ends launcher parsing: everything after it is forwarded as-is
@@ -255,20 +263,23 @@ mcp_launch_main() {
255
263
  fi
256
264
 
257
265
  # 3. If the venv already has the SDK, use it directly. The server is launched
258
- # with PYTHONPATH=$root (NOT by cd-ing) so the user's cwd is preserved for
266
+ # by FILE PATH ($root/mcp/server.py) rather than `-m mcp.server`, with
267
+ # PYTHONPATH=$root (NOT by cd-ing) so the user's cwd is preserved for
259
268
  # .loki resolution; see _ml_sdk_importable for why.
260
- # Known narrow residual: if the user's cwd itself contains a Python
261
- # package literally named mcp/ with a server submodule, python -m puts
262
- # the cwd ahead of PYTHONPATH and that package wins. Essentially never
263
- # true for real projects; documented rather than fought.
269
+ # Running the file directly avoids the runpy RuntimeWarning that `-m`
270
+ # emits (the local mcp/ package is imported during SDK-namespace setup
271
+ # before runpy executes mcp.server). server.py uses only absolute imports
272
+ # (e.g. `from mcp._sdk_loader import ...`), which resolve via PYTHONPATH=$root
273
+ # under file execution. File-exec also removes the old narrow cwd-shadowing
274
+ # residual: an explicit path can never be shadowed by a cwd `mcp/` package.
264
275
  if [ -x "$venv_py" ] && _ml_sdk_importable "$venv_py" "$root"; then
265
- exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$venv_py" -m mcp.server "$@"
276
+ exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$venv_py" "$root/mcp/server.py" "$@"
266
277
  fi
267
278
 
268
279
  # 4. If the BASE python already has the SDK (e.g. user pip-installed it),
269
280
  # use it -- no venv needed.
270
281
  if _ml_sdk_importable "$base_py" "$root"; then
271
- exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$base_py" -m mcp.server "$@"
282
+ exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$base_py" "$root/mcp/server.py" "$@"
272
283
  fi
273
284
 
274
285
  # 5. SDK missing. Decide whether we may bootstrap.
@@ -374,7 +385,7 @@ mcp_launch_main() {
374
385
  return 2
375
386
  fi
376
387
  printf "%sMCP dependencies ready. Launching server over stdio ...%s\n" "$_ML_BOLD" "$_ML_NC" >&2
377
- exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$venv_py" -m mcp.server "$@"
388
+ exec env PYTHONPATH="$root${PYTHONPATH:+:$PYTHONPATH}" "$venv_py" "$root/mcp/server.py" "$@"
378
389
  }
379
390
 
380
391
  # Executed directly (tests, manual): run the dispatcher.
package/autonomy/run.sh CHANGED
@@ -12328,13 +12328,30 @@ except Exception as exc:
12328
12328
  # helpers (which expect tier names) resolve correctly. Unknown
12329
12329
  # model strings are passed through as-is; provider loaders fall
12330
12330
  # back to a sane default.
12331
- case "${LOKI_SESSION_MODEL:-sonnet}" in
12331
+ #
12332
+ # Normalize case + surrounding whitespace BEFORE the match so
12333
+ # 'OPUS' and ' opus ' resolve identically to 'opus'. We do NOT use
12334
+ # loki_normalize_model_alias here: that helper is the narrow
12335
+ # OVERRIDE-file allowlist (haiku|sonnet|opus|fable) and would strip
12336
+ # the documented tier-name pins (planning|development|fast) to
12337
+ # empty, collapsing them onto the default tier. The session pin
12338
+ # legitimately accepts tier names (skills/model-selection.md), and
12339
+ # the estimator + dashboard mirror this exact tier route, so the
12340
+ # canonical session-pin rule is trim+lowercase WITHOUT the alias
12341
+ # allowlist. Interior whitespace is preserved (so 'fab le' stays a
12342
+ # junk value that falls through the '*' default arm), matching the
12343
+ # estimator/dashboard ports.
12344
+ local _session_pin="${LOKI_SESSION_MODEL:-sonnet}"
12345
+ _session_pin="${_session_pin#"${_session_pin%%[![:space:]]*}"}"
12346
+ _session_pin="${_session_pin%"${_session_pin##*[![:space:]]}"}"
12347
+ _session_pin="$(printf '%s' "$_session_pin" | tr '[:upper:]' '[:lower:]')"
12348
+ case "$_session_pin" in
12332
12349
  opus) CURRENT_TIER="planning" ;;
12333
12350
  sonnet) CURRENT_TIER="development" ;;
12334
12351
  haiku) CURRENT_TIER="fast" ;;
12335
12352
  fable) CURRENT_TIER="fable" ;;
12336
- planning|development|fast) CURRENT_TIER="${LOKI_SESSION_MODEL}" ;;
12337
- *) CURRENT_TIER="${LOKI_SESSION_MODEL}" ;;
12353
+ planning|development|fast) CURRENT_TIER="$_session_pin" ;;
12354
+ *) CURRENT_TIER="$_session_pin" ;;
12338
12355
  esac
12339
12356
  fi
12340
12357
  # Architect opt-in (LOKI_FABLE_ARCHITECT=1): route ONLY the first
package/bin/loki CHANGED
@@ -65,6 +65,21 @@ elif [ "${BUN_FROM_SOURCE:-0}" = "1" ] || [ "${BUN_FROM_SOURCE:-}" = "true" ]; t
65
65
  fi
66
66
  elif [ -f "$REPO_ROOT/loki-ts/dist/loki.js" ]; then
67
67
  BUN_CLI="$REPO_ROOT/loki-ts/dist/loki.js"
68
+ # Stale-dist freshness guard. dist/loki.js is gitignored (loki-ts/.gitignore)
69
+ # and rebuilt by package.json's prepack and the release Docker build, so on
70
+ # released channels (npm/Docker/brew) src/cli.ts is absent and this guard is
71
+ # a no-op -- dist is always current there. On a DEV machine / worktree the
72
+ # gitignored dist can predate the current dispatcher: e.g. a new shim->Bun
73
+ # route (`report kpis`) added in src that the old dist bundle does not know,
74
+ # which would make the canonical form fail with "Unknown command" while a
75
+ # deprecated alias the old dist still knows silently works -- the exact
76
+ # deprecation inversion the report-kpis route exists to prevent. So when src
77
+ # exists AND is newer than dist (`-nt`, available on bash 3.2), prefer the src
78
+ # form so dev runs never execute a stale dispatcher. Released channels have no
79
+ # src, so they keep using dist unchanged.
80
+ if [ -f "$REPO_ROOT/loki-ts/src/cli.ts" ] && [ "$REPO_ROOT/loki-ts/src/cli.ts" -nt "$REPO_ROOT/loki-ts/dist/loki.js" ]; then
81
+ BUN_CLI="$REPO_ROOT/loki-ts/src/cli.ts"
82
+ fi
68
83
  elif [ -f "$REPO_ROOT/loki-ts/src/cli.ts" ]; then
69
84
  BUN_CLI="$REPO_ROOT/loki-ts/src/cli.ts"
70
85
  else
@@ -131,6 +146,43 @@ if [ "${1:-}" = "trust" ]; then
131
146
  done
132
147
  fi
133
148
 
149
+ # CLI consolidation (Phase B): `report kpis` is the canonical form of the
150
+ # Bun-only KPI snapshot. The `report` noun is otherwise bash-owned (every other
151
+ # subcommand -- session/metrics/cost/export/share/dogfood -- routes to bash
152
+ # cmd_report), so it is NOT in the Bun allowlist below. But kpis has no bash
153
+ # implementation (it reuses the canonical cost arithmetic in runner/budget.ts),
154
+ # so the canonical `report kpis` must reach the Bun handler -- otherwise the
155
+ # canonical form would print the honest "requires Bun" message on a Bun machine
156
+ # while the deprecated `kpis` alias actually worked, inverting the deprecation.
157
+ # Route `report kpis` to Bun when `kpis` is the report SUBCOMMAND, i.e. the FIRST
158
+ # non-flag token after `report`. This satisfies `report kpis`, `report kpis
159
+ # --json`, and `report --json kpis` (the flag-anywhere orderings the trust-detail
160
+ # precedent established), while NOT hijacking `kpis` when it appears as a
161
+ # positional VALUE of a different report subcommand. `report export json kpis`
162
+ # (kpis = the export output filename) must keep working exactly as on main
163
+ # (v7.31.0): exit 0, file `kpis` created. So we scan past leading flags, take the
164
+ # first real token, and only route to Bun if it is literally `kpis`. Fire the
165
+ # same cli_command telemetry the Bun case-arm below fires (command=report), so a
166
+ # Bun-routed `report kpis` is not invisible to usage analytics (the v7.8.2 parity
167
+ # the case-arm comment documents). Backgrounded, FD-detached, opt-out honored by
168
+ # loki_telemetry itself.
169
+ if [ "${1:-}" = "report" ]; then
170
+ _report_first_sub=""
171
+ for _report_arg in "${@:2}"; do
172
+ case "$_report_arg" in
173
+ -*) continue ;;
174
+ *) _report_first_sub="$_report_arg"; break ;;
175
+ esac
176
+ done
177
+ if [ "$_report_first_sub" = "kpis" ]; then
178
+ if command -v curl &>/dev/null && [ -f "$REPO_ROOT/autonomy/telemetry.sh" ]; then
179
+ ( SCRIPT_DIR="$REPO_ROOT/autonomy"; source "$SCRIPT_DIR/telemetry.sh" 2>/dev/null && loki_telemetry "cli_command" "command=${1:-}" 2>/dev/null ) >/dev/null 2>&1 </dev/null &
180
+ disown 2>/dev/null || true
181
+ fi
182
+ exec bun "$BUN_CLI" "$@"
183
+ fi
184
+ fi
185
+
134
186
  # Commands ported in Phase 2 -- route to Bun. Everything else goes to bash.
135
187
  # Two-token routes (provider show/list, memory list/index) match on the first
136
188
  # token only; the Bun dispatcher handles subcommand routing internally.
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "7.31.0"
10
+ __version__ = "7.32.1"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2233,6 +2233,29 @@ def _normalize_session_model(raw: str | None) -> str:
2233
2233
  return val if val in _SESSION_MODEL_ALLOWLIST else ""
2234
2234
 
2235
2235
 
2236
+ # Session-pin allowlist is BROADER than the override-file allowlist above.
2237
+ # run.sh's session-pin case (run.sh:12331) accepts the four model aliases AND
2238
+ # the three raw tier names (planning|development|fast) -- documented at
2239
+ # skills/model-selection.md:8. The OVERRIDE file / POST path keeps the narrow
2240
+ # _SESSION_MODEL_ALLOWLIST because that value is fed straight to `claude
2241
+ # --model`, where tier names are not valid. The session pin is a tier route, so
2242
+ # tier names ARE valid pins.
2243
+ _SESSION_PIN_ALLOWLIST = _SESSION_MODEL_ALLOWLIST + ("planning", "development", "fast")
2244
+
2245
+
2246
+ def _normalize_session_pin(raw: str | None) -> str:
2247
+ """Normalize a LOKI_SESSION_MODEL pin value (aliases + raw tier names).
2248
+
2249
+ Mirrors run.sh's session-pin case: trim + lowercase, accept the four model
2250
+ aliases and the three tier names. Interior whitespace is preserved (so
2251
+ "fab le" stays junk and falls through to the default tier, exactly like the
2252
+ runner's "*" arm). Use this for the session-pin (no-override) derivation;
2253
+ use _normalize_session_model for the override-file / POST path.
2254
+ """
2255
+ val = (raw or "").strip().lower()
2256
+ return val if val in _SESSION_PIN_ALLOWLIST else ""
2257
+
2258
+
2236
2259
  # Provider-config model resolution mirror.
2237
2260
  #
2238
2261
  # SYNC: This is a byte-faithful python port of the claude provider's tier->model
@@ -2268,6 +2291,17 @@ def _provider_model_development() -> str:
2268
2291
  )
2269
2292
 
2270
2293
 
2294
+ def _provider_model_planning() -> str:
2295
+ # claude.sh:65 -> LOKI_CLAUDE_MODEL_PLANNING > LOKI_MODEL_PLANNING > opus.
2296
+ # CLAUDE_DEFAULT_PLANNING is always opus (LOKI_ALLOW_HAIKU lowers only the
2297
+ # development and fast defaults, not planning).
2298
+ return (
2299
+ os.environ.get("LOKI_CLAUDE_MODEL_PLANNING")
2300
+ or os.environ.get("LOKI_MODEL_PLANNING")
2301
+ or "opus"
2302
+ )
2303
+
2304
+
2271
2305
  def _clamp_to_max_tier(alias: str) -> str:
2272
2306
  """Apply the operator LOKI_MAX_TIER ceiling to a model alias.
2273
2307
 
@@ -2297,44 +2331,102 @@ def _clamp_to_max_tier(alias: str) -> str:
2297
2331
  return alias
2298
2332
 
2299
2333
 
2334
+ def _resolve_session_pin(alias: str) -> str:
2335
+ """Resolve a session-pin alias the way the runner's NO-OVERRIDE path does.
2336
+
2337
+ The runner does NOT feed a session pin straight to --model. It maps the alias
2338
+ to an abstract TIER (run.sh:12331 -- opus->planning, sonnet->development,
2339
+ haiku->fast, fable->fable) and resolves that tier through
2340
+ resolve_model_for_tier (claude.sh:353), then applies
2341
+ loki_apply_max_tier_clamp(model, REAL_tier). This DIFFERS from
2342
+ _clamp_to_max_tier (the override-path clamp): a 'sonnet' SESSION pin
2343
+ dispatches OPUS (development tier -> PROVIDER_MODEL_DEVELOPMENT=opus on stock
2344
+ config), whereas a 'sonnet' OVERRIDE file dispatches sonnet (fed straight to
2345
+ --model). Use this for the no-override `default`/`effective` derivation so the
2346
+ dashboard reports the model the run actually dispatches on the default path.
2347
+
2348
+ SYNC: byte-faithful with run.sh's session-pin case + claude.sh
2349
+ resolve_model_for_tier + loki_apply_max_tier_clamp, and with the estimator's
2350
+ _resolve_session_pin in autonomy/loki. Locked by the session-pin parity matrix
2351
+ in tests/test-model-override.sh.
2352
+ """
2353
+ pin_tier = {
2354
+ "opus": "planning",
2355
+ "sonnet": "development",
2356
+ "haiku": "fast",
2357
+ "fable": "fable",
2358
+ # Raw tier-name pins (run.sh:12336 passthrough arm) map to their own
2359
+ # tier, NOT through the alias table. pin=fast -> fast tier ->
2360
+ # PROVIDER_MODEL_FAST, matching the runner's dispatch instead of
2361
+ # collapsing onto development.
2362
+ "planning": "planning",
2363
+ "development": "development",
2364
+ "fast": "fast",
2365
+ }.get((alias or "").strip().lower(), "development")
2366
+ if pin_tier == "planning":
2367
+ model = _provider_model_planning()
2368
+ elif pin_tier == "fast":
2369
+ model = _provider_model_fast()
2370
+ elif pin_tier == "fable":
2371
+ model = "fable"
2372
+ else: # development (and the unknown-alias '*' fallthrough)
2373
+ model = _provider_model_development()
2374
+ max_tier = (os.environ.get("LOKI_MAX_TIER") or "").strip().lower()
2375
+ if not max_tier:
2376
+ return model
2377
+ if max_tier == "haiku":
2378
+ return _provider_model_fast()
2379
+ if max_tier == "sonnet":
2380
+ # claude.sh sonnet-cap downgrades planning/fable tiers (or a fable model)
2381
+ # to PROVIDER_MODEL_DEVELOPMENT; development/fast pass through.
2382
+ if pin_tier in ("planning", "fable") or model == "fable":
2383
+ return _provider_model_development()
2384
+ return model
2385
+ if max_tier == "opus":
2386
+ return "opus" if model == "fable" else model
2387
+ return model
2388
+
2389
+
2300
2390
  @app.get("/api/session/model", dependencies=[Depends(auth.require_scope("read"))])
2301
2391
  async def get_session_model():
2302
2392
  """Report the live run's model override and the effective default.
2303
2393
 
2304
2394
  `override` is the alias currently written to .loki/state/model-override
2305
- (None when no override is active). `default` is the session model the run
2306
- falls back to when there is no override (LOKI_SESSION_MODEL or the catalog
2307
- default). `effective` is the model the next iteration will actually use,
2308
- after the LOKI_MAX_TIER cost ceiling is applied (so the dashboard never
2309
- reports a model the run would clamp down).
2310
-
2311
- The clamp resolves through the SAME provider config the runner uses
2312
- (LOKI_ALLOW_HAIKU plus the LOKI_CLAUDE_MODEL_FAST/DEVELOPMENT and
2313
- LOKI_MODEL_FAST/DEVELOPMENT overrides): _clamp_to_max_tier mirrors
2314
- providers/claude.sh loki_apply_max_tier_clamp byte-for-byte (locked by the
2315
- resolver parity matrix in tests/test-model-override.sh). So for the OVERRIDE
2316
- case -- the feature this endpoint exists for -- the reported `effective` model
2317
- equals the model the runner's mid-flight override path dispatches, given the
2318
- same environment.
2395
+ (None when no override is active). `default` is the session pin alias the run
2396
+ falls back to when there is no override (LOKI_SESSION_MODEL or "sonnet").
2397
+ `effective` is the model the next iteration will actually DISPATCH, resolved
2398
+ on the SAME route the runner uses for the active case, so the dashboard never
2399
+ reports a model that differs from what the run runs:
2400
+
2401
+ - OVERRIDE active: the runner feeds the alias straight to --model via
2402
+ loki_apply_max_tier_clamp(alias, alias). `effective` = _clamp_to_max_tier
2403
+ (the override-path clamp). A "sonnet" override dispatches sonnet.
2404
+ - NO override (session pin): the runner maps the pin through a tier
2405
+ (opus->planning, sonnet->development, haiku->fast) and resolves the tier
2406
+ through PROVIDER_MODEL_* (then the cost-ceiling clamp). `effective` =
2407
+ _resolve_session_pin. A "sonnet" pin dispatches OPUS (development tier ->
2408
+ PROVIDER_MODEL_DEVELOPMENT=opus on stock config).
2409
+
2410
+ Both routes resolve through the SAME provider config the runner uses
2411
+ (LOKI_ALLOW_HAIKU plus the LOKI_CLAUDE_MODEL_PLANNING/FAST/DEVELOPMENT and
2412
+ LOKI_MODEL_* overrides) and the SAME LOKI_MAX_TIER ceiling, mirroring
2413
+ providers/claude.sh byte-for-byte. The agreement (estimator == dashboard ==
2414
+ runner) on BOTH routes -- including the no-override stock path -- is locked by
2415
+ the cross-route cases and the session-pin parity matrix in
2416
+ tests/test-model-override.sh. (Before task 568 the no-override path applied the
2417
+ override-path clamp to the pin, so a stock "sonnet" pin reported "sonnet" while
2418
+ the run dispatched opus; that gap is now closed.)
2319
2419
 
2320
2420
  KNOWN LIMITATION (cross-process env divergence): the resolution reads
2321
2421
  LOKI_MAX_TIER, LOKI_ALLOW_HAIKU, LOKI_SESSION_MODEL and the model-override env
2322
2422
  vars from the DASHBOARD process's environment, which is usually a different
2323
2423
  process than the live run. So if the run was launched with a different
2324
2424
  environment than the dashboard, the no-override `default`/`effective` may not
2325
- reflect the run's real pinned tier or ceiling (e.g. a run pinned to opus still
2326
- reads "sonnet" here). The override case reads the run's own state file, so its
2327
- alias is always accurate and the clamp is exact whenever the dashboard shares
2425
+ reflect the run's real pinned tier or ceiling (e.g. a run launched with
2426
+ LOKI_SESSION_MODEL=opus while the dashboard's env has no pin still reads the
2427
+ default here). The override case reads the run's own state file, so its alias
2428
+ is always accurate and the resolution is exact whenever the dashboard shares
2328
2429
  the run's environment.
2329
-
2330
- SCOPE NOTE (no-override default path): when there is no override, `effective`
2331
- applies the override-path clamp to the session default. The runner's
2332
- no-override route instead maps a session pin through a tier
2333
- (resolve_model_for_tier: opus->planning, sonnet->development), which can differ
2334
- from the override-path clamp in one cell (e.g. an opus pin under sonnet cap +
2335
- LOKI_ALLOW_HAIKU: the tier route yields sonnet, the override-path clamp yields
2336
- opus). That session-pin modeling gap is pre-existing and out of scope here;
2337
- the override case this endpoint serves is exact.
2338
2430
  """
2339
2431
  override = None
2340
2432
  try:
@@ -2343,8 +2435,16 @@ async def get_session_model():
2343
2435
  override = _normalize_session_model(p.read_text()) or None
2344
2436
  except OSError:
2345
2437
  override = None
2346
- default = _normalize_session_model(os.environ.get("LOKI_SESSION_MODEL")) or "sonnet"
2347
- effective = _clamp_to_max_tier(override or default)
2438
+ # Session pin accepts tier names too (run.sh:12336), so use the broader
2439
+ # session-pin normalizer here (NOT the narrow override allowlist).
2440
+ default = _normalize_session_pin(os.environ.get("LOKI_SESSION_MODEL")) or "sonnet"
2441
+ # Resolve on the route the runner will actually take: override-path clamp when
2442
+ # an override file is present, session-pin tier route otherwise. This closes
2443
+ # the task-568 stock-path gap (a "sonnet" pin dispatches opus).
2444
+ if override is not None:
2445
+ effective = _clamp_to_max_tier(override)
2446
+ else:
2447
+ effective = _resolve_session_pin(default)
2348
2448
  return {
2349
2449
  "override": override,
2350
2450
  "default": default,
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v7.31.0
5
+ **Version:** v7.32.1
6
6
 
7
7
  ---
8
8