amina-cli 0.4.0__tar.gz → 0.4.2__tar.gz

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.
Files changed (105) hide show
  1. {amina_cli-0.4.0 → amina_cli-0.4.2}/PKG-INFO +1 -1
  2. {amina_cli-0.4.0 → amina_cli-0.4.2}/pyproject.toml +1 -1
  3. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/__init__.py +1 -1
  4. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/jobs_cmd.py +27 -14
  5. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/__init__.py +91 -9
  6. {amina_cli-0.4.0 → amina_cli-0.4.2}/.gitignore +0 -0
  7. {amina_cli-0.4.0 → amina_cli-0.4.2}/LICENSE +0 -0
  8. {amina_cli-0.4.0 → amina_cli-0.4.2}/README.md +0 -0
  9. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/auth.py +0 -0
  10. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/client.py +0 -0
  11. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/__init__.py +0 -0
  12. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/auth_cmd.py +0 -0
  13. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/run_cmd.py +0 -0
  14. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/__init__.py +0 -0
  15. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/docs/hydrophobicity.yaml +0 -0
  16. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/docs/mmseqs2_cluster.yaml +0 -0
  17. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/docs/residue_accessibility.yaml +0 -0
  18. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/docs/rmsd.yaml +0 -0
  19. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/docs/sasa.yaml +0 -0
  20. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/docs/simple_rmsd.yaml +0 -0
  21. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/docs/surface_charge.yaml +0 -0
  22. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/docs/usalign.yaml +0 -0
  23. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/hydrophobicity.py +0 -0
  24. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/mmseqs2_cluster.py +0 -0
  25. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/residue_accessibility.py +0 -0
  26. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/rmsd.py +0 -0
  27. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/sasa.py +0 -0
  28. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/simple_rmsd.py +0 -0
  29. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/surface_charge.py +0 -0
  30. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/analysis/usalign.py +0 -0
  31. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/design/__init__.py +0 -0
  32. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/design/docs/esm_if1.yaml +0 -0
  33. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/design/docs/protein_mc.yaml +0 -0
  34. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/design/docs/proteinmpnn.yaml +0 -0
  35. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/design/docs/rfdiffusion.yaml +0 -0
  36. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/design/esm_if1.py +0 -0
  37. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/design/protein_mc.py +0 -0
  38. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/design/proteinmpnn.py +0 -0
  39. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/design/rfdiffusion.py +0 -0
  40. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/display.py +0 -0
  41. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/doccard.py +0 -0
  42. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/folding/__init__.py +0 -0
  43. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/folding/boltz2.py +0 -0
  44. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/folding/docs/boltz2.yaml +0 -0
  45. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/folding/docs/esmfold.yaml +0 -0
  46. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/folding/docs/openfold3.yaml +0 -0
  47. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/folding/docs/protenix.yaml +0 -0
  48. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/folding/esmfold.py +0 -0
  49. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/folding/openfold3.py +0 -0
  50. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/folding/protenix.py +0 -0
  51. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/__init__.py +0 -0
  52. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/autodock_vina.py +0 -0
  53. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/diffdock.py +0 -0
  54. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/dockq.py +0 -0
  55. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/docs/autodock_vina.yaml +0 -0
  56. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/docs/diffdock.yaml +0 -0
  57. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/docs/dockq.yaml +0 -0
  58. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/docs/emngly.yaml +0 -0
  59. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/docs/glycosylation_ensemble.yaml +0 -0
  60. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/docs/interface_identifier.yaml +0 -0
  61. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/docs/isoglyp.yaml +0 -0
  62. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/docs/lmngly.yaml +0 -0
  63. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/docs/p2rank.yaml +0 -0
  64. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/docs/pesto.yaml +0 -0
  65. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/emngly.py +0 -0
  66. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/glycosylation_ensemble.py +0 -0
  67. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/interface_identifier.py +0 -0
  68. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/isoglyp.py +0 -0
  69. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/lmngly.py +0 -0
  70. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/p2rank.py +0 -0
  71. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/interactions/pesto.py +0 -0
  72. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/properties/__init__.py +0 -0
  73. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/properties/aminosol.py +0 -0
  74. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/properties/docs/aminosol.yaml +0 -0
  75. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/properties/docs/esm1v.yaml +0 -0
  76. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/properties/docs/esm2_embedding.yaml +0 -0
  77. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/properties/esm1v.py +0 -0
  78. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/properties/esm2_embedding.py +0 -0
  79. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/__init__.py +0 -0
  80. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/activesite_verifier.py +0 -0
  81. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/chain_select.py +0 -0
  82. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/distance_calculator.py +0 -0
  83. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/docs/activesite_verifier.yaml +0 -0
  84. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/docs/chain_select.yaml +0 -0
  85. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/docs/distance_calculator.yaml +0 -0
  86. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/docs/maxit_convert.yaml +0 -0
  87. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/docs/mol_size_calculator.yaml +0 -0
  88. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/docs/obabel_convert.yaml +0 -0
  89. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/docs/pdb_bfactor_overwrite.yaml +0 -0
  90. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/docs/pdb_cleaner.yaml +0 -0
  91. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/docs/pdb_quality_assessment.yaml +0 -0
  92. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/docs/pdb_to_fasta.yaml +0 -0
  93. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/docs/protein_relaxer.yaml +0 -0
  94. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/maxit_convert.py +0 -0
  95. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/mol_size_calculator.py +0 -0
  96. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/obabel_convert.py +0 -0
  97. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/pdb_bfactor_overwrite.py +0 -0
  98. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/pdb_cleaner.py +0 -0
  99. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/pdb_quality_assessment.py +0 -0
  100. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/pdb_to_fasta.py +0 -0
  101. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools/utilities/protein_relaxer.py +0 -0
  102. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/commands/tools_cmd.py +0 -0
  103. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/main.py +0 -0
  104. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/registry.py +0 -0
  105. {amina_cli-0.4.0 → amina_cli-0.4.2}/src/amina_cli/storage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amina-cli
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: CLI for AminoAnalytica protein engineering platform
5
5
  Project-URL: Homepage, https://aminoanalytica.com
