optio-opencode 0.2.0__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/PKG-INFO +1 -1
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/pyproject.toml +1 -1
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/src/optio_opencode/session.py +66 -63
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/src/optio_opencode.egg-info/PKG-INFO +1 -1
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_host_local.py +21 -3
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_host_primitives_remote.py +16 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/README.md +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/setup.cfg +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/src/optio_opencode/__init__.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/src/optio_opencode/host_actions.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/src/optio_opencode/prompt.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/src/optio_opencode/seed_manifest.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/src/optio_opencode/snapshots.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/src/optio_opencode/types.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/src/optio_opencode.egg-info/SOURCES.txt +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/src/optio_opencode.egg-info/dependency_links.txt +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/src/optio_opencode.egg-info/requires.txt +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/src/optio_opencode.egg-info/top_level.txt +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_agent_sender_opencode.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_host_actions.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_host_primitives_local.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_host_remote_resume.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_host_resume.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_prompt.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_purge_seed.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_resume_sentence_opencode.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_sanity.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_seed_config.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_session_blob_hooks.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_session_hooks.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_session_local.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_session_remote.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_session_resume.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_session_seed.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_smart_install.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_snapshots.py +0 -0
- {optio_opencode-0.2.0 → optio_opencode-0.2.1}/tests/test_types.py +0 -0
|
@@ -96,74 +96,76 @@ async def run_opencode_session(ctx: ProcessContext, config: OpencodeTaskConfig)
|
|
|
96
96
|
# server (for the seed model default) before terminating it.
|
|
97
97
|
worker_port: int | None = None
|
|
98
98
|
|
|
99
|
-
#
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if resume_requested:
|
|
103
|
-
snapshot = await load_latest_snapshot(
|
|
104
|
-
ctx._db, prefix=ctx._prefix, process_id=ctx.process_id,
|
|
105
|
-
)
|
|
99
|
+
# Set by _prepare (the driver runs it after the workdir wipe, before the
|
|
100
|
+
# optio.log tail); read by the body and the teardown finally.
|
|
101
|
+
resuming = False
|
|
106
102
|
|
|
107
|
-
# Connect + install BEFORE deciding fresh vs resume. The resume path
|
|
108
|
-
# needs ``opencode import`` to replay the saved session DB, which
|
|
109
|
-
# requires opencode to be installed on the host and resolved to an
|
|
110
|
-
# absolute path. Hoisting also lets the fresh path skip the redundant
|
|
111
|
-
# ``host.connect()`` later. ``setup_workdir`` is idempotent (mkdir -p)
|
|
112
|
-
# and the protocol driver still calls it again for the fresh path —
|
|
113
|
-
# harmless. Install progress reports through ``ctx``, so the
|
|
114
|
-
# dashboard sees activity from the very first step.
|
|
115
103
|
await host.connect()
|
|
116
|
-
await host.setup_workdir()
|
|
117
|
-
opencode_exec = await host_actions.ensure_opencode_installed(
|
|
118
|
-
HookContext(ctx, host),
|
|
119
|
-
install_if_missing=config.install_if_missing,
|
|
120
|
-
install_dir=config.opencode_install_dir,
|
|
121
|
-
)
|
|
122
104
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
# by appending it to optio.log.old.
|
|
146
|
-
await _rotate_optio_log(host)
|
|
147
|
-
preserved_session_id = snapshot["sessionId"]
|
|
148
|
-
except Exception as resume_exc:
|
|
149
|
-
# If the failure was the session-blob decrypt hook raising,
|
|
150
|
-
# this indicates the snapshot was tampered with or the
|
|
151
|
-
# consumer's keypair changed. Fail loud — silently dropping
|
|
152
|
-
# to fresh-start would mask the security-relevant signal.
|
|
153
|
-
if "decrypt" in repr(resume_exc).lower() and "blob" in repr(resume_exc).lower():
|
|
154
|
-
_LOG.error(
|
|
155
|
-
"resume restore failed inside session_blob_decrypt; "
|
|
156
|
-
"refusing to fall through to fresh-start. Operator must "
|
|
157
|
-
"investigate the snapshot blob.",
|
|
158
|
-
)
|
|
159
|
-
raise
|
|
160
|
-
_LOG.exception(
|
|
161
|
-
"resume restore failed; falling back to fresh-start path "
|
|
162
|
-
"(Mongo blob preserved for inspection)",
|
|
105
|
+
async def _prepare(host: Host, hook_ctx: HookContext) -> None:
|
|
106
|
+
"""Install opencode and restore a resume snapshot.
|
|
107
|
+
|
|
108
|
+
Handed to run_log_protocol_session, which runs it AFTER
|
|
109
|
+
host.setup_workdir() wiped the workdir and BEFORE it subscribes the
|
|
110
|
+
optio.log tail. The resume path needs ``opencode import`` to replay
|
|
111
|
+
the saved session DB (so opencode must be installed + resolved to an
|
|
112
|
+
absolute path first), and the restored optio.log is rotated away below
|
|
113
|
+
before the tail can re-emit its stale DELIVERABLE/DONE/ERROR lines.
|
|
114
|
+
"""
|
|
115
|
+
nonlocal opencode_exec, resuming, preserved_session_id
|
|
116
|
+
opencode_exec = await host_actions.ensure_opencode_installed(
|
|
117
|
+
hook_ctx,
|
|
118
|
+
install_if_missing=config.install_if_missing,
|
|
119
|
+
install_dir=config.opencode_install_dir,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
resume_requested = bool(getattr(ctx, "resume", False))
|
|
123
|
+
snapshot: dict | None = None
|
|
124
|
+
if resume_requested:
|
|
125
|
+
snapshot = await load_latest_snapshot(
|
|
126
|
+
ctx._db, prefix=ctx._prefix, process_id=ctx.process_id,
|
|
163
127
|
)
|
|
128
|
+
|
|
129
|
+
resuming = snapshot is not None
|
|
130
|
+
if resuming:
|
|
164
131
|
await host.remove_file(opencode_db)
|
|
165
|
-
|
|
166
|
-
|
|
132
|
+
try:
|
|
133
|
+
await host.restore_workdir(_stream_blob(ctx, snapshot["workdirBlobId"]))
|
|
134
|
+
session_bytes_raw = await _read_blob_bytes(ctx, snapshot["sessionBlobId"])
|
|
135
|
+
decrypt = config.session_blob_decrypt or (lambda b: b)
|
|
136
|
+
session_bytes = decrypt(session_bytes_raw)
|
|
137
|
+
await host_actions.opencode_import(
|
|
138
|
+
host, opencode_db, session_bytes,
|
|
139
|
+
opencode_executable=opencode_exec,
|
|
140
|
+
)
|
|
141
|
+
# Move the restored log channel out of the way BEFORE the
|
|
142
|
+
# protocol driver subscribes its tail. The snapshot tar
|
|
143
|
+
# includes optio.log from the previous run; without rotation,
|
|
144
|
+
# ``tail -F -n +1`` would re-emit every old DELIVERABLE /
|
|
145
|
+
# DONE / ERROR line and the resumed process would terminate
|
|
146
|
+
# within seconds of launch. Preserve the historical content
|
|
147
|
+
# by appending it to optio.log.old.
|
|
148
|
+
await _rotate_optio_log(host)
|
|
149
|
+
preserved_session_id = snapshot["sessionId"]
|
|
150
|
+
except Exception as resume_exc:
|
|
151
|
+
# If the failure was the session-blob decrypt hook raising,
|
|
152
|
+
# this indicates the snapshot was tampered with or the
|
|
153
|
+
# consumer's keypair changed. Fail loud — silently dropping
|
|
154
|
+
# to fresh-start would mask the security-relevant signal.
|
|
155
|
+
if "decrypt" in repr(resume_exc).lower() and "blob" in repr(resume_exc).lower():
|
|
156
|
+
_LOG.error(
|
|
157
|
+
"resume restore failed inside session_blob_decrypt; "
|
|
158
|
+
"refusing to fall through to fresh-start. Operator must "
|
|
159
|
+
"investigate the snapshot blob.",
|
|
160
|
+
)
|
|
161
|
+
raise
|
|
162
|
+
_LOG.exception(
|
|
163
|
+
"resume restore failed; falling back to fresh-start path "
|
|
164
|
+
"(Mongo blob preserved for inspection)",
|
|
165
|
+
)
|
|
166
|
+
await host.remove_file(opencode_db)
|
|
167
|
+
resuming = False
|
|
168
|
+
preserved_session_id = None
|
|
167
169
|
|
|
168
170
|
async def _opencode_body(host: Host, hook_ctx: HookContext) -> None:
|
|
169
171
|
"""Opencode-specific body that runs inside the protocol driver.
|
|
@@ -368,6 +370,7 @@ async def run_opencode_session(ctx: ProcessContext, config: OpencodeTaskConfig)
|
|
|
368
370
|
await run_log_protocol_session(
|
|
369
371
|
host, ctx,
|
|
370
372
|
body=_opencode_body,
|
|
373
|
+
prepare=_prepare,
|
|
371
374
|
on_deliverable=config.on_deliverable,
|
|
372
375
|
after_execute=config.after_execute,
|
|
373
376
|
protocol=protocol,
|
|
@@ -22,9 +22,27 @@ def local_host(tmp_workdir):
|
|
|
22
22
|
async def test_setup_workdir_creates_workdir(local_host):
|
|
23
23
|
await local_host.setup_workdir()
|
|
24
24
|
assert os.path.isdir(local_host.workdir)
|
|
25
|
-
#
|
|
26
|
-
# The protocol-specific deliverables/ + optio.log are owned by
|
|
27
|
-
# protocol session driver in optio_agents.protocol.session.
|
|
25
|
+
# setup_workdir destructively (re)creates the workdir (see the clean-start
|
|
26
|
+
# test below). The protocol-specific deliverables/ + optio.log are owned by
|
|
27
|
+
# the protocol session driver in optio_agents.protocol.session.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def test_setup_workdir_wipes_stale_workdir_but_keeps_taskdir(local_host):
|
|
31
|
+
# Clean-start invariant: a destructive wipe of the workdir on every setup, so
|
|
32
|
+
# leftovers from a prior run (e.g. a force-cancel that skipped teardown) never
|
|
33
|
+
# leak into a fresh run. taskdir-side state (opencode.db / .env) lives OUTSIDE
|
|
34
|
+
# the workdir and must survive.
|
|
35
|
+
await local_host.setup_workdir()
|
|
36
|
+
with open(os.path.join(local_host.workdir, "stale.txt"), "w") as fh:
|
|
37
|
+
fh.write("leftover from a previous run")
|
|
38
|
+
taskdir_db = os.path.join(local_host.taskdir, "opencode.db")
|
|
39
|
+
with open(taskdir_db, "w") as fh:
|
|
40
|
+
fh.write("session transcript")
|
|
41
|
+
|
|
42
|
+
await local_host.setup_workdir()
|
|
43
|
+
|
|
44
|
+
assert not os.path.exists(os.path.join(local_host.workdir, "stale.txt"))
|
|
45
|
+
assert os.path.exists(taskdir_db)
|
|
28
46
|
|
|
29
47
|
|
|
30
48
|
@pytest.mark.asyncio
|
|
@@ -240,3 +240,19 @@ async def test_remote_setup_workdir_sets_taskdir_and_workdir_mode_0o700(remote_h
|
|
|
240
240
|
res_w = await remote_host._conn.run(f"stat -c %a {qw}", check=True)
|
|
241
241
|
assert res_t.stdout.strip() == "700", f"taskdir mode is {res_t.stdout!r}"
|
|
242
242
|
assert res_w.stdout.strip() == "700", f"workdir mode is {res_w.stdout!r}"
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@pytest.mark.asyncio
|
|
246
|
+
async def test_remote_setup_workdir_wipes_stale_workdir_but_keeps_taskdir(remote_host: RemoteHost):
|
|
247
|
+
"""Clean-start invariant asserted over SSH (mirrors the local test)."""
|
|
248
|
+
await remote_host.setup_workdir()
|
|
249
|
+
qt = shlex.quote(remote_host.taskdir)
|
|
250
|
+
qw = shlex.quote(remote_host.workdir)
|
|
251
|
+
await remote_host._conn.run(
|
|
252
|
+
f"touch {qw}/stale.txt {qt}/opencode.db", check=True,
|
|
253
|
+
)
|
|
254
|
+
await remote_host.setup_workdir()
|
|
255
|
+
stale = await remote_host._conn.run(f"test -e {qw}/stale.txt; echo $?")
|
|
256
|
+
keep = await remote_host._conn.run(f"test -e {qt}/opencode.db; echo $?")
|
|
257
|
+
assert stale.stdout.strip() == "1", "workdir was not wiped"
|
|
258
|
+
assert keep.stdout.strip() == "0", "taskdir state was lost"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optio_opencode-0.2.0 → optio_opencode-0.2.1}/src/optio_opencode.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|