tenseleyflow/loader / c694b5a

Browse files

Persist runtime owner metadata in sessions

Authored by espadonne
SHA
c694b5a28f9d20ef9e96c1b1b4926176cd65db28
Parents
69d958e
Tree
6af580d

9 changed files

StatusFile+-
M src/loader/runtime/bootstrap.py 2 1
A src/loader/runtime/owner_metadata.py 73 0
M src/loader/runtime/public_shell.py 21 0
M src/loader/runtime/session.py 33 1
M tests/test_runtime_bootstrap.py 8 2
M tests/test_runtime_handle.py 8 2
M tests/test_runtime_launcher.py 4 1
M tests/test_runtime_public_shell.py 10 0
M tests/test_session_state.py 33 0
src/loader/runtime/bootstrap.pymodified
@@ -17,6 +17,7 @@ from .context import (
17
     RuntimeSafeguardsProtocol,
17
     RuntimeSafeguardsProtocol,
18
 )
18
 )
19
 from .events import TurnSummary
19
 from .events import TurnSummary
20
+from .owner_metadata import build_runtime_owner_metadata
20
 from .permissions import PermissionConfigStatus, PermissionPolicy
21
 from .permissions import PermissionConfigStatus, PermissionPolicy
21
 from .reasoning_service import RuntimeReasoningService
22
 from .reasoning_service import RuntimeReasoningService
22
 from .session import ConversationSession
23
 from .session import ConversationSession
@@ -174,7 +175,7 @@ def build_runtime_bootstrap_source(source: RuntimeBootstrapSource | Any) -> Runt
174
         _queue_steering_message=source.queue_steering_message,
175
         _queue_steering_message=source.queue_steering_message,
175
         _drain_steering_messages=source.drain_steering_messages,
176
         _drain_steering_messages=source.drain_steering_messages,
176
         _refresh_capability_profile=source.refresh_capability_profile,
177
         _refresh_capability_profile=source.refresh_capability_profile,
177
-        metadata={"owner_type": type(source).__name__},
178
+        metadata=build_runtime_owner_metadata(source),
178
     )
179
     )
179
 
180
 
180
 
181
 
src/loader/runtime/owner_metadata.pyadded
@@ -0,0 +1,73 @@
1
+"""Shared helpers for runtime-owner metadata."""
2
+
3
+from __future__ import annotations
4
+
5
+from typing import Any
6
+
7
+_KNOWN_RUNTIME_OWNER_PATHS = {
8
+    "Agent": "public-agent",
9
+    "RuntimeHandle": "runtime-handle",
10
+}
11
+
12
+
13
+def normalize_runtime_owner_type(value: Any) -> str | None:
14
+    """Coerce persisted runtime-owner types into optional text."""
15
+
16
+    if value is None:
17
+        return None
18
+    text = str(value).strip()
19
+    return text or None
20
+
21
+
22
+def normalize_runtime_owner_path(
23
+    value: Any,
24
+    *,
25
+    owner_type: str | None = None,
26
+) -> str | None:
27
+    """Coerce persisted runtime-owner paths into canonical text."""
28
+
29
+    if value is not None:
30
+        text = str(value).strip()
31
+        if text:
32
+            return text
33
+    if owner_type is None:
34
+        return None
35
+    return _KNOWN_RUNTIME_OWNER_PATHS.get(owner_type, _camel_to_kebab(owner_type))
36
+
37
+
38
+def build_runtime_owner_metadata(source: Any) -> dict[str, str | None]:
39
+    """Build canonical runtime-owner metadata from one shell owner."""
40
+
41
+    owner_type = (
42
+        normalize_runtime_owner_type(source)
43
+        if isinstance(source, str)
44
+        else normalize_runtime_owner_type(type(source).__name__)
45
+    )
46
+    return {
47
+        "owner_type": owner_type,
48
+        "owner_path": normalize_runtime_owner_path(None, owner_type=owner_type),
49
+    }
50
+
51
+
52
+def format_runtime_owner_label(
53
+    owner_type: str | None,
54
+    owner_path: str | None,
55
+) -> str | None:
56
+    """Render one compact human-readable runtime-owner label."""
57
+
58
+    normalized_type = normalize_runtime_owner_type(owner_type)
59
+    normalized_path = normalize_runtime_owner_path(owner_path, owner_type=normalized_type)
60
+    if normalized_type and normalized_path:
61
+        return f"{normalized_path} ({normalized_type})"
62
+    return normalized_path or normalized_type
63
+
64
+
65
+def _camel_to_kebab(value: str) -> str:
66
+    """Convert one CamelCase-ish class name into kebab-case."""
67
+
68
+    chars: list[str] = []
69
+    for index, char in enumerate(value):
70
+        if char.isupper() and index > 0:
71
+            chars.append("-")
72
+        chars.append(char.lower())
73
+    return "".join(chars)
src/loader/runtime/public_shell.pymodified
@@ -18,6 +18,7 @@ from .capabilities import CapabilityProfile, resolve_backend_capability_profile
18
 from .dod import DefinitionOfDoneStore
