agent-hitch 0.2.4 → 0.2.6

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 (36) hide show
  1. package/dist/src/adapters/providers/deepseek.js +3 -1
  2. package/dist/src/adapters/providers/deepseek.js.map +1 -1
  3. package/dist/src/adapters/providers/shared.js +3 -2
  4. package/dist/src/adapters/providers/shared.js.map +1 -1
  5. package/dist/src/backends/harbor/backend.js +21 -5
  6. package/dist/src/backends/harbor/backend.js.map +1 -1
  7. package/dist/src/backends/harbor/verifier-config.js +11 -0
  8. package/dist/src/backends/harbor/verifier-config.js.map +1 -0
  9. package/dist/src/cli/arguments.js +14 -0
  10. package/dist/src/cli/arguments.js.map +1 -1
  11. package/dist/src/cli/output.js +4 -1
  12. package/dist/src/cli/output.js.map +1 -1
  13. package/dist/src/evals/harbor-bridge-error.js +65 -0
  14. package/dist/src/evals/harbor-bridge-error.js.map +1 -0
  15. package/dist/src/evals/index.js +2 -2
  16. package/dist/src/evals/index.js.map +1 -1
  17. package/dist/src/evals/infrastructure-retry.js +181 -0
  18. package/dist/src/evals/infrastructure-retry.js.map +1 -0
  19. package/dist/src/evals/progress.js +3 -0
  20. package/dist/src/evals/progress.js.map +1 -1
  21. package/dist/src/evals/request.js +14 -1
  22. package/dist/src/evals/request.js.map +1 -1
  23. package/dist/src/evals/rerun-slots.js +103 -0
  24. package/dist/src/evals/rerun-slots.js.map +1 -0
  25. package/dist/src/evals/rerun.js +167 -148
  26. package/dist/src/evals/rerun.js.map +1 -1
  27. package/dist/src/evals/service.js +196 -84
  28. package/dist/src/evals/service.js.map +1 -1
  29. package/dist/src/evals/trial-import.js +52 -36
  30. package/dist/src/evals/trial-import.js.map +1 -1
  31. package/dist/src/evals/verifier-diagnostics.js +235 -0
  32. package/dist/src/evals/verifier-diagnostics.js.map +1 -0
  33. package/docs/schemas/eval-request.schema.json +2 -0
  34. package/integrations/harbor/hitch_harbor_agent.py +389 -40
  35. package/integrations/harbor/hitch_harbor_verifier.py +314 -0
  36. package/package.json +3 -2
@@ -28,6 +28,20 @@ LOCAL_GIT_TRANSPORT_MAX_BYTES = 512 * 1024 * 1024
28
28
  LOCAL_GIT_REMOTE_ROOT = "/opt/hitch-local-source"
29
29
  HARNESS_ARTIFACT_REMOTE_ROOT = "/opt/hitch-harness-artifact"
30
30
  HITCH_CONTAINER_STATE_ROOT = "/tmp/hitch-state"
31
+ HITCH_BRIDGE_ERROR_LOG = "/logs/agent/hitch-bridge-error.json"
32
+ HITCH_DIAGNOSTIC_MAX_BYTES = 8 * 1024
33
+ HITCH_BRIDGE_ERROR_MAX_BYTES = 64 * 1024
34
+ HITCH_RESULT_MISSING_EXIT = 44
35
+ HITCH_RESULT_NOT_FILE_EXIT = 45
36
+
37
+
38
+ class HitchBridgeError(RuntimeError):
39
+ """Stable Harbor-facing infrastructure failure with structured evidence."""
40
+
41
+ def __init__(self, code: str, message: str, evidence: dict[str, Any]) -> None:
42
+ super().__init__(f"{code}: {message}")
43
+ self.code = code
44
+ self.evidence = evidence
31
45
 
32
46
 
33
47
  class HitchHarborAgent(BaseAgent):
@@ -46,11 +60,12 @@ class HitchHarborAgent(BaseAgent):
46
60
  local_source_transport: dict[str, Any] | None = None,
47
61
  hitch_timeout_ms: int = 900_000,
