agent-bios 0.15.0 → 0.16.0
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.
- package/DEPENDENCIES.md +35 -12
- package/README.md +346 -31
- package/claude/CLAUDE.md +2 -2
- package/claude/agents/frontier.md +1 -1
- package/claude/agents/sweep.md +3 -3
- package/claude/agents/workhorse.md +2 -2
- package/claude/guides/claude-prompting.md +72 -39
- package/claude/guides/cli-multi-model-workflow.md +33 -15
- package/claude/guides/gpt-prompting.md +103 -39
- package/claude/guides/review-request.md +27 -0
- package/claude/guides/session-distill-workflow.md +54 -2
- package/claude/guides/slide-writing/RUNBOOK.md +137 -0
- package/claude/guides/slide-writing/scripts/pair.py +979 -0
- package/claude/guides/slide-writing/scripts/render.mjs +82 -0
- package/claude/guides/slide-writing.md +195 -0
- package/claude/guides/svg-visualization-guide.md +9 -0
- package/claude/guides/verification-discipline.md +5 -1
- package/claude/hooks/tooling-gotchas-hook.py +7 -5
- package/codex/AGENTS.md +2 -2
- package/codex/agents/frontier.toml +2 -1
- package/codex/agents/reviewer.toml +1 -1
- package/codex/agents/sweep.toml +3 -3
- package/codex/agents/workhorse.toml +1 -1
- package/codex/config-additions.toml +1 -1
- package/codex/guides/claude-prompting.md +72 -39
- package/codex/guides/cli-multi-model-workflow.md +33 -15
- package/codex/guides/gpt-prompting.md +103 -39
- package/codex/guides/review-request.md +27 -0
- package/codex/guides/session-distill-workflow.md +54 -2
- package/codex/guides/slide-writing/RUNBOOK.md +137 -0
- package/codex/guides/slide-writing/scripts/pair.py +979 -0
- package/codex/guides/slide-writing/scripts/render.mjs +82 -0
- package/codex/guides/slide-writing.md +195 -0
- package/codex/guides/svg-visualization-guide.md +9 -0
- package/codex/guides/verification-discipline.md +5 -1
- package/compose/assemble.py +290 -14
- package/compose/bootstrap/SKILL.md +119 -0
- package/compose/check-domains.py +102 -9
- package/compose/corpus-state.py +4 -0
- package/compose/corpus.py +387 -0
- package/compose/corpus_catalog.py +882 -0
- package/compose/corpus_install.py +1617 -0
- package/compose/corpus_session.py +726 -0
- package/compose/corpus_store.py +1414 -0
- package/compose/corpus_transaction.py +236 -0
- package/compose/corpus_ui.py +644 -0
- package/compose/domains.json +101 -100
- package/install.sh +63 -18
- package/launch/agent-launch.py +1216 -179
- package/launch/agent-launch.toml +12 -16
- package/launch/i18n/en.toml +112 -7
- package/launch/i18n/ja.toml +112 -7
- package/launch/i18n/ko.toml +112 -7
- package/learn/collect-learning.py +46 -19
- package/learn/migrate-learnings.py +10 -1
- package/package.json +11 -3
- package/provenance.json +1 -1
- package/session-cost.py +22 -2
- package/wrappers/codex-helm.sh +3 -3
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
"""Per-call corpus delivery and durable host-session bindings.
|
|
2
|
+
|
|
3
|
+
Native homes stay native. The only files this module writes are private activation
|
|
4
|
+
records and pins; no global instructions, auth, or discovery registration is copied.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import pathlib
|
|
12
|
+
import queue
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
import uuid
|
|
19
|
+
import unicodedata
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SessionError(RuntimeError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _global_instruction_choice(host, include_global_instructions):
|
|
27
|
+
if type(include_global_instructions) is not bool:
|
|
28
|
+
raise SessionError('include_global_instructions must be a boolean')
|
|
29
|
+
if not include_global_instructions and host != 'claude':
|
|
30
|
+
raise SessionError(
|
|
31
|
+
'excluding global instruction files is not supported by the current Codex adapter; '
|
|
32
|
+
'keep global instructions included. The native CLI has no selective source control, '
|
|
33
|
+
'and the OS-level workaround interferes with its execution sandbox'
|
|
34
|
+
)
|
|
35
|
+
return include_global_instructions
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _claude_global_instruction_patterns(env, cwd, check_paths=True):
|
|
39
|
+
"""Match Claude's user-scope lookup without redirecting its configuration home."""
|
|
40
|
+
configured = env.get('CLAUDE_CONFIG_DIR')
|
|
41
|
+
if configured is None:
|
|
42
|
+
home = env.get('HOME')
|
|
43
|
+
if home is None:
|
|
44
|
+
import pwd
|
|
45
|
+
home = pwd.getpwuid(os.getuid()).pw_dir
|
|
46
|
+
if not isinstance(home, str) or not home or not pathlib.Path(home).is_absolute():
|
|
47
|
+
raise SessionError('global instruction exclusion requires an absolute native home; keep inclusion enabled')
|
|
48
|
+
configured = str(pathlib.Path(home) / '.claude')
|
|
49
|
+
if not isinstance(configured, str) or not configured or not pathlib.Path(configured).is_absolute():
|
|
50
|
+
raise SessionError(
|
|
51
|
+
'global instruction exclusion does not support an empty or relative CLAUDE_CONFIG_DIR; '
|
|
52
|
+
'keep inclusion enabled without changing the existing login/configuration identity'
|
|
53
|
+
)
|
|
54
|
+
path = pathlib.Path(os.path.normpath(unicodedata.normalize('NFC', configured))) / 'CLAUDE.md'
|
|
55
|
+
# Claude's exclusion API uses globs and adds resolved-path variants. Restrict
|
|
56
|
+
# the adapter to literal, unambiguous roots so it cannot exclude project files.
|
|
57
|
+
if any(char in str(path) for char in '*?[]{}()!+@|\\'):
|
|
58
|
+
raise SessionError('global instruction exclusion does not support glob metacharacters in the config path; keep inclusion enabled')
|
|
59
|
+
rules = path.parent / 'rules'
|
|
60
|
+
if check_paths:
|
|
61
|
+
if any(part.is_symlink() for part in (path, *path.parents)):
|
|
62
|
+
raise SessionError('global instruction exclusion does not support a symlinked global instruction path; keep inclusion enabled')
|
|
63
|
+
if rules.is_symlink() or (rules.is_dir() and any(member.is_symlink() for member in rules.rglob('*'))):
|
|
64
|
+
raise SessionError('global instruction exclusion does not support symlinked user rules; keep inclusion enabled')
|
|
65
|
+
directory = pathlib.Path(cwd).resolve()
|
|
66
|
+
parents = (directory, *directory.parents)
|
|
67
|
+
project_root = next((parent for parent in parents if (parent / '.git').exists()), directory)
|
|
68
|
+
for parent in parents:
|
|
69
|
+
for relative in ('CLAUDE.md', 'CLAUDE.local.md', '.claude/CLAUDE.md'):
|
|
70
|
+
project = parent / relative
|
|
71
|
+
if project == path:
|
|
72
|
+
if directory.is_relative_to(path.parent) or path.is_relative_to(project_root):
|
|
73
|
+
raise SessionError(f'global instructions also belong to this project: {path}; keep inclusion enabled')
|
|
74
|
+
elif project.is_file() and (
|
|
75
|
+
(path.is_file() and project.samefile(path)) or project.resolve().is_relative_to(rules)
|
|
76
|
+
):
|
|
77
|
+
raise SessionError(f'global and project instructions refer to the same file: {project}; keep inclusion enabled')
|
|
78
|
+
# User rules are a separate native instruction source, not imports of CLAUDE.md.
|
|
79
|
+
return [str(path), str(rules) + '/**']
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _claude_exclusion_version(command, cwd, env):
|
|
83
|
+
try:
|
|
84
|
+
result = subprocess.run([command, '--version'], cwd=cwd, env=env, input='',
|
|
85
|
+
capture_output=True, text=True, timeout=10)
|
|
86
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
87
|
+
raise SessionError('could not verify Claude support for global instruction exclusion; keep inclusion enabled') from exc
|
|
88
|
+
version = re.match(r'^(\d+)\.(\d+)\.(\d+)(?:\s|$)', result.stdout.strip())
|
|
89
|
+
if result.returncode or version is None or tuple(map(int, version.groups())) < (2, 1, 263):
|
|
90
|
+
raise SessionError('global instruction exclusion requires Claude Code 2.1.263 or newer; keep inclusion enabled')
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _settings_values(argv):
|
|
94
|
+
if not isinstance(argv, (list, tuple)) or not all(isinstance(token, str) for token in argv):
|
|
95
|
+
raise SessionError('global instruction exclusion requires a valid argument list')
|
|
96
|
+
options = argv[:argv.index('--')] if '--' in argv else argv
|
|
97
|
+
values = []
|
|
98
|
+
for index, token in enumerate(options):
|
|
99
|
+
if token == '--settings':
|
|
100
|
+
values.append(options[index + 1] if index + 1 < len(options) else None)
|
|
101
|
+
elif token.startswith('--settings='):
|
|
102
|
+
values.append(token.split('=', 1)[1])
|
|
103
|
+
return values
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _record_instruction_choice(record, env, check_paths=True):
|
|
107
|
+
include = _global_instruction_choice(record['host'], record.get('include_global_instructions', True))
|
|
108
|
+
if not include:
|
|
109
|
+
values = _settings_values(record['argv'])
|
|
110
|
+
expected = {'claudeMdExcludes': _claude_global_instruction_patterns(env, record['cwd'], check_paths)}
|
|
111
|
+
try:
|
|
112
|
+
valid = len(values) == 1 and json.loads(values[0]) == expected
|
|
113
|
+
except (TypeError, ValueError):
|
|
114
|
+
valid = False
|
|
115
|
+
if not valid:
|
|
116
|
+
raise SessionError('session pin does not carry its declared global instruction exclusion; start a new session')
|
|
117
|
+
return include
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def atomic_json(path, data):
|
|
121
|
+
path = pathlib.Path(path)
|
|
122
|
+
if path.parent.is_symlink():
|
|
123
|
+
raise SessionError(f"refusing symlink directory: {path.parent}")
|
|
124
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
125
|
+
if path.is_symlink():
|
|
126
|
+
raise SessionError(f"refusing symlink: {path}")
|
|
127
|
+
tmp = path.with_name(path.name + '.' + uuid.uuid4().hex + '.tmp')
|
|
128
|
+
try:
|
|
129
|
+
with open(tmp, 'x', encoding='utf-8') as out:
|
|
130
|
+
os.chmod(tmp, 0o600)
|
|
131
|
+
json.dump(data, out, ensure_ascii=False, indent=2)
|
|
132
|
+
out.write('\n')
|
|
133
|
+
out.flush()
|
|
134
|
+
os.fsync(out.fileno())
|
|
135
|
+
os.replace(tmp, path)
|
|
136
|
+
fd = os.open(path.parent, os.O_RDONLY)
|
|
137
|
+
try:
|
|
138
|
+
os.fsync(fd)
|
|
139
|
+
finally:
|
|
140
|
+
os.close(fd)
|
|
141
|
+
finally:
|
|
142
|
+
tmp.unlink(missing_ok=True)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class CodexServer:
|
|
146
|
+
"""One owned stdio server. No turn/start or model generation is used."""
|
|
147
|
+
|
|
148
|
+
def __init__(self, command, config_args=(), cwd=None, env=None):
|
|
149
|
+
self.command, self.config_args, self.cwd, self.env = command, config_args, cwd, env
|
|
150
|
+
self.sequence = 0
|
|
151
|
+
self.events = queue.Queue()
|
|
152
|
+
|
|
153
|
+
def __enter__(self):
|
|
154
|
+
self.proc = subprocess.Popen(
|
|
155
|
+
[self.command, *self.config_args, 'app-server', '--stdio'],
|
|
156
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
|
|
157
|
+
cwd=self.cwd, env=self.env, text=True, encoding='utf-8',
|
|
158
|
+
)
|
|
159
|
+
self.reader = threading.Thread(target=self._read, daemon=True)
|
|
160
|
+
self.reader.start()
|
|
161
|
+
try:
|
|
162
|
+
self.call('initialize', {'clientInfo': {'name': 'agent-bios', 'version': '1'},
|
|
163
|
+
'capabilities': {'experimentalApi': True}})
|
|
164
|
+
self.proc.stdin.write('{"method":"initialized"}\n')
|
|
165
|
+
self.proc.stdin.flush()
|
|
166
|
+
return self
|
|
167
|
+
except BaseException:
|
|
168
|
+
self.__exit__(None, None, None)
|
|
169
|
+
raise
|
|
170
|
+
|
|
171
|
+
def _read(self):
|
|
172
|
+
try:
|
|
173
|
+
for line in self.proc.stdout:
|
|
174
|
+
try:
|
|
175
|
+
self.events.put(json.loads(line))
|
|
176
|
+
except ValueError:
|
|
177
|
+
self.events.put({'transport_error': 'invalid app-server JSON'})
|
|
178
|
+
finally:
|
|
179
|
+
self.events.put({'transport_error': 'app-server closed before its response'})
|
|
180
|
+
|
|
181
|
+
def call(self, method, params, timeout=45):
|
|
182
|
+
self.sequence += 1
|
|
183
|
+
request_id = self.sequence
|
|
184
|
+
self.proc.stdin.write(json.dumps({'id': request_id, 'method': method, 'params': params}) + '\n')
|
|
185
|
+
self.proc.stdin.flush()
|
|
186
|
+
deadline = time.monotonic() + timeout
|
|
187
|
+
while time.monotonic() < deadline:
|
|
188
|
+
try:
|
|
189
|
+
message = self.events.get(timeout=max(0.01, deadline - time.monotonic()))
|
|
190
|
+
except queue.Empty as exc:
|
|
191
|
+
raise SessionError(f"app-server timed out: {method}") from exc
|
|
192
|
+
if 'transport_error' in message:
|
|
193
|
+
raise SessionError(message['transport_error'])
|
|
194
|
+
if message.get('id') != request_id:
|
|
195
|
+
continue
|
|
196
|
+
if 'error' in message:
|
|
197
|
+
raise SessionError(f"app-server {method}: {message['error'].get('message', 'refused')}")
|
|
198
|
+
return message['result']
|
|
199
|
+
raise SessionError(f"app-server timed out: {method}")
|
|
200
|
+
|
|
201
|
+
def __exit__(self, *unused):
|
|
202
|
+
if self.proc.stdin:
|
|
203
|
+
self.proc.stdin.close()
|
|
204
|
+
try:
|
|
205
|
+
self.proc.wait(timeout=5)
|
|
206
|
+
except subprocess.TimeoutExpired:
|
|
207
|
+
self.proc.terminate()
|
|
208
|
+
try:
|
|
209
|
+
self.proc.wait(timeout=5)
|
|
210
|
+
except subprocess.TimeoutExpired:
|
|
211
|
+
self.proc.kill()
|
|
212
|
+
self.proc.wait(timeout=5)
|
|
213
|
+
if self.proc.stdout:
|
|
214
|
+
self.proc.stdout.close()
|
|
215
|
+
self.reader.join(timeout=2)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def config_flags(argv, exclude_developer=False):
|
|
219
|
+
result = []
|
|
220
|
+
index = 0
|
|
221
|
+
while index < len(argv):
|
|
222
|
+
token = argv[index]
|
|
223
|
+
if token in ('-c', '--config'):
|
|
224
|
+
if index + 1 >= len(argv):
|
|
225
|
+
raise SessionError(f"missing value for {token}")
|
|
226
|
+
value = argv[index + 1]
|
|
227
|
+
if not (exclude_developer and value.split('=', 1)[0] == 'developer_instructions'):
|
|
228
|
+
result += ['-c', value]
|
|
229
|
+
index += 2
|
|
230
|
+
continue
|
|
231
|
+
if token.startswith('--config='):
|
|
232
|
+
value = token[len('--config='):]
|
|
233
|
+
if not (exclude_developer and value.split('=', 1)[0] == 'developer_instructions'):
|
|
234
|
+
result += ['-c', value]
|
|
235
|
+
if token in ('-p', '--profile') and index + 1 < len(argv):
|
|
236
|
+
result += ['--profile', argv[index + 1]]
|
|
237
|
+
index += 1
|
|
238
|
+
if token.startswith('--profile='):
|
|
239
|
+
result += ['--profile', token.split('=', 1)[1]]
|
|
240
|
+
index += 1
|
|
241
|
+
return result
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def named_profile(argv):
|
|
245
|
+
"""Return the one native profile requested by a Codex argv, if any.
|
|
246
|
+
|
|
247
|
+
Profiles are a top-level Codex runtime feature. The app-server used below
|
|
248
|
+
for ``config/read`` deliberately has no profile input, so treating a
|
|
249
|
+
profile as ``-c profile=...`` either changes its meaning or makes the
|
|
250
|
+
server reject the request. Keep this parser narrow and fail before a host
|
|
251
|
+
process is started rather than silently reading the base configuration.
|
|
252
|
+
"""
|
|
253
|
+
profile = None
|
|
254
|
+
index = 0
|
|
255
|
+
while index < len(argv):
|
|
256
|
+
token = argv[index]
|
|
257
|
+
if token in ('-p', '--profile'):
|
|
258
|
+
if index + 1 >= len(argv):
|
|
259
|
+
raise SessionError(f'missing value for {token}')
|
|
260
|
+
value = argv[index + 1]
|
|
261
|
+
index += 2
|
|
262
|
+
elif token.startswith('--profile='):
|
|
263
|
+
value = token.split('=', 1)[1]
|
|
264
|
+
index += 1
|
|
265
|
+
else:
|
|
266
|
+
index += 1
|
|
267
|
+
continue
|
|
268
|
+
if not value:
|
|
269
|
+
raise SessionError('invalid empty Codex profile')
|
|
270
|
+
if profile is not None and profile != value:
|
|
271
|
+
raise SessionError('multiple Codex profiles are not supported')
|
|
272
|
+
profile = value
|
|
273
|
+
return profile
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def replace_developer(argv, text):
|
|
277
|
+
result, index = [], 0
|
|
278
|
+
while index < len(argv):
|
|
279
|
+
token = argv[index]
|
|
280
|
+
if token in ('-c', '--config') and index + 1 < len(argv):
|
|
281
|
+
if argv[index + 1].split('=', 1)[0] == 'developer_instructions':
|
|
282
|
+
index += 2
|
|
283
|
+
continue
|
|
284
|
+
if token.startswith('--config=developer_instructions='):
|
|
285
|
+
index += 1
|
|
286
|
+
continue
|
|
287
|
+
result.append(token)
|
|
288
|
+
index += 1
|
|
289
|
+
return [*result, '-c', 'developer_instructions=' + json.dumps(text)]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def instruction_value(argv, host):
|
|
293
|
+
argv = list(argv[:argv.index('--')]) if '--' in argv else argv
|
|
294
|
+
if host == 'claude':
|
|
295
|
+
values = [argv[i + 1] for i, a in enumerate(argv[:-1]) if a == '--append-system-prompt']
|
|
296
|
+
return '\n\n'.join(values)
|
|
297
|
+
for i, a in enumerate(argv[:-1]):
|
|
298
|
+
if a in ('-c', '--config') and argv[i + 1].startswith('developer_instructions='):
|
|
299
|
+
return json.loads(argv[i + 1].split('=', 1)[1])
|
|
300
|
+
return ''
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _claude_plugin_paths(snapshot):
|
|
304
|
+
assets = snapshot.get('assets') or {}
|
|
305
|
+
if not isinstance(assets, dict) or set(assets) - {'claude_plugins'}:
|
|
306
|
+
raise SessionError('invalid native snapshot assets')
|
|
307
|
+
values = assets.get('claude_plugins', [])
|
|
308
|
+
if not isinstance(values, list) or not all(isinstance(value, str) for value in values):
|
|
309
|
+
raise SessionError('invalid native plugin paths')
|
|
310
|
+
if not values:
|
|
311
|
+
return []
|
|
312
|
+
root = pathlib.Path(snapshot['path']).resolve()
|
|
313
|
+
result = []
|
|
314
|
+
for value in values:
|
|
315
|
+
relative = pathlib.PurePosixPath(value)
|
|
316
|
+
if relative.is_absolute() or '..' in relative.parts or relative.as_posix() != value:
|
|
317
|
+
raise SessionError('native plugin is outside the pinned snapshot')
|
|
318
|
+
path = root / relative
|
|
319
|
+
if not path.resolve().is_relative_to(root) or path == root:
|
|
320
|
+
raise SessionError('native plugin is outside the pinned snapshot')
|
|
321
|
+
result.append(str(path))
|
|
322
|
+
if len(result) != len(set(result)):
|
|
323
|
+
raise SessionError('duplicate native plugin')
|
|
324
|
+
return result
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def validate_claude_plugins(command, snapshot, cwd, env):
|
|
328
|
+
"""Native validation is manifest-only; assert actual carriers separately."""
|
|
329
|
+
for raw in _claude_plugin_paths(snapshot):
|
|
330
|
+
root = pathlib.Path(raw)
|
|
331
|
+
agents = list((root / 'agents').glob('*.md'))
|
|
332
|
+
hooks = root / 'hooks/hooks.json'
|
|
333
|
+
if bool(agents) == hooks.is_file():
|
|
334
|
+
raise SessionError('native item plugin must have one agent or hook carrier')
|
|
335
|
+
if agents and len(agents) != 1:
|
|
336
|
+
raise SessionError('native item plugin has unexpected agents')
|
|
337
|
+
completed = subprocess.run([command, 'plugin', 'validate', '--json', str(root)],
|
|
338
|
+
cwd=cwd, env=env, input='', capture_output=True, text=True, timeout=30)
|
|
339
|
+
try:
|
|
340
|
+
result = json.loads(completed.stdout)
|
|
341
|
+
except ValueError as exc:
|
|
342
|
+
raise SessionError('Claude must support plugin validate --json for native corpus activation') from exc
|
|
343
|
+
manifest = result.get('manifest') if isinstance(result, dict) else None
|
|
344
|
+
if completed.returncode or not isinstance(result, dict) or result.get('success') is not True or not isinstance(manifest, dict) or manifest.get('errors') != []:
|
|
345
|
+
raise SessionError(f'Claude rejected native corpus plugin {root.name}: {completed.stdout[-1500:]}')
|
|
346
|
+
for warning in manifest.get('warnings', []):
|
|
347
|
+
print(f"agent-bios: native plugin warning: {warning.get('message', warning)}", file=sys.stderr)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _verified_launch_snapshot(state_root, snapshot):
|
|
351
|
+
from corpus_store import verify_snapshot
|
|
352
|
+
ref = snapshot.get('content_ref')
|
|
353
|
+
if not isinstance(ref, str) or not re.fullmatch('[0-9a-f]{64}', ref):
|
|
354
|
+
raise SessionError('invalid corpus content ref')
|
|
355
|
+
root = pathlib.Path(state_root) / 'sessions/snapshots' / ref
|
|
356
|
+
if pathlib.Path(snapshot.get('path', '')).resolve() != root.resolve():
|
|
357
|
+
raise SessionError('activation snapshot is not owned by this private store')
|
|
358
|
+
verified = verify_snapshot(root, ref)
|
|
359
|
+
return {**verified['output'], 'path': str(root), 'content_ref': ref,
|
|
360
|
+
'assets': verified['output'].get('assets', {})}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def compose_argv(command, argv, host, snapshot, cwd=None, env=None, include_global_instructions=True):
|
|
364
|
+
"""Preserve native instructions before the selected corpus and launch contract."""
|
|
365
|
+
_global_instruction_choice(host, include_global_instructions)
|
|
366
|
+
native_env = dict(os.environ if env is None else env)
|
|
367
|
+
native_cwd = pathlib.Path(cwd or pathlib.Path.cwd()).resolve()
|
|
368
|
+
exclusion = []
|
|
369
|
+
if not include_global_instructions:
|
|
370
|
+
if _settings_values(argv):
|
|
371
|
+
raise SessionError(
|
|
372
|
+
'global instruction exclusion cannot be combined with an existing --settings argument: '
|
|
373
|
+
'Claude would replace it; keep inclusion enabled to preserve those settings'
|
|
374
|
+
)
|
|
375
|
+
patterns = _claude_global_instruction_patterns(native_env, native_cwd)
|
|
376
|
+
_claude_exclusion_version(command, native_cwd, native_env)
|
|
377
|
+
exclusion = ['--settings', json.dumps({'claudeMdExcludes': patterns}, ensure_ascii=False)]
|
|
378
|
+
content = snapshot['instruction_text']
|
|
379
|
+
contract = instruction_value(argv, host)
|
|
380
|
+
if host == 'codex':
|
|
381
|
+
profile = named_profile(argv)
|
|
382
|
+
if profile is not None:
|
|
383
|
+
raise SessionError(
|
|
384
|
+
f'named Codex profile {profile!r} cannot be used with private corpus activation: '
|
|
385
|
+
'the native app-server config/read route does not accept --profile, so its '
|
|
386
|
+
'effective developer instructions cannot be read without rebuilding native '
|
|
387
|
+
'profile semantics'
|
|
388
|
+
)
|
|
389
|
+
with CodexServer(command, config_flags(argv, exclude_developer=True), cwd, env) as server:
|
|
390
|
+
config = server.call('config/read', {'cwd': str(cwd or pathlib.Path.cwd()), 'includeLayers': False})
|
|
391
|
+
native = config.get('config', {}).get('developer_instructions') or ''
|
|
392
|
+
if not isinstance(native, str):
|
|
393
|
+
raise SessionError('effective developer_instructions is not text')
|
|
394
|
+
return replace_developer(argv, '\n\n'.join(x for x in (native, content, contract) if x))
|
|
395
|
+
boundary = argv.index('--') if '--' in argv else len(argv)
|
|
396
|
+
options, tail = argv[:boundary], argv[boundary:]
|
|
397
|
+
result, index = [], 0
|
|
398
|
+
while index < len(options):
|
|
399
|
+
if options[index] == '--append-system-prompt':
|
|
400
|
+
index += 2
|
|
401
|
+
else:
|
|
402
|
+
result.append(options[index])
|
|
403
|
+
index += 1
|
|
404
|
+
for path in _claude_plugin_paths(snapshot):
|
|
405
|
+
result += ['--plugin-dir', path]
|
|
406
|
+
return [*result, *exclusion, '--append-system-prompt', '\n\n'.join(x for x in (content, contract) if x), *tail]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def session_paths(state_root):
|
|
410
|
+
root = pathlib.Path(state_root)
|
|
411
|
+
for relative in ('', 'runtime', 'runtime/activations', 'sessions', 'sessions/pins',
|
|
412
|
+
'sessions/pins/codex', 'sessions/pins/claude', 'sessions/snapshots'):
|
|
413
|
+
if (root / relative).is_symlink():
|
|
414
|
+
raise SessionError(f'refusing symlink state directory: {root / relative}')
|
|
415
|
+
return root / 'runtime' / 'activations', root / 'sessions' / 'pins'
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def validate_session_id(value):
|
|
419
|
+
if not isinstance(value, str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_-]{3,127}', value):
|
|
420
|
+
raise SessionError('invalid host session id')
|
|
421
|
+
return value
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def native_home(host, env, cwd=None):
|
|
425
|
+
key, default = ('CODEX_HOME', '.codex') if host == 'codex' else ('CLAUDE_CONFIG_DIR', '.claude')
|
|
426
|
+
base = pathlib.Path(cwd or pathlib.Path.cwd())
|
|
427
|
+
configured = env.get(key)
|
|
428
|
+
if configured:
|
|
429
|
+
path = pathlib.Path(configured)
|
|
430
|
+
else:
|
|
431
|
+
home = pathlib.Path(env.get('HOME', str(pathlib.Path.home())))
|
|
432
|
+
path = home / default
|
|
433
|
+
if not path.is_absolute():
|
|
434
|
+
path = base / path
|
|
435
|
+
return path.resolve()
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def environment_provenance(host, env):
|
|
439
|
+
"""Capture only the native-home representation, never credential contents."""
|
|
440
|
+
if host not in ('codex', 'claude'):
|
|
441
|
+
raise SessionError('unknown host')
|
|
442
|
+
key = 'CODEX_HOME' if host == 'codex' else 'CLAUDE_CONFIG_DIR'
|
|
443
|
+
value = env.get(key)
|
|
444
|
+
if key in env and not isinstance(value, str):
|
|
445
|
+
raise SessionError(f'invalid {key} environment value')
|
|
446
|
+
variables = {key: {'state': 'set', 'value': value} if key in env else {'state': 'unset'}}
|
|
447
|
+
# HOME affects a native default when the host variable is absent or empty.
|
|
448
|
+
if value in (None, ''):
|
|
449
|
+
home = env.get('HOME')
|
|
450
|
+
if 'HOME' in env and not isinstance(home, str):
|
|
451
|
+
raise SessionError('invalid HOME environment value')
|
|
452
|
+
variables['HOME'] = {'state': 'set', 'value': home} if 'HOME' in env else {'state': 'unset'}
|
|
453
|
+
return {'schema_version': 1, 'variables': variables}
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def restore_environment(record, env):
|
|
457
|
+
"""Restore an exact captured set/unset native-home representation."""
|
|
458
|
+
host = record.get('host')
|
|
459
|
+
key = 'CODEX_HOME' if host == 'codex' else 'CLAUDE_CONFIG_DIR' if host == 'claude' else None
|
|
460
|
+
provenance = record.get('environment')
|
|
461
|
+
if key is None or not isinstance(provenance, dict) or set(provenance) != {'schema_version', 'variables'} or type(provenance.get('schema_version')) is not int or provenance['schema_version'] != 1:
|
|
462
|
+
raise SessionError('pin lacks environment provenance; start a new activated session or run an explicit future migration')
|
|
463
|
+
variables = provenance.get('variables')
|
|
464
|
+
if not isinstance(variables, dict) or key not in variables:
|
|
465
|
+
raise SessionError('pin has invalid environment provenance; start a new activated session or run an explicit future migration')
|
|
466
|
+
host_entry = variables[key]
|
|
467
|
+
if not isinstance(host_entry, dict):
|
|
468
|
+
raise SessionError('pin has invalid native-home environment entry')
|
|
469
|
+
needs_home = host_entry.get('state') == 'unset' or host_entry.get('value') == ''
|
|
470
|
+
if set(variables) != ({key, 'HOME'} if needs_home else {key}):
|
|
471
|
+
raise SessionError('pin has incomplete or unknown native-home environment entries')
|
|
472
|
+
restored = dict(env)
|
|
473
|
+
for name in (key, 'HOME'):
|
|
474
|
+
if name not in variables:
|
|
475
|
+
continue
|
|
476
|
+
entry = variables[name]
|
|
477
|
+
if not isinstance(entry, dict) or set(entry) - {'state', 'value'} or entry.get('state') not in {'set', 'unset'}:
|
|
478
|
+
raise SessionError('pin has invalid environment provenance; start a new activated session or run an explicit future migration')
|
|
479
|
+
if entry['state'] == 'unset':
|
|
480
|
+
if 'value' in entry:
|
|
481
|
+
raise SessionError('pin has invalid environment provenance; start a new activated session or run an explicit future migration')
|
|
482
|
+
restored.pop(name, None)
|
|
483
|
+
elif not isinstance(entry.get('value'), str):
|
|
484
|
+
raise SessionError('pin has invalid environment provenance; start a new activated session or run an explicit future migration')
|
|
485
|
+
else:
|
|
486
|
+
restored[name] = entry['value']
|
|
487
|
+
return restored
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def prepare(state_root, host, snapshot, argv, cwd=None, env=None, include_global_instructions=True):
|
|
491
|
+
if host not in ('codex', 'claude'):
|
|
492
|
+
raise SessionError('unknown host')
|
|
493
|
+
_global_instruction_choice(host, include_global_instructions)
|
|
494
|
+
intents, _ = session_paths(state_root)
|
|
495
|
+
cwd = pathlib.Path(cwd or pathlib.Path.cwd()).resolve()
|
|
496
|
+
intent_id = uuid.uuid4().hex
|
|
497
|
+
record = {'schema_version': 1, 'intent_id': intent_id, 'state': 'PREPARED',
|
|
498
|
+
'host': host, 'content_ref': snapshot['content_ref'], 'snapshot_path': str(snapshot['path']),
|
|
499
|
+
'config_home': str(native_home(host, os.environ if env is None else env, cwd)),
|
|
500
|
+
'environment': environment_provenance(host, os.environ if env is None else env),
|
|
501
|
+
'include_global_instructions': include_global_instructions,
|
|
502
|
+
'argv': list(argv), 'cwd': str(cwd or pathlib.Path.cwd()), 'created': time.time()}
|
|
503
|
+
_record_instruction_choice(record, dict(os.environ if env is None else env))
|
|
504
|
+
atomic_json(intents / intent_id / 'journal.json', record)
|
|
505
|
+
return record
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def recorded_cwd(record):
|
|
509
|
+
value = record.get('cwd')
|
|
510
|
+
if not isinstance(value, str) or not pathlib.Path(value).is_absolute():
|
|
511
|
+
raise SessionError('session pin lacks an absolute working directory; start a new activated session')
|
|
512
|
+
try:
|
|
513
|
+
canonical = str(pathlib.Path(value).resolve())
|
|
514
|
+
except (OSError, RuntimeError, ValueError) as exc:
|
|
515
|
+
raise SessionError('session working directory cannot be resolved') from exc
|
|
516
|
+
if canonical != value:
|
|
517
|
+
raise SessionError('session working directory is no longer canonical; start a new activated session')
|
|
518
|
+
return value
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def observe_and_pin(state_root, record, host_id, evidence):
|
|
522
|
+
if not isinstance(record.get('intent_id'), str) or not re.fullmatch('[0-9a-f]{32}', record['intent_id']):
|
|
523
|
+
raise SessionError('invalid activation journal identity')
|
|
524
|
+
recorded_cwd(record)
|
|
525
|
+
native_env = restore_environment(record, {})
|
|
526
|
+
_record_instruction_choice(record, native_env, check_paths=False)
|
|
527
|
+
host_id = validate_session_id(host_id)
|
|
528
|
+
intents, pins = session_paths(state_root)
|
|
529
|
+
pin_path = pins / record['host'] / f'{host_id}.json'
|
|
530
|
+
if pin_path.exists():
|
|
531
|
+
existing = read_pin(state_root, record['host'], host_id)
|
|
532
|
+
if existing['intent_id'] != record['intent_id'] or existing['content_ref'] != record['content_ref']:
|
|
533
|
+
raise SessionError('host session already belongs to a different corpus activation')
|
|
534
|
+
record.update(state='HOST_OBSERVED', session_id=host_id, evidence=evidence)
|
|
535
|
+
atomic_json(intents / record['intent_id'] / 'journal.json', record)
|
|
536
|
+
atomic_json(pin_path, record)
|
|
537
|
+
record['state'] = 'PINNED'
|
|
538
|
+
atomic_json(intents / record['intent_id'] / 'journal.json', record)
|
|
539
|
+
return record
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def read_pin(state_root, host, session_id):
|
|
543
|
+
if host not in ('codex', 'claude'):
|
|
544
|
+
raise SessionError('unknown host')
|
|
545
|
+
session_id = validate_session_id(session_id)
|
|
546
|
+
_, pins = session_paths(state_root)
|
|
547
|
+
path = pins / host / f'{session_id}.json'
|
|
548
|
+
if path.is_symlink() or not path.is_file():
|
|
549
|
+
raise SessionError(f'no corpus pin for {host} session {session_id}')
|
|
550
|
+
record = json.loads(path.read_text())
|
|
551
|
+
if record.get('host') != host or record.get('session_id') != session_id:
|
|
552
|
+
raise SessionError('session pin identity mismatch')
|
|
553
|
+
recorded_cwd(record)
|
|
554
|
+
native_env = restore_environment(record, {})
|
|
555
|
+
_record_instruction_choice(record, native_env, check_paths=False)
|
|
556
|
+
snapshot = pathlib.Path(record['snapshot_path'])
|
|
557
|
+
allowed = (pathlib.Path(state_root) / 'sessions' / 'snapshots').resolve()
|
|
558
|
+
if not snapshot.resolve().is_relative_to(allowed) or snapshot.is_symlink():
|
|
559
|
+
raise SessionError('session pin names an unowned snapshot')
|
|
560
|
+
if not snapshot.is_dir():
|
|
561
|
+
raise SessionError('pinned corpus snapshot missing; recover it before resuming')
|
|
562
|
+
from corpus_store import verify_snapshot
|
|
563
|
+
verify_snapshot(snapshot, record['content_ref'])
|
|
564
|
+
return record
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def claude_evidence(record, env):
|
|
568
|
+
requested = record.get('requested_session_id')
|
|
569
|
+
if not requested:
|
|
570
|
+
return None
|
|
571
|
+
validate_session_id(requested)
|
|
572
|
+
home = native_home('claude', restore_environment(record, env), recorded_cwd(record))
|
|
573
|
+
from corpus_store import _reject_symlink_path
|
|
574
|
+
_reject_symlink_path(home / 'projects')
|
|
575
|
+
for trace in (home / 'projects').glob(f'*/{requested}.jsonl'):
|
|
576
|
+
_reject_symlink_path(trace.parent)
|
|
577
|
+
_reject_symlink_path(trace)
|
|
578
|
+
with trace.open(encoding='utf-8') as source:
|
|
579
|
+
for line in source:
|
|
580
|
+
try:
|
|
581
|
+
event = json.loads(line)
|
|
582
|
+
except ValueError:
|
|
583
|
+
continue
|
|
584
|
+
if isinstance(event, dict) and event.get('sessionId') == requested:
|
|
585
|
+
if event.get('cwd') and pathlib.Path(event['cwd']).resolve() != pathlib.Path(record['cwd']).resolve():
|
|
586
|
+
continue
|
|
587
|
+
return {'method': 'native-session-log', 'path': str(trace)}
|
|
588
|
+
return None
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def recover_activations(state_root, command=None, host=None, env=None):
|
|
592
|
+
"""Reconcile exact recorded ids against host evidence; retain unresolved intents."""
|
|
593
|
+
env = dict(os.environ if env is None else env)
|
|
594
|
+
intents, _ = session_paths(state_root)
|
|
595
|
+
result = {'pinned': [], 'pending': []}
|
|
596
|
+
for path in sorted(intents.glob('*/journal.json')):
|
|
597
|
+
if path.is_symlink() or path.parent.is_symlink():
|
|
598
|
+
raise SessionError('symlink activation journal')
|
|
599
|
+
record = json.loads(path.read_text())
|
|
600
|
+
if not isinstance(record, dict) or record.get('intent_id') != path.parent.name or not re.fullmatch('[0-9a-f]{32}', path.parent.name):
|
|
601
|
+
raise SessionError('activation journal identity does not match its owned directory')
|
|
602
|
+
if record.get('state') in ('PREPARED', 'HOST_OBSERVED'):
|
|
603
|
+
try:
|
|
604
|
+
recorded_cwd(record)
|
|
605
|
+
native_env = restore_environment(record, env)
|
|
606
|
+
_record_instruction_choice(record, native_env, check_paths=False)
|
|
607
|
+
except SessionError as exc:
|
|
608
|
+
print(f"agent-bios: activation {record.get('intent_id', path.parent.name)} remains pending: {exc}", file=sys.stderr)
|
|
609
|
+
result['pending'].append(record.get('intent_id', path.parent.name))
|
|
610
|
+
continue
|
|
611
|
+
if record.get('state') == 'HOST_OBSERVED':
|
|
612
|
+
observe_and_pin(state_root, record, record['session_id'], record['evidence'])
|
|
613
|
+
result['pinned'].append(record['session_id'])
|
|
614
|
+
elif record.get('state') == 'PREPARED':
|
|
615
|
+
evidence = None
|
|
616
|
+
requested = record.get('requested_session_id')
|
|
617
|
+
if requested and record['host'] == 'claude':
|
|
618
|
+
try:
|
|
619
|
+
evidence = claude_evidence(record, env)
|
|
620
|
+
except SessionError as exc:
|
|
621
|
+
print(f"agent-bios: activation {record.get('intent_id', '?')} remains pending: {exc}", file=sys.stderr)
|
|
622
|
+
elif requested and record['host'] == 'codex' and host == 'codex' and command:
|
|
623
|
+
validate_session_id(requested)
|
|
624
|
+
try:
|
|
625
|
+
native_env = restore_environment(record, env)
|
|
626
|
+
with CodexServer(command, config_flags(record['argv']), record['cwd'], native_env) as server:
|
|
627
|
+
observed = server.call('thread/read', {'threadId': requested, 'includeTurns': False})
|
|
628
|
+
if observed.get('thread', {}).get('id') == requested:
|
|
629
|
+
evidence = {'method': 'recovered-thread/read'}
|
|
630
|
+
except SessionError as exc:
|
|
631
|
+
print(f"agent-bios: activation {record['intent_id']} remains pending: {exc}", file=sys.stderr)
|
|
632
|
+
if evidence:
|
|
633
|
+
observe_and_pin(state_root, record, requested, evidence)
|
|
634
|
+
result['pinned'].append(requested)
|
|
635
|
+
else:
|
|
636
|
+
result['pending'].append(record['intent_id'])
|
|
637
|
+
return result
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def create_codex_session(command, argv, state_root, record, cwd, env):
|
|
641
|
+
with CodexServer(command, config_flags(argv), cwd, env) as server:
|
|
642
|
+
params = {'cwd': str(cwd), 'developerInstructions': instruction_value(argv, 'codex'),
|
|
643
|
+
'ephemeral': False, 'experimentalRawEvents': False}
|
|
644
|
+
for index, token in enumerate(argv[:-1]):
|
|
645
|
+
if token in ('--model', '-m'):
|
|
646
|
+
params['model'] = argv[index + 1]
|
|
647
|
+
result = server.call('thread/start', params)
|
|
648
|
+
thread = result['thread']
|
|
649
|
+
host_id = validate_session_id(thread['id'])
|
|
650
|
+
# thread/start alone may return an id before persisting a resumable thread.
|
|
651
|
+
# Retain the exact id in PREPARED before recording a small developer item;
|
|
652
|
+
# inject_items persists history without generating a model turn.
|
|
653
|
+
record['requested_session_id'] = host_id
|
|
654
|
+
intents, _ = session_paths(state_root)
|
|
655
|
+
atomic_json(intents / record['intent_id'] / 'journal.json', record)
|
|
656
|
+
server.call('thread/inject_items', {'threadId': host_id, 'items': [
|
|
657
|
+
{'type': 'message', 'role': 'developer', 'content': [
|
|
658
|
+
{'type': 'input_text', 'text': f"agent-bios session snapshot: {record['content_ref']}"}]}]})
|
|
659
|
+
with CodexServer(command, config_flags(argv), cwd, env) as server:
|
|
660
|
+
persisted = server.call('thread/read', {'threadId': host_id, 'includeTurns': False})
|
|
661
|
+
if persisted.get('thread', {}).get('id') != host_id:
|
|
662
|
+
raise SessionError('host did not persist the requested corpus session')
|
|
663
|
+
observe_and_pin(state_root, record, host_id,
|
|
664
|
+
{'method': 'thread/start+inject_items+read', 'thread_path': thread.get('path')})
|
|
665
|
+
return host_id
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def launch(command, argv, state_root, host, snapshot, cwd=None, env=None, resume_id=None,
|
|
669
|
+
include_global_instructions=True):
|
|
670
|
+
"""Start a pinned native session, returning its exit status."""
|
|
671
|
+
cwd = pathlib.Path(cwd or pathlib.Path.cwd()).resolve()
|
|
672
|
+
env = dict(os.environ if env is None else env)
|
|
673
|
+
if resume_id and include_global_instructions is not True:
|
|
674
|
+
raise SessionError('resume uses its recorded global instruction choice; omit the override or start a new session')
|
|
675
|
+
if not resume_id:
|
|
676
|
+
_global_instruction_choice(host, include_global_instructions)
|
|
677
|
+
recovered = recover_activations(state_root, command, host, env)
|
|
678
|
+
if recovered['pending']:
|
|
679
|
+
print(f"agent-bios: {len(recovered['pending'])} activation(s) lack host evidence; their snapshots remain retained.", file=sys.stderr)
|
|
680
|
+
if resume_id:
|
|
681
|
+
record = read_pin(state_root, host, resume_id)
|
|
682
|
+
env = restore_environment(record, env)
|
|
683
|
+
if not _record_instruction_choice(record, env):
|
|
684
|
+
_claude_exclusion_version(command, record['cwd'], env)
|
|
685
|
+
if host == 'claude':
|
|
686
|
+
pinned = _verified_launch_snapshot(state_root, {'path': record['snapshot_path'], 'content_ref': record['content_ref']})
|
|
687
|
+
validate_claude_plugins(command, pinned, record['cwd'], env)
|
|
688
|
+
argv = record['argv']
|
|
689
|
+
native = ['resume', resume_id, *argv] if host == 'codex' else ['--resume', resume_id, *argv]
|
|
690
|
+
return subprocess.call([command, *native], cwd=record['cwd'], env=env)
|
|
691
|
+
snapshot = _verified_launch_snapshot(state_root, snapshot)
|
|
692
|
+
argv = compose_argv(command, argv, host, snapshot, cwd, env, include_global_instructions)
|
|
693
|
+
if host == 'claude':
|
|
694
|
+
validate_claude_plugins(command, snapshot, cwd, env)
|
|
695
|
+
record = prepare(state_root, host, snapshot, argv, cwd, env, include_global_instructions)
|
|
696
|
+
if host == 'codex':
|
|
697
|
+
# Create the durable thread before opening the native resume UI. The returned
|
|
698
|
+
# id comes from the real host and is pinned before any user turn is possible.
|
|
699
|
+
create_codex_session(command, argv, state_root, record, cwd, env)
|
|
700
|
+
native = ['resume', record['session_id'], *argv]
|
|
701
|
+
return subprocess.call([command, *native], cwd=cwd, env=env)
|
|
702
|
+
requested_id = str(uuid.uuid4())
|
|
703
|
+
record['requested_session_id'] = requested_id
|
|
704
|
+
intents, _ = session_paths(state_root)
|
|
705
|
+
atomic_json(intents / record['intent_id'] / 'journal.json', record)
|
|
706
|
+
native = ['--session-id', requested_id, *argv]
|
|
707
|
+
child = subprocess.Popen([command, *native], cwd=cwd, env=env)
|
|
708
|
+
try:
|
|
709
|
+
while True:
|
|
710
|
+
if record['state'] == 'PREPARED':
|
|
711
|
+
evidence = claude_evidence(record, env)
|
|
712
|
+
if evidence:
|
|
713
|
+
observe_and_pin(state_root, record, requested_id, evidence)
|
|
714
|
+
if child.poll() is not None:
|
|
715
|
+
if record['state'] != 'PINNED':
|
|
716
|
+
print(f"agent-bios: no host session evidence; activation {record['intent_id']} retained for recovery.", file=sys.stderr)
|
|
717
|
+
return child.returncode
|
|
718
|
+
time.sleep(0.15)
|
|
719
|
+
finally:
|
|
720
|
+
if child.poll() is None:
|
|
721
|
+
child.terminate()
|
|
722
|
+
try:
|
|
723
|
+
child.wait(timeout=5)
|
|
724
|
+
except subprocess.TimeoutExpired:
|
|
725
|
+
child.kill()
|
|
726
|
+
child.wait(timeout=5)
|