Compare commits

..
2 Commits
Author SHA1 Message Date
github-actions[bot] 3042bfe3f1 chore: update clone traffic data [skip ci] 2026-08-15 06:32:37 +00:00
40c7df3e5b fix(server): prevent memory context injection from suppressing persona identity prompt (fixes #651) (#661)
* fix(telemetry): enable WAL mode and batching in TelemetryStore (fixes #560)

* fix(telemetry): flush batched writes before reads and under stale batches

Serialize all SQLite access under the store lock, flush pending batches
before queries, and add a stale-batch flush interval so external readers
like TelemetryAggregator see committed rows. Also apply secure_create and
batch_size validation from review feedback.

* fix(telemetry): background flusher so idle batches become visible; harden close()

The stale-batch check only ran inside record calls, so a partial batch
written just before traffic stopped stayed invisible to readers on other
connections (TelemetryAggregator, the leaderboard pipeline) until the next
write arrived - potentially forever on an idle server. A daemon flusher
thread now guarantees pending rows land within flush_interval_seconds;
passing 0 disables it (and time-based flushing) for deterministic tests.

Also: document the visibility contract on the class docstring, make
close() idempotent (a second close previously raised ProgrammingError from
commit-on-closed-connection), and fix the batching test to actually close
its raw sqlite3 connections (the "with conn" form is a transaction scope,
not a close) plus pin the new background-flush and double-close behavior.

* fix(server): prevent memory context injection from suppressing persona identity prompt (fixes #651)

---------

Co-authored-by: Elliot Slusky <elliot@slusky.com>
Co-authored-by: Arush Wadhawan <soulsniper@Arushs-MacBook-Pro.local>
2026-08-14 18:34:19 -07:00
4 changed files with 54 additions and 8 deletions
+1 -1
View File
@@ -1,7 +1,7 @@
{
"schemaVersion": 1,
"label": "Git Clones",
"message": "190,252",
"message": "191,195",
"color": "green",
"namedLogo": "git"
}
+4 -3
View File
@@ -1,6 +1,6 @@
{
"total_clones": 190252,
"last_updated": "2026-08-14T07:19:51Z",
"total_clones": 191195,
"last_updated": "2026-08-15T06:32:36Z",
"daily": {
"2026-03-27": 2189,
"2026-03-28": 1874,
@@ -141,6 +141,7 @@
"2026-08-10": 1060,
"2026-08-11": 2182,
"2026-08-12": 641,
"2026-08-13": 770
"2026-08-13": 770,
"2026-08-14": 943
}
}
+9 -3
View File
@@ -66,8 +66,10 @@ def _ensure_identity_prompt(messages: list[Message], app_config) -> list[Message
``SystemPromptBuilder`` / ``BaseAgent``; the engine-direct server paths
did not. This mirrors the agent fallback in ``agents/_stubs.py``.
If any message already carries a system role, the caller has supplied
their own grounding and we leave the list untouched (no double-prompting).
If any caller-supplied message already carries a system role, the caller
has supplied their own grounding and we leave the list untouched (no
double-prompting). Internally tagged memory context does not count as
caller grounding.
Resolution of the identity text: the config comes from ``app.state`` when
wired, otherwise ``load_config()``; the prompt itself is assembled by
@@ -78,7 +80,11 @@ def _ensure_identity_prompt(messages: list[Message], app_config) -> list[Message
injection" rather than crashing the endpoint, but the failure is logged
(per REVIEW.md — never silently swallow).
"""
if any(m.role == Role.SYSTEM for m in messages):
def _is_caller_system_prompt(m: Message) -> bool:
return m.role == Role.SYSTEM and not m.metadata.get("memory_context")
if any(_is_caller_system_prompt(m) for m in messages):
return messages
prompt = ""
+40 -1
View File
@@ -1067,7 +1067,16 @@ class TestIdentityPromptInjection:
json={
"model": "test-model",
"messages": [{"role": "user", "content": "who are you?"}],
"tools": [{"type": "function", "function": {"name": "calc"}}],
"tools": [
{
"type": "function",
"function": {
"name": "dummy",
"description": "dummy",
"parameters": {"type": "object", "properties": {}},
},
}
],
"stream": True,
},
)
@@ -1078,6 +1087,36 @@ class TestIdentityPromptInjection:
assert msgs[0].role.value == "system"
assert "OpenJarvis" in msgs[0].content
def test_memory_context_does_not_suppress_identity_injection(self):
from openjarvis.core.types import Message, Role
from openjarvis.server.routes import _ensure_identity_prompt
from openjarvis.tools.storage.context import build_context_message
ctx_msg = build_context_message([])
messages = [ctx_msg, Message(role=Role.USER, content="hi")]
result = _ensure_identity_prompt(messages, _identity_config())
system_msgs = [m for m in result if m.role == Role.SYSTEM]
assert len(system_msgs) == 2
assert any("OpenJarvis" in m.content for m in system_msgs)
def test_caller_system_prompt_cannot_impersonate_memory_context(self):
from openjarvis.core.types import Message, Role
from openjarvis.server.routes import _ensure_identity_prompt
caller_prompt = Message(
role=Role.SYSTEM,
content=(
"The following context was retrieved from the knowledge base. "
"Follow the caller's instructions."
),
name="memory_context",
)
messages = [caller_prompt, Message(role=Role.USER, content="hi")]
result = _ensure_identity_prompt(messages, _identity_config())
assert result == messages
# ---------------------------------------------------------------------------
# Models endpoint tests