From b216e4a7e76365d4d31deaa03d2cd5b05869c632 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Sun, 1 Nov 2026 23:40:35 +0530 Subject: [PATCH 1/5] fix(chat): withhold ambiguous recovered collaboration requests Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../manager_context/presentation.py | 13 +++++- tests/test_collaboration_goal_instance.py | 41 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/loopx/capabilities/manager_context/presentation.py b/loopx/capabilities/manager_context/presentation.py index b8002f5f0f..8d53b308ce 100644 --- a/loopx/capabilities/manager_context/presentation.py +++ b/loopx/capabilities/manager_context/presentation.py @@ -74,7 +74,13 @@ def project_collaboration( or not is_source_chat_host(root, route, store) ): continue - recovered.setdefault(client, []).append(route) + request_id = route.get("request_id") + if not isinstance(request_id, str): + continue + candidates = recovered.setdefault(client, {}) + # A duplicate route cannot establish this request's lost answer. + # Other recipients of the same Turn remain independently readable. + candidates[request_id] = route if request_id not in candidates else None except (OSError, ValueError, TypeError): continue result = [] @@ -90,7 +96,10 @@ def project_collaboration( if receipts is None: receipt = response.get("context_handoff_receipt") receipts = ( - [receipt] if receipt else recovered.get(turn.get("client_turn_id"), []) + [receipt] if receipt else [ + route for route in recovered.get(turn.get("client_turn_id"), {}).values() + if route is not None + ] ) collaborations = [] for receipt in receipts if isinstance(receipts, list) else []: diff --git a/tests/test_collaboration_goal_instance.py b/tests/test_collaboration_goal_instance.py index 333f0e673f..cf3b5864e7 100644 --- a/tests/test_collaboration_goal_instance.py +++ b/tests/test_collaboration_goal_instance.py @@ -1328,6 +1328,47 @@ def test_lost_answer_readback_cannot_invent_or_rebind_a_commitment(tmp_path, dam snapshot = project_chat_session_snapshot(tmp_path, store, session["session_id"], registry=registry) assert snapshot["messages"] == before assert not any(m.get("collaboration") for m in snapshot["messages"]) + assert _http_snapshot(tmp_path, registry, session["session_id"])["messages"] == before + + +@pytest.mark.parametrize("exact", [False, True], ids=["legacy", "exact"]) +def test_lost_answer_recovers_distinct_recipients_around_an_ambiguous_request(tmp_path, exact): + registry = _create_source_registry(tmp_path) + if not exact: + registry.write_text(json.dumps({"goals": _source_payload(tmp_path, INSTANCE_A)["goals"]})) + store, session, receipt = _manager_request(tmp_path, registry, complete=False) + turn = store.turn_for_client(session["session_id"], "owner-request") + other = deliver(tmp_path, registry, session=session, turn=turn, + request={"goal_id": "delivery", "agent_id": "reviewer", + "brief": _brief("Independently review the delivery plan")}) + store.update_turn(session["session_id"], turn["turn_id"], status="failed", response=None) + messages = store.messages(session["session_id"]) + + def source_bytes(): + return {path.relative_to(_root(tmp_path)): path.read_bytes() + for path in _root(tmp_path).rglob("*.json")} + + def read_requests(): + before = source_bytes() + snapshot = _http_snapshot(tmp_path, registry, session["session_id"]) + assert store.messages(session["session_id"]) == messages + assert source_bytes() == before + assert store.load_turn(session["session_id"], turn["turn_id"])["response"] is None + cards = [message for message in snapshot["messages"] if message.get("collaborations")] + assert len(cards) == 1 and cards[0]["role"] == "user" + return {item["request_id"]: item for item in cards[0]["collaborations"]} + + initial = read_requests() + assert set(initial) == {receipt["request_id"], other["request_id"]} + assert {item["agent_id"] for item in initial.values()} == {"builder", "reviewer"} + assert all(item["returns"] == [] for item in initial.values()) + route_path = _root(tmp_path) / "roundtrips" / (receipt["request_id"] + ".json") + duplicate = route_path.with_name("b" * 64 + ".json") + _write(duplicate, json.loads(route_path.read_text())) + for _ in range(2): + assert read_requests() == {other["request_id"]: initial[other["request_id"]]} + duplicate.unlink() + assert read_requests() == initial def test_exact_instance_updates_keep_original_route_and_http_readback(tmp_path): From e18805883e4c6048fb83d8a75abda39f7e62fef3 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 2 Nov 2026 02:27:48 +0530 Subject: [PATCH 2/5] fix(collaboration): compare return deadlines as instants Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- .../collaboration/return_delivery.ts | 24 ++++-- .../manager_return_transaction.test.ts | 84 +++++++++++++++++++ tests/test_collaboration_goal_instance.py | 64 ++++++++++++++ 3 files changed, 165 insertions(+), 7 deletions(-) diff --git a/loopx/control_plane/collaboration/return_delivery.ts b/loopx/control_plane/collaboration/return_delivery.ts index cb86fac865..fb26ebb6b5 100644 --- a/loopx/control_plane/collaboration/return_delivery.ts +++ b/loopx/control_plane/collaboration/return_delivery.ts @@ -142,6 +142,7 @@ interface ReturnCommand { key: string; path: string; now: string; + nowMicros: bigint; exact: boolean; facts: JsonObject; } @@ -152,6 +153,13 @@ function returnAddress(value: unknown): {request_id: string; result_key: string} result_key: matchingString(row.result_key, "result_key", RESULT_KEY)}; } +function returnClockMicros(value: unknown): bigint { + const instant = typeof value === "string" && value.length <= 64 + ? parseTodoTimestampMicros(value) : null; + if (instant === null) throw new EffectRuntimeRequestError("invalid return clock"); + return instant; +} + function returnCommand(value: unknown): ReturnCommand { const request = requireJsonObject(value, "return delivery transaction"); if (request.schema_version !== RETURN_DELIVERY_TRANSACTION) { @@ -161,10 +169,10 @@ function returnCommand(value: unknown): ReturnCommand { if (!isAbsolute(root)) throw new EffectRuntimeRequestError("return runtime root must be absolute"); const {request_id: id, result_key: key} = returnAddress(request); const now = requireNonEmptyString(request.now, "now"); - if (now.length > 64 || parseTodoTimestampMicros(now) === null) throw new EffectRuntimeRequestError("invalid return clock"); + const nowMicros = returnClockMicros(now); requireStringLiteral(request.phase, ["prepare", "attempt", "settle"], "return delivery phase"); const folder = join(root, ".local", "manager-context"); - return { request, folder, key, now, + return { request, folder, key, now, nowMicros, path: join(folder, "replies", id, `${key}.delivery.json`), exact: requireBoolean(request.exact, "exact"), facts: requireJsonObject(request.facts, "return source facts") }; @@ -207,12 +215,12 @@ function unavailableRecord(error: unknown): boolean { function pendingDelivery(command: ReturnCommand, state: JsonObject): boolean { if (TERMINAL.has(String(state.status))) return false; if (!command.exact || state.status !== "admitted") { - if (state.retry_at && command.now < String(state.retry_at)) return false; + if (state.retry_at && command.nowMicros < returnClockMicros(state.retry_at)) return false; } const admission = state.admission; return !(command.exact && admission && typeof admission === "object" && (admission as JsonObject).expires_at - && command.now < String((admission as JsonObject).expires_at)); + && command.nowMicros < returnClockMicros((admission as JsonObject).expires_at)); } function deliveryState(value: unknown): JsonObject { @@ -251,7 +259,7 @@ async function scanReturns(request: JsonObject): Promise { } function shiftedClock(now: string, seconds: number): string { - // Preserve Python's offset/fraction representation, used by existing lexical retry fences. + // Preserve Python's offset/fraction representation; eligibility compares instants. const match = /^(\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d)(\.\d+)?(Z|[+-]\d\d:\d\d(?::\d\d(?:\.\d+)?)?)?$/.exec(now); if (!match) throw new EffectRuntimeRequestError("invalid return clock representation"); return new Date(Date.parse(`${match[1]}Z`) + seconds * 1000).toISOString().slice(0, 19) @@ -379,7 +387,6 @@ function returnAuthorityBlocker(command: ReturnCommand): string | null { } async function prepareReturn(command: ReturnCommand, state: JsonObject): Promise { - if (!pendingDelivery(command, state)) return returnResponse(); if (command.facts.error) return sourceFailure(command, state); let source: Awaited>; try { @@ -529,7 +536,10 @@ export async function transactManagerReturnDelivery(value: unknown): Promise { let state: JsonObject; - try { state = deliveryState(await readReturnRecord(command.path, {})); } + try { + state = deliveryState(await readReturnRecord(command.path, {})); + if (command.request.phase === "prepare" && !pendingDelivery(command, state)) return returnResponse(); + } catch (error) { if (command.request.phase === "prepare" && unavailableRecord(error)) return returnResponse(); throw error; diff --git a/tests/control_plane_ts/manager_return_transaction.test.ts b/tests/control_plane_ts/manager_return_transaction.test.ts index 7c16551856..564cea791f 100644 --- a/tests/control_plane_ts/manager_return_transaction.test.ts +++ b/tests/control_plane_ts/manager_return_transaction.test.ts @@ -146,6 +146,90 @@ test("an ambiguous expired external admission never dispatches another send", as } finally { await f.close(); } }); +const deadlineCases = [ + {label: "offset before", now: "2026-01-02T04:00:00+01:00", deadline: "2026-01-02T03:30:00+00:00", due: false}, + {label: "offset due", now: "2026-01-02T03:30:00Z", deadline: "2026-01-02T04:00:00+01:00", due: true}, + {label: "microsecond before", now: "2026-01-02T03:30:00Z", deadline: "2026-01-02T03:30:00.000001+00:00", due: false}, + {label: "fraction precision before", now: "2026-01-02T03:30:00.1Z", deadline: "2026-01-02T03:30:00.100001+00:00", due: false}, + {label: "equal instant", now: "2026-01-02T03:30:00+01:00", deadline: "2026-01-02T02:30:00Z", due: true}, + {label: "offset seconds due", now: "2026-01-02T03:30:00.123457Z", deadline: "2026-01-02T04:30:30.123456+01:00:30", due: true}, +]; + +for (const clock of deadlineCases) { + for (const exact of [false, true]) { + test(`return retry clock uses instants: ${clock.label} (${exact})`, async () => { + const f = await fixture(exact, true); + try { + await f.put(f.statePath, {status: "retry_pending", retry_at: clock.deadline}); + const bytes = await readFile(f.statePath); + const scanned = await f.run("scan", {now: clock.now, + candidates: [{request_id: id, result_key: "conclusion"}]}); + assert.deepEqual(scanned.candidates, clock.due ? [{request_id: id, result_key: "conclusion"}] : []); + assert.equal(scanned.unavailable_count, 0); + assert.deepEqual(await readFile(f.statePath), bytes); + assert.equal((await f.run("prepare", {now: clock.now})).action, clock.due ? "send" : "skip"); + if (!clock.due) assert.deepEqual(await readFile(f.statePath), bytes); + } finally { await f.close(); } + }); + } + + test(`return admission clock retains the original attempt: ${clock.label}`, async () => { + const f = await fixture(true, true); + try { + await f.put(f.statePath, {status: "admitted", goal_ref: ref, attempt, + admission: {token: "prior-send", prior_status: "verification_required", expires_at: clock.deadline}}); + const bytes = await readFile(f.statePath); + const scanned = await f.run("scan", {now: clock.now, + candidates: [{request_id: id, result_key: "conclusion"}]}); + assert.deepEqual(scanned.candidates, clock.due ? [{request_id: id, result_key: "conclusion"}] : []); + assert.deepEqual(await readFile(f.statePath), bytes); + const plan = await f.run("prepare", {now: clock.now}); + assert.equal(plan.action, clock.due ? "verify" : "skip"); + if (clock.due) { + assert.deepEqual(plan.attempt, attempt); + await f.run("settle", {...plan, now: clock.now, outcome: {kind: "verified", + verification: {verification_performed: true, reply_verified: true}}}); + assert.equal((await f.state()).status, "delivered"); + assert.equal((await f.state()).provider_receipt, attempt.provider_receipt); + } else { + assert.deepEqual(await readFile(f.statePath), bytes); + } + } finally { await f.close(); } + }); +} + +for (const field of ["retry_at", "expires_at"]) { + for (const malformed of ["not-a-clock", "2026-02-30T03:04:05Z", 42, {}]) { + test(`return clock damage is unavailable without rewriting ${field}: ${JSON.stringify(malformed)}`, async () => { + const f = await fixture(true, true); + try { + await f.put(f.statePath, field === "retry_at" + ? {status: "retry_pending", retry_at: malformed} + : {status: "admitted", admission: {expires_at: malformed}, attempt}); + const bytes = await readFile(f.statePath); + const scanned = await f.run("scan", {candidates: [{request_id: id, result_key: "conclusion"}]}); + assert.deepEqual(scanned.candidates, []); + assert.equal(scanned.unavailable_count, 1); + assert.equal((await f.run("prepare")).action, "skip"); + assert.deepEqual(await readFile(f.statePath), bytes); + } finally { await f.close(); } + }); + } +} + +test("return clock admission rechecks a due scan after another writer defers it", async () => { + const f = await fixture(true, true); + try { + const candidates = [{request_id: id, result_key: "conclusion"}]; + await f.put(f.statePath, {status: "retry_pending", retry_at: "2026-01-02T03:00:00Z"}); + assert.deepEqual((await f.run("scan", {candidates})).candidates, candidates); + await f.put(f.statePath, {status: "retry_pending", retry_at: "2026-01-02T04:04:05.123457+01:00"}); + const bytes = await readFile(f.statePath); + assert.equal((await f.run("prepare")).action, "skip"); + assert.deepEqual(await readFile(f.statePath), bytes); + } finally { await f.close(); } +}); + test("invalid transaction addresses and phases refuse before changing evidence", async () => { const f = await fixture(false); try { diff --git a/tests/test_collaboration_goal_instance.py b/tests/test_collaboration_goal_instance.py index 333f0e673f..ecacadfefc 100644 --- a/tests/test_collaboration_goal_instance.py +++ b/tests/test_collaboration_goal_instance.py @@ -1402,3 +1402,67 @@ def invoke(phase, *, crash=False, **payload): assert state["provider_receipt"] == attempt["provider_receipt"] assert invoke("prepare")["action"] == "skip" assert json.loads(state_path.read_text()) == state + + +@pytest.mark.parametrize("exact", [False, True], ids=["legacy", "exact"]) +@pytest.mark.parametrize("start_offset", [0, 1], ids=["earlier-local-clock", "later-local-clock"]) +def test_return_retry_clock_keeps_the_original_chat(tmp_path, monkeypatch, exact, start_offset): + registry = _create_source_registry(tmp_path) + if not exact: + registry.write_text(json.dumps({"goals": _source_payload(tmp_path, INSTANCE_A)["goals"]})) + store, session, receipt = _manager_request(tmp_path, registry) + rid, goal_ref = receipt["request_id"], receipt.get("goal_ref") + acknowledge(tmp_path, "delivery", "builder", rid, "adopt", "Accepted", registry=registry, caller_goal_ref=goal_ref) + report(tmp_path, "delivery", "builder", rid, "conclusion", "Verified delivery findings.", + registry=registry, caller_goal_ref=goal_ref) + started = datetime(2026, 1, 2, 3 + start_offset, tzinfo=timezone(timedelta(hours=start_offset))) + caller_zone = timezone(timedelta(hours=1 - start_offset)) + state_path = _root(tmp_path) / "replies" / rid / "conclusion.delivery.json" + route_path = _root(tmp_path) / "roundtrips" / (rid + ".json") + original_route = route_path.read_bytes() + original_turn = store.turn_for_client(session["session_id"], "owner-request") + original_messages = store.messages(session["session_id"]) + + def unavailable_chat(*_args, **_kwargs): + raise OSError("chat write temporarily unavailable") + + with monkeypatch.context() as temporary: + temporary.setattr(store, "append_message", unavailable_chat) + assert drain(tmp_path, registry, store, None, now=started) == 1 + state = json.loads(state_path.read_text()) + deadline = started + timedelta(seconds=10) + assert state["status"] == "retry_pending" + assert state["retry_at"] == deadline.isoformat() + before = state_path.read_bytes() + assert drain(tmp_path, registry, store, None, + now=(deadline - timedelta(microseconds=1)).astimezone(caller_zone)) == 0 + assert state_path.read_bytes() == before + assert store.messages(session["session_id"]) == original_messages + recover = """from datetime import datetime +from pathlib import Path +import sys +from loopx.chat_store import ChatSessionStore +from loopx.capabilities.manager_context.roundtrip import drain +root = Path(sys.argv[1]) +print(drain(root, Path(sys.argv[2]), ChatSessionStore(root), None, + now=datetime.fromisoformat(sys.argv[3]))) +""" + child = subprocess.run([sys.executable, "-c", recover, str(tmp_path), str(registry), + deadline.astimezone(caller_zone).isoformat()], + cwd=Path(__file__).parents[1], capture_output=True, text=True, timeout=30) + assert child.returncode == 0, child.stderr + assert child.stdout.strip() == "1", child.stderr + snapshot = _http_snapshot(tmp_path, registry, session["session_id"]) + returned = [m for m in snapshot["messages"] if m.get("origin") == "manager_followup"] + assert len(returned) == 1 + assert returned[0]["text"].endswith("Verified delivery findings.") + assert returned[0]["return_delivery"]["status"] == "delivered" + assert route_path.read_bytes() == original_route + current_turn = store.load_turn(session["session_id"], original_turn["turn_id"]) + assert current_turn["response"] == original_turn["response"] + assert current_turn["status"] == original_turn["status"] + saved, messages = state_path.read_bytes(), store.messages(session["session_id"]) + assert drain(tmp_path, registry, ChatSessionStore(tmp_path), None, + now=(deadline + timedelta(seconds=1)).astimezone(caller_zone)) == 0 + assert state_path.read_bytes() == saved + assert store.messages(session["session_id"]) == messages From ba5d3c25f286d52807b974b216ca2145f164ac46 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 2 Nov 2026 03:12:52 +0530 Subject: [PATCH 3/5] test(native-chat): isolate ambient provider selection Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/conftest.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 0cded7d70e..e60e366b8b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -91,6 +91,8 @@ def _isolated_default_runtime_routes(tmp_path_factory, monkeypatch): monkeypatch.setenv("HOME", str(home)) monkeypatch.setenv("USERPROFILE", str(home)) monkeypatch.delenv("CODEX_HOME", raising=False) + # Tests opt into provider selection; isolate the operator's Chat route. + monkeypatch.delenv("LOOPX_CHAT_CODEX_MODEL_PROVIDER", raising=False) for module_name, attribute, parts in _DEFAULT_ROUTE_REFERENCES: monkeypatch.setattr(importlib.import_module(module_name), attribute, home.joinpath(*parts)) yield From 25783eb344af16acdf6b6b60f01333c257724607 Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 2 Nov 2026 06:32:02 +0530 Subject: [PATCH 4/5] test(chat): characterize native result digest proofs Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- tests/control_plane_ts/conversation_binding.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/control_plane_ts/conversation_binding.test.ts b/tests/control_plane_ts/conversation_binding.test.ts index 2117ddd91d..ad25a868b2 100644 --- a/tests/control_plane_ts/conversation_binding.test.ts +++ b/tests/control_plane_ts/conversation_binding.test.ts @@ -45,6 +45,16 @@ test("native terminal recovery adopts a result without Turn admission or lost fa assert.equal(plan.operation, "adopt_native_result"); assert.equal(plan.recovery_eligible, true); assert.equal(plan.client_turn_id, undefined); + for (const field of ["input_sha256", "answer_sha256"]) { + for (const invalid of [null, false, 1, [], {}, "", "1".repeat(63), "1".repeat(65), + "A".repeat(64), `sha256:${"1".repeat(64)}`]) { + assert.equal(planBoundConversationRequest({...packet, context_recovery: {...packet.context_recovery, + native_observation: {...proof, [field]: invalid}}}).recovery_eligible, false, + `${field} must reject ${JSON.stringify(invalid)}`); + } + assert.equal(planBoundConversationRequest({...packet, context_recovery: {...packet.context_recovery, + native_observation: {...proof, [field]: "abcdef0123456789".repeat(4)}}}).recovery_eligible, true); + } for (const change of [{source_verified: false}, {stop_observed: true}, {steering_observed: true}, {attachments_present: true}, {native_observation: {...proof, thread_id: "other"}}, {native_observation: {...proof, request_ref: "a".repeat(24)}}, From 4509737dc18b5756b467209cbe13ab0bf916399a Mon Sep 17 00:00:00 2001 From: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> Date: Mon, 2 Nov 2026 06:38:59 +0530 Subject: [PATCH 5/5] fix(chat): reuse canonical digest owner for native recovery Signed-off-by: LoopX Agent <337587101+loopx-agent@users.noreply.github.com> --- loopx/control_plane/collaboration/conversation_binding.ts | 3 ++- tests/control_plane_ts/content_digest_single_owner.test.ts | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/loopx/control_plane/collaboration/conversation_binding.ts b/loopx/control_plane/collaboration/conversation_binding.ts index bd0a0e7f9b..2c7fc465da 100644 --- a/loopx/control_plane/collaboration/conversation_binding.ts +++ b/loopx/control_plane/collaboration/conversation_binding.ts @@ -1,6 +1,7 @@ import type {JsonObject} from "../effect_program.ts"; import {isDeepStrictEqual} from "node:util"; import {createHash} from "node:crypto"; +import {BARE_SHA256_PATTERN} from "../content_digest.ts"; import {EffectRuntimeRequestError} from "../effect_runtime_errors.ts"; import {requireInteger, requireJsonObject, requireNonEmptyString} from "../runtime_decode.ts"; import {requireAutomationCadenceEligibility} from "../quota/automation_cadence.ts"; @@ -611,7 +612,7 @@ function planContextRecovery(params: JsonObject, row: JsonObject, current: JsonO && Number.isFinite(created) && created >= start && created <= end && Number.isSafeInteger(proof.start_step_index) && Number(proof.start_step_index) >= 0 && Number.isSafeInteger(proof.total_steps) && Number(proof.total_steps) > Number(proof.start_step_index) + 1 - && [proof.input_sha256, proof.answer_sha256].every(value => typeof value === "string" && /^[a-f0-9]{64}$/.test(value)) + && [proof.input_sha256, proof.answer_sha256].every(value => typeof value === "string" && BARE_SHA256_PATTERN.test(value)) && response?.schema_version === "loopx_chat_agent_response_v0" && typeof response.message === "string" && response.message.trim().length > 0 && (adopted === null || adopted.turn_id === failed.turn_id diff --git a/tests/control_plane_ts/content_digest_single_owner.test.ts b/tests/control_plane_ts/content_digest_single_owner.test.ts index 7fdd69e9ae..90b2d1c931 100644 --- a/tests/control_plane_ts/content_digest_single_owner.test.ts +++ b/tests/control_plane_ts/content_digest_single_owner.test.ts @@ -108,6 +108,7 @@ const CANONICAL_CONSUMERS = [ "control_plane/capabilities/trajectory_input_usage.ts", "control_plane/capabilities/trajectory_tool_context.ts", "control_plane/collaboration/chat_mode.ts", + "control_plane/collaboration/conversation_binding.ts", "control_plane/collaboration/delegation.ts", "control_plane/collaboration/delegation_result_use.ts", "control_plane/collaboration/goal_instance_lifecycle.ts",