Skip to content

Commit 22c1254

Browse files
wave-l: budgets that were spent by failure (v0.18.2)
v0.18.1 left one measured defect open: fix-a-bug hit its 20-minute ceiling in one run of three with failed_tasks == 0. Chasing it found the cause, and the cause turned out to be a pattern rather than a bug. Six mechanisms here bounded themselves with a fixed count, charged that count for negative or failed attempts, and so switched themselves off on precisely the runs that needed them. Every one failed silently. The objective probe went blind three waves into a run. maxObjectiveProbes was 2 and the budget was charged for RED answers, so between-waves probing spent it on the earliest waves — the two moments when "not yet" is most certain and least worth paying to learn. The budget is per RUN, not per board, so two probes was two for an entire run, and the post-drain probe that corrective boards rely on (they run with early-stop off) was starved by the same exhaustion. A count also cannot be right for two projects at once. The bound is economic now: the command's cost is measured and asking continues while runway >= 6 x cost. A slow endpoint lost structured decoding for seven days. One deadline covers up to six sequential capability probes; a cold local model could spend most of it loading weights for the first, and attempt() returns false for a transport error exactly as for a 400. An all-false record was then stamped Source="probe" and honored for CapabilityTTL, with no path back because nothing re-probes a fresh record. A cut-short negotiation now falls back to the family preset and leaves Probed zero. Preferring the preset is the recoverable direction: an over-claimed mechanism costs one 400 and demoteCapability records it; an under-claimed one has nothing that can ever notice. Retrieval went blind when the corpus was most on-topic. The "corpus median" was measured over Search's already-truncated top-k, so with TopK=5 the threshold was third-best + NoiseMargin: at most two hits could survive, and a tightly clustered top five cleared nothing at all and returned "" with no error. A canceled QA gate reported red. Every cancellation path returned "the gate failed", which finalizeAfterExecute turns into a synthesized tester verdict through applyTesterFeedback — a planner call. Ctrl-C bought an LLM round-trip on the way out and annotated a done task about a gate never allowed to run. The QA gate re-ran unchanged trees: qaDiagnoseAndFix never reported whether it wrote anything, so a fix pass that produced nothing was followed by an identical command run at full price. Measured on a stalled gate: 4 command runs down to 1. A cold start pinned concurrency for a month. Profile.Current checked ID, MaxParallel, version and age but not Partial, so the degenerate max_parallel=1 a budget-exhausted probe produces was cached for DefaultTTL — for the slowest models, the ones with most to gain from a real measurement. The thrash detector blinded itself on thrashing runs. The signature map FROZE at MaxSignatures, and the classic near-miss ws_edit retry mints a distinct signature each attempt, so a thrashing run filled it faster and went blind sooner. Measured: 5 identical calls after 256 distinct ones counted 0 repeats, not 4. Measured, three runs per scenario, Qwen3.5-9B, same fixtures and ceiling: v0.18.1 v0.18.2 implement-from-tests median tokens 123,652 75,535 (-39%) implement-from-tests pass rate 3 of 3 3 of 3 fix-a-bug median tokens ~178,000 ~182,000 fix-a-bug pass rate 2 of 3 2 of 3 One scenario improved substantially and the other did not move. The probe fix is confirmed firing in a live run — the metrics record objective_met_early passed, ending a run in 6 LLM calls and 375s — but it was not what fix-a-bug was failing on. That scenario needs 8 tool calls and 69k tokens on a good run and 32 calls and 192k on a bad one; in the run that hits the ceiling the harness still returns a correct result, and only the suite's wall budget fails. Model variance, not a harness that cannot tell it is done. Wall-clock is absent from that table on purpose: the same suite showed fewer tokens taking more seconds across the session, so throughput degraded under an hour of continuous local GPU load. Token counts do not depend on machine speed. Three further findings are documented in the changelog rather than shipped, because their benefit cannot be proven without a controlled study: the 20ms reviewerStrictDelay means two reviewer requests per review with the second discarded almost always; readBudgetLines sizes ws_read from the legacy prompt-byte budget rather than the model's real context window; and loop-guard interventions are charged against the ReAct iteration budget.
1 parent fc0df86 commit 22c1254

