instar 1.3.593 → 1.3.594

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.
@@ -0,0 +1,48 @@
1
+ /**
2
+ * selfQuotaState — compute a machine's placement-eligibility block for the capacity
3
+ * heartbeat (spec: docs/specs/placement-llm-circuit-aware-quota.md).
4
+ *
5
+ * `quotaState.blocked` is the signal `PlacementExecutor` reads to decide whether a machine
6
+ * may serve a new LLM session. It is a **placement-eligibility** signal — "this machine
7
+ * cannot serve LLM work right now" — NOT a pure account-quota readout. Until now its only
8
+ * cause was account-quota exhaustion; an OPEN llm-circuit (the machine's provider calls are
9
+ * actually failing, rate-limited) is a second, more direct cause and MUST also block. The
10
+ * live test caught the gap: a machine with an open circuit reported `blocked:false` and
11
+ * placement routed a session onto it that died on arrival (2026-06-16, the Mac Mini).
12
+ *
13
+ * Extracted as a pure function so the two-signal contract (account-quota OR circuit) is
14
+ * unit-testable and cannot silently regress to quota-only.
15
+ */
16
+ export interface QuotaSnapshot {
17
+ blockedUntil?: string | null;
18
+ fiveHourPercent?: number | null;
19
+ blockReason?: string | null;
20
+ }
21
+ /**
22
+ * Type-level discriminator for the block CAUSE so consumers branch on a closed set, not a
23
+ * free string. `provider-block` / `five-hour-*` = account quota; `llm-circuit-open` =
24
+ * operational unavailability. Free-form legacy quota strings still flow through, typed as the
25
+ * open `string` arm (back-compat).
26
+ */
27
+ export type SelfQuotaBlockReason = 'llm-circuit-open' | 'five-hour-exhausted' | 'provider-block' | (string & {});
28
+ export interface SelfQuotaBlock {
29
+ blocked: boolean;
30
+ blockedUntil?: string;
31
+ reason?: SelfQuotaBlockReason;
32
+ }
33
+ /**
34
+ * Compute this machine's placement-eligibility block from BOTH the account-quota snapshot and
35
+ * the live llm-circuit availability.
36
+ *
37
+ * @param quota the account-quota snapshot (`quotaTracker.getState()`), or null/undefined when
38
+ * there is no tracker.
39
+ * @param circuitAvailable `llmCircuitAvailable()` — `true` when the breaker is disabled OR
40
+ * closed; `false` only when it is enabled AND open/half-open.
41
+ * @returns a block object, `{ blocked: false }`, or `undefined` (unknown ≠ blocked).
42
+ *
43
+ * Fail-open is preserved: an open circuit is the ONLY thing that newly blocks, and only on a
44
+ * *positively observed* unavailable circuit — missing information (no tracker + closed
45
+ * circuit) still returns `undefined`, which `PlacementExecutor` treats as not-blocked.
46
+ */
47
+ export declare function computeSelfQuotaState(quota: QuotaSnapshot | null | undefined, circuitAvailable: boolean, now?: number): SelfQuotaBlock | undefined;
48
+ //# sourceMappingURL=selfQuotaState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selfQuotaState.d.ts","sourceRoot":"","sources":["../../src/core/selfQuotaState.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,aAAa;IAC5B,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED;;;;;GAKG;AACH,MAAM,MAAM,oBAAoB,GAC5B,kBAAkB,GAClB,qBAAqB,GACrB,gBAAgB,GAChB,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,oBAAoB,CAAC;CAC/B;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,aAAa,GAAG,IAAI,GAAG,SAAS,EACvC,gBAAgB,EAAE,OAAO,EACzB,GAAG,GAAE,MAAmB,GACvB,cAAc,GAAG,SAAS,CAgB5B"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * selfQuotaState — compute a machine's placement-eligibility block for the capacity
3
+ * heartbeat (spec: docs/specs/placement-llm-circuit-aware-quota.md).
4
+ *
5
+ * `quotaState.blocked` is the signal `PlacementExecutor` reads to decide whether a machine
6
+ * may serve a new LLM session. It is a **placement-eligibility** signal — "this machine
7
+ * cannot serve LLM work right now" — NOT a pure account-quota readout. Until now its only
8
+ * cause was account-quota exhaustion; an OPEN llm-circuit (the machine's provider calls are
9
+ * actually failing, rate-limited) is a second, more direct cause and MUST also block. The
10
+ * live test caught the gap: a machine with an open circuit reported `blocked:false` and
11
+ * placement routed a session onto it that died on arrival (2026-06-16, the Mac Mini).
12
+ *
13
+ * Extracted as a pure function so the two-signal contract (account-quota OR circuit) is
14
+ * unit-testable and cannot silently regress to quota-only.
15
+ */
16
+ /**
17
+ * Compute this machine's placement-eligibility block from BOTH the account-quota snapshot and
18
+ * the live llm-circuit availability.
19
+ *
20
+ * @param quota the account-quota snapshot (`quotaTracker.getState()`), or null/undefined when
21
+ * there is no tracker.
22
+ * @param circuitAvailable `llmCircuitAvailable()` — `true` when the breaker is disabled OR
23
+ * closed; `false` only when it is enabled AND open/half-open.
24
+ * @returns a block object, `{ blocked: false }`, or `undefined` (unknown ≠ blocked).
25
+ *
26
+ * Fail-open is preserved: an open circuit is the ONLY thing that newly blocks, and only on a
27
+ * *positively observed* unavailable circuit — missing information (no tracker + closed
28
+ * circuit) still returns `undefined`, which `PlacementExecutor` treats as not-blocked.
29
+ */
30
+ export function computeSelfQuotaState(quota, circuitAvailable, now = Date.now()) {
31
+ // An open llm-circuit is a hard block regardless of the account-quota poll — the machine's
32
+ // provider calls are failing right now, so it cannot serve a session. This wins even when
33
+ // there is no quota snapshot (a machine with no tracker but an open circuit is still blocked).
34
+ if (!circuitAvailable)
35
+ return { blocked: true, reason: 'llm-circuit-open' };
36
+ if (!quota)
37
+ return undefined; // no tracker + circuit ok = unknown ≠ blocked
38
+ const blockActive = !!quota.blockedUntil && Date.parse(quota.blockedUntil) > now;
39
+ const fiveHourExhausted = (quota.fiveHourPercent ?? 0) >= 95;
40
+ if (!blockActive && !fiveHourExhausted)
41
+ return { blocked: false };
42
+ return {
43
+ blocked: true,
44
+ blockedUntil: quota.blockedUntil ?? undefined,
45
+ reason: quota.blockReason ??
46
+ (fiveHourExhausted ? `5-hour window at ${quota.fiveHourPercent}%` : 'provider block'),
47
+ };
48
+ }
49
+ //# sourceMappingURL=selfQuotaState.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"selfQuotaState.js","sourceRoot":"","sources":["../../src/core/selfQuotaState.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA0BH;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,qBAAqB,CACnC,KAAuC,EACvC,gBAAyB,EACzB,MAAc,IAAI,CAAC,GAAG,EAAE;IAExB,2FAA2F;IAC3F,0FAA0F;IAC1F,+FAA+F;IAC/F,IAAI,CAAC,gBAAgB;QAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC5E,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC,CAAC,8CAA8C;IAC5E,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,YAAY,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,YAAY,CAAC,GAAG,GAAG,CAAC;IACjF,MAAM,iBAAiB,GAAG,CAAC,KAAK,CAAC,eAAe,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;IAC7D,IAAI,CAAC,WAAW,IAAI,CAAC,iBAAiB;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAClE,OAAO;QACL,OAAO,EAAE,IAAI;QACb,YAAY,EAAE,KAAK,CAAC,YAAY,IAAI,SAAS;QAC7C,MAAM,EACJ,KAAK,CAAC,WAAW;YACjB,CAAC,iBAAiB,CAAC,CAAC,CAAC,oBAAoB,KAAK,CAAC,eAAe,GAAG,CAAC,CAAC,CAAC,gBAAgB,CAAC;KACxF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.593",
3
+ "version": "1.3.594",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-06-16T09:18:10.303Z",
5
- "instarVersion": "1.3.593",
4
+ "generatedAt": "2026-06-16T09:33:12.134Z",
5
+ "instarVersion": "1.3.594",
6
6
  "entryCount": 201,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,35 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ Fixed a multi-machine placement bug where a rate-limited machine could still be picked to serve