6
6
  Project-URL: Documentation, https://docs.aminoanalytica.com
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "amina-cli"
3
- version = "0.4.0"
3
+ version = "0.4.2"
4
4
  description = "CLI for AminoAnalytica protein engineering platform"
5
5
  readme = "README.md"
6
6
  license = {text = "Apache-2.0"}
@@ -9,4 +9,4 @@ Quick start:
9
9
  amina run esmfold --sequence "MKFLILLFNILCLFPVLAADNH"
10
10
  """
11
11
 
12
- __version__ = "0.4.0"
12
+ __version__ = "0.4.2"
@@ -824,7 +824,10 @@ def _find_submission_files(root: Path, recursive: bool) -> list[tuple[str, Path]
824
824
 
825
825
  Returns a list of ``(job_id, dir_containing_submission)`` tuples. Files
826
826
  that fail to parse or lack a ``job_id`` field are skipped silently — they
827
- weren't written by ``amina run --background``.
827
+ correspond to runs that aborted before the gateway returned (submit error,
828
+ interrupt, network failure), leaving the up-front input record without
829
+ any job-tracking fields ever being merged in. There is no server-side
830
+ job to reconcile in those cases.
828
831
  """
829
832
  pattern = "**/submission.json" if recursive else "*/submission.json"
830
833
  found: list[tuple[str, Path]] = []
@@ -955,19 +958,20 @@ def reconcile(
955
958
  per_job.append(entry)
956
959
  continue
957
960
 
958
- local_status = job_info.get("status", "")
959
- if local_status not in TERMINAL_STATUSES:
960
- # Local cache (now fresh) says this job hasn't terminated yet.
961
- # Don't waste a per-job HTTP roundtrip just to confirm.
962
- summary["still_running"] += 1
963
- entry["action"] = "still_running"
964
- entry["status"] = local_status
965
- per_job.append(entry)
966
- continue
967
-
968
- # Terminal: fetch the full payload (signed_urls, output_files) so the
969
- # response.json we write matches the shape `amina jobs download`
970
- # produces.
961
+ # Always go to the server for the truth, even if the local cache says
962
+ # non-terminal. Reason: the bulk /list_jobs refresh above can return
963
+ # stale data when the worker's completion callback dropped (Supabase
964
+ # blip, gateway crash mid-handler) — a 300-job campaign reported 197
965
+ # jobs still_running while all 300 had finished on Modal. The CLI
966
+ # used to early-return on local non-terminal status, which made this
967
+ # bug unrecoverable from the client. Now reconcile mirrors what
968
+ # `amina jobs status` does: ask Modal directly via call_id.
969
+ #
970
+ # Cost: O(pending) HTTP calls per cycle. Bounded by jobs the user is
971
+ # actively waiting on (response.json-present dirs short-circuit
972
+ # above), and the gateway's heal-on-read upgrade makes the post-merge
973
+ # local cache mostly fresh anyway, so this fall-through is rarely hit
974
+ # in practice. It exists as a defence-in-depth for gateway brownouts.
971
975
  try:
972
976
  status_result = _resolve_job_status(job_info)
973
977
  except ToolExecutionError as fetch_err:
@@ -978,6 +982,15 @@ def reconcile(
978
982
  continue
979
983
  _persist_terminal_status(job_info, status_result)
980
984
 
985
+ if not status_result.get("terminal", False) and status_result.get("status") not in TERMINAL_STATUSES:
986
+ # Genuinely still running on the server. Count, record the
987
+ # FRESH status (not the stale-cache one we started with), move on.
988
+ summary["still_running"] += 1
989
+ entry["action"] = "still_running"
990
+ entry["status"] = status_result.get("status", "running")
991
+ per_job.append(entry)
992
+ continue
993
+
981
994
  # Build the response payload. For completed jobs the gateway hands us
982
995
  # a full `result` dict (output_files, signed_urls, data, ...). For
983
996
  # failed/cancelled jobs Modal often returns only an error string with
@@ -8,6 +8,7 @@ Each tool file exports METADATA dict and register() function.
8
8
  import importlib
9
9
  import json
10
10
  import os
11
+ import sys
11
12
  from pathlib import Path
12
13
  from typing import Iterator, Optional
13
14
  import typer
@@ -15,6 +16,7 @@ from rich.console import Console
15
16
  from rich.panel import Panel
16
17
  from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
17
18
 
19
+ from amina_cli import __version__ as _CLI_VERSION
18
20
  from amina_cli.commands.tools.display import render_tool_output
19
21
 
20
22
  console = Console()
@@ -23,6 +25,17 @@ console = Console()
23
25
  err_console = Console(stderr=True)
24
26
 
25
27
 
28
+ def _reproducibility_fields() -> dict:
29
+ """Fields recorded in submission.json so a run can be re-executed exactly.
30
+
31
+ ``argv`` is the literal ``sys.argv`` of the current process — the user's
32
+ command-line invocation, preserved as a list. ``cli_version`` pins the
33
+ amina-cli version that built the request, so future readers can match the
34
+ parameter shape against the release that produced it.
35
+ """
36
+ return {"cli_version": _CLI_VERSION, "argv": list(sys.argv)}
37
+
38
+
26
39
  def _is_truthy_env(name: str) -> bool:
27
40
  """Standard env-var boolean read.
28
41
 
@@ -124,6 +137,11 @@ def run_tool_with_progress(
124
137
  console.print("[red]Error:[/red] --output / -o is required")
125
138
  raise typer.Exit(1)
126
139
 
140
+ # Snapshot once at function entry so the early and late submission.json
141
+ # writes agree on argv even if a caller (e.g. tests) mutates sys.argv
142
+ # mid-run.
143
+ repro = _reproducibility_fields()
144
+
127
145
  # Handle background submission
128
146
  if background:
129
147
  # Resolved before the try so error handlers can route to stderr
@@ -135,6 +153,20 @@ def run_tool_with_progress(
135
153
  tool_info = get_tool(tool_name)
136
154
  display_name = tool_info.get("display_name", tool_name) if tool_info else tool_name
137
155
 
156
+ # Early submission.json write — before any network call — so a
157
+ # reproducibility record exists even if submission fails (auth,
158
+ # credits, validation, network). Overwritten below with the
159
+ # augmented record once submit_tool_sync returns successfully.
160
+ output_dir.mkdir(parents=True, exist_ok=True)
161
+ submission_path = output_dir / "submission.json"
162
+ submission_path.write_text(
163
+ json.dumps(
164
+ {"tool_name": tool_name, "params": params, **repro},
165
+ indent=2,
166
+ default=str,
167
+ )
168
+ )
169
+
138
170
  if json_mode:
139
171
  # No spinner output to stdout when the caller wants JSON.
140
172
  result = submit_tool_sync(tool_name, params)
@@ -147,13 +179,30 @@ def run_tool_with_progress(
147
179
  progress.add_task(f"Submitting {display_name}...", total=None)
148
180
  result = submit_tool_sync(tool_name, params)
149
181
 
150
- # Save to local job history
151
- save_job(result)
152
-
153
- # Persist the structured submission payload for downstream agents/scripts
154
- output_dir.mkdir(parents=True, exist_ok=True)
155
- submission_path = output_dir / "submission.json"
156
- submission_path.write_text(json.dumps(result, indent=2, default=str))
182
+ # Save to local job history. Soft-fail on filesystem errors —
183
+ # the job is already submitted and being billed, so a stale
184
+ # local cache must not surface as a traceback that hides the
185
+ # job_id from the user.
186
+ warn_console = err_console if json_mode else console
187
+ try:
188
+ save_job(result)
189
+ except OSError as e:
190
+ warn_console.print(f"[yellow]Warning:[/yellow] Could not update local job history: {e}")
191
+
192
+ # Overwrite submission.json with the augmented record: input fields
193
+ # from the early write, plus the gateway tracking fields returned
194
+ # by submit_tool_sync (job_id, call_id, status, etc.). Soft-fail
195
+ # for the same reason as save_job above.
196
+ submission_record = {
197
+ "tool_name": tool_name,
198
+ "params": params,
199
+ **repro,
200
+ **result,
201
+ }
202
+ try:
203
+ submission_path.write_text(json.dumps(submission_record, indent=2, default=str))
204
+ except OSError as e:
205
+ warn_console.print(f"[yellow]Warning:[/yellow] Could not write {submission_path}: {e}")
157
206
 
158
207
  if json_mode:
159
208
  # Single-line JSON on stdout; nothing else, ever, in this branch.
@@ -276,6 +325,20 @@ def run_tool_with_progress(
276
325
  tool_info = get_tool(tool_name)
277
326
  display_name = tool_info.get("display_name", tool_name) if tool_info else tool_name
278
327
 
328
+ # Persist a reproducibility record up-front: written before submission so
329
+ # the user-facing inputs, CLI version, and argv survive mid-run failures,
330
+ # interrupts, or network errors. response.json (written after the run)
331
+ # captures the result side.
332
+ output_dir.mkdir(parents=True, exist_ok=True)
333
+ submission_path = output_dir / "submission.json"
334
+ submission_path.write_text(
335
+ json.dumps(
336
+ {"tool_name": tool_name, "params": params, **repro},
337
+ indent=2,
338
+ default=str,
339
+ )
340
+ )
341
+
279
342
  # Track queue position for progress updates
280
343
  queue_position_holder = {"position": 0, "is_queued": False}
281
344
 
@@ -337,9 +400,28 @@ def run_tool_with_progress(
337
400
 
338
401
  # Persist the structured response payload (success or failure) so agents
339
402
  # and scripts can read metrics/warnings/cost without re-parsing stdout.
340
- output_dir.mkdir(parents=True, exist_ok=True)
403
+ # Soft-fail on filesystem errors — the job has already run and been
404
+ # billed, so a disk hiccup must not surface as a traceback.
341
405
  response_path = output_dir / "response.json"
342
- response_path.write_text(json.dumps(result, indent=2, default=str))
406
+ try:
407
+ response_path.write_text(json.dumps(result, indent=2, default=str))
408
+ except OSError as e:
409
+ console.print(f"[yellow]Warning:[/yellow] Could not write {response_path}: {e}")
410
+
411
+ # Overwrite submission.json with the augmented record. Spreading
412
+ # **result mirrors the background path so consumers see the same
413
+ # shape on both, and the status field reflects what the worker
414
+ # actually reported. Soft-fail for the same reason as response.json.
415
+ submission_record = {
416
+ "tool_name": tool_name,
417
+ "params": params,
418
+ **repro,
419
+ **result,
420
+ }
421
+ try:
422
+ submission_path.write_text(json.dumps(submission_record, indent=2, default=str))
423
+ except OSError as e:
424
+ console.print(f"[yellow]Warning:[/yellow] Could not write {submission_path}: {e}")
343
425
 
344
426
  _display_result(result, downloaded, output_dir, params, tool_info, response_path)
345
427
 
File without changes
File without changes
File without changes