forget-connect 0.5.2 → 0.6.1
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/README.md +77 -21
- package/assets/hooks/forget_bstate.py +229 -0
- package/assets/hooks/forget_capture.py +42 -6
- package/assets/hooks/forget_sessionstart.py +41 -4
- package/assets/hooks/forget_toolrecall.py +118 -0
- package/assets/hooks/forget_turnrecall.py +664 -45
- package/assets/plugins/memory-agent/.claude-plugin/plugin.json +15 -0
- package/assets/plugins/memory-agent/.codex-plugin/plugin.json +29 -0
- package/assets/plugins/memory-agent/README.md +9 -0
- package/assets/plugins/memory-agent/skills/memory-agent/SKILL.md +22 -0
- package/assets/plugins/memory-agent/skills/memory-agent/agents/openai.yaml +6 -0
- package/assets/proxy/forget_proxy_watchdog.py +215 -0
- package/assets/skills/memory-agent/SKILL.md +22 -0
- package/assets/skills/memory-agent/agents/openai.yaml +6 -0
- package/package.json +7 -3
- package/src/cli.js +447 -58
- package/src/core.js +28 -9
- package/src/doctor.js +23 -0
- package/src/hooks.js +50 -20
- package/src/proxy.js +526 -0
- package/src/skills.js +95 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""forget_toolrecall — 진행-중 회상 v0 (PostToolUse, Bash 전용).
|
|
3
|
+
|
|
4
|
+
설계 정본: research/proxy-native-redesign.md §3.6 지평 1. 트리거는 사용자
|
|
5
|
+
프롬프트가 아니라 진행 중인 사고의 내용물 — 도구가 **실패**했을 때 그 에러
|
|
6
|
+
서명으로 러닝북을 소환해 결과에 동승시킨다. 원조 사례: 2026-08-12 새벽 pkill
|
|
7
|
+
자기-매칭 침묵 자살 3연속 — 첫 실패 때 러닝북이 도착했다면 한 번으로 끝났다.
|
|
8
|
+
|
|
9
|
+
v0 규율:
|
|
10
|
+
- Bash 결과에만 반응 (서명 전부 Bash-급 함정 + Read/Grep가 러닝북 파일을
|
|
11
|
+
읽을 때의 자기-매칭 차단 — pkill 러닝북이 가르친 바로 그 함정의 훅판).
|
|
12
|
+
- 정적 패턴만, LLM·서버 호출 없음 (ms급 — 지연 예산 논쟁 원천 차단).
|
|
13
|
+
- 실패 판정이 애매한 페이로드는 발동하지 않되 **형상을 원장에 남긴다**
|
|
14
|
+
(계기-우선 — 내일 실측으로 보정). 원장엔 내용 무기록, 형상 메타만.
|
|
15
|
+
- 세션당 같은 러닝북 1회. 텍스트 4000자 초과는 실패 메시지가 아니라
|
|
16
|
+
산출물로 간주하고 침묵.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
RUNBOOKS = Path(os.environ.get("FORGET_RUNBOOKS", str(Path.home() / ".forget/runbooks.md")))
|
|
28
|
+
STATE_DIR = Path.home() / ".forget/hooks/state"
|
|
29
|
+
LEDGER = STATE_DIR / "toolrecall_ledger.jsonl"
|
|
30
|
+
MAX_BODY_CHARS = 700
|
|
31
|
+
MAX_ERR_CHARS = 4000
|
|
32
|
+
|
|
33
|
+
SIGNATURES: list[tuple[str, str]] = [
|
|
34
|
+
(r"contains multiple operations|requires approval", "복합 Bash 명령 승인 회피"),
|
|
35
|
+
(r"brace with quote character", "heredoc·따옴표 중괄호 차단"),
|
|
36
|
+
(r"rm in '.*내-프롬프트를", "워크트리 한글 경로 rm 차단"),
|
|
37
|
+
(r"timed out after|output discarded", "타임아웃 계층 산수"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _note(kind: str, **fields) -> None:
|
|
42
|
+
try:
|
|
43
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
row = {"ts": time.strftime("%F %T"), "kind": kind, **fields}
|
|
45
|
+
with LEDGER.open("a") as fh:
|
|
46
|
+
fh.write(json.dumps(row, ensure_ascii=False) + "\n")
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _response_text(response) -> tuple[str, bool | None]:
|
|
52
|
+
"""(텍스트, 실패 여부) — 실패를 모르면 None."""
|
|
53
|
+
if isinstance(response, str):
|
|
54
|
+
return response, None
|
|
55
|
+
if isinstance(response, dict):
|
|
56
|
+
err = response.get("is_error", response.get("isError"))
|
|
57
|
+
parts = [str(response.get(k) or "") for k in ("error", "stderr", "stdout", "content", "message")]
|
|
58
|
+
text = "\n".join(p for p in parts if p) or json.dumps(response, ensure_ascii=False)
|
|
59
|
+
if err is None and response.get("stderr"):
|
|
60
|
+
err = None # stderr 존재만으로 실패 단정 금지 (경고 흔함)
|
|
61
|
+
return text, bool(err) if err is not None else None
|
|
62
|
+
return "", None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _load_runbook(title: str) -> str | None:
|
|
66
|
+
try:
|
|
67
|
+
text = RUNBOOKS.read_text(encoding="utf-8")
|
|
68
|
+
except Exception:
|
|
69
|
+
return None
|
|
70
|
+
match = re.search(rf"## runbook: {re.escape(title)}.*?(?=\n## |\Z)", text, re.S)
|
|
71
|
+
return match.group(0)[:MAX_BODY_CHARS] if match else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main() -> None:
|
|
75
|
+
try:
|
|
76
|
+
payload = json.load(sys.stdin)
|
|
77
|
+
except Exception:
|
|
78
|
+
return
|
|
79
|
+
if str(payload.get("tool_name") or "") != "Bash":
|
|
80
|
+
return
|
|
81
|
+
text, failed = _response_text(payload.get("tool_response"))
|
|
82
|
+
if not text or len(text) > MAX_ERR_CHARS:
|
|
83
|
+
return
|
|
84
|
+
hit = next((title for pattern, title in SIGNATURES if re.search(pattern, text, re.I)), None)
|
|
85
|
+
if hit is None:
|
|
86
|
+
return
|
|
87
|
+
if failed is False:
|
|
88
|
+
_note("sig_in_success", sig=hit, chars=len(text))
|
|
89
|
+
return
|
|
90
|
+
if failed is None:
|
|
91
|
+
# 형상 미상 — 발동 대신 관측 (내일 이 원장으로 실패 판정을 보정한다)
|
|
92
|
+
_note("sig_shape_unknown", sig=hit, chars=len(text),
|
|
93
|
+
keys=sorted(payload.get("tool_response", {}).keys()) if isinstance(payload.get("tool_response"), dict) else "str")
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
session_id = str(payload.get("session_id") or "nosession")
|
|
97
|
+
seen_path = STATE_DIR / f"toolrecall_{session_id}.json"
|
|
98
|
+
try:
|
|
99
|
+
seen = set(json.loads(seen_path.read_text()))
|
|
100
|
+
except Exception:
|
|
101
|
+
seen = set()
|
|
102
|
+
if hit in seen:
|
|
103
|
+
return
|
|
104
|
+
body = _load_runbook(hit)
|
|
105
|
+
if not body:
|
|
106
|
+
_note("runbook_missing", sig=hit)
|
|
107
|
+
return
|
|
108
|
+
seen.add(hit)
|
|
109
|
+
try:
|
|
110
|
+
seen_path.write_text(json.dumps(sorted(seen), ensure_ascii=False))
|
|
111
|
+
except Exception:
|
|
112
|
+
pass
|
|
113
|
+
_note("injected", sig=hit)
|
|
114
|
+
print(f"[forget 러닝북 — 이 실패 서명엔 검증된 절차가 있음]\n{body}")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
main()
|