48
62
  agent_args: list[str] | None = None,
49
- workdir: str = "/app",
63
+ workdir: str | None = None,
50
64
  eval_id: str | None = None,
51
65
  benchmark_id: str | None = None,
52
66
  benchmark_revision: str | None = None,
53
67
  verifier_identity: str | None = None,
68
+ logical_attempt: int | None = None,
54
69
  **kwargs: Any,
55
70
  ) -> None:
56
71
  super().__init__(logs_dir=logs_dir, **kwargs)
@@ -64,11 +79,17 @@ class HitchHarborAgent(BaseAgent):
64
79
  self.candidate_id = candidate_id
65
80
  self.hitch_timeout_ms = int(hitch_timeout_ms)
66
81
  self.agent_args = list(agent_args or [])
67
- self.workdir = workdir
82
+ if workdir is not None and (not isinstance(workdir, str) or not workdir.strip()):
83
+ raise ValueError("workdir must be a non-empty string when provided")
84
+ self.workdir = workdir.strip() if isinstance(workdir, str) else None
85
+ self._workdir_source: str | None = "agent_config" if self.workdir is not None else None
68
86
  self.eval_id = eval_id
69
87
  self.benchmark_id = benchmark_id
70
88
  self.benchmark_revision = benchmark_revision
71
89
  self.verifier_identity = verifier_identity
90
+ if logical_attempt is not None and (isinstance(logical_attempt, bool) or not isinstance(logical_attempt, int) or logical_attempt < 1):
91
+ raise ValueError("logical_attempt must be a positive integer")
92
+ self.logical_attempt = logical_attempt
72
93
  self._hitch_version: str | None = None
73
94
  self._entrypoint: str | None = None
74
95
  self._artifact_manifest: dict[str, Any] | None = None
@@ -127,6 +148,7 @@ class HitchHarborAgent(BaseAgent):
127
148
  payload_dir = self.hitch_runtime_dir / "payload"
128
149
  if not payload_dir.is_dir():
129
150
  raise RuntimeError(f"Hitch runtime bundle has no payload directory: {self.hitch_runtime_dir}")
151
+ await self._resolve_workdir(environment)
130
152
  # Upload the cached bundle's payload (package.json + dist/) as the
131
153
  # package root under /opt/hitch; the local cache path is host-side
132
154
  # bookkeeping and is not identity (spec §4.2).
@@ -175,6 +197,111 @@ class HitchHarborAgent(BaseAgent):
175
197
  if cache_lock is not None:
176
198
  await self._release_artifact_cache_lock(cache_lock)
177
199
 
