@misterhuydo/sentinel 1.1.7 → 1.1.9
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-23T09:
|
|
3
|
-
"checkpoint_at": "2026-03-23T09:
|
|
2
|
+
"message": "Auto-checkpoint at 2026-03-23T09:20:28.044Z",
|
|
3
|
+
"checkpoint_at": "2026-03-23T09:20:28.045Z",
|
|
4
4
|
"active_files": [],
|
|
5
5
|
"notes": [],
|
|
6
6
|
"mtime_snapshot": {}
|
package/package.json
CHANGED
|
@@ -187,6 +187,11 @@ Session context — critical rules:
|
|
|
187
187
|
- When handling a new request, call the tools fresh. Do not assume any prior tool result is still current or that any prior step "counts" toward the current task.
|
|
188
188
|
- The only exception: if the user explicitly asks about something from the history ("what did you find earlier?"), you may reference it — but note it is from a prior session.
|
|
189
189
|
|
|
190
|
+
Avoid redundant tool calls (within a single response only — always run tools fresh for new requests):
|
|
191
|
+
- If a broad search (e.g. search_logs with no source filter) already returned results in THIS response, do NOT repeat the same search with a source filter to "refine" — use what you already fetched.
|
|
192
|
+
- If a tool call fails in THIS response, do NOT retry the entire search from scratch. Continue with what succeeded and note the failure.
|
|
193
|
+
- One pass per task: gather all needed data in a single round of tool calls, then produce the final answer.
|
|
194
|
+
|
|
190
195
|
Issue identification — before calling create_issue:
|
|
191
196
|
1. Determine if the message is a REAL issue/task (bug report, feature request, investigation ask)
|
|
192
197
|
vs. a status question, tool query, or casual chat. If not an issue, just answer normally.
|
|
@@ -779,6 +784,48 @@ def _git_pull(path: Path) -> dict:
|
|
|
779
784
|
return {"status": "error", "detail": str(e)}
|
|
780
785
|
|
|
781
786
|
|
|
787
|
+
# ── Log-source name resolver ──────────────────────────────────────────────────
|
|
788
|
+
|
|
789
|
+
def _filter_log_sources(props_files: list, source_hint: str) -> list:
|
|
790
|
+
"""
|
|
791
|
+
Return the subset of props_files whose log source matches source_hint.
|
|
792
|
+
|
|
793
|
+
Matching is tried in order (first match wins per file):
|
|
794
|
+
1. Substring of the filename stem (e.g. "sts" → STS.properties)
|
|
795
|
+
2. Substring of REMOTE_SERVICE_USER (e.g. "ssolwa" → ...SSOLoginWebApp...)
|
|
796
|
+
3. Substring of HOSTS (e.g. hostname fragment)
|
|
797
|
+
|
|
798
|
+
Case-insensitive throughout. An empty source_hint returns all files unchanged.
|
|
799
|
+
"""
|
|
800
|
+
if not source_hint:
|
|
801
|
+
return props_files
|
|
802
|
+
hint = source_hint.lower()
|
|
803
|
+
|
|
804
|
+
def _props_contains(path: Path, key: str, hint: str) -> bool:
|
|
805
|
+
try:
|
|
806
|
+
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
|
|
807
|
+
stripped = line.strip()
|
|
808
|
+
if stripped.startswith("#"):
|
|
809
|
+
continue
|
|
810
|
+
if stripped.upper().startswith(key + "="):
|
|
811
|
+
val = stripped.split("=", 1)[1].partition("#")[0].strip().lower()
|
|
812
|
+
if hint in val:
|
|
813
|
+
return True
|
|
814
|
+
except OSError:
|
|
815
|
+
pass
|
|
816
|
+
return False
|
|
817
|
+
|
|
818
|
+
matched = []
|
|
819
|
+
for p in props_files:
|
|
820
|
+
if hint in p.stem.lower():
|
|
821
|
+
matched.append(p)
|
|
822
|
+
elif _props_contains(p, "REMOTE_SERVICE_USER", hint):
|
|
823
|
+
matched.append(p)
|
|
824
|
+
elif _props_contains(p, "HOSTS", hint):
|
|
825
|
+
matched.append(p)
|
|
826
|
+
return matched
|
|
827
|
+
|
|
828
|
+
|
|
782
829
|
# ── Tool execution ────────────────────────────────────────────────────────────
|
|
783
830
|
|
|
784
831
|
async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=None, user_id: str = "", channel: str = "", is_admin: bool = False) -> str:
|
|
@@ -1016,9 +1063,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
1016
1063
|
script = Path(__file__).resolve().parent.parent / "scripts" / "fetch_log.sh"
|
|
1017
1064
|
log_cfg_dir = Path("config") / "log-configs"
|
|
1018
1065
|
if script.exists() and log_cfg_dir.exists():
|
|
1019
|
-
props_files = sorted(log_cfg_dir.glob("*.properties"))
|
|
1020
|
-
if source:
|
|
1021
|
-
props_files = [p for p in props_files if source in p.stem.lower()]
|
|
1066
|
+
props_files = _filter_log_sources(sorted(log_cfg_dir.glob("*.properties")), source)
|
|
1022
1067
|
if props_files:
|
|
1023
1068
|
live_results = []
|
|
1024
1069
|
for props in props_files:
|
|
@@ -1165,9 +1210,7 @@ async def _run_tool(name: str, inputs: dict, cfg_loader, store, slack_client=Non
|
|
|
1165
1210
|
if not log_cfg_dir.exists():
|
|
1166
1211
|
return json.dumps({"error": "config/log-configs/ not found"})
|
|
1167
1212
|
|
|
1168
|
-
props_files = sorted(log_cfg_dir.glob("*.properties"))
|
|
1169
|
-
if source_filter:
|
|
1170
|
-
props_files = [p for p in props_files if source_filter in p.stem.lower()]
|
|
1213
|
+
props_files = _filter_log_sources(sorted(log_cfg_dir.glob("*.properties")), source_filter)
|
|
1171
1214
|
if not props_files:
|
|
1172
1215
|
return json.dumps({"error": f"No log-config found matching '{source_filter}'"})
|
|
1173
1216
|
|
|
@@ -367,31 +367,61 @@ async def _dispatch(event: dict, client, cfg_loader, store) -> None:
|
|
|
367
367
|
|
|
368
368
|
_MAX_HISTORY_TURNS = 20 # keep last 20 exchanges (~40 messages) to stay well within context limits
|
|
369
369
|
|
|
370
|
+
|
|
371
|
+
def _strip_tool_turns(history: list) -> list:
|
|
372
|
+
"""
|
|
373
|
+
Remove any message that consists entirely of tool_use or tool_result blocks.
|
|
374
|
+
Keeps only plain text exchanges so loaded prior history doesn't contain stale
|
|
375
|
+
search results that the model might treat as already-done work.
|
|
376
|
+
"""
|
|
377
|
+
result = []
|
|
378
|
+
for msg in history:
|
|
379
|
+
content = msg.get("content", "")
|
|
380
|
+
# Plain string content — always keep
|
|
381
|
+
if isinstance(content, str):
|
|
382
|
+
result.append(msg)
|
|
383
|
+
continue
|
|
384
|
+
# List content — keep only if it has at least one text block
|
|
385
|
+
if isinstance(content, list):
|
|
386
|
+
has_text = any(
|
|
387
|
+
isinstance(b, dict) and b.get("type") == "text" and b.get("text", "").strip()
|
|
388
|
+
for b in content
|
|
389
|
+
)
|
|
390
|
+
if has_text:
|
|
391
|
+
# Keep only the text blocks, drop tool_use/tool_result
|
|
392
|
+
text_blocks = [b for b in content if isinstance(b, dict) and b.get("type") == "text"]
|
|
393
|
+
result.append({**msg, "content": text_blocks})
|
|
394
|
+
return result
|
|
395
|
+
|
|
370
396
|
async def _run_turn(session: _Session, message: str, client, cfg_loader, store, attachments: list | None = None, is_admin: bool = False) -> None:
|
|
371
397
|
channel = session.channel
|
|
372
398
|
|
|
373
399
|
# Load persisted history from DB on the first turn of a new session.
|
|
374
|
-
# - _clean_history strips orphaned tool_use turns from
|
|
375
|
-
# -
|
|
376
|
-
#
|
|
377
|
-
#
|
|
400
|
+
# - _clean_history strips orphaned tool_use turns from previous crashed sessions.
|
|
401
|
+
# - Strip all tool_use / tool_result blocks from prior history — only keep conversational
|
|
402
|
+
# text. Stale search results in prior history cause the model to skip re-running tools
|
|
403
|
+
# for new requests ("I already searched that"), which produces wrong/empty answers.
|
|
404
|
+
# - Trim to last 6 text exchanges (12 messages) to limit context bleed.
|
|
405
|
+
# - Inject a session boundary marker so the model clearly separates old from new.
|
|
378
406
|
if not session.history_loaded:
|
|
379
407
|
loaded = _clean_history(store.load_conversation(session.user_id))
|
|
380
408
|
if loaded:
|
|
381
|
-
|
|
409
|
+
text_only = _strip_tool_turns(loaded)
|
|
382
410
|
_PRIOR_TURNS = 6
|
|
383
|
-
trimmed =
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
411
|
+
trimmed = text_only[-(_PRIOR_TURNS * 2):]
|
|
412
|
+
if trimmed:
|
|
413
|
+
session.history = [
|
|
414
|
+
{
|
|
415
|
+
"role": "user",
|
|
416
|
+
"content": "[system: new session started — the following is prior conversation context only, no tool calls needed]",
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
"role": "assistant",
|
|
420
|
+
"content": [{"type": "text", "text": "Understood. Starting fresh — prior context is for reference only."}],
|
|
421
|
+
},
|
|
422
|
+
] + trimmed
|
|
423
|
+
else:
|
|
424
|
+
session.history = []
|
|
395
425
|
else:
|
|
396
426
|
session.history = []
|
|
397
427
|
session.history_loaded = True
|