21 files changed

Lines changed: 1229 additions & 68 deletions

‎Makefile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MODULE := github.com/UnicoLab/slmcode
22
BIN := slmcode
3-
VERSION ?= 0.18.1
3+
VERSION ?= 0.18.2
44
PREFIX ?= $(HOME)/.local
55
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
66
BUILD_TIME := $(shell date -u +%Y-%m-%dT%H:%M:%SZ)

‎cmd/slmcode/version.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ package main
44
//
55
// go build -ldflags "-X main.Version=0.5.0 -X main.SourceRoot=/path -X main.GitCommit=abc -X main.BuildTime=…"
66
var (
7-
Version = "0.18.1"
7+
Version = "0.18.2"
88
SourceRoot = "" // absolute path to the slmcode checkout used to build this binary
99
GitCommit = "unknown"
1010
BuildTime = "unknown"

‎docs/changelog.md‎

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,177 @@
11
# Changelog
22

3+
## v0.18.2 — 2026-08-25
4+
5+
Budgets that were spent by failure.
6+
7+
v0.18.1 left one measured defect unsolved: `fix-a-bug` hit its 20-minute ceiling
8+
in one run of three with `failed_tasks == 0` — nothing wrong except that nobody
9+
asked whether the work was done. Chasing it found the cause, and the cause
10+
turned out to be a **pattern** rather than a bug. Six mechanisms in this
11+
codebase bounded themselves with a fixed count, charged that count for negative
12+
or failed attempts, and so switched themselves off precisely on the runs that
13+
needed them. Every one of them failed silently.
14+
15+
**Measured, three runs per scenario, Qwen3.5-9B on oMLX, same fixtures and the
16+
same 20-minute ceiling as the v0.18.1 measurement:**
17+
18+
| | v0.18.1 | v0.18.2 |
19+
|---|---|---|
20+
| `implement-from-tests` median prompt tokens | 123,652 | **75,535 (−39%)** |
21+
| `implement-from-tests` pass rate | 3 of 3 | 3 of 3 |
22+
| `fix-a-bug` median prompt tokens | ~178,000 | ~182,000 |
23+
| `fix-a-bug` pass rate | 2 of 3 | 2 of 3 |
24+
| runs terminating within budget | 5 of 6 | 5 of 6 |
25+
26+
**Read that table honestly: one scenario improved substantially and the other
27+
did not move.** The probe-starvation defect was real, is fixed, and is confirmed
28+
firing in a live run — the run metrics record
29+
`"gates":[{"name":"qa_gate","passed":true},{"name":"objective_met_early","passed":true}]`,
30+
which is the between-waves probe ending a run the moment the objective went
31+
green, in 6 LLM calls and 375s. But it was **not** what `fix-a-bug` was failing
32+
on, and that scenario's 1-in-3 ceiling miss is unchanged.
33+
34+
What that remaining miss actually is, measured: the same 9B needs 8 tool calls
35+
and 69k tokens for this three-line boundary fix on a good run and **32 tool
36+
calls and 192k tokens** on a bad one. In the run that hits the ceiling the
37+
harness still returns a CORRECT result — `engine_success=true`, the fixture
38+
tests green, the protected test file byte-identical, `failed_tasks=0`. The only
39+
failing assertion is the suite's own wall-clock budget. So this is model
40+
variance, not a harness that cannot tell it is done.
41+
42+
The identified next step, with evidence behind it and deliberately NOT taken
43+
here: the probe fires only between waves, so when a worker fixes the bug at tool
44+
call 10 of 32, nothing notices until the task ends. That worker runs the
45+
objective command itself through `ws_shell` during those calls, and the harness
46+
already sees the output — a green result there is free evidence the probe could
47+
harvest without spending anything. It is a real design and an unproven one, and
48+
this release does not ship unproven changes.
49+
50+
Wall-clock is deliberately absent from the table above. Across these runs the
51+
same suite showed FEWER tokens taking MORE seconds (75k/792s here against
52+
124k/608s for v0.18.1), i.e. throughput degraded over an hour of continuous
53+
local GPU load. Wall times are not comparable between sessions on this hardware;
54+
token counts do not depend on machine speed, so they are what is reported.
55+
56+
### Fixed
57+
58+
- **The objective probe no longer goes blind three waves into a run.**
59+
`maxObjectiveProbes` was 2, and the budget was charged for RED answers.
60+
Between-waves probing spends it on the earliest waves — the two moments in a
61+
run when "not yet" is most certain and least worth paying to learn — so from
62+
wave three nothing ever asked again. If the implementation landed at wave
63+
five, the run burned its whole ceiling with the work already complete. The
64+
budget is also **per run, not per board**: a run drives `RunBoard` once per
65+
corrective round, so two probes was two for the entire run, and the post-drain
66+
probe that corrective boards deliberately rely on (they run with early-stop
67+
off) was starved by the same exhaustion.
68+
A count also cannot be right for two projects at once — two probes is miserly
69+
for a 200ms unit suite and profligate for a 6-minute integration suite. The
70+
bound is now economic: the probe's cost is **measured** (`SmokeResult.Duration`,
71+
timed where the command actually runs) and asking continues while
72+
`runway >= 6 × cost`. A cheap gate is asked on every wave that wrote
73+
something; an expensive one only while there is runway for the answer to pay
74+
for itself. Neither number is guessed per project.
75+
- **A slow endpoint no longer loses structured decoding for seven days.**
76+
One deadline covers up to six sequential capability probes. A cold local model
77+
— the exact case the budget was widened for — could spend most of it loading
78+
weights for the first, after which every later probe died on the shared
79+
deadline. `attempt` returns false for a transport error exactly as for a 400,
80+
so the negotiation could not tell "the server refused this field" from "we
81+
never got to ask", and stamped `Source: "probe"` on a wholesale-false record.
82+
`capCache` then persisted and honored it for `CapabilityTTL` — seven days in
83+
which every structured role on that endpoint silently degraded to prompt-only
84+
+ repair, with no path back, because nothing re-probes a record that is still
85+
fresh. A cut-short negotiation now falls back to the family preset and leaves
86+
`Probed` zero, which keeps it out of the on-disk cache. Preferring the preset
87+
over all-false is the recoverable direction: an over-claimed mechanism costs
88+
one 400 and is then recorded by `demoteCapability`; an under-claimed one has
89+
nothing that can ever notice it.
90+
- **Retrieval no longer goes blind when the corpus is most on-topic.**
91+
The relative noise floor is documented as "the corpus's own median", but it
92+
was measured over the value `Search` returns — already truncated to `TopK`.
93+
With the default `TopK=5` the "median" was the **third-best hit**, so the
94+
threshold became third-best + `NoiseMargin`: arithmetically at most two hits
95+
could ever survive, and a tightly clustered top five — a set of uniformly
96+
strong matches — cleared nothing at all and `RetrieveForQuery` returned `""`
97+
with no error and no warning. It also made `MinChunksForNoiseFloor`
98+
unreachable for its stated purpose, since it compared against `len(top-k)` and
99+
never the corpus size. `Retriever.SearchAll` now supplies the whole scored
100+
distribution for the floor, and the top-k truncation happens after.
101+
- **A cancelled QA gate no longer reports a red verdict.** `runQAGate` returns
102+
"the gate failed", and every cancellation path returned **true**.
103+
`finalizeAfterExecute` reads that as `QAFailed`, sets `TesterRejected`, and
104+
feeds the board a synthesized tester verdict
105+
(`{"passed":false,...,"qa_gate red"}`) through `applyTesterFeedback` — a
106+
planner call. So Ctrl-C, or a scenario budget expiring, bought an extra LLM
107+
round-trip during shutdown and annotated a done task with "QA gate still
108+
failing" about a gate never allowed to finish. A cancelled gate now records
109+
nothing: no verdict, no annotation.
110+
- **The QA gate stops re-running an unchanged tree.** `qaDiagnoseAndFix`
111+
discarded both role outputs and never reported whether anything was written,
112+
so a fix pass that produced nothing (budget exhausted, a refusal, a prose-only
113+
answer) was followed by a byte-identical command run against a byte-identical
114+
tree, at full price, for every remaining round. The objective probe has
115+
refused exactly this since it was written; the gate never learned it. Measured
116+
on a stalled gate: 4 command runs down to 1. Gate rounds also now price the
117+
command for the probe budget, since they run the same one.
118+
- **A cold start no longer pins concurrency for a month.** The calibration probe
119+
runs inside a fixed wall-clock budget in which every unit of work is a model
120+
call — the thing being measured. On a slow server the warm-up and solo
121+
baseline can exhaust it before any concurrency level is measured, leaving only
122+
the synthetic single-level entry, from which `SelectKnee` returns 1.
123+
`Profile.Current` checked ID, `MaxParallel`, version and age but **not
124+
`Partial`**, so that degenerate verdict was served from cache for
125+
`DefaultTTL`; `Apply` only checks `MaxParallel > 0`, and the "partial" marker
126+
appears solely in `Summary()`, which the auto path never prints. So the
127+
slowest models — the ones with the most to gain from a real measurement — were
128+
silently capped at `max_parallel=1` for thirty days. Partial profiles now
129+
expire after `PartialTTL` (1 hour): a cold start is transient, so the retry
130+
should be too.
131+
- **The thrash detector no longer blinds itself on thrashing runs.** The
132+
signature map **froze** at `MaxSignatures` — once full, a signature it had not
133+
already seen was never admitted, so its later repeats were never counted. The
134+
asymmetry is what makes it bite: the classic small-model edit failure is a
135+
near-miss `ws_edit` retried with a slightly different `old_str` each time, and
136+
every one of those is a *distinct* signature, so a thrashing run fills the map
137+
faster than a healthy one and goes blind sooner. Measured: 5 identical calls
138+
after 256 distinct ones counted **0** repeats instead of 4.
139+
`RunReport.RedundantCalls` and the redundant-call-rate KPI under-reported with
140+
nothing marking the count as capped, and evolve learned from the truncated
141+
signal. It now evicts oldest-first, like every other bound in that file.
142+
- **A reviewer that timed out is no longer recorded as one that judged the work
143+
worthless.** The synthesized error attempt carries `NoVerdict`, and
144+
`Attempt.Score` documents that a 0 under an `error` verdict is an absence, not
145+
a judgement.
146+
- **`autoresearch` no longer claims a surface was exhausted when it was not.**
147+
`StopExhausted` is reached on `ErrNoProposal` from the *deterministic*
148+
proposer, which cannot touch a text knob at all — yet its sentence read "every
149+
value of every knob was tried", the one message in that package built to be
150+
trusted, while every sibling carefully says when the surface was *not*
151+
exhausted.
152+
153+
### Known, reported rather than changed
154+
155+
Three further findings are real and measured but are **behaviour changes whose
156+
benefit cannot be proven without a controlled study**, so they are documented
157+
instead of shipped unmeasured:
158+
159+
- `reviewerStrictDelay` (20ms) means the default `max_parallel=4` issues **two
160+
reviewer LLM requests per review**, and `strictOut` is used only when the
161+
primary reply is empty — so the second is discarded almost always. The code is
162+
honest about the cost (`noteExtraRequests` reports it), but on a local server
163+
that runs inference serially this roughly doubles review latency. A delay
164+
derived from measured reviewer p50 would keep the insurance and drop the cost.
165+
- `readBudgetLines` sizes `ws_read` from `MaxContextKB`, the legacy prompt-byte
166+
budget, rather than the model's real `ContextLimit` — the exact conflation
167+
`compact.WindowTokensFromKB` is already marked Deprecated for. On typical
168+
source that caps a read around 80–120 lines whatever the model's real window
169+
is. Fixing it would raise read sizes several-fold on a large-context model,
170+
which needs measuring before it ships.
171+
- `agents.factory` charges loop-guard interventions against the ReAct iteration
172+
budget (both escalation paths return a successful tool result), and a model
173+
profile's `max_turns` can only *lower* the default 8, never raise it.
174+
3175
## v0.18.1 — 2026-08-25
4176