9
+ new conversations. The "can this machine take work?" health flag was computed only from the
10
+ slow account-usage poll and ignored the circuit breaker that trips when a machine's AI calls are
11
+ actually failing. So a machine with an open circuit advertised itself as available, and a new
12
+ conversation routed there died on arrival. Placement now treats an open (or recovering) circuit
13
+ as "blocked", so it steers new work to a machine that can actually answer. Caught by applying
14
+ the live-testing standard to the multi-machine transfer.
15
+
16
+ ## What to Tell Your User
17
+
18
+ If one of your machines is rate-limited, I now send new conversations to a machine that can
19
+ actually answer, instead of into a dead end where you'd get a "session stopped" notice. The only
20
+ time nothing improves is when every machine is rate-limited at once — then there is genuinely
21
+ nowhere good to send it, and I say so honestly. Single-machine setups are unaffected, and there
22
+ is no new setting to learn.
23
+
24
+ ## Summary of New Capabilities
25
+
26
+ | Capability | How to Use |
27
+ |-----------|-----------|
28
+ | Placement avoids a machine whose AI calls are currently failing | Automatic; a rate-limited machine shows as "blocked — llm-circuit-open" in the machines view and new conversations skip it |
29
+
30
+ ## Evidence
31
+
32
+ - 8 new unit tests (`tests/unit/selfQuotaState.test.ts`, both sides of every boundary) + a new
33
+ PlacementExecutor test asserting a circuit-open machine is skipped; tsc clean.
34
+ - Release gate: live two-machine re-run — a circuit-open machine reports blocked in the pool view
35
+ and a new conversation lands on a machine that can serve it.
@@ -0,0 +1,50 @@
1
+ # Side-Effects Review — Quota-aware placement must see the open LLM circuit
2
+
3
+ **Slug:** placement-llm-circuit-aware-quota
4
+ **Spec:** docs/specs/placement-llm-circuit-aware-quota.md
5
+ **Parent principle:** No Silent Degradation to a Brittle Fallback — a machine that cannot serve LLM work must report itself blocked, not present a healthy-looking signal that misroutes work onto it.
6
+
7
+ ## What changed
8
+
9
+ `selfQuotaState()` (the capacity-heartbeat block `PlacementExecutor` reads) derived `blocked`
10
+ ONLY from the account-quota poll, ignoring the `LlmCircuitBreaker`. So a machine whose CLI was
11
+ actually rate-limited (circuit OPEN) could still report `quotaState:{blocked:false}` and
12
+ placement would route a session onto it that dies on arrival. Caught live (2026-06-16, the Mac
13
+ Mini) by the gold-standard live test of the multi-machine transfer.
14
+
15
+ Fix: extract `computeSelfQuotaState(quota, circuitAvailable)` into `src/core/selfQuotaState.ts`
16
+ (pure, unit-tested), OR-ing an open/half-open circuit (`!llmCircuitAvailable()`) into the block.
17
+ `server.ts` calls it with the live quota snapshot AND `llmCircuitAvailable()`.
18
+
19
+ ## Blast radius
20
+
21
+ - **Multi-machine placement (the target):** a circuit-open machine is now excluded from
22
+ placement; new sessions land on a machine that can serve them. This was BROKEN before — net
23
+ improvement, strictly better whenever ANY machine is available.
24
+ - **All-machines-circuit-open:** identical to today's all-account-quota-blocked case —
25
+ placement falls back to least-loaded and flags `all-machines-quota-blocked` (now possibly
26
+ carrying `llm-circuit-open` reasons). Not a regression; nowhere good to route by definition.
27
+ - **Single machine:** no peers to compare; the existing all-blocked least-loaded fallback
28
+ applies, unchanged.
29
+ - **Fail-open preserved:** only a positively-observed open circuit newly blocks. Missing info
30
+ (no tracker + closed circuit, or any throw) → `undefined` = unknown ≠ blocked, exactly as
31
+ before. A DISABLED breaker reports available → never a false block.
32
+ - **Wire field:** `quotaState` keeps its name (a replicated heartbeat field consumed
33
+ cross-machine + by the dashboard; renaming is a separate wire migration). Its MEANING is
34
+ pinned by the doc contract + a new `SelfQuotaBlockReason` enum (`llm-circuit-open` |
35
+ `five-hour-exhausted` | `provider-block` | string). No consumer keys on a specific reason
36
+ string, so widening the cause set is non-breaking.
37
+
38
+ ## Reversibility
39
+
40
+ Governed by the existing `intelligence.circuitBreaker` config — disabling the breaker (or it
41
+ never tripping) keeps `llmCircuitAvailable()` true so this never blocks. Revertable by reverting
42
+ the two-file diff; no durable state is written (the block is recomputed per heartbeat).
43
+
44
+ ## Risk / monitoring
45
+
46
+ Low. Pure-function change off the hot path (computed per 30s heartbeat). Observable via
47
+ `GET /pool` (`quotaState.reason: 'llm-circuit-open'`) + the existing placement decision flags +
48
+ the already-audited `[llm-circuit]` transitions in `logs/server.log`. Release gate: the live
49
+ two-machine re-run — a circuit-open machine shows `blocked:true` in `/pool` and a new
50
+ conversation lands on a machine that can serve it.