diff mrjunejune/inference/copilot_sidecar.py @ 281:c57149ad216e default tip

Copilot-Session: f68442b1-fa8f-46a0-9689-81710613bbd4
author MrJuneJune <me@mrjunejune.com>
date Tue, 18 Aug 2026 22:18:15 -0700
parents 49e9e591c9bb
children
line wrap: on
line diff
--- a/mrjunejune/inference/copilot_sidecar.py	Tue Aug 18 19:14:53 2026 -0700
+++ b/mrjunejune/inference/copilot_sidecar.py	Tue Aug 18 22:18:15 2026 -0700
@@ -34,12 +34,34 @@
 The user message includes only the visible canvas context plus one new thought.
 Treat visible conversation entities as the only append candidates. Append when
 the new thought clearly continues one of them; otherwise create a new
-conversation. Never append to a conversation ID absent from visible context.
+conversation only when the request benefits from retained discussion. Never
+append to a conversation ID absent from visible context.
 Return only one compact JSON object with these fields:
 - action: "create" or "append"
+- presentation: "action" for a one-off canvas action, or "conversation" for
+  retained discussion
 - conversation_id: an existing numeric conversation ID for append, otherwise 0
 - title: a short session title
 - response: the direct response that should appear on the canvas
+- entities: an array of zero to four useful visual entities to create
+Each entities item may contain:
+- kind: one of "text", "button", "text_area", "accordion", "card",
+  "calendar", "table", "notification", "scroll_area", "image", or
+  "web_content"
+- label and text: short display strings; image/web_content text must be an
+  absolute http:// or https:// URL
+- x and y: optional world coordinates; omit both to let the canvas place it
+- width and height: optional dimensions; omit both for the native default
+- value: optional integer state; for a calendar it is the 1-based selected
+  day of the month
+Create entities only when they materially demonstrate or organize the answer.
+Never create more than four. Do not create conversation entities through this
+array; action and conversation_id own the conversation lifecycle.
+For a simple one-off task, use presentation "action", action "create",
+conversation_id 0, and at least one useful entity. Handle it directly in this
+orchestrator: do not create or delegate to another agent, worker, or session.
+For a complex answer that should remain discussable, use presentation
+"conversation". Append always requires presentation "conversation".
 Do not use Markdown fences, tools, or text outside the JSON object.
 """
 
@@ -305,6 +327,38 @@
         }
         await self._send("ready", None, None, status="ok", profiles=profile_meta)
 
+    def _resume_marker(self, session_id: str) -> str:
+        return os.path.join(
+            self._config.base_directory,
+            ".session-markers",
+            session_id,
+        )
+
+    def _should_attempt_resume(self, session_id: str) -> bool:
+        if not os.path.isdir(self._config.base_directory):
+            return True
+        return os.path.isfile(self._resume_marker(session_id))
+
+    def _mark_session_created(self, session_id: str) -> None:
+        marker = self._resume_marker(session_id)
+        try:
+            os.makedirs(os.path.dirname(marker), mode=0o700, exist_ok=True)
+            with open(marker, "wb"):
+                pass
+        except OSError as error:
+            print(
+                f"unable to write Copilot session marker: {error}",
+                file=sys.stderr,
+            )
+
+    def _remove_resume_marker(self, session_id: str) -> None:
+        try:
+            os.remove(self._resume_marker(session_id))
+        except FileNotFoundError:
+            pass
+        except OSError:
+            pass
+
     async def dispatch(self, command: JsonObject) -> None:
         command_name = command.get("command")
         request_id = command.get("request_id")
@@ -582,12 +636,12 @@
 
             base_options = self._session_options(compiled)
             session: Any = None
-            if resume_existing:
+            if resume_existing and self._should_attempt_resume(derived_id):
                 # Resume persisted user-facing sessions, then create if absent.
                 try:
                     session = await self._client.resume_session(derived_id, **base_options)
                 except Exception:
-                    pass
+                    self._remove_resume_marker(derived_id)
 
             if session is None:
                 # Fresh create — inject bounded transcript context into system message.
@@ -599,6 +653,7 @@
                 session = await self._client.create_session(
                     session_id=derived_id, **create_options
                 )
+                self._mark_session_created(derived_id)
 
             def handle_event(event: Any) -> None:
                 task = asyncio.create_task(self._handle_event(conversation_id, event))
@@ -758,6 +813,7 @@
                 conversation.unsubscribe()
                 await conversation.session.disconnect()
                 await self._client.delete_session(conversation.session.session_id)
+                self._remove_resume_marker(conversation.session.session_id)
                 deleted_ids.add(conversation.session.session_id)
             for compiled in self._compiled.values():
                 sdk_session_id = _derive_sdk_session_id(conversation_id, compiled)
@@ -768,6 +824,7 @@
                 except Exception:
                     # A profile-specific persisted session may never have existed.
                     pass
+                self._remove_resume_marker(sdk_session_id)
             await self._send(
                 "turn.done", request_id, conversation_id, action="conversation.delete"
             )