fix(ui): stabilize company chat result topology
This commit is contained in:
+65
-2
@@ -144,6 +144,7 @@ from opc.layer2_organization.work_item_identity import (
|
||||
projection_id_for_task,
|
||||
projection_id_for_work_item,
|
||||
rework_projection_id_for_gate,
|
||||
result_delivery_identity_payload_for_task,
|
||||
turn_type_for_task,
|
||||
turn_type_for_work_item,
|
||||
work_item_identity_payload,
|
||||
@@ -9076,6 +9077,48 @@ class OPCEngine:
|
||||
in {"suspending", "suspended"}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _ensure_result_delivery_identity_for_commit(
|
||||
task: Task,
|
||||
result: TaskResult,
|
||||
) -> dict[str, str]:
|
||||
"""Reuse the runtime's immutable delivery id or mint one once.
|
||||
|
||||
A runtime-generated canonical turn is not guaranteed to be copied back
|
||||
into mutable Task metadata. The TaskResult artifact is therefore the
|
||||
hand-off boundary between runtime persistence and the engine mirrors.
|
||||
External/pause results without a canonical turn receive a per-result
|
||||
execution seed here. Runtime/provider *session* ids are deliberately
|
||||
excluded because those sessions survive resume and can produce several
|
||||
legitimate deliveries for the same task and retry count.
|
||||
|
||||
The generated identity is written back to ``TaskResult.artifacts``
|
||||
before the task result is persisted. Replaying that durable result is
|
||||
therefore stable instead of minting a second UI row after a restart.
|
||||
"""
|
||||
artifacts = dict(result.artifacts or {})
|
||||
canonical_turn_id = str(
|
||||
artifacts.get("canonical_turn_id")
|
||||
or artifacts.get("conversation_turn_id")
|
||||
or ""
|
||||
).strip()
|
||||
persisted_delivery_id = str(artifacts.get("result_delivery_id") or "").strip()
|
||||
execution_id = str(artifacts.get("result_execution_id") or "").strip()
|
||||
if not persisted_delivery_id and not canonical_turn_id and not execution_id:
|
||||
execution_id = uuid.uuid4().hex
|
||||
identity = result_delivery_identity_payload_for_task(
|
||||
task,
|
||||
canonical_turn_id=canonical_turn_id,
|
||||
execution_id=execution_id,
|
||||
result_delivery_id=persisted_delivery_id,
|
||||
)
|
||||
result.artifacts = {
|
||||
**artifacts,
|
||||
**({"result_execution_id": execution_id} if execution_id else {}),
|
||||
**identity,
|
||||
}
|
||||
return identity
|
||||
|
||||
async def _execute_registered_task_attempt(self, task: Task) -> TaskResult:
|
||||
try:
|
||||
result = await self._run_task_once(task)
|
||||
@@ -9115,6 +9158,7 @@ class OPCEngine:
|
||||
"company runtime was suspended before result commit"
|
||||
)
|
||||
self._apply_runtime_state_to_task(task, result)
|
||||
result_identity = self._ensure_result_delivery_identity_for_commit(task, result)
|
||||
task.status = result.status
|
||||
task.result = {"content": result.content, "artifacts": result.artifacts}
|
||||
await self.store.save_task(task)
|
||||
@@ -9131,6 +9175,8 @@ class OPCEngine:
|
||||
"status": result.status.value,
|
||||
"employee_id": str(assignment.get("employee_id", "")).strip(),
|
||||
"role_id": str(assignment.get("role_id") or task.assigned_to or "").strip(),
|
||||
"child_session_id": str(task.session_id),
|
||||
**result_identity,
|
||||
**work_item_identity_payload_for_task(task),
|
||||
},
|
||||
)
|
||||
@@ -9141,7 +9187,7 @@ class OPCEngine:
|
||||
and task.session_id != task.parent_session_id
|
||||
):
|
||||
assignment = dict(task.metadata.get("employee_assignment", {}) or {})
|
||||
await self.memory.record_assistant_turn(
|
||||
child_result_message = await self.memory.record_assistant_turn(
|
||||
session_id=task.session_id,
|
||||
content=result.content,
|
||||
project_id=task.project_id,
|
||||
@@ -9152,6 +9198,8 @@ class OPCEngine:
|
||||
"status": result.status.value,
|
||||
"employee_id": str(assignment.get("employee_id", "")).strip(),
|
||||
"role_id": str(assignment.get("role_id") or task.assigned_to or "").strip(),
|
||||
"child_session_id": str(task.session_id),
|
||||
**result_identity,
|
||||
**work_item_identity_payload_for_task(task),
|
||||
},
|
||||
)
|
||||
@@ -9161,6 +9209,11 @@ class OPCEngine:
|
||||
task=task,
|
||||
result_content=result.content,
|
||||
artifacts=result.artifacts,
|
||||
result_delivery_id=result_identity.get("result_delivery_id", ""),
|
||||
source_result_message_id=str(
|
||||
getattr(child_result_message, "message_id", "") or ""
|
||||
),
|
||||
canonical_turn_id=result_identity.get("canonical_turn_id", ""),
|
||||
)
|
||||
if result.status == TaskStatus.DONE:
|
||||
await self._record_task_mode_external_result_reply(task, result.content)
|
||||
@@ -9197,6 +9250,7 @@ class OPCEngine:
|
||||
"company runtime was suspended before retry result commit"
|
||||
)
|
||||
self._apply_runtime_state_to_task(task, result)
|
||||
result_identity = self._ensure_result_delivery_identity_for_commit(task, result)
|
||||
task.status = result.status
|
||||
task.result = {"content": result.content, "artifacts": result.artifacts}
|
||||
await self.store.save_task(task)
|
||||
@@ -9214,6 +9268,8 @@ class OPCEngine:
|
||||
"retry_count": task.retry_count,
|
||||
"employee_id": str(assignment.get("employee_id", "")).strip(),
|
||||
"role_id": str(assignment.get("role_id") or task.assigned_to or "").strip(),
|
||||
"child_session_id": str(task.session_id),
|
||||
**result_identity,
|
||||
**work_item_identity_payload_for_task(task),
|
||||
},
|
||||
)
|
||||
@@ -9224,7 +9280,7 @@ class OPCEngine:
|
||||
and task.session_id != task.parent_session_id
|
||||
):
|
||||
assignment = dict(task.metadata.get("employee_assignment", {}) or {})
|
||||
await self.memory.record_assistant_turn(
|
||||
child_result_message = await self.memory.record_assistant_turn(
|
||||
session_id=task.session_id,
|
||||
content=result.content,
|
||||
project_id=task.project_id,
|
||||
@@ -9236,6 +9292,8 @@ class OPCEngine:
|
||||
"retry_count": task.retry_count,
|
||||
"employee_id": str(assignment.get("employee_id", "")).strip(),
|
||||
"role_id": str(assignment.get("role_id") or task.assigned_to or "").strip(),
|
||||
"child_session_id": str(task.session_id),
|
||||
**result_identity,
|
||||
**work_item_identity_payload_for_task(task),
|
||||
},
|
||||
)
|
||||
@@ -9245,6 +9303,11 @@ class OPCEngine:
|
||||
task=task,
|
||||
result_content=result.content,
|
||||
artifacts=result.artifacts,
|
||||
result_delivery_id=result_identity.get("result_delivery_id", ""),
|
||||
source_result_message_id=str(
|
||||
getattr(child_result_message, "message_id", "") or ""
|
||||
),
|
||||
canonical_turn_id=result_identity.get("canonical_turn_id", ""),
|
||||
)
|
||||
if result.status == TaskStatus.DONE:
|
||||
await self._record_task_mode_external_result_reply(task, result.content)
|
||||
|
||||
@@ -7,6 +7,8 @@ from typing import Any, Mapping
|
||||
|
||||
WORK_ITEM_PROJECTION_ID_KEY = "work_item_projection_id"
|
||||
WORK_ITEM_TURN_TYPE_KEY = "work_item_turn_type"
|
||||
RESULT_DELIVERY_ID_KEY = "result_delivery_id"
|
||||
SOURCE_TASK_ID_KEY = "source_task_id"
|
||||
GATE_REWORK_PROJECTION_ID_KEY = "rework_projection_id"
|
||||
GATE_TARGET_PROJECTION_ID_KEY = "target_projection_id"
|
||||
GATE_TARGET_PROJECTION_IDS_KEY = "target_projection_ids"
|
||||
@@ -259,6 +261,94 @@ def work_item_identity_payload_for_task(
|
||||
)
|
||||
|
||||
|
||||
def canonical_result_turn_id_for_task(
|
||||
task: Any,
|
||||
*,
|
||||
canonical_turn_id: str = "",
|
||||
) -> str:
|
||||
"""Return the canonical conversation turn owning a task result.
|
||||
|
||||
Runtime events also carry iteration-scoped ``turn_id`` values. Result
|
||||
surfaces must never use those values as their logical delivery identity,
|
||||
so this helper only falls back to the task's canonical runtime fields.
|
||||
"""
|
||||
explicit = _clean(canonical_turn_id)
|
||||
if explicit:
|
||||
return explicit
|
||||
metadata = dict(getattr(task, "metadata", {}) or {}) if task is not None else {}
|
||||
runtime_metadata = dict(metadata.get("runtime_v2", {}) or {})
|
||||
for source in (metadata, runtime_metadata):
|
||||
for key in (
|
||||
"canonical_turn_id",
|
||||
"conversation_turn_id",
|
||||
"current_turn_id",
|
||||
"runtime_v2_current_turn_id",
|
||||
):
|
||||
value = _clean(source.get(key))
|
||||
if value:
|
||||
return value
|
||||
return ""
|
||||
|
||||
|
||||
def result_delivery_id_for_task(
|
||||
task: Any,
|
||||
*,
|
||||
canonical_turn_id: str = "",
|
||||
execution_id: str = "",
|
||||
result_delivery_id: str = "",
|
||||
) -> str:
|
||||
"""Build one stable identity shared by every surface of a task result.
|
||||
|
||||
The attempt suffix keeps a failed result and its retry distinct while the
|
||||
canonical turn (when available) links the runtime final, child result and
|
||||
parent mirror without inspecting their display text.
|
||||
"""
|
||||
explicit = _clean(result_delivery_id)
|
||||
if explicit:
|
||||
return explicit
|
||||
task_id = _clean(getattr(task, "id", ""))
|
||||
turn_id = canonical_result_turn_id_for_task(
|
||||
task,
|
||||
canonical_turn_id=canonical_turn_id,
|
||||
)
|
||||
execution = _clean(execution_id)
|
||||
if not task_id or (not turn_id and not execution):
|
||||
return ""
|
||||
try:
|
||||
attempt = max(0, int(getattr(task, "retry_count", 0) or 0))
|
||||
except (TypeError, ValueError):
|
||||
attempt = 0
|
||||
identity_kind = "turn" if turn_id else "execution"
|
||||
identity_value = turn_id or execution
|
||||
return f"result:task:{task_id}:{identity_kind}:{identity_value}:attempt:{attempt}"
|
||||
|
||||
|
||||
def result_delivery_identity_payload_for_task(
|
||||
task: Any,
|
||||
*,
|
||||
canonical_turn_id: str = "",
|
||||
execution_id: str = "",
|
||||
result_delivery_id: str = "",
|
||||
) -> dict[str, str]:
|
||||
"""Return structured lineage shared by persisted result projections."""
|
||||
delivery_id = result_delivery_id_for_task(
|
||||
task,
|
||||
canonical_turn_id=canonical_turn_id,
|
||||
execution_id=execution_id,
|
||||
result_delivery_id=result_delivery_id,
|
||||
)
|
||||
source_task_id = _clean(getattr(task, "id", ""))
|
||||
canonical_id = canonical_result_turn_id_for_task(
|
||||
task,
|
||||
canonical_turn_id=canonical_turn_id,
|
||||
)
|
||||
return {
|
||||
**({RESULT_DELIVERY_ID_KEY: delivery_id} if delivery_id else {}),
|
||||
**({SOURCE_TASK_ID_KEY: source_task_id} if source_task_id else {}),
|
||||
**({"canonical_turn_id": canonical_id} if canonical_id else {}),
|
||||
}
|
||||
|
||||
|
||||
def migrate_work_item_projection_metadata(
|
||||
metadata: Mapping[str, Any] | None,
|
||||
*,
|
||||
|
||||
@@ -19,6 +19,7 @@ from opc.core.models import OPCEvent, PermissionResolution, Task, TaskResult, Ta
|
||||
from opc.layer2_organization.collaboration_policy import ownership_guard_violation
|
||||
from opc.layer2_organization.work_item_identity import (
|
||||
projection_id_for_task,
|
||||
result_delivery_identity_payload_for_task,
|
||||
turn_type_for_task,
|
||||
work_item_identity_payload_for_task,
|
||||
)
|
||||
@@ -703,6 +704,14 @@ class NativeRuntimeV2:
|
||||
runtime_notes=runtime_notes,
|
||||
)
|
||||
if verification_gate is not None:
|
||||
verification_gate.artifacts = {
|
||||
**dict(verification_gate.artifacts or {}),
|
||||
"runtime_session_id": runtime_session_id,
|
||||
**result_delivery_identity_payload_for_task(
|
||||
task,
|
||||
canonical_turn_id=conversation_turn_id,
|
||||
),
|
||||
}
|
||||
await self._save_runtime_session(
|
||||
runtime_session_id,
|
||||
task,
|
||||
@@ -728,6 +737,10 @@ class NativeRuntimeV2:
|
||||
artifacts = {
|
||||
**aggregated_artifacts,
|
||||
"runtime_session_id": runtime_session_id,
|
||||
**result_delivery_identity_payload_for_task(
|
||||
task,
|
||||
canonical_turn_id=conversation_turn_id,
|
||||
),
|
||||
"permission_requests": self._permission_requests_from_results([]),
|
||||
"active_subagents": active_subagents,
|
||||
"compaction_boundaries": list(compaction_boundaries),
|
||||
@@ -2799,6 +2812,16 @@ class NativeRuntimeV2:
|
||||
"runtime_session_id": runtime_session_id,
|
||||
"source_kind": source_kind,
|
||||
}
|
||||
if not tool_calls:
|
||||
metadata.update(
|
||||
result_delivery_identity_payload_for_task(
|
||||
task,
|
||||
canonical_turn_id=canonical_turn_id,
|
||||
)
|
||||
)
|
||||
metadata.update(work_item_identity_payload_for_task(task))
|
||||
if task.session_id:
|
||||
metadata["child_session_id"] = str(task.session_id)
|
||||
if is_company_mode:
|
||||
metadata["execution_mode"] = "company_mode"
|
||||
metadata["company_runtime_raw_turn"] = True
|
||||
|
||||
@@ -713,6 +713,9 @@ class MemoryManager:
|
||||
task: Any,
|
||||
result_content: str,
|
||||
artifacts: dict[str, Any] | None = None,
|
||||
result_delivery_id: str = "",
|
||||
source_result_message_id: str = "",
|
||||
canonical_turn_id: str = "",
|
||||
) -> None:
|
||||
if not self.store:
|
||||
return
|
||||
@@ -728,9 +731,13 @@ class MemoryManager:
|
||||
metadata={
|
||||
"kind": "child_result",
|
||||
"child_session_id": child_session_id,
|
||||
"source_task_id": str(getattr(task, "id", "") or ""),
|
||||
"task_title": getattr(task, "title", ""),
|
||||
"employee_id": str(assignment.get("employee_id", "")).strip(),
|
||||
"role_id": str(assignment.get("role_id") or getattr(task, "assigned_to", "") or "").strip(),
|
||||
**({"result_delivery_id": str(result_delivery_id).strip()} if str(result_delivery_id).strip() else {}),
|
||||
**({"source_result_message_id": str(source_result_message_id).strip()} if str(source_result_message_id).strip() else {}),
|
||||
**({"canonical_turn_id": str(canonical_turn_id).strip()} if str(canonical_turn_id).strip() else {}),
|
||||
**work_item_identity_payload_for_task(task),
|
||||
},
|
||||
)
|
||||
@@ -747,6 +754,8 @@ class MemoryManager:
|
||||
"agent_id": getattr(task, "assigned_to", ""),
|
||||
"summary": summary,
|
||||
"artifacts": self._compact_artifacts(artifacts or {}),
|
||||
**({"result_delivery_id": str(result_delivery_id).strip()} if str(result_delivery_id).strip() else {}),
|
||||
**({"source_result_message_id": str(source_result_message_id).strip()} if str(source_result_message_id).strip() else {}),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -575,23 +575,43 @@ class ChatStore:
|
||||
@staticmethod
|
||||
def _strip_narrative_title_prefix(content: str) -> str:
|
||||
trimmed = str(content or "").strip()
|
||||
markdown_title = re.match(r"^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$", trimmed)
|
||||
if markdown_title:
|
||||
# Only discard an explicit, anchored narrative wrapper. The former
|
||||
# fallback used the first ``": "`` anywhere in the first 160
|
||||
# characters, so ordinary Markdown such as ``**Work Item 1: ...**``
|
||||
# was progressively truncated every time a sync was merged. Peeling
|
||||
# explicit nested wrappers to a fixed point keeps this comparison
|
||||
# helper idempotent without interpreting body punctuation as structure.
|
||||
while True:
|
||||
markdown_title = re.match(r"^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$", trimmed)
|
||||
if not markdown_title:
|
||||
return trimmed
|
||||
body = markdown_title.group(2).strip()
|
||||
if len(body) >= 80:
|
||||
return body
|
||||
colon_index = trimmed.find(": ")
|
||||
if colon_index < 8 or colon_index > 160:
|
||||
return trimmed
|
||||
prefix = trimmed[:colon_index].replace("*", "").strip()
|
||||
body = trimmed[colon_index + 2 :].strip()
|
||||
if len(body) < 80:
|
||||
return trimmed
|
||||
if not re.search(r"[A-Za-z\u4e00-\u9fff]", prefix):
|
||||
return trimmed
|
||||
if re.match(r"^(https?|file)$", prefix, flags=re.IGNORECASE):
|
||||
return trimmed
|
||||
return body
|
||||
if len(body) < 80 or body == trimmed:
|
||||
return trimmed
|
||||
trimmed = body
|
||||
|
||||
@classmethod
|
||||
def _select_duplicate_display_content(
|
||||
cls,
|
||||
preferred: dict[str, Any],
|
||||
secondary: dict[str, Any],
|
||||
) -> str:
|
||||
"""Choose an exact input body; never persist the comparison key itself."""
|
||||
preferred_content = str(preferred.get("content", "") or "")
|
||||
secondary_content = str(secondary.get("content", "") or "")
|
||||
preferred_key = cls._normalize_duplicate_content(preferred_content)
|
||||
if not preferred_key or preferred_key != cls._normalize_duplicate_content(secondary_content):
|
||||
return preferred_content
|
||||
# If one real surface is already the comparison body while the other
|
||||
# carries an explicit narrative wrapper/footer, reuse that real body.
|
||||
# This preserves the historical visible answer without manufacturing
|
||||
# display text from a lossy canonicalization function.
|
||||
if (
|
||||
secondary_content.strip() == preferred_key
|
||||
and preferred_content.strip() != preferred_key
|
||||
):
|
||||
return secondary_content
|
||||
return preferred_content
|
||||
|
||||
@staticmethod
|
||||
def _message_timestamp(message: dict[str, Any]) -> float:
|
||||
@@ -663,6 +683,9 @@ class ChatStore:
|
||||
normalized = str(value or "").strip()
|
||||
if normalized:
|
||||
keys.add(normalized)
|
||||
result_delivery_id = str(metadata.get("result_delivery_id", "") or "").strip()
|
||||
if result_delivery_id:
|
||||
keys.add(f"result_delivery:{result_delivery_id}")
|
||||
return keys
|
||||
|
||||
@classmethod
|
||||
@@ -681,10 +704,15 @@ class ChatStore:
|
||||
cls,
|
||||
existing: dict[str, Any],
|
||||
candidate: dict[str, Any],
|
||||
*,
|
||||
prefer_candidate: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
preferred = existing
|
||||
secondary = candidate
|
||||
if cls._message_preference_score(candidate) > cls._message_preference_score(existing):
|
||||
if (
|
||||
prefer_candidate
|
||||
or cls._message_preference_score(candidate) > cls._message_preference_score(existing)
|
||||
):
|
||||
preferred = candidate
|
||||
secondary = existing
|
||||
|
||||
@@ -694,12 +722,7 @@ class ChatStore:
|
||||
secondary_meta = dict(secondary.get("metadata", {}) or {})
|
||||
preferred_meta = dict(preferred.get("metadata", {}) or {})
|
||||
merged["metadata"] = {**secondary_meta, **preferred_meta}
|
||||
normalized_content = cls._normalize_duplicate_content(preferred.get("content", ""))
|
||||
if (
|
||||
normalized_content
|
||||
and normalized_content == cls._normalize_duplicate_content(secondary.get("content", ""))
|
||||
):
|
||||
merged["content"] = normalized_content
|
||||
merged["content"] = cls._select_duplicate_display_content(preferred, secondary)
|
||||
|
||||
shared_ids = cls._message_identity_keys(existing) & cls._message_identity_keys(candidate)
|
||||
canonical_id = ""
|
||||
@@ -742,7 +765,10 @@ class ChatStore:
|
||||
return (
|
||||
str(existing.get("sender", "") or "") == str(candidate.get("sender", "") or "")
|
||||
and str(existing.get("sender_name", "") or "") == str(candidate.get("sender_name", "") or "")
|
||||
and cls._normalize_duplicate_content(existing.get("content", "")) == cls._normalize_duplicate_content(candidate.get("content", ""))
|
||||
# Display content is persisted data, not a duplicate-comparison
|
||||
# key. Exact comparison lets authoritative transcript backfill
|
||||
# repair a legacy row whose body was destructively normalized.
|
||||
and str(existing.get("content", "") or "") == str(candidate.get("content", "") or "")
|
||||
and cls._message_timestamp(existing) == cls._message_timestamp(candidate)
|
||||
and str(existing.get("reply_to_id", "") or "") == str(candidate.get("reply_to_id", "") or "")
|
||||
and list(existing.get("mentions", []) or []) == list(candidate.get("mentions", []) or [])
|
||||
@@ -791,6 +817,55 @@ class ChatStore:
|
||||
return None
|
||||
return await self._message_scope(str(message_id).strip())
|
||||
|
||||
async def _persist_merged_message(
|
||||
self,
|
||||
existing: dict[str, Any],
|
||||
merged: dict[str, Any],
|
||||
*,
|
||||
channel_id: str,
|
||||
project_id: str,
|
||||
preserve_timestamp: bool = False,
|
||||
) -> tuple[dict[str, Any], bool]:
|
||||
"""Persist a duplicate merge while retaining the mounted cache row.
|
||||
|
||||
Semantic transcript duplicates can have a different source message id.
|
||||
Keeping the existing cache identity (and, for semantic replacement, its
|
||||
timestamp) upgrades the row in place instead of moving it in the UI.
|
||||
"""
|
||||
persisted_id = str(existing.get("message_id", "") or existing.get("id", "") or "")
|
||||
existing_timestamp = self._message_timestamp(existing)
|
||||
merged_timestamp = (
|
||||
existing_timestamp
|
||||
if preserve_timestamp and existing_timestamp
|
||||
else self._message_timestamp(merged) or time.time()
|
||||
)
|
||||
persisted = {
|
||||
**merged,
|
||||
"message_id": persisted_id,
|
||||
"channel_id": channel_id,
|
||||
"timestamp": merged_timestamp,
|
||||
"created_at": merged_timestamp,
|
||||
}
|
||||
if self._message_persisted_equal(existing, persisted):
|
||||
return persisted, False
|
||||
await self._db.execute(
|
||||
"UPDATE messages SET sender = ?, sender_name = ?, content = ?, timestamp = ?, "
|
||||
"reply_to_id = ?, mentions = ?, metadata = ? WHERE message_id = ? AND channel_id = ? AND project_id = ?",
|
||||
(
|
||||
persisted["sender"],
|
||||
persisted["sender_name"],
|
||||
persisted["content"],
|
||||
merged_timestamp,
|
||||
persisted.get("reply_to_id"),
|
||||
json.dumps(persisted.get("mentions", [])),
|
||||
json.dumps(persisted.get("metadata", {})),
|
||||
persisted_id,
|
||||
channel_id,
|
||||
project_id,
|
||||
),
|
||||
)
|
||||
return persisted, True
|
||||
|
||||
async def _merge_into_same_scope_row(
|
||||
self,
|
||||
message_id: str,
|
||||
@@ -798,13 +873,14 @@ class ChatStore:
|
||||
channel_id: str,
|
||||
project_id: str,
|
||||
candidate: dict[str, Any],
|
||||
) -> dict[str, Any] | None:
|
||||
prefer_candidate: bool = False,
|
||||
) -> tuple[dict[str, Any], bool] | None:
|
||||
"""Merge ``candidate`` into an already-persisted row with the same id/scope.
|
||||
|
||||
Returns the merged row when the update happened (or nothing changed), or
|
||||
None when the row could not be loaded. Backfill and the live insert path
|
||||
can race on the same message id; the duplicate must merge in place, never
|
||||
be re-inserted under a scoped alias id in the same channel.
|
||||
Returns ``(merged row, materially changed)`` or ``None`` when the row
|
||||
could not be loaded. Backfill and the live insert path can race on the
|
||||
same message id; the duplicate must merge in place, never be re-inserted
|
||||
under a scoped alias id in the same channel.
|
||||
"""
|
||||
cursor = await self._db.execute(
|
||||
"SELECT message_id, channel_id, sender, sender_name, content, "
|
||||
@@ -816,29 +892,17 @@ class ChatStore:
|
||||
if row is None:
|
||||
return None
|
||||
existing = self._row_to_message_dict(row)
|
||||
merged = self._merge_duplicate_messages(existing, candidate)
|
||||
if self._message_persisted_equal(existing, merged):
|
||||
return merged
|
||||
merged_timestamp = self._message_timestamp(merged) or time.time()
|
||||
await self._db.execute(
|
||||
"UPDATE messages SET sender = ?, sender_name = ?, content = ?, timestamp = ?, "
|
||||
"reply_to_id = ?, mentions = ?, metadata = ? WHERE message_id = ? AND channel_id = ? AND project_id = ?",
|
||||
(
|
||||
merged["sender"],
|
||||
merged["sender_name"],
|
||||
merged["content"],
|
||||
merged_timestamp,
|
||||
merged.get("reply_to_id"),
|
||||
json.dumps(merged.get("mentions", [])),
|
||||
json.dumps(merged.get("metadata", {})),
|
||||
message_id,
|
||||
channel_id,
|
||||
project_id,
|
||||
),
|
||||
merged = self._merge_duplicate_messages(
|
||||
existing,
|
||||
candidate,
|
||||
prefer_candidate=prefer_candidate,
|
||||
)
|
||||
return await self._persist_merged_message(
|
||||
existing,
|
||||
merged,
|
||||
channel_id=channel_id,
|
||||
project_id=project_id,
|
||||
)
|
||||
merged["timestamp"] = merged_timestamp
|
||||
merged["created_at"] = merged_timestamp
|
||||
return merged
|
||||
|
||||
async def _allocate_scoped_message_id(
|
||||
self,
|
||||
@@ -1581,35 +1645,24 @@ class ChatStore:
|
||||
existing_index = existing_positions.get(mid)
|
||||
if existing_index is not None:
|
||||
existing_match = existing_messages[existing_index]
|
||||
merged_existing = self._merge_duplicate_messages(existing_match, normalized_message)
|
||||
if not self._message_persisted_equal(existing_match, merged_existing):
|
||||
merged_timestamp = self._message_timestamp(merged_existing) or time.time()
|
||||
await self._db.execute(
|
||||
"UPDATE messages SET sender = ?, sender_name = ?, content = ?, timestamp = ?, "
|
||||
"reply_to_id = ?, mentions = ?, metadata = ? WHERE message_id = ? AND channel_id = ? AND project_id = ?",
|
||||
(
|
||||
merged_existing["sender"],
|
||||
merged_existing["sender_name"],
|
||||
merged_existing["content"],
|
||||
merged_timestamp,
|
||||
merged_existing.get("reply_to_id"),
|
||||
json.dumps(merged_existing.get("mentions", [])),
|
||||
json.dumps(merged_existing.get("metadata", {})),
|
||||
mid,
|
||||
channel_id,
|
||||
project_id,
|
||||
),
|
||||
)
|
||||
semantic_index.replace(existing_index, {
|
||||
**merged_existing,
|
||||
"created_at": merged_timestamp,
|
||||
})
|
||||
inserted_messages.append({
|
||||
**merged_existing,
|
||||
"channel_id": channel_id,
|
||||
"timestamp": merged_timestamp,
|
||||
"created_at": merged_timestamp,
|
||||
})
|
||||
# This candidate came directly from the durable transcript.
|
||||
# For the same identity its original display body is
|
||||
# authoritative, while metadata unique to the cache is
|
||||
# still retained by the merge.
|
||||
merged_existing = self._merge_duplicate_messages(
|
||||
existing_match,
|
||||
normalized_message,
|
||||
prefer_candidate=True,
|
||||
)
|
||||
persisted, did_change = await self._persist_merged_message(
|
||||
existing_match,
|
||||
merged_existing,
|
||||
channel_id=channel_id,
|
||||
project_id=project_id,
|
||||
)
|
||||
if did_change:
|
||||
semantic_index.replace(existing_index, persisted)
|
||||
inserted_messages.append(persisted)
|
||||
changed_existing = True
|
||||
continue
|
||||
|
||||
@@ -1623,10 +1676,15 @@ class ChatStore:
|
||||
channel_id=channel_id,
|
||||
project_id=project_id,
|
||||
candidate=normalized_message,
|
||||
prefer_candidate=True,
|
||||
)
|
||||
if merged is not None:
|
||||
persisted, did_change = merged
|
||||
existing_ids.add(mid)
|
||||
existing_positions[mid] = semantic_index.append(merged)
|
||||
existing_positions[mid] = semantic_index.append(persisted)
|
||||
if did_change:
|
||||
inserted_messages.append(persisted)
|
||||
changed_existing = True
|
||||
continue
|
||||
if existing_scope and existing_scope != (channel_id, project_id):
|
||||
metadata = dict(normalized_message.get("metadata", {}) or {})
|
||||
@@ -1644,9 +1702,33 @@ class ChatStore:
|
||||
excluded_message_ids=consumed_existing_ids,
|
||||
)
|
||||
if duplicate_index is not None:
|
||||
consumed_existing_ids.add(
|
||||
existing_messages[duplicate_index]["message_id"]
|
||||
existing_duplicate = existing_messages[duplicate_index]
|
||||
existing_id = str(existing_duplicate["message_id"])
|
||||
# Prefer the authoritative transcript on an equal score, but
|
||||
# do not replace a stronger canonical result surface with a
|
||||
# lower-priority mirror. The existing cache id and timestamp
|
||||
# remain stable either way.
|
||||
prefer_candidate = (
|
||||
self._message_preference_score(normalized_message)
|
||||
>= self._message_preference_score(existing_duplicate)
|
||||
)
|
||||
merged_duplicate = self._merge_duplicate_messages(
|
||||
existing_duplicate,
|
||||
normalized_message,
|
||||
prefer_candidate=prefer_candidate,
|
||||
)
|
||||
persisted, did_change = await self._persist_merged_message(
|
||||
existing_duplicate,
|
||||
merged_duplicate,
|
||||
channel_id=channel_id,
|
||||
project_id=project_id,
|
||||
preserve_timestamp=True,
|
||||
)
|
||||
if did_change:
|
||||
semantic_index.replace(duplicate_index, persisted)
|
||||
inserted_messages.append(persisted)
|
||||
changed_existing = True
|
||||
consumed_existing_ids.add(existing_id)
|
||||
continue
|
||||
|
||||
try:
|
||||
@@ -1674,11 +1756,16 @@ class ChatStore:
|
||||
channel_id=channel_id,
|
||||
project_id=project_id,
|
||||
candidate=normalized_message,
|
||||
prefer_candidate=True,
|
||||
)
|
||||
if merged is not None:
|
||||
persisted, did_change = merged
|
||||
merged_id = normalized_message["message_id"]
|
||||
existing_ids.add(merged_id)
|
||||
existing_positions[merged_id] = semantic_index.append(merged)
|
||||
existing_positions[merged_id] = semantic_index.append(persisted)
|
||||
if did_change:
|
||||
inserted_messages.append(persisted)
|
||||
changed_existing = True
|
||||
continue
|
||||
metadata = dict(normalized_message.get("metadata", {}) or {})
|
||||
metadata.setdefault("ui_message_id", normalized_message["message_id"])
|
||||
|
||||
+75
-63
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="data:," />
|
||||
<title>OpenOPC Pixel Office</title>
|
||||
<script type="module" crossorigin src="./assets/index-Cson66Y4.js"></script>
|
||||
<script type="module" crossorigin src="./assets/index-CTK-c7_K.js"></script>
|
||||
<link rel="modulepreload" crossorigin href="./assets/phaser-DFK5Ua9d.js">
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-DEvLDWDw.css">
|
||||
</head>
|
||||
|
||||
@@ -127,16 +127,37 @@ assert.match(
|
||||
'runtime events carrying a non-empty tool_name must update stable displayTool (empty tool_name keeps the sticky last command)',
|
||||
)
|
||||
|
||||
// 8. Assistant streaming drafts must disappear at real terminal boundaries.
|
||||
// 8. A streaming draft is one mounted logical turn. Completion/checkpoint
|
||||
// events cannot remove it before the matching persisted final is merged.
|
||||
assert.match(
|
||||
src,
|
||||
/evt\.type === 'turn_completed' \|\| evt\.type === 'turn_failed' \|\| evt\.type === 'checkpoint_saved'/,
|
||||
'runtime terminal/checkpoint events must clear task-mode Live Reply drafts',
|
||||
/const turnId = resolveCanonicalTurnId\(data\) \|\| undefined/,
|
||||
'runtime deltas must use the shared canonical-turn resolver',
|
||||
)
|
||||
assert.match(
|
||||
src,
|
||||
/detailHasFinalForDraft[\s\S]*runtime_v2_assistant[\s\S]*ss\.clearDraft\(detailTaskId\)/,
|
||||
'session_detail backfill of the final runtime assistant turn must clear matching Live Reply drafts',
|
||||
/const startsNewCanonicalTurn = evt\.type === 'turn_started'[\s\S]*turnId !== activeDraftTurnId[\s\S]*evt\.type === 'turn_failed' \|\| startsNewCanonicalTurn/,
|
||||
'only failure or a genuinely new canonical turn may clear an uncommitted draft',
|
||||
)
|
||||
assert.doesNotMatch(
|
||||
src,
|
||||
/evt\.type === 'turn_completed'[^\n]*clearDraft|evt\.type === 'checkpoint_saved'[^\n]*clearDraft/,
|
||||
'turn completion and checkpoint persistence must not clear a draft ahead of its final message',
|
||||
)
|
||||
assert.match(
|
||||
src,
|
||||
/detailHasFinalForDraft[\s\S]*terminalAssistantTurnId\(message\) === draftTurnId[\s\S]*ss\.clearDraft\(detailTaskId\)/,
|
||||
'session_detail may clear only after merging the matching terminal assistant turn',
|
||||
)
|
||||
assert.match(
|
||||
src,
|
||||
/cs\.addMessageFromBackend\(mapped\)[\s\S]*terminalTurnId = terminalAssistantTurnId\(mapped\)[\s\S]*terminalTurnId === activeDraftTurnId[\s\S]*clearDraft\(taskId\)/,
|
||||
'session_message/chat_new_message must merge a matching terminal final before clearing its draft',
|
||||
)
|
||||
assert.doesNotMatch(
|
||||
src,
|
||||
/mapped\.sender !== 'user'\)\s*\{\s*sessionStoreRef\.current\?\.clearDraft/,
|
||||
'an unrelated non-user session message must never clear an active draft',
|
||||
)
|
||||
|
||||
// 9. Summary/full pagination and transport-local failures have independent
|
||||
|
||||
@@ -24,6 +24,7 @@ import { companyRuntimeControlPatchForBoardStatus } from './lib/sessionRuntime'
|
||||
import { getExecutionTurnId } from './lib/workItemRuntimeIds'
|
||||
import { normalizeSessionCompanyProfile, normalizeSessionExecMode } from './lib/sessionIdentity'
|
||||
import { extractSessionRecruitmentByRole, sessionChannelId } from './lib/sessionRecruitment'
|
||||
import { resolveCanonicalTurnId, terminalAssistantTurnId } from './lib/turnIdentity'
|
||||
import { unassignAgent } from './game/map/OfficeStore'
|
||||
import type { AgentAnimStatus, EmployeeAssignment, KanbanPhase, KanbanTask, RoleAggregatedStatus, RoleWorkItemSummary, Session, TaskPreferredAgent } from './types/kanban'
|
||||
|
||||
@@ -919,9 +920,11 @@ export default function App() {
|
||||
const taskId = typeof data.task_id === 'string' ? data.task_id : ''
|
||||
if (taskId) {
|
||||
const isDeltaEvent = evt.type === 'assistant_delta' || evt.type === 'thinking_delta'
|
||||
const bufferedDraftTurnId = pendingDeltaFlushRef.current.get(taskId)?.draftTurnId
|
||||
// Store-write ordering guarantee: everything buffered for this
|
||||
// task lands before a non-delta event (turn boundaries call
|
||||
// clearDraft; the draft must be flushed first, not after).
|
||||
// task lands before a non-delta event. A matching terminal
|
||||
// message replaces the draft later; flushing here must not make
|
||||
// a completion boundary remove the visible turn prematurely.
|
||||
if (!isDeltaEvent) flushPendingDeltas(taskId)
|
||||
const ss = sessionStoreRef.current
|
||||
const bs = boardStoreRef.current
|
||||
@@ -931,13 +934,10 @@ export default function App() {
|
||||
: ''
|
||||
const executionMode = typeof data.execution_mode === 'string' ? data.execution_mode : ''
|
||||
const isTaskModeRuntime = executionMode === 'task_mode' || projectionId === 'task_mode_execution'
|
||||
const turnId = typeof data.turn_id === 'string' && data.turn_id
|
||||
? data.turn_id
|
||||
: typeof data.canonical_turn_id === 'string' && data.canonical_turn_id
|
||||
? data.canonical_turn_id
|
||||
: typeof data.execution_turn_id === 'string' && data.execution_turn_id
|
||||
? data.execution_turn_id
|
||||
: undefined
|
||||
// Drafts represent one logical assistant turn. The shared
|
||||
// resolver deliberately prefers conversation identity over the
|
||||
// iteration-scoped runtime turn id.
|
||||
const turnId = resolveCanonicalTurnId(data) || undefined
|
||||
const marksCompanyRuntime =
|
||||
!!projectionId && projectionId !== 'task_mode_execution' && !isTaskModeRuntime
|
||||
if (marksCompanyRuntime && !isDeltaEvent && existingSession?.isCompanyRuntime !== true) {
|
||||
@@ -993,7 +993,14 @@ export default function App() {
|
||||
entry.kanbanPatch = { ...entry.kanbanPatch, ...kanbanPatch }
|
||||
scheduleDeltaFlush()
|
||||
} else {
|
||||
if (evt.type === 'turn_started' || evt.type === 'turn_completed' || evt.type === 'turn_failed' || evt.type === 'checkpoint_saved') {
|
||||
const activeDraftTurnId = String(
|
||||
bufferedDraftTurnId ?? existingSession?.draftTurnId ?? '',
|
||||
).trim()
|
||||
const startsNewCanonicalTurn = evt.type === 'turn_started'
|
||||
&& !!turnId
|
||||
&& !!activeDraftTurnId
|
||||
&& turnId !== activeDraftTurnId
|
||||
if (evt.type === 'turn_failed' || startsNewCanonicalTurn) {
|
||||
ss?.clearDraft(taskId)
|
||||
}
|
||||
ss?.updateSession(taskId, runtimePartial)
|
||||
@@ -1084,14 +1091,9 @@ export default function App() {
|
||||
payload.client_history_page === true,
|
||||
)
|
||||
const draftTurnId = String(existingSession?.draftTurnId ?? '').trim()
|
||||
const detailHasFinalForDraft = !!draftTurnId && detailMessages.some((message) => {
|
||||
if (message.sender === 'user') return false
|
||||
const metadata = message.metadata ?? {}
|
||||
const transcriptKind = String(metadata.transcript_kind ?? metadata.kind ?? '').trim()
|
||||
if (transcriptKind !== 'runtime_v2_assistant') return false
|
||||
const messageTurnId = String(metadata.canonical_turn_id ?? metadata.turn_id ?? '').trim()
|
||||
return messageTurnId === draftTurnId
|
||||
})
|
||||
const detailHasFinalForDraft = !!draftTurnId
|
||||
&& !!cs
|
||||
&& detailMessages.some(message => terminalAssistantTurnId(message) === draftTurnId)
|
||||
if (detailHasFinalForDraft) {
|
||||
ss.clearDraft(detailTaskId)
|
||||
}
|
||||
@@ -1672,8 +1674,14 @@ export default function App() {
|
||||
console.debug('[onSessionMessage]', mapped.sender, mapped.channelId, mapped.content?.slice(0, 60))
|
||||
cs.addMessageFromBackend(mapped)
|
||||
const taskId = mapped.channelId.startsWith('session:') ? mapped.channelId.slice('session:'.length) : ''
|
||||
const terminalTurnId = terminalAssistantTurnId(mapped)
|
||||
const activeDraftTurnId = String(
|
||||
sessionStoreRef.current?.sessions.find(session => session.taskId === taskId)?.draftTurnId ?? '',
|
||||
).trim()
|
||||
if (taskId && mapped.sender !== 'user') {
|
||||
sessionStoreRef.current?.clearDraft(taskId)
|
||||
if (terminalTurnId && terminalTurnId === activeDraftTurnId) {
|
||||
sessionStoreRef.current?.clearDraft(taskId)
|
||||
}
|
||||
// Force refresh — session messages are critical content that must sync
|
||||
scheduleSessionDetailRefresh(taskId, 'full', true)
|
||||
}
|
||||
|
||||
@@ -241,6 +241,153 @@ assert.equal(repeatedNativeCompanySync.length, 1)
|
||||
assert.equal(repeatedNativeCompanySync[0].metadata?.ui_timeline_id, 'message:native-raw-1')
|
||||
assert.equal(repeatedNativeCompanySync[0].timestamp, 5)
|
||||
|
||||
const structuredDeliveryMerge = __chatStoreTestUtils.dedupeMessages([
|
||||
{
|
||||
...nativeCompanyRawTurn,
|
||||
content: 'Raw runtime wording before the canonical result wrapper.',
|
||||
metadata: {
|
||||
...nativeCompanyRawTurn.metadata,
|
||||
result_delivery_id: 'result:company-turn-1:attempt:0',
|
||||
},
|
||||
},
|
||||
{
|
||||
...companyRoleResult,
|
||||
content: 'Canonical committed wording may differ without creating another row.',
|
||||
metadata: {
|
||||
...companyRoleResult.metadata,
|
||||
result_delivery_id: 'result:company-turn-1:attempt:0',
|
||||
},
|
||||
},
|
||||
])
|
||||
assert.equal(structuredDeliveryMerge.length, 1)
|
||||
assert.equal(structuredDeliveryMerge[0].id, 'role-result-1')
|
||||
assert.equal(
|
||||
structuredDeliveryMerge[0].content,
|
||||
'Canonical committed wording may differ without creating another row.',
|
||||
)
|
||||
|
||||
const deliveryMirrorWithLongerWrapper = __chatStoreTestUtils.dedupeMessages([
|
||||
{
|
||||
...nativeCompanyRawTurn,
|
||||
content: 'Runtime wrapper that must not outrank the committed surface.\n\nCanonical result body.',
|
||||
metadata: {
|
||||
...nativeCompanyRawTurn.metadata,
|
||||
result_delivery_id: 'result:company-turn-wrapper:attempt:0',
|
||||
},
|
||||
},
|
||||
{
|
||||
...companyRoleResult,
|
||||
content: 'Canonical result body.',
|
||||
metadata: {
|
||||
...companyRoleResult.metadata,
|
||||
result_delivery_id: 'result:company-turn-wrapper:attempt:0',
|
||||
},
|
||||
},
|
||||
])
|
||||
assert.equal(deliveryMirrorWithLongerWrapper.length, 1)
|
||||
assert.equal(
|
||||
deliveryMirrorWithLongerWrapper[0].content,
|
||||
'Canonical result body.',
|
||||
'delivery mirrors follow surface authority; suffix repair only applies to one persistent message id',
|
||||
)
|
||||
|
||||
const ctoDispatchContent = `Both work items have been successfully dispatched to my senior engineer. Here's the status:
|
||||
|
||||
## Dispatch Summary
|
||||
|
||||
**Work Item 1: OpenOPC Source Code Architecture Deep-Dive Analysis**
|
||||
- ID: \`1ed5f5f1-ac41-49a1-b1fa-23bbc9adab82\`
|
||||
- Owner: senior_engineer
|
||||
- Scope: \`openopc-source-analysis\`
|
||||
- Output: \`/data2/bjdwhzzh/project-hku/OpenOPC_workplace/0009/openopc-architecture-analysis.md\`
|
||||
- Covers: Layered architecture, the work-item state machine, collaboration policy, and seat executor mechanisms.
|
||||
|
||||
**Work Item 2: External Multi-Agent Frameworks Architecture Research**
|
||||
- ID: \`d0307208-6b95-44c1-9b51-6bf073bbdcef\`
|
||||
- Owner: senior_engineer
|
||||
|
||||
Both work items are independent and can execute in parallel.`
|
||||
|
||||
const ctoCompanyFinal: ChatMessage = {
|
||||
id: 'runtime-v2-company-assistant-final:cto-turn-9',
|
||||
channelId: 'session:cto-work-item',
|
||||
sender: 'cto',
|
||||
senderName: 'CTO',
|
||||
content: ctoDispatchContent,
|
||||
timestamp: 20,
|
||||
mentions: [],
|
||||
metadata: {
|
||||
source: 'engine',
|
||||
transcript_kind: 'runtime_v2_company_assistant',
|
||||
canonical_turn_id: 'cto-turn-9',
|
||||
ui_message_id: 'runtime-v2-company-assistant-final:cto-turn-9',
|
||||
},
|
||||
}
|
||||
|
||||
const repeatedCtoSnapshot = __chatStoreTestUtils.dedupeMessages([
|
||||
ctoCompanyFinal,
|
||||
{ ...ctoCompanyFinal, metadata: { ...ctoCompanyFinal.metadata } },
|
||||
])
|
||||
assert.equal(repeatedCtoSnapshot.length, 1)
|
||||
assert.equal(
|
||||
repeatedCtoSnapshot[0].content,
|
||||
ctoDispatchContent,
|
||||
'comparison normalization must never peel Work Item, ID, or Owner fields from rendered content',
|
||||
)
|
||||
|
||||
let repeatedCtoMerge = [ctoCompanyFinal]
|
||||
for (let replay = 0; replay < 6; replay += 1) {
|
||||
repeatedCtoMerge = __chatStoreTestUtils.mergeMessagesIntoExisting(
|
||||
repeatedCtoMerge,
|
||||
[{ ...ctoCompanyFinal, metadata: { ...ctoCompanyFinal.metadata } }],
|
||||
)
|
||||
assert.equal(repeatedCtoMerge.length, 1)
|
||||
assert.equal(
|
||||
repeatedCtoMerge[0].content,
|
||||
ctoDispatchContent,
|
||||
`identical MERGE replay ${replay + 1} must preserve the complete source text`,
|
||||
)
|
||||
}
|
||||
|
||||
const truncatedCtoContent = ctoDispatchContent.slice(
|
||||
ctoDispatchContent.indexOf('OpenOPC Source Code Architecture Deep-Dive Analysis**'),
|
||||
)
|
||||
const truncatedCtoFinal: ChatMessage = {
|
||||
...ctoCompanyFinal,
|
||||
content: truncatedCtoContent,
|
||||
}
|
||||
|
||||
let interleavedCtoReplay = [ctoCompanyFinal]
|
||||
for (const replayedMessage of [
|
||||
truncatedCtoFinal,
|
||||
ctoCompanyFinal,
|
||||
truncatedCtoFinal,
|
||||
ctoCompanyFinal,
|
||||
]) {
|
||||
interleavedCtoReplay = __chatStoreTestUtils.mergeMessagesIntoExisting(
|
||||
interleavedCtoReplay,
|
||||
[{ ...replayedMessage, metadata: { ...replayedMessage.metadata } }],
|
||||
)
|
||||
assert.equal(interleavedCtoReplay.length, 1)
|
||||
assert.equal(
|
||||
interleavedCtoReplay[0].content,
|
||||
ctoDispatchContent,
|
||||
'a same-identity truncated cache replay must neither replace nor duplicate the complete message',
|
||||
)
|
||||
}
|
||||
|
||||
const repairedCtoReplay = __chatStoreTestUtils.mergeMessagesIntoExisting(
|
||||
[truncatedCtoFinal],
|
||||
[ctoCompanyFinal],
|
||||
)
|
||||
assert.equal(repairedCtoReplay.length, 1)
|
||||
assert.equal(repairedCtoReplay[0].content, ctoDispatchContent)
|
||||
assert.equal(
|
||||
__chatStoreTestUtils.mergeMessagesIntoExisting(repairedCtoReplay, [truncatedCtoFinal])[0].content,
|
||||
ctoDispatchContent,
|
||||
'once a complete same-identity source arrives, later truncated replays must not regress it',
|
||||
)
|
||||
|
||||
const mountedHighPriorityResult: ChatMessage = {
|
||||
...companyRoleResult,
|
||||
id: 'mounted-high-result',
|
||||
|
||||
@@ -23,8 +23,8 @@ function messageMetadata(message: ChatMessage): Record<string, unknown> {
|
||||
return (message.metadata ?? {}) as Record<string, unknown>
|
||||
}
|
||||
|
||||
function normalizeMessageContent(content: string): string {
|
||||
const normalized = String(content ?? '')
|
||||
function normalizeMessageFormatting(content: string): string {
|
||||
return String(content ?? '')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
.split('\n')
|
||||
@@ -32,6 +32,10 @@ function normalizeMessageContent(content: string): string {
|
||||
.join('\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
}
|
||||
|
||||
function normalizeMessageContent(content: string): string {
|
||||
const normalized = normalizeMessageFormatting(content)
|
||||
const titleStripped = stripNarrativeTitlePrefix(normalized)
|
||||
const paragraphs = titleStripped.split(/\n{2,}/).map(part => part.trim()).filter(Boolean)
|
||||
if (paragraphs.length > 1 && /^Verification:\s/i.test(paragraphs[paragraphs.length - 1])) {
|
||||
@@ -41,21 +45,40 @@ function normalizeMessageContent(content: string): string {
|
||||
}
|
||||
|
||||
function stripNarrativeTitlePrefix(content: string): string {
|
||||
const trimmed = String(content || '').trim()
|
||||
const markdownTitle = trimmed.match(/^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$/)
|
||||
if (markdownTitle) {
|
||||
let trimmed = String(content || '').trim()
|
||||
// Only an explicit, first-line "**Title**: body" wrapper is removable.
|
||||
// Searching for an arbitrary `: ` in the first N characters is destructive:
|
||||
// ordinary Markdown such as "**Work Item 1: ...**" and list fields such as
|
||||
// "- ID: ..." would be peeled one layer at a time on repeated syncs.
|
||||
for (;;) {
|
||||
const markdownTitle = trimmed.match(/^\*\*([^\r\n]{8,160}?)\*\*:(?:[ \t]+|\r?\n+)([\s\S]+)$/)
|
||||
if (!markdownTitle) return trimmed
|
||||
const body = markdownTitle[2].trim()
|
||||
if (body.length >= 80) return body
|
||||
if (body.length < 80 || body === trimmed) return trimmed
|
||||
trimmed = body
|
||||
}
|
||||
const colonIndex = trimmed.indexOf(': ')
|
||||
if (colonIndex < 8 || colonIndex > 160) return trimmed
|
||||
}
|
||||
|
||||
const prefix = trimmed.slice(0, colonIndex).replace(/\*/g, '').trim()
|
||||
const body = trimmed.slice(colonIndex + 2).trim()
|
||||
if (body.length < 80) return trimmed
|
||||
if (!/[A-Za-z\u4e00-\u9fff]/.test(prefix)) return trimmed
|
||||
if (/^(https?|file)$/i.test(prefix)) return trimmed
|
||||
return body
|
||||
function losslessIdentityContent(
|
||||
existing: ChatMessage,
|
||||
candidate: ChatMessage,
|
||||
preferred: ChatMessage,
|
||||
sharesConcreteIdentity: boolean,
|
||||
): string {
|
||||
if (!sharesConcreteIdentity) return preferred.content
|
||||
|
||||
const existingComparable = normalizeMessageFormatting(existing.content)
|
||||
const candidateComparable = normalizeMessageFormatting(candidate.content)
|
||||
if (!existingComparable || !candidateComparable || existingComparable === candidateComparable) {
|
||||
return preferred.content
|
||||
}
|
||||
|
||||
// A cache/detail replay can contain a prefix-truncated copy of the same
|
||||
// persistent message. Preserve the lossless source regardless of arrival
|
||||
// order; unrelated edits still follow the normal preference rules.
|
||||
if (existingComparable.endsWith(candidateComparable)) return existing.content
|
||||
if (candidateComparable.endsWith(existingComparable)) return candidate.content
|
||||
return preferred.content
|
||||
}
|
||||
|
||||
function messageIdentityKeys(message: ChatMessage): Set<string> {
|
||||
@@ -66,6 +89,9 @@ function messageIdentityKeys(message: ChatMessage): Set<string> {
|
||||
for (const value of [
|
||||
message.id,
|
||||
typeof metadata.ui_message_id === 'string' ? metadata.ui_message_id : '',
|
||||
typeof metadata.result_delivery_id === 'string' && metadata.result_delivery_id.trim()
|
||||
? `delivery:${metadata.result_delivery_id.trim()}`
|
||||
: '',
|
||||
checkpointType && checkpointId ? `checkpoint:${checkpointType}:${checkpointId}` : '',
|
||||
]) {
|
||||
const normalized = String(value ?? '').trim()
|
||||
@@ -81,7 +107,7 @@ function scopedMessageIdentityKeys(message: ChatMessage): Set<string> {
|
||||
}
|
||||
|
||||
function isDerivedIdentityKey(value: string): boolean {
|
||||
return value.startsWith('checkpoint:')
|
||||
return value.startsWith('checkpoint:') || value.startsWith('delivery:')
|
||||
}
|
||||
|
||||
function messageTimestamp(message: ChatMessage): number {
|
||||
@@ -229,18 +255,17 @@ function mergeDuplicateMessages(
|
||||
|
||||
const existingIds = messageIdentityKeys(existing)
|
||||
const candidateIds = messageIdentityKeys(candidate)
|
||||
let sharesConcreteIdentity = false
|
||||
let canonicalId = ''
|
||||
for (const id of existingIds) {
|
||||
if (candidateIds.has(id) && !isDerivedIdentityKey(id)) {
|
||||
canonicalId = id
|
||||
break
|
||||
if (!candidateIds.has(id)) continue
|
||||
if (!isDerivedIdentityKey(id)) {
|
||||
sharesConcreteIdentity = true
|
||||
if (!canonicalId) canonicalId = id
|
||||
}
|
||||
}
|
||||
|
||||
const normalizedContent = normalizeMessageContent(preferred.content)
|
||||
const content = normalizedContent && normalizedContent === normalizeMessageContent(secondary.content)
|
||||
? normalizedContent
|
||||
: preferred.content
|
||||
const content = losslessIdentityContent(existing, candidate, preferred, sharesConcreteIdentity)
|
||||
|
||||
const existingCheckpointId = String(messageMetadata(existing).checkpoint_id ?? '').trim()
|
||||
const candidateCheckpointId = String(messageMetadata(candidate).checkpoint_id ?? '').trim()
|
||||
|
||||
@@ -17,6 +17,16 @@ assert.match(
|
||||
assert.match(messageListSource, /scrollPolicy = 'follow'/, 'main transcript behavior should default to follow mode')
|
||||
assert.doesNotMatch(messageListSource, /useVirtualizer/, 'chat transcript must use stable normal DOM rows')
|
||||
assert.doesNotMatch(messageListSource, /PROGRAMMATIC_SCROLL_GRACE_MS/, 'scroll behavior must not regress to timer-based intent guessing')
|
||||
assert.doesNotMatch(
|
||||
messageListSource,
|
||||
/seenNarrativeMessages|seenProjectUpdates/,
|
||||
'MessageList must not independently remove durable rows using rendered content',
|
||||
)
|
||||
assert.match(
|
||||
messageListSource,
|
||||
/terminalAssistantTurnId\(message\)/,
|
||||
'draft suppression must use the shared committed-turn resolver',
|
||||
)
|
||||
|
||||
const progressWithoutServerId = {
|
||||
type: 'status_change' as const,
|
||||
@@ -147,7 +157,16 @@ const companyTurnKeys = [
|
||||
metadata: { kind: 'company_role_result', canonical_turn_id: sharedCompanyTurn },
|
||||
}),
|
||||
]
|
||||
assert.equal(new Set(companyTurnKeys).size, companyTurnKeys.length, 'distinct company rows sharing a turn need unique DOM keys')
|
||||
assert.equal(
|
||||
companyTurnKeys[1],
|
||||
companyTurnKeys[2],
|
||||
'company draft/final surfaces for one canonical turn must reuse the same DOM slot',
|
||||
)
|
||||
assert.notEqual(
|
||||
companyTurnKeys[2],
|
||||
companyTurnKeys[3],
|
||||
'a separately committed role result keeps its result-delivery identity',
|
||||
)
|
||||
|
||||
const narrativeItems = buildNarrativeMessageItems([
|
||||
baseMessage('m1', '[Company:cto::execute::abc] starting Research source reliability', 1000),
|
||||
@@ -163,35 +182,63 @@ assert.equal(narrativeItems[0].kind === 'ops-bundle' ? narrativeItems[0].events.
|
||||
assert.equal(narrativeItems[1].kind, 'message')
|
||||
assert.equal(narrativeItems[2].kind, 'ops-bundle')
|
||||
|
||||
const longResult = 'Completed the focused recheck and produced the QA artifact with caveats for downstream aggregation.'
|
||||
const dedupedProjectUpdates = buildNarrativeMessageItems([
|
||||
baseMessage('u1', prefixedPayload, 2000, 'qa_analyst'),
|
||||
baseMessage('u2', `**Report #1: Recheck remediated screen**: ${prefixedPayload}`, 2000, 'qa_analyst'),
|
||||
], { isCompanyRuntime: true, detailMode: 'summary' })
|
||||
assert.equal(dedupedProjectUpdates.length, 1)
|
||||
assert.equal(
|
||||
dedupedProjectUpdates.length,
|
||||
2,
|
||||
'renderer must preserve distinct project-update rows; upstream identity owns consolidation',
|
||||
)
|
||||
assert.equal(dedupedProjectUpdates[0].kind, 'message')
|
||||
assert.equal(dedupedProjectUpdates[0].kind === 'message' ? dedupedProjectUpdates[0].msg.id : '', 'u1')
|
||||
|
||||
const longResult = 'Completed the focused recheck and produced the QA artifact with caveats for downstream aggregation.'
|
||||
const dedupedNarrativeMessages = buildNarrativeMessageItems([
|
||||
const identicalDurableNarratives = buildNarrativeMessageItems([
|
||||
baseMessage('same-content-1', longResult, 2500, 'qa_analyst'),
|
||||
baseMessage('same-content-2', longResult, 2500, 'qa_analyst'),
|
||||
], { isCompanyRuntime: true, detailMode: 'summary' })
|
||||
assert.deepEqual(
|
||||
identicalDurableNarratives.map(item => item.kind === 'message' ? item.msg.id : item.id),
|
||||
['same-content-1', 'same-content-2'],
|
||||
'distinct stable identities must survive even when sender, timestamp, and content are identical',
|
||||
)
|
||||
|
||||
const ambiguousNarrativeMessages = buildNarrativeMessageItems([
|
||||
baseMessage('n1', longResult, 3000, 'qa_analyst'),
|
||||
baseMessage('n2', `Recheck remediated ten-bagger candidate screen: ${longResult}`, 3000, 'qa_analyst'),
|
||||
], { isCompanyRuntime: true, detailMode: 'summary' })
|
||||
assert.equal(dedupedNarrativeMessages.length, 1)
|
||||
assert.equal(dedupedNarrativeMessages[0].kind === 'message' ? dedupedNarrativeMessages[0].msg.id : '', 'n1')
|
||||
assert.equal(
|
||||
ambiguousNarrativeMessages.length,
|
||||
2,
|
||||
'plain narrative prefixes are content and must not be stripped to guess message identity',
|
||||
)
|
||||
|
||||
const duplicatedResultSurface = buildNarrativeMessageItems([
|
||||
{
|
||||
...baseMessage('r1', longResult, 4000, 'chao'),
|
||||
metadata: { source: 'engine', transcript_kind: 'child_task_result' },
|
||||
metadata: {
|
||||
source: 'engine',
|
||||
transcript_kind: 'child_task_result',
|
||||
result_delivery_id: 'delivery-r1',
|
||||
},
|
||||
},
|
||||
{
|
||||
...baseMessage('r2', `Deliver final result to user: ${longResult}`, 4500, 'system'),
|
||||
senderName: 'Company Member',
|
||||
metadata: { source: 'runtime_event', kind: 'worker_notification', notification_kind: 'task_complete' },
|
||||
metadata: {
|
||||
source: 'engine',
|
||||
transcript_kind: 'child_result',
|
||||
result_delivery_id: 'delivery-r1',
|
||||
},
|
||||
},
|
||||
], { isCompanyRuntime: true, detailMode: 'summary' })
|
||||
assert.equal(duplicatedResultSurface.length, 1)
|
||||
assert.equal(duplicatedResultSurface[0].kind === 'message' ? duplicatedResultSurface[0].msg.id : '', 'r1')
|
||||
assert.equal(
|
||||
duplicatedResultSurface.length,
|
||||
2,
|
||||
'MessageList must not run a second result consolidator after the store/company projection',
|
||||
)
|
||||
|
||||
const fullItems = buildNarrativeMessageItems([
|
||||
baseMessage('m1', '[Company:cto::execute::abc] starting Research source reliability', 1000),
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { ProgressEntry, RoleWorkItemSummary, Session, WorkItemProgressEntry
|
||||
import { progressEntryKey } from '../lib/progressEntryKey'
|
||||
import { stableMessageTimelineKey } from '../lib/messageTimelineIdentity'
|
||||
import { isMessageVisibleAtDetailLevel, resultSurfaceDedupeKey } from '../lib/workItemSessions'
|
||||
import { resolveCanonicalTurnId, terminalAssistantTurnId } from '../lib/turnIdentity'
|
||||
import { IconCopy, IconCheck, IconChat, IconSparkle, IconShield, IconActivity, IconChevron } from './SvgIcons'
|
||||
import { AgentProgressBlock, AgentProgressEntryCard, INLINE_PROGRESS_ENTRY_TYPES } from './AgentProgressBlock'
|
||||
import { MarkdownBody } from './MarkdownBody'
|
||||
@@ -345,13 +346,6 @@ export function messageTimelineKey(message: ChatMessage): string {
|
||||
return stableMessageTimelineKey(message)
|
||||
}
|
||||
|
||||
function terminalAssistantTurnId(message: ChatMessage): string {
|
||||
const timelineKey = messageTimelineKey(message)
|
||||
return timelineKey.startsWith('turn:assistant:')
|
||||
? timelineKey.slice('turn:assistant:'.length)
|
||||
: ''
|
||||
}
|
||||
|
||||
function formatTime(ts: number) {
|
||||
return new Date(ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', hour12: false })
|
||||
}
|
||||
@@ -400,44 +394,6 @@ function compactWhitespace(value: string): string {
|
||||
return value.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
function stripNarrativeTitlePrefix(content: string): string {
|
||||
const trimmed = String(content || '').trim()
|
||||
const markdownTitle = trimmed.match(/^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$/)
|
||||
if (markdownTitle) {
|
||||
const body = markdownTitle[2].trim()
|
||||
if (body.length >= 80) return body
|
||||
}
|
||||
const colonIndex = trimmed.indexOf(': ')
|
||||
if (colonIndex < 8 || colonIndex > 160) return trimmed
|
||||
|
||||
const prefix = trimmed.slice(0, colonIndex).replace(/\*/g, '').trim()
|
||||
const body = trimmed.slice(colonIndex + 2).trim()
|
||||
if (body.length < 80) return trimmed
|
||||
if (!/[A-Za-z\u4e00-\u9fff]/.test(prefix)) return trimmed
|
||||
if (/^(https?|file)$/i.test(prefix)) return trimmed
|
||||
return body
|
||||
}
|
||||
|
||||
function isResultSurfaceMessage(message: ChatMessage): boolean {
|
||||
const transcriptKind = String(message.metadata?.transcript_kind ?? message.metadata?.kind ?? '').trim()
|
||||
if ([
|
||||
'runtime_v2_assistant',
|
||||
'runtime_v2_company_assistant',
|
||||
'top_level_reply',
|
||||
'company_role_result',
|
||||
'company_role_result_retry',
|
||||
'child_task_result',
|
||||
'child_task_result_retry',
|
||||
'child_result',
|
||||
].includes(transcriptKind)) {
|
||||
return true
|
||||
}
|
||||
if (String(message.metadata?.kind ?? '').trim() === 'worker_notification') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function parseJsonObjectText(content: string): Record<string, unknown> | null {
|
||||
const trimmed = String(content || '').trim()
|
||||
if (!trimmed) return null
|
||||
@@ -830,8 +786,6 @@ export function buildNarrativeMessageItems(
|
||||
const items: TimelineItem[] = []
|
||||
let bundle: SystemOpsBundleEvent[] = []
|
||||
let bundleSortOrder = 0
|
||||
const seenProjectUpdates = new Set<string>()
|
||||
const seenNarrativeMessages = new Set<string>()
|
||||
|
||||
const flushBundle = () => {
|
||||
if (bundle.length === 0) return
|
||||
@@ -854,33 +808,6 @@ export function buildNarrativeMessageItems(
|
||||
bundle.push(ops)
|
||||
return
|
||||
}
|
||||
const projectUpdate = detailMode === 'summary' ? parseProjectUpdatePayload(msg.content) : null
|
||||
if (projectUpdate) {
|
||||
const dedupeKey = [
|
||||
msg.sender,
|
||||
Math.round(msg.timestamp / 1000),
|
||||
projectUpdate.kind,
|
||||
compactWhitespace(projectUpdate.summary || projectUpdate.acceptanceSummary || '').slice(0, 500),
|
||||
projectUpdate.verdict ?? '',
|
||||
projectUpdate.deliverables.map(item => `${item.name}:${item.path}`).join('|').slice(0, 800),
|
||||
].join('\u0001')
|
||||
if (seenProjectUpdates.has(dedupeKey)) return
|
||||
seenProjectUpdates.add(dedupeKey)
|
||||
}
|
||||
if (detailMode === 'summary' && !isCheckpointCardMetadata(msg.metadata)) {
|
||||
const canonicalContent = compactWhitespace(stripNarrativeTitlePrefix(msg.content)).slice(0, 1200)
|
||||
if (canonicalContent) {
|
||||
const dedupeKey = isResultSurfaceMessage(msg)
|
||||
? ['result', canonicalContent].join('\u0001')
|
||||
: [
|
||||
msg.sender,
|
||||
Math.round(msg.timestamp / 1000),
|
||||
canonicalContent,
|
||||
].join('\u0001')
|
||||
if (seenNarrativeMessages.has(dedupeKey)) return
|
||||
seenNarrativeMessages.add(dedupeKey)
|
||||
}
|
||||
}
|
||||
flushBundle()
|
||||
items.push({
|
||||
kind: 'message',
|
||||
@@ -1331,7 +1258,7 @@ export const MessageList = React.memo(function MessageList({
|
||||
for (const message of timelineMessages) {
|
||||
const thinking = String(message.metadata?.runtime_thinking ?? '').trim()
|
||||
if (!thinking) continue
|
||||
const turnId = String(message.metadata?.canonical_turn_id ?? message.metadata?.turn_id ?? '').trim()
|
||||
const turnId = resolveCanonicalTurnId(message.metadata)
|
||||
if (turnId && thinkingProgressTurnIds.has(turnId)) continue
|
||||
entries.push({
|
||||
type: 'thinking' as const,
|
||||
|
||||
@@ -1,4 +1,103 @@
|
||||
import type { ChatMessage } from '../types/chat'
|
||||
import { resolveCanonicalTurnId, terminalAssistantTurnId } from './turnIdentity'
|
||||
|
||||
const COMMITTED_RESULT_SURFACE_KINDS = new Set([
|
||||
'child_task_result',
|
||||
'child_task_result_retry',
|
||||
'company_role_result',
|
||||
'company_role_result_retry',
|
||||
'child_result',
|
||||
'top_level_reply',
|
||||
])
|
||||
|
||||
const RUNTIME_RESULT_SURFACE_KINDS = new Set([
|
||||
'runtime_v2_assistant',
|
||||
'runtime_v2_company_assistant',
|
||||
])
|
||||
|
||||
function metadataValue(metadata: Record<string, unknown>, ...keys: string[]): string {
|
||||
for (const key of keys) {
|
||||
const value = String(metadata[key] ?? '').trim()
|
||||
if (value) return value
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function resultGenerationSuffix(metadata: Record<string, unknown>, kind: string): string {
|
||||
const explicitAttempt = metadataValue(
|
||||
metadata,
|
||||
'result_attempt',
|
||||
'attempt',
|
||||
'attempt_index',
|
||||
'retry_count',
|
||||
'retryCount',
|
||||
)
|
||||
const attempt = explicitAttempt || (kind.endsWith('_retry') ? 'retry' : '')
|
||||
const revision = metadataValue(
|
||||
metadata,
|
||||
'result_revision',
|
||||
'delivery_revision',
|
||||
'revision',
|
||||
)
|
||||
return [
|
||||
attempt ? `attempt:${attempt}` : '',
|
||||
revision ? `revision:${revision}` : '',
|
||||
].filter(Boolean).join(':')
|
||||
}
|
||||
|
||||
function withResultGeneration(
|
||||
base: string,
|
||||
metadata: Record<string, unknown>,
|
||||
kind: string,
|
||||
): string {
|
||||
const suffix = resultGenerationSuffix(metadata, kind)
|
||||
return suffix ? `${base}:${suffix}` : base
|
||||
}
|
||||
|
||||
/** Stable protocol identity shared by mirrors of one committed result. */
|
||||
export function stableResultDeliveryKey(message: ChatMessage): string {
|
||||
const metadata = (message.metadata ?? {}) as Record<string, unknown>
|
||||
const kind = String(metadata.transcript_kind ?? metadata.kind ?? '').trim()
|
||||
if (!COMMITTED_RESULT_SURFACE_KINDS.has(kind) && !RUNTIME_RESULT_SURFACE_KINDS.has(kind)) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const deliveryId = metadataValue(
|
||||
metadata,
|
||||
'canonical_delivery_id',
|
||||
'result_delivery_id',
|
||||
'delivery_id',
|
||||
)
|
||||
if (deliveryId) return `delivery:${deliveryId}`
|
||||
|
||||
if (RUNTIME_RESULT_SURFACE_KINDS.has(kind)) {
|
||||
const turnId = resolveCanonicalTurnId(metadata)
|
||||
return turnId ? `turn:${turnId}` : ''
|
||||
}
|
||||
|
||||
// A committed result belongs to a task/work-item delivery, not merely to
|
||||
// the surrounding conversation turn. Parallel roles commonly share that
|
||||
// turn, so a turn-only fallback would collapse independent deliveries.
|
||||
const explicitSourceTaskId = metadataValue(metadata, 'source_task_id', 'child_task_id')
|
||||
const sourceTaskId = explicitSourceTaskId || (
|
||||
kind === 'top_level_reply'
|
||||
? ''
|
||||
: metadataValue(metadata, 'task_id', 'taskId')
|
||||
)
|
||||
if (sourceTaskId) {
|
||||
return withResultGeneration(`source-task:${sourceTaskId}`, metadata, kind)
|
||||
}
|
||||
|
||||
const workItemId = metadataValue(metadata, 'work_item_id', 'work_item_projection_id')
|
||||
if (workItemId) {
|
||||
return withResultGeneration(`work-item:${workItemId}`, metadata, kind)
|
||||
}
|
||||
|
||||
const childSessionId = metadataValue(metadata, 'child_session_id')
|
||||
return childSessionId
|
||||
? withResultGeneration(`child-session:${childSessionId}`, metadata, kind)
|
||||
: ''
|
||||
}
|
||||
|
||||
export function stableMessageTimelineKey(message: ChatMessage): string {
|
||||
const metadata = message.metadata ?? {}
|
||||
@@ -21,10 +120,19 @@ export function stableMessageTimelineKey(message: ChatMessage): string {
|
||||
const retainedTimelineId = String(metadata.ui_timeline_id ?? '').trim()
|
||||
if (retainedTimelineId) return retainedTimelineId
|
||||
|
||||
const turnId = String(metadata.canonical_turn_id ?? metadata.turn_id ?? '').trim()
|
||||
if (!isUserTurn && turnId && transcriptKind === 'runtime_v2_assistant') {
|
||||
// Only a streamed runtime terminal owns the live draft's React slot.
|
||||
// Committed result surfaces may expose the same conversation turn through
|
||||
// terminalAssistantTurnId, but their row identity must remain delivery/task
|
||||
// scoped so parallel role results cannot collide.
|
||||
const turnId = RUNTIME_RESULT_SURFACE_KINDS.has(transcriptKind)
|
||||
? terminalAssistantTurnId(message)
|
||||
: ''
|
||||
if (!isUserTurn && turnId) {
|
||||
return `turn:assistant:${turnId}`
|
||||
}
|
||||
|
||||
const resultDeliveryKey = stableResultDeliveryKey(message)
|
||||
if (!isUserTurn && resultDeliveryKey) return `result:${resultDeliveryKey}`
|
||||
|
||||
return `message:${message.id}`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { ChatMessage, ChatMessageMeta } from '../types/chat'
|
||||
|
||||
const TERMINAL_ASSISTANT_KINDS = new Set([
|
||||
'runtime_v2_assistant',
|
||||
'runtime_v2_company_assistant',
|
||||
'child_task_result',
|
||||
'child_task_result_retry',
|
||||
'company_role_result',
|
||||
'company_role_result_retry',
|
||||
'child_result',
|
||||
'top_level_reply',
|
||||
])
|
||||
|
||||
export function resolveCanonicalTurnId(
|
||||
metadata: ChatMessageMeta | Record<string, unknown> | null | undefined,
|
||||
): string {
|
||||
const source = (metadata ?? {}) as Record<string, unknown>
|
||||
for (const key of [
|
||||
'canonical_turn_id',
|
||||
'conversation_turn_id',
|
||||
'turn_id',
|
||||
'execution_turn_id',
|
||||
]) {
|
||||
const value = String(source[key] ?? '').trim()
|
||||
if (value) return value
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function terminalAssistantTurnId(message: ChatMessage): string {
|
||||
if (message.sender === 'user') return ''
|
||||
const metadata = (message.metadata ?? {}) as Record<string, unknown>
|
||||
const kind = String(metadata.transcript_kind ?? metadata.kind ?? '').trim()
|
||||
if (!TERMINAL_ASSISTANT_KINDS.has(kind)) return ''
|
||||
if (kind === 'runtime_v2_company_assistant') {
|
||||
// Company tool-call iterations intentionally share this transcript kind
|
||||
// and canonical turn with the final answer. Treating every iteration as
|
||||
// terminal hides the live draft during detail refresh, then grows it back
|
||||
// on the next delta. Only the actual final surface may replace the draft.
|
||||
const uiMessageId = String(metadata.ui_message_id ?? '').trim()
|
||||
const isFinal = metadata.company_final_turn === true
|
||||
|| !!String(metadata.result_delivery_id ?? '').trim()
|
||||
|| uiMessageId.startsWith('runtime-v2-company-assistant-final:')
|
||||
// Snapshot translation retains final-vs-intermediate as visibility even
|
||||
// when reading records created before structured delivery ids existed.
|
||||
|| String(metadata.detail_visibility ?? '').trim() === 'summary'
|
||||
if (!isFinal) return ''
|
||||
}
|
||||
return resolveCanonicalTurnId(metadata)
|
||||
}
|
||||
@@ -2,8 +2,10 @@ import assert from 'node:assert/strict'
|
||||
import type { ChatMessage } from '../types/chat'
|
||||
import type { Session } from '../types/kanban'
|
||||
import { mapBackendSession, mergeSessionDetailHasMore } from './collabSync'
|
||||
import { stableMessageTimelineKey, stableResultDeliveryKey } from './messageTimelineIdentity'
|
||||
import { canonicalizeSessionExecutionIdentity } from './sessionIdentity'
|
||||
import { deriveCompanyRuntimeDisplayStatus, getConversationHeaderSession, getConversationSessionView, getWorkItemChildSessions, getWorkItemRoleSessions, mergeConversationMessages, projectSessionConversation, selectCompanySummaryMessages } from './workItemSessions'
|
||||
import { resolveCanonicalTurnId, terminalAssistantTurnId } from './turnIdentity'
|
||||
import { deriveCompanyRuntimeDisplayStatus, getConversationHeaderSession, getConversationSessionView, getWorkItemChildSessions, getWorkItemRoleSessions, mergeConversationMessages, projectSessionConversation, resultSurfaceDedupeKey, selectCompanySummaryMessages } from './workItemSessions'
|
||||
|
||||
function makeSession(overrides: Partial<Session> & Pick<Session, 'taskId' | 'channelId' | 'title' | 'status' | 'columnId' | 'assigneeIds' | 'priority' | 'tags' | 'progressLog' | 'createdAt' | 'updatedAt' | 'messageCount'>): Session {
|
||||
return {
|
||||
@@ -338,12 +340,380 @@ assert.deepEqual(
|
||||
[
|
||||
'parent-user',
|
||||
'canonical-role-result',
|
||||
'summary-company-final',
|
||||
'summary-terminal-b',
|
||||
'child-checkpoint',
|
||||
'child-checkpoint-response',
|
||||
].sort(),
|
||||
)
|
||||
|
||||
const companyRuntimeTurn = resultMessage(
|
||||
'company-runtime-turn',
|
||||
'session:company-child',
|
||||
'A company runtime terminal surface.',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'runtime_v2_company_assistant',
|
||||
canonical_turn_id: 'company-turn-0009',
|
||||
result_delivery_id: 'company-delivery-0009',
|
||||
},
|
||||
'assistant',
|
||||
)
|
||||
assert.equal(
|
||||
stableMessageTimelineKey(companyRuntimeTurn),
|
||||
'turn:assistant:company-turn-0009',
|
||||
)
|
||||
|
||||
const conversationOnlyRuntimeTurn = resultMessage(
|
||||
'company-runtime-conversation-turn',
|
||||
'session:company-child',
|
||||
'A terminal surface with only its conversation identity.',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'runtime_v2_company_assistant',
|
||||
conversation_turn_id: 'conversation-turn-only',
|
||||
turn_id: 'iteration-turn-must-not-win',
|
||||
execution_turn_id: 'execution-turn-must-not-win',
|
||||
detail_visibility: 'summary',
|
||||
},
|
||||
'assistant',
|
||||
)
|
||||
assert.equal(
|
||||
resolveCanonicalTurnId(conversationOnlyRuntimeTurn.metadata),
|
||||
'conversation-turn-only',
|
||||
)
|
||||
assert.equal(terminalAssistantTurnId(conversationOnlyRuntimeTurn), 'conversation-turn-only')
|
||||
assert.equal(
|
||||
terminalAssistantTurnId({
|
||||
...conversationOnlyRuntimeTurn,
|
||||
metadata: {
|
||||
...conversationOnlyRuntimeTurn.metadata,
|
||||
detail_visibility: 'full',
|
||||
},
|
||||
}),
|
||||
'',
|
||||
'a company tool-call iteration must not suppress the active draft',
|
||||
)
|
||||
assert.equal(
|
||||
stableMessageTimelineKey(conversationOnlyRuntimeTurn),
|
||||
'turn:assistant:conversation-turn-only',
|
||||
)
|
||||
assert.equal(
|
||||
terminalAssistantTurnId({
|
||||
...conversationOnlyRuntimeTurn,
|
||||
metadata: {
|
||||
...conversationOnlyRuntimeTurn.metadata,
|
||||
transcript_kind: 'company_role_result',
|
||||
},
|
||||
}),
|
||||
'conversation-turn-only',
|
||||
'committed result kinds must participate in terminal-turn matching',
|
||||
)
|
||||
assert.equal(
|
||||
stableMessageTimelineKey({
|
||||
...conversationOnlyRuntimeTurn,
|
||||
metadata: {
|
||||
...conversationOnlyRuntimeTurn.metadata,
|
||||
transcript_kind: 'company_role_result',
|
||||
source_task_id: 'committed-source-task',
|
||||
},
|
||||
}),
|
||||
'result:source-task:committed-source-task',
|
||||
'a committed terminal may expose its turn for matching without taking the runtime draft key',
|
||||
)
|
||||
|
||||
const project0009CtoResult = [
|
||||
'Both work items have been successfully dispatched to my senior engineer. Here\'s the status:',
|
||||
'',
|
||||
'## Dispatch Summary',
|
||||
'',
|
||||
'**Work Item 1: OpenOPC Source Code Architecture Deep-Dive Analysis**',
|
||||
'- ID: `1ed5f5f1-ac41-49a1-b1fa-23bbc9adab82`',
|
||||
'- Owner: senior_engineer',
|
||||
'- Scope: `openopc-source-analysis`',
|
||||
'- Output: `/data2/bjdwhzzh/project-hku/OpenOPC_workplace/0009/openopc-architecture-analysis.md`',
|
||||
'- Covers: Layered architecture, collaboration policy, seat executor pattern, and self-evolution mechanisms.',
|
||||
'',
|
||||
'**Work Item 2: External Multi-Agent Frameworks Architecture Research**',
|
||||
'- ID: `d0307208-6b95-44c1-9b51-6bf073bbdcef`',
|
||||
'- Owner: senior_engineer',
|
||||
'- Scope: `external-frameworks-research`',
|
||||
'- Output: `/data2/bjdwhzzh/project-hku/OpenOPC_workplace/0009/external-frameworks-analysis.md`',
|
||||
'',
|
||||
'Both are independent and can execute in parallel. The runtime will monitor their completion.',
|
||||
].join('\n')
|
||||
|
||||
const project0009WorkItemSuffix = project0009CtoResult.slice(
|
||||
project0009CtoResult.indexOf('OpenOPC Source Code Architecture Deep-Dive Analysis'),
|
||||
)
|
||||
const project0009IdSuffix = project0009CtoResult.slice(
|
||||
project0009CtoResult.indexOf('`1ed5f5f1-ac41-49a1-b1fa-23bbc9adab82`'),
|
||||
)
|
||||
|
||||
// Content fallback must not treat arbitrary Markdown colons as removable
|
||||
// narrative wrappers. The old normalization repeatedly turned the full 0009
|
||||
// result into these two shorter variants, which changed the rendered height.
|
||||
const fallback0009Full = resultMessage(
|
||||
'0009-fallback-full',
|
||||
'session:company-root',
|
||||
project0009CtoResult,
|
||||
{ source: 'engine', transcript_kind: 'child_result' },
|
||||
'assistant',
|
||||
)
|
||||
const fallback0009WorkItem = resultMessage(
|
||||
'0009-fallback-work-item',
|
||||
'session:company-child',
|
||||
project0009WorkItemSuffix,
|
||||
{ source: 'engine', transcript_kind: 'runtime_v2_assistant' },
|
||||
'assistant',
|
||||
)
|
||||
const fallback0009Id = resultMessage(
|
||||
'0009-fallback-id',
|
||||
'session:company-child',
|
||||
project0009IdSuffix,
|
||||
{ source: 'engine', transcript_kind: 'runtime_v2_assistant' },
|
||||
'assistant',
|
||||
)
|
||||
assert.notEqual(resultSurfaceDedupeKey(fallback0009Full), resultSurfaceDedupeKey(fallback0009WorkItem))
|
||||
assert.notEqual(resultSurfaceDedupeKey(fallback0009WorkItem), resultSurfaceDedupeKey(fallback0009Id))
|
||||
|
||||
const committed0009Parent = {
|
||||
...resultMessage(
|
||||
'0009-parent-result',
|
||||
'session:company-root',
|
||||
project0009CtoResult,
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'child_result',
|
||||
source_task_id: 'cto-task-0009',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
timestamp: 2_000,
|
||||
}
|
||||
const committed0009Child = {
|
||||
...resultMessage(
|
||||
'0009-child-result',
|
||||
'session:company-child',
|
||||
project0009CtoResult,
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'child_task_result',
|
||||
task_id: 'cto-task-0009',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
timestamp: 2_010,
|
||||
}
|
||||
const raw0009WorkItem = {
|
||||
...fallback0009WorkItem,
|
||||
metadata: {
|
||||
...fallback0009WorkItem.metadata,
|
||||
detail_visibility: 'summary' as const,
|
||||
canonical_turn_id: 'cto-turn-0009',
|
||||
},
|
||||
timestamp: 2_020,
|
||||
}
|
||||
const raw0009Id = {
|
||||
...fallback0009Id,
|
||||
metadata: {
|
||||
...fallback0009Id.metadata,
|
||||
detail_visibility: 'summary' as const,
|
||||
canonical_turn_id: 'cto-turn-0009',
|
||||
},
|
||||
timestamp: 2_030,
|
||||
}
|
||||
|
||||
for (const messages of [
|
||||
[committed0009Parent, committed0009Child, raw0009WorkItem, raw0009Id],
|
||||
[raw0009Id, raw0009WorkItem, committed0009Child, committed0009Parent],
|
||||
]) {
|
||||
const summary = selectCompanySummaryMessages(messages, 'session:company-root')
|
||||
assert.equal(summary.length, 1)
|
||||
assert.equal(summary[0]?.id, '0009-child-result')
|
||||
assert.equal(summary[0]?.content, project0009CtoResult)
|
||||
assert.equal(stableMessageTimelineKey(summary[0]!), 'result:source-task:cto-task-0009')
|
||||
}
|
||||
|
||||
const sameRoleText = 'The role completed its independent architecture analysis and committed the result.'
|
||||
const multiRoleSummary = selectCompanySummaryMessages([
|
||||
resultMessage(
|
||||
'0009-cto-role-result',
|
||||
'session:company-cto',
|
||||
sameRoleText,
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
work_item_projection_id: '0009-cto-work-item',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
resultMessage(
|
||||
'0009-coo-role-result',
|
||||
'session:company-coo',
|
||||
sameRoleText,
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
work_item_projection_id: '0009-coo-work-item',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
], 'session:company-root')
|
||||
assert.deepEqual(
|
||||
multiRoleSummary.map(message => stableMessageTimelineKey(message)).sort(),
|
||||
[
|
||||
'result:work-item:0009-coo-work-item',
|
||||
'result:work-item:0009-cto-work-item',
|
||||
],
|
||||
)
|
||||
|
||||
const sharedConversationTurn = 'shared-company-conversation-turn'
|
||||
const parallelRoleResults = [
|
||||
resultMessage(
|
||||
'parallel-cto-result',
|
||||
'session:company-cto',
|
||||
'CTO completed the architecture assessment.',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
canonical_turn_id: sharedConversationTurn,
|
||||
work_item_projection_id: 'architecture-assessment',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
resultMessage(
|
||||
'parallel-coo-result',
|
||||
'session:company-coo',
|
||||
'COO completed the feature assessment.',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
canonical_turn_id: sharedConversationTurn,
|
||||
work_item_projection_id: 'feature-assessment',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
]
|
||||
assert.deepEqual(
|
||||
parallelRoleResults.map(resultSurfaceDedupeKey),
|
||||
[
|
||||
'result:work-item:architecture-assessment',
|
||||
'result:work-item:feature-assessment',
|
||||
],
|
||||
'parallel committed roles must use work-item identity before a shared conversation turn',
|
||||
)
|
||||
assert.equal(
|
||||
selectCompanySummaryMessages(parallelRoleResults, 'session:company-root').length,
|
||||
2,
|
||||
)
|
||||
|
||||
const versionedSourceResult = resultMessage(
|
||||
'versioned-source-result',
|
||||
'session:company-child',
|
||||
'Versioned result.',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result_retry',
|
||||
source_task_id: 'source-task-versioned',
|
||||
retry_count: 2,
|
||||
delivery_revision: 4,
|
||||
} as ChatMessage['metadata'],
|
||||
'assistant',
|
||||
)
|
||||
assert.equal(
|
||||
stableResultDeliveryKey(versionedSourceResult),
|
||||
'source-task:source-task-versioned:attempt:2:revision:4',
|
||||
)
|
||||
|
||||
const fullEqualPriorityResult = {
|
||||
...resultMessage(
|
||||
'full-equal-priority',
|
||||
'session:company-child-a',
|
||||
'The complete authoritative body includes every required architectural conclusion and its supporting rationale.',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
result_delivery_id: 'deterministic-delivery',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
timestamp: 2_100,
|
||||
}
|
||||
const truncatedEqualPriorityResult = {
|
||||
...resultMessage(
|
||||
'truncated-equal-priority',
|
||||
'session:company-child-b',
|
||||
'supporting rationale.',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
result_delivery_id: 'deterministic-delivery',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
timestamp: 2_200,
|
||||
}
|
||||
for (const groups of [
|
||||
[[fullEqualPriorityResult], [truncatedEqualPriorityResult]],
|
||||
[[truncatedEqualPriorityResult], [fullEqualPriorityResult]],
|
||||
]) {
|
||||
const merged = mergeConversationMessages(groups)
|
||||
assert.equal(merged.length, 1)
|
||||
assert.equal(merged[0]?.id, 'full-equal-priority')
|
||||
assert.equal(merged[0]?.content, fullEqualPriorityResult.content)
|
||||
assert.equal(merged[0]?.timestamp, 2_100)
|
||||
assert.equal(
|
||||
stableMessageTimelineKey(merged[0]!),
|
||||
'result:delivery:deterministic-delivery',
|
||||
)
|
||||
const replayed = mergeConversationMessages([
|
||||
merged,
|
||||
[truncatedEqualPriorityResult],
|
||||
])
|
||||
assert.equal(replayed.length, 1)
|
||||
assert.equal(replayed[0]?.id, 'full-equal-priority')
|
||||
assert.equal(replayed[0]?.content, fullEqualPriorityResult.content)
|
||||
}
|
||||
|
||||
const equalLengthStableWinner = {
|
||||
...resultMessage(
|
||||
'z-stable-winner',
|
||||
'session:company-child-a',
|
||||
'BBBB',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
result_delivery_id: 'equal-length-delivery',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
timestamp: 3_200,
|
||||
}
|
||||
const equalLengthLoser = {
|
||||
...resultMessage(
|
||||
'a-stable-loser',
|
||||
'session:company-child-b',
|
||||
'AAAA',
|
||||
{
|
||||
source: 'engine',
|
||||
transcript_kind: 'company_role_result',
|
||||
result_delivery_id: 'equal-length-delivery',
|
||||
},
|
||||
'assistant',
|
||||
),
|
||||
timestamp: 3_100,
|
||||
}
|
||||
for (const groups of [
|
||||
[[equalLengthStableWinner], [equalLengthLoser]],
|
||||
[[equalLengthLoser], [equalLengthStableWinner]],
|
||||
]) {
|
||||
const firstMerge = mergeConversationMessages(groups)
|
||||
assert.equal(firstMerge[0]?.id, 'z-stable-winner')
|
||||
assert.equal(firstMerge[0]?.content, 'BBBB')
|
||||
assert.equal(firstMerge[0]?.timestamp, 3_100)
|
||||
const replayed = mergeConversationMessages([firstMerge, [equalLengthLoser]])
|
||||
assert.equal(replayed[0]?.id, 'z-stable-winner')
|
||||
assert.equal(replayed[0]?.content, 'BBBB')
|
||||
}
|
||||
assert.equal(companyHeaderView?.status, 'running')
|
||||
assert.equal(companyHeaderView?.contextTokens, 0)
|
||||
assert.equal(companyHeaderView?.contextWindow, 128000)
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { ChatMessage } from '../types/chat'
|
||||
import type { ProgressEntry, Session } from '../types/kanban'
|
||||
import { getContextUsageMetrics } from './contextUsage'
|
||||
import { isSessionWorking } from './sessionRuntime'
|
||||
import { stableMessageTimelineKey } from './messageTimelineIdentity'
|
||||
import { stableMessageTimelineKey, stableResultDeliveryKey } from './messageTimelineIdentity'
|
||||
|
||||
const CONTEXT_TOKENS_RE = /(\d[\d,]*)\s*\/\s*(\d[\d,]*)\s+tokens/i
|
||||
const USED_PCT_RE = /(\d{1,3})%\s*used/i
|
||||
@@ -20,22 +20,32 @@ function compactWhitespace(value: string): string {
|
||||
return value.replace(/\s+/g, ' ').trim()
|
||||
}
|
||||
|
||||
function stripNarrativeTitlePrefix(content: string): string {
|
||||
const trimmed = String(content || '').trim()
|
||||
const markdownTitle = trimmed.match(/^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$/)
|
||||
if (markdownTitle) {
|
||||
const body = markdownTitle[2].trim()
|
||||
if (body.length >= 80) return body
|
||||
}
|
||||
const colonIndex = trimmed.indexOf(': ')
|
||||
if (colonIndex < 8 || colonIndex > 160) return trimmed
|
||||
function normalizeResultContentFallback(content: string): string {
|
||||
let normalized = String(content || '')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
.split('\n')
|
||||
.map(line => line.trimEnd())
|
||||
.join('\n')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
|
||||
const prefix = trimmed.slice(0, colonIndex).replace(/\*/g, '').trim()
|
||||
const body = trimmed.slice(colonIndex + 2).trim()
|
||||
if (body.length < 80) return trimmed
|
||||
if (!/[A-Za-z\u4e00-\u9fff]/.test(prefix)) return trimmed
|
||||
if (/^(https?|file)$/i.test(prefix)) return trimmed
|
||||
return body
|
||||
// Some result mirrors add one explicit Markdown narrative label. Only
|
||||
// remove that anchored wrapper; a colon later in Markdown (for example
|
||||
// "**Work Item 1: ..." or "- ID: ...") is message content, not a title.
|
||||
for (;;) {
|
||||
const markdownTitle = normalized.match(/^\*\*([^\r\n]{8,160}?)\*\*:(?:[ \t]+|\r?\n+)([\s\S]+)$/)
|
||||
if (!markdownTitle) break
|
||||
const body = markdownTitle[2].trim()
|
||||
if (body.length < 80 || body === normalized) break
|
||||
normalized = body
|
||||
}
|
||||
|
||||
const paragraphs = normalized.split(/\n{2,}/).map(part => part.trim()).filter(Boolean)
|
||||
if (paragraphs.length > 1 && /^Verification:\s/i.test(paragraphs[paragraphs.length - 1])) {
|
||||
normalized = paragraphs.slice(0, -1).join('\n\n').trim()
|
||||
}
|
||||
return compactWhitespace(normalized).slice(0, 2000)
|
||||
}
|
||||
|
||||
function resultSurfacePriority(message: ChatMessage): number {
|
||||
@@ -67,9 +77,52 @@ function resultSurfacePriority(message: ChatMessage): number {
|
||||
return 0
|
||||
}
|
||||
|
||||
function normalizedResultContentLength(message: ChatMessage): number {
|
||||
return String(message.content ?? '')
|
||||
.replace(/\r\n/g, '\n')
|
||||
.replace(/\r/g, '\n')
|
||||
.split('\n')
|
||||
.map(line => line.trimEnd())
|
||||
.join('\n')
|
||||
.trim()
|
||||
.length
|
||||
}
|
||||
|
||||
function resultAuthorityScore(message: ChatMessage): number {
|
||||
const metadata = (message.metadata ?? {}) as Record<string, unknown>
|
||||
let score = 0
|
||||
if (String(metadata.source ?? '').trim() === 'engine') score += 4
|
||||
if (String(metadata.source_result_message_id ?? '').trim()) score += 2
|
||||
if (metadata.authoritative_output === true || metadata.canonical_result === true) score += 1
|
||||
return score
|
||||
}
|
||||
|
||||
function compareResultSurfacePreference(left: ChatMessage, right: ChatMessage): number {
|
||||
const numericComparisons: Array<[number, number]> = [
|
||||
[resultSurfacePriority(left), resultSurfacePriority(right)],
|
||||
[resultAuthorityScore(left), resultAuthorityScore(right)],
|
||||
[normalizedResultContentLength(left), normalizedResultContentLength(right)],
|
||||
]
|
||||
for (const [leftValue, rightValue] of numericComparisons) {
|
||||
if (leftValue !== rightValue) return leftValue > rightValue ? 1 : -1
|
||||
}
|
||||
const idComparison = left.id.localeCompare(right.id)
|
||||
if (idComparison !== 0) return idComparison
|
||||
const contentComparison = left.content.localeCompare(right.content)
|
||||
if (contentComparison !== 0) return contentComparison
|
||||
// Result chronology is later rewritten to the earliest underlying delivery.
|
||||
// Timestamp is therefore safe only after immutable identity/content have
|
||||
// tied; it must never be able to flip the selected surface on replay.
|
||||
if (left.timestamp === right.timestamp) return 0
|
||||
return left.timestamp > right.timestamp ? 1 : -1
|
||||
}
|
||||
|
||||
export function resultSurfaceDedupeKey(message: ChatMessage): string {
|
||||
if (resultSurfacePriority(message) <= 0) return ''
|
||||
const content = compactWhitespace(stripNarrativeTitlePrefix(message.content)).slice(0, 2000)
|
||||
const deliveryKey = stableResultDeliveryKey(message)
|
||||
if (deliveryKey) return `result:${deliveryKey}`
|
||||
|
||||
const content = normalizeResultContentFallback(message.content)
|
||||
return content ? `result:${content}` : ''
|
||||
}
|
||||
|
||||
@@ -492,7 +545,7 @@ export function mergeConversationMessages(messageGroups: ChatMessage[][]): ChatM
|
||||
const existingIndex = resultKeyIndex.get(resultKey)
|
||||
if (existingIndex !== undefined) {
|
||||
const existing = merged[existingIndex]
|
||||
const candidateWins = resultSurfacePriority(message) > resultSurfacePriority(existing)
|
||||
const candidateWins = compareResultSurfacePreference(message, existing) > 0
|
||||
const preferred = candidateWins ? message : existing
|
||||
const secondary = candidateWins ? existing : message
|
||||
merged[existingIndex] = {
|
||||
@@ -505,7 +558,7 @@ export function mergeConversationMessages(messageGroups: ChatMessage[][]): ChatM
|
||||
metadata: {
|
||||
...(secondary.metadata ?? {}),
|
||||
...(preferred.metadata ?? {}),
|
||||
ui_timeline_id: stableMessageTimelineKey(existing),
|
||||
ui_timeline_id: resultKey || stableMessageTimelineKey(existing),
|
||||
},
|
||||
}
|
||||
continue
|
||||
@@ -541,22 +594,6 @@ export function selectCompanySummaryMessages(
|
||||
messages: ChatMessage[],
|
||||
parentChannelId: string,
|
||||
): ChatMessage[] {
|
||||
const terminalAssistantTurn = (message: ChatMessage): string => {
|
||||
const metadata = (message.metadata ?? {}) as Record<string, unknown>
|
||||
const kind = String(metadata.transcript_kind ?? metadata.kind ?? '').trim()
|
||||
if (kind !== 'runtime_v2_assistant' && kind !== 'runtime_v2_company_assistant') return ''
|
||||
return String(metadata.canonical_turn_id ?? metadata.turn_id ?? '').trim()
|
||||
}
|
||||
const parentTerminalTurns = new Set(
|
||||
messages
|
||||
.filter(message => (
|
||||
message.channelId === parentChannelId
|
||||
&& isMessageVisibleAtDetailLevel(message, 'summary')
|
||||
))
|
||||
.map(terminalAssistantTurn)
|
||||
.filter(Boolean),
|
||||
)
|
||||
const childTerminalByTurn = new Map<string, ChatMessage>()
|
||||
const durableMessages: ChatMessage[] = []
|
||||
for (const message of messages) {
|
||||
if (message.channelId === parentChannelId) {
|
||||
@@ -584,38 +621,8 @@ export function selectCompanySummaryMessages(
|
||||
'top_level_reply',
|
||||
].includes(kind)) {
|
||||
durableMessages.push(message)
|
||||
continue
|
||||
}
|
||||
// Snapshot builder deliberately marks the final runtime surface as
|
||||
// summary-visible. Preserve that durable contract instead of reusing the
|
||||
// result-dedupe priority table as a visibility threshold.
|
||||
const isSummaryTerminal = String(metadata.detail_visibility ?? '').trim() === 'summary'
|
||||
&& (kind === 'runtime_v2_assistant' || kind === 'runtime_v2_company_assistant')
|
||||
if (!isSummaryTerminal) continue
|
||||
const turnId = terminalAssistantTurn(message)
|
||||
if (!turnId) {
|
||||
durableMessages.push(message)
|
||||
continue
|
||||
}
|
||||
if (parentTerminalTurns.has(turnId)) continue
|
||||
const existing = childTerminalByTurn.get(turnId)
|
||||
if (!existing) {
|
||||
childTerminalByTurn.set(turnId, message)
|
||||
continue
|
||||
}
|
||||
const existingPriority = resultSurfacePriority(existing)
|
||||
const candidatePriority = resultSurfacePriority(message)
|
||||
if (candidatePriority > existingPriority) {
|
||||
childTerminalByTurn.set(turnId, message)
|
||||
} else if (
|
||||
candidatePriority === existingPriority
|
||||
&& (message.timestamp > existing.timestamp
|
||||
|| (message.timestamp === existing.timestamp && message.id.localeCompare(existing.id) > 0))
|
||||
) {
|
||||
childTerminalByTurn.set(turnId, message)
|
||||
}
|
||||
}
|
||||
durableMessages.push(...childTerminalByTurn.values())
|
||||
return mergeConversationMessages([durableMessages])
|
||||
}
|
||||
|
||||
|
||||
@@ -472,7 +472,7 @@ async function runTimelineIdentityCases(page: Page, baseUrl: string): Promise<vo
|
||||
collapsed: !!committed.querySelector('.msg-collapse-toggle'),
|
||||
}
|
||||
})
|
||||
assert.equal(finalizedDraft.reused, true, 'draft -> runtime_v2_assistant final must reuse the same outer timeline DOM node')
|
||||
assert.equal(finalizedDraft.reused, true, 'draft -> runtime_v2_company_assistant final must reuse the same outer timeline DOM node')
|
||||
assert.equal(finalizedDraft.collapsed, false, 'a mounted expanded draft must not auto-collapse when its final arrives')
|
||||
assert.ok(
|
||||
finalizedDraft.height >= draftGeometry.height - 4,
|
||||
|
||||
@@ -366,7 +366,8 @@ function Fixture() {
|
||||
mentions: [],
|
||||
metadata: {
|
||||
canonical_turn_id: LIVE_TURN_ID,
|
||||
transcript_kind: 'runtime_v2_assistant',
|
||||
result_delivery_id: `result:${LIVE_TURN_ID}:attempt:0`,
|
||||
transcript_kind: 'runtime_v2_company_assistant',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -341,7 +341,13 @@ export interface ChatMessageMeta {
|
||||
/** UI-only identity retained across semantic result-surface replacement. */
|
||||
ui_timeline_id?: string
|
||||
canonical_turn_id?: string
|
||||
conversation_turn_id?: string
|
||||
turn_id?: string
|
||||
execution_turn_id?: string
|
||||
result_delivery_id?: string
|
||||
source_result_message_id?: string
|
||||
source_task_id?: string
|
||||
child_session_id?: string
|
||||
execution_mode?: string
|
||||
transcript_kind?: string
|
||||
detail_visibility?: DetailVisibility
|
||||
|
||||
@@ -192,23 +192,33 @@ def _normalize_duplicate_content(content: Any) -> str:
|
||||
|
||||
def _strip_narrative_title_prefix(content: str) -> str:
|
||||
trimmed = str(content or "").strip()
|
||||
markdown_title = re.match(r"^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$", trimmed)
|
||||
if markdown_title:
|
||||
# Comparison-only canonicalization may unwrap a deliberate leading title,
|
||||
# but must never interpret a colon inside ordinary Markdown body text as a
|
||||
# wrapper boundary. Resolve explicit nested wrappers to a fixed point so
|
||||
# the comparison key itself is idempotent.
|
||||
while True:
|
||||
markdown_title = re.match(r"^\*\*(.{8,160}?)\*\*:\s+([\s\S]+)$", trimmed)
|
||||
if not markdown_title:
|
||||
return trimmed
|
||||
body = markdown_title.group(2).strip()
|
||||
if len(body) >= 80:
|
||||
return body
|
||||
colon_index = trimmed.find(": ")
|
||||
if colon_index < 8 or colon_index > 160:
|
||||
return trimmed
|
||||
prefix = trimmed[:colon_index].replace("*", "").strip()
|
||||
body = trimmed[colon_index + 2 :].strip()
|
||||
if len(body) < 80:
|
||||
return trimmed
|
||||
if not re.search(r"[A-Za-z\u4e00-\u9fff]", prefix):
|
||||
return trimmed
|
||||
if re.match(r"^(https?|file)$", prefix, flags=re.IGNORECASE):
|
||||
return trimmed
|
||||
return body
|
||||
if len(body) < 80 or body == trimmed:
|
||||
return trimmed
|
||||
trimmed = body
|
||||
|
||||
|
||||
def _select_duplicate_display_content(
|
||||
preferred: dict[str, Any],
|
||||
secondary: dict[str, Any],
|
||||
) -> str:
|
||||
"""Return one original surface body, never the lossy comparison key."""
|
||||
preferred_content = str(preferred.get("content", "") or "")
|
||||
secondary_content = str(secondary.get("content", "") or "")
|
||||
preferred_key = _normalize_duplicate_content(preferred_content)
|
||||
if not preferred_key or preferred_key != _normalize_duplicate_content(secondary_content):
|
||||
return preferred_content
|
||||
if secondary_content.strip() == preferred_key and preferred_content.strip() != preferred_key:
|
||||
return secondary_content
|
||||
return preferred_content
|
||||
|
||||
|
||||
def _normalize_transcript_detail_level(value: Any) -> TranscriptDetailLevel:
|
||||
@@ -1132,7 +1142,19 @@ def _transcript_item_to_ui_message(
|
||||
**({"runtime_thinking": runtime_thinking} if runtime_thinking else {}),
|
||||
**({
|
||||
key: message_metadata.get(key)
|
||||
for key in ("canonical_turn_id", "turn_id")
|
||||
for key in (
|
||||
"canonical_turn_id",
|
||||
"turn_id",
|
||||
"result_delivery_id",
|
||||
"source_result_message_id",
|
||||
"source_task_id",
|
||||
"child_session_id",
|
||||
"conversation_turn_id",
|
||||
"execution_turn_id",
|
||||
"work_item_projection_id",
|
||||
"work_item_turn_type",
|
||||
"runtime_session_id",
|
||||
)
|
||||
if message_metadata.get(key)
|
||||
}),
|
||||
**ui_meta,
|
||||
@@ -1193,18 +1215,10 @@ def collapse_adjacent_transcript_duplicates(messages: list[dict[str, Any]]) -> l
|
||||
):
|
||||
merged_visibility = "full"
|
||||
merged_metadata["detail_visibility"] = merged_visibility
|
||||
preferred_content = str(preferred.get("content", "") or "")
|
||||
secondary_content = str(secondary.get("content", "") or "")
|
||||
normalized_content = _normalize_duplicate_content(preferred_content)
|
||||
merged_content = (
|
||||
normalized_content
|
||||
if normalized_content and normalized_content == _normalize_duplicate_content(secondary_content)
|
||||
else preferred_content
|
||||
)
|
||||
collapsed[-1] = {
|
||||
**secondary,
|
||||
**preferred,
|
||||
"content": merged_content,
|
||||
"content": _select_duplicate_display_content(preferred, secondary),
|
||||
"metadata": merged_metadata,
|
||||
}
|
||||
return collapsed
|
||||
|
||||
@@ -37,6 +37,7 @@ from opc.layer2_organization.work_item_identity import (
|
||||
migrate_work_item_projection_metadata,
|
||||
projection_id_for_work_item,
|
||||
rework_projection_id_for_gate,
|
||||
result_delivery_identity_payload_for_task,
|
||||
target_projection_id_for_decision,
|
||||
target_projection_ids_for_decision,
|
||||
work_item_identity_payload,
|
||||
@@ -217,6 +218,52 @@ class WorkItemProjectionIdentityTests(unittest.TestCase):
|
||||
self.assertEqual(payload[WORK_ITEM_PROJECTION_ID_KEY], "task-proj")
|
||||
self.assertEqual(payload[WORK_ITEM_TURN_TYPE_KEY], "report")
|
||||
|
||||
def test_result_delivery_identity_uses_canonical_turn_and_retry_attempt(self) -> None:
|
||||
task = SimpleNamespace(
|
||||
id="task-1",
|
||||
retry_count=2,
|
||||
metadata={"runtime_v2_current_turn_id": "canonical-turn-1"},
|
||||
)
|
||||
|
||||
payload = result_delivery_identity_payload_for_task(task)
|
||||
|
||||
self.assertEqual(
|
||||
payload["result_delivery_id"],
|
||||
"result:task:task-1:turn:canonical-turn-1:attempt:2",
|
||||
)
|
||||
self.assertEqual(payload["source_task_id"], "task-1")
|
||||
self.assertEqual(payload["canonical_turn_id"], "canonical-turn-1")
|
||||
|
||||
def test_result_delivery_identity_does_not_collide_for_parallel_tasks(self) -> None:
|
||||
first = SimpleNamespace(id="task-1", retry_count=0, metadata={})
|
||||
second = SimpleNamespace(id="task-2", retry_count=0, metadata={})
|
||||
|
||||
first_payload = result_delivery_identity_payload_for_task(
|
||||
first,
|
||||
canonical_turn_id="shared-parent-turn",
|
||||
)
|
||||
second_payload = result_delivery_identity_payload_for_task(
|
||||
second,
|
||||
canonical_turn_id="shared-parent-turn",
|
||||
)
|
||||
|
||||
self.assertNotEqual(
|
||||
first_payload["result_delivery_id"],
|
||||
second_payload["result_delivery_id"],
|
||||
)
|
||||
self.assertIn(":task-1:", first_payload["result_delivery_id"])
|
||||
self.assertIn(":task-2:", second_payload["result_delivery_id"])
|
||||
|
||||
def test_result_delivery_identity_requires_an_execution_scope(self) -> None:
|
||||
task = SimpleNamespace(id="reused-task", retry_count=0, metadata={})
|
||||
|
||||
self.assertEqual(result_delivery_identity_payload_for_task(task), {
|
||||
"source_task_id": "reused-task",
|
||||
})
|
||||
first = result_delivery_identity_payload_for_task(task, execution_id="execution-1")
|
||||
second = result_delivery_identity_payload_for_task(task, execution_id="execution-2")
|
||||
self.assertNotEqual(first["result_delivery_id"], second["result_delivery_id"])
|
||||
|
||||
def test_migrate_projection_metadata_backfills_from_fallbacks_without_overwriting(self) -> None:
|
||||
migrated, changed = migrate_work_item_projection_metadata(
|
||||
{},
|
||||
|
||||
@@ -67,8 +67,95 @@ class MemoryManagerCompactionTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
compactor.maybe_compact_after_message.assert_not_awaited()
|
||||
|
||||
async def test_parent_child_result_preserves_structured_source_lineage(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
store = _MemoryStoreStub()
|
||||
memory = MemoryManager(Path(tmpdir), project_id="0009", store=store)
|
||||
task = Task(
|
||||
id="task-cto-result",
|
||||
title="CTO Result",
|
||||
assigned_to="cto",
|
||||
project_id="0009",
|
||||
session_id="child-session",
|
||||
parent_session_id="parent-session",
|
||||
metadata={"work_item_projection_id": "cto::analysis"},
|
||||
)
|
||||
|
||||
await memory.record_child_session_result(
|
||||
"parent-session",
|
||||
"child-session",
|
||||
task=task,
|
||||
result_content="done",
|
||||
result_delivery_id=(
|
||||
"result:task:task-cto-result:turn:canonical-cto-turn:attempt:0"
|
||||
),
|
||||
source_result_message_id="source-result-message",
|
||||
canonical_turn_id="canonical-cto-turn",
|
||||
)
|
||||
|
||||
metadata = store.session_messages[-1].metadata
|
||||
self.assertEqual(
|
||||
metadata["result_delivery_id"],
|
||||
"result:task:task-cto-result:turn:canonical-cto-turn:attempt:0",
|
||||
)
|
||||
self.assertEqual(metadata["source_result_message_id"], "source-result-message")
|
||||
self.assertEqual(metadata["source_task_id"], "task-cto-result")
|
||||
self.assertEqual(metadata["child_session_id"], "child-session")
|
||||
self.assertEqual(metadata["canonical_turn_id"], "canonical-cto-turn")
|
||||
|
||||
|
||||
class SharedRoleSessionExecutionTests(unittest.IsolatedAsyncioTestCase):
|
||||
def test_fallback_delivery_identity_is_per_result_not_per_provider_session(self) -> None:
|
||||
task = Task(
|
||||
id="shared-role-task",
|
||||
title="Shared role",
|
||||
assigned_to="cto",
|
||||
retry_count=0,
|
||||
)
|
||||
first = TaskResult(
|
||||
status=TaskStatus.AWAITING_HUMAN,
|
||||
content="first pause",
|
||||
artifacts={"provider_session_id": "provider-session-reused"},
|
||||
)
|
||||
second = TaskResult(
|
||||
status=TaskStatus.DONE,
|
||||
content="second result",
|
||||
artifacts={"provider_session_id": "provider-session-reused"},
|
||||
)
|
||||
|
||||
first_identity = OPCEngine._ensure_result_delivery_identity_for_commit(task, first)
|
||||
second_identity = OPCEngine._ensure_result_delivery_identity_for_commit(task, second)
|
||||
|
||||
self.assertNotEqual(
|
||||
first_identity["result_delivery_id"],
|
||||
second_identity["result_delivery_id"],
|
||||
)
|
||||
self.assertNotEqual(
|
||||
first.artifacts["result_execution_id"],
|
||||
second.artifacts["result_execution_id"],
|
||||
)
|
||||
self.assertNotIn("provider-session-reused", first_identity["result_delivery_id"])
|
||||
|
||||
def test_fallback_delivery_identity_is_stable_after_result_persistence(self) -> None:
|
||||
task = Task(id="external-task", title="External", assigned_to="cto")
|
||||
result = TaskResult(
|
||||
status=TaskStatus.DONE,
|
||||
content="done",
|
||||
artifacts={"runtime_session_id": "runtime-session-reused"},
|
||||
)
|
||||
first_identity = OPCEngine._ensure_result_delivery_identity_for_commit(task, result)
|
||||
persisted_artifacts = dict(result.artifacts)
|
||||
reloaded = TaskResult(
|
||||
status=TaskStatus.DONE,
|
||||
content="done",
|
||||
artifacts=persisted_artifacts,
|
||||
)
|
||||
|
||||
replay_identity = OPCEngine._ensure_result_delivery_identity_for_commit(task, reloaded)
|
||||
|
||||
self.assertEqual(replay_identity, first_identity)
|
||||
self.assertEqual(reloaded.artifacts, persisted_artifacts)
|
||||
|
||||
async def test_company_shared_role_session_keeps_results_local(self) -> None:
|
||||
engine = OPCEngine()
|
||||
engine.store = SimpleNamespace(
|
||||
@@ -81,7 +168,11 @@ class SharedRoleSessionExecutionTests(unittest.IsolatedAsyncioTestCase):
|
||||
record_task_completion_async=AsyncMock(),
|
||||
)
|
||||
engine._run_task_once = AsyncMock(
|
||||
return_value=TaskResult(status=TaskStatus.DONE, content="done", artifacts={})
|
||||
return_value=TaskResult(
|
||||
status=TaskStatus.DONE,
|
||||
content="done",
|
||||
artifacts={"result_delivery_id": "delivery-shared-role"},
|
||||
)
|
||||
)
|
||||
engine._apply_runtime_state_to_task = lambda task, result: None
|
||||
|
||||
@@ -105,3 +196,64 @@ class SharedRoleSessionExecutionTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
engine.memory.record_assistant_turn.assert_awaited_once()
|
||||
engine.memory.record_child_session_result.assert_not_awaited()
|
||||
metadata = engine.memory.record_assistant_turn.await_args.kwargs["metadata"]
|
||||
self.assertEqual(metadata["result_delivery_id"], "delivery-shared-role")
|
||||
self.assertEqual(metadata["source_task_id"], "task-cmo-review")
|
||||
self.assertEqual(metadata["child_session_id"], "app14:role:cmo")
|
||||
|
||||
async def test_child_result_and_parent_mirror_share_delivery_identity(self) -> None:
|
||||
engine = OPCEngine()
|
||||
engine.store = SimpleNamespace(
|
||||
get_task=AsyncMock(return_value=None),
|
||||
save_task=AsyncMock(),
|
||||
)
|
||||
engine.memory = SimpleNamespace(
|
||||
record_assistant_turn=AsyncMock(
|
||||
return_value=SimpleNamespace(message_id="source-result-message")
|
||||
),
|
||||
record_child_session_result=AsyncMock(),
|
||||
record_task_completion_async=AsyncMock(),
|
||||
)
|
||||
engine._run_task_once = AsyncMock(
|
||||
return_value=TaskResult(
|
||||
status=TaskStatus.DONE,
|
||||
content="done",
|
||||
artifacts={
|
||||
"canonical_turn_id": "runtime-generated-turn",
|
||||
"result_delivery_id": (
|
||||
"result:task:task-cto-result:turn:runtime-generated-turn:attempt:0"
|
||||
),
|
||||
"runtime_session_id": "runtime-session-1",
|
||||
},
|
||||
)
|
||||
)
|
||||
engine._apply_runtime_state_to_task = lambda task, result: None
|
||||
|
||||
task = Task(
|
||||
id="task-cto-result",
|
||||
title="CTO Result",
|
||||
assigned_to="cto",
|
||||
status=TaskStatus.PENDING,
|
||||
project_id="0009",
|
||||
session_id="child-session",
|
||||
parent_session_id="parent-session",
|
||||
metadata={
|
||||
"execution_mode": "company_mode",
|
||||
"work_item_projection_id": "cto::analysis",
|
||||
},
|
||||
)
|
||||
|
||||
await engine._execute_task(task)
|
||||
|
||||
child_metadata = engine.memory.record_assistant_turn.await_args.kwargs["metadata"]
|
||||
parent_call = engine.memory.record_child_session_result.await_args.kwargs
|
||||
self.assertEqual(
|
||||
child_metadata["result_delivery_id"],
|
||||
"result:task:task-cto-result:turn:runtime-generated-turn:attempt:0",
|
||||
)
|
||||
self.assertEqual(
|
||||
parent_call["result_delivery_id"],
|
||||
child_metadata["result_delivery_id"],
|
||||
)
|
||||
self.assertEqual(parent_call["source_result_message_id"], "source-result-message")
|
||||
self.assertEqual(parent_call["canonical_turn_id"], "runtime-generated-turn")
|
||||
|
||||
@@ -414,6 +414,13 @@ class NativeRuntimeV2Tests(unittest.IsolatedAsyncioTestCase):
|
||||
# cannot swallow it into the frozen shared-iteration row.
|
||||
self.assertTrue(metadata["company_final_turn"])
|
||||
self.assertEqual(metadata["ui_message_id"], "runtime-v2-company-assistant-final:ui-turn:company")
|
||||
self.assertEqual(
|
||||
metadata["result_delivery_id"],
|
||||
"result:task:company-role-task:turn:ui-turn:company:attempt:0",
|
||||
)
|
||||
self.assertEqual(metadata["source_task_id"], "company-role-task")
|
||||
self.assertEqual(metadata["child_session_id"], "sess-company")
|
||||
self.assertEqual(metadata["work_item_projection_id"], "chao::intake")
|
||||
self.assertNotIn("visible_speaker", metadata)
|
||||
|
||||
await runtime._persist_assistant_turn(
|
||||
@@ -429,6 +436,7 @@ class NativeRuntimeV2Tests(unittest.IsolatedAsyncioTestCase):
|
||||
intermediate_metadata = memory.appended_messages[-1]["kwargs"]["metadata"]
|
||||
self.assertEqual(intermediate_metadata["kind"], "runtime_v2_company_assistant")
|
||||
self.assertNotIn("company_final_turn", intermediate_metadata)
|
||||
self.assertNotIn("result_delivery_id", intermediate_metadata)
|
||||
self.assertEqual(
|
||||
intermediate_metadata["ui_message_id"],
|
||||
"runtime-v2-company-assistant:ui-turn:company",
|
||||
|
||||
@@ -16,10 +16,34 @@ from opc.plugins.office_ui.chat_store import (
|
||||
_MessageMatchIndex,
|
||||
_MessageMatchState,
|
||||
)
|
||||
from opc.plugins.office_ui.snapshot_builder import build_transcript_ui_messages
|
||||
from opc.plugins.office_ui.snapshot_builder import (
|
||||
build_transcript_ui_messages,
|
||||
collapse_adjacent_transcript_duplicates,
|
||||
)
|
||||
from opc.plugins.office_ui.ws_handler import WSHandler
|
||||
|
||||
|
||||
_WORK_ITEM_RESULT = """Both work items have been successfully dispatched and can execute in parallel.
|
||||
|
||||
**Dispatch Summary**
|
||||
|
||||
**Work Item 1: OpenOPC Source Code Architecture Deep-Dive Analysis**
|
||||
- ID: `1ed5f5f1-ac41-49a1-b1fa-23bbc9adab82`
|
||||
- Owner: senior_engineer
|
||||
- Scope: `openopc-source-analysis`
|
||||
- Output: `/workspace/openopc-architecture-analysis.md`
|
||||
- Covers: Layered architecture, work-item state machines, collaboration policy, and seat executors.
|
||||
|
||||
**Work Item 2: External Multi-Agent Frameworks Architecture Research**
|
||||
- ID: `d0307208-6b95-44c1-9b51-6bf073bbdcef`
|
||||
- Owner: senior_engineer
|
||||
- Scope: `external-frameworks-research`
|
||||
- Output: `/workspace/external-frameworks-analysis.md`
|
||||
- Covers: Architecture models, coordination, communication, extensibility, and implementation details.
|
||||
|
||||
Both are independent and can execute in parallel."""
|
||||
|
||||
|
||||
class TranscriptStorePaginationTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_summary_page_filters_full_detail_rows_before_limit(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
@@ -216,6 +240,43 @@ class TranscriptStorePaginationTests(unittest.IsolatedAsyncioTestCase):
|
||||
detail_level="summary",
|
||||
))
|
||||
|
||||
def test_renderer_preserves_structured_result_lineage(self) -> None:
|
||||
lineage = {
|
||||
"canonical_turn_id": "turn-canonical",
|
||||
"turn_id": "turn-execution",
|
||||
"result_delivery_id": "delivery-1",
|
||||
"source_result_message_id": "source-message-1",
|
||||
"source_task_id": "source-task-1",
|
||||
"child_session_id": "child-session-1",
|
||||
"conversation_turn_id": "conversation-turn-1",
|
||||
"execution_turn_id": "execution-turn-1",
|
||||
"work_item_projection_id": "architecture",
|
||||
"work_item_turn_type": "delivery",
|
||||
"runtime_session_id": "runtime-session-1",
|
||||
}
|
||||
rendered = build_transcript_ui_messages(
|
||||
[{
|
||||
"message": SimpleNamespace(
|
||||
message_id="result-message-1",
|
||||
role="assistant",
|
||||
agent_id="cto",
|
||||
created_at=datetime(2026, 7, 14, 10, 0, 0),
|
||||
summary_flag=False,
|
||||
metadata={"kind": "child_result", **lineage},
|
||||
),
|
||||
"parts": [SimpleNamespace(
|
||||
part_type="text",
|
||||
payload={"text": "Completed architecture analysis."},
|
||||
)],
|
||||
}],
|
||||
channel_id="session:parent-task",
|
||||
task_id="parent-task",
|
||||
)
|
||||
|
||||
self.assertEqual(len(rendered), 1)
|
||||
for key, value in lineage.items():
|
||||
self.assertEqual(rendered[0]["metadata"].get(key), value)
|
||||
|
||||
|
||||
class ChatStorePaginationTests(unittest.TestCase):
|
||||
@staticmethod
|
||||
@@ -293,6 +354,129 @@ class ChatStorePaginationTests(unittest.TestCase):
|
||||
actual = store._dedupe_messages(messages)
|
||||
self.assertEqual(actual, expected)
|
||||
|
||||
def test_work_item_colons_are_not_treated_as_narrative_wrappers(self) -> None:
|
||||
normalized = ChatStore._normalize_duplicate_content(_WORK_ITEM_RESULT)
|
||||
self.assertEqual(normalized, _WORK_ITEM_RESULT)
|
||||
self.assertEqual(ChatStore._normalize_duplicate_content(normalized), normalized)
|
||||
|
||||
def test_duplicate_merge_keeps_work_item_display_content_across_replays(self) -> None:
|
||||
store = ChatStore(None) # type: ignore[arg-type]
|
||||
current = {
|
||||
"message_id": "runtime-final",
|
||||
"channel_id": "session:cto",
|
||||
"sender": "assistant",
|
||||
"sender_name": "CTO",
|
||||
"content": _WORK_ITEM_RESULT,
|
||||
"created_at": 1.0,
|
||||
"reply_to_id": None,
|
||||
"mentions": [],
|
||||
"metadata": {
|
||||
"source": "engine",
|
||||
"transcript_kind": "runtime_v2_company_assistant",
|
||||
},
|
||||
}
|
||||
canonical = {
|
||||
**current,
|
||||
"message_id": "child-task-result",
|
||||
"created_at": 2.0,
|
||||
"metadata": {
|
||||
"source": "engine",
|
||||
"transcript_kind": "child_task_result",
|
||||
},
|
||||
}
|
||||
|
||||
for _ in range(6):
|
||||
current = store._merge_duplicate_messages(current, canonical)
|
||||
self.assertEqual(current["content"], _WORK_ITEM_RESULT)
|
||||
|
||||
def test_duplicate_merge_reuses_an_original_unwrapped_surface(self) -> None:
|
||||
store = ChatStore(None) # type: ignore[arg-type]
|
||||
body = "Canonical answer " + ("with implementation details. " * 8)
|
||||
wrapped = {
|
||||
"message_id": "runtime-final",
|
||||
"channel_id": "session:answer",
|
||||
"sender": "assistant",
|
||||
"sender_name": "OPC",
|
||||
"content": f"**Top level answer**: {body}",
|
||||
"created_at": 1.0,
|
||||
"metadata": {
|
||||
"source": "engine",
|
||||
"transcript_kind": "runtime_v2_assistant",
|
||||
},
|
||||
}
|
||||
unwrapped = {
|
||||
**wrapped,
|
||||
"message_id": "top-level-reply",
|
||||
"content": body,
|
||||
"created_at": 2.0,
|
||||
"metadata": {
|
||||
"source": "engine",
|
||||
"transcript_kind": "top_level_reply",
|
||||
},
|
||||
}
|
||||
|
||||
merged = store._merge_duplicate_messages(wrapped, unwrapped)
|
||||
self.assertEqual(merged["content"], unwrapped["content"])
|
||||
self.assertIn(merged["content"], (wrapped["content"], unwrapped["content"]))
|
||||
|
||||
def test_result_delivery_identity_is_namespaced_without_canonical_turn_fallback(self) -> None:
|
||||
store = ChatStore(None) # type: ignore[arg-type]
|
||||
existing = {
|
||||
"message_id": "raw-final",
|
||||
"channel_id": "session:result",
|
||||
"sender": "assistant",
|
||||
"content": "runtime wording",
|
||||
"created_at": 1.0,
|
||||
"metadata": {
|
||||
"result_delivery_id": "delivery-1",
|
||||
"canonical_turn_id": "turn-shared",
|
||||
},
|
||||
}
|
||||
delivered = {
|
||||
**existing,
|
||||
"message_id": "child-task-result",
|
||||
"content": "canonical result wording",
|
||||
"metadata": {
|
||||
"result_delivery_id": "delivery-1",
|
||||
"canonical_turn_id": "turn-shared",
|
||||
},
|
||||
}
|
||||
unrelated_user = {
|
||||
**existing,
|
||||
"message_id": "user-turn",
|
||||
"sender": "user",
|
||||
"content": "different user text",
|
||||
"metadata": {"canonical_turn_id": "turn-shared"},
|
||||
}
|
||||
|
||||
self.assertIn("result_delivery:delivery-1", store._message_identity_keys(existing))
|
||||
self.assertTrue(store._messages_semantically_match(existing, delivered))
|
||||
self.assertFalse(store._messages_semantically_match(existing, unrelated_user))
|
||||
|
||||
def test_transcript_collapse_keeps_work_item_display_content_across_replays(self) -> None:
|
||||
runtime = {
|
||||
"message_id": "runtime-final",
|
||||
"sender": "assistant",
|
||||
"sender_name": "CTO",
|
||||
"content": _WORK_ITEM_RESULT,
|
||||
"created_at": 1.0,
|
||||
"metadata": {"transcript_kind": "runtime_v2_company_assistant"},
|
||||
}
|
||||
canonical = {
|
||||
**runtime,
|
||||
"message_id": "child-task-result",
|
||||
"created_at": 2.0,
|
||||
"metadata": {"transcript_kind": "child_task_result"},
|
||||
}
|
||||
|
||||
collapsed = collapse_adjacent_transcript_duplicates([runtime, canonical])
|
||||
self.assertEqual(len(collapsed), 1)
|
||||
self.assertEqual(collapsed[0]["content"], _WORK_ITEM_RESULT)
|
||||
|
||||
replayed = collapse_adjacent_transcript_duplicates([runtime, collapsed[0]])
|
||||
self.assertEqual(len(replayed), 1)
|
||||
self.assertEqual(replayed[0]["content"], _WORK_ITEM_RESULT)
|
||||
|
||||
def test_indexed_dedupe_normalizes_long_content_once_per_row(self) -> None:
|
||||
class CountingChatStore(ChatStore):
|
||||
normalize_calls = 0
|
||||
@@ -501,6 +685,136 @@ class ChatStorePaginationTests(unittest.TestCase):
|
||||
await db.close()
|
||||
tmpdir.cleanup()
|
||||
|
||||
def test_backfill_same_id_repairs_destructively_normalized_content(self) -> None:
|
||||
asyncio.run(self._exercise_backfill_same_id_repairs_content())
|
||||
|
||||
async def _exercise_backfill_same_id_repairs_content(self) -> None:
|
||||
tmpdir = tempfile.TemporaryDirectory()
|
||||
db = _SQLiteConnectionAdapter(str(Path(tmpdir.name) / "ui-state.db"))
|
||||
store = ChatStore(db) # type: ignore[arg-type]
|
||||
await store.initialize()
|
||||
channel_id = "session:work-item-repair"
|
||||
project_id = "test-project"
|
||||
damaged_content = _WORK_ITEM_RESULT[_WORK_ITEM_RESULT.index("OpenOPC Source"):]
|
||||
metadata = {
|
||||
"source": "engine",
|
||||
"transcript_kind": "child_result",
|
||||
"legacy_cache_marker": True,
|
||||
}
|
||||
authoritative = {
|
||||
"message_id": "result-message-1",
|
||||
"sender": "assistant",
|
||||
"sender_name": "CTO",
|
||||
"content": _WORK_ITEM_RESULT,
|
||||
"created_at": 1.0,
|
||||
"metadata": {
|
||||
"source": "engine",
|
||||
"transcript_kind": "child_result",
|
||||
},
|
||||
}
|
||||
try:
|
||||
await store.insert_message(
|
||||
channel_id,
|
||||
"assistant",
|
||||
"CTO",
|
||||
damaged_content,
|
||||
metadata=metadata,
|
||||
message_id="result-message-1",
|
||||
project_id=project_id,
|
||||
created_at=1.0,
|
||||
)
|
||||
|
||||
repaired = await store.backfill_messages(
|
||||
channel_id,
|
||||
[authoritative],
|
||||
project_id=project_id,
|
||||
)
|
||||
replayed = await store.backfill_messages(
|
||||
channel_id,
|
||||
[authoritative],
|
||||
project_id=project_id,
|
||||
)
|
||||
rows = await store.get_channel_messages(
|
||||
channel_id,
|
||||
limit=20,
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
self.assertEqual([message["message_id"] for message in repaired], ["result-message-1"])
|
||||
self.assertEqual(replayed, [])
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0]["content"], _WORK_ITEM_RESULT)
|
||||
self.assertTrue(rows[0]["metadata"].get("legacy_cache_marker"))
|
||||
finally:
|
||||
await db.close()
|
||||
tmpdir.cleanup()
|
||||
|
||||
def test_backfill_semantic_duplicate_upgrades_existing_row_in_place(self) -> None:
|
||||
asyncio.run(self._exercise_backfill_semantic_duplicate_upgrade())
|
||||
|
||||
async def _exercise_backfill_semantic_duplicate_upgrade(self) -> None:
|
||||
tmpdir = tempfile.TemporaryDirectory()
|
||||
db = _SQLiteConnectionAdapter(str(Path(tmpdir.name) / "ui-state.db"))
|
||||
store = ChatStore(db) # type: ignore[arg-type]
|
||||
await store.initialize()
|
||||
channel_id = "session:semantic-result-repair"
|
||||
project_id = "test-project"
|
||||
authoritative_content = _WORK_ITEM_RESULT
|
||||
authoritative = {
|
||||
"message_id": "source-result-message",
|
||||
"sender": "cto",
|
||||
"sender_name": "CTO",
|
||||
"content": authoritative_content,
|
||||
"created_at": 2.0,
|
||||
"metadata": {
|
||||
"source": "engine",
|
||||
"transcript_kind": "child_task_result",
|
||||
},
|
||||
}
|
||||
try:
|
||||
await store.insert_message(
|
||||
channel_id,
|
||||
"assistant",
|
||||
"CTO",
|
||||
f"**Legacy result**: {_WORK_ITEM_RESULT}",
|
||||
metadata={
|
||||
"source": "engine",
|
||||
"transcript_kind": "runtime_v2_company_assistant",
|
||||
"legacy_cache_marker": True,
|
||||
},
|
||||
message_id="mounted-cache-row",
|
||||
project_id=project_id,
|
||||
created_at=1.0,
|
||||
)
|
||||
|
||||
upgraded = await store.backfill_messages(
|
||||
channel_id,
|
||||
[authoritative],
|
||||
project_id=project_id,
|
||||
)
|
||||
replayed = await store.backfill_messages(
|
||||
channel_id,
|
||||
[authoritative],
|
||||
project_id=project_id,
|
||||
)
|
||||
rows = await store.get_channel_messages(
|
||||
channel_id,
|
||||
limit=20,
|
||||
project_id=project_id,
|
||||
)
|
||||
|
||||
self.assertEqual([message["message_id"] for message in upgraded], ["mounted-cache-row"])
|
||||
self.assertEqual(replayed, [])
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0]["message_id"], "mounted-cache-row")
|
||||
self.assertEqual(rows[0]["created_at"], 1.0)
|
||||
self.assertEqual(rows[0]["content"], authoritative_content)
|
||||
self.assertEqual(rows[0]["metadata"].get("transcript_kind"), "child_task_result")
|
||||
self.assertTrue(rows[0]["metadata"].get("legacy_cache_marker"))
|
||||
finally:
|
||||
await db.close()
|
||||
tmpdir.cleanup()
|
||||
|
||||
def test_summary_cache_page_filters_before_raw_fetch_limit(self) -> None:
|
||||
asyncio.run(self._exercise_summary_cache_page())
|
||||
|
||||
|
||||
Reference in New Issue
Block a user