@intentic/sandbox-contract 1.165.0 → 1.167.0
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.
- package/dist/agent-catalog.d.ts +0 -1
- package/dist/agent-catalog.d.ts.map +1 -1
- package/dist/agent-catalog.js +1 -2
- package/dist/agent-catalog.js.map +1 -1
- package/dist/contracts/agent.contract.d.ts +2 -2
- package/dist/contracts/agents.contract.d.ts +18 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/automations.contract.d.ts +1 -1
- package/dist/contracts/ci.contract.d.ts +4 -0
- package/dist/contracts/ci.contract.d.ts.map +1 -1
- package/dist/contracts/ci.contract.js +2 -1
- package/dist/contracts/ci.contract.js.map +1 -1
- package/dist/contracts/extensions.contract.d.ts +1 -0
- package/dist/contracts/extensions.contract.d.ts.map +1 -1
- package/dist/contracts/gate.contract.d.ts +47 -0
- package/dist/contracts/gate.contract.d.ts.map +1 -0
- package/dist/contracts/gate.contract.js +9 -0
- package/dist/contracts/gate.contract.js.map +1 -0
- package/dist/contracts/kimi.contract.d.ts +0 -70
- package/dist/contracts/kimi.contract.d.ts.map +1 -1
- package/dist/contracts/kimi.contract.js +1 -5
- package/dist/contracts/kimi.contract.js.map +1 -1
- package/dist/contracts/settings.contract.d.ts +37 -3
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +68 -8
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.js +3 -1
- package/dist/contracts/system.contract.js.map +1 -1
- package/dist/contracts/translator.contract.d.ts +11 -0
- package/dist/contracts/translator.contract.d.ts.map +1 -1
- package/dist/contracts/translator.contract.js.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +3 -1
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/events.d.ts +98 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +21 -2
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +188 -81
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/model-order.d.ts.map +1 -1
- package/dist/model-order.js +25 -6
- package/dist/model-order.js.map +1 -1
- package/dist/schemas.d.ts +179 -21
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +90 -13
- package/dist/schemas.js.map +1 -1
- package/dist/session-names.d.ts +8 -0
- package/dist/session-names.d.ts.map +1 -0
- package/dist/session-names.js +17 -0
- package/dist/session-names.js.map +1 -0
- package/package.json +13 -2
- package/src/agent-catalog.ts +6 -11
- package/src/contracts/agents.contract.ts +8 -6
- package/src/contracts/ci.contract.ts +5 -1
- package/src/contracts/gate.contract.ts +19 -0
- package/src/contracts/kimi.contract.ts +4 -21
- package/src/contracts/system.contract.ts +9 -0
- package/src/contracts/translator.contract.ts +4 -6
- package/src/events.ts +56 -8
- package/src/index.ts +3 -0
- package/src/model-order.test.ts +29 -12
- package/src/model-order.ts +50 -18
- package/src/quick-model.test.ts +3 -3
- package/src/schemas.test.ts +20 -0
- package/src/schemas.ts +305 -64
- package/src/session-names.ts +44 -0
package/src/agent-catalog.ts
CHANGED
|
@@ -42,7 +42,7 @@ export const PROVIDER_ACCESS: Record<NativeProvider, ProviderAccess> = {
|
|
|
42
42
|
claude: { kind: "subscription", requirement: "Claude subscription", runs: "Claude Code" },
|
|
43
43
|
codex: { kind: "subscription", requirement: "ChatGPT subscription", runs: "Codex" },
|
|
44
44
|
grok: { kind: "subscription", requirement: "SuperGrok subscription", runs: "Grok" },
|
|
45
|
-
kimi: { kind: "
|
|
45
|
+
kimi: { kind: "subscription", requirement: "Kimi Code subscription", runs: "Kimi Code" },
|
|
46
46
|
gemini: { kind: "free", requirement: "Google sign-in", runs: "Gemini, Claude and GPT-OSS under Claude Code" },
|
|
47
47
|
};
|
|
48
48
|
|
|
@@ -71,8 +71,8 @@ export const HARNESSES: readonly { label: string; value: AgentHarness }[] = [
|
|
|
71
71
|
|
|
72
72
|
// Whether a turn on this provider/harness pair ACTUALLY runs the Claude Code Agent SDK loop — which is not the
|
|
73
73
|
// same question as `harness === "claude-code"`. Claude is always its own Claude Code loop, and kimi/gemini have
|
|
74
|
-
// no native runtime at all (
|
|
75
|
-
//
|
|
74
|
+
// no native runtime at all (both are re-served through the translator), so all three run it whatever harness the
|
|
75
|
+
// client happened to send; only codex/grok have a native
|
|
76
76
|
// runtime to switch away from. An ACP agent runs its own loop and is never one of these.
|
|
77
77
|
//
|
|
78
78
|
// Everything the SDK loop owns keys off this: the SteeringQueue that makes mid-turn injection possible, and the
|
|
@@ -114,13 +114,8 @@ export const modelsFor = (provider: AgentProvider): CatalogOption[] => {
|
|
|
114
114
|
// the only constrained tier and it fails two ways: no non-Claude scale HAS it, and Claude's API rejects it
|
|
115
115
|
// outright when extended thinking is disabled ("effort 'max' is not supported when thinking is disabled on this
|
|
116
116
|
// model" — a 400 that kills the turn before the model sees it, surfacing only as the SDK's `unknown` error
|
|
117
|
-
// category).
|
|
118
|
-
//
|
|
119
|
-
//
|
|
117
|
+
// category). It is the one rule a MODEL's published tier list can't express — the daemon reports what a model
|
|
118
|
+
// accepts without knowing this turn's thinking setting — so the consumer that assembles the offered scale
|
|
119
|
+
// (effortsFor, web-side) filters through here, and the clamp over that scale makes the pair unreachable.
|
|
120
120
|
export const effortAllowed = (effort: string, provider: AgentProvider, thinking: boolean): boolean =>
|
|
121
121
|
effort !== "max" || (provider === "claude" && thinking);
|
|
122
|
-
|
|
123
|
-
// The tier a selection falls back to when effortAllowed rejects it — one rung down from 'max', the top of every
|
|
124
|
-
// scale that excludes it.
|
|
125
|
-
export const clampEffort = (effort: string, provider: AgentProvider, thinking: boolean): string =>
|
|
126
|
-
effortAllowed(effort, provider, thinking) ? effort : "xhigh";
|
|
@@ -20,19 +20,21 @@ import {
|
|
|
20
20
|
OkSchema,
|
|
21
21
|
} from "../schemas.js";
|
|
22
22
|
|
|
23
|
-
// The fleet: every registered conversation-agent (see AgentSummarySchema).
|
|
24
|
-
//
|
|
23
|
+
// The fleet: every registered conversation-agent (see AgentSummarySchema). Registry-level actions (read,
|
|
24
|
+
// rename, seen, archive) apply uniformly. For branch-backed conversations, `diff` is the worktree's CUMULATIVE
|
|
25
|
+
// multi-repo delta vs its recorded per-repo bases — one flat change set per repo
|
|
25
26
|
// (AgentChanges), each file flagged `landed` or not, deliberately not the working tree's staged/unstaged
|
|
26
27
|
// shape: a worktree the user never checks out has no index they could stage into. `land` merges the worktree
|
|
27
28
|
// branches into the main tree (per-repo, conflicts reported, nothing lost on failure); `discard` removes
|
|
28
|
-
// worktrees + branches + registry entry.
|
|
29
|
+
// worktrees + branches + registry entry. Those branch actions reject workspace conversations explicitly. An
|
|
30
|
+
// unknown {id} is NOT_FOUND; land/discard while the turn runs is CONFLICT.
|
|
29
31
|
// `rename` sets the user-chosen display title — legal mid-turn (it touches no worktree state).
|
|
30
32
|
// `seen`/`seenAll` stamp the read marker behind the cards' unread badge (AgentSummarySchema.seenAt) — also
|
|
31
33
|
// legal mid-turn, and like `rename` they never bump `updatedAt` (reading is not activity).
|
|
32
34
|
//
|
|
33
|
-
// ARCHIVE is the non-destructive counterpart to discard, and the one the board leans on:
|
|
34
|
-
// whatever the worktree still holds onto agent/<id
|
|
35
|
-
//
|
|
35
|
+
// ARCHIVE is the non-destructive counterpart to discard, and the one the board leans on: for an isolated
|
|
36
|
+
// conversation, `archive` commits whatever the worktree still holds onto agent/<id> and drops the CHECKOUT;
|
|
37
|
+
// for a workspace conversation it has no git teardown. Both keep the entry and transcript. `list` stops carrying it and
|
|
36
38
|
// `archived` does; `unarchive` puts it back, and either way the next turn re-attaches a worktree from the
|
|
37
39
|
// surviving branch. Archiving a running agent is CONFLICT, same as land/discard.
|
|
38
40
|
//
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { oc } from "@orpc/contract";
|
|
2
|
-
import { CiFixResponseSchema, CiJobsResponseSchema, CiRunParamSchema, CiRunsResponseSchema, OkSchema } from "../schemas.js";
|
|
2
|
+
import { CiFixResponseSchema, CiJobsResponseSchema, CiRunParamSchema, CiRunsResponseSchema, CiSeenResponseSchema, OkSchema } from "../schemas.js";
|
|
3
3
|
|
|
4
4
|
// Pipelines on the workspace repos' github/gitlab remotes. `runs` serves the Pipelines rail view (cache +
|
|
5
5
|
// on-demand backfill; per-repo webhook warnings ride along); `rerun`/`cancel` proxy to the vendor; `fix`
|
|
@@ -12,4 +12,8 @@ export const ciContract = {
|
|
|
12
12
|
cancel: oc.route({ method: "POST", path: "/ci/runs/cancel" }).input(CiRunParamSchema).output(OkSchema),
|
|
13
13
|
jobs: oc.route({ method: "POST", path: "/ci/runs/jobs" }).input(CiRunParamSchema).output(CiJobsResponseSchema),
|
|
14
14
|
fix: oc.route({ method: "POST", path: "/ci/fix" }).input(CiRunParamSchema).output(CiFixResponseSchema),
|
|
15
|
+
// "I have looked at the pipelines" — what silences the rail badge for breakages already read. No input:
|
|
16
|
+
// the surface is read as a whole, and the daemon stamps its own clock so a skewed browser can't mark
|
|
17
|
+
// future failures as already seen.
|
|
18
|
+
seen: oc.route({ method: "POST", path: "/ci/seen" }).output(CiSeenResponseSchema),
|
|
15
19
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { oc } from "@orpc/contract";
|
|
2
|
+
import { GateVerdictSchema, OkSchema } from "../schemas.js";
|
|
3
|
+
|
|
4
|
+
// The landing gate — the check command run over the composite of landed work once the fleet goes quiet (see
|
|
5
|
+
// GateVerdictSchema for where this sits and why). Four verbs, all about ONE verdict: the gate answers about the
|
|
6
|
+
// main working tree, of which there is exactly one, so nothing here is addressed by id.
|
|
7
|
+
//
|
|
8
|
+
// `verdict` is the one read, and the panel polls it; it recomputes staleness per call, so a passed verdict stops
|
|
9
|
+
// claiming a green light the moment the tree moves under it. The three writes answer `ok` and nothing else —
|
|
10
|
+
// each starts work that outlives the request, so there is no result to return and the poll is what reports.
|
|
11
|
+
// `run` arms nothing and waits for nothing: it starts the check now, the user's own "I'm about to commit, check
|
|
12
|
+
// this". `fix` opens a seeded workspace conversation for a red verdict; it has the same registry lifecycle as
|
|
13
|
+
// /ci/fix, minus the isolated worktree it must not have.
|
|
14
|
+
export const gateContract = {
|
|
15
|
+
verdict: oc.route({ method: "GET", path: "/gate/verdict" }).output(GateVerdictSchema),
|
|
16
|
+
run: oc.route({ method: "POST", path: "/gate/run" }).output(OkSchema),
|
|
17
|
+
cancel: oc.route({ method: "POST", path: "/gate/cancel" }).output(OkSchema),
|
|
18
|
+
fix: oc.route({ method: "POST", path: "/gate/fix" }).output(OkSchema),
|
|
19
|
+
};
|
|
@@ -1,26 +1,9 @@
|
|
|
1
1
|
import { oc } from "@orpc/contract";
|
|
2
|
-
import {
|
|
3
|
-
AccountIdSchema,
|
|
4
|
-
AccountRenameSchema,
|
|
5
|
-
KimiConnectSchema,
|
|
6
|
-
ModelsSchema,
|
|
7
|
-
OauthAccountListSchema,
|
|
8
|
-
OauthAccountSchema,
|
|
9
|
-
OkSchema,
|
|
10
|
-
} from "../schemas.js";
|
|
2
|
+
import { ModelsSchema } from "../schemas.js";
|
|
11
3
|
|
|
12
|
-
// Kimi Code
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// `connect` stores a pasted key as a new account and returns it; `models` is the live catalog for the picker;
|
|
16
|
-
// `accounts` lists connected keys (tokens never ride back); `rename` renames one (a pasted key carries no
|
|
17
|
-
// identity to derive a name from, so this is the ONLY way a Kimi row gets a distinguishing name);
|
|
18
|
-
// `disconnect` clears the one named by id.
|
|
4
|
+
// Kimi Code's picker catalog. Authentication and accounts belong to the bundled translator just like the other
|
|
5
|
+
// routed subscriptions (translator.contract.ts); this provider-specific route only projects CLIProxyAPI's Kimi
|
|
6
|
+
// model definitions into the shared picker shape.
|
|
19
7
|
export const kimiContract = {
|
|
20
|
-
connect: oc.route({ method: "POST", path: "/kimi/account/connect" }).input(KimiConnectSchema).output(OauthAccountSchema),
|
|
21
|
-
// Kimi/Moonshot's live models for the picker — the source of valid ids (see kimi-models.ts).
|
|
22
8
|
models: oc.route({ method: "GET", path: "/kimi/models" }).output(ModelsSchema),
|
|
23
|
-
accounts: oc.route({ method: "GET", path: "/kimi/accounts" }).output(OauthAccountListSchema),
|
|
24
|
-
rename: oc.route({ method: "POST", path: "/kimi/account/rename" }).input(AccountRenameSchema).output(OauthAccountSchema),
|
|
25
|
-
disconnect: oc.route({ method: "POST", path: "/kimi/account/disconnect" }).input(AccountIdSchema).output(OkSchema),
|
|
26
9
|
};
|
|
@@ -2,6 +2,8 @@ import { eventIterator, oc } from "@orpc/contract";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { SystemEventSchema } from "../events.js";
|
|
4
4
|
import {
|
|
5
|
+
BrowserNameParamSchema,
|
|
6
|
+
BrowsersListSchema,
|
|
5
7
|
DaemonSessionSchema,
|
|
6
8
|
HostTunnelInputSchema,
|
|
7
9
|
HostTunnelSchema,
|
|
@@ -38,4 +40,11 @@ export const systemContract = {
|
|
|
38
40
|
// (browser fetch sends the header), unlike the header-less WS route which app.ts exempts.
|
|
39
41
|
terminals: oc.route({ method: "GET", path: "/system/terminals" }).output(TerminalsListSchema),
|
|
40
42
|
killTerminal: oc.route({ method: "DELETE", path: "/system/terminals/{name}" }).input(TerminalNameParamSchema).output(OkSchema),
|
|
43
|
+
// The agent's live Chromiums and the pages each has open — the Browsers view's roster, polled while it is on
|
|
44
|
+
// screen and by the rail so its tile can appear the moment a turn starts browsing. The frames are the
|
|
45
|
+
// separate /system/browser-view WebSocket; this is the control plane, exactly as `terminals` is for tmux.
|
|
46
|
+
// `closeBrowser` shuts one Chromium down: the agent's next browser tool call then fails as if it had crashed,
|
|
47
|
+
// which is the honest account of the owner pulling the plug.
|
|
48
|
+
browsers: oc.route({ method: "GET", path: "/system/browsers" }).output(BrowsersListSchema),
|
|
49
|
+
closeBrowser: oc.route({ method: "DELETE", path: "/system/browsers/{name}" }).input(BrowserNameParamSchema).output(OkSchema),
|
|
41
50
|
};
|
|
@@ -7,12 +7,10 @@ import { KeyedProviderSchema, OkSchema, TranslatorAccountsSchema, TranslatorComp
|
|
|
7
7
|
// than an API key. A provider can hold several accounts side by side (the translator balances across them);
|
|
8
8
|
// `accounts` lists what's connected per provider and `disconnect` clears ONE account by its auth-file `name`.
|
|
9
9
|
//
|
|
10
|
-
// Two login shapes ride one pair of routes. Codex
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// landed on, and `complete` hands it to the translator to finish the exchange. The card branches on that empty
|
|
15
|
-
// code rather than on the provider id, so a provider that later gains a device flow needs no UI change.
|
|
10
|
+
// Two login shapes ride one pair of routes. Codex, Grok and Kimi use device authorization: the translator polls
|
|
11
|
+
// to completion in the background, so the UI only polls `accounts`. Google redirects the browser to a loopback
|
|
12
|
+
// URL this sandbox can't receive, so `complete` hands the landing URL to the translator. `connect.flow` tells the
|
|
13
|
+
// card which mechanic it received without inferring it from whether an optional device code happened to exist.
|
|
16
14
|
export const translatorContract = {
|
|
17
15
|
accounts: oc.route({ method: "GET", path: "/translator/accounts" }).output(TranslatorAccountsSchema),
|
|
18
16
|
connect: oc
|
package/src/events.ts
CHANGED
|
@@ -262,12 +262,14 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
262
262
|
* actually loses the work — so the wait has to be visible, with its own next-attempt clock.
|
|
263
263
|
*
|
|
264
264
|
* `attempt`/`maxAttempts` are the harness's own counters; `nextAttemptAt` (epoch ms) is when it will try
|
|
265
|
-
* again, so the readout counts down instead of freezing on a number nobody can interpret.
|
|
265
|
+
* again, so the readout counts down instead of freezing on a number nobody can interpret. Optional because
|
|
266
|
+
* only Claude's harness reports the delay: Codex says which attempt it is on and nothing else
|
|
267
|
+
* (codex-agent.ts), and inventing a countdown for it would be a clock the retry never keeps. */
|
|
266
268
|
z.object({
|
|
267
269
|
kind: z.literal("provider_retry"),
|
|
268
270
|
attempt: z.number(),
|
|
269
271
|
maxAttempts: z.number(),
|
|
270
|
-
nextAttemptAt: z.number(),
|
|
272
|
+
nextAttemptAt: z.number().optional(),
|
|
271
273
|
// The HTTP status behind it when there was one (529 reads as capacity, 500 as a fault — the client says
|
|
272
274
|
// which). Absent for a transport failure that never got a response.
|
|
273
275
|
status: z.number().optional(),
|
|
@@ -348,7 +350,8 @@ export const AgentEventSchema = z.discriminatedUnion("kind", [
|
|
|
348
350
|
// provider outage, because the client's reading of them is the same: "scheduled" = the resume is armed
|
|
349
351
|
// and this turn comes back by itself; "available" = the daemon remembered the failed turn and turning
|
|
350
352
|
// the setting on (autoResumeOnLimit / resumeAfterOutage) arms that same resume, which is what the
|
|
351
|
-
// chat's offer banner hangs off. Absent
|
|
353
|
+
// chat's offer banner hangs off. Absent normally means there is nothing automatic to resume; the
|
|
354
|
+
// usage-limit feature gate also leaves it absent while preserving the explicit account-switch path.
|
|
352
355
|
autoResume: z.enum(["scheduled", "available"]).optional(),
|
|
353
356
|
/* provider-outage only: the shape of the wait. `retryAt` (epoch seconds) is when the next attempt is
|
|
354
357
|
* due — not a fixed cadence, because an outage has no reset instant to aim at and hammering a provider
|
|
@@ -395,19 +398,63 @@ export type IntenticLine = z.infer<typeof IntenticLineSchema>;
|
|
|
395
398
|
export const HeartbeatSchema = z.object({ kind: z.literal("heartbeat") });
|
|
396
399
|
export type Heartbeat = z.infer<typeof HeartbeatSchema>;
|
|
397
400
|
|
|
401
|
+
// One step of the daemon's boot chain. `key` is the stable id the daemon declares it under, `label` the words
|
|
402
|
+
// the browser shows. A step that FAILED is still a step that finished — the boot chain is log-and-continue by
|
|
403
|
+
// design (see main.ts), so a failure degrades one subsystem rather than holding the gate closed forever.
|
|
404
|
+
export const BootStepSchema = z.object({
|
|
405
|
+
key: z.string(),
|
|
406
|
+
label: z.string(),
|
|
407
|
+
state: z.enum(["pending", "running", "done", "failed"]),
|
|
408
|
+
// Elapsed ms, once the step has finished.
|
|
409
|
+
ms: z.number().optional(),
|
|
410
|
+
});
|
|
411
|
+
export type BootStep = z.infer<typeof BootStepSchema>;
|
|
412
|
+
|
|
413
|
+
/* WHERE THE DAEMON IS IN ITS BOOT. The listeners come up before the state they serve has converged (main.ts:
|
|
414
|
+
* "listen first, converge behind the gate"), which is what stops a restart from reading as an outage — but it
|
|
415
|
+
* also means the daemon spends the first seconds of every boot both reachable and unable to answer, and until
|
|
416
|
+
* this frame existed the browser had no way to tell that apart from a healthy sandbox. It painted an operable
|
|
417
|
+
* workspace off its persisted cache and then parked every request the user made against the readiness gate.
|
|
418
|
+
*
|
|
419
|
+
* The step list is declared UP FRONT and sent whole, pending entries included, so the browser can say "4 of 11,
|
|
420
|
+
* loading the conversation registry" rather than "something is happening" — a boot that takes minutes has one
|
|
421
|
+
* slow step, and naming it is the whole point. Snapshot-not-diff, like every other roster on this stream. */
|
|
422
|
+
export const BootProgressSchema = z.object({
|
|
423
|
+
// False only while the chain is still converging. The browser holds every daemon read until this is true.
|
|
424
|
+
ready: z.boolean(),
|
|
425
|
+
// Epoch ms the daemon started converging, so the browser can show a total elapsed that survives a reconnect.
|
|
426
|
+
startedAt: z.number(),
|
|
427
|
+
steps: z.array(BootStepSchema),
|
|
428
|
+
});
|
|
429
|
+
export type BootProgress = z.infer<typeof BootProgressSchema>;
|
|
430
|
+
|
|
431
|
+
// Pushed on every step transition and once more when the gate opens. Rides /events, which answers before the
|
|
432
|
+
// gate precisely so this can be delivered while everything else waits.
|
|
433
|
+
export const BootSchema = z.object({ kind: z.literal("boot"), ...BootProgressSchema.shape });
|
|
434
|
+
export type Boot = z.infer<typeof BootSchema>;
|
|
435
|
+
|
|
398
436
|
// The stream's first frame: the workspace's stable identity, minted at the first boot of an empty /work. The
|
|
399
437
|
// browser remembers it per sandbox id and drops that sandbox's persisted query cache when it changes — a wiped
|
|
400
438
|
// and recreated workspace (cleanup.sh + reconnect keeps the same sandbox id) must not be painted from the
|
|
401
|
-
// previous workspace's cache.
|
|
439
|
+
// previous workspace's cache. `build` is the same guard against a different axis: the daemon's own compiled
|
|
440
|
+
// tree, so an image update (or a `pnpm build:sandbox` swap in dev) drops what the browser cached from the
|
|
441
|
+
// PREVIOUS build instead of hydrating payloads the new one no longer shapes that way.
|
|
402
442
|
//
|
|
403
443
|
// It also advertises `routes` — the contract route names (`vpn.list`, `kimi.models`) this daemon actually
|
|
404
444
|
// implements, from ITS build of the contract. A browser is routinely newer than the daemon it talks to (a
|
|
405
445
|
// released app plane serves whatever image each user last pulled; in local dev the web app is always ahead of
|
|
406
446
|
// the last `pnpm build:sandbox`), and that stays fully supported — the browser just compares the two sets so a
|
|
407
447
|
// route the daemon predates surfaces as a named, explained gap instead of a bare 404 nobody can attribute.
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
|
|
448
|
+
//
|
|
449
|
+
// Every added field is optional: a daemon built before one simply says nothing, and the browser's fallback is
|
|
450
|
+
// the pre-existing behaviour — routes all assumed present, the daemon assumed ready, the cache left alone.
|
|
451
|
+
export const HelloSchema = z.object({
|
|
452
|
+
kind: z.literal("hello"),
|
|
453
|
+
workspaceId: z.string(),
|
|
454
|
+
routes: z.array(z.string()).optional(),
|
|
455
|
+
build: z.string().optional(),
|
|
456
|
+
boot: BootProgressSchema.optional(),
|
|
457
|
+
});
|
|
411
458
|
export type Hello = z.infer<typeof HelloSchema>;
|
|
412
459
|
|
|
413
460
|
// The FULL discovered repo set (sorted root-relative ids), pushed whenever it changes — a clone, a scaffold,
|
|
@@ -457,12 +504,13 @@ export type Presence = z.infer<typeof PresenceSchema>;
|
|
|
457
504
|
export const AgentsSchema = z.object({ kind: z.literal("agents"), agents: z.array(AgentSummarySchema), rev: z.number() });
|
|
458
505
|
export type Agents = z.infer<typeof AgentsSchema>;
|
|
459
506
|
|
|
460
|
-
// The /events stream union: the hello identity frame, then liveness heartbeats interleaved with
|
|
507
|
+
// The /events stream union: the hello identity frame, then liveness heartbeats interleaved with boot progress,
|
|
461
508
|
// workspace-change batches, repo-set snapshots, and presence + fleet roster snapshots. oRPC validates every
|
|
462
509
|
// yielded frame against this, so all kinds must live here.
|
|
463
510
|
export const SystemEventSchema = z.discriminatedUnion("kind", [
|
|
464
511
|
HelloSchema,
|
|
465
512
|
HeartbeatSchema,
|
|
513
|
+
BootSchema,
|
|
466
514
|
WorkspaceChangedSchema,
|
|
467
515
|
ReposChangedSchema,
|
|
468
516
|
PresenceSchema,
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { claudeContract } from "./contracts/claude.contract.js";
|
|
|
10
10
|
import { codexContract } from "./contracts/codex.contract.js";
|
|
11
11
|
import { draftsContract } from "./contracts/drafts.contract.js";
|
|
12
12
|
import { extensionsContract } from "./contracts/extensions.contract.js";
|
|
13
|
+
import { gateContract } from "./contracts/gate.contract.js";
|
|
13
14
|
import { geminiContract } from "./contracts/gemini.contract.js";
|
|
14
15
|
import { gitContract } from "./contracts/git.contract.js";
|
|
15
16
|
import { grokContract } from "./contracts/grok.contract.js";
|
|
@@ -41,6 +42,7 @@ export { claudeContract } from "./contracts/claude.contract.js";
|
|
|
41
42
|
export { codexContract } from "./contracts/codex.contract.js";
|
|
42
43
|
export { draftsContract } from "./contracts/drafts.contract.js";
|
|
43
44
|
export { extensionsContract } from "./contracts/extensions.contract.js";
|
|
45
|
+
export { gateContract } from "./contracts/gate.contract.js";
|
|
44
46
|
export { geminiContract } from "./contracts/gemini.contract.js";
|
|
45
47
|
export { gitContract } from "./contracts/git.contract.js";
|
|
46
48
|
export { grokContract } from "./contracts/grok.contract.js";
|
|
@@ -92,6 +94,7 @@ export const sandboxContract = {
|
|
|
92
94
|
settings: settingsContract,
|
|
93
95
|
intentic: intenticContract,
|
|
94
96
|
gemini: geminiContract,
|
|
97
|
+
gate: gateContract,
|
|
95
98
|
git: gitContract,
|
|
96
99
|
grok: grokContract,
|
|
97
100
|
kimi: kimiContract,
|
package/src/model-order.test.ts
CHANGED
|
@@ -7,37 +7,42 @@ import { compareCheapestFirst, compareModelIds, compareUnrankedModelIds, familyO
|
|
|
7
7
|
* conversations on whichever id sorted first. */
|
|
8
8
|
|
|
9
9
|
// A Codex catalog exactly as an OpenAI-compatible /v1/models hands it over: alphabetical, i.e. meaningless.
|
|
10
|
-
const CODEX = ["gpt-5.1-codex", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-sol", "gpt-5.6-terra"];
|
|
10
|
+
const CODEX = ["gpt-5.1-codex", "gpt-5.4-mini", "gpt-5.5", "gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"];
|
|
11
11
|
|
|
12
12
|
test("ranks the frontier line above the cheap one and the newest release above its predecessors", () => {
|
|
13
13
|
// The base line (no tier word) leads, newest first; the mini rung sinks under all of it regardless of how
|
|
14
14
|
// recently it shipped — which is the whole decision a user makes in this list.
|
|
15
|
-
expect(CODEX.toSorted(compareModelIds)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.5", "gpt-5.1-codex", "gpt-5.4-mini"]);
|
|
15
|
+
expect(CODEX.toSorted(compareModelIds)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5", "gpt-5.1-codex", "gpt-5.4-mini"]);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
test("
|
|
19
|
-
// Arrival order survives only as the tiebreak between two ids the rule ranks equally (the 5.6 siblings), so
|
|
20
|
-
// an alphabetical registry and a reversed one can no longer disagree about which model the group opens on.
|
|
18
|
+
test("orders a release's named tiers strongest-first whichever order the endpoint listed them in", () => {
|
|
21
19
|
for (const arrival of [CODEX.toSorted(), CODEX.toReversed()]) {
|
|
22
20
|
const ordered = arrival.toSorted(compareModelIds);
|
|
23
21
|
|
|
24
|
-
expect(ordered.slice(0,
|
|
22
|
+
expect(ordered.slice(0, 3)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]);
|
|
25
23
|
expect(ordered.at(-1)).toBe("gpt-5.4-mini");
|
|
26
24
|
}
|
|
27
25
|
});
|
|
28
26
|
|
|
29
|
-
test("
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
test("keeps release-local tiers below the next generation and above the previous one", () => {
|
|
28
|
+
expect(["gpt-5.6-luna", "gpt-5.5", "gpt-5.7", "gpt-5.6-sol"].toSorted(compareModelIds)).toEqual([
|
|
29
|
+
"gpt-5.7",
|
|
30
|
+
"gpt-5.6-sol",
|
|
31
|
+
"gpt-5.6-luna",
|
|
32
|
+
"gpt-5.5",
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("the Codex release-tier order is stable across catalog refreshes", () => {
|
|
33
37
|
const arrivals = [
|
|
34
38
|
["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"],
|
|
35
39
|
["gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.6-sol"],
|
|
36
40
|
["gpt-5.6-luna", "gpt-5.6-sol", "gpt-5.6-terra"],
|
|
37
41
|
];
|
|
38
|
-
const heads = arrivals.map((arrival) => arrival.toSorted(compareUnrankedModelIds)[0]);
|
|
39
42
|
|
|
40
|
-
|
|
43
|
+
for (const arrival of arrivals) {
|
|
44
|
+
expect(arrival.toSorted(compareUnrankedModelIds)).toEqual(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"]);
|
|
45
|
+
}
|
|
41
46
|
// Same rule, so ranking still outranks the tiebreak: the mini rung stays at the tail, under every sibling.
|
|
42
47
|
expect(["gpt-5.4-mini", ...arrivals[0]!].toSorted(compareUnrankedModelIds).at(-1)).toBe("gpt-5.4-mini");
|
|
43
48
|
});
|
|
@@ -68,6 +73,14 @@ test("reads each vendor's tier vocabulary, not just Claude's", () => {
|
|
|
68
73
|
]);
|
|
69
74
|
});
|
|
70
75
|
|
|
76
|
+
test("reads Kimi's k-prefixed generation so K3 leads the K2.x catalog", () => {
|
|
77
|
+
const catalog = ["kimi-k2.6", "kimi-k2.7-code-highspeed", "kimi-k3", "kimi-k2.7-code"];
|
|
78
|
+
|
|
79
|
+
expect(releaseOf("kimi-k3")).toEqual({ version: [3], date: 0 });
|
|
80
|
+
expect(familyOf("kimi-k3")).toBe(familyOf("kimi-k2.6"));
|
|
81
|
+
expect(catalog.toSorted(compareUnrankedModelIds)).toEqual(["kimi-k3", "kimi-k2.7-code", "kimi-k2.7-code-highspeed", "kimi-k2.6"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
71
84
|
test("the rightmost tier word wins, because tier words compose", () => {
|
|
72
85
|
// flash-lite is the cheap end of Flash, codex-max the frontier end of Codex — reading the leftmost word
|
|
73
86
|
// instead would file both under the tier they modify.
|
|
@@ -169,6 +182,10 @@ test("finds each vendor's own cheap rung, including a re-served open-weights row
|
|
|
169
182
|
expect(["grok-4", "grok-4-fast"].toSorted(compareCheapestFirst)[0]).toBe("grok-4-fast");
|
|
170
183
|
});
|
|
171
184
|
|
|
185
|
+
test("reads a release-local tier ladder from the cheap end too", () => {
|
|
186
|
+
expect(["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna"].toSorted(compareCheapestFirst)[0]).toBe("gpt-5.6-luna");
|
|
187
|
+
});
|
|
188
|
+
|
|
172
189
|
test("falls back on the newest release for a catalog that publishes no cheap tier at all", () => {
|
|
173
190
|
// Kimi names no tier word anywhere, so every row is UNRANKED and the tier term cancels. Serving the newest
|
|
174
191
|
// of what it does publish is the honest answer — there is no cheaper rung to find.
|
package/src/model-order.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Anthropic's REST /v1/models answers newest-first: that IS a provider opinion, and Claude's catalog rides it
|
|
5
5
|
* (claude-models.ts). Every other provider here is read through an OpenAI-compatible /v1/models — Codex and
|
|
6
|
-
* Gemini via the bundled translator
|
|
6
|
+
* Gemini and Kimi via the bundled translator — or out of xAI's "Did you mean" rejection, and those
|
|
7
7
|
* endpoints publish a SET, not a ranking: they hand the ids back in whatever order their registry iterates,
|
|
8
8
|
* which in practice is alphabetical. Reading that as a preference is what put "GPT 5.4 Mini" at the head of the
|
|
9
9
|
* Codex group with GPT 5.6 below it, and what made a fresh Codex conversation start on whichever id happened to
|
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
* every vendor names its models the same way — which is what lets the daemon's four catalog services and the
|
|
15
15
|
* web's picker share one rule instead of each inventing a local one. */
|
|
16
16
|
|
|
17
|
-
// A version-ish segment: digits and dots, optionally
|
|
18
|
-
//
|
|
19
|
-
|
|
17
|
+
// A version-ish segment: digits and dots, optionally prefixed by the vendor's version marker (`4`, `5.1`, `v2`,
|
|
18
|
+
// `k3`, `k2.7`, `20251001`). Kimi is the one provider that fuses the marker with the generation; treating `k3`
|
|
19
|
+
// as a name made the current flagship look unversioned, so K2.x sorted above it. Everything else is a NAME
|
|
20
|
+
// segment and belongs to the family — which is what makes the split below exhaustive.
|
|
21
|
+
const VERSION_SEGMENT = /^(?:v|k)?[\d.]+$/i;
|
|
20
22
|
|
|
21
23
|
// A date stamp rather than a version component: six digits or more (20251001, 250514). The distinction is not
|
|
22
24
|
// cosmetic — claude-opus-4-1-20250805 (Opus 4.1) and claude-opus-4-20250514 (Opus 4.0) compare as (4,1) vs (4)
|
|
@@ -48,7 +50,7 @@ export interface ModelRelease {
|
|
|
48
50
|
export const releaseOf = (id: string): ModelRelease => {
|
|
49
51
|
const numeric = segmentsOf(id)
|
|
50
52
|
.filter((segment) => VERSION_SEGMENT.test(segment))
|
|
51
|
-
.map((segment) => segment.replace(/^
|
|
53
|
+
.map((segment) => segment.replace(/^[vk]/i, ""));
|
|
52
54
|
const stamps = numeric.filter((segment) => DATE_SEGMENT.test(segment)).map(Number);
|
|
53
55
|
return {
|
|
54
56
|
version: numeric
|
|
@@ -109,12 +111,21 @@ const TIER_RANK: Readonly<Record<string, number>> = {
|
|
|
109
111
|
|
|
110
112
|
const UNRANKED = -1;
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
/* Some providers name a capability ladder INSIDE one release instead of using the cross-release adjectives
|
|
115
|
+
* above. Codex 5.6's Sol/Terra/Luna rows are that shape: they must remain together ahead of the older 5.5 line,
|
|
116
|
+
* but their order is not an arbitrary id tiebreak — Sol is the strongest, followed by Terra, then Luna. Keeping
|
|
117
|
+
* this as a separate rank lets release recency still win across generations (a future GPT 5.7 base model must
|
|
118
|
+
* not be buried under a recognized 5.6 suffix), while the three siblings sort by their real tier. */
|
|
119
|
+
const RELEASE_TIER_RANK: Readonly<Record<string, number>> = {
|
|
120
|
+
sol: 0,
|
|
121
|
+
terra: 1,
|
|
122
|
+
luna: 2,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const lastRankOf = (family: string, ranks: Readonly<Record<string, number>>): number => {
|
|
115
126
|
let rank = UNRANKED;
|
|
116
127
|
for (const segment of family.split("-")) {
|
|
117
|
-
const found =
|
|
128
|
+
const found = ranks[segment];
|
|
118
129
|
if (found !== undefined) {
|
|
119
130
|
rank = found;
|
|
120
131
|
}
|
|
@@ -122,18 +133,32 @@ export const tierRankOf = (family: string): number => {
|
|
|
122
133
|
return rank;
|
|
123
134
|
};
|
|
124
135
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
const releaseTierRankOf = (family: string): number => lastRankOf(family, RELEASE_TIER_RANK);
|
|
137
|
+
|
|
138
|
+
// The LAST recognized word wins, because tier words compose and the rightmost is the most specific one:
|
|
139
|
+
// gemini-flash-lite is the cheap end of Flash, gpt-codex-max the frontier end of Codex.
|
|
140
|
+
export const tierRankOf = (family: string): number => lastRankOf(family, TIER_RANK);
|
|
141
|
+
|
|
142
|
+
// The canonical order of two model ids: broad tier first, then release, then a tier declared within that release.
|
|
143
|
+
// Hand it straight to Array#toSorted — that sort is stable, so two ids this rule cannot separate keep the order
|
|
144
|
+
// they arrived in (for Claude, the provider's own).
|
|
145
|
+
export const compareModelIds = (left: string, right: string): number => {
|
|
146
|
+
const leftFamily = familyOf(left);
|
|
147
|
+
const rightFamily = familyOf(right);
|
|
148
|
+
return (
|
|
149
|
+
tierRankOf(leftFamily) - tierRankOf(rightFamily) ||
|
|
150
|
+
compareRelease(releaseOf(left), releaseOf(right)) ||
|
|
151
|
+
releaseTierRankOf(leftFamily) - releaseTierRankOf(rightFamily)
|
|
152
|
+
);
|
|
153
|
+
};
|
|
129
154
|
|
|
130
155
|
/* The order for a catalog its endpoint published as a SET — Codex, Gemini, Kimi and Grok, i.e. everything but
|
|
131
156
|
* Anthropic's ranked list. Falling back on arrival order is what the rule above does with a tie, and for a RANKED
|
|
132
157
|
* catalog that is exactly right: the tie is the provider's own opinion, so claude-opus-5 stays ahead of
|
|
133
158
|
* claude-fable-5. For a set there is no opinion to keep, and the header of this file assumed the leftover order
|
|
134
|
-
* was at least alphabetical — it is not. A subscription
|
|
135
|
-
*
|
|
136
|
-
*
|
|
159
|
+
* was at least alphabetical — it is not. A subscription can hand tied rows back in whatever order its registry
|
|
160
|
+
* iterated THIS request, so the tie decides which model a fresh conversation opens on and can flip between
|
|
161
|
+
* catalog refreshes.
|
|
137
162
|
*
|
|
138
163
|
* So a set breaks its own ties on the id. Which sibling that seats first is arbitrary — but it is the same
|
|
139
164
|
* arbitrary answer every refresh, which is the property `default` actually needs. */
|
|
@@ -151,5 +176,12 @@ export const compareUnrankedModelIds = (left: string, right: string): number =>
|
|
|
151
176
|
* not the efficient rung — and the cheap end is only ever a family whose tier word is actually recognized.
|
|
152
177
|
* Falling off the end of a catalog with no efficient tier at all (Kimi publishes none) is then honest: the
|
|
153
178
|
* newest of what it does publish, chosen by the release tiebreak below. */
|
|
154
|
-
export const compareCheapestFirst = (left: string, right: string): number =>
|
|
155
|
-
|
|
179
|
+
export const compareCheapestFirst = (left: string, right: string): number => {
|
|
180
|
+
const leftFamily = familyOf(left);
|
|
181
|
+
const rightFamily = familyOf(right);
|
|
182
|
+
return (
|
|
183
|
+
tierRankOf(rightFamily) - tierRankOf(leftFamily) ||
|
|
184
|
+
compareRelease(releaseOf(left), releaseOf(right)) ||
|
|
185
|
+
releaseTierRankOf(rightFamily) - releaseTierRankOf(leftFamily)
|
|
186
|
+
);
|
|
187
|
+
};
|
package/src/quick-model.test.ts
CHANGED
|
@@ -9,7 +9,7 @@ import { type QuickModelSource, quickModelKey, resolveQuickModel } from "./quick
|
|
|
9
9
|
const CLAUDE: QuickModelSource = { provider: `claude`, ready: true, models: [`claude-opus-5`, `claude-sonnet-5`, `claude-haiku-4-5-20251001`] };
|
|
10
10
|
const GOOGLE: QuickModelSource = { provider: `gemini`, ready: true, models: [`gemini-3-flash`, `gemini-3-flash-lite`, `gemini-3-pro`] };
|
|
11
11
|
const CODEX: QuickModelSource = { provider: `codex`, ready: true, models: [`gpt-5.4-mini`, `gpt-5.6`] };
|
|
12
|
-
const KIMI: QuickModelSource = { provider: `kimi`, ready: true, models: [`kimi-k2
|
|
12
|
+
const KIMI: QuickModelSource = { provider: `kimi`, ready: true, models: [`kimi-k2.6`, `kimi-k2.7-code`, `kimi-k3`] };
|
|
13
13
|
|
|
14
14
|
const offline = (source: QuickModelSource): QuickModelSource => ({ ...source, ready: false });
|
|
15
15
|
|
|
@@ -31,7 +31,7 @@ test("puts tier ahead of cost — a free frontier model is still the wrong tool
|
|
|
31
31
|
expect(resolveQuickModel([CLAUDE, proOnly], ``)).toEqual({ provider: `claude`, model: `claude-haiku-4-5-20251001` });
|
|
32
32
|
});
|
|
33
33
|
|
|
34
|
-
test("
|
|
34
|
+
test("uses stable provider order when two subscriptions offer the same tier", () => {
|
|
35
35
|
const kimiCheap: QuickModelSource = { provider: `kimi`, ready: true, models: [`kimi-k2-mini`] };
|
|
36
36
|
const claudeCheap: QuickModelSource = { provider: `claude`, ready: true, models: [`claude-haiku-4-5`] };
|
|
37
37
|
|
|
@@ -72,7 +72,7 @@ test("ignores a malformed pin instead of running an empty model id", () => {
|
|
|
72
72
|
|
|
73
73
|
test("serves the newest of a catalog that publishes no cheap tier at all", () => {
|
|
74
74
|
// Kimi names no tier word anywhere. There is no cheaper rung to find, so the newest row is the honest answer.
|
|
75
|
-
expect(resolveQuickModel([KIMI], ``)).toEqual({ provider: `kimi`, model: `kimi-
|
|
75
|
+
expect(resolveQuickModel([KIMI], ``)).toEqual({ provider: `kimi`, model: `kimi-k3` });
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
test("reports nothing when no account is connected, so the button can say so instead of failing on click", () => {
|
package/src/schemas.test.ts
CHANGED
|
@@ -21,6 +21,8 @@ test("a payload from a build that predates a toggle parses, with the new toggle
|
|
|
21
21
|
expect(SandboxSettingsSchema.parse(older)).toEqual({
|
|
22
22
|
...older,
|
|
23
23
|
terseHoldout: 0,
|
|
24
|
+
iqContext: false,
|
|
25
|
+
iqContextHoldout: 0,
|
|
24
26
|
filterBackend: "native",
|
|
25
27
|
systemPromptMode: "intentic",
|
|
26
28
|
systemPrompt: "",
|
|
@@ -30,6 +32,10 @@ test("a payload from a build that predates a toggle parses, with the new toggle
|
|
|
30
32
|
autoResumeOnLimit: false,
|
|
31
33
|
resumeAfterOutage: true,
|
|
32
34
|
autoResumeOnRestart: true,
|
|
35
|
+
gateCommand: "",
|
|
36
|
+
gateQuietMs: 20_000,
|
|
37
|
+
gateTimeoutMs: 900_000,
|
|
38
|
+
gateAutoFix: true,
|
|
33
39
|
});
|
|
34
40
|
});
|
|
35
41
|
|
|
@@ -42,6 +48,10 @@ test("an empty object is the full default settings object", () => {
|
|
|
42
48
|
// Off: the steer's turn-level control spends the tokens it measures, so measuring is opt-in.
|
|
43
49
|
terseHoldout: 0,
|
|
44
50
|
iqSearch: false,
|
|
51
|
+
// Off, and its holdout with it: pre-injection spends input tokens on every eligible turn, and the
|
|
52
|
+
// control that would tell you whether they paid for themselves costs the turns it measures.
|
|
53
|
+
iqContext: false,
|
|
54
|
+
iqContextHoldout: 0,
|
|
45
55
|
outputCleaners: "off",
|
|
46
56
|
outputHoldout: 0,
|
|
47
57
|
filterBackend: "native",
|
|
@@ -65,6 +75,12 @@ test("an empty object is the full default settings object", () => {
|
|
|
65
75
|
// On: a daemon restart is usually intentic's own doing (an image update, an approved environment
|
|
66
76
|
// change), not the user's decision, so the turn it interrupted resumes rather than staying stuck.
|
|
67
77
|
autoResumeOnRestart: true,
|
|
78
|
+
// Empty disables the landing gate until the owner supplies this workspace's verification command.
|
|
79
|
+
gateCommand: "",
|
|
80
|
+
gateQuietMs: 20_000,
|
|
81
|
+
gateTimeoutMs: 900_000,
|
|
82
|
+
// Once a gate is configured, a red verdict wakes one fixer by default.
|
|
83
|
+
gateAutoFix: true,
|
|
68
84
|
});
|
|
69
85
|
});
|
|
70
86
|
|
|
@@ -74,3 +90,7 @@ test("a key of the wrong type is still a parse failure — tolerance is for abse
|
|
|
74
90
|
// The prompt cap is a real bound, not advice: the text IS the system prompt, and every turn pays for it.
|
|
75
91
|
expect(SandboxSettingsSchema.safeParse({ systemPrompt: "x".repeat(20001) }).success).toBe(false);
|
|
76
92
|
});
|
|
93
|
+
|
|
94
|
+
test("usage-limit auto-resume stays off while the feature is disabled, including for an older saved true value", () => {
|
|
95
|
+
expect(SandboxSettingsSchema.parse({ autoResumeOnLimit: true }).autoResumeOnLimit).toBe(false);
|
|
96
|
+
});
|