Compare commits

...
3 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
Elliot Slusky da841e5282 fix(connectors): sync new Apple Notes (#746)
Fixes #719.
2026-08-14 18:20:16 -07:00
7 changed files with 164 additions and 30 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
}
}
@@ -422,12 +422,17 @@ We recommend creating **one Slack app** that handles both. The App Manifest belo
2. Apple Notes is detected automatically when Full Disk Access is granted
OpenJarvis searches an indexed snapshot rather than querying Notes.app live.
After creating notes, open **Data Sources** and click **Re-sync** on Apple Notes
before searching for the new content.
### Troubleshooting
| Issue | Solution |
|-------|----------|
| "Not connected" despite Full Disk Access | Restart your terminal app after granting access |
| Notes content is garbled | Some very old notes may have encoding issues. Most notes should be clean. |
| New notes are missing | In **Data Sources**, click **Re-sync** on Apple Notes to refresh the index. |
| Missing notes | Only notes stored locally or in iCloud are indexed. Notes in third-party accounts (Gmail, Exchange) may not appear. |
---
+42 -20
View File
@@ -9,8 +9,9 @@ System Settings → Privacy & Security → Full Disk Access.
Timestamp notes
---------------
The Notes database stores modification timestamps as seconds since the Apple
epoch of 2001-01-01 00:00:00 UTC. Conversion formula::
Modern Notes schemas store note modification timestamps in
``ZMODIFICATIONDATE1``; older schemas use ``ZMODIFICATIONDATE``. Both are
seconds since the Apple epoch of 2001-01-01 00:00:00 UTC. Conversion formula::
dt = datetime(2001, 1, 1, tzinfo=utc) + timedelta(seconds=ZMODIFICATIONDATE)
@@ -171,25 +172,46 @@ class AppleNotesConnector(BaseConnector):
return
try:
try:
rows = conn.execute(
"SELECT n.ZIDENTIFIER, "
" COALESCE(n.ZTITLE1, n.ZTITLE, '') AS title, "
" n.ZMODIFICATIONDATE, d.ZDATA "
"FROM ZICCLOUDSYNCINGOBJECT n "
"JOIN ZICNOTEDATA d ON d.ZNOTE = n.Z_PK "
"ORDER BY n.ZMODIFICATIONDATE ASC"
).fetchall()
except sqlite3.OperationalError:
# Older macOS schemas may lack ZTITLE1
rows = conn.execute(
"SELECT n.ZIDENTIFIER, "
" COALESCE(n.ZTITLE, '') AS title, "
" n.ZMODIFICATIONDATE, d.ZDATA "
"FROM ZICCLOUDSYNCINGOBJECT n "
"JOIN ZICNOTEDATA d ON d.ZNOTE = n.Z_PK "
"ORDER BY n.ZMODIFICATIONDATE ASC"
object_columns = {
row[1]
for row in conn.execute(
"PRAGMA table_info(ZICCLOUDSYNCINGOBJECT)"
).fetchall()
}
title_columns = [
f"n.{column}"
for column in ("ZTITLE1", "ZTITLE")
if column in object_columns
]
title_expr = (
f"COALESCE({', '.join(title_columns)}, '')" if title_columns else "''"
)
# Modern Apple Notes stores a note's modification timestamp in
# ZMODIFICATIONDATE1. ZMODIFICATIONDATE is still present in some
# schemas, but applies to other cloud-sync object types and can be
# NULL for notes. Treating that NULL as zero makes incremental
# syncs incorrectly discard newly-created notes as 2001-era data.
modification_columns = [
f"n.{column}"
for column in ("ZMODIFICATIONDATE1", "ZMODIFICATIONDATE")
if column in object_columns
]
modification_expr = (
f"COALESCE({', '.join(modification_columns)}, 0)"
if modification_columns
else "0"
)
rows = conn.execute(
"SELECT n.ZIDENTIFIER, "
f" {title_expr} AS title, "
f" {modification_expr} AS modification_date, d.ZDATA "
"FROM ZICCLOUDSYNCINGOBJECT n "
"JOIN ZICNOTEDATA d ON d.ZNOTE = n.Z_PK "
"ORDER BY modification_date ASC"
).fetchall()
self._items_total = len(rows)
synced = 0
+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 = ""
+63 -2
View File
@@ -8,6 +8,7 @@ from __future__ import annotations
import gzip
import sqlite3
from datetime import datetime, timezone
from pathlib import Path
from typing import List
@@ -24,7 +25,8 @@ from openjarvis.core.registry import ConnectorRegistry
def _create_fake_notes_db(db_path: Path) -> None:
"""Populate a SQLite file with the Apple Notes schema and sample rows."""
conn = sqlite3.connect(str(db_path))
conn.executescript("""
conn.executescript(
"""
CREATE TABLE ZICCLOUDSYNCINGOBJECT (
Z_PK INTEGER PRIMARY KEY,
ZTITLE TEXT,
@@ -38,7 +40,8 @@ def _create_fake_notes_db(db_path: Path) -> None:
ZDATA BLOB,
ZNOTE INTEGER
);
""")
"""
)
# Note 1 — Shopping List
html1 = "<html><body><h1>Shopping List</h1><p>Milk, eggs, bread</p></body></html>"
@@ -208,3 +211,61 @@ def test_registry() -> None:
assert ConnectorRegistry.contains("apple_notes")
cls = ConnectorRegistry.get("apple_notes")
assert cls.connector_id == "apple_notes"
# ---------------------------------------------------------------------------
# Test 10 — modern modification timestamp drives incremental sync
# ---------------------------------------------------------------------------
def test_incremental_sync_uses_modern_note_modification_date(tmp_path: Path) -> None:
"""Modern Notes rows use ZMODIFICATIONDATE1 for incremental sync."""
db_path = tmp_path / "ModernNoteStore.sqlite"
conn = sqlite3.connect(str(db_path))
conn.executescript(
"""
CREATE TABLE ZICCLOUDSYNCINGOBJECT (
Z_PK INTEGER PRIMARY KEY,
ZTITLE TEXT,
ZTITLE1 TEXT,
ZMODIFICATIONDATE REAL,
ZMODIFICATIONDATE1 REAL,
ZIDENTIFIER TEXT
);
CREATE TABLE ZICNOTEDATA (
Z_PK INTEGER PRIMARY KEY,
ZDATA BLOB,
ZNOTE INTEGER
);
"""
)
compressed = gzip.compress(b"<p>New movie list</p>")
conn.execute(
"INSERT INTO ZICCLOUDSYNCINGOBJECT VALUES "
"(1, NULL, 'Movies', NULL, 800000000.0, 'note-modern')"
)
conn.execute("INSERT INTO ZICNOTEDATA VALUES (1, ?, 1)", (compressed,))
conn.commit()
conn.close()
from openjarvis.connectors.apple_notes import AppleNotesConnector # noqa: PLC0415
connector = AppleNotesConnector(db_path=str(db_path))
docs = list(connector.sync(since=datetime(2026, 1, 1, tzinfo=timezone.utc)))
assert [doc.doc_id for doc in docs] == ["apple_notes:note-modern"]
# ---------------------------------------------------------------------------
# Test 11 — legacy modification timestamp remains supported
# ---------------------------------------------------------------------------
def test_incremental_sync_falls_back_to_legacy_modification_date(connector) -> None:
"""Older Notes rows continue to use ZMODIFICATIONDATE."""
docs = list(connector.sync(since=datetime(2023, 1, 1, tzinfo=timezone.utc)))
assert {doc.doc_id for doc in docs} == {
"apple_notes:note-001",
"apple_notes:note-002",
}
+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