@ponythewhite/base-context 1.0.6 → 1.0.8

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 (136) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/base-context-runtime/pyproject.toml +1 -1
  3. package/dist/base-context-runtime/src/rlm/__init__.py +3 -0
  4. package/dist/base-context-runtime/src/rlm/harness.py +86 -48
  5. package/dist/base-context-runtime/test/test_harness.py +215 -140
  6. package/dist/base-context-runtime/uv.lock +1 -1
  7. package/dist/build-info.json +1 -1
  8. package/dist/bundle/amazon-bedrock.js +3 -3
  9. package/dist/bundle/{anthropic-UV5GZDGU.js → anthropic-5PSCZEER.js} +2 -7
  10. package/dist/bundle/{azure-openai-responses-WSNPXLYC.js → azure-openai-responses-AYTLJR33.js} +2 -2
  11. package/dist/bundle/{bundled-modules-MEACET4U.js → bundled-modules-IGMJH2MU.js} +5 -5
  12. package/dist/bundle/{chunk-UPVDERMD.js → chunk-2IPDSTDR.js} +11 -9
  13. package/dist/bundle/chunk-D3KOF57W.js +22040 -0
  14. package/dist/bundle/{chunk-QKI6KEGP.js → chunk-EXEFSSF4.js} +1509 -577
  15. package/dist/bundle/{chunk-VSIYFWYI.js → chunk-KT2BNEPE.js} +21 -8
  16. package/dist/bundle/{chunk-KTNETTHI.js → chunk-Q3MBSBEB.js} +3 -101
  17. package/dist/bundle/{chunk-LFPWHI42.js → chunk-QKIFBA7N.js} +2 -2
  18. package/dist/bundle/{chunk-LIZVVGHR.js → chunk-RVJVXZCS.js} +13 -10
  19. package/dist/bundle/{cli-main-ANUJY7Y4.js → cli-main-BUK7QTHO.js} +4 -4
  20. package/dist/bundle/cli.js +1 -1
  21. package/dist/bundle/{google-T32FMD4G.js → google-HTMCUJ7X.js} +2 -2
  22. package/dist/bundle/{google-vertex-56KP6I3B.js → google-vertex-ZQO65AXF.js} +2 -2
  23. package/dist/bundle/{main-4GIKOHIT.js → main-2FWHT2WZ.js} +5 -5
  24. package/dist/bundle/{mistral-WGBMYIVU.js → mistral-SB7LU56G.js} +1 -1
  25. package/dist/bundle/{openai-codex-responses-PIV2RQ3G.js → openai-codex-responses-5CEW4VN3.js} +53 -27
  26. package/dist/bundle/{openai-completions-64C3JPR4.js → openai-completions-BAZJQSS2.js} +1 -1
  27. package/dist/bundle/{openai-responses-UI7RSTDV.js → openai-responses-VHOE427P.js} +48 -5
  28. package/dist/core/agent-session.d.ts +6 -1
  29. package/dist/core/agent-session.d.ts.map +1 -1
  30. package/dist/core/agent-session.js +189 -31
  31. package/dist/core/agent-session.js.map +1 -1
  32. package/dist/core/canonical-context.d.ts +8 -0
  33. package/dist/core/canonical-context.d.ts.map +1 -1
  34. package/dist/core/canonical-context.js +37 -5
  35. package/dist/core/canonical-context.js.map +1 -1
  36. package/dist/core/compaction/compaction.d.ts +2 -2
  37. package/dist/core/compaction/compaction.d.ts.map +1 -1
  38. package/dist/core/compaction/compaction.js +31 -13
  39. package/dist/core/compaction/compaction.js.map +1 -1
  40. package/dist/core/context-epoch.d.ts +1 -1
  41. package/dist/core/context-epoch.d.ts.map +1 -1
  42. package/dist/core/context-epoch.js +20 -12
  43. package/dist/core/context-epoch.js.map +1 -1
  44. package/dist/core/extensions/types.d.ts +2 -2
  45. package/dist/core/extensions/types.d.ts.map +1 -1
  46. package/dist/core/extensions/types.js.map +1 -1
  47. package/dist/core/history-index-worker.d.ts.map +1 -1
  48. package/dist/core/history-index-worker.js +29 -8
  49. package/dist/core/history-index-worker.js.map +1 -1
  50. package/dist/core/history-index.d.ts +11 -1
  51. package/dist/core/history-index.d.ts.map +1 -1
  52. package/dist/core/history-index.js +2 -1
  53. package/dist/core/history-index.js.map +1 -1
  54. package/dist/core/inference-coordinator.d.ts.map +1 -1
  55. package/dist/core/inference-coordinator.js +110 -13
  56. package/dist/core/inference-coordinator.js.map +1 -1
  57. package/dist/core/kernel/bootstrap.d.ts.map +1 -1
  58. package/dist/core/kernel/bootstrap.js +97 -107
  59. package/dist/core/kernel/bootstrap.js.map +1 -1
  60. package/dist/core/kernel/repl-manager.d.ts.map +1 -1
  61. package/dist/core/kernel/repl-manager.js +11 -0
  62. package/dist/core/kernel/repl-manager.js.map +1 -1
  63. package/dist/core/kernel/shared.d.ts +2 -0
  64. package/dist/core/kernel/shared.d.ts.map +1 -1
  65. package/dist/core/kernel/shared.js.map +1 -1
  66. package/dist/core/messages.d.ts.map +1 -1
  67. package/dist/core/messages.js +4 -1
  68. package/dist/core/messages.js.map +1 -1
  69. package/dist/core/prompts/rlm.d.ts.map +1 -1
  70. package/dist/core/prompts/rlm.js +4 -2
  71. package/dist/core/prompts/rlm.js.map +1 -1
  72. package/dist/core/public-context.d.ts.map +1 -1
  73. package/dist/core/public-context.js +16 -0
  74. package/dist/core/public-context.js.map +1 -1
  75. package/dist/core/refinement/refinement.d.ts +19 -5
  76. package/dist/core/refinement/refinement.d.ts.map +1 -1
  77. package/dist/core/refinement/refinement.js +90 -35
  78. package/dist/core/refinement/refinement.js.map +1 -1
  79. package/dist/core/request-view-selection.d.ts +12 -4
  80. package/dist/core/request-view-selection.d.ts.map +1 -1
  81. package/dist/core/request-view-selection.js +242 -100
  82. package/dist/core/request-view-selection.js.map +1 -1
  83. package/dist/core/retained-tool-output.d.ts +28 -0
  84. package/dist/core/retained-tool-output.d.ts.map +1 -0
  85. package/dist/core/retained-tool-output.js +102 -0
  86. package/dist/core/retained-tool-output.js.map +1 -0
  87. package/dist/core/sdk.d.ts.map +1 -1
  88. package/dist/core/sdk.js +1 -1
  89. package/dist/core/sdk.js.map +1 -1
  90. package/dist/core/selective-recovery.d.ts +47 -3
  91. package/dist/core/selective-recovery.d.ts.map +1 -1
  92. package/dist/core/selective-recovery.js +428 -109
  93. package/dist/core/selective-recovery.js.map +1 -1
  94. package/dist/core/session-history-index.d.ts +3 -1
  95. package/dist/core/session-history-index.d.ts.map +1 -1
  96. package/dist/core/session-history-index.js +1 -1
  97. package/dist/core/session-history-index.js.map +1 -1
  98. package/dist/core/settings-manager.d.ts +5 -0
  99. package/dist/core/settings-manager.d.ts.map +1 -1
  100. package/dist/core/settings-manager.js +3 -0
  101. package/dist/core/settings-manager.js.map +1 -1
  102. package/dist/core/system-prompt.d.ts +3 -1
  103. package/dist/core/system-prompt.d.ts.map +1 -1
  104. package/dist/core/system-prompt.js +3 -3
  105. package/dist/core/system-prompt.js.map +1 -1
  106. package/dist/core/task-frame.d.ts +1 -1
  107. package/dist/core/task-frame.d.ts.map +1 -1
  108. package/dist/core/task-frame.js +21 -1
  109. package/dist/core/task-frame.js.map +1 -1
  110. package/dist/core/task-state-reader.d.ts +10 -2
  111. package/dist/core/task-state-reader.d.ts.map +1 -1
  112. package/dist/core/task-state-reader.js +75 -9
  113. package/dist/core/task-state-reader.js.map +1 -1
  114. package/dist/core/tools/ipython.d.ts +5 -0
  115. package/dist/core/tools/ipython.d.ts.map +1 -1
  116. package/dist/core/tools/ipython.js +59 -17
  117. package/dist/core/tools/ipython.js.map +1 -1
  118. package/dist/core/tools/prime-context.d.ts.map +1 -1
  119. package/dist/core/tools/prime-context.js +1 -1
  120. package/dist/core/tools/prime-context.js.map +1 -1
  121. package/dist/core/view-units.d.ts +4 -0
  122. package/dist/core/view-units.d.ts.map +1 -1
  123. package/dist/core/view-units.js +28 -19
  124. package/dist/core/view-units.js.map +1 -1
  125. package/dist/installer.mjs +98 -77
  126. package/dist/modes/interactive/components/refinement-outcome-message.d.ts.map +1 -1
  127. package/dist/modes/interactive/components/refinement-outcome-message.js.map +1 -1
  128. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  129. package/dist/modes/interactive/interactive-mode.js +5 -5
  130. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  131. package/dist/skills/skill-creator/SKILL.md +18 -1
  132. package/docs/settings.md +45 -0
  133. package/docs/skills.md +77 -2
  134. package/package.json +4 -4
  135. package/skills/skill-creator/SKILL.md +18 -1
  136. package/dist/bundle/chunk-W46VZFI6.js +0 -21681
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.0.8] - 2026-09-18
4
+
5
+ - Fixed harness-state saves to replace files atomically, preserve failed mutations, and report corrupt state without overwriting it.
6
+ - Changed Python kernel bootstrap to reuse package metadata and dependency planning within each startup operation.
7
+ - Fixed accumulated TaskFrame overflow by preparing one bounded base and requiring an acknowledged context epoch before sending it.
8
+ - Fixed literal public-history recovery to scan exact substrings and resume bounded searches against the original captured branch.
9
+ - Added decoded UTF-8 byte windows for public fields and retained tool output, with actual offsets and omission flags.
10
+ - Changed large IPython results to publish bounded error-first previews and finalized detail-channel excerpts. Full captured text is retained in session output logs and is available through source-qualified recovery references. Live output streaming and structured result fields are preserved; bytes discarded upstream remain unavailable.
11
+ - Fixed kernel capture completeness when output arrives after a stream reaches its exact output limit.
12
+ - Reused reduced task evidence for unchanged captured sources and process only appended evidence; rebuild on branch/source changes while retaining relation updates and complete-view budget checks.
13
+ - Reused prepared dependency closure data within request selection and prepare the fixed history INSERT once per SQLite connection.
14
+ - Allowed request-budget recovery to propose a fresh TaskFrame base without publishing it before the native epoch ACK.
15
+ - Kept session paths after stable harness guidance and flush completed/error stream presentation before shutdown.
16
+ - Changed automatic recovery to try bounded TaskFrame rebasing and legal public-view reduction before summary recovery; reject oversized mandatory context and size summaries against retained context and output allowance.
17
+ - Reused prepared history-index inserts and guide repeated Python work toward small editable helpers with fresh inputs.
18
+ - Fixed resuming overflowed sessions using canonical source order and restored assistant identities; filter failed replies before provider mapping and re-acknowledge unbudgeted summary checkpoints before sending.
19
+ - Skipped automatic refinement when task and goal content are unchanged, and kept refinement history and version-only bookkeeping out of the main prompt.
20
+ - Added opt-in scoped error-fix advice with explicit workspace memory CRUD and bounded deterministic retrieval.
21
+
22
+ ## [1.0.7] - 2026-09-18
23
+
24
+ - Fixed reopening saved sessions with native tool continuation history.
25
+ - Allowed subsequent native requests to retain the full public tool view when no token budget is configured, without changing configured budget checks.
26
+ - Kept startup, compaction reloads, and tree navigation read-only until a fresh native request is admitted.
27
+
3
28
  ## [1.0.6] - 2026-09-18
