optio-opencode 0.1.9__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.1.9 → optio_opencode-0.2.1}/PKG-INFO +2 -2
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/pyproject.toml +2 -2
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/src/optio_opencode/prompt.py +3 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/src/optio_opencode/session.py +83 -63
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/src/optio_opencode.egg-info/PKG-INFO +2 -2
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/src/optio_opencode.egg-info/SOURCES.txt +2 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/src/optio_opencode.egg-info/requires.txt +1 -1
- optio_opencode-0.2.1/tests/test_agent_sender_opencode.py +29 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_host_local.py +21 -3
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_host_primitives_remote.py +16 -0
- optio_opencode-0.2.1/tests/test_resume_sentence_opencode.py +7 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_session_seed.py +21 -10
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/README.md +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/setup.cfg +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/src/optio_opencode/__init__.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/src/optio_opencode/host_actions.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/src/optio_opencode/seed_manifest.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/src/optio_opencode/snapshots.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/src/optio_opencode/types.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/src/optio_opencode.egg-info/dependency_links.txt +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/src/optio_opencode.egg-info/top_level.txt +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_host_actions.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_host_primitives_local.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_host_remote_resume.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_host_resume.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_prompt.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_purge_seed.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_sanity.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_seed_config.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_session_blob_hooks.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_session_hooks.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_session_local.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_session_remote.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_session_resume.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_smart_install.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_snapshots.py +0 -0
- {optio_opencode-0.1.9 → optio_opencode-0.2.1}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optio-opencode
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Run opencode web as an optio task; local subprocess or remote via SSH.
|
|
5
5
|
Author-email: Kristof Csillag <kristof.csillag@deai-labs.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -22,7 +22,7 @@ Requires-Python: >=3.11
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
Requires-Dist: optio-core<0.3,>=0.2
|
|
24
24
|
Requires-Dist: optio-host<0.3,>=0.2
|
|
25
|
-
Requires-Dist: optio-agents<0.
|
|
25
|
+
Requires-Dist: optio-agents<0.3,>=0.2
|
|
26
26
|
Requires-Dist: asyncssh>=2.14
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "optio-opencode"
|
|
7
|
-
version = "0.1
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Run opencode web as an optio task; local subprocess or remote via SSH."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -30,7 +30,7 @@ dependencies = [
|
|
|
30
30
|
# 0.1.1 introduces the bind_addr kwarg on Host.establish_tunnel —
|
|
31
31
|
# required for OPTIO_WIDGET_TUNNEL_BIND to work.
|
|
32
32
|
"optio-host>=0.2,<0.3",
|
|
33
|
-
"optio-agents>=0.
|
|
33
|
+
"optio-agents>=0.2,<0.3",
|
|
34
34
|
"asyncssh>=2.14",
|
|
35
35
|
]
|
|
36
36
|
|
|
@@ -81,6 +81,9 @@ the situation as a resume:
|
|
|
81
81
|
|
|
82
82
|
If a resume slips past unnoticed, a failing tool call is the
|
|
83
83
|
next-best signal — re-check `./resume.log` then.
|
|
84
|
+
|
|
85
|
+
You may also be notified of a resume by a `System:` message on your input
|
|
86
|
+
channel; when you see one, follow the `resume.log` procedure above.
|
|
84
87
|
"""
|
|
85
88
|
|
|
86
89
|
|
|
@@ -31,6 +31,7 @@ from optio_core.context import ProcessContext
|
|
|
31
31
|
from optio_core.models import BasicAuth, TaskInstance
|
|
32
32
|
|
|
33
33
|
from optio_agents import HookContext
|
|
34
|
+
from optio_agents import RESUME_NOTICE, SYSTEM_MESSAGE_PREFIX
|
|
34
35
|
from optio_host.host import Host, LocalHost, ProcessHandle, RemoteHost
|
|
35
36
|
from optio_host.paths import task_dir
|
|
36
37
|
from optio_agents.protocol.session import _SessionFailed, run_log_protocol_session
|
|
@@ -95,74 +96,76 @@ async def run_opencode_session(ctx: ProcessContext, config: OpencodeTaskConfig)
|
|
|
95
96
|
# server (for the seed model default) before terminating it.
|
|
96
97
|
worker_port: int | None = None
|
|
97
98
|
|
|
98
|
-
#
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
if resume_requested:
|
|
102
|
-
snapshot = await load_latest_snapshot(
|
|
103
|
-
ctx._db, prefix=ctx._prefix, process_id=ctx.process_id,
|
|
104
|
-
)
|
|
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
|
|
105
102
|
|
|
106
|
-
# Connect + install BEFORE deciding fresh vs resume. The resume path
|
|
107
|
-
# needs ``opencode import`` to replay the saved session DB, which
|
|
108
|
-
# requires opencode to be installed on the host and resolved to an
|
|
109
|
-
# absolute path. Hoisting also lets the fresh path skip the redundant
|
|
110
|
-
# ``host.connect()`` later. ``setup_workdir`` is idempotent (mkdir -p)
|
|
111
|
-
# and the protocol driver still calls it again for the fresh path —
|
|
112
|
-
# harmless. Install progress reports through ``ctx``, so the
|
|
113
|
-
# dashboard sees activity from the very first step.
|
|
114
103
|
await host.connect()
|
|
115
|
-
await host.setup_workdir()
|
|
116
|
-
opencode_exec = await host_actions.ensure_opencode_installed(
|
|
117
|
-
HookContext(ctx, host),
|
|
118
|
-
install_if_missing=config.install_if_missing,
|
|
119
|
-
install_dir=config.opencode_install_dir,
|
|
120
|
-
)
|
|
121
104
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
# by appending it to optio.log.old.
|
|
145
|
-
await _rotate_optio_log(host)
|
|
146
|
-
preserved_session_id = snapshot["sessionId"]
|
|
147
|
-
except Exception as resume_exc:
|
|
148
|
-
# If the failure was the session-blob decrypt hook raising,
|
|
149
|
-
# this indicates the snapshot was tampered with or the
|
|
150
|
-
# consumer's keypair changed. Fail loud — silently dropping
|
|
151
|
-
# to fresh-start would mask the security-relevant signal.
|
|
152
|
-
if "decrypt" in repr(resume_exc).lower() and "blob" in repr(resume_exc).lower():
|
|
153
|
-
_LOG.error(
|
|
154
|
-
"resume restore failed inside session_blob_decrypt; "
|
|
155
|
-
"refusing to fall through to fresh-start. Operator must "
|
|
156
|
-
"investigate the snapshot blob.",
|
|
157
|
-
)
|
|
158
|
-
raise
|
|
159
|
-
_LOG.exception(
|
|
160
|
-
"resume restore failed; falling back to fresh-start path "
|
|
161
|
-
"(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,
|
|
162
127
|
)
|
|
128
|
+
|
|
129
|
+
resuming = snapshot is not None
|
|
130
|
+
if resuming:
|
|
163
131
|
await host.remove_file(opencode_db)
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
166
169
|
|
|
167
170
|
async def _opencode_body(host: Host, hook_ctx: HookContext) -> None:
|
|
168
171
|
"""Opencode-specific body that runs inside the protocol driver.
|
|
@@ -327,6 +330,13 @@ async def run_opencode_session(ctx: ProcessContext, config: OpencodeTaskConfig)
|
|
|
327
330
|
await _post_opencode_prompt(
|
|
328
331
|
worker_port, password, session_id, AUTO_START_PROMPT,
|
|
329
332
|
)
|
|
333
|
+
elif resuming and config.supports_resume:
|
|
334
|
+
# Push notification: make the resumed agent NOTICE the resume
|
|
335
|
+
# promptly (resume.log remains the pull-based source of truth).
|
|
336
|
+
await _post_opencode_prompt(
|
|
337
|
+
worker_port, password, session_id,
|
|
338
|
+
f"{SYSTEM_MESSAGE_PREFIX}{RESUME_NOTICE}",
|
|
339
|
+
)
|
|
330
340
|
|
|
331
341
|
# --- await opencode subprocess exit -----------------------------
|
|
332
342
|
# The protocol driver runs this body alongside the tail dispatcher
|
|
@@ -343,6 +353,14 @@ async def run_opencode_session(ctx: ProcessContext, config: OpencodeTaskConfig)
|
|
|
343
353
|
# --- run the protocol session -----------------------------------------
|
|
344
354
|
# host.connect() already happened up-front (before install + resume).
|
|
345
355
|
session_error: BaseException | None = None
|
|
356
|
+
|
|
357
|
+
async def _agent_sender(message: str) -> None:
|
|
358
|
+
# worker_port / session_id are set by _opencode_body at launch;
|
|
359
|
+
# password is established at function scope. _post_opencode_prompt
|
|
360
|
+
# raises on a non-2xx / unreachable worker, which
|
|
361
|
+
# send_to_agent converts to False.
|
|
362
|
+
await _post_opencode_prompt(worker_port, password, session_id, message)
|
|
363
|
+
|
|
346
364
|
try:
|
|
347
365
|
# before_execute is wired manually inside _opencode_body (after
|
|
348
366
|
# install, before launch) per opencode's documented timing.
|
|
@@ -352,9 +370,11 @@ async def run_opencode_session(ctx: ProcessContext, config: OpencodeTaskConfig)
|
|
|
352
370
|
await run_log_protocol_session(
|
|
353
371
|
host, ctx,
|
|
354
372
|
body=_opencode_body,
|
|
373
|
+
prepare=_prepare,
|
|
355
374
|
on_deliverable=config.on_deliverable,
|
|
356
375
|
after_execute=config.after_execute,
|
|
357
376
|
protocol=protocol,
|
|
377
|
+
agent_sender=_agent_sender,
|
|
358
378
|
)
|
|
359
379
|
except _SessionFailed as fail:
|
|
360
380
|
session_error = fail
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: optio-opencode
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Run opencode web as an optio task; local subprocess or remote via SSH.
|
|
5
5
|
Author-email: Kristof Csillag <kristof.csillag@deai-labs.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -22,7 +22,7 @@ Requires-Python: >=3.11
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
Requires-Dist: optio-core<0.3,>=0.2
|
|
24
24
|
Requires-Dist: optio-host<0.3,>=0.2
|
|
25
|
-
Requires-Dist: optio-agents<0.
|
|
25
|
+
Requires-Dist: optio-agents<0.3,>=0.2
|
|
26
26
|
Requires-Dist: asyncssh>=2.14
|
|
27
27
|
Provides-Extra: dev
|
|
28
28
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -12,6 +12,7 @@ src/optio_opencode.egg-info/SOURCES.txt
|
|
|
12
12
|
src/optio_opencode.egg-info/dependency_links.txt
|
|
13
13
|
src/optio_opencode.egg-info/requires.txt
|
|
14
14
|
src/optio_opencode.egg-info/top_level.txt
|
|
15
|
+
tests/test_agent_sender_opencode.py
|
|
15
16
|
tests/test_host_actions.py
|
|
16
17
|
tests/test_host_local.py
|
|
17
18
|
tests/test_host_primitives_local.py
|
|
@@ -20,6 +21,7 @@ tests/test_host_remote_resume.py
|
|
|
20
21
|
tests/test_host_resume.py
|
|
21
22
|
tests/test_prompt.py
|
|
22
23
|
tests/test_purge_seed.py
|
|
24
|
+
tests/test_resume_sentence_opencode.py
|
|
23
25
|
tests/test_sanity.py
|
|
24
26
|
tests/test_seed_config.py
|
|
25
27
|
tests/test_session_blob_hooks.py
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
import optio_opencode.session as S
|
|
4
|
+
from optio_agents import RESUME_NOTICE, SYSTEM_MESSAGE_PREFIX
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@pytest.mark.asyncio
|
|
8
|
+
async def test_post_prompt_signature_used_by_sender(monkeypatch):
|
|
9
|
+
"""The opencode sender forwards (port, password, session_id, message) to
|
|
10
|
+
_post_opencode_prompt verbatim. Mirrors the closure built in
|
|
11
|
+
run_opencode_session."""
|
|
12
|
+
calls = []
|
|
13
|
+
|
|
14
|
+
async def fake_post(port, password, session_id, message):
|
|
15
|
+
calls.append((port, password, session_id, message))
|
|
16
|
+
|
|
17
|
+
monkeypatch.setattr(S, "_post_opencode_prompt", fake_post)
|
|
18
|
+
|
|
19
|
+
worker_port, password, session_id = 4321, "pw", "sess-1"
|
|
20
|
+
|
|
21
|
+
async def _agent_sender(message: str) -> None:
|
|
22
|
+
await S._post_opencode_prompt(worker_port, password, session_id, message)
|
|
23
|
+
|
|
24
|
+
await _agent_sender("hello")
|
|
25
|
+
assert calls == [(4321, "pw", "sess-1", "hello")]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_resume_notice_string():
|
|
29
|
+
assert f"{SYSTEM_MESSAGE_PREFIX}{RESUME_NOTICE}" == "System: you have been resumed"
|
|
@@ -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"
|
|
@@ -308,9 +308,10 @@ async def test_second_session_consumes_seed(
|
|
|
308
308
|
async def test_auto_start_posts_on_fresh_and_not_on_resume(
|
|
309
309
|
mongo_db, task_root, _supply_scenario, monkeypatch,
|
|
310
310
|
):
|
|
311
|
-
"""auto_start=True POSTs the kickoff prompt on a fresh launch
|
|
312
|
-
|
|
313
|
-
conversation)
|
|
311
|
+
"""auto_start=True POSTs the kickoff prompt on a fresh launch; on resume
|
|
312
|
+
the kickoff is suppressed (the restored session already carries its
|
|
313
|
+
conversation) but a System: resume notice is POSTed so the agent notices
|
|
314
|
+
the resume."""
|
|
314
315
|
import optio_opencode.session as session_mod
|
|
315
316
|
|
|
316
317
|
_supply_scenario["name"] = "happy"
|
|
@@ -335,19 +336,29 @@ async def test_auto_start_posts_on_fresh_and_not_on_resume(
|
|
|
335
336
|
# fall back to a fresh launch and POST the kickoff prompt again.
|
|
336
337
|
before_execute=_plant_env,
|
|
337
338
|
))
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
339
|
+
# The 'happy' scenario also emits a DELIVERABLE, which the mandatory
|
|
340
|
+
# acknowledgment POSTs as a "deliverable ...: accepted" ack via the same
|
|
341
|
+
# _post_opencode_prompt. This test is about the kickoff / resume-notice
|
|
342
|
+
# posts, so filter those out.
|
|
343
|
+
notice = f"{session_mod.SYSTEM_MESSAGE_PREFIX}{session_mod.RESUME_NOTICE}"
|
|
344
|
+
kickoffs = [m for _sid, m in posts if m == session_mod.AUTO_START_PROMPT]
|
|
345
|
+
notices = [m for _sid, m in posts if m == notice]
|
|
346
|
+
assert kickoffs == [session_mod.AUTO_START_PROMPT] # kickoff posted exactly once
|
|
347
|
+
assert notices == [] # no resume notice on fresh
|
|
348
|
+
assert all(sid for sid, _m in posts) # real (pre-created) session ids
|
|
349
|
+
|
|
350
|
+
# resume the same process → the kickoff is suppressed, but a System:
|
|
351
|
+
# resume notice is POSTed so the agent notices the resume.
|
|
344
352
|
ctx_resume = await _make_ctx(mongo_db, pid, resume=True)
|
|
345
353
|
await run_opencode_session(ctx_resume, OpencodeTaskConfig(
|
|
346
354
|
consumer_instructions="(scenario: happy)",
|
|
347
355
|
auto_start=True,
|
|
348
356
|
before_execute=_plant_env,
|
|
349
357
|
))
|
|
350
|
-
|
|
358
|
+
kickoffs = [m for _sid, m in posts if m == session_mod.AUTO_START_PROMPT]
|
|
359
|
+
notices = [m for _sid, m in posts if m == notice]
|
|
360
|
+
assert kickoffs == [session_mod.AUTO_START_PROMPT] # no NEW kickoff on resume
|
|
361
|
+
assert notices == [notice] # resume notice posted once
|
|
351
362
|
|
|
352
363
|
|
|
353
364
|
def test_post_opencode_prompt_uses_prompt_async_parts_body(monkeypatch):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{optio_opencode-0.1.9 → 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
|