@intentic/sandbox-contract 1.264.1 → 1.266.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/workspace-scope.d.ts.map +1 -1
- package/dist/chores/workspace-scope.js.map +1 -1
- package/dist/contracts/agent.contract.d.ts +1 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +87 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/automations.contract.d.ts +1 -0
- package/dist/contracts/automations.contract.d.ts.map +1 -1
- package/dist/contracts/issues.contract.d.ts.map +1 -1
- package/dist/contracts/issues.contract.js.map +1 -1
- package/dist/contracts/runner.contract.d.ts +4 -0
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +4 -0
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +12 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.d.ts +8 -2
- package/dist/contracts/workspace.contract.d.ts.map +1 -1
- package/dist/contracts/workspace.contract.js +13 -4
- package/dist/contracts/workspace.contract.js.map +1 -1
- package/dist/events/agent-events.d.ts +2 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/cards.d.ts +2 -0
- package/dist/events/cards.d.ts.map +1 -1
- package/dist/events/cards.js +5 -0
- package/dist/events/cards.js.map +1 -1
- package/dist/events/system-events.d.ts +12 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/index.d.ts +117 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/models/provider-specs.d.ts.map +1 -1
- package/dist/models/provider-specs.js.map +1 -1
- package/dist/policy/batch-runs.d.ts.map +1 -1
- package/dist/policy/batch-runs.js.map +1 -1
- package/dist/policy/capability-env.d.ts.map +1 -1
- package/dist/policy/capability-env.js.map +1 -1
- package/dist/protocol/host-protocol.d.ts.map +1 -1
- package/dist/protocol/host-protocol.js.map +1 -1
- package/dist/protocol/peer-dial.d.ts.map +1 -1
- package/dist/protocol/peer-dial.js.map +1 -1
- package/dist/protocol/runner-protocol.d.ts.map +1 -1
- package/dist/protocol/runner-protocol.js.map +1 -1
- package/dist/protocol/webext-protocol.d.ts.map +1 -1
- package/dist/protocol/webext-protocol.js.map +1 -1
- package/dist/schemas/agent.d.ts +1 -0
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +5 -0
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents.d.ts +18 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +9 -0
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automations.d.ts +18 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/automations.js +5 -0
- package/dist/schemas/automations.js.map +1 -1
- package/dist/schemas/codebase-health.js +1 -1
- package/dist/schemas/codebase-health.js.map +1 -1
- package/dist/schemas/devices.d.ts +32 -64
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +3 -2
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/issues.d.ts.map +1 -1
- package/dist/schemas/issues.js.map +1 -1
- package/dist/schemas/plan-limits.js +1 -1
- package/dist/schemas/plan-limits.js.map +1 -1
- package/dist/schemas/settings.d.ts +7 -0
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +11 -1
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/terminal.d.ts.map +1 -1
- package/dist/schemas/terminal.js.map +1 -1
- package/dist/schemas/{workspace-repos.d.ts → workspace/workspace-repos.d.ts} +3 -0
- package/dist/schemas/workspace/workspace-repos.d.ts.map +1 -0
- package/dist/schemas/{workspace-repos.js → workspace/workspace-repos.js} +4 -1
- package/dist/schemas/workspace/workspace-repos.js.map +1 -0
- package/dist/schemas/workspace/workspace-search.d.ts.map +1 -0
- package/dist/schemas/workspace/workspace-search.js.map +1 -0
- package/dist/schemas/workspace/workspace-setup.d.ts.map +1 -0
- package/dist/schemas/workspace/workspace-setup.js.map +1 -0
- package/dist/schemas/workspace/workspace-tree.d.ts.map +1 -0
- package/dist/schemas/{workspace-tree.js → workspace/workspace-tree.js} +1 -1
- package/dist/schemas/workspace/workspace-tree.js.map +1 -0
- package/dist/state/definition.d.ts +8 -8
- package/dist/state/fix-attempt-plan.d.ts.map +1 -1
- package/dist/state/fix-attempt-plan.js.map +1 -1
- package/dist/state/fix-stance.d.ts.map +1 -1
- package/dist/state/fix-stance.js.map +1 -1
- package/dist/state/workspace-state.d.ts +5 -0
- package/dist/state/workspace-state.d.ts.map +1 -1
- package/dist/state/workspace-state.js +6 -0
- package/dist/state/workspace-state.js.map +1 -1
- package/package.json +4 -4
- package/src/chores/digest.test.ts +1 -3
- package/src/chores/stack.test.ts +1 -2
- package/src/chores/workspace-scope.ts +1 -5
- package/src/contracts/issues.contract.ts +1 -8
- package/src/contracts/workspace.contract.ts +15 -4
- package/src/events/cards.ts +9 -0
- package/src/ids/share-paths.test.ts +2 -5
- package/src/index.ts +4 -4
- package/src/models/provider-specs.test.ts +1 -2
- package/src/models/provider-specs.ts +1 -2
- package/src/policy/batch-runs.ts +3 -32
- package/src/policy/capability-env.ts +1 -6
- package/src/policy/owner-ticket.test.ts +1 -2
- package/src/protocol/host-protocol.ts +1 -6
- package/src/protocol/peer-dial.test.ts +3 -11
- package/src/protocol/peer-dial.ts +9 -46
- package/src/protocol/peer-mcp-server.test.ts +1 -2
- package/src/protocol/runner-protocol.ts +1 -4
- package/src/protocol/webext-protocol.ts +1 -7
- package/src/schemas/agent.ts +12 -60
- package/src/schemas/agents.ts +15 -0
- package/src/schemas/automations.ts +13 -0
- package/src/schemas/codebase-health.ts +1 -1
- package/src/schemas/devices.ts +13 -6
- package/src/schemas/issues.ts +1 -2
- package/src/schemas/plan-limits.ts +2 -2
- package/src/schemas/settings.test.ts +25 -0
- package/src/schemas/settings.ts +18 -2
- package/src/schemas/terminal.ts +3 -1
- package/src/schemas/{workspace-repos.ts → workspace/workspace-repos.ts} +6 -1
- package/src/schemas/{workspace-tree.ts → workspace/workspace-tree.ts} +1 -1
- package/src/state/fix-attempt-plan.ts +1 -9
- package/src/state/fix-stance.ts +1 -9
- package/src/state/versions.test.ts +1 -3
- package/src/state/workspace-state.ts +8 -6
- package/src/text/embed.test.ts +1 -5
- package/dist/schemas/workspace-repos.d.ts.map +0 -1
- package/dist/schemas/workspace-repos.js.map +0 -1
- package/dist/schemas/workspace-search.d.ts.map +0 -1
- package/dist/schemas/workspace-search.js.map +0 -1
- package/dist/schemas/workspace-setup.d.ts.map +0 -1
- package/dist/schemas/workspace-setup.js.map +0 -1
- package/dist/schemas/workspace-tree.d.ts.map +0 -1
- package/dist/schemas/workspace-tree.js.map +0 -1
- /package/dist/schemas/{workspace-search.d.ts → workspace/workspace-search.d.ts} +0 -0
- /package/dist/schemas/{workspace-search.js → workspace/workspace-search.js} +0 -0
- /package/dist/schemas/{workspace-setup.d.ts → workspace/workspace-setup.d.ts} +0 -0
- /package/dist/schemas/{workspace-setup.js → workspace/workspace-setup.js} +0 -0
- /package/dist/schemas/{workspace-tree.d.ts → workspace/workspace-tree.d.ts} +0 -0
- /package/src/schemas/{workspace-search.ts → workspace/workspace-search.ts} +0 -0
- /package/src/schemas/{workspace-setup.ts → workspace/workspace-setup.ts} +0 -0
package/src/chores/stack.test.ts
CHANGED
|
@@ -100,8 +100,7 @@ describe(`recognising the stack`, () => {
|
|
|
100
100
|
});
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
//
|
|
104
|
-
// not.
|
|
103
|
+
// Keep component-overlap cases explicit when stems must not form a family.
|
|
105
104
|
describe(`the name two components share`, () => {
|
|
106
105
|
test(`framework and qualifier noise falls away`, () => {
|
|
107
106
|
expect(componentStem(`src/components/Button.vue`)).toBe(`button`);
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
/* Maintenance probes normally run inside one discovered repository, where a `refs/` directory is ordinary
|
|
2
|
-
* project content. They also run once against the workspace root, where the same first segment is the reserved
|
|
3
|
-
* reference shelf and can hold hundreds of thousands of files. The daemon sets this variable only for that
|
|
4
|
-
* root scope; shell commands opt into the matching prune argument without baking the shelf's name into the
|
|
5
|
-
* browser-safe contract package. */
|
|
1
|
+
/* Maintenance probes normally run inside one discovered repository, where a `refs/` directory is ordinary project content. */
|
|
6
2
|
export const WORKSPACE_ROOT_EXCLUDE_ENV = `INTENTIC_WORKSPACE_ROOT_EXCLUDE`;
|
|
7
3
|
|
|
8
4
|
// Unquoted parameter expansion is intentional: when the variable is absent it contributes zero arguments;
|
|
@@ -2,14 +2,7 @@ import { oc } from "@orpc/contract";
|
|
|
2
2
|
import { IssueIdParamSchema, IssueInstallsSchema, IssueIntakeIdParamSchema, IssuesListSchema, IssueStatusInputSchema } from "../schemas/issues.js";
|
|
3
3
|
import { OkSchema } from "../schemas/shared.js";
|
|
4
4
|
|
|
5
|
-
/* The issues inbox: bug reports that arrived from the owner's own sites and apps, grouped by fingerprint.
|
|
6
|
-
*
|
|
7
|
-
* THE OWNER'S SIDE ONLY. Reports come in through the public `/intake/…` routes, which are deliberately a
|
|
8
|
-
* different prefix rather than a verb on this one: those are reachable by any browser on the internet, these
|
|
9
|
-
* are not, and two id spaces (an automation's public id out there, an issue's fingerprint in here) sharing one
|
|
10
|
-
* path prefix is how a widened rule stops being visible.
|
|
11
|
-
*
|
|
12
|
-
* Nothing here creates an issue. The daemon writes them; this is triage. */
|
|
5
|
+
/* The issues inbox: bug reports that arrived from the owner's own sites and apps, grouped by fingerprint. */
|
|
13
6
|
export const issuesContract = {
|
|
14
7
|
list: oc
|
|
15
8
|
.route({
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
AppParamSchema,
|
|
8
8
|
AppsListSchema,
|
|
9
9
|
CloneRepoSchema,
|
|
10
|
+
CreateRepoSchema,
|
|
10
11
|
CloneResultSchema,
|
|
11
12
|
RepoAppsParamSchema,
|
|
12
13
|
ReposListSchema,
|
|
@@ -14,9 +15,9 @@ import {
|
|
|
14
15
|
TemplatesListSchema,
|
|
15
16
|
WorkspaceGraphSchema,
|
|
16
17
|
WorkspaceSyncSchema,
|
|
17
|
-
} from "../schemas/workspace-repos.js";
|
|
18
|
-
import { WorkspaceSearchQuerySchema, WorkspaceSearchResultSchema } from "../schemas/workspace-search.js";
|
|
19
|
-
import { WorkspaceInstallResultSchema, WorkspaceInstallSchema, WorkspaceSetupSchema } from "../schemas/workspace-setup.js";
|
|
18
|
+
} from "../schemas/workspace/workspace-repos.js";
|
|
19
|
+
import { WorkspaceSearchQuerySchema, WorkspaceSearchResultSchema } from "../schemas/workspace/workspace-search.js";
|
|
20
|
+
import { WorkspaceInstallResultSchema, WorkspaceInstallSchema, WorkspaceSetupSchema } from "../schemas/workspace/workspace-setup.js";
|
|
20
21
|
import {
|
|
21
22
|
WorkspaceChildrenQuerySchema,
|
|
22
23
|
WorkspaceChildrenSchema,
|
|
@@ -34,7 +35,7 @@ import {
|
|
|
34
35
|
WorkspaceResolveSchema,
|
|
35
36
|
WorkspaceScopeSchema,
|
|
36
37
|
WorkspaceTreeSchema,
|
|
37
|
-
} from "../schemas/workspace-tree.js";
|
|
38
|
+
} from "../schemas/workspace/workspace-tree.js";
|
|
38
39
|
|
|
39
40
|
// The full /work view plus extra-repo cloning. /workspace/raw stays a plain Hono route (a streamed binary body doesn't
|
|
40
41
|
// fit oRPC's request/response shape).
|
|
@@ -224,6 +225,16 @@ export const workspaceContract = {
|
|
|
224
225
|
})
|
|
225
226
|
.input(CloneRepoSchema)
|
|
226
227
|
.output(CloneResultSchema),
|
|
228
|
+
createRepo: oc
|
|
229
|
+
.route({
|
|
230
|
+
method: "POST",
|
|
231
|
+
path: "/workspace/repos/new",
|
|
232
|
+
summary: "Start a new repo",
|
|
233
|
+
description:
|
|
234
|
+
"Makes an empty repository in the workspace: a folder named after it, initialised, with a README that names it and one commit, so an agent can start on it at once. Nothing is cloned and nothing leaves the machine.",
|
|
235
|
+
})
|
|
236
|
+
.input(CreateRepoSchema)
|
|
237
|
+
.output(CloneResultSchema),
|
|
227
238
|
// Mutates the tree (fetch + fast-forward), which is why this is POST rather than GET.
|
|
228
239
|
sync: oc
|
|
229
240
|
.route({
|
package/src/events/cards.ts
CHANGED
|
@@ -157,6 +157,15 @@ export type TodoItem = z.infer<typeof TodoItemSchema>;
|
|
|
157
157
|
export const ContextUsageSchema = z.object({
|
|
158
158
|
tokens: z.number().describe("How much the latest request sent, all told."),
|
|
159
159
|
contextWindow: z.number().describe("How much the model can hold. The gap between these two is how close the conversation is to being compacted."),
|
|
160
|
+
// Both absent unless the turn's last request actually used the prompt cache; a provider whose TTL we cannot ground
|
|
161
|
+
// reports neither rather than a guessed pair.
|
|
162
|
+
cachedAt: z
|
|
163
|
+
.number()
|
|
164
|
+
.optional()
|
|
165
|
+
.describe(
|
|
166
|
+
"When that request last touched the provider's prompt cache, in milliseconds. The cache's clock runs from here, since a read refreshes it as a write does.",
|
|
167
|
+
),
|
|
168
|
+
cacheTtlMs: z.number().optional().describe("How long that cache entry lives from `cachedAt`, in milliseconds."),
|
|
160
169
|
});
|
|
161
170
|
export type ContextUsage = z.infer<typeof ContextUsageSchema>;
|
|
162
171
|
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { SHARE_ID, shareId, shareStem } from "./share-paths.js";
|
|
3
3
|
|
|
4
|
-
/* The id a shared conversation is filed under is two things at once: the readable half of a link somebody
|
|
5
|
-
* pastes, and the half of the address that has to be safe to join onto a directory path. Both halves are
|
|
6
|
-
* checked here, because the id is minted from a TITLE the user typed. */
|
|
4
|
+
/* The id a shared conversation is filed under is two things at once: the readable half of a link somebody pastes. */
|
|
7
5
|
|
|
8
6
|
describe("the readable half", () => {
|
|
9
7
|
it("makes a link that says what it points at", () => {
|
|
@@ -25,8 +23,7 @@ describe("the readable half", () => {
|
|
|
25
23
|
});
|
|
26
24
|
|
|
27
25
|
describe("the safe half", () => {
|
|
28
|
-
/* The id is joined onto a path, so a title is the one attacker-shaped input in it. These are the shapes
|
|
29
|
-
* that would matter if the alphabet were not closed. */
|
|
26
|
+
/* The id is joined onto a path, so a title is the one attacker-shaped input in it. These are the shapes. */
|
|
30
27
|
it.each([
|
|
31
28
|
["../../etc/passwd", "etc-passwd"],
|
|
32
29
|
["a/b", "a-b"],
|
package/src/index.ts
CHANGED
|
@@ -180,10 +180,10 @@ export * from "./schemas/usage.js";
|
|
|
180
180
|
export * from "./schemas/vpn.js";
|
|
181
181
|
export * from "./schemas/webext.js";
|
|
182
182
|
export * from "./schemas/workflows.js";
|
|
183
|
-
export * from "./schemas/workspace-repos.js";
|
|
184
|
-
export * from "./schemas/workspace-search.js";
|
|
185
|
-
export * from "./schemas/workspace-setup.js";
|
|
186
|
-
export * from "./schemas/workspace-tree.js";
|
|
183
|
+
export * from "./schemas/workspace/workspace-repos.js";
|
|
184
|
+
export * from "./schemas/workspace/workspace-search.js";
|
|
185
|
+
export * from "./schemas/workspace/workspace-setup.js";
|
|
186
|
+
export * from "./schemas/workspace/workspace-tree.js";
|
|
187
187
|
export * from "./state/arrival.js";
|
|
188
188
|
export * from "./state/definition.js";
|
|
189
189
|
export * from "./policy/search-globs.js";
|
|
@@ -57,8 +57,7 @@ describe("every provider in the table", () => {
|
|
|
57
57
|
/\/v\d+$/,
|
|
58
58
|
);
|
|
59
59
|
expect(variant.label.trim(), `${id}/${variant.id} has no estate label`).not.toBe("");
|
|
60
|
-
//
|
|
61
|
-
// only.
|
|
60
|
+
// Resolve each named variant exactly to itself.
|
|
62
61
|
expect(mintedVariant(id, variant.id), `${id}/${variant.id} does not resolve`).toEqual(variant);
|
|
63
62
|
}
|
|
64
63
|
// An absent variant defaults to the list's head: what a single-estate row or a choice-less login/start
|
|
@@ -162,8 +162,7 @@ export const PROVIDER_SPECS = [
|
|
|
162
162
|
access: { kind: "subscription", requirement: "Muse Code subscription", runs: "Muse Spark under Claude Code" },
|
|
163
163
|
auth: {
|
|
164
164
|
kind: "minted",
|
|
165
|
-
//
|
|
166
|
-
// it.
|
|
165
|
+
// Preserve the minted variant id because stored accounts reference it.
|
|
167
166
|
variants: [
|
|
168
167
|
{
|
|
169
168
|
id: "meta",
|
package/src/policy/batch-runs.ts
CHANGED
|
@@ -55,21 +55,7 @@ export const parseBatchFile = <T>(text: string, shape: (value: Record<string, un
|
|
|
55
55
|
}
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
/* WHAT THE AGENT IS TOLD ABOUT WHERE TO LEAVE ITS ANSWER, appended to whatever prompt the pack composed.
|
|
59
|
-
*
|
|
60
|
-
* WHY THE AGENT WRITES A FILE AND NOT A ROUTE. A ledger is a daemon route, and reaching it from a turn would
|
|
61
|
-
* mean handing the agent a token and a client it needs for nothing else. Writing one small JSON file is
|
|
62
|
-
* something every agent can already do, and the surface promotes finished runs when it next sees them. The
|
|
63
|
-
* promotion is idempotent and re-runs on every poll, so nothing is lost by not being watched.
|
|
64
|
-
*
|
|
65
|
-
* `outcomes` is the pack's vocabulary and is spelled out in full, because a closed set is what lets a surface
|
|
66
|
-
* debounce without hiding anything: an agent that verified some findings and concluded they were false
|
|
67
|
-
* positives has to be able to SAY so, or the next poll starts the same turn again forever. Pass the
|
|
68
|
-
* explanations with the words — a model that reads an outcome as an admission of having done nothing useful
|
|
69
|
-
* will avoid it and report something else, and the surface never goes quiet.
|
|
70
|
-
*
|
|
71
|
-
* The closing line is not decoration. A turn that concludes there was nothing to do and writes no file is
|
|
72
|
-
* indistinguishable from a turn that died, and the surface has to show the second as an unknown. */
|
|
58
|
+
/* WHAT THE AGENT IS TOLD ABOUT WHERE TO LEAVE ITS ANSWER, appended to whatever prompt the pack composed. */
|
|
73
59
|
export const batchReportingClause = (params: { readonly path: string; readonly fields: string; readonly outcomes?: string | undefined }): string =>
|
|
74
60
|
[
|
|
75
61
|
`When you are finished, write your conclusion to ${params.path} as JSON:`,
|
|
@@ -78,23 +64,8 @@ export const batchReportingClause = (params: { readonly path: string; readonly f
|
|
|
78
64
|
`Write that file even if you conclude there was nothing to do.`,
|
|
79
65
|
].join(`\n\n`);
|
|
80
66
|
|
|
81
|
-
/*
|
|
82
|
-
|
|
83
|
-
* which is why none of these packs owns session machinery; `unattended: true` is what the turn IS — started by
|
|
84
|
-
* a row rather than by a person at a composer — and `runRole` is which of those rows, so the daemon answers
|
|
85
|
-
* with the owner's list FOR THAT JOB (model-roles.ts) unless the caller pinned a model on the row's caret, in
|
|
86
|
-
* which case the pick rides along and the daemon's fill step leaves it alone.
|
|
87
|
-
*
|
|
88
|
-
* PERMISSIONS AND ISOLATION ARE THE CALLER'S, deliberately. They are the two decisions that differ by kind and
|
|
89
|
-
* both are about safety rather than plumbing: an acceptance test that parks on a permission card is a test that
|
|
90
|
-
* never finishes, so that surface trades the prompt away; a maintenance chore is different in kind — nobody is
|
|
91
|
-
* waiting on it, it may take until tomorrow, and a sweep that can answer its own permission prompts is exactly
|
|
92
|
-
* the thing an owner would want to have been asked about. A default here would decide that for both. */
|
|
93
|
-
/* WHAT THE CARET ON THE RUN BUTTON CHOSE, in the shell's own vocabulary (`provider`; the turn calls it `agent`).
|
|
94
|
-
* Every field but the provider is optional and absent means absent: the turn goes out without it and the model's
|
|
95
|
-
* own default answers. All of them travel, because all of them are things the picker can now set and each is a
|
|
96
|
-
* different price — a run re-pointed at a frontier model but not at the tier, the loop or the account it was
|
|
97
|
-
* pinned under is not the run the reader configured. */
|
|
67
|
+
/* The batch request fixes the isolation and run flags for one item. */
|
|
68
|
+
/* WHAT THE CARET ON THE RUN BUTTON CHOSE, in the shell's own vocabulary (`provider`; the turn calls it `agent`). */
|
|
98
69
|
export interface BatchTurnPick {
|
|
99
70
|
readonly provider: string;
|
|
100
71
|
readonly model?: string | undefined;
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
/* HOW A CLI CAPABILITY'S ENV VARS ARE NAMED, the one rule both ends of that wire have to apply.
|
|
2
|
-
*
|
|
3
|
-
* The daemon writes the agent's shell env by suffixing every var a cli connector declares with the instance id
|
|
4
|
-
* (cli-env.ts), so two connections of the same provider coexist on one flat environment. A connector's own
|
|
5
|
-
* TOOL then has to read them back, and an extension may not import daemon internals, so without this the
|
|
6
|
-
* rule would be spelled twice, and a change to it would silently split the writer from the reader. */
|
|
1
|
+
/* HOW A CLI CAPABILITY'S ENV VARS ARE NAMED, the one rule both ends of that wire have to apply. */
|
|
7
2
|
|
|
8
3
|
// `analytics` → `POSTGRES_URL_ANALYTICS`; the default-named `github` → `GITHUB_TOKEN_GITHUB`.
|
|
9
4
|
// ponytail: ids differing only by case or `-`/`_` (my-db vs my_db) map to the same suffix, last wins.
|
|
@@ -38,8 +38,7 @@ describe("owner ticket", () => {
|
|
|
38
38
|
expect(isOwnerTicket("ig1.x.y")).toBe(false);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
/* One key signs both the reachability grant and the owner ticket, and neither may ever pass as the other
|
|
42
|
-
* a grant is a sandbox's right to serve its hostnames, a ticket is a person's right to drive it. */
|
|
41
|
+
/* One key signs both the reachability grant and the owner ticket, and neither may ever pass as the other. */
|
|
43
42
|
it("is never a reachability grant, and a grant is never a ticket", () => {
|
|
44
43
|
const ticket = mintOwnerTicket(privatePem, { sandboxId: "0123456789ab", email: "o@x.dev", issuedAtMs: NOW });
|
|
45
44
|
const grant = mintReachabilityGrant(privatePem, "0123456789ab", NOW);
|
|
@@ -7,12 +7,7 @@ import { z } from "zod";
|
|
|
7
7
|
// Shared with the daemon's peer bridge, which answers the handshake itself when the machine is asleep.
|
|
8
8
|
export const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
9
9
|
|
|
10
|
-
/* HOW OFTEN THIS DOOR PINGS A CONNECTED MACHINE, read by both sides of the socket
|
|
11
|
-
* it (hosts/host-peer.ts) and the agent presumes a link dead after a few of them pass in silence
|
|
12
|
-
* (peer-dial.ts's peerLinkSilenceMs). Frequent enough to stay inside the idle timeout of every tunnel and proxy
|
|
13
|
-
* in the path, and it is the failure of a ping that tells the daemon a lid closed without a close frame ever
|
|
14
|
-
* arriving. One number, because the two sides disagreeing about it is a device that is either dropped while
|
|
15
|
-
* healthy or believed alive for as long as its socket stays open. */
|
|
10
|
+
/* HOW OFTEN THIS DOOR PINGS A CONNECTED MACHINE, read by both sides of the socket. */
|
|
16
11
|
export const HOST_HEARTBEAT_MS = 30_000;
|
|
17
12
|
|
|
18
13
|
export const HostHelloSchema = z.object({
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { expect, test, vi } from "vitest";
|
|
2
2
|
import { dialPeer, type SocketLike } from "./peer-dial.js";
|
|
3
3
|
|
|
4
|
-
/* The loop every peer runs, over a socket the test plays.
|
|
5
|
-
* (handler attached BEFORE the hello goes out), the retry after a drop, the one close that is never retried,
|
|
6
|
-
* and that a stop reaches a dial still deciding where to go. */
|
|
4
|
+
/* The loop every peer runs, over a socket the test plays. */
|
|
7
5
|
|
|
8
6
|
class FakeSocket implements SocketLike {
|
|
9
7
|
readyState = 0;
|
|
@@ -117,10 +115,7 @@ test("a drop redials on the ladder, telling it how long the socket held", async
|
|
|
117
115
|
}
|
|
118
116
|
});
|
|
119
117
|
|
|
120
|
-
/*
|
|
121
|
-
* stays open — a sandbox container recreated behind a port relay that outlived it, a NAT that forgot the flow, a
|
|
122
|
-
* laptop's wifi suspended mid-session. Note what this test never calls: `drops`. Nothing closes, nothing errors,
|
|
123
|
-
* and the loop has to reach the conclusion on its own or hold a dead link forever. */
|
|
118
|
+
/* A silent socket must be abandoned even when no close event arrives. */
|
|
124
119
|
test("a socket that goes silent is abandoned and redialled, though no close ever arrives", async () => {
|
|
125
120
|
vi.useFakeTimers();
|
|
126
121
|
try {
|
|
@@ -157,10 +152,7 @@ test("a socket that goes silent is abandoned and redialled, though no close ever
|
|
|
157
152
|
}
|
|
158
153
|
});
|
|
159
154
|
|
|
160
|
-
/* THE FAR END THAT IS NEVER COMING BACK, which is not a failure the loop can fix and not one it may narrate
|
|
161
|
-
* forever: a sandbox deleted, a tunnel repointed, a machine left on for a week. Every attempt here errors and
|
|
162
|
-
* closes without ever opening, exactly as a laptop's agent did 1,992 times into a 2.3 MB log. What is asserted
|
|
163
|
-
* is BOTH halves: the log stops repeating, and the dialling does not slow down to achieve it. */
|
|
155
|
+
/* THE FAR END THAT IS NEVER COMING BACK, which is not a failure the loop can fix and not one it may narrate. */
|
|
164
156
|
test("a far end that never answers is reported a few times, then retried quietly at the same cadence", async () => {
|
|
165
157
|
vi.useFakeTimers();
|
|
166
158
|
try {
|
|
@@ -5,32 +5,11 @@
|
|
|
5
5
|
// Reconnect backoff: fast floor for a restart, low cap so a reopened laptop is back within a minute.
|
|
6
6
|
export const PEER_LINK_BACKOFF = { floorMs: 1_000, capMs: 30_000, stableMs: 60_000 } as const;
|
|
7
7
|
|
|
8
|
-
/* HOW MUCH A LINK THAT CANNOT BE REACHED IS ALLOWED TO SAY, which is a different question from how often it
|
|
9
|
-
* may try. At the cap above, a far end that is gone for good — a sandbox deleted, a tunnel pointed elsewhere —
|
|
10
|
-
* costs two lines every 30 seconds for as long as the machine is on: 5,760 a day. One laptop's agent had
|
|
11
|
-
* written 1,992 pairs of them, 2.3 MB, and this log is exactly where its owner had been sent to read why a
|
|
12
|
-
* DIFFERENT thing had failed; the answer was in there, under an hour of repetition.
|
|
13
|
-
*
|
|
14
|
-
* The cadence is not the problem and is deliberately untouched — a reopened laptop must be back within a
|
|
15
|
-
* minute, which is what the low cap buys. The REPETITION is. So the first few failures are reported in full,
|
|
16
|
-
* then the loop says so once more to mark that it is going quiet, and after that repeats itself at most once
|
|
17
|
-
* per QUIET_LOG_MS with the attempt count that says how long it has been trying. A link that opens resets all
|
|
18
|
-
* of it: every reconnect is news, and the reconnect line is what reports it. */
|
|
8
|
+
/* HOW MUCH A LINK THAT CANNOT BE REACHED IS ALLOWED TO SAY, which is a different question from how often it may try. */
|
|
19
9
|
const LOUD_ATTEMPTS = 3;
|
|
20
10
|
const QUIET_LOG_MS = 10 * 60_000;
|
|
21
11
|
|
|
22
|
-
/* HOW LONG A SOCKET MAY SAY NOTHING before this side calls the link dead, as a multiple of the door's own
|
|
23
|
-
* heartbeat: the hub pings every live peer on an interval (peer-hub.ts), so a socket with nothing on it for
|
|
24
|
-
* three heartbeats is not quiet, it is gone.
|
|
25
|
-
*
|
|
26
|
-
* WHY A PEER NEEDS THIS AT ALL — a close event is not guaranteed, and the loop below has nothing else to react
|
|
27
|
-
* to. The socket that taught us is a loopback one: a machine agent dialling a sandbox container on its own
|
|
28
|
-
* machine through Docker Desktop's port relay. The container was recreated; the relay was not, so it held the
|
|
29
|
-
* agent's TCP connection open with nothing behind it. No FIN, no error, no close event — the agent held an open
|
|
30
|
-
* socket to a daemon that had never heard of it for 25 hours, its Devices card offline the whole time, every
|
|
31
|
-
* control on that card dead, and the only cure anyone found was re-running setup by hand. Any path with a
|
|
32
|
-
* middlebox in it (a NAT, a tunnel, a userland proxy, a laptop's suspended wifi) can do this to a socket with
|
|
33
|
-
* nothing to say, and the middlebox is never the side that notices. The peer is. */
|
|
12
|
+
/* HOW LONG A SOCKET MAY SAY NOTHING before this side calls the link dead, as a multiple of the door's own heartbeat. */
|
|
34
13
|
export const PEER_LINK_SILENCE_HEARTBEATS = 3;
|
|
35
14
|
export const peerLinkSilenceMs = (heartbeatMs: number): number => heartbeatMs * PEER_LINK_SILENCE_HEARTBEATS;
|
|
36
15
|
|
|
@@ -43,9 +22,7 @@ const OPEN = 1;
|
|
|
43
22
|
// Structural interface a browser's WebSocket, node's global one, and a test's fake all satisfy without a cast.
|
|
44
23
|
export interface SocketLike {
|
|
45
24
|
readonly readyState: number;
|
|
46
|
-
|
|
47
|
-
* the frames belongs to the oRPC handler `attach` hands the socket to, and a listener added here does not
|
|
48
|
-
* take it from that one (both runtimes' sockets fan an event out to every listener). */
|
|
25
|
+
/* `message` is here for the watchdog alone, which needs no more than the fact that one arrived. */
|
|
49
26
|
addEventListener(type: "open" | "close" | "error" | "message", listener: (event: { readonly code?: number }) => void): void;
|
|
50
27
|
send(data: string): void;
|
|
51
28
|
close(code?: number, reason?: string): void;
|
|
@@ -61,10 +38,7 @@ export interface PeerDialSpec<S extends SocketLike> {
|
|
|
61
38
|
readonly attach: (socket: S) => void;
|
|
62
39
|
// The retry ladder: how long to wait after a drop, given how long the socket had held.
|
|
63
40
|
readonly backoff: { readonly next: (heldMs: number) => number };
|
|
64
|
-
|
|
65
|
-
* heartbeat the door's own hub pings on. Required rather than defaulted, because the number belongs to the
|
|
66
|
-
* door and a peer that quietly inherited someone else's would be guessing about the one deadline that
|
|
67
|
-
* decides whether it ever notices a dead link. */
|
|
41
|
+
/* How long this socket may hear nothing before the link is presumed dead: `peerLinkSilenceMs` of the heartbeat the door's own hub pings on. */
|
|
68
42
|
readonly silenceMs: number;
|
|
69
43
|
readonly log: (message: string) => void;
|
|
70
44
|
// Sandbox refused the enrollment (1008); the loop has already stopped, this is where the peer says so.
|
|
@@ -100,9 +74,7 @@ export const dialPeer = <S extends SocketLike>(spec: PeerDialSpec<S>): PeerLink
|
|
|
100
74
|
let failures = 0;
|
|
101
75
|
let quietSince = 0;
|
|
102
76
|
|
|
103
|
-
|
|
104
|
-
* failures in full, then the line that marks the loop going quiet (so a reader who sees it knows the retries
|
|
105
|
-
* continue unlogged), then a complaint carrying the attempt count at most once per window. */
|
|
77
|
+
/* What ONE failed attempt is allowed to say. */
|
|
106
78
|
const complain = (said: string, delay: number): void => {
|
|
107
79
|
const every = `retrying every ${Math.round(delay / 1000)}s`;
|
|
108
80
|
if (failures <= LOUD_ATTEMPTS) {
|
|
@@ -148,9 +120,7 @@ export const dialPeer = <S extends SocketLike>(spec: PeerDialSpec<S>): PeerLink
|
|
|
148
120
|
};
|
|
149
121
|
disarmWatchdog = disarm;
|
|
150
122
|
|
|
151
|
-
/*
|
|
152
|
-
* watchdog. Latched, because a socket the watchdog abandoned may still emit its close minutes later,
|
|
153
|
-
* and two redials on one link is two loops racing to hold the same door. */
|
|
123
|
+
/* An attempt drop is recorded when either endpoint notices the closed far end. */
|
|
154
124
|
let dropped = false;
|
|
155
125
|
const drop = (said: string): void => {
|
|
156
126
|
if (dropped) {
|
|
@@ -176,9 +146,7 @@ export const dialPeer = <S extends SocketLike>(spec: PeerDialSpec<S>): PeerLink
|
|
|
176
146
|
const arm = (): void => {
|
|
177
147
|
disarm();
|
|
178
148
|
watchdog = setTimeout(() => {
|
|
179
|
-
/* ABANDONED, not closed politely. A close frame sent to an end that is gone waits on a reply
|
|
180
|
-
* that never comes — CLOSING is the state this watchdog exists to escape — so `close` is called
|
|
181
|
-
* for the runtime's sake and the redial does not wait on an event that may never fire. */
|
|
149
|
+
/* ABANDONED, not closed politely. A close frame sent to an end that is gone waits on a reply. */
|
|
182
150
|
ws.close(1000, "no heartbeat");
|
|
183
151
|
drop(`nothing heard for ${Math.round(spec.silenceMs / 1000)}s`);
|
|
184
152
|
}, spec.silenceMs);
|
|
@@ -216,9 +184,7 @@ export const dialPeer = <S extends SocketLike>(spec: PeerDialSpec<S>): PeerLink
|
|
|
216
184
|
});
|
|
217
185
|
|
|
218
186
|
ws.addEventListener("close", (event) => {
|
|
219
|
-
/* A revocation is answered here rather than through `drop`: it is the one close that ends the loop
|
|
220
|
-
* instead of feeding the ladder, and it must still land on a socket the watchdog abandoned first —
|
|
221
|
-
* an owner who revoked a peer has said so, whether or not this side had already given up on it. */
|
|
187
|
+
/* A revocation is answered here rather than through `drop`: it is the one close that ends the loop. */
|
|
222
188
|
if (event.code === UNAUTHORIZED) {
|
|
223
189
|
dropped = true;
|
|
224
190
|
disarm();
|
|
@@ -233,10 +199,7 @@ export const dialPeer = <S extends SocketLike>(spec: PeerDialSpec<S>): PeerLink
|
|
|
233
199
|
drop(`disconnected (${event.code ?? "no code"})`);
|
|
234
200
|
});
|
|
235
201
|
|
|
236
|
-
/*
|
|
237
|
-
* carry, so it is silenced with the rest once the loop goes quiet — it is half of every repeated pair in
|
|
238
|
-
* a dead link's log, and it says nothing the drop line beside it does not. The attempt that finally
|
|
239
|
-
* reconnects is loud again, this line included. */
|
|
202
|
+
/* Socket errors record a cause; the close event owns retry scheduling. */
|
|
240
203
|
ws.addEventListener("error", () => {
|
|
241
204
|
if (failures < LOUD_ATTEMPTS) {
|
|
242
205
|
spec.log("connection error");
|
|
@@ -2,8 +2,7 @@ import { expect, test } from "vitest";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { createMcpServer, type McpAuditEntry, textResult, tool } from "./peer-mcp-server.js";
|
|
4
4
|
|
|
5
|
-
/* The dispatch every peer serves its tools through, against two hand-written tools: what the model is shown,
|
|
6
|
-
* what an arriving call is checked against, and the rule that a failed tool is a result and not a fault. */
|
|
5
|
+
/* The dispatch every peer serves its tools through, against two hand-written tools: what the model is shown, what an arriving call is checked against. */
|
|
7
6
|
|
|
8
7
|
class Refused extends Error {}
|
|
9
8
|
|
|
@@ -30,10 +30,7 @@ export type RunnerHello = z.infer<typeof RunnerHelloSchema>;
|
|
|
30
30
|
|
|
31
31
|
// The URL a runner dials, given its parent's public URL. One builder, so `ic`, the Fly provisioner and the
|
|
32
32
|
// daemon route cannot disagree about where the door is (hostConnectUrl's rule).
|
|
33
|
-
/* HOW OFTEN THE PARENT PINGS A CONNECTED RUNNER, read by both sides of the socket
|
|
34
|
-
* it (runners/runner-peer.ts) and the runner presumes the link dead after a few of them pass in silence
|
|
35
|
-
* (peer-dial.ts's peerLinkSilenceMs). One number, because the two sides disagreeing about it is a runner either
|
|
36
|
-
* dropped while healthy or believed alive for as long as its socket happens to stay open. */
|
|
33
|
+
/* HOW OFTEN THE PARENT PINGS A CONNECTED RUNNER, read by both sides of the socket. */
|
|
37
34
|
export const RUNNER_HEARTBEAT_MS = 30_000;
|
|
38
35
|
|
|
39
36
|
export const runnerConnectUrl = (parentUrl: string): string => `${parentUrl.replace(/^http/, "ws").replace(/\/$/, "")}/system/runners/connect`;
|
|
@@ -4,13 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
// host-protocol.ts. A separate protocol from host's, since the extension has no shell or filesystem, only tabs and
|
|
5
5
|
// origins the person granted one at a time.
|
|
6
6
|
|
|
7
|
-
/* HOW OFTEN THIS DOOR PINGS A CONNECTED BROWSER, read by both sides of the socket
|
|
8
|
-
* it (webext/webext-peer.ts) and the extension presumes a link dead after a few of them pass in silence
|
|
9
|
-
* (peer-dial.ts's peerLinkSilenceMs). Tighter than the machine door's and the number is not a taste: an MV3
|
|
10
|
-
* service worker is killed after 30 seconds of inactivity and WebSocket traffic is what counts as activity, so
|
|
11
|
-
* a heartbeat at or above Chrome's own limit would race the browser into shutting the extension down between
|
|
12
|
-
* beats. One number, because the two sides disagreeing about it is either a browser dropped while healthy or
|
|
13
|
-
* one believed alive for as long as its socket happens to stay open. */
|
|
7
|
+
/* HOW OFTEN THIS DOOR PINGS A CONNECTED BROWSER, read by both sides of the socket. */
|
|
14
8
|
export const WEBEXT_HEARTBEAT_MS = 20_000;
|
|
15
9
|
|
|
16
10
|
export const WebExtHelloSchema = z.object({
|
package/src/schemas/agent.ts
CHANGED
|
@@ -158,6 +158,14 @@ export const AgentTurnSchema = z
|
|
|
158
158
|
.describe(
|
|
159
159
|
"Work in this conversation's own private copy of the repos rather than the shared tree, so several agents can work at once. Needs a conversation id.",
|
|
160
160
|
),
|
|
161
|
+
// Decided like `isolated`: the first turn's choice, latched. A persona's own start folder wins over it.
|
|
162
|
+
startIn: z
|
|
163
|
+
.string()
|
|
164
|
+
.max(200)
|
|
165
|
+
.optional()
|
|
166
|
+
.describe(
|
|
167
|
+
"Which folder the conversation opens in, relative to the workspace root; the project it belongs to. Decided on the first turn. A persona that names its own start folder wins.",
|
|
168
|
+
),
|
|
161
169
|
// Decided like `isolated`: the request's choice on the first turn, the registry's after. `runner` implies
|
|
162
170
|
// isolation; absent means local.
|
|
163
171
|
placement: AgentPlacementSchema.optional().describe(
|
|
@@ -225,7 +233,7 @@ export const AgentTurnSchema = z
|
|
|
225
233
|
"Content from outside caused this turn, and what to call the source. It is what makes the sandbox treat the turn as carrying somebody else's words.",
|
|
226
234
|
),
|
|
227
235
|
// How tool calls are gated for this turn (the SDK's permissionMode, verbatim):
|
|
228
|
-
// plan: propose → approve → execute
|
|
236
|
+
// plan: propose → approve → execute; the proposing half asks nothing, it only withholds writes
|
|
229
237
|
// default: prompts per tool on the permission side channel
|
|
230
238
|
// acceptEdits: auto-accepts file edits
|
|
231
239
|
// bypassPermissions: runs everything
|
|
@@ -285,30 +293,7 @@ export const AgentTurnSchema = z
|
|
|
285
293
|
message: 'forkOf.files "then" requires isolated',
|
|
286
294
|
});
|
|
287
295
|
export type AgentTurn = z.infer<typeof AgentTurnSchema>;
|
|
288
|
-
/* A MODEL CHOSEN FOR ONE SURFACE-STARTED RUN, what the caret on the shared run button (<AgentRunButton>) sends
|
|
289
|
-
* along with the click that starts it.
|
|
290
|
-
*
|
|
291
|
-
* Shared rather than re-declared per route because every surface that starts an agent for the user now carries
|
|
292
|
-
* that caret, and they must all mean the same thing by it: the pair rides onto the turn as `agent`/`model`, and
|
|
293
|
-
* the daemon's own fill step then leaves it alone (turn-resume.ts fills only what is absent). ABSENT is the
|
|
294
|
-
* ordinary case and the one to keep cheap, nobody touched the caret, so the turn's `runRole` list answers.
|
|
295
|
-
*
|
|
296
|
-
* Both halves or neither, because a model id is only meaningful to the provider that vends it: half a pick
|
|
297
|
-
* would send a Codex model id to Claude. Routes that accept this pass it through verbatim; a model this build
|
|
298
|
-
* has never heard of is a supported pick, since the picker offers a custom-id escape hatch.
|
|
299
|
-
*
|
|
300
|
-
* AND EVERYTHING ELSE THE PANEL CAN SET, which is the half this schema used to drop on the floor. A pinned entry
|
|
301
|
-
* carries its own account, harness, effort, thinking and speed (ModelPinSchema), the daemon applies a pin's
|
|
302
|
-
* knobs ONLY to a turn that named no model (turn-resume.ts), and the picker the caret opens offers every one of
|
|
303
|
-
* them — so a pick that carried the pair alone moved each override onto the provider's own defaults for the
|
|
304
|
-
* rest. That is worst exactly where the caret gets reached for: the one moment somebody opens it is the failure
|
|
305
|
-
* that just beat the standing order, and a run re-pointed at a frontier model but not at the tier, the loop or
|
|
306
|
-
* the account that model was pinned under is not the run they configured.
|
|
307
|
-
*
|
|
308
|
-
* The fields are AgentTurn's own (it carries all five already), so a route that accepts this spreads it onto
|
|
309
|
-
* the turn verbatim. Every one is optional and absent means absent: the turn goes out without the field and the
|
|
310
|
-
* model's own default answers. `thinking: false` is therefore a different statement from no thinking at all,
|
|
311
|
-
* which is the distinction Claude's own refusal of `max` beside disabled thinking turns on. */
|
|
296
|
+
/* A MODEL CHOSEN FOR ONE SURFACE-STARTED RUN, what the caret on the shared run button (<AgentRunButton>) sends along with the click that starts it. */
|
|
312
297
|
export const AgentRunPickSchema = z
|
|
313
298
|
.object({
|
|
314
299
|
agent: z.string().min(1).describe("Which provider."),
|
|
@@ -330,13 +315,7 @@ export const AgentRunPickSchema = z
|
|
|
330
315
|
})
|
|
331
316
|
.optional();
|
|
332
317
|
export type AgentRunPick = z.infer<typeof AgentRunPickSchema>;
|
|
333
|
-
/* THE PICK A RUN BUTTON HOLDS, AS THE WIRE SPELLS IT.
|
|
334
|
-
* (`provider`, the kit's AgentRunChoice) and a turn calls the same field `agent`, so every surface that starts
|
|
335
|
-
* an agent had this translation written out by hand — seven of them, each re-deciding which fields to carry,
|
|
336
|
-
* which is exactly how the tier came to travel from four of them and the account from none.
|
|
337
|
-
*
|
|
338
|
-
* ABSENT STAYS ABSENT, field by field: `undefined` means the turn goes out without it and the model's own
|
|
339
|
-
* default answers, which is not the same as any value this could invent. */
|
|
318
|
+
/* THE PICK A RUN BUTTON HOLDS, AS THE WIRE SPELLS IT. */
|
|
340
319
|
export const runPickOf = (choice: {
|
|
341
320
|
readonly provider: string;
|
|
342
321
|
readonly model: string;
|
|
@@ -354,34 +333,7 @@ export const runPickOf = (choice: {
|
|
|
354
333
|
...(choice.thinking === undefined ? {} : { thinking: choice.thinking }),
|
|
355
334
|
...(choice.fast === undefined ? {} : { fast: choice.fast }),
|
|
356
335
|
});
|
|
357
|
-
/* ONE ENTRY OF ONE ROLE'S MODEL LIST (settings.modelRoles): the standing version of the pick above, and not
|
|
358
|
-
* merely which model but HOW it is to be run.
|
|
359
|
-
*
|
|
360
|
-
* THE KNOBS RIDE THE ENTRY RATHER THAN THE LIST, which is the whole reason this is an object rather than a
|
|
361
|
-
* `${provider}:${model}` string. The reasoning effort was once a single field beside a list, so one tier
|
|
362
|
-
* answered for every model in it — and the entries of such a list are deliberately NOT interchangeable: it is a
|
|
363
|
-
* frontier pin with the cheap account underneath that catches it when the first is spent. A tier scale is a
|
|
364
|
-
* property of the MODEL as well ('max' is off Kimi's scale entirely, and off Claude's own the moment thinking
|
|
365
|
-
* is switched off), so a shared effort was either off-scale for half the list or the lowest common rung for all
|
|
366
|
-
* of it. Each entry carries what the composer's picker configures for the turn in front of you.
|
|
367
|
-
*
|
|
368
|
-
* THE SAME SHAPE FOR EVERY ROLE, one-shot helpers included, and that is a deliberate widening. A commit
|
|
369
|
-
* message or a session title used to be pinnable by model alone, on the argument that the daemon runs those
|
|
370
|
-
* with reasoning off and no effort, so a control for either would be a switch with nothing behind it. True of
|
|
371
|
-
* the machinery, and it made the machinery the argument: an owner who pins a reasoning model to their commit
|
|
372
|
-
* subjects was paying that model's price to have its distinguishing feature suppressed. The knobs now travel
|
|
373
|
-
* through the one-shot path too, so an entry means the same thing wherever it is written.
|
|
374
|
-
*
|
|
375
|
-
* EVERY FIELD BUT THE PAIR IS OPTIONAL, AND ABSENT MEANS ABSENT: the work goes out without the field and the
|
|
376
|
-
* provider's own default answers, exactly as an unconfigured pin always did. Nothing here invents a "low".
|
|
377
|
-
*
|
|
378
|
-
* NO TIER HOLD, and its absence is the rule rather than an omission: automatic tier selection gates on
|
|
379
|
-
* `unattended` (prompt-complexity.ts), so a role-started run is never downgraded in the first place and a veto
|
|
380
|
-
* over it would be a control whose state can make no difference to anything.
|
|
381
|
-
*
|
|
382
|
-
* The pair is BOTH HALVES for the reason the pick above is: a model id is only meaningful to the provider that
|
|
383
|
-
* vends it, so half a pin would send a Codex id to Claude. Taken verbatim, never validated against a catalog:
|
|
384
|
-
* the picker offers a custom-id escape hatch, so a model this build has never heard of is a supported pin. */
|
|
336
|
+
/* ONE ENTRY OF ONE ROLE'S MODEL LIST (settings.modelRoles): the standing version of the pick above, and not merely which model but HOW it is to be run. */
|
|
385
337
|
export const ModelPinSchema = z.object({
|
|
386
338
|
provider: AgentProviderSchema.describe("Which provider serves this work."),
|
|
387
339
|
model: z.string().min(1).describe("Which of its models. Both halves, because a model name only means anything to the provider that serves it."),
|
package/src/schemas/agents.ts
CHANGED
|
@@ -174,6 +174,9 @@ export const AgentSummarySchema = z.object({
|
|
|
174
174
|
harness: AgentHarnessSchema.describe("Which agentic loop it runs on."),
|
|
175
175
|
// Latched with the conversation, so the card can say where work runs without asking anything else.
|
|
176
176
|
runner: z.string().optional().describe("The runner this conversation runs on. Absent means this sandbox."),
|
|
177
|
+
// Both latched at the first turn, so a board scoped to one project can say which conversations are its own.
|
|
178
|
+
startIn: z.string().optional().describe("Which folder it opened in, relative to the workspace root. Absent means the root."),
|
|
179
|
+
actsAs: z.string().optional().describe("Which persona its first turn acted as. Absent for an ordinary chat."),
|
|
177
180
|
// Per-conversation, so opening it restores its own choices rather than another tab's; `fast` here is what was asked
|
|
178
181
|
// for, not what was served.
|
|
179
182
|
model: z
|
|
@@ -250,6 +253,18 @@ export const AgentSummarySchema = z.object({
|
|
|
250
253
|
outputTokens: z.number().optional().describe("Tokens received."),
|
|
251
254
|
contextTokens: z.number().optional().describe("How much of the window the conversation currently fills."),
|
|
252
255
|
contextWindow: z.number().optional().describe("How large that window is."),
|
|
256
|
+
// The pair travels whole or not at all: an instant with no lifetime, or a lifetime with no instant, names no
|
|
257
|
+
// deadline. Absent for every provider whose prompt-cache TTL the daemon cannot ground in a measurement or a
|
|
258
|
+
// documented rule (prompt-cache.ts), which is most of them.
|
|
259
|
+
promptCache: z
|
|
260
|
+
.object({
|
|
261
|
+
at: z.number().describe("When its last request touched the provider's prompt cache, in milliseconds."),
|
|
262
|
+
ttlMs: z.number().describe("How long that entry lives from `at`, in milliseconds."),
|
|
263
|
+
})
|
|
264
|
+
.optional()
|
|
265
|
+
.describe(
|
|
266
|
+
"When this conversation's prompt cache was last kept alive and how long it lasts, which together say when picking the conversation up stops being cheap. Absent when the provider publishes nothing to ground it on.",
|
|
267
|
+
),
|
|
253
268
|
activity: AgentActivitySchema.optional().describe("What it is doing at this moment."),
|
|
254
269
|
// The whole drafting story (which models, how long, what refused), replacing a boolean that hid all of it;
|
|
255
270
|
// runtime-only, forgotten on restart like the draft itself.
|
|
@@ -202,6 +202,16 @@ export const WebchatMessageSchema = z.object({
|
|
|
202
202
|
.optional(),
|
|
203
203
|
});
|
|
204
204
|
export type WebchatMessage = z.infer<typeof WebchatMessageSchema>;
|
|
205
|
+
// What GET /webchat/<id>/messages answers: replies queued after the visitor's stream closed, which is every reply an
|
|
206
|
+
// approval-gated desk produces and every one a human writes as the agent. Public by construction, like the config:
|
|
207
|
+
// reaching it needs nothing but an allowed origin and the conversation id already in that browser.
|
|
208
|
+
export const WebchatPendingSchema = z.object({
|
|
209
|
+
// Oldest first, so appending them to the log preserves the order they were written in.
|
|
210
|
+
replies: z.array(z.object({ seq: z.number(), at: z.number(), text: z.string() })),
|
|
211
|
+
// Send back as `after` next time. Can jump past the last reply returned, when older ones were trimmed away.
|
|
212
|
+
cursor: z.number(),
|
|
213
|
+
});
|
|
214
|
+
export type WebchatPending = z.infer<typeof WebchatPendingSchema>;
|
|
205
215
|
export const AutomationSchema = z.object({
|
|
206
216
|
id: entryId.describe("The automation's id."),
|
|
207
217
|
trigger: TriggerSchema.describe("What sets it off: a schedule, an event in the workspace, a message arriving from outside, or a webhook."),
|
|
@@ -286,6 +296,9 @@ export const AutomationApprovalSchema = z.object({
|
|
|
286
296
|
"The thread this belongs to, when it has one, so approving continues that conversation rather than opening a new one. Without it, one visitor's chat becomes a card per approved message and an agent that meets them again every turn.",
|
|
287
297
|
),
|
|
288
298
|
sessionId: z.string().optional().describe("The provider session that thread last ran on."),
|
|
299
|
+
// Snapshotted from the automation at hold time, so a project-scoped board can file the wake under what the persona
|
|
300
|
+
// reaches without reading the automation.
|
|
301
|
+
actsAs: entryId.optional().describe("Which persona the approved run would speak as."),
|
|
289
302
|
createdAt: z.number().describe("When it started waiting, in milliseconds."),
|
|
290
303
|
// When the daemon may run this itself, for a `holdForSeconds` hold; absent for a `requireApproval` hold, which only
|
|
291
304
|
// the owner releases.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// codebase health: one repository's structure and risk, in numbers
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { WorkspaceSearchFreshnessSchema } from "./workspace-search.js";
|
|
3
|
+
import { WorkspaceSearchFreshnessSchema } from "./workspace/workspace-search.js";
|
|
4
4
|
// Repo-level companion to the management panel and git-history graph: the resident engine's `hotspots` (churn ×
|
|
5
5
|
// complexity) and `map` (PageRank over imports) verbs, as figures a panel can plot. Every field is a recountable count;
|
|
6
6
|
// no composite maintainability grade, since those aren't comparable across projects or checkable. HEALTH_LIMIT is how
|