Uh oh!
There was an error while loading. Please reload this page.
feat(hp): cloud-only enforcement + cloud iters/frames reach the Run Inspector - #219
Merged
Conversation
…forced Selecting Piccolissimo + Altissimo was supposed to mean "this solve runs in Harmoniqs Cloud". In practice the agent kept dispatching it LOCALLY: the laptop precompiled the whole HP stack (IPOPT included) until amico-run's process-group timeout SIGTERMed Julia mid-precompile. Three things allowed that, none of them the pieces people assumed were missing (the API-key prompt and the entitlement flip were already built and work): 1. Nothing refused a local HP launch. The gate's tier=hpc rules are solid, but they only fire on a spec that SAYS tier="hpc" — and runGate only runs for --spec launches at all. A bare `amico-run script.jl` never reached them. 2. Selecting HP grants the `issimo` entitlement, so the import scan happily admits a local `using Piccolissimo`. The entitlement that unlocks the tier was also unlocking the failure mode. 3. The agent was told two contradictory things. solverModeSection() said "launch HP solves in the cloud" while the routing section beside it said "routing is PER-SOLVE and EXPLICIT, you confirm where EVERY solve runs, default local" — and the base AGENTS.md step 5 said the same. Given a cloud-only tier and an instruction to default local, it defaulted local. The fix, in the same two-layer shape the tier already uses: - ENFORCEMENT: amico-run refuses `--executor local` while HP is selected, at the one choke point every run passes through (launch.ts, before the gate), so it covers spec and no-spec runs alike. Reads the extension's solver-mode.json — status only, never a token — and fails SAFE to piccolo on an absent or corrupt file, so a fresh install and every free-tier local run behave exactly as now. - GUIDANCE: the routing section now states the cloud-only contract instead of asking a routing question, and AGENTS.md step 5 makes the injected section authoritative over its local default. Piccolo sessions are untouched: the section is still "" unless mode is hp AND the cloud is connected. The estimate keeps its reporting role and loses its deciding role — an estimate that fits in local RAM does not make an HP solve local. Also names the service "Harmoniqs Cloud" everywhere a user can see it (gate refusals, routing prose, AGENTS.md, the HP row's tooltips) instead of the internal "company compute" / bare "the cloud". The wire id stays `company-compute` — renaming it would break the credential route, the status cache, and the fork's CONNECTION_IDS for no user-visible gain. Tests: 9 new (the reader's fail-safe directions; the refusal, including on the no-spec path and that it leaves no run dir; that a REMOTE HP launch still completes, against FakeCloud) + the routing/AGENTS contract updated to pin the new copy, with an explicit regression test that the per-solve question does not come back. amico-run 934 passed, extension suite green apart from two live-model E2Es (interview_e2e, scores_e2e) that fail identically with this change stashed — pre-existing, tracked separately. Incidental but load-bearing: the CLI tests were inheriting the DEVELOPER's ~/.amico/amicode/solver-mode.json, so on an hp machine nine local-solve tests failed on correct behaviour. They now spawn with a hermetic ops dir. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A cloud solve produced an empty Run Inspector. The client-side chain was fully
built — RemoteExecutor polls the cloud, synthesizes AMICODE_ITER lines into
run.log, writes frames, and touches run.log's mtime for the stall logic — but it
read the WRONG FIELDS, so it dropped everything:
stats client read `body.iters` · service returns `{task_id, stats[], submitter}`
frames client read `png_base64` · service returns `{task_id, iter, key, url, submitter}`
Both reads sit inside best-effort try/catch blocks ("stats are advisory"), so
the failure was completely silent: `undefined ?? []` iterated zero times, run.log
stayed 0 bytes, and the Inspector had nothing to tail. Confirmed against task
419a57e6 on staging, which ran 60 IPOPT iterations and banked 11 frames to S3
while the local run.log stayed empty.
The reason nobody caught it: fake_cloud.ts served the shapes the CLIENT read,
not the shapes the SERVICE returns. Every test passed against a fake that agreed
with the bug. That is the real defect here, so the fake is corrected first and
two tests now pin the live payloads by exact key set — a fake that mirrors the
client proves nothing.
- stats: read `stats`, falling back to `iters` so an older runner still works.
- frames: fetch the presigned url (no auth header — the signature IS the
credential) and keep the base64 lane for older runners. FakeCloud now serves
artifact bytes from a route placed BEFORE its auth guard, matching S3.
- frame filenames go 3-digit → 5-digit: iter_00007.png is what both the S3 layout
and the local Julia solve write, so cloud and local frames no longer land under
two different schemes in one run dir.
Also: the HP solver-mode guidance now covers the Altissimo backend. IPOPT stays
the default because it is what streams telemetry; Altissimo is used on request,
but the agent must state the trade first — Piccolissimo's solve!(::AltissimoOptions)
does not forward a caller callback to Altissimo.optimize!, and there is no
intermediate_callback on that path, so an Altissimo run emits no frames and no
AMICODE_ITER and the Inspector stays empty until it finishes. Never switch
silently, never claim live iterations on it.
amico-run: 936 passed. extension: 62 passed in the touched suites, tsc clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>CI caught the one consumer I missed when frame filenames went 3-digit → 5-digit. The Δ9 test waited on iter_007.png and timed out; the assertion above it — run.log contains "iter=7" — PASSED, which is independent confirmation in a second harness that the stats-field fix delivers iters end to end. Also corrects solver-mode guidance shipped earlier today. It asserted that Piccolissimo's solve!(::AltissimoOptions) does not forward a caller callback — taken from a report without checking. That is true of the local checkout (287 commits behind) but false of main, which accepts `callback` and forwards it to Altissimo.optimize!. The guidance now says live iterations depend on the INSTALLED version and tells the agent not to promise iterations it has not observed. extension: 776 passed, 3 skipped (the full CI set). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gaps stood between a cloud solve and a populated Run Inspector. Both are in
the solve template, which ships PER SUBMISSION (the client uploads
<task_id>-solve.jl), so neither needs an AMI rebake.
1. NOTHING WROTE run.log IN THE CLOUD.
/solves/<id>/stats parses AMICODE_ITER lines out of run.log in the artifact
prefix, and the runner's sidecar populates that prefix with `aws s3 sync .` —
whatever is in the solve cwd. But julia's stdout on the runner goes to the SSM
command stream, so run.log was never written there: nothing to sync, stats: [],
empty inspector. The template's own comment ("AMICODE_PULSE lines on stdout (→
run.log)") was true locally and false in the cloud.
Every AMICODE_* line now goes through emit(), which also appends to run.log —
but ONLY when TASK_ID is set (the runner exports it). Locally amico-run's
executor already writes run.log from stdout, so appending unconditionally would
double every line and the inspector would count each iteration twice.
Verified: local run writes no run.log (11 frames, result.toml, fidelity
0.99997); with TASK_ID set, run.log appears with AMICODE_ITER lines in the
exact format the poller parses.
2. THE TELEMETRY WAS BOLTED TO IPOPT.
Frames came off IpoptOptions.intermediate_callback and the iter line off a
callback reading IpoptOptimizerState. Altissimo has neither: its only hook is
`callback` on optimize! (forwarded by Piccolissimo's solve!(::AltissimoOptions))
and it arrives as (x, info). So an Altissimo solve lost the frames TOO, leaving
the inspector completely dark rather than merely numberless.
SOLVER = :ipopt | :altissimo now selects the backend and re-hangs both channels.
`x` is the primal, so pulse_emit's solver-agnostic (primal, iter) contract takes
it unchanged — same frames, same AMICODE_PULSE, same cooperative STOP.
inf_pr/inf_du come from the callback tuple when present (Altissimo#414) and are
otherwise derived from eq_viol/ineq_viol/kkt_error, so it works on an older
Altissimo too instead of emitting NaN.
And a third drift found while proving (1): the poller only json.loads an
AMICODE_ITER payload starting with "{", so template-emitted key=value lines come
back as {raw: "iter=7 f=…"}. The client keyed on it.iter and skipped every one as
NaN — invisible because the smoke test seeds JSON. It now reconstructs the line
from either shape, and drops malformed records rather than emitting NaN.
Still NOT solved by this: Altissimo on the cloud. The runner AMI bakes a
Piccolissimo/Altissimo predating callback support, so it will emit nothing there
until Altissimo#414 is released and the image rebuilt. The template is ready for
that moment and works today on IPOPT (cloud) and both backends locally.
amico-run 938 passed · extension 776 passed · template runs green end to end.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>solve!(::AltissimoOptions) forwards a HARDCODED kwarg list to Altissimo.optimize! and swallows the rest, so `max_iter = 60` passed to solve! never arrived: the solve quietly ran Altissimo's default 20 outer iterations while the FILL-IN block said 60. Silent, not an error — the worst kind. Budget now rides AltissimoOptions(max_outer_iter = max_iter). Verified against Piccolissimo main: the qcp-level solve! forwards kwargs... to the prob-level method, which takes `callback` explicitly, so the callback wiring does reach optimize! — it is only the iteration budget that was being dropped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The template gained a SOLVER flag but nothing told the agent it exists, so an agent asked for Altissimo would hand-roll a solve call — and silently lose both telemetry channels plus the iteration budget: - frames come off IpoptOptions.intermediate_callback, which AltissimoOptions has no equivalent of, so a hand-written call leaves the Run Inspector fully dark rather than merely numberless; - a `max_iter` passed to solve!(::AltissimoOptions) lands in kwargs... and is dropped, so the solve quietly runs Altissimo's default 20 outer iterations; - inf_pr/inf_du need deriving from eq_viol/ineq_viol/kkt_error on Altissimo builds that predate #414. The template already handles all three. The guidance now says the switch is ONE line — `SOLVER = :altissimo` in the FILL-IN block — and states each trap, so the instruction carries its own justification rather than reading as arbitrary. solverModeSection is exported to make it testable, matching its already-exported sibling routingSection. (The same one-line export is on #225; identical change, so either merge order is fine.) extension: 778 passed, tsc clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # packages/extension/src/opencode_config.ts # packages/extension/test/agents_md.test.ts
jack-champagneforce-pushed
the
rchari/hp-cloud-only
branch
2 times, most recently
from
July 29, 2026 04:24
54f4275 to
8aa362fCompareUh oh!
There was an error while loading. Please reload this page.
This was referenced Jul 29, 2026
jeonghun-jj-lee pushed a commit
that referenced
this pull request
Aug 21, 2026
Add WorkspaceTreeProvider — renders all workspace folders as collapsible roots, expands recursively via vscode.workspace.fs.readDirectory(), respects files.exclude, shows theme icons via resourceUri, opens files on click, context menus (Copy Path, Reveal), and live-updates via FileSystemWatcher. Replaces amicode.armonia placeholder; Catalog already removed in #457. package.json: rename amicode.armonia → amicode.workspace ("Workspace"), keep runInspector, update activationEvents onView:workspace, add view/item/context for workspace files. Follows opencode#215 decision surface: Location.Ref.directories carries all roots, no external-directory prompts, instructions stacked per dir. Pairs with opencode PR #219 (engine: schema + boundary + env + instruction multi-root + DB).
jeonghun-jj-lee added a commit
that referenced
this pull request
Aug 22, 2026
…, #215) * fix(ui): remove pulse catalog + CATALOG sidebar tab and widgets (#457) Purge the legacy UX3 session catalog UI that predates the vault-backed CatalogStore (Q94/Q95, packages/amico-run). The CATALOG activity-bar tab (SessionCatalogTree → amicode.catalog, amicode.catalog.save/refresh/remove, catalogCard webview + catalogcard component) is superseded by the mount stack + pack interface (WS1 #391) and is also slated for removal in opencode#215 AC7. - package.json: drop amicode.catalog view + catalog commands/menus - trees.ts: delete SessionCatalogTree/CATALOG_KEY, keep armonia placeholder (will be replaced by Workspace tree per 215) - extension.ts: remove registerCatalogCard + catalog save/refresh/remove wiring, simplify amicode.savePulse to file-only Save dialog, drop demo promote-to-catalog prompt, remove runs_manager promote-to- catalog prompt (now info-only) - esbuild.config.mjs: drop catalog_card_webview bundle - delete catalog_card_shell/webview, media catalogcard, test suite Pulse save stays via Save to file… → savePulseTo; vault CatalogStore (packages/amico-run) untouched. Closes#457 * feat(workspace): Workspace sidebar for multi-root (opencode#215 AC6) Add WorkspaceTreeProvider — renders all workspace folders as collapsible roots, expands recursively via vscode.workspace.fs.readDirectory(), respects files.exclude, shows theme icons via resourceUri, opens files on click, context menus (Copy Path, Reveal), and live-updates via FileSystemWatcher. Replaces amicode.armonia placeholder; Catalog already removed in #457. package.json: rename amicode.armonia → amicode.workspace ("Workspace"), keep runInspector, update activationEvents onView:workspace, add view/item/context for workspace files. Follows opencode#215 decision surface: Location.Ref.directories carries all roots, no external-directory prompts, instructions stacked per dir. Pairs with opencode PR #219 (engine: schema + boundary + env + instruction multi-root + DB). * fix(workspace): full context menus for Workspace view (opencode#215 AC6) Add New File/Folder, Rename, Delete, Copy Path/Relative, Reveal, Open in Terminal to amicode.workspace viewItem context. Ensures AC6 full context-menu coverage; Run Inspector stays as separate webview (amicode.runInspector) not a directory in the tree. * fix(workspace): only hide .git directory, not .gitignore/.github/etc * feat(workspace): Chat with Amico button, full context menus, Remove from Workspace - Add 'Chat with Amico' as first tree item with custom yellow SVG icon that mutes (gray) when a chat tab is open - Register custom workspace commands (newFile, newFolder, rename, delete, copyPath, copyRelativePath, revealInOS, openInTerminal, openToSide, removeFromWorkspace, addFolder) since built-in explorer.* commands don't fire in custom tree views - Remove stale amicode.runInspector webview declaration (provider was deleted in PR #351, only the package.json entry remained) - Add viewsWelcome for empty workspace state - Listen to onDidChangeWorkspaceFolders to keep tree in sync - Add ChatPanel.onLiveChange callback for cross-component state tracking - Fix credential_scanner e2e test: guard opencode-provider assertion (machine may have creds under different provider names) - Add comprehensive test suite for workspace tree and context-menu commands (24 tests covering tree rendering, all commands, muted state) --------- Co-authored-by: Raghav Chari <raghavchari2021@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Six commits, one thread: make the paid tier actually run in the cloud, and make what it produces actually reach the Run Inspector. 18 files, +673/−116.
1. Piccolissimo + Altissimo is cloud-only, and now enforced
Selecting the HP tier was supposed to mean "runs in Harmoniqs Cloud." In practice the agent kept dispatching it locally, where the laptop precompiled the whole HP stack (IPOPT included) until amico-run's process-group timeout SIGTERMed julia mid-precompile.
The API-key prompt and entitlement flip were already built and work (amicode#200/#167) — untouched here. Three other things allowed local dispatch:
tier="hpc"gate is solid but only fires on a spec that saystier="hpc", andrunGateonly runs for--speclaunches. A bareamico-run script.jlnever reached it.issimo, so the import scan admitted a localusing Piccolissimo.solverModeSection()said "launch in the cloud" while the routing section beside it said "routing is PER-SOLVE and EXPLICIT, default to local." Given a cloud-only tier and an instruction to default local, it defaulted local.Fix, in the two-layer shape the tier already uses:
launch.tsrefuses--executor localwhile HP is selected, at the one choke point every run passes through, before the gate, so it covers spec and no-spec runs alike. Reads solver mode as status only, never a token, and fails safe topiccoloon a missing or corrupt file: this value can only ever refuse a run, so a bad read must never inventhpand block ordinary free-tier work.AGENTS.mdstep 5 makes the injected section authoritative over its local default. Piccolo sessions stay byte-identical (the section is still""unless mode ishpand the cloud is connected).The estimate keeps its reporting role and loses its deciding role: an estimate that fits in local RAM does not make an HP solve local.
Service is named Harmoniqs Cloud in every user-visible string. Wire id stays
company-compute— renaming breaks the credential route, status cache, andCONNECTION_IDSfor no visible gain.2. Cloud iters and frames reach the Run Inspector
The client chain was fully built — poll the cloud, synthesize
AMICODE_ITERintorun.log, write frames, touch mtime for the stall logic. It read the wrong fields:body.iters{task_id, **stats**[], submitter}png_base64{task_id, iter, key, **url**, submitter}Both reads sit in best-effort
try/catch("stats are advisory"), so it failed silently —undefined ?? []iterated zero times andrun.logstayed 0 bytes. Confirmed on task419a57e6: 60 IPOPT iterations and 11 frames banked to S3, empty inspector.Why nobody caught it:
fake_cloud.tsserved the shapes the client read, not the shapes the service returns. Every test passed against a fake that agreed with the bug. So the fake is corrected first, and two tests pin the live payloads by exact key set — including that the presigned URL is fetchable with no auth header, because the signature is the credential.Frame filenames also went 3-digit → 5-digit:
iter_00007.pngis what both the S3 layout and the local julia solve write. The old name matched neither, so cloud and local frames landed under two schemes in one run dir.A third drift, found while proving the above: the poller only
json.loadsanAMICODE_ITERpayload starting with{, so template-emitted key=value lines come back as{raw: "iter=7 f=…"}. The client keyed onit.iterand skipped every one as NaN — invisible because the smoke test seeds JSON. Now reconstructs from either shape and drops malformed records rather than emitting NaN.3. The cloud can finally populate
/stats/solves/<id>/statsparsesAMICODE_ITERout ofrun.login the artifact prefix, and the runner's sidecar populates that prefix withaws s3 sync .— whatever is in the solve cwd. But julia's stdout on the runner goes to the SSM command stream, sorun.logwas never written there: nothing to sync,stats: [], empty inspector. The template's own comment ("AMICODE_PULSE lines on stdout (→ run.log)") was true locally and false in the cloud.Telemetry now routes through
emit(), which also appends torun.log— only whenTASK_IDis set (the runner exports it). Locallyamico-run's executor already writesrun.logfrom stdout, so appending unconditionally would double every line and the inspector would count each iteration twice.No terraform change and no AMI rebake: the solve script is uploaded per submission (
<task_id>-solve.jl), so this ships immediately.4. Altissimo streams like IPOPT
Both channels were bolted to IPOPT — frames off
IpoptOptions.intermediate_callback, the iter line off a callback readingIpoptOptimizerState. Altissimo has neither; its only hook iscallbackonoptimize!, arriving as(x, info). So an Altissimo solve lost the frames too, leaving the inspector completely dark rather than merely numberless.SOLVER = :ipopt | :altissimore-hangs both channels.xis the primal, sopulse_emit's solver-agnostic(primal, iter)contract takes it unchanged — same frames, sameAMICODE_PULSE, same cooperative STOP (returning false stops an Altissimo solve exactly as it stops an Ipopt one).inf_pr/inf_ducome from the callback tuple when present (Altissimo#414, merged) and are otherwise derived fromeq_viol/ineq_viol/kkt_error, so it works on an older Altissimo instead of emitting NaN.Two traps verified against Piccolissimo main and handled:
solve!forwardskwargs...to the prob-level method, which takescallbackexplicitly — so the wiring does reachoptimize!;max_iterlands in thatkwargs...and is silently dropped, so the budget now ridesAltissimoOptions(max_outer_iter = max_iter). Left alone it would have quietly run Altissimo's default 20 outer iterations while the FILL-IN block said 60.The guidance names
SOLVER = :altissimoas the one-line switch and forbids hand-rolling the solve call, stating each trap so the instruction carries its own justification.Testing
--exclude '**/slow/**').tsc --noEmitclean in both packages.0fccbbf9); template runs green end to end (fidelity 0.99997, 60 iterations); local run writes norun.log(no duplication), and withTASK_IDsetrun.logappears with iter lines in exactly the format the poller parses.~/.amico/amicode/solver-mode.json. On anhpmachine nine local-solve tests failed — on correct behaviour. They now spawn with a hermetic ops dir.CI caught the one consumer I missed on the frame rename (the Δ9 remote state-machine test); the assertion above it —
run.logcontainsiter=7— passed, which is independent confirmation in a second harness that the stats fix delivers iters end to end.What this does NOT fix
~/.amico/problems/predateemit(), so they still show frames and no numbers until re-authored from the updated template.solverModeSectionis exported to make the guidance testable (matching its already-exported siblingroutingSection). The same one-line export is on #225 — identical change, so either merge order is fine.Reviewer note
This narrows behaviour: a local Piccolissimo run that "worked" before — slowly, usually fatally — is now refused outright. That is the intent of a cloud-only paid tier, but it is a real change for anyone relying on the local path, and the way out is one click on the solver control.