200
+ async def _resolve_workdir(self, environment: BaseEnvironment) -> str:
201
+ """Resolve Harbor's effective task directory and prove it is usable.
202
+
203
+ Harbor environments already combine task-level ``[environment].workdir``
204
+ with the container image's ``WORKDIR``. Honor an explicit bridge
205
+ override, then the task configuration, then ask the running container
206
+ for its default cwd instead of assuming a global path such as /app.
207
+ """
208
+ candidate = self.workdir
209
+ source = self._workdir_source
210
+ task_config = getattr(environment, "task_env_config", None)
211
+ task_workdir = getattr(task_config, "workdir", None)
212
+ discovery: ExecResult | None = None
213
+ if candidate is None and isinstance(task_workdir, str) and task_workdir.strip():
214
+ candidate = task_workdir.strip()
215
+ source = "task_environment"
216
+ if candidate is None:
217
+ discovery = await environment.exec("pwd -P")
218
+ candidate = (discovery.stdout or "").strip()
219
+ source = "container_workdir"
220
+ if discovery.return_code != 0 or not candidate:
221
+ detail = self._exec_diagnostic(discovery)
222
+ await self._raise_workdir_error(
223
+ environment,
224
+ "Could not determine the Harbor task working directory from the container "
225
+ f"(exit={discovery.return_code}): {detail}",
226
+ source=source,
227
+ candidate=candidate or None,
228
+ probe=discovery,
229
+ )
230
+ if (
231
+ candidate is None
232
+ or not PurePosixPath(candidate).is_absolute()
233
+ or "\x00" in candidate
234
+ or "\n" in candidate
235
+ or "\r" in candidate
236
+ ):
237
+ await self._raise_workdir_error(
238
+ environment,
239
+ f"Harbor task working directory must be an absolute POSIX path; got {candidate!r} "
240
+ f"from {source or 'unknown'}",
241
+ source=source,
242
+ candidate=candidate,
243
+ probe=discovery,
244
+ )
245
+
246
+ exists = await environment.exec(f"test -d {shlex.quote(candidate)}", cwd="/")
247
+ if exists.return_code != 0:
248
+ detail = self._exec_diagnostic(exists)
249
+ await self._raise_workdir_error(
250
+ environment,
251
+ f"Harbor task working directory does not exist or is not a directory: {candidate} "
252
+ f"(source={source}, exit={exists.return_code}): {detail}",
253
+ source=source,
254
+ candidate=candidate,
255
+ probe=exists,
256
+ )
257
+
258
+ usable = await environment.exec("pwd -P", cwd=candidate)
259
+ resolved = (usable.stdout or "").strip()
260
+ if usable.return_code != 0 or not resolved or not PurePosixPath(resolved).is_absolute():
261
+ detail = self._exec_diagnostic(usable)
262
+ await self._raise_workdir_error(
263
+ environment,
264
+ f"Harbor task working directory exists but cannot be used to start the Hitch agent: {candidate} "
265
+ f"(source={source}, exit={usable.return_code}): {detail}",
266
+ source=source,
267
+ candidate=candidate,
268
+ probe=usable,
269
+ )
270
+ self.workdir = resolved
271
+ self._workdir_source = source
272
+ return resolved
273
+
274
+ async def _raise_workdir_error(
275
+ self,
276
+ environment: BaseEnvironment,
277
+ message: str,
278
+ *,
279
+ source: str | None,
280
+ candidate: str | None,
281
+ probe: ExecResult | None,
282
+ ) -> None:
283
+ evidence: dict[str, Any] = {
284
+ "schema_version": "1",
285
+ "code": "hitch_workdir_invalid",
286
+ "message": self._bounded_tail(message, 2048),
287
+ "recorded_at": datetime.now(timezone.utc).isoformat(),
288
+ "eval_id": self.eval_id,
289
+ "workdir": {
290
+ "source": source,
291
+ "candidate": candidate,
292
+ "return_code": probe.return_code if probe is not None else None,
293
+ "stdout_tail": self._bounded_tail(probe.stdout or "") if probe is not None else "",
294
+ "stderr_tail": self._bounded_tail(probe.stderr or "") if probe is not None else "",
295
+ },
296
+ }
297
+ await self._write_bridge_error(environment, evidence)
298
+ raise HitchBridgeError("hitch_workdir_invalid", message, evidence)
299
+
300
+ def _require_workdir(self) -> str:
301
+ if self.workdir is None:
302
+ raise RuntimeError("Hitch agent setup() must resolve the Harbor task working directory before run()")
303
+ return self.workdir
304
+
178
305
  def _verify_harness_artifact_host(self) -> dict[str, Any]:
179
306
  """Pin host artifact metadata before Harbor copies the directory."""
180
307
  transport = self.harness_artifact or {}
@@ -816,7 +943,9 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
816
943
  context: AgentContext,
817
944
  ) -> None:
818
945
  self.logs_dir.mkdir(parents=True, exist_ok=True)
819
- run_id = "run_" + uuid.uuid4().hex
946
+ workdir = self._require_workdir()
947
+ assigned_run_id = "run_" + uuid.uuid4().hex
948
+ run_id = assigned_run_id
820
949
  trial_id, task_id, attempt = self._trial_identity()
821
950
  context_payload: dict[str, Any] = {"kind": "ad_hoc"}
822
951
  parent_payload: dict[str, Any] | None = None
@@ -890,7 +1019,7 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
890
1019
  *self._local_source_cli_args(),
891
1020
  *self._artifact_cli_args(),
