@misterhuydo/sentinel 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.cairn/session.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"message": "Auto-checkpoint at 2026-03-23T08:
|
|
3
|
-
"checkpoint_at": "2026-03-23T08:
|
|
2
|
+
"message": "Auto-checkpoint at 2026-03-23T08:30:55.526Z",
|
|
3
|
+
"checkpoint_at": "2026-03-23T08:30:55.527Z",
|
|
4
4
|
"active_files": [],
|
|
5
5
|
"notes": [],
|
|
6
6
|
"mtime_snapshot": {}
|
package/package.json
CHANGED
|
@@ -177,7 +177,8 @@ When to act vs. when to ask:
|
|
|
177
177
|
- Clear command ("check status", "fetch logs", "pause sentinel") → call the tool immediately, reply with results.
|
|
178
178
|
- Ambiguous or exploratory ("what does get_repo_status do?", "tell me about search_logs") → explain the tool naturally, then ask: "Want me to run it?"
|
|
179
179
|
- Unclear intent (could be either) → use judgment: brief explanation + "Want me to run this now?"
|
|
180
|
-
|
|
180
|
+
- If a tool call will take a moment (search, fetch, pull), prefix your reply with a brief "working" line ending in "..." before the results, e.g. "Searching SSOLWA for TryDig activity..." then the actual output.
|
|
181
|
+
Never just say a working line and stop — always follow it with the results in the same message.
|
|
181
182
|
|
|
182
183
|
Issue identification — before calling create_issue:
|
|
183
184
|
1. Determine if the message is a REAL issue/task (bug report, feature request, investigation ask)
|
|
@@ -1721,6 +1722,80 @@ async def _handle_with_cli(
|
|
|
1721
1722
|
return reply, is_done
|
|
1722
1723
|
|
|
1723
1724
|
|
|
1725
|
+
# ── History serialization helpers ────────────────────────────────────────────
|
|
1726
|
+
|
|
1727
|
+
def _serialize_content(content) -> list:
|
|
1728
|
+
"""Convert Anthropic SDK response content (Pydantic objects) to plain dicts.
|
|
1729
|
+
|
|
1730
|
+
The SDK returns TextBlock / ToolUseBlock instances. json.dumps(..., default=str)
|
|
1731
|
+
turns them into useless strings like "TextBlock(type='text', text='...')".
|
|
1732
|
+
This converts them to proper dicts so history round-trips through SQLite safely.
|
|
1733
|
+
"""
|
|
1734
|
+
if not isinstance(content, list):
|
|
1735
|
+
return content
|
|
1736
|
+
result = []
|
|
1737
|
+
for block in content:
|
|
1738
|
+
if isinstance(block, dict):
|
|
1739
|
+
result.append(block)
|
|
1740
|
+
elif hasattr(block, "model_dump"):
|
|
1741
|
+
result.append(block.model_dump())
|
|
1742
|
+
elif hasattr(block, "dict"):
|
|
1743
|
+
result.append(block.dict())
|
|
1744
|
+
elif hasattr(block, "type"):
|
|
1745
|
+
if block.type == "text":
|
|
1746
|
+
result.append({"type": "text", "text": getattr(block, "text", "")})
|
|
1747
|
+
elif block.type == "tool_use":
|
|
1748
|
+
result.append({
|
|
1749
|
+
"type": "tool_use",
|
|
1750
|
+
"id": getattr(block, "id", ""),
|
|
1751
|
+
"name": getattr(block, "name", ""),
|
|
1752
|
+
"input": getattr(block, "input", {}),
|
|
1753
|
+
})
|
|
1754
|
+
else:
|
|
1755
|
+
result.append({"type": "text", "text": str(block)})
|
|
1756
|
+
return result
|
|
1757
|
+
|
|
1758
|
+
|
|
1759
|
+
def _clean_history(history: list) -> list:
|
|
1760
|
+
"""Remove turns that would cause a 400 from the Anthropic API.
|
|
1761
|
+
|
|
1762
|
+
Strips orphaned tool_use blocks (assistant turn with tool_use but no
|
|
1763
|
+
following tool_result turn) and consecutive same-role turns that result
|
|
1764
|
+
from a previous session that crashed mid-tool-loop.
|
|
1765
|
+
"""
|
|
1766
|
+
cleaned = []
|
|
1767
|
+
i = 0
|
|
1768
|
+
while i < len(history):
|
|
1769
|
+
turn = history[i]
|
|
1770
|
+
role = turn.get("role", "")
|
|
1771
|
+
content = turn.get("content", [])
|
|
1772
|
+
|
|
1773
|
+
# Drop assistant turns that contain tool_use if the next turn isn't tool_result
|
|
1774
|
+
if role == "assistant" and isinstance(content, list):
|
|
1775
|
+
has_tool_use = any(
|
|
1776
|
+
(isinstance(b, dict) and b.get("type") == "tool_use")
|
|
1777
|
+
for b in content
|
|
1778
|
+
)
|
|
1779
|
+
if has_tool_use:
|
|
1780
|
+
next_turn = history[i + 1] if i + 1 < len(history) else None
|
|
1781
|
+
next_content = (next_turn or {}).get("content", [])
|
|
1782
|
+
has_result = isinstance(next_content, list) and any(
|
|
1783
|
+
(isinstance(b, dict) and b.get("type") == "tool_result")
|
|
1784
|
+
for b in next_content
|
|
1785
|
+
)
|
|
1786
|
+
if not has_result:
|
|
1787
|
+
i += 1 # skip orphaned tool_use turn
|
|
1788
|
+
continue
|
|
1789
|
+
|
|
1790
|
+
# Drop consecutive same-role turns (keep the last one)
|
|
1791
|
+
if cleaned and cleaned[-1].get("role") == role:
|
|
1792
|
+
cleaned[-1] = turn
|
|
1793
|
+
else:
|
|
1794
|
+
cleaned.append(turn)
|
|
1795
|
+
i += 1
|
|
1796
|
+
return cleaned
|
|
1797
|
+
|
|
1798
|
+
|
|
1724
1799
|
# ── API-key path (structured tools, full agentic loop) ────────────────────────
|
|
1725
1800
|
|
|
1726
1801
|
async def _handle_with_api(
|
|
@@ -1768,13 +1843,15 @@ async def _handle_with_api(
|
|
|
1768
1843
|
user_content = attach_blocks + [{"type": "text", "text": message}]
|
|
1769
1844
|
else:
|
|
1770
1845
|
user_content = message
|
|
1771
|
-
|
|
1772
|
-
|
|
1846
|
+
|
|
1847
|
+
# Work on a local copy — only commit to history on success to prevent
|
|
1848
|
+
# cascading 400s if the API rejects a malformed/corrupted history.
|
|
1849
|
+
messages = list(history) + [{"role": "user", "content": user_content}]
|
|
1773
1850
|
|
|
1774
1851
|
while True:
|
|
1775
1852
|
response = client.messages.create(
|
|
1776
1853
|
model="claude-opus-4-6",
|
|
1777
|
-
max_tokens=
|
|
1854
|
+
max_tokens=2048,
|
|
1778
1855
|
system=system,
|
|
1779
1856
|
tools=_TOOLS,
|
|
1780
1857
|
messages=messages,
|
|
@@ -1798,10 +1875,12 @@ async def _handle_with_api(
|
|
|
1798
1875
|
# Heuristic override: if reply ends with a question, Claude is waiting for input
|
|
1799
1876
|
if is_done and re.search(r'\?\s*$', reply):
|
|
1800
1877
|
is_done = False
|
|
1801
|
-
history
|
|
1878
|
+
# Commit to history only on success — serialize SDK objects to plain dicts
|
|
1879
|
+
history.append({"role": "user", "content": user_content})
|
|
1880
|
+
history.append({"role": "assistant", "content": _serialize_content(response.content)})
|
|
1802
1881
|
return reply, is_done
|
|
1803
1882
|
|
|
1804
|
-
messages.append({"role": "assistant", "content": response.content})
|
|
1883
|
+
messages.append({"role": "assistant", "content": _serialize_content(response.content)})
|
|
1805
1884
|
tool_results = []
|
|
1806
1885
|
for tc in tool_blocks:
|
|
1807
1886
|
result = await _run_tool(tc.name, tc.input, cfg_loader, store, slack_client=slack_client, user_id=user_id, channel=channel, is_admin=is_admin)
|
|
@@ -23,7 +23,7 @@ from dataclasses import dataclass, field
|
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from typing import Optional
|
|
25
25
|
|
|
26
|
-
from .sentinel_boss import handle_message
|
|
26
|
+
from .sentinel_boss import handle_message, _clean_history
|
|
27
27
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
|
|
@@ -370,9 +370,10 @@ _MAX_HISTORY_TURNS = 20 # keep last 20 exchanges (~40 messages) to stay well w
|
|
|
370
370
|
async def _run_turn(session: _Session, message: str, client, cfg_loader, store, attachments: list | None = None, is_admin: bool = False) -> None:
|
|
371
371
|
channel = session.channel
|
|
372
372
|
|
|
373
|
-
# Load persisted history from DB on the first turn of a new session
|
|
373
|
+
# Load persisted history from DB on the first turn of a new session.
|
|
374
|
+
# Clean it to strip any orphaned tool_use turns from a previous crashed session.
|
|
374
375
|
if not session.history_loaded:
|
|
375
|
-
session.history = store.load_conversation(session.user_id)
|
|
376
|
+
session.history = _clean_history(store.load_conversation(session.user_id))
|
|
376
377
|
session.history_loaded = True
|
|
377
378
|
|
|
378
379
|
# Trim history to avoid context overflow on long conversations
|