@intentic/sandbox-contract 1.215.0 → 1.216.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentic/sandbox-contract",
3
- "version": "1.215.0",
3
+ "version": "1.216.0",
4
4
  "description": "oRPC wire contract for the intentic sandbox daemon — shared by the daemon and its browser client",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -89,9 +89,9 @@
89
89
  "@orpc/contract": "1.14.13",
90
90
  "tslib": "2.8.1",
91
91
  "zod": "4.4.3",
92
- "@intentic/registry": "1.215.0",
93
- "@intentic/extension-manifest": "1.215.0",
94
- "@intentic/constants": "1.215.0"
92
+ "@intentic/constants": "1.216.0",
93
+ "@intentic/extension-manifest": "1.216.0",
94
+ "@intentic/registry": "1.216.0"
95
95
  },
96
96
  "devDependencies": {
97
97
  "@types/node": "24.13.2",
package/src/schemas.ts CHANGED
@@ -1725,11 +1725,6 @@ export const SkillRemoveSchema = z.object({ name: SkillNameSchema });
1725
1725
  // only. Opt-in (default off); the browser Search box uses iq regardless.
1726
1726
  // iqSearchHoldout — conversation-level measurement control for iqSearch (UsageTurn.iqSearchArm). The arm
1727
1727
  // stays fixed because teaching already loaded into a session cannot be removed next turn.
1728
- // iqContext — retrieves for the user's message BEFORE the turn starts and prepends the ranked answer
1729
- // to it, so the model opens with the anchors instead of paying a search round-trip to
1730
- // find them. Independent of iqSearch: that one teaches the agent to search, this one
1731
- // answers ahead of it.
1732
- // iqContextHoldout — measurement control for iqContext, same shape as terseHoldout (UsageTurn.iqContext).
1733
1728
  // workspaceMap — computes an AREA index of the project a run starts in and prepends it to the
1734
1729
  // conversation's opening message, so the turn does not have to buy its own orientation
1735
1730
  // with a directory listing. Generated from the filesystem every time, never stored.
@@ -1799,29 +1794,12 @@ export const SandboxSettingsSchema = z.object({
1799
1794
  * a valid control here: once the teaching enters a provider session, withholding it from the next request
1800
1795
  * does not make the model forget it. 0 ⇒ no measurement and every conversation receives the teaching. */
1801
1796
  iqSearchHoldout: z.number().min(0).max(1).default(0),
1802
- /* RETRIEVE BEFORE THE TURN, don't wait to be asked. The daemon runs the user's message through the resident
1803
- * iq engine and prepends the ranked answer to it, so a turn that would have opened with two or three search
1804
- * calls opens with the anchors already in hand. Independent of `iqSearch`, which only teaches the agent to
1805
- * reach for the CLI once it decides to search — this one answers ahead of that decision, and the two
1806
- * compose: the injected capsule names the anchors, the CLI is there for what it missed.
1807
- *
1808
- * It rides the USER message (turn-context.ts), never the system prompt, for the same reason the setup
1809
- * notice does: it changes every turn, and the system prefix is kept byte-stable for the prompt cache.
1810
- * Off by default — it spends input tokens on every eligible turn, and whether that trade pays is exactly
1811
- * what the holdout below is for. */
1812
- iqContext: z.boolean().default(false),
1813
- // Measurement control for the pre-injection, identical in shape to `terseHoldout`: a fraction [0,1] of
1814
- // otherwise-eligible turns run WITHOUT the retrieved context and stamp their arm onto the ledger
1815
- // (UsageTurn.iqContext), so the report compares two real populations of turns instead of asserting a saving.
1816
- iqContextHoldout: z.number().min(0).max(1).default(0),
1817
1797
  /* THE MAP THE TURN OPENS WITH — which areas the project a run starts in has, one derived line on what each
1818
1798
  * is for, and where the run is standing among them (agent/workspace-map.ts).
1819
1799
  *
1820
- * A different answer to the same question `iqContext` answers. That one retrieves for the WORDS of the
1821
- * message and is only as good as the question; this one answers the question every first turn has whatever
1822
- * it was asked "what is this and where am I in it" which across a hundred sessions of this workspace was
1823
- * being bought with a directory listing in two turns out of five, and with ~5.3k tokens of tool results
1824
- * before the job was touched. The two compose: this says which area, retrieval says which file.
1800
+ * It answers the question every first turn has whatever it was asked "what is this and where am I in
1801
+ * it" which across a hundred sessions of this workspace was being bought with a directory listing in two
1802
+ * turns out of five, and with ~5.3k tokens of tool results before the job was touched.
1825
1803
  *
1826
1804
  * ROOTED AT THE RUN'S STARTING FOLDER rather than at the workspace: a persona's start folder, an isolated
1827
1805
  * conversation's worktree, or wherever the turn's cwd is. It maps the project containing that folder and
@@ -2059,34 +2037,17 @@ export type InputSavings = z.infer<typeof InputSavingsSchema>;
2059
2037
  // without it. A mean PER TURN, because the arms never hold the same number of turns.
2060
2038
  export const SavingsArmSchema = z.object({ turns: z.number(), mean: z.number() });
2061
2039
 
2062
- /* WHAT BECAME OF A TURN'S PRE-TURN RETRIEVAL. `note` is the only outcome that reaches the model; the rest are
2063
- * the ordinary ways it declines, and none of them is an error — each is a turn that proceeded exactly as it
2064
- * would have without the feature.
2065
- *
2066
- * ineligible — the prompt failed a lexical gate before retrieval ran (it named its own file, or said nothing
2067
- * to search for). By design, and the largest share by far.
2068
- * deadline — the resident engine outran its budget and the turn went on without it.
2069
- * indexing — the index had not caught up with disk, so any answer would have been confidently partial.
2070
- * no-hits — the query matched nothing.
2071
- * failed — retrieval threw; swallowed on purpose, since this is an optimisation nobody asked for.
2072
- *
2073
- * It lives in the contract rather than in the daemon because two things downstream of the daemon have to speak
2074
- * it: the ledger row that records it (UsageTurnSchema.iqContextOutcome) and the experiment that reads it back
2075
- * (TurnExperimentSchema.outcomes). Declared here, above both. */
2076
- export const IqContextOutcomeSchema = z.enum(["note", "ineligible", "deadline", "indexing", "no-hits", "failed"]);
2077
- export type IqContextOutcome = z.infer<typeof IqContextOutcomeSchema>;
2078
-
2079
2040
  /* ONE METRIC'S READING of a turn-level experiment: the two arms, and whatever the arithmetic over them will
2080
2041
  * stand behind. An experiment can carry several — see TurnExperimentSchema.
2081
2042
  *
2082
2043
  * `metric` says what `mean` counts and what `deltaPct` is a delta in, and choosing it is most of the work.
2083
2044
  * proseChars — the terse steer: the thing it steers, and the only part of the model's output that
2084
2045
  * responds to being asked to be brief (UsageTurn.proseChars has why output tokens cannot).
2085
- * searchCalls — pre-injection: the searches a turn ran, which the retrieval directly removes.
2086
- * openingSearches — pre-injection, narrower: the searches before the turn first touched a file.
2087
- * Pre-injection used to be judged on COST, and could not be. UsageTurn.searchCalls has the nine days of data
2088
- * that settled it cost is a whole turn's work, retrieval moves one part of it, and the part sat inside the
2089
- * noise of the rest exactly as the steer's effect once sat inside its tool-call arguments. */
2046
+ * searchCalls — the search teaching: the searches a turn ran, which the teaching directly changes.
2047
+ * openingSearches — the same, narrower: the searches before the turn first touched a file.
2048
+ * Search mechanisms must not be judged on COST. Cost is a whole turn's work, a search mechanism moves one part
2049
+ * of it, and the part sits inside the noise of the rest exactly as the steer's effect once sat inside its
2050
+ * tool-call arguments. */
2090
2051
  export const TurnMetricReadingSchema = z.object({
2091
2052
  metric: z.enum(["proseChars", "searchCalls", "openingSearches"]),
2092
2053
  on: SavingsArmSchema,
@@ -2134,11 +2095,11 @@ export type TurnMetricReading = z.infer<typeof TurnMetricReadingSchema>;
2134
2095
  * counted: a turn under a custom system prompt drops the terse steer along with everything else the daemon
2135
2096
  * appends, so it belongs to neither arm.
2136
2097
  *
2137
- * ONE COIN FLIP, SEVERAL READINGS. `metrics` is a list because pre-injection is judged on two — the searches a
2138
- * turn ran, and the ones it ran before touching a file — and they are two readings of the SAME experiment, not
2139
- * two experiments. Splitting them into separate entries would duplicate the arm assignment and the delivery
2140
- * rate below, and let a screen show a turn count on one that disagrees with the other. Headline first: the
2141
- * screens read `metrics[0]` for the big number and the rest as supporting lines. */
2098
+ * ONE COIN FLIP, SEVERAL READINGS. `metrics` is a list because the search teaching is judged on two — the
2099
+ * searches a turn ran, and the ones it ran before touching a file — and they are two readings of the SAME
2100
+ * experiment, not two experiments. Splitting them into separate entries would duplicate the arm assignment and
2101
+ * let a screen show a turn count on one that disagrees with the other. Headline first: the screens read
2102
+ * `metrics[0]` for the big number and the rest as supporting lines. */
2142
2103
  export const TurnExperimentSchema = z.object({
2143
2104
  // A head and a tail rather than a plain array, because an experiment judged on nothing is not an experiment:
2144
2105
  // the screens take the first reading for their headline and stack the rest under it, and this is what makes
@@ -2155,32 +2116,16 @@ export const TurnExperimentSchema = z.object({
2155
2116
  // Content-addressed treatment version. Present where mixing rows from two instruction revisions would turn
2156
2117
  // one experiment into two unnamed ones; the reader filters to this (latest) cohort.
2157
2118
  cohort: z.string().optional(),
2158
- /* How much of the treatment arm the treatment actually REACHED, when that is knowable and less than all of
2159
- * it — pre-injection's arm is the coin flip (intention-to-treat, deliberately), and a turn can be assigned
2160
- * the retrieval and still have nothing to prepend. Measured at four turns in five, which is the difference
2161
- * between a mechanism worth little and one worth five times what the delta says.
2162
- *
2163
- * Absent ⇒ delivery is not a separate question for this experiment (the terse steer always lands) or no
2164
- * turn in the window recorded it. The screen shows the delta as diluted rather than silently scaling it:
2165
- * the correction is a division by a rate this small only when the rate is itself well measured. */
2166
- deliveredPct: z.number().optional(),
2167
- /* WHERE THE REST OF THE TREATMENT ARM WENT, most common first. `deliveredPct` says a mechanism reached one
2168
- * turn in five; this says whether the other four were the eligibility gate declining on purpose or a
2169
- * two-second deadline quietly eating the feature, which are the same number and opposite problems.
2170
- *
2171
- * Absent ⇒ the experiment has no delivery question (the terse steer always lands) or no turn recorded one. */
2172
- outcomes: z.array(z.object({ outcome: IqContextOutcomeSchema, turns: z.number() })).optional(),
2173
2119
  });
2174
2120
  export type TurnExperiment = z.infer<typeof TurnExperimentSchema>;
2175
2121
 
2176
- // `output`/`context` are absent when that experiment isn't running at all (its flag off, or no holdout set) — a
2122
+ // `output`/`search` are absent when that experiment isn't running at all (its flag off, or no holdout set) — a
2177
2123
  // section that isn't there reads as "not measured", which is the truth, while zeros would read as "measured,
2178
2124
  // worth nothing".
2179
2125
  export const SavingsReportSchema = z.object({
2180
2126
  input: InputSavingsSchema,
2181
2127
  output: TurnExperimentSchema.optional(),
2182
2128
  search: TurnExperimentSchema.optional(),
2183
- context: TurnExperimentSchema.optional(),
2184
2129
  });
2185
2130
  export type SavingsReport = z.infer<typeof SavingsReportSchema>;
2186
2131
 
@@ -5597,8 +5542,25 @@ export type MachinePort = z.infer<typeof MachinePortSchema>;
5597
5542
  export const MachineWatcherSchema = z.object({
5598
5543
  running: z.boolean(),
5599
5544
  pid: z.number().int().optional(),
5545
+ /* When the watcher last FINISHED a pass — the field that makes `running` mean something. The agent holds its
5546
+ * SSH transport listeners on its own event loop, so a failure that escapes the loop leaves a process that is
5547
+ * alive and a loop that is gone: pid present, unit "active", mirroring and the git bridge stopped. Absent
5548
+ * means the agent has not reported one (too old to stamp, or its first pass has not landed) — which is not
5549
+ * the same as stalled, and readers must not treat it as either state. */
5600
5550
  lastTickAt: z.number().optional(),
5601
5551
  });
5552
+ export type MachineWatcher = z.infer<typeof MachineWatcherSchema>;
5553
+
5554
+ /* How long a watcher may go without finishing a pass before "running" stops being the honest word for it. Its
5555
+ * loop polls every 5s and its slowest step is bounded by two 10s network timeouts per pairing, so a minute is
5556
+ * several passes of slack — the same yardstick the Computers view already ages a whole report by.
5557
+ *
5558
+ * The rule lives HERE, next to the field, because the terminal and the browser both answer this question and a
5559
+ * machine that is "running" in one and "stalled" in the other is worse than either answer alone. */
5560
+ export const WATCHER_STALL_AFTER_MS = 60_000;
5561
+
5562
+ export const watcherStalled = (watcher: MachineWatcher, now: number): boolean =>
5563
+ watcher.running && watcher.lastTickAt !== undefined && now - watcher.lastTickAt > WATCHER_STALL_AFTER_MS;
5602
5564
 
5603
5565
  export const MachineReportSchema = z.object({
5604
5566
  /* The OS hostname, and the JOIN KEY. A machine can arrive here two ways at once — volunteered by its sync
@@ -6507,31 +6469,6 @@ export const UsageTurnSchema = z.object({
6507
6469
  * no control to be compared against. Pooling those into the off-arm would compare steered turns against a
6508
6470
  * population selected by something other than the coin flip, which is not a control at all. */
6509
6471
  terse: z.boolean().optional(),
6510
- /* Which arm of the pre-injection experiment this turn ran on (settings.iqContextHoldout), on the same terms
6511
- * as `terse` above: absent ⇒ outside the experiment.
6512
- *
6513
- * TRUE means the turn was ASSIGNED the retrieved context, not that a note was necessarily prepended — a
6514
- * treatment turn whose retrieval came back empty or unconfident injects nothing. That is deliberate: the
6515
- * arms have to be the coin flip's populations, and re-labelling a turn by what retrieval happened to find
6516
- * would sort turns by how searchable their question was, which is a property of the question. The control
6517
- * arm contains the same unsearchable questions in the same proportion, so they cancel. */
6518
- iqContext: z.boolean().optional(),
6519
- /* WHAT ACTUALLY HAPPENED to the retrieval on this turn — the companion to `iqContext`, and the answer to the
6520
- * question that field's design deliberately refuses to answer.
6521
- *
6522
- * Keeping the arm on the coin flip is right, and it costs something: the treatment arm contains turns the
6523
- * treatment never reached, so the delta it yields is diluted by however many those are. Measured over nine
6524
- * days of real use that was four turns in five, which makes the difference between "this mechanism is worth
6525
- * little" and "this mechanism is worth five times what the number says".
6526
- *
6527
- * IT RECORDS THE REASON, not merely a yes/no, because the yes/no left the whole loss unexplained. 81% of an
6528
- * assigned arm delivering nothing is either a gate doing its job or a deadline quietly eating the feature,
6529
- * and those call for opposite responses — one is working as designed, the other is a two-second timeout to
6530
- * raise. A boolean cannot tell them apart, so nothing downstream could act on the number it produced.
6531
- *
6532
- * `note` is delivery; every other value is a turn that ran exactly as the control arm would have, labelled
6533
- * with what took the treatment away. Absent ⇒ outside the experiment, exactly as for the arm. */
6534
- iqContextOutcome: IqContextOutcomeSchema.optional(),
6535
6472
  /* Which arm of the iq SEARCH-TEACHING experiment this conversation runs on
6536
6473
  * (settings.iqSearchHoldout). Stable for every turn in one conversation: the treatment is instruction
6537
6474
  * loaded into a provider session, so flipping it per turn would call a remembered treatment a control.
@@ -6540,10 +6477,6 @@ export const UsageTurnSchema = z.object({
6540
6477
  // Hash of the plugin nudge + skill body used for this arm. Control turns carry it too, so a report can keep
6541
6478
  // both sides of one treatment revision together and exclude older wording after an upgrade.
6542
6479
  iqSearchCohort: z.string().optional(),
6543
- /* Wall time spent deciding the pre-turn retrieval outcome, including the full deadline on a timeout.
6544
- * Absent on rows written before latency was recorded or turns outside the retrieval experiment. Keeping it
6545
- * per outcome is what lets the report distinguish a slow engine from an eligibility gate. */
6546
- iqContextDurationMs: z.number().nonnegative().optional(),
6547
6480
  /* Characters of the model's own PROSE this turn — the `delta` frames only, so no tool-call arguments and no
6548
6481
  * thinking. What the terse steer is judged on, and the reason it can be judged at all.
6549
6482
  *
@@ -6562,33 +6495,29 @@ export const UsageTurnSchema = z.object({
6562
6495
  proseChars: z.number().optional(),
6563
6496
  /* SEARCHES THIS TURN RAN — every tool call that went looking for code, the dedicated search tools and the
6564
6497
  * CLI searches alike (isSearchCall owns the rule; `iq q` is Bash and would otherwise not be counted at all).
6565
- * What pre-injection is judged on, and the same correction `proseChars` is to the terse steer.
6498
+ * What the search teaching is judged on, and the same correction `proseChars` is to the terse steer.
6566
6499
  *
6567
- * COST PER TURN CANNOT SERVE, which is what this replaced. Nine days of real use reported +27.0% ± 29.9pp on
6568
- * cost, an interval from −2.9% to +56.9%, and reading it against the transcripts showed the gap was not the
6569
- * mechanism: every raw per-turn outcome moved with it (reads +58%, duration +73%, cache-read tokens +28%)
6570
- * and every one of them was flat to within a point once turn size was divided out. It was the coin flip
6571
- * handing the treatment arm the bigger jobs. Cost is a whole turn's worth of work, and retrieval touches one
6572
- * part of it, so the part lives inside the noise of the rest — exactly the shape that made output tokens
6573
- * unable to see the steer.
6500
+ * COST PER TURN CANNOT SERVE: cost is a whole turn's worth of work, a search mechanism touches one part of
6501
+ * it, and the part lives inside the noise of the rest — exactly the shape that made output tokens unable to
6502
+ * see the steer. Nine days of a since-removed retrieval experiment proved it with an interval from −2.9% to
6503
+ * +56.9%, driven entirely by which arm had drawn the bigger jobs.
6574
6504
  *
6575
- * Searches are what the mechanism acts on directly: a turn handed the anchors up front does not go and find
6576
- * them. Turns that never search stay in the population at zero rather than being filtered out they dilute
6577
- * both arms equally, while selecting on "did it search" would select on the treatment itself.
6505
+ * Searches are what the mechanism acts on directly. Turns that never search stay in the population at zero
6506
+ * rather than being filtered out they dilute both arms equally, while selecting on "did it search" would
6507
+ * select on the treatment itself.
6578
6508
  *
6579
6509
  * Absent ⇒ the turn predates this being measured; `armOf` drops it rather than reading it as a turn that
6580
6510
  * searched nothing. */
6581
6511
  searchCalls: z.number().optional(),
6582
- /* …and how many of them came BEFORE the turn first opened or changed a file — the orientation burst, which
6583
- * is the part retrieval is actually aimed at. A turn that already knows where to look starts working; one
6584
- * that doesn't goes hunting first, and pre-injection's whole claim is that it removes that hunt.
6512
+ /* …and how many of them came BEFORE the turn first opened or changed a file — the orientation burst. A turn
6513
+ * that already knows where to look starts working; one that doesn't goes hunting first.
6585
6514
  *
6586
6515
  * The narrower of the two readings and the less confounded: `searchCalls` still grows with the size of the
6587
6516
  * job, while the walk up to the first file is roughly the same act whatever the job turns out to be.
6588
6517
  *
6589
6518
  * A turn that never reads or edits counts all of its searches here — it never arrived, so all of it was
6590
- * orientation. Dropping those instead would select the population by an OUTCOME the treatment moves (a turn
6591
- * handed its anchors is likelier to reach a file), which is the one bias an arm-based reading cannot absorb.
6519
+ * orientation. Dropping those instead would select the population by an OUTCOME the treatment moves, which
6520
+ * is the one bias an arm-based reading cannot absorb.
6592
6521
  *
6593
6522
  * Absent ⇒ as for `searchCalls`. */
6594
6523
  openingSearches: z.number().optional(),