@intentic/sandbox-contract 1.222.0 → 1.224.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/chores/chores.js +1 -1
- package/dist/chores/chores.js.map +1 -1
- package/dist/chores/prompt.d.ts.map +1 -1
- package/dist/chores/prompt.js +3 -3
- package/dist/chores/prompt.js.map +1 -1
- package/dist/contracts/ports.contract.d.ts +11 -0
- package/dist/contracts/ports.contract.d.ts.map +1 -1
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/schemas.d.ts +22 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +3 -0
- package/dist/schemas.js.map +1 -1
- package/package.json +4 -4
- package/src/agent-catalog.ts +78 -78
- package/src/agent-run-model.ts +8 -8
- package/src/capability-env.ts +3 -3
- package/src/capability-secrets.ts +5 -5
- package/src/chores/chores.ts +67 -67
- package/src/chores/digest.ts +3 -3
- package/src/chores/extension-update.ts +3 -3
- package/src/chores/fix-deps.ts +3 -3
- package/src/chores/probes.ts +16 -16
- package/src/chores/prompt.ts +9 -9
- package/src/chores/stack.ts +17 -17
- package/src/chores/verdict.ts +23 -23
- package/src/contract-lock.ts +7 -7
- package/src/contracts/activity.contract.ts +1 -1
- package/src/contracts/agent.contract.ts +2 -2
- package/src/contracts/agents.contract.ts +11 -11
- package/src/contracts/automations.contract.ts +5 -5
- package/src/contracts/capabilities.contract.ts +7 -7
- package/src/contracts/chores.contract.ts +3 -3
- package/src/contracts/ci.contract.ts +2 -2
- package/src/contracts/claude.contract.ts +3 -3
- package/src/contracts/drafts.contract.ts +1 -1
- package/src/contracts/endpoints.contract.ts +2 -2
- package/src/contracts/extensions.contract.ts +8 -8
- package/src/contracts/git.contract.ts +6 -6
- package/src/contracts/grok.contract.ts +4 -4
- package/src/contracts/host.contract.ts +5 -5
- package/src/contracts/intentic.contract.ts +2 -2
- package/src/contracts/logs.contract.ts +1 -1
- package/src/contracts/loops.contract.ts +8 -8
- package/src/contracts/personas.contract.ts +8 -8
- package/src/contracts/ports.contract.ts +1 -1
- package/src/contracts/prepush.contract.ts +2 -2
- package/src/contracts/providers.contract.ts +4 -4
- package/src/contracts/public.contract.ts +1 -1
- package/src/contracts/push.contract.ts +2 -2
- package/src/contracts/secrets.contract.ts +2 -2
- package/src/contracts/sessions.contract.ts +2 -2
- package/src/contracts/settings.contract.ts +3 -3
- package/src/contracts/share.contract.ts +1 -1
- package/src/contracts/skills.contract.ts +2 -2
- package/src/contracts/system.contract.ts +9 -9
- package/src/contracts/usage.contract.ts +1 -1
- package/src/contracts/vpn.contract.ts +5 -5
- package/src/contracts/workflows.contract.ts +12 -12
- package/src/contracts/workspace.contract.ts +13 -13
- package/src/conversation-ids.ts +7 -7
- package/src/events.ts +136 -136
- package/src/history-state.ts +12 -12
- package/src/host-protocol.ts +7 -7
- package/src/hostnames.ts +15 -15
- package/src/index.ts +8 -8
- package/src/listener-protocol.ts +12 -12
- package/src/model-order.ts +30 -30
- package/src/output-fields.ts +9 -9
- package/src/path-refs.ts +6 -6
- package/src/publish-drafts.ts +6 -6
- package/src/quick-model.ts +15 -15
- package/src/routes.ts +19 -19
- package/src/runtime-state.ts +22 -22
- package/src/schemas.ts +930 -918
- package/src/search-globs.ts +6 -6
- package/src/session-names.ts +5 -5
- package/src/share-paths.ts +7 -7
- package/src/sse.ts +1 -1
- package/src/state-portability.ts +8 -8
- package/src/terminal-protocol.ts +3 -3
- package/src/title.ts +20 -20
- package/src/tunnel-ids.ts +7 -7
- package/src/versions.ts +5 -5
- package/src/workflow-faults.ts +11 -11
- package/src/workspace-state.ts +119 -119
package/src/history-state.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { StateFile } from "./state-portability.js";
|
|
2
2
|
|
|
3
|
-
/* WHAT LIVES ON /history
|
|
3
|
+
/* WHAT LIVES ON /history, the second half of the daemon's state, and the half nothing declared until an
|
|
4
4
|
* export had to reason about it.
|
|
5
5
|
*
|
|
6
6
|
* `WORKSPACE_STATE_FILES` covers `<workspace>/.intentic/`, which is where the manifests live. It is not where
|
|
7
|
-
* the machinery lives. Every repo's REAL git dir is here (a repo's in-tree `.git` is a pointer file
|
|
7
|
+
* the machinery lives. Every repo's REAL git dir is here (a repo's in-tree `.git` is a pointer file, see
|
|
8
8
|
* git/repo-git-dirs.ts for the invariant that forces it), and so are the fleet registry, the turn journal, the
|
|
9
9
|
* ledgers, the checkpoint scopes and the isolated agents' checkouts. A "workspace export" that took `/work`
|
|
10
|
-
* alone would carry a tree of repos with dangling gitdir pointers
|
|
11
|
-
* answering `fatal: not a git repository
|
|
10
|
+
* alone would carry a tree of repos with dangling gitdir pointers, every git command in the restored sandbox
|
|
11
|
+
* answering `fatal: not a git repository`, and an empty agent board.
|
|
12
12
|
*
|
|
13
13
|
* The two tables stay separate rather than becoming one keyed by volume, because they answer different
|
|
14
14
|
* questions. A `.intentic` entry also declares which browser QUERY it makes stale, since the file watcher
|
|
@@ -17,7 +17,7 @@ import type { StateFile } from "./state-portability.js";
|
|
|
17
17
|
* imported rather than duplicated.
|
|
18
18
|
*
|
|
19
19
|
* `history-state-coverage.test.ts` fails when a daemon store builds a `/history` path this list doesn't carry, in both
|
|
20
|
-
* directions
|
|
20
|
+
* directions, the same shape-recognizing guard that covers the workspace table.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
// Paths are historyRoot-relative, forward-slash, matched by PREFIX; a directory entry keeps its trailing slash
|
|
@@ -27,7 +27,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
27
27
|
|
|
28
28
|
/* THE ONE THAT MAKES A BUNDLE A WORKSPACE. Every repo's real git dir, including the /work root's own
|
|
29
29
|
* ("root"), keyed by URI-encoded repo id. Carrying the working tree without this hands the target files
|
|
30
|
-
* whose `.git` points at a path that does not exist there
|
|
30
|
+
* whose `.git` points at a path that does not exist there, which is not a degraded repo but a broken one,
|
|
31
31
|
* and it takes the Changes review, the diff, land and every agent branch with it. The agent BRANCHES live
|
|
32
32
|
* in here too, which is what lets the checkouts below be left out. */
|
|
33
33
|
{ path: "gits/", portability: "carry" },
|
|
@@ -38,7 +38,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
38
38
|
{ path: "agents.json", portability: "carry" },
|
|
39
39
|
{ path: "turns/", portability: "carry" },
|
|
40
40
|
{ path: "transcripts/", portability: "carry" },
|
|
41
|
-
// What each message can be put back to
|
|
41
|
+
// What each message can be put back to, a workspace checkpoint, or an isolated conversation's own commits.
|
|
42
42
|
// Carried WITH the transcripts and the scopes above, because it is the join between them: without it a
|
|
43
43
|
// restored conversation reads back whole and offers no way back into it, even though both the messages and
|
|
44
44
|
// the states they name travelled.
|
|
@@ -48,7 +48,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
48
48
|
* Carried, and it is the entry with the most to say for itself: the PAGES live in the workspace's outbox
|
|
49
49
|
* (`public/`), so they travel with `/work` whatever this says. Leaving the index behind would restore a
|
|
50
50
|
* sandbox that is still serving somebody's conversation on the internet with nothing in the app that knows
|
|
51
|
-
* it
|
|
51
|
+
* it, no row, no link, and no way to stop sharing short of deleting files by hand. The index is what makes
|
|
52
52
|
* a published page withdrawable, so it goes wherever the pages go. */
|
|
53
53
|
{ path: "shares.json", portability: "carry" },
|
|
54
54
|
{ path: "activity.jsonl", portability: "carry" },
|
|
@@ -58,7 +58,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
58
58
|
// Explicit first-time dependency setup requests. Carrying the worklist preserves the owner's decision when
|
|
59
59
|
// an export interrupts the queue before its terminal starts; fulfilled entries remove themselves.
|
|
60
60
|
{ path: "dependency-requests.json", portability: "carry" },
|
|
61
|
-
// The deploy engine's own ledgers
|
|
61
|
+
// The deploy engine's own ledgers, a run's events and the check results the Pipelines view reads back.
|
|
62
62
|
{ path: "apply-events.ndjson", portability: "carry" },
|
|
63
63
|
{ path: "check-events/", portability: "carry" },
|
|
64
64
|
|
|
@@ -68,7 +68,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
68
68
|
*
|
|
69
69
|
* A conversation's worktree is a full checkout of the monorepo per agent (plus its overlay upper dir), and
|
|
70
70
|
* there can be a hundred of them. None of it is unique: the branch it holds is in `gits/` above, and the
|
|
71
|
-
* registry entry naming it travels in agents.json
|
|
71
|
+
* registry entry naming it travels in agents.json, so an imported conversation arrives in exactly the
|
|
72
72
|
* shape the system already has a name for. `attached()` reports its checkout as absent, the board renders
|
|
73
73
|
* it, and the next turn's `ensure()` re-creates it from the recorded composition, which is the same path an
|
|
74
74
|
* archived agent takes when it runs again. The boot sweep's `git worktree prune` clears the stale admin
|
|
@@ -84,7 +84,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
84
84
|
{ path: ".isolation-probe", portability: "derived" },
|
|
85
85
|
/* The finished bundles themselves. `derived` is doing real work here rather than describing leftovers: an
|
|
86
86
|
* export that carried the export directory would pack every previous bundle into the new one, and the next
|
|
87
|
-
* export would pack THAT
|
|
87
|
+
* export would pack THAT, each one a multiple of the last. Living on this volume is the other half of the
|
|
88
88
|
* same guard; under `/work` the file would also be watched, indexed by iq, and snapshotted into history. */
|
|
89
89
|
{ path: "exports/", portability: "derived" },
|
|
90
90
|
|
|
@@ -107,7 +107,7 @@ export const HISTORY_STATE_FILES: readonly StateFile[] = [
|
|
|
107
107
|
/* ---- identity: what binds this sandbox to its owner, its browsers and its host ---- */
|
|
108
108
|
|
|
109
109
|
/* Signs every browser session cookie. Carrying it would let a bundle's holder mint sessions against the
|
|
110
|
-
* target
|
|
110
|
+
* target, an export becomes a credential, and the target minting its own costs exactly one sign-in. */
|
|
111
111
|
{ path: "session-secret", portability: "identity", note: "Sign in again — the target signs its own sessions." },
|
|
112
112
|
{
|
|
113
113
|
path: "browser-access-disabled",
|
package/src/host-protocol.ts
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
-
/* The handshake on /system/hosts/connect
|
|
3
|
+
/* The handshake on /system/hosts/connect, the ONE message that is not oRPC.
|
|
4
4
|
*
|
|
5
5
|
* Everything a connected computer is asked lives in `hostContract` (contracts/host.contract.ts), spoken over
|
|
6
6
|
* this socket by oRPC's websocket adapter: the machine hosts the server, the daemon holds the client. But a
|
|
7
7
|
* socket has to prove whose it is before it can be given a typed client, and that proof cannot itself be an
|
|
8
|
-
* oRPC call
|
|
8
|
+
* oRPC call, the daemon has nothing to call yet, and would be attaching a link to a stranger.
|
|
9
9
|
*
|
|
10
10
|
* So the machine's first act is this frame, in plain JSON. The daemon verifies the token, learns which
|
|
11
11
|
* capability the socket belongs to, and only then attaches the link; from that message on, every byte on the
|
|
12
12
|
* wire is oRPC. Anything arriving before the link exists is either this frame or a closed socket. */
|
|
13
13
|
|
|
14
14
|
// The MCP protocol revision the machine's tool server implements. Shared because the daemon answers the
|
|
15
|
-
// handshake ITSELF when the machine is asleep (hosts/host.routes.ts)
|
|
15
|
+
// handshake ITSELF when the machine is asleep (hosts/host.routes.ts), two spellings of this would mean an
|
|
16
16
|
// offline machine negotiating a different protocol than the same machine awake.
|
|
17
17
|
export const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
18
18
|
|
|
19
19
|
export const HostHelloSchema = z.object({
|
|
20
20
|
type: z.literal("hello"),
|
|
21
|
-
/* The machine's enrollment token
|
|
21
|
+
/* The machine's enrollment token, in the FIRST FRAME, never in the URL. A WebSocket has no headers to put
|
|
22
22
|
* it in, and the obvious `?token=` would write a durable key to somebody's laptop into Cloudflare's edge
|
|
23
23
|
* logs, the connector's logs and every proxy in between (the reasoning that moved the browser's upgrades
|
|
24
|
-
* onto one-shot tickets
|
|
24
|
+
* onto one-shot tickets, auth/ws-tickets.ts). A frame is body, not URL, so it is logged nowhere. Until this
|
|
25
25
|
* arrives the socket is anonymous and short-lived: the daemon closes it in seconds if it never does. */
|
|
26
26
|
token: z.string(),
|
|
27
|
-
// The @intentic/host build the machine is running
|
|
27
|
+
// The @intentic/host build the machine is running, surfaced per machine so an old binary is visible rather
|
|
28
28
|
// than mysteriously missing a tool. What the machine IS (`describe`) is not here: it is pulled over the
|
|
29
29
|
// typed link a moment later, so there is one definition of those facts rather than two.
|
|
30
30
|
version: z.string(),
|
|
31
31
|
});
|
|
32
32
|
export type HostHello = z.infer<typeof HostHelloSchema>;
|
|
33
33
|
|
|
34
|
-
// The URL the machine's agent dials, given the sandbox's public URL. Carries no credential
|
|
34
|
+
// The URL the machine's agent dials, given the sandbox's public URL. Carries no credential, the token rides
|
|
35
35
|
// the hello frame. One place builds it, so the agent and the daemon route can't disagree about where it lives.
|
|
36
36
|
export const hostConnectUrl = (sandboxUrl: string): string => `${sandboxUrl.replace(/^http/, "ws").replace(/\/$/, "")}/system/hosts/connect`;
|
package/src/hostnames.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The single source for the tunnel/preview hostname + ingress scheme, shared by the CLI, platform API, daemon,
|
|
2
2
|
// AND the browser. Pure string builders/parsers with NO node imports (unlike ./tunnel-ids, which needs
|
|
3
|
-
// node:crypto for the digest)
|
|
3
|
+
// node:crypto for the digest), so the web bundle can import them and derive identical names. The caller supplies
|
|
4
4
|
// the 12-hex id (sandboxIdFromToken/hostSshIdFromToken in node; WebCrypto in the browser).
|
|
5
5
|
//
|
|
6
6
|
// All four apps MUST agree on these strings: a divergence resolves to NXDOMAIN that resolvers negative-cache for
|
|
@@ -18,7 +18,7 @@ export const sshHostname = (id: string, zone: string): string => `ssh-${id}.${zo
|
|
|
18
18
|
*
|
|
19
19
|
* A public DNS name for a private address looks odd until you ask what the alternative is. A browser on the
|
|
20
20
|
* same machine as the sandbox can reach its daemon in microseconds instead of crossing to a Cloudflare edge
|
|
21
|
-
* and back
|
|
21
|
+
* and back, but only over HTTPS, because Safari refuses http://127.0.0.1 from an HTTPS page as mixed content
|
|
22
22
|
* (WebKit 171934, open since 2017), and HTTPS needs a name a public CA will certify. An IP literal cannot have
|
|
23
23
|
* one; this can. The daemon holds the key and gets the certificate by proving control of the zone over
|
|
24
24
|
* DNS-01 (there is nothing on the public internet for a CA to connect to).
|
|
@@ -36,36 +36,36 @@ export const hostSshTunnelName = (id: string): string => `host-ssh-${id}`;
|
|
|
36
36
|
// The proxied-CNAME target every tunnel points its DNS record at.
|
|
37
37
|
export const cfargotunnelCname = (tunnelId: string): string => `${tunnelId}.cfargotunnel.com`;
|
|
38
38
|
|
|
39
|
-
// The cloudflared ingress catch-all
|
|
39
|
+
// The cloudflared ingress catch-all, must be the LAST rule.
|
|
40
40
|
export const CATCH_ALL = { service: "http_status:404" } as const;
|
|
41
41
|
|
|
42
|
-
// Preview scheme: `preview-<panel>-<sandboxId>.<zone
|
|
42
|
+
// Preview scheme: `preview-<panel>-<sandboxId>.<zone>`, one DNS label (the free Universal SSL `*.<zone>` cert
|
|
43
43
|
// covers exactly one level), where <panel> is `<repo>` or `<repo>--<app>` and <sandboxId> pins the hostname to
|
|
44
44
|
// this sandbox (the shared intentic zone hosts many sandboxes; without the id two users' panels would collide).
|
|
45
|
-
// Port-forward scheme: `port-<slot>-<sandboxId>.<zone
|
|
45
|
+
// Port-forward scheme: `port-<slot>-<sandboxId>.<zone>`, the same shape with a `port-` prefix, where <slot>
|
|
46
46
|
// is one of the sandbox's forward slots (portSlotsFromToken in ./tunnel-ids), not the port number itself:
|
|
47
47
|
// slots keep the intentic-provided path's minted routes bounded and warm while dev servers churn ephemeral
|
|
48
48
|
// ports. The slot labels are salted with the connect token rather than being the letters a…h, so a forwarded
|
|
49
|
-
// port's hostname is not derivable from the (public) sandbox id alone
|
|
49
|
+
// port's hostname is not derivable from the (public) sandbox id alone, see tunnel-ids for why that matters.
|
|
50
50
|
//
|
|
51
|
-
// Outbox scheme: `public-<slot>-<sandboxId>.<zone
|
|
51
|
+
// Outbox scheme: `public-<slot>-<sandboxId>.<zone>`, the same shape again, serving the workspace's `public/`
|
|
52
52
|
// directory as static files. <slot> is publicSlotFromToken (./tunnel-ids), salted for the same reason the port
|
|
53
53
|
// slots are, and one record per sandbox rather than a pool: there is one outbox, and its link has to stay good
|
|
54
54
|
// for as long as the file does.
|
|
55
55
|
//
|
|
56
56
|
// A *label* is the first-DNS-label prefix before `-<sandboxId>` (`preview-<panel>` / `port-<slot>` /
|
|
57
|
-
// `public-<slot>`)
|
|
57
|
+
// `public-<slot>`), the unit the platform's /sandbox/preview-route mints, so one endpoint serves all three.
|
|
58
58
|
export const previewLabel = (panel: string): string => `preview-${panel}`;
|
|
59
59
|
export const portLabel = (slot: string): string => `port-${slot}`;
|
|
60
60
|
export const publicLabel = (slot: string): string => `public-${slot}`;
|
|
61
61
|
|
|
62
|
-
// The hostname a label resolves to
|
|
62
|
+
// The hostname a label resolves to, what the platform's /sandbox/preview-route mints from the label alone.
|
|
63
63
|
export const labelHostname = (label: string, id: string, zone: string): string => `${label}-${id}.${zone}`;
|
|
64
64
|
export const previewHostname = (panel: string, id: string, zone: string): string => labelHostname(previewLabel(panel), id, zone);
|
|
65
65
|
export const portHostname = (slot: string, id: string, zone: string): string => labelHostname(portLabel(slot), id, zone);
|
|
66
66
|
export const publicHostname = (slot: string, id: string, zone: string): string => labelHostname(publicLabel(slot), id, zone);
|
|
67
67
|
|
|
68
|
-
// A label's public URL
|
|
68
|
+
// A label's public URL, undefined unless the sandbox has both a zone and an id (headless/loopback sandboxes
|
|
69
69
|
// have neither and advertise nothing). One builder, three vocabularies: a panel's preview, a forwarded port's,
|
|
70
70
|
// and the outbox's.
|
|
71
71
|
const labelUrl = (label: string, zone: string | undefined, sandboxId: string | undefined): string | undefined =>
|
|
@@ -79,7 +79,7 @@ export const publicUrl = (slot: string, zone: string | undefined, sandboxId: str
|
|
|
79
79
|
|
|
80
80
|
// The key after `<prefix>` from a request's Host header. The first DNS label must carry the prefix (the
|
|
81
81
|
// own-Cloudflare wildcard also catches stray subdomains → undefined → the caller's 404) and, when the sandbox
|
|
82
|
-
// has an id, the exact `-<sandboxId>` suffix
|
|
82
|
+
// has an id, the exact `-<sandboxId>` suffix, a fixed-length match, so keys containing `-` stay unambiguous.
|
|
83
83
|
// Without an id the bare label is the key (loopback tests and provider-deployed workspaces, which front the
|
|
84
84
|
// proxy themselves).
|
|
85
85
|
const keyFromHost = (prefix: string, hostHeader: string | undefined, sandboxId: string | undefined): string | undefined => {
|
|
@@ -103,7 +103,7 @@ export const publicSlotFromHost = (hostHeader: string | undefined, sandboxId: st
|
|
|
103
103
|
keyFromHost("public-", hostHeader, sandboxId);
|
|
104
104
|
|
|
105
105
|
// The sandbox's identity AS THE USER SEES IT: the leading DNS label of its public URL, minus the `sandbox-`
|
|
106
|
-
// prefix
|
|
106
|
+
// prefix, `https://sandbox-0f310c3c4db4.intentic.dev` → `0f310c3c4db4`, i.e. sandboxIdFromToken's digest read
|
|
107
107
|
// back off the wire by anyone holding only the URL. On the own-Cloudflare path the label is whatever subdomain
|
|
108
108
|
// the owner chose, so that is the id there. undefined until the sandbox has a URL at all.
|
|
109
109
|
export const sandboxIdFromUrl = (url: string | undefined): string | undefined => {
|
|
@@ -124,8 +124,8 @@ export const sandboxIdFromUrl = (url: string | undefined): string | undefined =>
|
|
|
124
124
|
};
|
|
125
125
|
|
|
126
126
|
// The LOCAL folder desktop sync mirrors /work into: `~/intentic/<sandbox name>-<sandboxIdFromUrl>`. Both halves
|
|
127
|
-
// are strings the user already has in front of them
|
|
128
|
-
// bar
|
|
127
|
+
// are strings the user already has in front of them, the name in the sandbox switcher, the id in the address
|
|
128
|
+
// bar, so the folder on disk and the sandbox it mirrors read as ONE identity: `~/intentic/shop-0f310c3c4db4`
|
|
129
129
|
// belongs to `https://sandbox-0f310c3c4db4.intentic.dev` and nothing else. Keyed on the URL rather than the
|
|
130
130
|
// name alone for the same reason the hostname is: a torn-down sandbox recreated under the same name gets a new
|
|
131
131
|
// id, hence its own fresh folder instead of reusing the dead one's (which cleanup never deletes) and colliding
|
|
@@ -142,7 +142,7 @@ export const syncFolder = (name: string, url: string | undefined): string => {
|
|
|
142
142
|
|
|
143
143
|
// The Cloudflare zone from a sandbox public URL (https://sandbox-<id>.<zone> → <zone>): the hostname minus its
|
|
144
144
|
// first DNS label. undefined when the URL is unparsable OR the hostname has fewer than three labels (no zone
|
|
145
|
-
// suffix to strip
|
|
145
|
+
// suffix to strip, e.g. a 2-label host would otherwise yield a bare TLD). Accepts scheme-less input too, so it
|
|
146
146
|
// works whether the caller passes `https://…` (daemon/CLI) or a bare host. This is the single reconciled
|
|
147
147
|
// implementation of what used to be the daemon's `zoneFromPublicUrl` and the web's `zoneFromDaemonUrl`.
|
|
148
148
|
export const zoneFromUrl = (url: string | undefined): string | undefined => {
|
package/src/index.ts
CHANGED
|
@@ -53,7 +53,7 @@ export { gitContract } from "./contracts/git.contract.js";
|
|
|
53
53
|
export { grokContract } from "./contracts/grok.contract.js";
|
|
54
54
|
export { historyContract } from "./contracts/history.contract.js";
|
|
55
55
|
/* Deliberately NOT part of `sandboxContract` below: that map is the daemon's own HTTP surface, and this one is
|
|
56
|
-
* spoken the other way round
|
|
56
|
+
* spoken the other way round, over a connected computer's WebSocket, with the MACHINE implementing it. */
|
|
57
57
|
export { hostContract } from "./contracts/host.contract.js";
|
|
58
58
|
export { intenticContract } from "./contracts/intentic.contract.js";
|
|
59
59
|
export { inventoryContract } from "./contracts/inventory.contract.js";
|
|
@@ -82,13 +82,13 @@ export * from "./routes.js";
|
|
|
82
82
|
/* THE CONTAINER'S FIXED DIRECTORY LAYOUT, re-exported so extensions can reach it.
|
|
83
83
|
*
|
|
84
84
|
* The names are defined once in @intentic/constants, which sits at the bottom of the dependency graph. An
|
|
85
|
-
* EXTENSION may not import that package
|
|
85
|
+
* EXTENSION may not import that package, the boundary rule (.oxlintrc.json, _extensions/README.md) allows
|
|
86
86
|
* only the SDK halves and this contract, so that an extension cannot couple itself to app or engine internals.
|
|
87
87
|
* That rule is right, and it left extensions with no way to name the workspace root except by spelling it.
|
|
88
88
|
*
|
|
89
89
|
* Re-exporting here is what closes that gap without widening the boundary: the layout is exactly the kind of
|
|
90
|
-
* thing this package already carries
|
|
91
|
-
* state-file table below
|
|
90
|
+
* thing this package already carries, shared vocabulary both sides of the wire must agree on, alongside the
|
|
91
|
+
* state-file table below, and there is still one definition, in one place, that everything resolves to. */
|
|
92
92
|
export { HISTORY_ROOT, HOST_STATE_ROOT, STATE_DIR, WORKSPACE_ROOT } from "@intentic/constants";
|
|
93
93
|
export * from "./workspace-state.js";
|
|
94
94
|
export * from "./runtime-state.js";
|
|
@@ -114,7 +114,7 @@ export * from "./title.js";
|
|
|
114
114
|
export * from "./versions.js";
|
|
115
115
|
export * from "./workflow-faults.js";
|
|
116
116
|
|
|
117
|
-
// The aggregated contract
|
|
117
|
+
// The aggregated contract, implemented on the server by the per-domain route factories and consumed by the
|
|
118
118
|
// browser's typed oRPC client (ContractRouterClient<typeof sandboxContract>). The wire paths it declares are
|
|
119
119
|
// mounted at the sandbox root, so /health and /workspace/raw (plain Hono routes) sit alongside it.
|
|
120
120
|
export const sandboxContract = {
|
|
@@ -157,14 +157,14 @@ export const sandboxContract = {
|
|
|
157
157
|
};
|
|
158
158
|
|
|
159
159
|
// Every route in THIS build of the contract, and the names the daemon advertises on its hello frame. Bound here
|
|
160
|
-
// rather than in routes.ts so that module stays a pure function of whatever contract it is handed
|
|
160
|
+
// rather than in routes.ts so that module stays a pure function of whatever contract it is handed, importing
|
|
161
161
|
// `sandboxContract` from there would close a load-time cycle back through this file. See routes.ts for why a
|
|
162
162
|
// daemon advertises its route surface at all.
|
|
163
163
|
export const SANDBOX_ROUTES: readonly ContractRoute[] = contractRoutes(sandboxContract);
|
|
164
164
|
export const SANDBOX_ROUTE_NAMES: readonly string[] = SANDBOX_ROUTES.map((route) => route.name);
|
|
165
165
|
|
|
166
166
|
/* And the SHAPE of each of those routes, advertised beside the names for the failure the names cannot describe:
|
|
167
|
-
* a route both builds have, answering a payload only one of them expects. Computed once at module load
|
|
167
|
+
* a route both builds have, answering a payload only one of them expects. Computed once at module load, it
|
|
168
168
|
* walks every contract schema through `z.toJSONSchema`, which is far too much work to repeat per connection and
|
|
169
169
|
* exactly the kind of thing that never changes for the life of a process. See routes.ts. */
|
|
170
170
|
export const SANDBOX_ROUTE_SHAPES: Readonly<Record<string, string>> = routeShapes(sandboxContract);
|
|
@@ -174,7 +174,7 @@ export const sandboxRouteName = (method: string, pathWithQuery: string): string
|
|
|
174
174
|
routeNameForRequest(SANDBOX_ROUTES, method, pathWithQuery);
|
|
175
175
|
|
|
176
176
|
// The method and concrete path a TYPED call is about to put on the wire, bound to this build's route table.
|
|
177
|
-
// Undefined when the procedure is not one this contract declares, which a typed caller cannot reach
|
|
177
|
+
// Undefined when the procedure is not one this contract declares, which a typed caller cannot reach, the host
|
|
178
178
|
// gate treats it as a refusal rather than assuming it is harmless.
|
|
179
179
|
export const sandboxRequestFor = (procedure: readonly string[], input: unknown): { method: string; path: string } | undefined => {
|
|
180
180
|
const route = routeForProcedure(SANDBOX_ROUTES, procedure);
|
package/src/listener-protocol.ts
CHANGED
|
@@ -3,14 +3,14 @@ import { ActivityStatusSchema } from "./schemas.js";
|
|
|
3
3
|
|
|
4
4
|
/* The wire between the daemon and an extension's realtime-listener GATEWAY process (ext-discord, ext-slack,
|
|
5
5
|
* ext-telegram, ext-whatsapp, ext-imap): the four provider-scoped routes app.ts mounts under
|
|
6
|
-
* /listeners/:provider
|
|
6
|
+
* /listeners/:provider, state, dispatch, failure, status. These shapes used to live daemon-side only, with
|
|
7
7
|
* every gateway hand-writing its own copy of the payloads as untyped literals; a field rename broke five
|
|
8
|
-
* producers silently. They live in the contract now so BOTH ends compile against one declaration
|
|
8
|
+
* producers silently. They live in the contract now so BOTH ends compile against one declaration, the daemon
|
|
9
9
|
* parses with the schemas, the gateways (via @intentic/connector-runtime) type against the inferred types. */
|
|
10
10
|
|
|
11
|
-
// One normalized inbound event
|
|
11
|
+
// One normalized inbound event, serialized as a JSON line in the automation's payload, and the JSON body a
|
|
12
12
|
// realtime source POSTs to /listeners/<provider>/dispatch. A zod schema (not a bare interface) because it's
|
|
13
|
-
// parsed from an extension gateway's request; `provider` and `type` are open strings
|
|
13
|
+
// parsed from an extension gateway's request; `provider` and `type` are open strings, the source is
|
|
14
14
|
// extension-declared (contributes.listener), not a core enum.
|
|
15
15
|
export const ListenerMessageSchema = z.object({
|
|
16
16
|
provider: z.string().min(1),
|
|
@@ -22,7 +22,7 @@ export const ListenerMessageSchema = z.object({
|
|
|
22
22
|
// Discord message: it @mentions one of our bots or replies to a bot's message. Voice events never set it.
|
|
23
23
|
mentioned: z.boolean().optional(),
|
|
24
24
|
// CI pipeline event: the ref it ran on. Top-level rather than inside `extra` for the same reason
|
|
25
|
-
// `mentioned` is
|
|
25
|
+
// `mentioned` is, the dispatcher MATCHES on it, and a narrowing axis the trigger can name has to be
|
|
26
26
|
// a field of the message rather than a key in a provider's opaque bag.
|
|
27
27
|
branch: z.string().optional(),
|
|
28
28
|
// Prior channel messages (chronological) fetched when a bot is tagged, so the agent can reason about why.
|
|
@@ -45,13 +45,13 @@ export const ListenerMessageSchema = z.object({
|
|
|
45
45
|
});
|
|
46
46
|
export type ListenerMessage = z.infer<typeof ListenerMessageSchema>;
|
|
47
47
|
|
|
48
|
-
// One ndjson frame of a /listeners/<provider>/dispatch?stream=1 response
|
|
48
|
+
// One ndjson frame of a /listeners/<provider>/dispatch?stream=1 response, a text delta for one automation's
|
|
49
49
|
// reply, the provider's own failure sentence, or that automation's terminal marker. A type, not a schema: the
|
|
50
50
|
// DAEMON produces these (listener.routes.ts), so nothing parses them from untrusted input.
|
|
51
51
|
export interface ListenerDispatchFrame {
|
|
52
52
|
readonly automationId: string;
|
|
53
53
|
readonly delta?: string;
|
|
54
|
-
// The turn refused or broke, in the provider's own words
|
|
54
|
+
// The turn refused or broke, in the provider's own words, forwarded verbatim because a gateway delivers
|
|
55
55
|
// into the owner's own channel, where the actual sentence is the useful thing.
|
|
56
56
|
readonly failed?: string;
|
|
57
57
|
readonly end?: boolean;
|
|
@@ -61,16 +61,16 @@ export interface ListenerDispatchFrame {
|
|
|
61
61
|
*
|
|
62
62
|
* A CODE IS NOT THE ONLY THING WORTH SAYING, and publishing only codes is what made a phone that had never
|
|
63
63
|
* linked read as connected: the seconds before the first code, a gateway that just restarted, and a number
|
|
64
|
-
* WhatsApp refused all looked identical from the daemon's side
|
|
64
|
+
* WhatsApp refused all looked identical from the daemon's side, an absent code, so the card fell through to
|
|
65
65
|
* "ready" and the owner was sent away from the one screen that could have shown them the step. Each of those is
|
|
66
66
|
* its own state here, and every one of them means NOT PAIRED YET.
|
|
67
67
|
*
|
|
68
68
|
* `since` stamps the CURRENT code. WhatsApp closes an unpaired socket after a minute or so and each reopen mints
|
|
69
|
-
* a fresh code, so a code is a thing with an age
|
|
69
|
+
* a fresh code, so a code is a thing with an age, the card says how old the one on screen is rather than
|
|
70
70
|
* leaving the owner to type a dead one twice. */
|
|
71
71
|
export const ListenerPairingSchema = z.object({
|
|
72
72
|
// waiting: a socket is up and the code hasn't arrived (or the last one died with its socket).
|
|
73
|
-
// code: `code` is live
|
|
73
|
+
// code: `code` is live, type it on the phone. failed: `detail` says what WhatsApp refused.
|
|
74
74
|
state: z.enum(["waiting", "code", "failed"]),
|
|
75
75
|
code: z.string().optional(),
|
|
76
76
|
detail: z.string().optional(),
|
|
@@ -79,11 +79,11 @@ export const ListenerPairingSchema = z.object({
|
|
|
79
79
|
export type ListenerPairing = z.infer<typeof ListenerPairingSchema>;
|
|
80
80
|
|
|
81
81
|
// Push-based listener status: a gateway process POSTs its live connection/voice snapshot to
|
|
82
|
-
// /listeners/<provider>/status, and the activity route reads it from there
|
|
82
|
+
// /listeners/<provider>/status, and the activity route reads it from there, the daemon holds no provider
|
|
83
83
|
// connection of its own to probe. The body IS the ActivityStatus the /activity/status probe used to build from
|
|
84
84
|
// in-process discord singletons, plus the per-gateway extras that ride the same channel: whether whisper is
|
|
85
85
|
// present (discord's voice-pending signal) and each unpaired capability's ceremony by id (whatsapp's
|
|
86
|
-
// link-a-device flow
|
|
86
|
+
// link-a-device flow, the capability card renders it as the step the owner is standing in front of).
|
|
87
87
|
export const ListenerStatusSchema = ActivityStatusSchema.extend({
|
|
88
88
|
whisperReady: z.boolean().optional(),
|
|
89
89
|
pairing: z.record(z.string(), ListenerPairingSchema).optional(),
|
package/src/model-order.ts
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
/* HOW A MODEL CATALOG IS ORDERED
|
|
1
|
+
/* HOW A MODEL CATALOG IS ORDERED, one rule for every provider, because only one provider publishes an order
|
|
2
2
|
* worth keeping.
|
|
3
3
|
*
|
|
4
4
|
* Anthropic's REST /v1/models answers newest-first: that IS a provider opinion, and Claude's catalog rides it
|
|
5
|
-
* (claude-models.ts). Every other provider here is read through an OpenAI-compatible /v1/models
|
|
6
|
-
* Gemini and Kimi via the bundled translator
|
|
5
|
+
* (claude-models.ts). Every other provider here is read through an OpenAI-compatible /v1/models. Codex and
|
|
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
|
|
10
|
-
* sort first
|
|
10
|
+
* sort first, models[0] is the provider default.
|
|
11
11
|
*
|
|
12
12
|
* So for those providers the order is DERIVED from the id, out of the only two facts an id reliably carries:
|
|
13
|
-
* which TIER the model is (the adjective) and which RELEASE it is (the numbers). Both are provider-agnostic
|
|
14
|
-
* every vendor names its models the same way
|
|
13
|
+
* which TIER the model is (the adjective) and which RELEASE it is (the numbers). Both are provider-agnostic,
|
|
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
17
|
// A version-ish segment: digits and dots, optionally prefixed by the vendor's version marker (`4`, `5.1`, `v2`,
|
|
18
18
|
// `k3`, `k2.7`, `20251001`). Kimi is the one provider that fuses the marker with the generation; treating `k3`
|
|
19
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
|
|
20
|
+
// segment and belongs to the family, which is what makes the split below exhaustive.
|
|
21
21
|
const VERSION_SEGMENT = /^(?:v|k)?[\d.]+$/i;
|
|
22
22
|
|
|
23
23
|
// A date stamp rather than a version component: six digits or more (20251001, 250514). The distinction is not
|
|
24
|
-
// cosmetic
|
|
25
|
-
// with the stamps held apart, and as (4,1,20250805) vs (4,20250514)
|
|
24
|
+
// cosmetic, claude-opus-4-1-20250805 (Opus 4.1) and claude-opus-4-20250514 (Opus 4.0) compare as (4,1) vs (4)
|
|
25
|
+
// with the stamps held apart, and as (4,1,20250805) vs (4,20250514), the OLDER model winning, without.
|
|
26
26
|
const DATE_SEGMENT = /^\d{6,}$/;
|
|
27
27
|
|
|
28
28
|
const segmentsOf = (id: string): string[] => id.split(/[-_]/);
|
|
29
29
|
|
|
30
|
-
// A model's FAMILY
|
|
30
|
+
// A model's FAMILY, its id with every version-ish segment dropped, so claude-opus-5 and claude-opus-4-8 land
|
|
31
31
|
// together (as do gpt-5.1/gpt-5, and claude-haiku-4-5-20251001 with its date suffix). Derived, never listed: a
|
|
32
|
-
// family that ships tomorrow groups itself. The id is the stable key here
|
|
32
|
+
// family that ships tomorrow groups itself. The id is the stable key here, labels get renamed, ids don't.
|
|
33
33
|
export const familyOf = (id: string): string => {
|
|
34
34
|
const stem = segmentsOf(id)
|
|
35
35
|
.filter((segment) => !VERSION_SEGMENT.test(segment))
|
|
@@ -43,7 +43,7 @@ export interface ModelRelease {
|
|
|
43
43
|
// id (kimi-latest, gemini-pro-agent), which therefore reads as the oldest of its tier: a rolling alias names
|
|
44
44
|
// no release, and inventing one for it would outrank the models that do name theirs.
|
|
45
45
|
readonly version: readonly number[];
|
|
46
|
-
// The id's date stamp, 0 for none
|
|
46
|
+
// The id's date stamp, 0 for none, the tiebreak between two builds of the SAME version.
|
|
47
47
|
readonly date: number;
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -75,14 +75,14 @@ const compareRelease = (left: ModelRelease, right: ModelRelease): number => {
|
|
|
75
75
|
|
|
76
76
|
/* THE ONE CURATED FACT in this file, and the only one the providers publish nowhere the app can read: which tier
|
|
77
77
|
* is the frontier and which is the cheap one. It ranks FAMILIES, never models, and it is a vocabulary of tier
|
|
78
|
-
* ADJECTIVES rather than a table of ids
|
|
78
|
+
* ADJECTIVES rather than a table of ids, that scoping is the whole point, because a per-model ranking table
|
|
79
79
|
* failed here once already. The words are the ones every vendor reaches for, so a release that ships tomorrow
|
|
80
80
|
* ranks itself as long as it is named like its predecessors, and a release named some other way ranks as unknown.
|
|
81
81
|
*
|
|
82
82
|
* An UNKNOWN family LEADS rather than sinks, and that direction is the point: the ranking this replaced sank
|
|
83
83
|
* unrecognized ids to a floor below the everyday tier, so a brand-new flagship sorted beneath the model it
|
|
84
|
-
* replaced. An id carrying no tier word at all is the provider's BASE line (gpt-5.6, grok-4, kimi-k2)
|
|
85
|
-
* exactly the line a user reaches for
|
|
84
|
+
* replaced. An id carrying no tier word at all is the provider's BASE line (gpt-5.6, grok-4, kimi-k2), which is
|
|
85
|
+
* exactly the line a user reaches for, and a family nobody here has heard of is far likelier to be the next
|
|
86
86
|
* flagship than the next budget tier. Being wrong costs one row's position; being wrong the other way hides a
|
|
87
87
|
* launch. */
|
|
88
88
|
const TIER_RANK: Readonly<Record<string, number>> = {
|
|
@@ -113,7 +113,7 @@ const UNRANKED = -1;
|
|
|
113
113
|
|
|
114
114
|
/* Some providers name a capability ladder INSIDE one release instead of using the cross-release adjectives
|
|
115
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
|
|
116
|
+
* but their order is not an arbitrary id tiebreak. Sol is the strongest, followed by Terra, then Luna. Keeping
|
|
117
117
|
* this as a separate rank lets release recency still win across generations (a future GPT 5.7 base model must
|
|
118
118
|
* not be buried under a recognized 5.6 suffix), while the three siblings sort by their real tier. */
|
|
119
119
|
const RELEASE_TIER_RANK: Readonly<Record<string, number>> = {
|
|
@@ -135,23 +135,23 @@ const lastRankOf = (family: string, ranks: Readonly<Record<string, number>>): nu
|
|
|
135
135
|
|
|
136
136
|
const releaseTierRankOf = (family: string): number => lastRankOf(family, RELEASE_TIER_RANK);
|
|
137
137
|
|
|
138
|
-
/* HOW HARD AN ID SAYS IT WILL THINK. A routed catalog does not publish one row per model
|
|
138
|
+
/* HOW HARD AN ID SAYS IT WILL THINK. A routed catalog does not publish one row per model, it publishes one row
|
|
139
139
|
* per model PER THINKING LEVEL, spelling the level into the id: `gemini-3.6-flash-high` and
|
|
140
140
|
* `gemini-3.5-flash-extra-low` are the same Flash at opposite ends of its reasoning budget.
|
|
141
141
|
*
|
|
142
142
|
* Which the ranking above cannot see at all, and that blind spot has a direction: `high` and `low` are both
|
|
143
|
-
* unrecognized words, so two variants of one model tie on tier and the RELEASE tiebreak settles them
|
|
143
|
+
* unrecognized words, so two variants of one model tie on tier and the RELEASE tiebreak settles them, and the
|
|
144
144
|
* newest variant a channel publishes is routinely the high one. The quick model, whose entire job is to be the
|
|
145
145
|
* cheap rung, would therefore reach for the most expensive reading of the cheapest model it can find.
|
|
146
146
|
*
|
|
147
147
|
* That is not a small mis-sort. Thinking is the difference between a commit message that is in the box before
|
|
148
|
-
* the user has finished reading the file list and one that arrives half a minute later
|
|
148
|
+
* the user has finished reading the file list and one that arrives half a minute later, measured at ~2s versus
|
|
149
149
|
* ~27s on the same model and the same diff (agent/one-shot.ts, which disables thinking for the rungs where a
|
|
150
150
|
* request parameter can). For a routed rung there is no such parameter: the id IS the setting, so this ranking
|
|
151
151
|
* is the only place the choice can be made.
|
|
152
152
|
*
|
|
153
153
|
* Read ONLY by the cheap-end order. A picker orders a catalog by what a person reaches for, and a person
|
|
154
|
-
* pinning `-high` on purpose means it
|
|
154
|
+
* pinning `-high` on purpose means it, see compareCheapestFirst for the seam this belongs to. */
|
|
155
155
|
const THINKING_RANK: Readonly<Record<string, number>> = {
|
|
156
156
|
minimal: 0,
|
|
157
157
|
none: 0,
|
|
@@ -164,7 +164,7 @@ const THINKING_RANK: Readonly<Record<string, number>> = {
|
|
|
164
164
|
thinking: 4,
|
|
165
165
|
};
|
|
166
166
|
|
|
167
|
-
/* An id naming NO level sits between the two ends rather than at either
|
|
167
|
+
/* An id naming NO level sits between the two ends rather than at either, the provider's own default, which for
|
|
168
168
|
* a model that can think is usually some thinking. Ranking it cheapest would seat a silent default ahead of an
|
|
169
169
|
* id that explicitly says `minimal`, and ranking it dearest would bury every model whose channel publishes no
|
|
170
170
|
* variants at all (Anthropic's, Kimi's) beneath one that does. Neither is what the id claims. */
|
|
@@ -180,7 +180,7 @@ const thinkingRankOf = (family: string): number => {
|
|
|
180
180
|
export const tierRankOf = (family: string): number => lastRankOf(family, TIER_RANK);
|
|
181
181
|
|
|
182
182
|
// The canonical order of two model ids: broad tier first, then release, then a tier declared within that release.
|
|
183
|
-
// Hand it straight to Array#toSorted
|
|
183
|
+
// Hand it straight to Array#toSorted, that sort is stable, so two ids this rule cannot separate keep the order
|
|
184
184
|
// they arrived in (for Claude, the provider's own).
|
|
185
185
|
export const compareModelIds = (left: string, right: string): number => {
|
|
186
186
|
const leftFamily = familyOf(left);
|
|
@@ -192,28 +192,28 @@ export const compareModelIds = (left: string, right: string): number => {
|
|
|
192
192
|
);
|
|
193
193
|
};
|
|
194
194
|
|
|
195
|
-
/* The order for a catalog its endpoint published as a SET
|
|
195
|
+
/* The order for a catalog its endpoint published as a SET. Codex, Gemini, Kimi and Grok, i.e. everything but
|
|
196
196
|
* Anthropic's ranked list. Falling back on arrival order is what the rule above does with a tie, and for a RANKED
|
|
197
197
|
* catalog that is exactly right: the tie is the provider's own opinion, so claude-opus-5 stays ahead of
|
|
198
198
|
* claude-fable-5. For a set there is no opinion to keep, and the header of this file assumed the leftover order
|
|
199
|
-
* was at least alphabetical
|
|
199
|
+
* was at least alphabetical, it is not. A subscription can hand tied rows back in whatever order its registry
|
|
200
200
|
* iterated THIS request, so the tie decides which model a fresh conversation opens on and can flip between
|
|
201
201
|
* catalog refreshes.
|
|
202
202
|
*
|
|
203
|
-
* So a set breaks its own ties on the id. Which sibling that seats first is arbitrary
|
|
203
|
+
* So a set breaks its own ties on the id. Which sibling that seats first is arbitrary, but it is the same
|
|
204
204
|
* arbitrary answer every refresh, which is the property `default` actually needs. */
|
|
205
205
|
export const compareUnrankedModelIds = (left: string, right: string): number => compareModelIds(left, right) || left.localeCompare(right);
|
|
206
206
|
|
|
207
207
|
/* THE SAME TIER SCALE READ FROM THE OTHER END, for the one caller that wants the WEAKEST model rather than the
|
|
208
208
|
* strongest: the quick model behind an automatic helper (the commit message written at land time). A picker
|
|
209
209
|
* orders a catalog by what a user reaches for; this orders it by what a helper should spend, and the two are
|
|
210
|
-
* exact opposites
|
|
210
|
+
* exact opposites, so they share TIER_RANK rather than each naming its own list of cheap ids.
|
|
211
211
|
*
|
|
212
212
|
* The direction of UNRANKED is the reason this can't just be compareModelIds reversed. There, an unrecognized
|
|
213
213
|
* family LEADS, because an id carrying no tier word is the provider's base line and a family nobody here has
|
|
214
214
|
* heard of is likelier the next flagship than the next budget tier. Reversing would therefore seat exactly that
|
|
215
|
-
* unknown-probably-flagship id as the cheap pick. So unknown sinks to LAST here too
|
|
216
|
-
* not the efficient rung
|
|
215
|
+
* unknown-probably-flagship id as the cheap pick. So unknown sinks to LAST here too, both orders agree it is
|
|
216
|
+
* not the efficient rung, and the cheap end is only ever a family whose tier word is actually recognized.
|
|
217
217
|
* Falling off the end of a catalog with no efficient tier at all (Kimi publishes none) is then honest: the
|
|
218
218
|
* newest of what it does publish, chosen by the release tiebreak below. */
|
|
219
219
|
export const compareCheapestFirst = (left: string, right: string): number => {
|
|
@@ -223,14 +223,14 @@ export const compareCheapestFirst = (left: string, right: string): number => {
|
|
|
223
223
|
tierRankOf(rightFamily) - tierRankOf(leftFamily) ||
|
|
224
224
|
// BEFORE release, and that placement is the point: two rows of one model differing only in thinking
|
|
225
225
|
// level are the same model, so recency has nothing to say between them, and letting it speak is what
|
|
226
|
-
// seated the high variant. Tier still wins over both
|
|
226
|
+
// seated the high variant. Tier still wins over both, a thinking Haiku is cheaper than a silent Opus.
|
|
227
227
|
thinkingRankOf(leftFamily) - thinkingRankOf(rightFamily) ||
|
|
228
228
|
compareRelease(releaseOf(left), releaseOf(right)) ||
|
|
229
229
|
releaseTierRankOf(rightFamily) - releaseTierRankOf(leftFamily)
|
|
230
230
|
);
|
|
231
231
|
};
|
|
232
232
|
|
|
233
|
-
/* WOULD RUNNING THIS ID MAKE THE MODEL THINK
|
|
233
|
+
/* WOULD RUNNING THIS ID MAKE THE MODEL THINK, as far as its name admits, which for a routed catalog is as far
|
|
234
234
|
* as anyone can tell without running it. True only for an id that spells out a level ABOVE the quiet end, so an
|
|
235
235
|
* ordinary id nobody has annotated (claude-haiku-4-5, kimi-k2) is never accused of it.
|
|
236
236
|
*
|