4
29
 
5
30
  - Fixed automatic and manual compaction failing after native tool continuations.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "base-context-runtime"
3
- version = "1.0.6"
3
+ version = "1.0.8"
4
4
  description = "Kernel-side runtime shim for Base Context recursion"
5
5
  license = "MIT"
6
6
  license-files = ["LICENSE", "NOTICE"]
@@ -89,6 +89,9 @@ async def prime_context(request: object) -> dict[str, object]:
89
89
  attached intact to this active cell's finalized ipython tool result. The
90
90
  returned value alone is not a model-context update. Freshness is unknown.
91
91
  Detached requests and sessions without the native tool are not authorized.
92
+ Resume search/recover with its cursor until exhausted; cursors are ephemeral.
93
+ Read accepts one-based lines or zero-based, end-exclusive UTF-8 byte windows.
94
+ Returned offsets delimit complete decoded characters; omission flags mark gaps.
92
95
  """
93
96
  return await host_request("prime_context", {"request": request})
94
97
 
@@ -10,16 +10,18 @@ Execution still belongs to Base Context's TypeScript host and the existing
10
10
  from __future__ import annotations
11
11
 
12
12
  import json
13
+ import os
13
14
  from dataclasses import asdict, dataclass, field, fields
14
15
  from datetime import datetime, timezone
15
16
  from os import fstat, linesep
16
17
  from pathlib import Path
18
+ from tempfile import NamedTemporaryFile
17
19
  from typing import Any, Literal
18
20
 
19
21
  from .product import assert_product_state_path, product_env, product_state_path
20
22
 
21
23
  HarnessKind = Literal["prompt", "memory", "skill", "subagent"]
22
- HarnessScope = Literal["local", "global"]
24
+ HarnessScope = Literal["local", "global", "workspace"]
23
25
 
24
26
  _DEFAULT_FILE_NAME = "harness_state.json"
25
27
  _DEFAULT_HARNESS_DIR_NAME = "harness"
@@ -56,15 +58,12 @@ def _resolve_global_flag(global_: bool = False, extra: dict[str, Any] | None = N
56
58
  return bool(global_)
57
59
 
58
60
 
59
- def _strip_scope_prefix(id: str | None, global_: bool) -> tuple[str | None, bool]:
60
- # overview() displays entries as [local:id]/[global:id]; accept those ids
61
- # verbatim. A global: prefix routes to the global store unless the caller
62
- # already forced a scope via global_.
63
- if isinstance(id, str):
64
- scope, sep, rest = id.partition(":")
65
- if sep and rest and scope in ("local", "global"):
66
- return rest, global_ or scope == "global"
67
- return id, global_
61
+ def _resolve_scope(global_: bool, scope: HarnessScope | None, default: HarnessScope = "local") -> HarnessScope:
62
+ if scope is not None and scope not in ("local", "global", "workspace"):
63
+ raise ValueError(f"unknown harness scope {scope!r}")
64
+ if global_ and scope not in (None, "global"):
65
+ raise ValueError("global_=True conflicts with the selected harness scope")
66
+ return scope or ("global" if global_ else default)
68
67
 
69
68
 
70
69
  def _env_dir(suffix: str) -> str | None:
@@ -73,7 +72,16 @@ def _env_dir(suffix: str) -> str | None:
73
72
  return value or None
74
73
 
75
74
 
76
- def _state_file(state_dir: str | Path | None = None, *, global_: bool = False) -> Path:
75
+ def _state_file(
76
+ state_dir: str | Path | None = None, *, global_: bool = False, scope: HarnessScope | None = None
77
+ ) -> Path:
78
+ scope = _resolve_scope(global_, scope)
79
+ if scope == "workspace":
80
+ root = state_dir or _env_dir("WORKSPACE_HARNESS_STATE_DIR")
81
+ if root is None:
82
+ raise RuntimeError("Workspace harness state requires the owner's BASE_CONTEXT_WORKSPACE_HARNESS_STATE_DIR")
83
+ return assert_product_state_path(Path(root).expanduser() / "harness-state.json")
84
+ global_ = scope == "global"
77
85
  root: str | Path | None = state_dir
78
86
  if root is None:
79
87
  root = _env_dir("GLOBAL_HARNESS_STATE_DIR") if global_ else _env_dir("HARNESS_STATE_DIR")
@@ -163,7 +171,7 @@ class HarnessState:
163
171
  self.file_path = (
164
172
  assert_product_state_path(file_path)
165
173
  if file_path
166
- else _state_file(global_=(scope == "global"))
174
+ else _state_file(scope=scope)
167
175
  )
168
176
  self.scope: HarnessScope = scope
169
177
  # When set, local mutations raise instead of vanishing into a volatile
@@ -251,13 +259,9 @@ class HarnessState:
251
259
  if error is admission_error:
252
260
  raise
253
261
  raise admission_error from error
254
- # A corrupt or unreadable state file must not crash the kernel or block
255
- # refinement. Treat it as empty; the next save() rewrites it cleanly.
256
- data = {}
257
- # json.load returns non-dict types for valid JSON like `null`, `[]`, or a bare
258
- # string; coerce those to an empty object before attribute access.
262
+ raise ValueError(f"Invalid harness state at {self.file_path}: {error}") from error
259
263
  if not isinstance(data, dict):
260
- data = {}
264
+ raise ValueError(f"Invalid harness state at {self.file_path}: expected a JSON object")
261
265
  # Refuse before replacing cached state; count candidates before normalization.
262
266
  self._check_item_count(data.get("entries"), data.get("refinements"))
263
267
 
@@ -279,7 +283,7 @@ class HarnessState:
279
283
  continue
280
284
  if not isinstance(entry_data.get("path"), str):
281
285
  entry_data["path"] = "general"
282
- if entry_data.get("scope") not in ("local", "global"):
286
+ if entry_data.get("scope") not in ("local", "global", "workspace"):
283
287
  entry_data["scope"] = self.scope
284
288
  if not isinstance(entry_data.get("source"), str):
285
289
  entry_data["source"] = "agent"
@@ -322,14 +326,35 @@ class HarnessState:
322
326
  self._loaded_mtime = mtime
323
327
  return self
324
328
 
325
- def _global_target(self, global_: bool, extra: dict[str, Any] | None = None) -> "HarnessState | None":
326
- if not _resolve_global_flag(global_, extra):
329
+ def _target(self, global_: bool, extra: dict[str, Any] | None = None) -> "HarnessState | None":
330
+ extra = dict(extra or {})
331
+ scope = extra.pop("scope", None)
332
+ global_ = _resolve_global_flag(global_, extra)
333
+ scope = _resolve_scope(global_, scope, self.scope)
334
+ if scope == self.scope and not global_:
327
335
  return None
328
- target = get_harness_state(state_dir=self._global_target_state_dir, global_=True)
336
+ target = get_harness_state(
337
+ state_dir=self._global_target_state_dir if scope == "global" else None, scope=scope
338
+ )
329
339
  if self.file_path is not None and target.file_path == self.file_path and target.scope == self.scope:
330
340
  return None
331
341
  return target
332
342
 
343
+ def _entry_target(
344
+ self, id: str | None, global_: bool, extra: dict[str, Any]
345
+ ) -> tuple[str | None, "HarnessState | None"]:
346
+ extra = dict(extra)
347
+ if isinstance(id, str):
348
+ scope, sep, rest = id.partition(":")
349
+ if sep and rest and scope in ("local", "global", "workspace"):
350
+ if extra.get("scope") not in (None, scope):
351
+ raise ValueError("entry id conflicts with the selected harness scope")
352
+ extra["scope"] = scope
353
+ if scope == "global":
354
+ global_ = True
355
+ id = rest
356
+ return id, self._target(global_, extra)
357
+
333
358
  def save(self) -> "HarnessState":
334
359
  if self.file_path is None:
335
360
  # Volatile stores have no encoded image; their containers remain uncapped.
@@ -349,8 +374,17 @@ class HarnessState:
349
374
  if len(image) > self._max_source_bytes:
350
375
  raise HarnessStateLimitError(f"Harness state exceeds max_source_bytes={self._max_source_bytes}")
351
376
  self.file_path.parent.mkdir(parents=True, exist_ok=True)
352
- with self.file_path.open("wb") as f:
353
- f.write(image)
377
+ temporary_path: Path | None = None
378
+ try:
379
+ with NamedTemporaryFile(
380
+ mode="wb", dir=self.file_path.parent, prefix=f".{self.file_path.name}.", suffix=".tmp", delete=False
381
+ ) as temporary:
382
+ temporary_path = Path(temporary.name)
383
+ temporary.write(image)
384
+ os.replace(temporary_path, self.file_path)
385
+ finally:
386
+ if temporary_path is not None:
387
+ temporary_path.unlink(missing_ok=True)
354
388
  self._loaded_mtime = self._disk_mtime()
355
389
  return self
356
390
 
@@ -369,8 +403,8 @@ class HarnessState:
369
403
  global_: bool = False,
370
404
  **kwargs: Any,
371
405
  ) -> HarnessEntry:
372
- id, global_ = _strip_scope_prefix(id, global_)
373
- if target := self._global_target(global_, kwargs):
406
+ id, target = self._entry_target(id, global_, kwargs)
407
+ if target is not None:
374
408
  return target.upsert(
375
409
  kind,
376
410
  title,
@@ -454,7 +488,7 @@ class HarnessState:
454
488
  self.entries[kind][entry_id] = entry
455
489
  try:
456
490
  self.save()
457
- except HarnessStateLimitError:
491
+ except BaseException:
458
492
  if existing:
459
493
  vars(existing).update(previous_fields)
460
494
  else:
@@ -463,8 +497,8 @@ class HarnessState:
463
497
  return entry
464
498
 
465
499
  def get(self, kind: HarnessKind, id: str, *, global_: bool = False, **kwargs: Any) -> HarnessEntry | None:
466
- id, global_ = _strip_scope_prefix(id, global_)
467
- if target := self._global_target(global_, kwargs):
500
+ id, target = self._entry_target(id, global_, kwargs)
501
+ if target is not None:
468
502
  return target.get(kind, id)
469
503
  self._sync_from_disk()
470
504
  if kind not in self.entries:
@@ -472,8 +506,8 @@ class HarnessState:
472
506
  return self.entries[kind].get(id)
473
507
 
474
508
  def delete(self, kind: HarnessKind, id: str, *, global_: bool = False, **kwargs: Any) -> bool:
475
- id, global_ = _strip_scope_prefix(id, global_)
476
- if target := self._global_target(global_, kwargs):
509
+ id, target = self._entry_target(id, global_, kwargs)
510
+ if target is not None:
477
511
  return target.delete(kind, id)
478
512
  self._ensure_local_writable()
479
513
  self._sync_from_disk()
@@ -486,15 +520,15 @@ class HarnessState:
486
520
  del records[id]
487
521
  try:
488
522
  self.save()
489
- except HarnessStateLimitError:
490
- # Keep the original map object and insertion order on budget refusal.
523
+ except BaseException:
524
+ # Keep the original map object and insertion order when saving fails.
491
525
  records.clear()
492
526
  records.update(previous_records)
493
527
  raise
494
528
  return True
495
529
 
496
530
  def list(self, kind: HarnessKind | None = None, *, global_: bool = False, **kwargs: Any) -> list[HarnessEntry]:
497
- if target := self._global_target(global_, kwargs):
531
+ if target := self._target(global_, kwargs):
498
532
  return target.list(kind)
499
533
  self._sync_from_disk()
500
534
  kinds = [kind] if kind else list(_KINDS)
@@ -520,8 +554,8 @@ class HarnessState:
520
554
  global_: bool = False,
521
555
  **kwargs: Any,
522
556
  ) -> HarnessEntry:
523
- id, global_ = _strip_scope_prefix(id, global_)
524
- if target := self._global_target(global_, kwargs):
557
+ id, target = self._entry_target(id, global_, kwargs)
558
+ if target is not None:
525
559
  return target.create(
526
560
  kind,
527
561
  title,
@@ -567,8 +601,8 @@ class HarnessState:
567
601
  global_: bool = False,
568
602
  **kwargs: Any,
569
603
  ) -> HarnessEntry:
570
- id, global_ = _strip_scope_prefix(id, global_)
571
- if target := self._global_target(global_, kwargs):
604
+ id, target = self._entry_target(id, global_, kwargs)
605
+ if target is not None:
572
606
  return target.update(
573
607
  kind,
574
608
  id,
@@ -755,7 +789,7 @@ class HarnessState:
755
789
  global_: bool = False,
756
790
  **kwargs: Any,
757
791
  ) -> RefinementEvent:
758
- if target := self._global_target(global_, kwargs):
792
+ if target := self._target(global_, kwargs):
759
793
  return target.record_refinement(trigger, changes, evidence=evidence, outcome=outcome, id=id)
760
794
  self._ensure_local_writable()
761
795
  self._sync_from_disk()
@@ -771,7 +805,7 @@ class HarnessState:
771
805
  self.refinements.append(event)
772
806
  try:
773
807
  self.save()
774
- except HarnessStateLimitError:
808
+ except BaseException:
775
809
  self.refinements.pop()
776
810
  raise
777
811
  return event
@@ -794,7 +828,7 @@ class HarnessState:
794
828
  return plan
795
829
 
796
830
  def overview(self, *, max_entries_per_kind: int = 20, global_: bool = False, **kwargs: Any) -> str:
797
- if target := self._global_target(global_, kwargs):
831
+ if target := self._target(global_, kwargs):
798
832
  return target.overview(max_entries_per_kind=max_entries_per_kind)
799
833
  self._sync_from_disk()
800
834
  lines = [
@@ -843,7 +877,7 @@ class HarnessState:
843
877
  return "\n".join(lines)
844
878
 
845
879
  def snapshot(self, *, global_: bool = False, **kwargs: Any) -> dict[str, Any]:
846
- if target := self._global_target(global_, kwargs):
880
+ if target := self._target(global_, kwargs):
847
881
  return target.snapshot()
848
882
  self._sync_from_disk()
849
883
  return {
@@ -858,12 +892,16 @@ class HarnessState:
858
892
 
859
893
 
860
894
  def get_harness_state(
861
- state_dir: str | Path | None = None, *, global_: bool = False, **kwargs: Any
895
+ state_dir: str | Path | None = None, *, global_: bool = False, scope: HarnessScope | None = None, **kwargs: Any
862
896
  ) -> HarnessState:
863
- """Return the cached local harness state, or global when requested."""
897
+ """Return a store for an explicit scope; local remains the default.
898
+
899
+ Workspace paths come from the session owner, never the mutable REPL cwd.
900
+ Explicit CRUD is available independently of automatic learning settings.
901
+ """
864
902
  global_ = _resolve_global_flag(global_, kwargs)
865
- file_path = _state_file(state_dir, global_=global_)
866
- scope: HarnessScope = "global" if global_ else "local"
903
+ scope = _resolve_scope(global_, scope)
904
+ file_path = _state_file(state_dir, scope=scope)
867
905
  cache_key = (file_path, scope)
868
906
  state = _state_cache.get(cache_key)
869
907
  if state is None:
@@ -873,9 +911,9 @@ def get_harness_state(
873
911
  # state_dir call aliases the same local file. An explicit dir that merely
874
912
  # aliases the env resolution must not sandbox later global_=True writes
875
913
  # either, so pin only when the explicit dir actually diverges.
876
- if state_dir is not None:
914
+ if state_dir is not None and scope != "workspace":
877
915
  try:
878
- env_file: Path | None = _state_file(global_=global_)
916
+ env_file: Path | None = _state_file(scope=scope)
879
917
  except RuntimeError:
880
918
  env_file = None
881
919
  if file_path != env_file: