instar 1.3.1221 → 1.3.1223
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/core/CodexDeliveryObserver.d.ts.map +1 -1
- package/dist/core/CodexDeliveryObserver.js +8 -8
- package/dist/core/CodexDeliveryObserver.js.map +1 -1
- package/dist/core/InboundDeliveryStore.d.ts.map +1 -1
- package/dist/core/InboundDeliveryStore.js +10 -2
- package/dist/core/InboundDeliveryStore.js.map +1 -1
- package/dist/core/ModelTierEscalation.d.ts +1 -1
- package/dist/core/ModelTierEscalation.d.ts.map +1 -1
- package/dist/core/ModelTierEscalation.js +10 -2
- package/dist/core/ModelTierEscalation.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +5 -0
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/SessionManager.d.ts.map +1 -1
- package/dist/core/SessionManager.js +13 -0
- package/dist/core/SessionManager.js.map +1 -1
- package/dist/core/StuckInputSentinel.d.ts.map +1 -1
- package/dist/core/StuckInputSentinel.js +16 -3
- package/dist/core/StuckInputSentinel.js.map +1 -1
- package/dist/core/topicProfileValidation.d.ts +2 -2
- package/dist/core/topicProfileValidation.js +3 -3
- package/dist/core/topicProfileValidation.js.map +1 -1
- package/dist/data/codexStageBReleaseEvidence.d.ts.map +1 -1
- package/dist/data/codexStageBReleaseEvidence.js +13 -12
- package/dist/data/codexStageBReleaseEvidence.js.map +1 -1
- package/dist/data/stageBCertifiedSet.d.ts +7 -7
- package/dist/data/stageBCertifiedSet.js +7 -7
- package/dist/data/stageBCertifiedSet.js.map +1 -1
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/lifeline/MessageQueue.d.ts +3 -0
- package/dist/lifeline/MessageQueue.d.ts.map +1 -1
- package/dist/lifeline/MessageQueue.js.map +1 -1
- package/dist/lifeline/TelegramLifeline.d.ts.map +1 -1
- package/dist/lifeline/TelegramLifeline.js +34 -14
- package/dist/lifeline/TelegramLifeline.js.map +1 -1
- package/dist/lifeline/queueDeliveryNotice.d.ts +14 -0
- package/dist/lifeline/queueDeliveryNotice.d.ts.map +1 -0
- package/dist/lifeline/queueDeliveryNotice.js +31 -0
- package/dist/lifeline/queueDeliveryNotice.js.map +1 -0
- package/dist/monitoring/SessionWatchdog.d.ts +24 -1
- package/dist/monitoring/SessionWatchdog.d.ts.map +1 -1
- package/dist/monitoring/SessionWatchdog.js +107 -19
- package/dist/monitoring/SessionWatchdog.js.map +1 -1
- package/dist/scaffold/templates.js +1 -1
- package/dist/server/routes.d.ts +14 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +33 -8
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/model-registry-freshness.manifest.json +1 -1
- package/src/data/builtin-manifest.json +65 -65
- package/src/data/codexStageBReleaseEvidence.ts +13 -12
- package/src/data/stageBCertifiedSet.ts +7 -7
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/src/scaffold/templates.ts +1 -1
- package/upgrades/1.3.1222.md +72 -0
- package/upgrades/1.3.1223.md +50 -0
- package/upgrades/inbound-delivery-unknown-terminalization.eli16.md +36 -0
- package/upgrades/side-effects/codex-gpt6-astra-allowlist.md +299 -0
- package/upgrades/side-effects/codex-observer-bounded-backlog.md +84 -0
- package/upgrades/side-effects/codex-test-worker-control-plane.md +86 -0
- package/upgrades/side-effects/inbound-delivery-unknown-terminalization.md +82 -0
- package/upgrades/side-effects/stage-b-evidence-code-binding.md +1 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "8e6606a6722433c5ffc15b3870402283c5406083a14cdbd0e18e735731e2aae2",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1223",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "61b875cd1e0246e1a8594066393f7fef2f166b258b2bc88e589814bdb295eb0c",
|
|
3
3
|
"registrySha256": "8e6606a6722433c5ffc15b3870402283c5406083a14cdbd0e18e735731e2aae2",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1223"
|
|
5
5
|
}
|
|
@@ -1729,7 +1729,7 @@ instar test-as-self --slack # Slack permission demonstr
|
|
|
1729
1729
|
|
|
1730
1730
|
Progress monitoring must not manufacture user-facing narration. These systems are signals, never gates, and every error path fails toward silence.
|
|
1731
1731
|
- **Silent-freeze watchdog** (ActiveWorkSilenceSentinel): before claiming a session is stuck, it re-captures the LIVE frame and corroborates — if the frame still shows an active-work indicator (spinner / "esc to interrupt"), a sub-agent is live, or it's a clean idle prompt, it stays SILENT. It speaks only when genuinely wedged, and even then hedges ("…hasn't changed in N min and a nudge didn't wake it — it may be stuck, or on a long task I can't see into. Want me to check?"). Threshold raised 15m→30m; a 90m frozen-indicator backstop still surfaces a real mid-tool hang. Tune/disable: \`monitoring.activeWorkSilenceSentinel.enabled\` (off), \`.silenceThresholdMs\` (default 30m), \`.activeWorkMaxFrozenIndicatorMs\` (default 90m).
|
|
1732
|
-
- **Command-watchdog attribution:** \`SessionWatchdog\` never interrupts the Codex framework host
|
|
1732
|
+
- **Command-watchdog attribution:** \`SessionWatchdog\` never interrupts the Codex framework host, known bounded external waiters, or pinned long-lived test/compiler services such as Vitest's esbuild service. A selected descendant is signaled directly only after an action-time PID/parent/argv identity check; the watchdog never sends pane-wide Ctrl+C for a descendant helper. If it interrupts a genuinely stuck user command, its audit entry names \`principal: session-watchdog\` and \`operatorInitiated: false\`; Codex's generic "aborted by user" text is transport wording, not proof the operator acted. Continuation is owned by the canonical continuation/recovery funnel, never an independent watchdog prompt.
|
|
1733
1733
|
- **Crash-safe inbound delivery:** Normal tmux injection and Codex stranded-draft key attempts use FULL-durable armed/started/terminal phases. A crash-open mutation becomes \`effect-unknown\` and is never blindly repeated. Diagnose privacy-safe counts at \`GET /sessions/inbound-delivery-status\`; no message body is exposed.
|
|
1734
1734
|
- **PromiseBeacon is internal by default:** commitment cadence, detection, revival, and escalation bookkeeping continue, but PromiseBeacon produces no topic/Slack messages, close-outs, escalation statuses, or Attention items. User output requires an explicit deployment opt-in with \`promiseBeacon.userOutputEnabled: true\`; do not enable it merely to retain follow-through. Effectiveness remains measurable in \`logs/sentinel-events.jsonl\` and the per-feature LLM-metrics surface (feature keys \`active-work-silence\`, \`promise-beacon\`).
|
|
1735
1735
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The session watchdog now recognizes Vitest's long-lived esbuild service as
|
|
9
|
+
framework infrastructure and excludes it before stuck-command judgment. If a
|
|
10
|
+
genuine descendant process must be stopped, the watchdog revalidates the exact
|
|
11
|
+
process identity and session incarnation immediately before signaling that
|
|
12
|
+
process, then escalates through bounded targeted signals instead of sending a
|
|
13
|
+
pane-wide interrupt that can collapse the surrounding Codex turn.
|
|
14
|
+
|
|
15
|
+
Codex session discovery now uses the maintained state cache on asynchronous
|
|
16
|
+
control-plane paths, and quiet Codex panes are not synchronously probed unless
|
|
17
|
+
there is evidence of a stranded draft. This keeps test execution from starving
|
|
18
|
+
health and Telegram handling. Recovery notices also state their actual queue
|
|
19
|
+
reason so operators can distinguish a queued continuation from a delivered
|
|
20
|
+
one.
|
|
21
|
+
|
|
22
|
+
Inbound deliveries whose physical effect cannot be proven after bounded
|
|
23
|
+
observation now enter an explicit terminal effect-unknown state. They remain
|
|
24
|
+
auditable without occupying live-capacity slots forever, preventing an
|
|
25
|
+
ambiguous historical backlog from blocking new Telegram messages.
|
|
26
|
+
|
|
27
|
+
The release canary also found a second observer defect under real load: when a
|
|
28
|
+
busy Codex transcript grew by more than one read budget, a read ending partway
|
|
29
|
+
through the next JSONL record discarded the valid complete prefix as unknown.
|
|
30
|
+
The observer now durably advances through the last complete record and re-reads
|
|
31
|
+
the partial tail on its next bounded sweep. Truly oversized single records,
|
|
32
|
+
malformed JSON, schema drift, and identity mismatches still fail closed.
|
|
33
|
+
|
|
34
|
+
## What to Tell Your User
|
|
35
|
+
|
|
36
|
+
Long-running Codex test work is protected from false watchdog interruptions,
|
|
37
|
+
and the agent's health and messaging paths remain responsive while those tests
|
|
38
|
+
run. Ambiguous old deliveries remain visible for investigation but no longer
|
|
39
|
+
crowd new messages out of the live queue.
|
|
40
|
+
|
|
41
|
+
## Summary of New Capabilities
|
|
42
|
+
|
|
43
|
+
- Automatic protection for the exact Vitest esbuild service process.
|
|
44
|
+
- Process-targeted watchdog remediation with action-time identity checks.
|
|
45
|
+
- Responsive cached session discovery on asynchronous control-plane paths.
|
|
46
|
+
- An explicit terminal state for deliveries whose physical effect is unknown.
|
|
47
|
+
- Multi-pass observation of valid busy-session transcript backlogs without
|
|
48
|
+
false unknown outcomes at non-aligned byte boundaries.
|
|
49
|
+
- Clear queue-reason wording in Codex recovery notices.
|
|
50
|
+
|
|
51
|
+
## Evidence
|
|
52
|
+
|
|
53
|
+
On topic 67366, the watchdog audit and live process tree showed the watchdog
|
|
54
|
+
selecting Vitest's persistent esbuild service and then issuing a pane-wide
|
|
55
|
+
interrupt; the surrounding test run collapsed while the local health endpoint
|
|
56
|
+
also stopped responding and Telegram retried the inbound message. The repaired
|
|
57
|
+
candidate's focused unit, HTTP integration, and production-lifecycle E2E gates
|
|
58
|
+
all pass, including a real long-running Vitest worker whose esbuild service
|
|
59
|
+
survives unchanged while the lifecycle suite completes. A backed-up copy of the
|
|
60
|
+
live inbound ledger contained 496 ambiguous rows consuming capacity; applying
|
|
61
|
+
the new terminal transition reduced the live count to four while retaining the
|
|
62
|
+
rows for audit, and normal inbound dispatch resumed.
|
|
63
|
+
|
|
64
|
+
The first exact-candidate two-hour/50-delivery canary correctly refused to sign
|
|
65
|
+
after finding three false unknowns on unrelated busy sessions. Their transcript
|
|
66
|
+
records proved the messages and responses existed beyond a non-aligned 256 KiB
|
|
67
|
+
scan boundary. The corrected scanner has unit, integration, and production-wired
|
|
68
|
+
E2E coverage for that exact multi-budget shape. The fresh corrected candidate
|
|
69
|
+
then passed 50/50 deliveries over 7,213,141 ms, all required special cases,
|
|
70
|
+
30/30 responsiveness samples, and zero false-unknown, exhaustion, duplicate
|
|
71
|
+
ownership, lost-input, or stale-owner outcomes. Echo signed the resulting
|
|
72
|
+
evidence and the certified source manifest binds its exact digest and bytes.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
instar gates codex model ids against a closed list — an unknown id is refused so a
|
|
9
|
+
typo can never launch a session that dies at startup. That list did not include
|
|
10
|
+
`gpt-6-astra`, so pinning a topic to it was refused `off-enum` even on machines whose
|
|
11
|
+
codex CLI runs the model perfectly well. Sessions launched with the model passed
|
|
12
|
+
directly were unaffected, which made the gap read as "instar doesn't support this
|
|
13
|
+
model" rather than "instar's list is stale."
|
|
14
|
+
|
|
15
|
+
The id is added, verified working first: codex CLI 0.153.4 runs it; 0.149.0 returns
|
|
16
|
+
`400 … requires a newer version of Codex`. Only observed-working ids are added — a
|
|
17
|
+
plausible sibling name that was never run is still refused, and a test now asserts
|
|
18
|
+
that refusal so the list cannot drift from "verified" to "seemed likely."
|
|
19
|
+
|
|
20
|
+
The second half is the durable one. The `/sessions/spawn` route did not read the
|
|
21
|
+
shared list; it carried its own hand-typed copy, "kept in lockstep" by comment with
|
|
22
|
+
nothing enforcing it. Because `KNOWN_MODEL_IDS['codex-cli']` already IS
|
|
23
|
+
`KNOWN_CODEX_MODEL_IDS`, the general arm every other framework uses yields an
|
|
24
|
+
identical list — so the codex special case is deleted rather than kept in sync. A
|
|
25
|
+
test asserts the two surfaces resolve to one list, and it was falsified by
|
|
26
|
+
deliberately re-splitting them before being trusted.
|
|
27
|
+
|
|
28
|
+
## What to Tell Your User
|
|
29
|
+
|
|
30
|
+
You can now pin a conversation to codex's gpt-6-astra model the way you pin any
|
|
31
|
+
other model — just say so in the topic. If your machine's codex tool is older than
|
|
32
|
+
version 0.153.4 the model itself will refuse at launch, so update it there first.
|
|
33
|
+
Nothing changes for any model that already worked.
|
|
34
|
+
|
|
35
|
+
## Summary of New Capabilities
|
|
36
|
+
|
|
37
|
+
- `gpt-6-astra` is a valid codex model id for topic-profile pins and session spawns.
|
|
38
|
+
- Unverified sibling `gpt-6-*` names still fail closed, now with a test holding that.
|
|
39
|
+
- The spawn route and the pin validator read one codex list instead of two, so a
|
|
40
|
+
future model can no longer be spawn-accepted while pin-refused.
|
|
41
|
+
|
|
42
|
+
## Evidence
|
|
43
|
+
|
|
44
|
+
Unit tests cover the new id against the pin validator and the spawn route, the
|
|
45
|
+
fail-closed refusal of an unverified sibling id, and the single-list parity between
|
|
46
|
+
`KNOWN_MODEL_IDS['codex-cli']` and `KNOWN_CODEX_MODEL_IDS`. Each was falsified before
|
|
47
|
+
being trusted: removing the id fails both acceptance tests for the right reason, and
|
|
48
|
+
pointing the map at a filtered copy fails the parity guard. Typecheck clean; full unit
|
|
49
|
+
suite green. Live verification of the model against both codex CLI versions is
|
|
50
|
+
recorded in the side-effects artifact.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Inbound Delivery Capacity Recovery — Plain-English Overview
|
|
2
|
+
|
|
3
|
+
> The one-line version: delivery attempts whose observation window ended uncertain must stop occupying the finite live-message queue.
|
|
4
|
+
|
|
5
|
+
## The problem in one breath
|
|
6
|
+
|
|
7
|
+
Instar safely refused to guess whether hundreds of older injected messages were consumed, marking them “unknown.” However, it left their transport state as “dispatched,” and the admission counter treats every dispatched row as live. Once 500 such rows accumulated, every new Telegram message was rejected with “Message could not be delivered to the session.”
|
|
8
|
+
|
|
9
|
+
## What already exists
|
|
10
|
+
|
|
11
|
+
- **Durable inbound journal** — records each physical terminal injection before it happens so crashes cannot silently replay uncertain effects.
|
|
12
|
+
- **Codex delivery observer** — watches the composer and rollout transcript until it proves consumption/response or reaches a fixed observation deadline.
|
|
13
|
+
- **Admission ceilings** — prevent an unbounded number of live rows or bytes from exhausting the server.
|
|
14
|
+
- **Terminal evidence retention** — keeps consumed, failed, and effect-unknown evidence for audit, then garbage-collects it under bounded age/count rules.
|
|
15
|
+
|
|
16
|
+
## What this adds
|
|
17
|
+
|
|
18
|
+
When the observer reaches its deadline without proof, the existing `markObservationUnknown` transition now records `effect-unknown` as the transport state as well as unknown transcript/composer eligibility. This does not claim that the message succeeded or failed. It only makes the already non-replayable uncertainty terminal, allowing the admission counter to release its slot while retaining the evidence for audit.
|
|
19
|
+
|
|
20
|
+
The status projection now reports these records as `effect-unknown` before considering their eligibility label, so uncertain-effect metrics remain honest. A regression test fills a one-row admission capacity, terminalizes the uncertain observation, verifies capacity returns, and verifies that a subsequent message can be prepared.
|
|
21
|
+
|
|
22
|
+
## The safeguards
|
|
23
|
+
|
|
24
|
+
**No automatic replay.** Effect-unknown is explicitly non-replayable, preserving the core no-duplicate guarantee.
|
|
25
|
+
|
|
26
|
+
**No evidence deletion.** Rows are retained under the existing terminal evidence policy. This change is a state transition, not data erasure.
|
|
27
|
+
|
|
28
|
+
**Bounded authority.** Only dispatched rows that the observer already selected after its observation deadline can take this transition. Prepared or actively observed rows are untouched.
|
|
29
|
+
|
|
30
|
+
## What ships when
|
|
31
|
+
|
|
32
|
+
The transition and status correction ship together. Existing stale rows on this machine are migrated with the identical narrow predicate after a complete SQLite backup; future rows self-terminalize through the observer.
|
|
33
|
+
|
|
34
|
+
## What you actually need to decide
|
|
35
|
+
|
|
36
|
+
Should expired, already-uncertain delivery evidence become terminal so it cannot permanently block new Telegram messages?
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Side-Effects Review — codex `gpt-6-astra` allowlist + codex spawn-arm de-duplication
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `codex-gpt6-astra-allowlist`
|
|
4
|
+
**Date:** 2026-09-05
|
|
5
|
+
**Author:** Echo (instar-dev agent)
|
|
6
|
+
**Second-pass reviewer:** required (touches `/sessions/spawn` allow decision — session lifecycle)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Two changes, one root cause. (1) `gpt-6-astra` is added to `KNOWN_CODEX_MODEL_IDS`
|
|
11
|
+
(`src/core/ModelTierEscalation.ts`) — the closed enum the topic-profile validator and the
|
|
12
|
+
spawn route both gate codex model ids against. It was absent, so pinning a topic to a model
|
|
13
|
+
that codex CLI ≥ 0.153.4 runs fine was refused `off-enum`, while sessions launched with the
|
|
14
|
+
model passed directly ran it happily — a capability the agent HAD but could not be
|
|
15
|
+
configured to use. (2) The spawn route's codex special-case arm in `src/server/routes.ts` is
|
|
16
|
+
deleted. That arm read `CODEX_MODELS_SUBSCRIPTION`, a hand-maintained literal copy of the
|
|
17
|
+
same list, "kept in lockstep" by comment only with nothing enforcing it. `KNOWN_MODEL_IDS['codex-cli']`
|
|
18
|
+
IS `KNOWN_CODEX_MODEL_IDS`, so the general arm every other framework already uses yields the
|
|
19
|
+
identical list; deleting the special case removes the drift class instead of documenting it.
|
|
20
|
+
|
|
21
|
+
Files: `src/core/ModelTierEscalation.ts`, `src/server/routes.ts`,
|
|
22
|
+
`tests/unit/topicProfileValidation.test.ts`, `tests/unit/route-validation-edge.test.ts`.
|
|
23
|
+
|
|
24
|
+
## Decision-point inventory
|
|
25
|
+
|
|
26
|
+
- `validateModelId` (`src/core/topicProfileValidation.ts`, reads `KNOWN_MODEL_IDS`) — **modify (data only)** — the closed enum it gates topic-profile model pins against gains one live-verified id. The check's logic and fail-closed shape are untouched.
|
|
27
|
+
- `POST /sessions/spawn` model allowlist (`src/server/routes.ts`) — **modify (source of the list, not the decision)** — the codex arm is removed; codex now reads the same canonical per-framework map as every other framework. Accepted set is provably identical to the post-(1) codex list.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 1. Over-block
|
|
32
|
+
|
|
33
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
34
|
+
|
|
35
|
+
None newly. The change only ever WIDENS the accepted set, by exactly one id
|
|
36
|
+
(`gpt-6-astra`). Every id accepted before is still accepted: `KNOWN_MODEL_IDS['codex-cli']`
|
|
37
|
+
is a reference to `KNOWN_CODEX_MODEL_IDS`, and `GENERIC_TIERS` is still spread in both arms.
|
|
38
|
+
The residual over-block is pre-existing and deliberate: sibling `gpt-6-*` names that were
|
|
39
|
+
never live-verified are still refused `off-enum`. That is the intended fail-closed posture
|
|
40
|
+
(a typo must not strand a topic on a model the CLI will reject at launch), and it is now
|
|
41
|
+
asserted by a test rather than left implicit.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 2. Under-block
|
|
46
|
+
|
|
47
|
+
**What failure modes does this still miss?**
|
|
48
|
+
|
|
49
|
+
- The enum cannot tell whether the LOCAL codex CLI is new enough. `gpt-6-astra` needs codex
|
|
50
|
+
CLI ≥ 0.153.4; 0.149.0 returns `400 … requires a newer version of Codex`. A pin set on a
|
|
51
|
+
machine with a current CLI, then resolved on a machine with an older one, still fails at
|
|
52
|
+
launch. That is unchanged by this PR and is the same shape the 5.6 family already carries
|
|
53
|
+
(its comment records the ≥ 0.144.0 floor). Naming it rather than fixing it here: a CLI
|
|
54
|
+
version probe at pin time is a real feature with its own failure modes, not a line in an
|
|
55
|
+
enum. <!-- tracked: ACT-420 -->
|
|
56
|
+
- The enum is a static list, so a genuinely new codex model is refused until a human adds it.
|
|
57
|
+
That is the deliberate closed-enum design (`§5.2(c)`), not a miss.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 3. Level-of-abstraction fit
|
|
62
|
+
|
|
63
|
+
Correct layer, and the change moves it further in the right direction. The list is data
|
|
64
|
+
consumed by two existing validators; this PR adds an entry and deletes a duplicate reader.
|
|
65
|
+
It does not add a new check, a new layer, or a parallel path. The deletion specifically
|
|
66
|
+
removes a lower-quality copy in favour of the canonical primitive that already existed —
|
|
67
|
+
the "does a lower-level primitive already exist that this should USE instead of
|
|
68
|
+
re-implementing?" question, answered by deletion.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 4. Signal vs authority compliance
|
|
73
|
+
|
|
74
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
75
|
+
|
|
76
|
+
- [x] No — this change has no block/allow surface of its own.
|
|
77
|
+
|
|
78
|
+
The blocking authority (a closed-enum validator) already existed and is unchanged in shape,
|
|
79
|
+
strictness, and failure mode. This PR edits the DATA the authority reads and removes a
|
|
80
|
+
duplicate copy of that data. No brittle logic gains authority; no authority is weakened —
|
|
81
|
+
unknown ids still fail closed.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## 4b. Judgment-point check (Judgment Within Floors standard)
|
|
86
|
+
|
|
87
|
+
No new static heuristic at a competing-signals decision point. The domain is enumerable by
|
|
88
|
+
construction: it is a closed set of model identifiers accepted by a CLI, which is exactly
|
|
89
|
+
the "it's an invariant, name it" case. There are no competing live signals to arbitrate —
|
|
90
|
+
an id is in the vendor's supported set or it is not.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 5. Interactions
|
|
95
|
+
|
|
96
|
+
- **Shadowing:** none introduced. The codex arm and the general arm were mutually exclusive
|
|
97
|
+
branches of one ternary; collapsing them removes a branch rather than reordering checks.
|
|
98
|
+
`GENERIC_TIERS` still precedes the per-framework ids in the spread, unchanged.
|
|
99
|
+
- **Double-fire:** not applicable — validation is a pure function of the request body.
|
|
100
|
+
- **Races:** none. Both constants are module-level frozen-by-convention arrays read
|
|
101
|
+
synchronously; no shared mutable state.
|
|
102
|
+
- **Feedback loops:** none. The enum is not written by anything at runtime.
|
|
103
|
+
- **Adjacent consumers — full inventory** (the first draft named only the first two; the
|
|
104
|
+
second-pass reviewer found three more by grepping the tree, so this list is now the result
|
|
105
|
+
of a sweep rather than recall):
|
|
106
|
+
- `escalatedModelIds` / `resolveTierModel` (`ModelTierEscalation.ts`) — fail closed on an
|
|
107
|
+
out-of-enum config value. A `models.tierEscalation.frameworks['codex-cli']` naming
|
|
108
|
+
`gpt-6-astra` now resolves instead of nulling; the shipped codex default is
|
|
109
|
+
`{ default: null, escalated: null }`, so nothing changes without a deliberate operator
|
|
110
|
+
config edit, which is the intended effect.
|
|
111
|
+
- `ModelSwapService.ts:254` — clamps a topic's pinned baseline against
|
|
112
|
+
`KNOWN_MODEL_IDS[framework]` before a **live-session** model swap. This is the most
|
|
113
|
+
material consumer of the three and the first draft missed it: widening the list means a
|
|
114
|
+
topic pinned to `gpt-6-astra` can now be swapped onto rather than clamped away. That is
|
|
115
|
+
the intended effect (it is the same authority the pin itself passed), and the swap path
|
|
116
|
+
keeps its own independent verification — but it deserved naming, not discovery.
|
|
117
|
+
- `ProfileIntentClassifier.ts:157` (`defaultKnownModelValues`) — the union is the
|
|
118
|
+
extraction vocabulary for conversational pins, so "use gpt-6-astra here" now resolves to
|
|
119
|
+
a model value instead of falling through unrecognised. Intended, and the reason the
|
|
120
|
+
conversational surface works at all.
|
|
121
|
+
- `CodexCliIntelligenceProvider.ts:496` — reads the list benignly; no behavior change.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 6. External surfaces
|
|
126
|
+
|
|
127
|
+
- **Other agents on the same machine:** none — no shared state touched.
|
|
128
|
+
- **Install base:** the only user-visible change is that a topic-profile pin naming
|
|
129
|
+
`gpt-6-astra` now succeeds where it returned `off-enum`. No previously-working
|
|
130
|
+
configuration changes behavior.
|
|
131
|
+
- **External systems:** none. instar does not call the model vendor here; it validates a
|
|
132
|
+
string that is later handed to the codex CLI.
|
|
133
|
+
- **Persistent state:** none written or migrated. A topic profile that could not previously
|
|
134
|
+
store this model id simply had nothing stored.
|
|
135
|
+
- **Timing / runtime conditions:** none introduced. The pre-existing dependence on the local
|
|
136
|
+
codex CLI version is recorded under §2.
|
|
137
|
+
- **Operator surface (Mobile-Complete Operator Actions):** no new operator-facing action.
|
|
138
|
+
The existing surface — the operator saying "use gpt-6-astra here" in the topic, which the
|
|
139
|
+
topic-profile write path already accepts from a verified operator — is phone-complete and
|
|
140
|
+
unchanged. This PR is what makes that sentence succeed rather than be refused.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## 6b. Operator-surface quality
|
|
145
|
+
|
|
146
|
+
No operator surface — not applicable. No dashboard renderer, approval page, or
|
|
147
|
+
grant/revoke/secret-drop form is touched.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
152
|
+
|
|
153
|
+
**Posture: machine-local BY DESIGN — with a replicated consumer, and that split is the
|
|
154
|
+
interesting part.**
|
|
155
|
+
|
|
156
|
+
The enum itself is compiled-in constant data, so it is machine-local in the same trivial
|
|
157
|
+
sense all code is: each machine runs whatever instar version it has installed. The reason
|
|
158
|
+
this is BY DESIGN rather than an oversight is that the thing it gates — a topic profile
|
|
159
|
+
pin — IS replicated (`multiMachine.stateSync.topicPins`). So the honest posture is: the
|
|
160
|
+
DATA is per-version, the STATE it validates is pool-wide.
|
|
161
|
+
|
|
162
|
+
That produces one real cross-machine behavior worth stating: a pin set on a machine running
|
|
163
|
+
this version replicates to a peer still on ≤ 1.3.1222, whose validator does not know the id.
|
|
164
|
+
This is not a new failure mode — it is exactly the documented "a pinned model that is no
|
|
165
|
+
longer available falls back to defaults with a once-per-transition notice" path, which is
|
|
166
|
+
already the shipped contract for a version-skewed pool and degrades to a working session
|
|
167
|
+
with a notice rather than a block. A rolling fleet update converges it, and the
|
|
168
|
+
machine-coherence guard already surfaces version skew.
|
|
169
|
+
|
|
170
|
+
- **User-facing notices:** emits none. The fallback notice above is pre-existing and already
|
|
171
|
+
one-voice gated.
|
|
172
|
+
- **Durable state on topic transfer:** holds none of its own. The topic pin it validates is
|
|
173
|
+
already replicated and already survives transfer.
|
|
174
|
+
- **Generated URLs:** none.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 8. Rollback cost
|
|
179
|
+
|
|
180
|
+
- **Hot-fix release:** revert the commit, ship as the next patch. Pure code change.
|
|
181
|
+
- **Data migration:** none — but one honest caveat. If an operator pins a topic to
|
|
182
|
+
`gpt-6-astra` and the change is then reverted, that stored pin becomes an unknown id on
|
|
183
|
+
the next resolve. It does NOT wedge the topic: the shipped behavior is fallback-to-default
|
|
184
|
+
with a once-per-transition notice. Cost is one notice per affected topic, no repair.
|
|
185
|
+
- **Agent state repair:** none required.
|
|
186
|
+
- **User visibility during rollback:** a topic pinned to the new id falls back to the
|
|
187
|
+
framework default and says so. No silent behavior change.
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## Conclusion
|
|
192
|
+
|
|
193
|
+
The review changed the shape of the fix twice, both times toward less code. The first draft
|
|
194
|
+
edited two lists in lockstep — which reproduced, in the same commit, the exact
|
|
195
|
+
comment-enforced coupling that caused the bug. The second draft made the route read the
|
|
196
|
+
canonical constant. The final version deletes the codex arm entirely, because
|
|
197
|
+
`KNOWN_MODEL_IDS['codex-cli']` already IS that constant and the general arm was provably
|
|
198
|
+
equivalent. Net effect: one id added, one duplicate reader removed, one drift class closed.
|
|
199
|
+
|
|
200
|
+
Two findings were surfaced rather than silently absorbed. The CLI-version floor (§2) is a
|
|
201
|
+
genuine residual gap and is tracked, not hand-waved. The version-skewed-pool pin (§7) is a
|
|
202
|
+
real cross-machine interaction, and it lands on an existing, correct degradation path.
|
|
203
|
+
|
|
204
|
+
The second-pass review then rejected the first version of the drift guard, correctly. That
|
|
205
|
+
guard iterated `KNOWN_MODEL_IDS['codex-cli']` and asserted `validateModelId` — but the
|
|
206
|
+
validator reads that same map, so the loop was tautological, and nothing in it read the
|
|
207
|
+
spawn route at all. The reviewer's negative control proved it: re-splitting the route to a
|
|
208
|
+
literal carrying 3 of 9 codex ids left the entire suite green. I had "falsified" that guard
|
|
209
|
+
by breaking the map alias, which it did catch — and mistook that for proof it watched the
|
|
210
|
+
route, which it never did. That is the precise failure mode of a guard that is blind to its
|
|
211
|
+
own subject, reproduced inside the very commit whose purpose is to close a drift class.
|
|
212
|
+
|
|
213
|
+
The fix is structural rather than another assertion: `spawnModelAllowlist(framework)` is
|
|
214
|
+
exported from `routes.ts`, the handler calls it, and the guard reads THAT — the route's own
|
|
215
|
+
accepted set — in both directions (the route accepts nothing the validator refuses, and
|
|
216
|
+
drops nothing the validator accepts). The reviewer's exact control now fails on
|
|
217
|
+
`gpt-5.2 missing from the spawn route`.
|
|
218
|
+
|
|
219
|
+
The review also caught that the commit re-planted the instruction it was deleting: three
|
|
220
|
+
comments still told a maintainer to "keep in lockstep with CODEX_MODELS_SUBSCRIPTION",
|
|
221
|
+
including one I newly added, pointing at a constant this change removes. All three are
|
|
222
|
+
rewritten. Clear to ship.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Second-pass review (required — touches the `/sessions/spawn` allow decision)
|
|
227
|
+
|
|
228
|
+
**Reviewer:** independent reviewer subagent, 2026-09-05
|
|
229
|
+
**Independent read of the artifact: CONCERN — resolved, re-review recommended**
|
|
230
|
+
|
|
231
|
+
The reviewer verified sections A, B, E, §7 and §8 against the code (including confirming
|
|
232
|
+
that `CODEX_MODELS_SUBSCRIPTION` and `KNOWN_CODEX_MODEL_IDS` were entry-identical and
|
|
233
|
+
same-order on `origin/main`, so the deletion is a provable no-op on the accepted set, and
|
|
234
|
+
that `?? KNOWN_CLAUDE_MODEL_IDS` is unreachable for codex because `KNOWN_MODEL_IDS` is an
|
|
235
|
+
exhaustive `Record` over the framework union). Five findings were raised; all five are
|
|
236
|
+
resolved in this commit:
|
|
237
|
+
|
|
238
|
+
1. **(blocking) The drift guard was blind to its own subject.** Proven by negative control:
|
|
239
|
+
re-splitting the route to a literal dropping 6 of 9 codex ids left both test files green,
|
|
240
|
+
58/58. Resolved — `spawnModelAllowlist` is exported and the guard reads the route's own
|
|
241
|
+
list bidirectionally; the same control now fails on
|
|
242
|
+
`codex-cli pinnable id gpt-5.2 missing from the spawn route`.
|
|
243
|
+
2. **The commit re-planted the instruction it deletes.** Three "keep in lockstep with
|
|
244
|
+
CODEX_MODELS_SUBSCRIPTION" comments survived, one newly added by this change, pointing at
|
|
245
|
+
the constant it removes. Resolved — all three rewritten; zero references remain in `src/`.
|
|
246
|
+
3. **Consumer inventory incomplete.** `ModelSwapService.ts:254`, `ProfileIntentClassifier.ts:157`
|
|
247
|
+
and `CodexCliIntelligenceProvider.ts:496` were unlisted. Resolved — §5 now carries the
|
|
248
|
+
swept inventory, with the live-session swap path called out as the material one.
|
|
249
|
+
4. **Falsification evidence not reproducible.** The quoted output did not correspond to the
|
|
250
|
+
committed code. Resolved — both controls re-run against the shipped code and quoted
|
|
251
|
+
verbatim under Evidence.
|
|
252
|
+
5. **Spawn-side coverage reduced under a wrong justification.** Resolved — `gpt-5.6-terra`
|
|
253
|
+
restored to the HTTP sample, and the comment now points at the route-observing guard
|
|
254
|
+
rather than claiming the validator test covers the route.
|
|
255
|
+
|
|
256
|
+
The reviewer's `Concern raised` verdict stands against the version it read. Every finding it
|
|
257
|
+
raised is addressed above; the load-bearing one changed the design.
|
|
258
|
+
|
|
259
|
+
**Re-review verdict: "Concur with the review"** (same reviewer, against the fixed tree). It
|
|
260
|
+
re-ran both injection controls itself and reproduced the quoted evidence verbatim (2/56 and
|
|
261
|
+
1/57), instrumented the guard's per-framework loop to confirm neither direction runs vacuous
|
|
262
|
+
(codex 9/9, claude 10/10, gemini 3/3 ids after the generic-tier filter), confirmed the
|
|
263
|
+
handler has exactly one list-computation site and that the test import creates no runtime
|
|
264
|
+
cycle (the CapabilityIndex back-reference is type-only). Two non-blocking nits were raised
|
|
265
|
+
and are fixed in this commit: a dangling `CODEX_MODELS_SUBSCRIPTION` reference in the
|
|
266
|
+
model-registry freshness manifest's descriptive note, and `grok-build` added to the guard
|
|
267
|
+
loop (the reviewer verified it passes; it was ROUND-21's original victim framework, so its
|
|
268
|
+
absence from the guard was the omission most worth closing). `pi-cli` stays omitted — its
|
|
269
|
+
enum is closed-empty by design, so both loop directions would be vacuous.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## Evidence pointers
|
|
274
|
+
|
|
275
|
+
- Live verification of the model id, Mac Mini, 2026-09-05: codex CLI 0.149.0 →
|
|
276
|
+
`400 invalid_request_error: The 'gpt-6-astra' model requires a newer version of Codex.`
|
|
277
|
+
After upgrading to 0.153.4, the same one-shot prompt returns a normal completion.
|
|
278
|
+
- Refusal that prompted the change: `POST /topic-profile/69507` →
|
|
279
|
+
`{"failure":"off-enum","reason":"'gpt-6-astra' is not a known codex-cli model id"}`, on an
|
|
280
|
+
agent whose own Studio machine was concurrently running seven sessions on that model.
|
|
281
|
+
- Falsification runs, re-captured against the COMMITTED code after the second-pass review
|
|
282
|
+
(the first draft quoted `expected [ …(6) ] to be [ …(7) ]`, which corresponded to an
|
|
283
|
+
intermediate version of the guard, not to what shipped — the reviewer was right that it
|
|
284
|
+
was unreproducible):
|
|
285
|
+
- **Control A — remove the id from the enum:** 2 failed | 56 passed.
|
|
286
|
+
`accepts codex-cli GPT-5.6 + GPT-6 family model values` → `expected 400 not to be 400`;
|
|
287
|
+
`accepts the GPT-6 family against codex-cli` → `expected { field: 'model', …(4) } to be null`.
|
|
288
|
+
- **Control B — re-split the route to a literal dropping 6 of 9 codex ids** (the reviewer's
|
|
289
|
+
own control, which the FIRST guard passed green): 1 failed | 57 passed.
|
|
290
|
+
`the spawn ROUTE accepts every id the pin validator does, for every framework` →
|
|
291
|
+
`codex-cli pinnable id gpt-5.2 missing from the spawn route: expected [ Array(3) ] to include 'gpt-5.2'`.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Class-Closure Declaration (display-only mirror)
|
|
296
|
+
|
|
297
|
+
No agent-authored-artifact defect — not applicable. This is a defect in hand-maintained
|
|
298
|
+
source data (a duplicated constant), not in an LLM prompt, hook, config, skill, or standards
|
|
299
|
+
text; and it adds no self-triggered controller.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Side-Effects Review — Codex observer bounded backlog
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `codex-observer-bounded-backlog`
|
|
4
|
+
**Date:** `2026-09-04`
|
|
5
|
+
**Author:** Codex
|
|
6
|
+
**Second-pass reviewer:** independent Codex reviewer
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The Codex delivery observer now advances every complete JSONL prefix inside its bounded read even when the byte budget ends partway through the next record. The incomplete tail is left unread and starts the next sweep. A single record with no newline inside the full budget remains typed `unknown`.
|
|
11
|
+
|
|
12
|
+
This was found by the exact-candidate Stage-B canary: all 50 dedicated rows converged, but three real messages in busy sessions generated more than 256 KiB between sweeps. The old scanner marked the entire valid backlog unknown solely because byte 262,144 was not a newline, even though hundreds of complete record boundaries preceded it. Transcript inspection proved the injected user events and assistant responses were present.
|
|
13
|
+
|
|
14
|
+
## Decision-point inventory
|
|
15
|
+
|
|
16
|
+
- `scanSharedRollout` — modify — advances only through the last complete newline in a bounded read.
|
|
17
|
+
- `scanRollout` — modify — applies the same prefix rule to the compatibility scanner.
|
|
18
|
+
|
|
19
|
+
## 1. Over-block
|
|
20
|
+
|
|
21
|
+
No new block is introduced. A record larger than the entire per-row budget still has no complete boundary and remains unknown; schema drift and malformed complete JSON remain unknown.
|
|
22
|
+
|
|
23
|
+
## 2. Under-block
|
|
24
|
+
|
|
25
|
+
An incomplete tail is not parsed or trusted. It is re-read from its exact first byte on the next sweep. The cursor advances only to a verified newline, so no partial JSON can be skipped or treated as evidence.
|
|
26
|
+
|
|
27
|
+
## 3. Level-of-abstraction fit
|
|
28
|
+
|
|
29
|
+
This is byte-framing behavior in the JSONL scanner, the layer that owns cursor advancement. It does not change delivery authority, replay policy, or message interpretation.
|
|
30
|
+
|
|
31
|
+
## 4. Signal vs authority compliance
|
|
32
|
+
|
|
33
|
+
Required reference: [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md).
|
|
34
|
+
|
|
35
|
+
- [x] No — this change has no block/allow judgment surface.
|
|
36
|
+
|
|
37
|
+
No new judgment is added. A complete newline is structural framing evidence; absence of any boundary at the hard budget remains the conservative uncertainty floor.
|
|
38
|
+
|
|
39
|
+
## 4b. Judgment-point check
|
|
40
|
+
|
|
41
|
+
No new static heuristic is added at a competing-signals decision point. This is an enumerable JSONL framing invariant: only bytes through a complete newline may advance the durable cursor.
|
|
42
|
+
|
|
43
|
+
## 5. Interactions
|
|
44
|
+
|
|
45
|
+
- **Races:** scans use the stat/read prefix already in place; bytes appended after the read wait for a later sweep.
|
|
46
|
+
- **Budgets:** each sweep still reads at most the configured per-row and aggregate limits.
|
|
47
|
+
- **Retries:** progress is durable in `inbound_rollout_cursor`; restart resumes from the last complete boundary.
|
|
48
|
+
- **Failure posture:** malformed complete JSON, unsupported events, identity mismatch, truncation, and an oversized single record still fail closed.
|
|
49
|
+
|
|
50
|
+
## 6. External surfaces
|
|
51
|
+
|
|
52
|
+
Busy Codex sessions no longer show false unknown delivery outcomes or strand valid messages from observability merely because their transcript grew by more than one scan budget. No API shape or user-facing wording changes.
|
|
53
|
+
|
|
54
|
+
## 6b. Operator-surface quality
|
|
55
|
+
|
|
56
|
+
No operator surface is changed — not applicable.
|
|
57
|
+
|
|
58
|
+
## 7. Multi-machine posture
|
|
59
|
+
|
|
60
|
+
Machine-local by design: each observer reads the local transcript for the delivery effect performed on that machine. Ownership and transfer fencing are unchanged.
|
|
61
|
+
|
|
62
|
+
## 8. Rollback cost
|
|
63
|
+
|
|
64
|
+
Pure scanner code; revert is mechanically simple but would restore demonstrated false-unknown outcomes under large valid transcript bursts.
|
|
65
|
+
|
|
66
|
+
## Conclusion
|
|
67
|
+
|
|
68
|
+
The first canary correctly refused certification after exposing three false unknowns. The fix preserves every hard bound and uncertainty rule while distinguishing a safely resumable complete prefix from a truly boundary-free oversized record. Unit, integration, and production-wired E2E tests cover the non-aligned multi-budget case. The fresh exact candidate subsequently passed 50/50 deliveries over 7,213,141 ms, the complete case matrix, 30/30 responsiveness samples, and zero forbidden outcomes; its signed evidence is now bound to the certified sources.
|
|
69
|
+
|
|
70
|
+
## Second-pass review
|
|
71
|
+
|
|
72
|
+
Concur. Cursor advancement is restricted to complete newline boundaries, partial bytes remain unread, and the next sweep revalidates them from the record start. The change cannot authorize delivery or replay and narrows false uncertainty without weakening malformed-input handling. The reviewer noted that the compatibility scanner needed the same direct regression assertion as the production shared path; that coverage was added before commit.
|
|
73
|
+
|
|
74
|
+
## Evidence pointers
|
|
75
|
+
|
|
76
|
+
- `tests/unit/CodexDeliveryObserver.test.ts`
|
|
77
|
+
- `tests/integration/codex-observer-bounded-backlog.test.ts`
|
|
78
|
+
- `tests/e2e/codex-session-lifecycle-reliability.test.ts`
|
|
79
|
+
- First canary ledger: 50/50 dedicated rows responded; three candidate-window false unknowns on busy sessions identified at non-aligned >256 KiB backlogs.
|
|
80
|
+
- Corrected canary ledger: 50/50 rows consumed/cleared/responded; zero false unknown, exhaustion, duplicate ownership, lost inbound, or stale-owner outcomes; signed artifact digest `a2db23a95530681953ffa3002ab14e349ebf53d19be2a54c1f4afacc3aead997`.
|
|
81
|
+
|
|
82
|
+
## Class-Closure Declaration
|
|
83
|
+
|
|
84
|
+
No agent-authored-artifact defect and no added or modified self-triggered controller — not applicable. The regression is nevertheless pinned at all three required test tiers by the evidence above.
|