18
 from .dod import DefinitionOfDoneStore
19
 from .events import AgentEvent, TurnSummary
19
 from .events import AgentEvent, TurnSummary
20
 from .launcher import build_runtime_launcher
20
 from .launcher import build_runtime_launcher
21
+from .owner_metadata import build_runtime_owner_metadata
21
 from .permissions import PermissionConfigStatus, PermissionMode, PermissionPolicy
22
 from .permissions import PermissionConfigStatus, PermissionMode, PermissionPolicy
22
 from .prompt_history import PromptSnapshot
23
 from .prompt_history import PromptSnapshot
23
 from .prompting import build_system_prompt_result
24
 from .prompting import build_system_prompt_result
@@ -174,6 +175,8 @@ def create_runtime_session(
174
     prompt_format: str | None,
175
     prompt_format: str | None,
175
     prompt_sections: list[str],
176
     prompt_sections: list[str],
176
     workflow_mode: str,
177
     workflow_mode: str,
178
+    runtime_owner_type: str | None,
179
+    runtime_owner_path: str | None,
177
     rotate_after_bytes: int,
180
     rotate_after_bytes: int,
178
     auto_compaction_input_tokens_threshold: int,
181
     auto_compaction_input_tokens_threshold: int,
179
     compaction_keep_last_messages: int,
182
     compaction_keep_last_messages: int,
@@ -187,6 +190,8 @@ def create_runtime_session(
187
         few_shot_factory=few_shot_factory,
190
         few_shot_factory=few_shot_factory,
188
         project_root=project_root,
191
         project_root=project_root,
189
         messages=messages or [],
192
         messages=messages or [],
193
+        runtime_owner_type=runtime_owner_type,
194
+        runtime_owner_path=runtime_owner_path,
190
         permission_mode=permission_policy.active_mode.as_str(),
195
         permission_mode=permission_policy.active_mode.as_str(),
191
         permission_prompting_enabled=permission_policy.prompting_enabled,
196
         permission_prompting_enabled=permission_policy.prompting_enabled,
192
         permission_rule_counts=_copy_rule_counts(permission_policy.rule_counts()),
197
         permission_rule_counts=_copy_rule_counts(permission_policy.rule_counts()),
@@ -211,6 +216,8 @@ def create_runtime_session_install(
211
     prompt_format: str | None,
216
     prompt_format: str | None,
212
     prompt_sections: list[str],
217
     prompt_sections: list[str],
213
     workflow_mode: str,
218
     workflow_mode: str,
219
+    runtime_owner_type: str | None,
220
+    runtime_owner_path: str | None,
214
     rotate_after_bytes: int,
221
     rotate_after_bytes: int,
215
     auto_compaction_input_tokens_threshold: int,
222
     auto_compaction_input_tokens_threshold: int,
216
     compaction_keep_last_messages: int,
223
     compaction_keep_last_messages: int,
@@ -227,6 +234,8 @@ def create_runtime_session_install(
227
         prompt_format=prompt_format,
234
         prompt_format=prompt_format,
228
         prompt_sections=prompt_sections,
235
         prompt_sections=prompt_sections,
229
         workflow_mode=workflow_mode,
236
         workflow_mode=workflow_mode,
237
+        runtime_owner_type=runtime_owner_type,
238
+        runtime_owner_path=runtime_owner_path,
230
         rotate_after_bytes=rotate_after_bytes,
239
         rotate_after_bytes=rotate_after_bytes,
231
         auto_compaction_input_tokens_threshold=(
240
         auto_compaction_input_tokens_threshold=(
232
             auto_compaction_input_tokens_threshold
241
             auto_compaction_input_tokens_threshold
@@ -262,6 +271,15 @@ def apply_runtime_session_install(
262
     owner.prompt_sections = list(install.restored.prompt_sections)
271
     owner.prompt_sections = list(install.restored.prompt_sections)
263
     owner.last_turn_summary = install.restored.last_turn_summary
272
     owner.last_turn_summary = install.restored.last_turn_summary
264
     owner._system_message = None
273
     owner._system_message = None
274
+    owner_metadata = build_runtime_owner_metadata(owner)
275
+    if (
276
+        install.session.runtime_owner_type != owner_metadata["owner_type"]
277
+        or install.session.runtime_owner_path != owner_metadata["owner_path"]
278
+    ):
279
+        install.session.update_runtime_state(
280
+            runtime_owner_type=owner_metadata["owner_type"],
281
+            runtime_owner_path=owner_metadata["owner_path"],
282
+        )
265
 
283
 
266
 
284
 
267
 def build_fresh_runtime_session_install(
285
 def build_fresh_runtime_session_install(
@@ -272,6 +290,7 @@ def build_fresh_runtime_session_install(
272
 ) -> RuntimeSessionInstall:
290
 ) -> RuntimeSessionInstall:
273
     """Build a fresh runtime session install from the current public shell."""
291
     """Build a fresh runtime session install from the current public shell."""
274
 
292
 
293
+    owner_metadata = build_runtime_owner_metadata(owner)
275
     return create_runtime_session_install(
294
     return create_runtime_session_install(
276
         project_root=owner.project_root,
295
         project_root=owner.project_root,
277
         messages=messages,
296
         messages=messages,
@@ -280,6 +299,8 @@ def build_fresh_runtime_session_install(
280
         prompt_format=owner.prompt_format,
299
         prompt_format=owner.prompt_format,
281
         prompt_sections=list(owner.prompt_sections),
300
         prompt_sections=list(owner.prompt_sections),
282
         workflow_mode=workflow_mode or owner.workflow_mode,
301
         workflow_mode=workflow_mode or owner.workflow_mode,
302
+        runtime_owner_type=owner_metadata["owner_type"],
303
+        runtime_owner_path=owner_metadata["owner_path"],
283
         rotate_after_bytes=owner.config.session_rotate_after_bytes,
304
         rotate_after_bytes=owner.config.session_rotate_after_bytes,
284
         auto_compaction_input_tokens_threshold=(
305
         auto_compaction_input_tokens_threshold=(
285
             owner.config.session_auto_compaction_input_tokens_threshold
306
             owner.config.session_auto_compaction_input_tokens_threshold
src/loader/runtime/session.pymodified
@@ -24,11 +24,15 @@ from .completion_trace import (
24
     has_canonical_completion_trace,
24
     has_canonical_completion_trace,
25
     normalize_completion_trace,
25
     normalize_completion_trace,
26
 )
26
 )
27
+from .owner_metadata import (
28
+    normalize_runtime_owner_path,
29
+    normalize_runtime_owner_type,
30
+)
27
 from .prompt_history import PromptSnapshot, normalize_prompt_history
31
 from .prompt_history import PromptSnapshot, normalize_prompt_history
28
 from .workflow_ledger import WorkflowLedger
32
 from .workflow_ledger import WorkflowLedger
29
 from .workflow_policy import WorkflowTimelineEntry
33
 from .workflow_policy import WorkflowTimelineEntry
30
 
34
 
31
-SESSION_VERSION = 10
35
+SESSION_VERSION = 11
32
 DEFAULT_ROTATE_AFTER_BYTES = 256 * 1024
36
 DEFAULT_ROTATE_AFTER_BYTES = 256 * 1024
33
 MAX_ROTATED_FILES = 3
37
 MAX_ROTATED_FILES = 3
34
 _UNSET = object()
38
 _UNSET = object()
@@ -173,6 +177,8 @@ class SessionSnapshot:
173
     usage: dict[str, int] = field(default_factory=dict)
177
     usage: dict[str, int] = field(default_factory=dict)
174
     active_dod_path: str | None = None
178
     active_dod_path: str | None = None
175
     current_task: str | None = None
179
     current_task: str | None = None
180
+    runtime_owner_type: str | None = None
181
+    runtime_owner_path: str | None = None
176
     workflow_mode: str = "execute"
182
     workflow_mode: str = "execute"
177
     permission_mode: str = "workspace-write"
183
     permission_mode: str = "workspace-write"
178
     permission_prompting_enabled: bool = False
184
     permission_prompting_enabled: bool = False
@@ -211,6 +217,8 @@ class SessionSnapshot:
211
             "usage": dict(self.usage),
217
             "usage": dict(self.usage),
212
             "active_dod_path": self.active_dod_path,
218
             "active_dod_path": self.active_dod_path,
213
             "current_task": self.current_task,
219
             "current_task": self.current_task,
220
+            "runtime_owner_type": self.runtime_owner_type,
221
+            "runtime_owner_path": self.runtime_owner_path,
214
             "workflow_mode": self.workflow_mode,
222
             "workflow_mode": self.workflow_mode,
215
             "permission_mode": self.permission_mode,
223
             "permission_mode": self.permission_mode,
216
             "permission_prompting_enabled": self.permission_prompting_enabled,
224
             "permission_prompting_enabled": self.permission_prompting_enabled,
@@ -265,6 +273,15 @@ class SessionSnapshot:
265
             usage=normalize_usage(data.get("usage")),
273
             usage=normalize_usage(data.get("usage")),
266
             active_dod_path=data.get("active_dod_path"),
274
             active_dod_path=data.get("active_dod_path"),
267
             current_task=data.get("current_task"),
275
             current_task=data.get("current_task"),
276
+            runtime_owner_type=normalize_runtime_owner_type(
277
+                data.get("runtime_owner_type")
278
+            ),
279
+            runtime_owner_path=normalize_runtime_owner_path(
280
+                data.get("runtime_owner_path"),
281
+                owner_type=normalize_runtime_owner_type(
282
+                    data.get("runtime_owner_type")
283
+                ),
284
+            ),
268
             workflow_mode=str(data.get("workflow_mode", "execute")),
285
             workflow_mode=str(data.get("workflow_mode", "execute")),
269
             permission_mode=str(data.get("permission_mode", "workspace-write")),
286
             permission_mode=str(data.get("permission_mode", "workspace-write")),
270
             permission_prompting_enabled=bool(
287
             permission_prompting_enabled=bool(
@@ -434,6 +451,8 @@ class ConversationSession:
434
     usage_totals: dict[str, int] = field(default_factory=dict)
451
     usage_totals: dict[str, int] = field(default_factory=dict)
435
     active_dod_path: str | None = None
452
     active_dod_path: str | None = None
436
     current_task: str | None = None
453
     current_task: str | None = None
454
+    runtime_owner_type: str | None = None
455
+    runtime_owner_path: str | None = None
437
     workflow_mode: str = "execute"
456
     workflow_mode: str = "execute"
438
     permission_mode: str = "workspace-write"
457
     permission_mode: str = "workspace-write"
439
     permission_prompting_enabled: bool = False
458
     permission_prompting_enabled: bool = False
@@ -568,6 +587,8 @@ class ConversationSession:
568
         *,
587
         *,
569
         active_dod_path: str | None = None,
588
         active_dod_path: str | None = None,
570
         current_task: str | None = None,
589
         current_task: str | None = None,
590
+        runtime_owner_type: str | None = None,
591
+        runtime_owner_path: str | None = None,
571
         workflow_mode: str | None = None,
592
         workflow_mode: str | None = None,
572
         permission_mode: str | None = None,
593
         permission_mode: str | None = None,
573
         permission_prompting_enabled: bool | None = None,
594
         permission_prompting_enabled: bool | None = None,
@@ -594,6 +615,13 @@ class ConversationSession:
594
             self.active_dod_path = active_dod_path
615
             self.active_dod_path = active_dod_path
595
         if current_task is not None:
616
         if current_task is not None:
596
             self.current_task = current_task
617
             self.current_task = current_task
618
+        if runtime_owner_type is not None:
619
+            self.runtime_owner_type = normalize_runtime_owner_type(runtime_owner_type)
620
+        if runtime_owner_path is not None or runtime_owner_type is not None:
621
+            self.runtime_owner_path = normalize_runtime_owner_path(
622
+                runtime_owner_path,
623
+                owner_type=self.runtime_owner_type,
624
+            )
597
         if workflow_mode is not None:
625
         if workflow_mode is not None:
598
             self.workflow_mode = workflow_mode
626
             self.workflow_mode = workflow_mode
599
         if permission_mode is not None:
627
         if permission_mode is not None:
@@ -792,6 +820,8 @@ class ConversationSession:
792
             usage=dict(self.usage_totals),
820
             usage=dict(self.usage_totals),
793
             active_dod_path=self.active_dod_path,
821
             active_dod_path=self.active_dod_path,
794
             current_task=self.current_task,
822
             current_task=self.current_task,
823
+            runtime_owner_type=self.runtime_owner_type,
824
+            runtime_owner_path=self.runtime_owner_path,
795
             workflow_mode=self.workflow_mode,
825
             workflow_mode=self.workflow_mode,
796
             permission_mode=self.permission_mode,
826
             permission_mode=self.permission_mode,
797
             permission_prompting_enabled=self.permission_prompting_enabled,
827
             permission_prompting_enabled=self.permission_prompting_enabled,
@@ -855,6 +885,8 @@ class ConversationSession:
855
         instance.usage_totals = dict(snapshot.usage)
885
         instance.usage_totals = dict(snapshot.usage)
856
         instance.active_dod_path = snapshot.active_dod_path
886
         instance.active_dod_path = snapshot.active_dod_path
857
         instance.current_task = snapshot.current_task
887
         instance.current_task = snapshot.current_task
888
+        instance.runtime_owner_type = snapshot.runtime_owner_type
889
+        instance.runtime_owner_path = snapshot.runtime_owner_path
858
         instance.workflow_mode = snapshot.workflow_mode
890
         instance.workflow_mode = snapshot.workflow_mode
859
         instance.permission_mode = snapshot.permission_mode
891
         instance.permission_mode = snapshot.permission_mode
860
         instance.permission_prompting_enabled = snapshot.permission_prompting_enabled
892
         instance.permission_prompting_enabled = snapshot.permission_prompting_enabled
tests/test_runtime_bootstrap.pymodified
@@ -41,7 +41,10 @@ def test_build_runtime_context_uses_shared_bootstrap_contract(
41
     assert context.workflow_mode == agent.workflow_mode
41
     assert context.workflow_mode == agent.workflow_mode
42
     assert context.prompt_format == agent.prompt_format
42
     assert context.prompt_format == agent.prompt_format
43
     assert context.prompt_sections == agent.prompt_sections
43
     assert context.prompt_sections == agent.prompt_sections
44
-    assert source.metadata == {"owner_type": "Agent"}
44
+    assert source.metadata == {
45
+        "owner_type": "Agent",
46
+        "owner_path": "public-agent",
47
+    }
45
 
48
 
46
 
49
 
47
 def test_sync_runtime_context_refreshes_prompt_and_capability_state(
50
 def test_sync_runtime_context_refreshes_prompt_and_capability_state(
@@ -142,4 +145,7 @@ def test_build_runtime_launcher_wraps_shared_bootstrap_source(
142
     assert isinstance(launcher, RuntimeLauncher)
145
     assert isinstance(launcher, RuntimeLauncher)
143
     assert isinstance(launcher.source, RuntimeBootstrapView)
146
     assert isinstance(launcher.source, RuntimeBootstrapView)
144
     assert launcher.source is not agent
147
     assert launcher.source is not agent
145
-    assert launcher.source.metadata == {"owner_type": "Agent"}
148
+    assert launcher.source.metadata == {
149
+        "owner_type": "Agent",
150
+        "owner_path": "public-agent",
151
+    }
tests/test_runtime_handle.pymodified
@@ -30,7 +30,10 @@ def test_runtime_handle_builds_runtime_bootstrap_contract(
30
     assert isinstance(launcher, RuntimeLauncher)
30
     assert isinstance(launcher, RuntimeLauncher)
31
     assert isinstance(launcher.source, RuntimeBootstrapView)
31
     assert isinstance(launcher.source, RuntimeBootstrapView)
32
     assert launcher.source is not handle
32
     assert launcher.source is not handle
33
-    assert launcher.source.metadata == {"owner_type": "RuntimeHandle"}
33
+    assert launcher.source.metadata == {
34
+        "owner_type": "RuntimeHandle",
35
+        "owner_path": "runtime-handle",
36
+    }
34
     assert context.project_root == temp_dir.resolve()
37
     assert context.project_root == temp_dir.resolve()
35
     assert context.backend is handle.backend
38
     assert context.backend is handle.backend
36
     assert context.registry is handle.registry
39
     assert context.registry is handle.registry
@@ -63,7 +66,10 @@ async def test_runtime_handle_runs_conversation_runtime_without_agent(
63
     )
66
     )
64
 
67
 
65
     assert summary.final_response == "Runtime handle reply."
68
     assert summary.final_response == "Runtime handle reply."
66
-    assert runtime.source.metadata == {"owner_type": "RuntimeHandle"}
69
+    assert runtime.source.metadata == {
70
+        "owner_type": "RuntimeHandle",
71
+        "owner_path": "runtime-handle",
72
+    }
67
     assert any(event.type == "response" for event in events)
73
     assert any(event.type == "response" for event in events)
68
 
74
 
69
 
75
 
tests/test_runtime_launcher.pymodified
@@ -28,7 +28,10 @@ def test_build_runtime_launcher_returns_launcher_for_agent_source(
28
     assert isinstance(launcher, RuntimeLauncher)
28
     assert isinstance(launcher, RuntimeLauncher)
29
     assert isinstance(launcher.source, RuntimeBootstrapView)
29
     assert isinstance(launcher.source, RuntimeBootstrapView)
30
     assert launcher.source is not agent
30
     assert launcher.source is not agent
31
-    assert launcher.source.metadata == {"owner_type": "Agent"}
31
+    assert launcher.source.metadata == {
32
+        "owner_type": "Agent",
33
+        "owner_path": "public-agent",
34
+    }
32
 
35
 
33
 
36
 
34
 @pytest.mark.asyncio
37
 @pytest.mark.asyncio
tests/test_runtime_public_shell.pymodified
@@ -71,6 +71,8 @@ def test_create_runtime_session_copies_public_shell_state(temp_dir: Path) -> Non
71
         prompt_format="native",
71
         prompt_format="native",
72
         prompt_sections=["Runtime Config", "Workflow Context"],
72
         prompt_sections=["Runtime Config", "Workflow Context"],
73
         workflow_mode="execute",
73
         workflow_mode="execute",
74
+        runtime_owner_type="RuntimeHandle",
75
+        runtime_owner_path="runtime-handle",
74
         rotate_after_bytes=handle.config.session_rotate_after_bytes,
76
         rotate_after_bytes=handle.config.session_rotate_after_bytes,
75
         auto_compaction_input_tokens_threshold=(
77
         auto_compaction_input_tokens_threshold=(
76
             handle.config.session_auto_compaction_input_tokens_threshold
78
             handle.config.session_auto_compaction_input_tokens_threshold
@@ -83,6 +85,8 @@ def test_create_runtime_session_copies_public_shell_state(temp_dir: Path) -> Non
83
     assert session.permission_mode == handle.active_permission_mode
85
     assert session.permission_mode == handle.active_permission_mode
84
     assert session.permission_prompting_enabled is handle.permission_policy.prompting_enabled
86
     assert session.permission_prompting_enabled is handle.permission_policy.prompting_enabled
85
     assert session.permission_rule_counts == handle.permission_policy.rule_counts()
87
     assert session.permission_rule_counts == handle.permission_policy.rule_counts()
88
+    assert session.runtime_owner_type == "RuntimeHandle"
89
+    assert session.runtime_owner_path == "runtime-handle"
86
     assert session.prompt_format == "native"
90
     assert session.prompt_format == "native"
87
     assert session.prompt_sections == ["Runtime Config", "Workflow Context"]
91
     assert session.prompt_sections == ["Runtime Config", "Workflow Context"]
88
 
92
 
@@ -374,6 +378,8 @@ def test_create_runtime_session_install_builds_restored_shell_state(
374
         prompt_format="native",
378
         prompt_format="native",
375
         prompt_sections=["Runtime Config", "Workflow Context"],
379
         prompt_sections=["Runtime Config", "Workflow Context"],
376
         workflow_mode="execute",
380
         workflow_mode="execute",
381
+        runtime_owner_type="RuntimeHandle",
382
+        runtime_owner_path="runtime-handle",
377
         rotate_after_bytes=handle.config.session_rotate_after_bytes,
383
         rotate_after_bytes=handle.config.session_rotate_after_bytes,
378
         auto_compaction_input_tokens_threshold=(
384
         auto_compaction_input_tokens_threshold=(
379
             handle.config.session_auto_compaction_input_tokens_threshold
385
             handle.config.session_auto_compaction_input_tokens_threshold
@@ -402,6 +408,8 @@ def test_apply_runtime_session_install_updates_owner_shell_state(
402
         prompt_format="native",
408
         prompt_format="native",
403
         prompt_sections=["Runtime Config", "Workflow Context"],
409
         prompt_sections=["Runtime Config", "Workflow Context"],
404
         workflow_mode="plan",
410
         workflow_mode="plan",
411
+        runtime_owner_type="Agent",
412
+        runtime_owner_path="public-agent",
405
         rotate_after_bytes=handle.config.session_rotate_after_bytes,
413
         rotate_after_bytes=handle.config.session_rotate_after_bytes,
406
         auto_compaction_input_tokens_threshold=(
414
         auto_compaction_input_tokens_threshold=(
407
             handle.config.session_auto_compaction_input_tokens_threshold
415
             handle.config.session_auto_compaction_input_tokens_threshold
@@ -423,6 +431,8 @@ def test_apply_runtime_session_install_updates_owner_shell_state(
423
     assert handle.active_permission_mode == "prompt"
431
     assert handle.active_permission_mode == "prompt"
424
     assert handle.prompt_format == "native"
432
     assert handle.prompt_format == "native"
425
     assert handle.prompt_sections == ["Runtime Config", "Workflow Context"]
433
     assert handle.prompt_sections == ["Runtime Config", "Workflow Context"]
434
+    assert handle.session.runtime_owner_type == "RuntimeHandle"
435
+    assert handle.session.runtime_owner_path == "runtime-handle"
426
 
436
 
427
 
437
 
428
 def test_build_fresh_runtime_session_install_uses_current_owner_shell_state(
438
 def test_build_fresh_runtime_session_install_uses_current_owner_shell_state(
tests/test_session_state.pymodified
@@ -12,6 +12,7 @@ from loader.llm.base import CompletionResponse, Message, Role, ToolCall
12
 from loader.runtime.completion_trace import CompletionTraceEntry
12
 from loader.runtime.completion_trace import CompletionTraceEntry
13
 from loader.runtime.evidence_provenance import EvidenceProvenance
13
 from loader.runtime.evidence_provenance import EvidenceProvenance
14
 from loader.runtime.prompt_history import PromptSnapshot
14
 from loader.runtime.prompt_history import PromptSnapshot
15
+from loader.runtime.runtime_handle import RuntimeHandle
15
 from loader.runtime.session import ConversationSession
16
 from loader.runtime.session import ConversationSession
16
 from loader.runtime.workflow_ledger import WorkflowLedger, WorkflowLedgerItem
17
 from loader.runtime.workflow_ledger import WorkflowLedger, WorkflowLedgerItem
17
 from loader.runtime.workflow_policy import WorkflowTimelineEntry
18
 from loader.runtime.workflow_policy import WorkflowTimelineEntry
@@ -173,6 +174,7 @@ def test_session_persists_permission_policy_metadata(temp_dir: Path) -> None:
173
 
174
 
174
     session.update_runtime_state(
175
     session.update_runtime_state(
175
         current_task="Inspect permission history",
176
         current_task="Inspect permission history",
177
+        runtime_owner_type="RuntimeHandle",
176
         permission_mode="allow",
178
         permission_mode="allow",
177
         permission_prompting_enabled=True,
179
         permission_prompting_enabled=True,
178
         permission_rule_counts={"allow": 2, "deny": 1, "ask": 4},
180
         permission_rule_counts={"allow": 2, "deny": 1, "ask": 4},
@@ -237,6 +239,8 @@ def test_session_persists_permission_policy_metadata(temp_dir: Path) -> None:
237
     assert reloaded.permission_rules_source == str(
239
     assert reloaded.permission_rules_source == str(
238
         temp_dir / ".loader" / "permission-rules.json"
240
         temp_dir / ".loader" / "permission-rules.json"
239
     )
241
     )
242
+    assert reloaded.runtime_owner_type == "RuntimeHandle"
243
+    assert reloaded.runtime_owner_path == "runtime-handle"
240
     assert reloaded.prompt_format == "native"
244
     assert reloaded.prompt_format == "native"
241
     assert reloaded.prompt_sections == [
245
     assert reloaded.prompt_sections == [
242
         "Runtime Config",
246
         "Runtime Config",
@@ -274,6 +278,35 @@ def test_session_persists_permission_policy_metadata(temp_dir: Path) -> None:
274
     ]
278
     ]
275
 
279
 
276
 
280
 
281
+def test_resume_session_updates_runtime_owner_metadata(temp_dir: Path) -> None:
282
+    agent = Agent(
283
+        backend=ScriptedBackend(),
284
+        config=AgentConfig(auto_context=False, stream=False),
285
+        project_root=temp_dir,
286
+    )
287
+    agent.session.persist()
288
+    session_id = agent.session.session_id
289
+
290
+    handle = RuntimeHandle(
291
+        backend=ScriptedBackend(),
292
+        config=AgentConfig(auto_context=False, stream=False),
293
+        project_root=temp_dir,
294
+    )
295
+
296
+    assert handle.resume_session(session_id) is True
297
+
298
+    reloaded = ConversationSession.load(
299
+        project_root=temp_dir,
300
+        system_message_factory=_dummy_system,
301
+        few_shot_factory=_dummy_few_shots,
302
+        session_id=session_id,
303
+    )
304
+
305
+    assert reloaded is not None
306
+    assert reloaded.runtime_owner_type == "RuntimeHandle"
307
+    assert reloaded.runtime_owner_path == "runtime-handle"
308
+
309
+
277
 def test_session_prefers_canonical_workflow_timeline_for_completion_trace(
310
 def test_session_prefers_canonical_workflow_timeline_for_completion_trace(
278
     temp_dir: Path,
311
     temp_dir: Path,
279
 ) -> None:
312
 ) -> None: