Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 12 additions & 4 deletions argus_skill/manager/front_door.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,18 @@ def _ensure_manager_runner(chat_state: dict[str, Any], mem: Any) -> Any:
from ..apps._runtime import build_life_runner

runner = build_life_runner(ns)
manager_backend = getattr(runner, "_backend", None)
set_acp_scope = getattr(manager_backend, "set_acp_scope", None)
if callable(set_acp_scope):
set_acp_scope(f"manager:{chat_state.get('session_id') or workspace_key}")
acp_scope = f"manager:{chat_state.get('session_id') or workspace_key}"
backends: list[Any] = []
for backend in (
getattr(runner, "_backend", None),
getattr(runner, "manager_backend", None),
):
if backend is not None and not any(backend is item for item in backends):
backends.append(backend)
for backend in backends:
set_acp_scope = getattr(backend, "set_acp_scope", None)
if callable(set_acp_scope):
set_acp_scope(acp_scope)
except Exception: # noqa: BLE001 — retry on the next operator turn
return None

Expand Down
4 changes: 2 additions & 2 deletions argus_skill/release_manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"package_version": "0.1.1",
"release_id": "0.1.1+87a2e4aae67e9583",
"release_id": "0.1.1+87d11d59e1912d08",
"schema_version": 1,
"source_digest": "87a2e4aae67e95833d5b12a2140b54f90aae0eca54255b656f60d0d5d1826adb"
"source_digest": "87d11d59e1912d08174f89073bcd219fb6036c534986e19d4097cf7b3f946095"
}
8 changes: 7 additions & 1 deletion argus_skill/webapi/manager_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,13 @@ def _prewarm_manager_context(
state["session_id"] = sid
state["global_root"] = str(mem.global_root)
runner = _ensure_manager_runner(state, mem)
backend = getattr(runner, "_backend", None) if runner is not None else None
backend = None
if runner is not None:
backend = getattr(runner, "manager_backend", None) or getattr(
runner,
"_backend",
None,
)
prewarm = getattr(backend, "prewarm_acp_client", None)
if not callable(prewarm):
return
Expand Down
4 changes: 2 additions & 2 deletions frontend/core/src/release.generated.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
// Generated by argus_skill.release_tools.generate_manifest. Do not edit.
export const RELEASE_ID = "0.1.1+87a2e4aae67e9583";
export const RELEASE_SOURCE_DIGEST = "87a2e4aae67e95833d5b12a2140b54f90aae0eca54255b656f60d0d5d1826adb";
export const RELEASE_ID = "0.1.1+87d11d59e1912d08";
export const RELEASE_SOURCE_DIGEST = "87d11d59e1912d08174f89073bcd219fb6036c534986e19d4097cf7b3f946095";
88 changes: 44 additions & 44 deletions frontend/tui/bundle/argus.mjs

Large diffs are not rendered by default.

11 changes: 9 additions & 2 deletions frontend/tui/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,10 +459,17 @@ export class ApiClient {
return (await r.json()) as { ok: boolean; sid: string; name: string };
}

async snapshot(eventsLimit = 1, signal?: AbortSignal): Promise<Snapshot> {
async snapshot(
eventsLimit = 1,
signal?: AbortSignal,
prewarm = false,
): Promise<Snapshot> {
await this.meta();
return requestWithTimeout(
this.p(`/snapshot?compact=true&events_limit=${eventsLimit}`),
this.p(
`/snapshot?compact=true&events_limit=${eventsLimit}`
+ (prewarm ? '&prewarm=true' : ''),
),
{ headers: this.authHeaders(), signal },
this.readTimeoutMs,
async (r) => {
Expand Down
4 changes: 3 additions & 1 deletion frontend/tui/src/appProjectFeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,16 @@ export function useProjectFeed(api: ApiClient, project: string): ProjectFeedStat
useEffect(() => {
let alive = true;
let snapshotInFlight = false;
let prewarm = true;
let snapshotController: AbortController | undefined;
const tick = async () => {
if (snapshotInFlight) return;
snapshotInFlight = true;
const controller = new AbortController();
snapshotController = controller;
try {
const s = await api.snapshot(1, controller.signal);
const s = await api.snapshot(1, controller.signal, prewarm);
prewarm = false;
if (alive) {
setSnap((current) => (
current && JSON.stringify(current) === JSON.stringify(s)
Expand Down
21 changes: 21 additions & 0 deletions frontend/tui/test/protocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -974,6 +974,27 @@ test('ApiClient validates snapshot schema after the one-time handshake', async (
}
});

test('ApiClient requests Manager prewarm only when asked', async () => {
const originalFetch = globalThis.fetch;
const urls: string[] = [];
let calls = 0;
globalThis.fetch = (async (input) => {
calls += 1;
urls.push(String(input));
if (calls === 1) return Response.json(meta());
return Response.json({ schema_version: SNAPSHOT_SCHEMA_VERSION, daemon: {} });
}) as typeof fetch;
try {
const api = new ApiClient({ host: '127.0.0.1', port: 8799, project: 's-test' });
await assert.rejects(() => api.snapshot(1, undefined, true), /daemon fields missing/);
await assert.rejects(() => api.snapshot(1), /daemon fields missing/);
assert.match(urls[1], /events_limit=1&prewarm=true$/);
assert.doesNotMatch(urls[2], /prewarm=true/);
} finally {
globalThis.fetch = originalFetch;
}
});

test('ApiClient forwards compatible source-drift warnings', async () => {
const originalFetch = globalThis.fetch;
const warnings: string[] = [];
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion frontend/web/dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
document.documentElement.dataset.theme = dark ? 'dark' : 'light';
})();
</script>
<script type="module" crossorigin src="/assets/index-zS7B6Urk.js"></script>
<script type="module" crossorigin src="/assets/index-CB0pjs9N.js"></script>
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-hePW80VL.js">
<link rel="modulepreload" crossorigin href="/assets/icons-BgG77X6K.js">
<link rel="modulepreload" crossorigin href="/assets/query-DOc9YWJi.js">
Expand Down
9 changes: 6 additions & 3 deletions tests/manager/test_front_door_workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,11 @@ def test_manager_runner_scopes_acp_to_session_id(tmp_path, monkeypatch) -> None:
sid = "s-private-acp"
memory = MemoryBundle.for_cwd(tmp_path, global_root=root, fingerprint=sid)
memory.init()
scopes: list[str] = []
default_scopes: list[str] = []
manager_scopes: list[str] = []
runner = SimpleNamespace(
_backend=SimpleNamespace(set_acp_scope=scopes.append),
_backend=SimpleNamespace(set_acp_scope=default_scopes.append),
manager_backend=SimpleNamespace(set_acp_scope=manager_scopes.append),
)
monkeypatch.setattr(
"argus_skill.apps._runtime.build_life_runner",
Expand All @@ -193,4 +195,5 @@ def test_manager_runner_scopes_acp_to_session_id(tmp_path, monkeypatch) -> None:
)

assert result is runner
assert scopes == [f"manager:{sid}"]
assert default_scopes == [f"manager:{sid}"]
assert manager_scopes == [f"manager:{sid}"]
38 changes: 38 additions & 0 deletions tests/webapi/test_manager_rotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import threading
import time
from pathlib import Path
from types import SimpleNamespace

from argus_skill.life.memory import BacklogItem, LifeMemory
from argus_skill.webapi import manager_bridge, manager_dispatch, manager_state
Expand Down Expand Up @@ -104,6 +105,43 @@ def schedule() -> None:
caller.join(timeout=1)


def test_manager_prewarm_uses_manager_backend(
tmp_path: Path,
monkeypatch,
) -> None:
sid = "s-prewarm-manager-backend"
_make_project(tmp_path, sid)
manager_state._STATES.clear()
monkeypatch.setattr(manager_state, "_MANAGER_PREWARM_OWNER", sid)
manager_state._STATES[sid] = {
"backend": "copilot",
"last_access_monotonic": time.monotonic(),
}
calls: list[str] = []

default_backend = SimpleNamespace(
prewarm_acp_client=lambda **_kwargs: (_ for _ in ()).throw(
AssertionError("default backend must not own Manager prewarm")
)
)
manager_backend = SimpleNamespace(
prewarm_acp_client=lambda **_kwargs: calls.append("manager")
)
runner = SimpleNamespace(
_backend=default_backend,
manager_backend=manager_backend,
)
monkeypatch.setattr(
"argus_skill.manager.front_door._ensure_manager_runner",
lambda _state, _memory: runner,
)

manager_state._prewarm_manager_context(sid, global_root=tmp_path)

assert calls == ["manager"]
assert manager_state._STATES[sid]["_manager_acp_prewarmed"] is True


def test_warm_manager_contexts_are_bounded_and_oldest_is_closed(
monkeypatch,
) -> None:
Expand Down
Loading