5177
The harness stops when the work is done.

‎pkg/autoresearch/ratchet.go‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,21 @@ func (r StopReason) Sentence() string {
6767
case StopTokens:
6868
return "token budget spent — the surface was NOT exhausted, so more remains untried"
6969
case StopExhausted:
70-
return "surface exhausted — every value of every knob was tried"
70+
// "every knob" was not true and this is the one message in the package
71+
// built to be trusted — every sibling above goes out of its way to say
72+
// when the surface was NOT exhausted.
73+
//
74+
// StopExhausted is reached on ErrNoProposal from the DETERMINISTIC
75+
// proposer, which enumerates values and therefore cannot touch a text
76+
// knob at all (see surface.go: a text domain has no enumeration and
77+
// returns nil). A text knob such as system_prompt is reachable only
78+
// through the LLM proposer, which is asked on a fixed cadence and gives
79+
// up its slot on a model error, an empty reply, or an unchanged
80+
// rewrite — so a small local model can burn every chance and leave the
81+
// knob untried while this line claimed it had been swept.
82+
return "enumerable surface exhausted — every value of every enumerable knob " +
83+
"was tried; text knobs, if the surface has any, are reachable only " +
84+
"through the LLM proposer and may remain untried"
7185
case StopCanceled:
7286
return "canceled — the surface was restored to its pre-run state"
7387
case StopEvalFailed:

‎pkg/backends/capabilities.go‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,10 @@ func runProbe(ctx context.Context, provider, endpoint, model, apiKey string) Cap
352352
"stream": false,
353353
}
354354
reachable := false
355+
// cutShort records that the SHARED negotiation deadline expired part way
356+
// through. Without it a timeout is indistinguishable from "the server
357+
// rejected this field" — see the note above the return.
358+
cutShort := false
355359
attempt := func(extra map[string]any) bool {
356360
body := make(map[string]any, len(base)+len(extra))
357361
for k, v := range base {
@@ -362,6 +366,9 @@ func runProbe(ctx context.Context, provider, endpoint, model, apiKey string) Cap
362366
}
363367
status, err := probeOnce(pctx, client, url, apiKey, body)
364368
if err != nil {
369+
if pctx.Err() != nil {
370+
cutShort = true
371+
}
365372
return false
366373
}
367374
reachable = true
@@ -415,6 +422,29 @@ func runProbe(ctx context.Context, provider, endpoint, model, apiKey string) Cap
415422
if !reachable {
416423
return Capabilities{Source: "unreachable"}
417424
}
425+
if cutShort {
426+
// ONE deadline covers up to six sequential requests, and a cold local
427+
// model — the exact case this budget was widened for — can spend most of
428+
// it loading weights for the first. When it expires part way through,
429+
// every field not yet asked about is false because it was NEVER ASKED,
430+
// not because the server refused it. Stamping that as `Source: "probe"`
431+
// used to publish an all-false record and, because Probed was set, cache
432+
// it for CapabilityTTL — seven days in which every structured role on
433+
// this endpoint silently degraded to prompt-only + repair, with no path
434+
// back: nothing re-probes a record that is still fresh.
435+
//
436+
// So fall back to the family preset and leave Probed ZERO, which is what
437+
// keeps this out of the on-disk cache (capCache.put skips zero-Probed
438+
// records) so the next process negotiates again from scratch.
439+
//
440+
// Preferring the preset over all-false is deliberate, and the asymmetry
441+
// is the argument: an over-claimed mechanism costs ONE 400 on first use
442+
// and is then recorded by demoteCapability, while an under-claimed one
443+
// costs a week of degraded decoding that nothing can detect. Guessing in
444+
// the recoverable direction is the whole point.
445+
prior.Source = "prior-probe-timeout"
446+
return prior
447+
}
418448
out.Probed = time.Now()
419449
out.Source = "probe"
420450
return out

‎pkg/backends/probetimeout_test.go‎

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package backends
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"sync/atomic"
8+
"testing"
9+
"time"
10+
)
11+
12+
// TestSlowEndpointDoesNotPoisonCapabilitiesForAWeek is the regression guard for
13+
// a defect with the worst possible blast radius: a COLD LOCAL MODEL — the exact
14+
// case slmcode exists to serve — silently losing structured decoding for seven
15+
// days.
16+
//
17+
// THE DEFECT: one deadline covers up to six sequential probe requests. If
18+
// weight-loading ate most of it, the plain probe still succeeded (setting
19+
// `reachable`), and every later probe died on the shared deadline. `attempt`
20+
// returns false for a transport error exactly as it does for a 400, so the
21+
// negotiation could not tell "the server refused this field" from "we never got
22+
// to ask". It then stamped Source="probe" and Probed=now on a wholesale-false
23+
// record, which capCache persisted and honored for CapabilityTTL. Nothing
24+
// re-probes a record that is still fresh, so there was no path back: every
25+
// structured role on that endpoint degraded to prompt-only + repair for a week.
26+
func TestSlowEndpointDoesNotPoisonCapabilitiesForAWeek(t *testing.T) {
27+
var calls int64
28+
// Every request costs 100ms — a stand-in for a model still loading weights.
29+
// The first one fits inside the budget and succeeds; the rest cannot.
30+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
31+
atomic.AddInt64(&calls, 1)
32+
time.Sleep(100 * time.Millisecond)
33+
w.Header().Set("Content-Type", "application/json")
34+
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"ok"}}]}`))
35+
}))
36+
defer srv.Close()
37+
38+
restore := ProbeTimeout
39+
ProbeTimeout = 150 * time.Millisecond
40+
defer func() { ProbeTimeout = restore }()
41+
42+
// runProbe directly: Probe() memoises per key and would hide a second call.
43+
got := runProbe(context.Background(), "omlx", srv.URL, "slow-model", "k")
44+
45+
if n := atomic.LoadInt64(&calls); n < 2 {
46+
t.Fatalf("the server saw %d request(s); the test needs the first to "+
47+
"succeed and a later one to hit the shared deadline", n)
48+
}
49+
// The load-bearing assertion. A zero Probed is what capCache.put checks
50+
// before persisting, so this is precisely what keeps a half-finished
51+
// negotiation out of the on-disk cache and out of the next process.
52+
if !got.Probed.IsZero() {
53+
t.Fatalf("a probe cut short by its own deadline was stamped as a "+
54+
"completed one (Probed=%s, Source=%q) — capCache will now persist it "+
55+
"and honor it for %s", got.Probed, got.Source, CapabilityTTL)
56+
}
57+
if got.Source == "probe" {
58+
t.Fatalf("Source=%q claims a completed negotiation", got.Source)
59+
}
60+
// Structured decoding must survive. Falling back to the family preset is
61+
// safe in a way that all-false is not: an over-claimed mechanism costs one
62+
// 400 and is then recorded by demoteCapability, while an under-claimed one
63+
// has nothing that can ever notice it.
64+
if !got.JSONSchema || !got.JSONObject {
65+
t.Fatalf("a slow endpoint lost structured decoding entirely: %+v — "+
66+
"omlx's preset supports json_schema and json_object", got)
67+
}
68+
}
69+
70+
// TestCompletedProbeStillWins is the control: without it the test above could
71+
// pass by never trusting a probe at all.
72+
func TestCompletedProbeStillWins(t *testing.T) {
73+
// json_schema is refused, everything else answered — a real negotiation
74+
// with a real negative result in it.
75+
f := newFakeServer(t, "json_object", "tools")
76+
restore := ProbeTimeout
77+
ProbeTimeout = 10 * time.Second
78+
defer func() { ProbeTimeout = restore }()
79+
80+
got := runProbe(context.Background(), "omlx", f.URL, "fast-model", "k")
81+
82+
if got.Source != "probe" || got.Probed.IsZero() {
83+
t.Fatalf("a negotiation that finished was not trusted: %+v", got)
84+
}
85+
if got.JSONSchema {
86+
t.Fatal("json_schema was reported supported though the server answered 400")
87+
}
88+
if !got.JSONObject {
89+
t.Fatal("json_object was refused though the server accepted it")
90+
}
91+
}

0 commit comments

Comments
 (0)