892
1021
  "--cwd",
893
- shlex.quote(self.workdir),
1022
+ shlex.quote(workdir),
894
1023
  "--workspace-mode",
895
1024
  "shared",
896
1025
  "--prompt-file",
@@ -918,29 +1047,39 @@ git -C {LOCAL_GIT_REMOTE_ROOT}/repo.git update-ref refs/heads/hitch-local {commi
918
1047
  + " 2> >(tee /logs/agent/hitch-stderr.log >&2)"
919
1048
  + " | tee /logs/agent/hitch-events.jsonl"
920
1049
  )
921
- execution = await environment.exec(command, cwd=self.workdir)
1050
+ execution = await environment.exec(command, cwd=workdir)
922
1051
  events = self._events(execution.stdout or "")
923
- observed_run_id = next((event.get("run_id") for event in events if event.get("run_id")), None)
1052
+ observed_run_id = next((
1053
+ value
1054
+ for event in events
1055
+ for value in [event.get("run_id")]
1056
+ if isinstance(value, str) and re.fullmatch(r"run_[a-f0-9]{32}", value)
1057
+ ), None)
924
1058
  if observed_run_id:
925
1059
  run_id = str(observed_run_id)
926
- hitch_result = None
927
- if run_id:
928
- result_path = f"/tmp/hitch-state/runs/{run_id}/result.json"
929
- result = await environment.exec(
930
- f"cat {shlex.quote(result_path)} | tee /logs/agent/hitch-result.json"
931
- )
932
- if result.return_code == 0 and result.stdout:
933
- hitch_result = json.loads(result.stdout)
934
- bundle_stage = f"/logs/agent/.hitch-run-bundle.{uuid.uuid4().hex}"
935
- bundle_marker = json.dumps({
936
- "schema_version": "1",
937
- "run_id": run_id,
938
- "eval_id": self.eval_id,
939
- "trial_id": trial_id,
940
- "completed_at": datetime.now(timezone.utc).isoformat(),
941
- }, separators=(",", ":"))
942
- export = await environment.exec(
943
- f"""
1060
+ result_path = f"/tmp/hitch-state/runs/{run_id}/result.json"
1061
+ quoted_result_path = shlex.quote(result_path)
1062
+ result_read = await environment.exec(
1063
+ f"""
1064
+ if [ ! -e {quoted_result_path} ]; then exit {HITCH_RESULT_MISSING_EXIT}; fi
1065
+ if [ -L {quoted_result_path} ] || [ ! -f {quoted_result_path} ]; then exit {HITCH_RESULT_NOT_FILE_EXIT}; fi
1066
+ cat -- {quoted_result_path}
1067
+ """.strip()
1068
+ )
1069
+ result_copy = await environment.exec(
1070
+ f"if [ -f {quoted_result_path} ] && [ ! -L {quoted_result_path} ]; then "
1071
+ f"cp -- {quoted_result_path} /logs/agent/hitch-result.json; fi"
1072
+ )
1073
+ bundle_stage = f"/logs/agent/.hitch-run-bundle.{uuid.uuid4().hex}"
1074
+ bundle_marker = json.dumps({
1075
+ "schema_version": "1",
1076
+ "run_id": run_id,
1077
+ "eval_id": self.eval_id,
1078
+ "trial_id": trial_id,
1079
+ "completed_at": datetime.now(timezone.utc).isoformat(),
1080
+ }, separators=(",", ":"))
1081
+ bundle_export = await environment.exec(
1082
+ f"""
944
1083
  set -eu
945
1084
  source_dir={shlex.quote(f'/tmp/hitch-state/runs/{run_id}')}
946
1085
  target_dir=/logs/agent/hitch-run-bundle
@@ -954,9 +1093,38 @@ printf '%s\n' {shlex.quote(bundle_marker)} > "$stage_dir/bundle.complete.json"
954
1093
  rm -rf "$target_dir"
955
1094
  mv "$stage_dir" "$target_dir"
956
1095
  """.strip()
1096
+ )
1097
+ hitch_result, result_error_code, result_error_message = self._parse_hitch_result(result_read, run_id)
1098
+ primary_code: str | None = None
1099
+ primary_message: str | None = None
1100
+ if execution.return_code != 0:
1101
+ primary_code = "hitch_process_failed"
1102
+ diagnostic = self._exec_diagnostic(execution)
1103
+ if hitch_result and isinstance(hitch_result.get("error"), dict):
1104
+ result_message = hitch_result["error"].get("message")
1105
+ if isinstance(result_message, str) and result_message.strip():
1106
+ diagnostic = result_message.strip()
1107
+ primary_message = (
1108
+ f"Hitch agent run failed with code {execution.return_code} "
1109
+ f"(run_id={run_id}, trial_id={trial_id}): {self._bounded_tail(diagnostic)}"
957
1110
  )
958
- if export.return_code != 0:
959
- raise RuntimeError("Hitch run bundle export failed before trial teardown")
1111
+ elif result_error_code is not None:
1112
+ primary_code = result_error_code
1113
+ primary_message = result_error_message
1114
+ elif hitch_result is not None and hitch_result.get("revision_identity") != self.revision_identity:
1115
+ primary_code = "hitch_revision_identity_mismatch"
1116
+ primary_message = (
1117
+ "Hitch resolved a different harness revision inside the trial container "
1118
+ f"(run_id={run_id}, trial_id={trial_id}): expected {self.revision_identity}, "
1119
+ f"got {hitch_result.get('revision_identity')}"
1120
+ )
1121
+ elif bundle_export.return_code != 0:
1122
+ primary_code = "hitch_run_bundle_export_failed"
1123
+ primary_message = f"Hitch run bundle export failed (run_id={run_id}, trial_id={trial_id})"
1124
+ elif result_copy.return_code != 0:
1125
+ primary_code = "hitch_result_artifact_copy_failed"
1126
+ primary_message = f"Hitch result artifact copy failed (run_id={run_id}, trial_id={trial_id})"
1127
+
960
1128
  context.metadata = {
961
1129
  "candidate_id": self.candidate_id,
962
1130
  "harness_ref": self.harness_ref,
@@ -964,6 +1132,8 @@ mv "$stage_dir" "$target_dir"
964
1132
  "controller_runtime_id": self.controller_runtime_id,
965
1133
  "hitch_run_id": run_id,
966
1134
  "hitch_run_bundle": "hitch-run-bundle",
1135
+ "hitch_workdir": workdir,
1136
+ "hitch_workdir_source": self._workdir_source,
967
1137
  "eval_id": self.eval_id,
968
1138
  "trial_id": trial_id,
969
1139
  "task_id": task_id,
@@ -989,26 +1159,197 @@ mv "$stage_dir" "$target_dir"
989
1159
  "node_version": self._artifact_manifest.get("toolchain", {}).get("node"),
990
1160
  "status": self._artifact_transport_status or "container_prepare",
991
1161
  }
992
- if execution.return_code != 0:
993
- message = (execution.stderr or "").strip()
994
- if hitch_result and hitch_result.get("error", {}).get("message"):
995
- message = hitch_result["error"]["message"]
996
- raise RuntimeError(
997
- f"Hitch agent run failed with code {execution.return_code}: {message or 'no diagnostic output'}"
1162
+ if primary_code is not None:
1163
+ context.metadata["hitch_bridge_error_code"] = primary_code
1164
+ context.metadata["hitch_bridge_error_artifact"] = "hitch-bridge-error.json"
1165
+ evidence = self._bridge_error_evidence(
1166
+ code=primary_code,
1167
+ message=primary_message or primary_code,
1168
+ trial_id=trial_id,
1169
+ task_id=task_id,
1170
+ attempt=attempt,
1171
+ assigned_run_id=assigned_run_id,
1172
+ observed_run_id=str(observed_run_id) if observed_run_id else None,
1173
+ result_path=result_path,
1174
+ execution=execution,
1175
+ result_read=result_read,
1176
+ result_diagnostic=result_error_code,
1177
+ last_event=events[-1] if events else None,
1178
+ bundle_export=bundle_export,
1179
+ result_copy=result_copy,
998
1180
  )
999
- if hitch_result is None:
1000
- raise RuntimeError("Hitch agent run completed without a persisted result")
1001
- if hitch_result.get("revision_identity") != self.revision_identity:
1002
- raise RuntimeError(
1003
- "Hitch resolved a different harness revision inside the trial container: "
1004
- f"expected {self.revision_identity}, got {hitch_result.get('revision_identity')}"
1181
+ await self._write_bridge_error(environment, evidence)
1182
+ raise HitchBridgeError(primary_code, primary_message or primary_code, evidence)
1183
+
1184
+ @staticmethod
1185
+ def _parse_hitch_result(
1186
+ result: ExecResult,
1187
+ expected_run_id: str,
1188
+ ) -> tuple[dict[str, Any] | None, str | None, str | None]:
1189
+ if result.return_code != 0:
1190
+ if result.return_code == HITCH_RESULT_MISSING_EXIT:
1191
+ return None, "hitch_result_missing", f"Hitch result file is missing (run_id={expected_run_id})"
1192
+ if result.return_code == HITCH_RESULT_NOT_FILE_EXIT:
1193
+ return None, "hitch_result_not_file", f"Hitch result path is not a regular file (run_id={expected_run_id})"
1194
+ return None, "hitch_result_read_failed", (
1195
+ f"Hitch result file could not be read with code {result.return_code} (run_id={expected_run_id})"
1005
1196
  )
1197
+ payload = (result.stdout or "").strip()
1198
+ if not payload:
1199
+ return None, "hitch_result_empty", f"Hitch result file is empty (run_id={expected_run_id})"
1200
+ try:
1201
+ value = json.loads(payload)
1202
+ except json.JSONDecodeError:
1203
+ return None, "hitch_result_invalid_json", f"Hitch result is not valid JSON (run_id={expected_run_id})"
1204
+ if not isinstance(value, dict):
1205
+ return None, "hitch_result_schema_invalid", f"Hitch result must be a JSON object (run_id={expected_run_id})"
1206
+ error = HitchHarborAgent._result_schema_error(value)
1207
+ if error is not None:
1208
+ return None, "hitch_result_schema_invalid", f"Hitch result schema is invalid: {error} (run_id={expected_run_id})"
1209
+ if value["run_id"] != expected_run_id:
1210
+ return None, "hitch_result_run_id_mismatch", (
1211
+ f"Hitch result run id mismatch: expected {expected_run_id}, got {value['run_id']}"
1212
+ )
1213
+ return value, None, None
1214
+
1215
+ @staticmethod
1216
+ def _result_schema_error(value: dict[str, Any]) -> str | None:
1217
+ if value.get("schema_version") != "1":
1218
+ return "schema_version must be '1'"
1219
+ run_id = value.get("run_id")
1220
+ if not isinstance(run_id, str) or re.fullmatch(r"run_[a-f0-9]{32}", run_id) is None:
1221
+ return "run_id is invalid"
1222
+ if value.get("status") not in {"succeeded", "failed", "timed_out", "cancelled"}:
1223
+ return "status is invalid"
1224
+ exit_code = value.get("exit_code")
1225
+ if isinstance(exit_code, bool) or not isinstance(exit_code, int) or exit_code < 0:
1226
+ return "exit_code must be a non-negative integer"
1227
+ completed_at = value.get("completed_at")
1228
+ if not isinstance(completed_at, str) or not completed_at.strip():
1229
+ return "completed_at must be a non-empty string"
1230
+ if "error" in value:
1231
+ error = value["error"]
1232
+ if not isinstance(error, dict):
1233
+ return "error must be an object"
1234
+ if not isinstance(error.get("code"), str) or not isinstance(error.get("message"), str):
1235
+ return "error.code and error.message must be strings"
1236
+ return None
1237
+
1238
+ def _bridge_error_evidence(
1239
+ self,
1240
+ *,
1241
+ code: str,
1242
+ message: str,
1243
+ trial_id: str,
1244
+ task_id: str,
1245
+ attempt: int,
1246
+ assigned_run_id: str,
1247
+ observed_run_id: str | None,
1248
+ result_path: str,
1249
+ execution: ExecResult,
1250
+ result_read: ExecResult,
1251
+ result_diagnostic: str | None,
1252
+ last_event: dict[str, Any] | None,
1253
+ bundle_export: ExecResult,
1254
+ result_copy: ExecResult,
1255
+ ) -> dict[str, Any]:
1256
+ signal_value = getattr(execution, "signal", None)
1257
+ evidence: dict[str, Any] = {
1258
+ "schema_version": "1",
1259
+ "code": code,
1260
+ "message": self._bounded_tail(message, 2048),
1261
+ "recorded_at": datetime.now(timezone.utc).isoformat(),
1262
+ "eval_id": self.eval_id,
1263
+ "trial_id": trial_id,
1264
+ "task_id": task_id,
1265
+ "attempt": attempt,
1266
+ "assigned_run_id": assigned_run_id,
1267
+ "observed_run_id": observed_run_id,
1268
+ "result_path": result_path,
1269
+ "process": {
1270
+ "return_code": execution.return_code,
1271
+ "signal": signal_value if isinstance(signal_value, str) else None,
1272
+ "stdout_tail": self._bounded_tail(execution.stdout or ""),
1273
+ "stderr_tail": self._bounded_tail(execution.stderr or ""),
1274
+ },
1275
+ "result_read": {
1276
+ "return_code": result_read.return_code,
1277
+ "stdout_tail": self._bounded_tail(result_read.stdout or ""),
1278
+ "stderr_tail": self._bounded_tail(result_read.stderr or ""),
1279
+ },
1280
+ "last_event": self._bounded_event(last_event),
1281
+ "result_diagnostic": result_diagnostic,
1282
+ }
1283
+ if bundle_export.return_code != 0:
1284
+ evidence["bundle_export"] = {
1285
+ "return_code": bundle_export.return_code,
1286
+ "stdout_tail": self._bounded_tail(bundle_export.stdout or ""),
1287
+ "stderr_tail": self._bounded_tail(bundle_export.stderr or ""),
1288
+ }
1289
+ if result_copy.return_code != 0:
1290
+ evidence["result_copy"] = {
1291
+ "return_code": result_copy.return_code,
1292
+ "stdout_tail": self._bounded_tail(result_copy.stdout or ""),
1293
+ "stderr_tail": self._bounded_tail(result_copy.stderr or ""),
1294
+ }
1295
+ if len(json.dumps(evidence, ensure_ascii=False, separators=(",", ":")).encode("utf-8")) > HITCH_BRIDGE_ERROR_MAX_BYTES:
1296
+ evidence["message"] = self._bounded_tail(str(evidence["message"]), 1024)
1297
+ evidence["last_event"] = self._bounded_event_summary(last_event)
1298
+ for section_name in ["process", "result_read", "bundle_export", "result_copy"]:
1299
+ section = evidence.get(section_name)
1300
+ if not isinstance(section, dict):
1301
+ continue
1302
+ for field in ["stdout_tail", "stderr_tail"]:
1303
+ section[field] = self._bounded_tail(str(section.get(field, "")), 2048)
1304
+ evidence["diagnostics_truncated"] = True
1305
+ return evidence
1306
+
1307
+ @staticmethod
1308
+ def _bounded_tail(value: str, max_bytes: int = HITCH_DIAGNOSTIC_MAX_BYTES) -> str:
1309
+ encoded = value.encode("utf-8", errors="replace")
1310
+ if len(encoded) <= max_bytes:
1311
+ return value
1312
+ marker = f"[truncated {len(encoded) - max_bytes} bytes]\n".encode("utf-8")
1313
+ available = max(0, max_bytes - len(marker))
1314
+ suffix = encoded[-available:] if available else b""
1315
+ suffix = suffix.decode("utf-8", errors="ignore").encode("utf-8")
1316
+ return (marker + suffix).decode("utf-8")
1317
+
1318
+ @staticmethod
1319
+ def _bounded_event(event: dict[str, Any] | None) -> dict[str, Any] | None:
1320
+ if event is None:
1321
+ return None
1322
+ encoded = json.dumps(event, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
1323
+ if len(encoded) <= HITCH_DIAGNOSTIC_MAX_BYTES:
1324
+ return event
1325
+ return HitchHarborAgent._bounded_event_summary(event)
1326
+
1327
+ @staticmethod
1328
+ def _bounded_event_summary(event: dict[str, Any] | None) -> dict[str, Any] | None:
1329
+ if event is None:
1330
+ return None
1331
+ return {
1332
+ "type": event.get("type"),
1333
+ "run_id": event.get("run_id"),
1334
+ "truncated": True,
1335
+ }
1336
+
1337
+ @staticmethod
1338
+ async def _write_bridge_error(environment: BaseEnvironment, evidence: dict[str, Any]) -> None:
1339
+ payload = json.dumps(evidence, ensure_ascii=False, separators=(",", ":"), sort_keys=True)
1340
+ await environment.exec(
1341
+ f"umask 077; printf '%s\\n' {shlex.quote(payload)} > {HITCH_BRIDGE_ERROR_LOG}",
1342
+ cwd="/",
1343
+ )
1006
1344
 
1007
1345
  def _trial_identity(self) -> tuple[str, str, int]:
1008
1346
  """Read Harbor's stable trial/task identity from the persisted trial state."""
1009
1347
  trial_dir = self.logs_dir.parent if self.logs_dir.name == "agent" else self.logs_dir
1010
1348
  trial_id = trial_dir.name or "trial__1"
1011
1349
  task_id = self._locked_task_id(trial_dir)
1350
+ if self.logical_attempt is not None:
1351
+ fallback_task_id = trial_id.rsplit("__", 1)[0] if "__" in trial_id else trial_id
1352
+ return trial_id, task_id or fallback_task_id, self.logical_attempt
1012
1353
  match = re.fullmatch(r"(.+)__(\d+)", trial_id)
1013
1354
  if match:
1014
1355
  return trial_id, task_id or match.group(1), max(1, int(match.group(2)))
@@ -1048,6 +1389,7 @@ mv "$stage_dir" "$target_dir"
1048
1389
  return Path(handle.name)
1049
1390
 
1050
1391
  async def _workspace_digest(self, environment: BaseEnvironment) -> str:
1392
+ workdir = self._require_workdir()
1051
1393
  script = r"""
1052
1394
  const fs = require('node:fs');
1053
1395
  const path = require('node:path');
@@ -1068,8 +1410,8 @@ walk(process.argv[1]);
1068
1410
  process.stdout.write('sha256:' + hash.digest('hex'));
1069
1411
  """.strip()
1070
1412
  result = await environment.exec(
1071
- " ".join([self._node_prefix(), "node", "-e", shlex.quote(script), shlex.quote(self.workdir)]),
1072
- cwd=self.workdir,
1413
+ " ".join([self._node_prefix(), "node", "-e", shlex.quote(script), shlex.quote(workdir)]),
1414
+ cwd=workdir,
1073
1415
  )
1074
1416
  digest = (result.stdout or "").strip()
1075
1417
  if result.return_code == 0 and re.fullmatch(r"sha256:[0-9a-f]{64}", digest):
@@ -1145,6 +1487,13 @@ node -e 'process.exit(Number(process.versions.node.split(".")[0]) >= 22 ? 0 : 1)
1145
1487
  raise RuntimeError(f"container setup command failed ({result.return_code}): {diagnostic}")
1146
1488
  return result
1147
1489
 
1490
+ @staticmethod
1491
+ def _exec_diagnostic(result: ExecResult) -> str:
1492
+ for value in (result.stderr, result.stdout):
1493
+ if value and value.strip():
1494
+ return value.strip()
1495
+ return "no diagnostic output"
1496
+
1148
1497
 
1149
1498
  def canonical_manifest_json(manifest: dict[str, Any]) -> str:
1150
1499
  """Canonically encode the runtime identity `{ schema_version, node_range,