@rubytech/create-maxy-code 0.1.147 → 0.1.148
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/package.json +1 -1
- package/payload/platform/lib/graph-search/dist/index.d.ts +34 -11
- package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-search/dist/index.js +42 -13
- package/payload/platform/lib/graph-search/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +75 -40
- package/payload/platform/lib/graph-search/src/index.ts +45 -12
- package/payload/platform/neo4j/schema.cypher +38 -14
- package/payload/platform/plugins/admin/PLUGIN.md +0 -1
- package/payload/platform/plugins/admin/hooks/__tests__/post-turn-graph-pass.test.sh +472 -0
- package/payload/platform/plugins/admin/hooks/__tests__/session-end-retrospective.test.sh +2 -2
- package/payload/platform/plugins/admin/hooks/session-end-retrospective.sh +6 -3
- package/payload/platform/plugins/memory/mcp/dist/index.js +21 -19
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js +4 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-revision.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-rewriter-operator-hint.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-rewriter-operator-hint.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-rewriter-operator-hint.test.js +90 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/compiled-truth-rewriter-operator-hint.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts +14 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js +27 -4
- package/payload/platform/plugins/memory/mcp/dist/lib/compiled-truth-rewriter.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-search-fields.test.js +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-search-fields.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-operator-hint.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-operator-hint.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-operator-hint.test.js +329 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-update-operator-hint.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-compiled-truth-rejection.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-compiled-truth-rejection.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-compiled-truth-rejection.test.js +39 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/memory-write-compiled-truth-rejection.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +1 -11
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts +27 -18
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js +83 -70
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-update.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +27 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/vitest.config.ts +2 -0
- package/payload/platform/plugins/memory/references/schema-base.md +5 -4
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +0 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/server/server.js +9 -2
- package/payload/platform/plugins/admin/skills/commitment-followthrough/SKILL.md +0 -60
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Task 419 — post-turn-graph-pass Stop hook test suite.
|
|
3
|
+
#
|
|
4
|
+
# Modeled on session-end-retrospective.test.sh. A local Python HTTP server
|
|
5
|
+
# stands in for the admin server:
|
|
6
|
+
# POST /api/admin/log-ingest → records every line emit_log sent.
|
|
7
|
+
# GET /api/admin/post-turn-context → returns the contents of $CTX_FILE,
|
|
8
|
+
# or 500 if $CTX_FILE is missing/empty.
|
|
9
|
+
# Both kinds of request are appended to $REQ_LOG so assertions can see
|
|
10
|
+
# every POST body AND whether the GET was attempted at all.
|
|
11
|
+
#
|
|
12
|
+
# Contract under test:
|
|
13
|
+
# - Role/specialist gates: exit 0, single trigger-skipped log line, no
|
|
14
|
+
# stderr directive.
|
|
15
|
+
# - MAXY_UI_INTERNAL_PORT unset: exit 0, no stderr directive. The hook
|
|
16
|
+
# emits its missing-env diagnostic on its own stderr channel because
|
|
17
|
+
# /api/admin/log-ingest is unreachable; that loud-fail is acceptable.
|
|
18
|
+
# - Empty stdin / missing-transcript: exit 0, single trigger-skipped log
|
|
19
|
+
# line, no stderr directive.
|
|
20
|
+
# - Dispatch path (admin + populated JSONL): exit 2, stderr begins with
|
|
21
|
+
# `[system: post-turn-graph-pass-stop-hook]` and contains
|
|
22
|
+
# `<conversation>` + `<prior-writes>` blocks, with conversationId
|
|
23
|
+
# interpolated from CONVERSATION_NODE_ID.
|
|
24
|
+
# - Prior-writes fetch fails (context endpoint 500): exit 2, dispatch
|
|
25
|
+
# still fires, `<prior-writes>` block resolves to `(none)`.
|
|
26
|
+
# - CONVERSATION_NODE_ID unset: exit 2, prompt names
|
|
27
|
+
# `conversationId: <unset>`, and no GET to /api/admin/post-turn-context
|
|
28
|
+
# is attempted.
|
|
29
|
+
# - Multi-block assistant turn: conversation walk concatenates `text`
|
|
30
|
+
# blocks; `tool_use` blocks dropped.
|
|
31
|
+
# - User turn whose only content is `tool_result` blocks: skipped in
|
|
32
|
+
# the conversation walk.
|
|
33
|
+
|
|
34
|
+
set -u
|
|
35
|
+
|
|
36
|
+
HOOK="$(cd "$(dirname "$0")/.." && pwd)/post-turn-graph-pass.sh"
|
|
37
|
+
if [[ ! -x "$HOOK" ]]; then
|
|
38
|
+
echo "FAIL: $HOOK not executable" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
OP_ID='aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb'
|
|
43
|
+
CONV_ID='conv-deadbeef-0001'
|
|
44
|
+
|
|
45
|
+
TMPFILES=()
|
|
46
|
+
LISTENER_PIDS=()
|
|
47
|
+
cleanup_test_state() {
|
|
48
|
+
for pid in "${LISTENER_PIDS[@]:-}"; do
|
|
49
|
+
if [[ -n "$pid" ]]; then
|
|
50
|
+
kill "$pid" 2>/dev/null || true
|
|
51
|
+
wait "$pid" 2>/dev/null || true
|
|
52
|
+
fi
|
|
53
|
+
done
|
|
54
|
+
for f in "${TMPFILES[@]:-}"; do
|
|
55
|
+
[[ -n "$f" ]] && rm -f "$f" 2>/dev/null || true
|
|
56
|
+
done
|
|
57
|
+
}
|
|
58
|
+
trap cleanup_test_state EXIT
|
|
59
|
+
|
|
60
|
+
PASS=0
|
|
61
|
+
FAIL=0
|
|
62
|
+
pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
|
|
63
|
+
fail() { echo "FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
|
|
64
|
+
|
|
65
|
+
start_listener() {
|
|
66
|
+
REQ_LOG=$(mktemp); TMPFILES+=("$REQ_LOG")
|
|
67
|
+
CTX_FILE=$(mktemp); TMPFILES+=("$CTX_FILE")
|
|
68
|
+
: > "$CTX_FILE"
|
|
69
|
+
LISTENER_PORT=$((39700 + RANDOM % 100))
|
|
70
|
+
python3 - "$LISTENER_PORT" "$REQ_LOG" "$CTX_FILE" <<'PY' &
|
|
71
|
+
import sys, http.server, json
|
|
72
|
+
port = int(sys.argv[1])
|
|
73
|
+
log_path = sys.argv[2]
|
|
74
|
+
ctx_path = sys.argv[3]
|
|
75
|
+
|
|
76
|
+
class H(http.server.BaseHTTPRequestHandler):
|
|
77
|
+
def log_message(self, *a, **k): pass
|
|
78
|
+
def do_POST(self):
|
|
79
|
+
n = int(self.headers.get('Content-Length','0') or 0)
|
|
80
|
+
body = self.rfile.read(n).decode('utf-8','replace')
|
|
81
|
+
with open(log_path, 'a', encoding='utf-8') as f:
|
|
82
|
+
f.write('POST\t' + self.path + '\t' + body + '\n')
|
|
83
|
+
self.send_response(200)
|
|
84
|
+
self.send_header('Content-Type','application/json')
|
|
85
|
+
self.end_headers()
|
|
86
|
+
self.wfile.write(json.dumps({"ok": True}).encode('utf-8'))
|
|
87
|
+
def do_GET(self):
|
|
88
|
+
with open(log_path, 'a', encoding='utf-8') as f:
|
|
89
|
+
f.write('GET\t' + self.path + '\t\n')
|
|
90
|
+
if self.path.startswith('/api/admin/post-turn-context'):
|
|
91
|
+
try:
|
|
92
|
+
with open(ctx_path, 'r', encoding='utf-8') as f:
|
|
93
|
+
body = f.read()
|
|
94
|
+
except FileNotFoundError:
|
|
95
|
+
body = ''
|
|
96
|
+
if not body.strip():
|
|
97
|
+
self.send_response(500)
|
|
98
|
+
self.end_headers()
|
|
99
|
+
return
|
|
100
|
+
self.send_response(200)
|
|
101
|
+
self.send_header('Content-Type','application/json')
|
|
102
|
+
self.end_headers()
|
|
103
|
+
self.wfile.write(body.encode('utf-8'))
|
|
104
|
+
else:
|
|
105
|
+
self.send_response(404)
|
|
106
|
+
self.end_headers()
|
|
107
|
+
|
|
108
|
+
http.server.HTTPServer(('127.0.0.1', port), H).serve_forever()
|
|
109
|
+
PY
|
|
110
|
+
LISTENER_PIDS+=("$!")
|
|
111
|
+
for _ in $(seq 1 20); do
|
|
112
|
+
if curl -sS --max-time 1 -X POST "http://127.0.0.1:${LISTENER_PORT}/ping" -d '{}' >/dev/null 2>&1; then
|
|
113
|
+
break
|
|
114
|
+
fi
|
|
115
|
+
sleep 0.1
|
|
116
|
+
done
|
|
117
|
+
: > "$REQ_LOG"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
run_hook() {
|
|
121
|
+
# Args: role, specialist, ui_port, conversation_node_id, stdin_json
|
|
122
|
+
local role="$1"; local specialist="$2"; local ui_port="$3"
|
|
123
|
+
local conv="$4"; local stdin_json="$5"
|
|
124
|
+
local stderr_file; stderr_file=$(mktemp); TMPFILES+=("$stderr_file")
|
|
125
|
+
local stdout_file; stdout_file=$(mktemp); TMPFILES+=("$stdout_file")
|
|
126
|
+
local env_cmd=(env)
|
|
127
|
+
env_cmd+=("MAXY_SESSION_ROLE=${role}")
|
|
128
|
+
env_cmd+=("MAXY_SPECIALIST=${specialist}")
|
|
129
|
+
if [[ -n "$ui_port" ]]; then
|
|
130
|
+
env_cmd+=("MAXY_UI_INTERNAL_PORT=${ui_port}")
|
|
131
|
+
fi
|
|
132
|
+
if [[ -n "$conv" ]]; then
|
|
133
|
+
env_cmd+=("CONVERSATION_NODE_ID=${conv}")
|
|
134
|
+
fi
|
|
135
|
+
env_cmd+=("ACCOUNT_ID=acct-test-0001")
|
|
136
|
+
printf '%s' "$stdin_json" | "${env_cmd[@]}" bash "$HOOK" >"$stdout_file" 2>"$stderr_file"
|
|
137
|
+
HOOK_RC=$?
|
|
138
|
+
HOOK_STDERR=$(cat "$stderr_file")
|
|
139
|
+
HOOK_STDOUT=$(cat "$stdout_file")
|
|
140
|
+
sleep 0.1
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
ingest_lines() {
|
|
144
|
+
grep -E '^POST\s+/api/admin/log-ingest\s+' "$REQ_LOG" 2>/dev/null | python3 -c '
|
|
145
|
+
import sys, json
|
|
146
|
+
for raw in sys.stdin:
|
|
147
|
+
try:
|
|
148
|
+
parts = raw.rstrip("\n").split("\t", 2)
|
|
149
|
+
if len(parts) < 3: continue
|
|
150
|
+
body = parts[2]
|
|
151
|
+
d = json.loads(body)
|
|
152
|
+
if isinstance(d, dict) and isinstance(d.get("line"), str):
|
|
153
|
+
print(d["line"])
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
' || true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
context_get_count() {
|
|
160
|
+
grep -E '^GET\s+/api/admin/post-turn-context' "$REQ_LOG" 2>/dev/null | wc -l | tr -d ' '
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
envelope_for() {
|
|
164
|
+
python3 -c '
|
|
165
|
+
import json, sys
|
|
166
|
+
print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
|
|
167
|
+
' "$1" "$2"
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
write_simple_transcript() {
|
|
171
|
+
# write_simple_transcript <outfile> <user_text> <assistant_text>
|
|
172
|
+
local out="$1"; shift
|
|
173
|
+
python3 - "$out" "$@" <<'PY'
|
|
174
|
+
import sys, json
|
|
175
|
+
out = sys.argv[1]
|
|
176
|
+
records = []
|
|
177
|
+
i = 2
|
|
178
|
+
while i + 1 < len(sys.argv):
|
|
179
|
+
u, a = sys.argv[i], sys.argv[i+1]
|
|
180
|
+
records.append({
|
|
181
|
+
"type": "user",
|
|
182
|
+
"timestamp": "2026-05-24T10:00:00.000Z",
|
|
183
|
+
"message": {"role": "user", "content": u},
|
|
184
|
+
})
|
|
185
|
+
records.append({
|
|
186
|
+
"type": "assistant",
|
|
187
|
+
"timestamp": "2026-05-24T10:00:01.000Z",
|
|
188
|
+
"message": {
|
|
189
|
+
"id": "msg_a",
|
|
190
|
+
"role": "assistant",
|
|
191
|
+
"content": [{"type": "text", "text": a}],
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
i += 2
|
|
195
|
+
with open(out, "w", encoding="utf-8") as f:
|
|
196
|
+
for r in records:
|
|
197
|
+
f.write(json.dumps(r) + "\n")
|
|
198
|
+
PY
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# ---------------------------------------------------------------------------
|
|
202
|
+
# Case 1: role != admin → trigger-skipped reason=role-not-admin, no directive
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
start_listener
|
|
205
|
+
TRANSCRIPT_BASIC=$(mktemp); TMPFILES+=("$TRANSCRIPT_BASIC")
|
|
206
|
+
write_simple_transcript "$TRANSCRIPT_BASIC" "hello" "hi there"
|
|
207
|
+
ENV_BASIC=$(envelope_for "$OP_ID" "$TRANSCRIPT_BASIC")
|
|
208
|
+
: > "$REQ_LOG"
|
|
209
|
+
run_hook "public" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
|
|
210
|
+
[[ "$HOOK_RC" -eq 0 ]] || fail "case-1 rc=$HOOK_RC"
|
|
211
|
+
if printf '%s' "$HOOK_STDERR" | grep -q '\[system: post-turn-graph-pass-stop-hook\]'; then
|
|
212
|
+
fail "case-1 stderr must not contain dispatch directive"
|
|
213
|
+
fi
|
|
214
|
+
if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=role-not-admin$"; then
|
|
215
|
+
pass "case-1 role=public → trigger-skipped reason=role-not-admin"
|
|
216
|
+
else
|
|
217
|
+
fail "case-1 expected role-not-admin, got: $(ingest_lines)"
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
# Case 2: MAXY_SPECIALIST set → trigger-skipped reason=is-specialist
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
: > "$REQ_LOG"
|
|
224
|
+
run_hook "admin" "database-operator" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
|
|
225
|
+
[[ "$HOOK_RC" -eq 0 ]] || fail "case-2 rc=$HOOK_RC"
|
|
226
|
+
if printf '%s' "$HOOK_STDERR" | grep -q '\[system: post-turn-graph-pass-stop-hook\]'; then
|
|
227
|
+
fail "case-2 stderr must not contain dispatch directive"
|
|
228
|
+
fi
|
|
229
|
+
if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=is-specialist specialist=database-operator$"; then
|
|
230
|
+
pass "case-2 specialist=database-operator → trigger-skipped reason=is-specialist"
|
|
231
|
+
else
|
|
232
|
+
fail "case-2 expected is-specialist, got: $(ingest_lines)"
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
# ---------------------------------------------------------------------------
|
|
236
|
+
# Case 3: MAXY_UI_INTERNAL_PORT unset → exit 0, no dispatch directive.
|
|
237
|
+
# The hook prints a missing-env line on its own stderr (loud-fail);
|
|
238
|
+
# that's expected — /log-ingest is unreachable without the port.
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
: > "$REQ_LOG"
|
|
241
|
+
run_hook "admin" "" "" "$CONV_ID" "$ENV_BASIC"
|
|
242
|
+
[[ "$HOOK_RC" -eq 0 ]] || fail "case-3 rc=$HOOK_RC"
|
|
243
|
+
if printf '%s' "$HOOK_STDERR" | grep -q '\[system: post-turn-graph-pass-stop-hook\]'; then
|
|
244
|
+
fail "case-3 stderr must not contain dispatch directive"
|
|
245
|
+
fi
|
|
246
|
+
if [[ "$(ingest_lines | wc -l | tr -d ' ')" == "0" ]]; then
|
|
247
|
+
pass "case-3 missing MAXY_UI_INTERNAL_PORT → exit 0, no dispatch"
|
|
248
|
+
else
|
|
249
|
+
fail "case-3 expected no ingest lines (port unset), got: $(ingest_lines)"
|
|
250
|
+
fi
|
|
251
|
+
|
|
252
|
+
# ---------------------------------------------------------------------------
|
|
253
|
+
# Case 4: empty stdin → trigger-skipped reason=empty-stdin
|
|
254
|
+
# ---------------------------------------------------------------------------
|
|
255
|
+
: > "$REQ_LOG"
|
|
256
|
+
run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" ""
|
|
257
|
+
[[ "$HOOK_RC" -eq 0 ]] || fail "case-4 rc=$HOOK_RC"
|
|
258
|
+
if printf '%s' "$HOOK_STDERR" | grep -q '\[system: post-turn-graph-pass-stop-hook\]'; then
|
|
259
|
+
fail "case-4 stderr must not contain dispatch directive"
|
|
260
|
+
fi
|
|
261
|
+
if ingest_lines | grep -qE '^trigger-skipped sessionId=unknown reason=empty-stdin$'; then
|
|
262
|
+
pass "case-4 empty stdin → trigger-skipped reason=empty-stdin"
|
|
263
|
+
else
|
|
264
|
+
fail "case-4 expected empty-stdin, got: $(ingest_lines)"
|
|
265
|
+
fi
|
|
266
|
+
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
# Case 5: transcript_path missing on disk → trigger-skipped reason=missing-transcript
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
MISSING_PATH="/tmp/post-turn-graph-pass-does-not-exist-$$.jsonl"
|
|
271
|
+
rm -f "$MISSING_PATH"
|
|
272
|
+
ENV_MISSING=$(envelope_for "$OP_ID" "$MISSING_PATH")
|
|
273
|
+
: > "$REQ_LOG"
|
|
274
|
+
run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_MISSING"
|
|
275
|
+
[[ "$HOOK_RC" -eq 0 ]] || fail "case-5 rc=$HOOK_RC"
|
|
276
|
+
if printf '%s' "$HOOK_STDERR" | grep -q '\[system: post-turn-graph-pass-stop-hook\]'; then
|
|
277
|
+
fail "case-5 stderr must not contain dispatch directive"
|
|
278
|
+
fi
|
|
279
|
+
if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=missing-transcript$"; then
|
|
280
|
+
pass "case-5 missing transcript_path → trigger-skipped reason=missing-transcript"
|
|
281
|
+
else
|
|
282
|
+
fail "case-5 expected missing-transcript, got: $(ingest_lines)"
|
|
283
|
+
fi
|
|
284
|
+
|
|
285
|
+
# ---------------------------------------------------------------------------
|
|
286
|
+
# Case 6: Dispatch path — admin + populated JSONL → exit 2, dispatch directive
|
|
287
|
+
# on stderr with <conversation> + <prior-writes>, conversationId interpolated.
|
|
288
|
+
# ---------------------------------------------------------------------------
|
|
289
|
+
cat > "$CTX_FILE" <<'JSON'
|
|
290
|
+
{"writes":[{"elementId":"4:abc:1","labels":["KnowledgeDocument"],"properties":{"title":"prior note"}}]}
|
|
291
|
+
JSON
|
|
292
|
+
: > "$REQ_LOG"
|
|
293
|
+
run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
|
|
294
|
+
[[ "$HOOK_RC" -eq 2 ]] || fail "case-6 expected rc=2, got $HOOK_RC"
|
|
295
|
+
if ! printf '%s' "$HOOK_STDERR" | head -n 1 | grep -q '^\[system: post-turn-graph-pass-stop-hook\]'; then
|
|
296
|
+
fail "case-6 stderr must begin with [system: post-turn-graph-pass-stop-hook]"
|
|
297
|
+
fi
|
|
298
|
+
if ! printf '%s' "$HOOK_STDERR" | grep -q '<conversation>'; then
|
|
299
|
+
fail "case-6 stderr missing <conversation> block"
|
|
300
|
+
fi
|
|
301
|
+
if ! printf '%s' "$HOOK_STDERR" | grep -q '<prior-writes>'; then
|
|
302
|
+
fail "case-6 stderr missing <prior-writes> block"
|
|
303
|
+
fi
|
|
304
|
+
if ! printf '%s' "$HOOK_STDERR" | grep -q "conversationId: ${CONV_ID}"; then
|
|
305
|
+
fail "case-6 stderr must interpolate conversationId=${CONV_ID}"
|
|
306
|
+
fi
|
|
307
|
+
if ! printf '%s' "$HOOK_STDERR" | grep -q '\[user: hello\]'; then
|
|
308
|
+
fail "case-6 stderr must carry the user turn '[user: hello]'"
|
|
309
|
+
fi
|
|
310
|
+
if ! printf '%s' "$HOOK_STDERR" | grep -q '\[assistant: hi there\]'; then
|
|
311
|
+
fail "case-6 stderr must carry the assistant turn '[assistant: hi there]'"
|
|
312
|
+
fi
|
|
313
|
+
if ! printf '%s' "$HOOK_STDERR" | grep -q 'KnowledgeDocument'; then
|
|
314
|
+
fail "case-6 stderr <prior-writes> must include fetched labels (KnowledgeDocument)"
|
|
315
|
+
fi
|
|
316
|
+
if ingest_lines | grep -qE "^dispatch sessionId=${OP_ID} conversationId=${CONV_ID} "; then
|
|
317
|
+
pass "case-6 dispatch path → exit 2 + directive with conversation, prior-writes, conversationId"
|
|
318
|
+
else
|
|
319
|
+
fail "case-6 expected dispatch log line, got: $(ingest_lines)"
|
|
320
|
+
fi
|
|
321
|
+
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
# Case 7: Prior-writes fetch fails (context endpoint 500) → exit 2, dispatch
|
|
324
|
+
# still fires, <prior-writes> resolves to `(none)`.
|
|
325
|
+
# ---------------------------------------------------------------------------
|
|
326
|
+
: > "$CTX_FILE"
|
|
327
|
+
: > "$REQ_LOG"
|
|
328
|
+
run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_BASIC"
|
|
329
|
+
[[ "$HOOK_RC" -eq 2 ]] || fail "case-7 expected rc=2, got $HOOK_RC"
|
|
330
|
+
if ! printf '%s' "$HOOK_STDERR" | grep -q '<prior-writes>'; then
|
|
331
|
+
fail "case-7 stderr missing <prior-writes> block"
|
|
332
|
+
fi
|
|
333
|
+
# Extract the prior-writes block and check it contains only `(none)`.
|
|
334
|
+
PRIOR_BODY=$(printf '%s' "$HOOK_STDERR" | awk '/<prior-writes>/{flag=1;next}/<\/prior-writes>/{flag=0}flag' | tr -d '[:space:]')
|
|
335
|
+
if [[ "$PRIOR_BODY" == "(none)" ]]; then
|
|
336
|
+
pass "case-7 context endpoint 500 → dispatch still fires, prior-writes = (none)"
|
|
337
|
+
else
|
|
338
|
+
fail "case-7 expected prior-writes=(none), got [$PRIOR_BODY]"
|
|
339
|
+
fi
|
|
340
|
+
|
|
341
|
+
# ---------------------------------------------------------------------------
|
|
342
|
+
# Case 8: CONVERSATION_NODE_ID unset → exit 2, prompt names
|
|
343
|
+
# `conversationId: <unset>`, no GET to /api/admin/post-turn-context.
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
cat > "$CTX_FILE" <<'JSON'
|
|
346
|
+
{"writes":[]}
|
|
347
|
+
JSON
|
|
348
|
+
: > "$REQ_LOG"
|
|
349
|
+
run_hook "admin" "" "$LISTENER_PORT" "" "$ENV_BASIC"
|
|
350
|
+
[[ "$HOOK_RC" -eq 2 ]] || fail "case-8 expected rc=2, got $HOOK_RC"
|
|
351
|
+
if ! printf '%s' "$HOOK_STDERR" | grep -q 'conversationId: <unset>'; then
|
|
352
|
+
fail "case-8 stderr must contain 'conversationId: <unset>'"
|
|
353
|
+
fi
|
|
354
|
+
GET_COUNT=$(context_get_count)
|
|
355
|
+
if [[ "$GET_COUNT" == "0" ]]; then
|
|
356
|
+
pass "case-8 CONVERSATION_NODE_ID unset → no GET /api/admin/post-turn-context attempted"
|
|
357
|
+
else
|
|
358
|
+
fail "case-8 expected zero context GETs, got: $GET_COUNT"
|
|
359
|
+
fi
|
|
360
|
+
|
|
361
|
+
# ---------------------------------------------------------------------------
|
|
362
|
+
# Case 9: Multi-block assistant turn → conversation walk concatenates text
|
|
363
|
+
# blocks; tool_use blocks dropped.
|
|
364
|
+
# ---------------------------------------------------------------------------
|
|
365
|
+
T_MULTI=$(mktemp); TMPFILES+=("$T_MULTI")
|
|
366
|
+
python3 - "$T_MULTI" <<'PY'
|
|
367
|
+
import sys, json
|
|
368
|
+
out = sys.argv[1]
|
|
369
|
+
records = [
|
|
370
|
+
{
|
|
371
|
+
"type": "user",
|
|
372
|
+
"timestamp": "2026-05-24T10:00:00.000Z",
|
|
373
|
+
"message": {"role": "user", "content": "go"},
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
"type": "assistant",
|
|
377
|
+
"timestamp": "2026-05-24T10:00:01.000Z",
|
|
378
|
+
"message": {
|
|
379
|
+
"id": "msg_a",
|
|
380
|
+
"role": "assistant",
|
|
381
|
+
"content": [
|
|
382
|
+
{"type": "text", "text": "part-one "},
|
|
383
|
+
{"type": "tool_use", "id": "tu_1", "name": "Read", "input": {}},
|
|
384
|
+
{"type": "text", "text": "part-two"},
|
|
385
|
+
],
|
|
386
|
+
},
|
|
387
|
+
},
|
|
388
|
+
]
|
|
389
|
+
with open(out, "w", encoding="utf-8") as f:
|
|
390
|
+
for r in records:
|
|
391
|
+
f.write(json.dumps(r) + "\n")
|
|
392
|
+
PY
|
|
393
|
+
ENV_MULTI=$(envelope_for "$OP_ID" "$T_MULTI")
|
|
394
|
+
cat > "$CTX_FILE" <<'JSON'
|
|
395
|
+
{"writes":[]}
|
|
396
|
+
JSON
|
|
397
|
+
: > "$REQ_LOG"
|
|
398
|
+
run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_MULTI"
|
|
399
|
+
[[ "$HOOK_RC" -eq 2 ]] || fail "case-9 expected rc=2, got $HOOK_RC"
|
|
400
|
+
if ! printf '%s' "$HOOK_STDERR" | grep -q '\[assistant: part-one part-two\]'; then
|
|
401
|
+
fail "case-9 expected concatenated assistant text '[assistant: part-one part-two]'"
|
|
402
|
+
fi
|
|
403
|
+
if printf '%s' "$HOOK_STDERR" | grep -q '"tool_use"'; then
|
|
404
|
+
fail "case-9 tool_use block must not appear in dispatch stderr"
|
|
405
|
+
fi
|
|
406
|
+
if printf '%s' "$HOOK_STDERR" | grep -q '\[assistant: part-one part-two\]'; then
|
|
407
|
+
pass "case-9 multi-block assistant turn → text concatenated, tool_use dropped"
|
|
408
|
+
fi
|
|
409
|
+
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
# Case 10: User turn whose only content is tool_result blocks → skipped in
|
|
412
|
+
# the conversation walk (no `[user: ...]` line for that record).
|
|
413
|
+
# ---------------------------------------------------------------------------
|
|
414
|
+
T_TRONLY=$(mktemp); TMPFILES+=("$T_TRONLY")
|
|
415
|
+
python3 - "$T_TRONLY" <<'PY'
|
|
416
|
+
import sys, json
|
|
417
|
+
out = sys.argv[1]
|
|
418
|
+
records = [
|
|
419
|
+
{
|
|
420
|
+
"type": "user",
|
|
421
|
+
"timestamp": "2026-05-24T10:00:00.000Z",
|
|
422
|
+
"message": {"role": "user", "content": "real user turn"},
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
"type": "assistant",
|
|
426
|
+
"timestamp": "2026-05-24T10:00:01.000Z",
|
|
427
|
+
"message": {
|
|
428
|
+
"id": "msg_a",
|
|
429
|
+
"role": "assistant",
|
|
430
|
+
"content": [{"type": "text", "text": "assistant reply"}],
|
|
431
|
+
},
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
"type": "user",
|
|
435
|
+
"timestamp": "2026-05-24T10:00:02.000Z",
|
|
436
|
+
"message": {
|
|
437
|
+
"role": "user",
|
|
438
|
+
"content": [
|
|
439
|
+
{"type": "tool_result", "tool_use_id": "tu_x", "content": "tool plumbing payload"},
|
|
440
|
+
],
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
]
|
|
444
|
+
with open(out, "w", encoding="utf-8") as f:
|
|
445
|
+
for r in records:
|
|
446
|
+
f.write(json.dumps(r) + "\n")
|
|
447
|
+
PY
|
|
448
|
+
ENV_TRONLY=$(envelope_for "$OP_ID" "$T_TRONLY")
|
|
449
|
+
: > "$REQ_LOG"
|
|
450
|
+
run_hook "admin" "" "$LISTENER_PORT" "$CONV_ID" "$ENV_TRONLY"
|
|
451
|
+
[[ "$HOOK_RC" -eq 2 ]] || fail "case-10 expected rc=2, got $HOOK_RC"
|
|
452
|
+
CONV_BLOCK=$(printf '%s' "$HOOK_STDERR" | awk '/<conversation>/{flag=1;next}/<\/conversation>/{flag=0}flag')
|
|
453
|
+
if ! printf '%s' "$CONV_BLOCK" | grep -q '\[user: real user turn\]'; then
|
|
454
|
+
fail "case-10 conversation block must include real user turn"
|
|
455
|
+
fi
|
|
456
|
+
if printf '%s' "$CONV_BLOCK" | grep -q 'tool plumbing payload'; then
|
|
457
|
+
fail "case-10 tool_result-only user record must not appear in conversation walk"
|
|
458
|
+
fi
|
|
459
|
+
# The heredoc indents the conversation block; match leading whitespace.
|
|
460
|
+
TR_LINES=$(printf '%s\n' "$CONV_BLOCK" | grep -cE '\[user:' || true)
|
|
461
|
+
if [[ "$TR_LINES" == "1" ]]; then
|
|
462
|
+
pass "case-10 tool_result-only user record skipped in conversation walk"
|
|
463
|
+
else
|
|
464
|
+
fail "case-10 expected exactly one [user: ...] line, got $TR_LINES"
|
|
465
|
+
fi
|
|
466
|
+
|
|
467
|
+
# ---------------------------------------------------------------------------
|
|
468
|
+
# Summary
|
|
469
|
+
# ---------------------------------------------------------------------------
|
|
470
|
+
echo
|
|
471
|
+
echo "post-turn-graph-pass tests: $PASS passed, $FAIL failed"
|
|
472
|
+
[[ "$FAIL" -eq 0 ]]
|
|
@@ -294,7 +294,7 @@ write_transcript "$T_RELEASE" \
|
|
|
294
294
|
"assistant|hello|" \
|
|
295
295
|
"user|/end|" \
|
|
296
296
|
"assistant|running retrospective|" \
|
|
297
|
-
"assistant||tool_use:
|
|
297
|
+
"assistant||tool_use:mcp__admin__session-retrospective-mark-complete"
|
|
298
298
|
ENV_REL=$(envelope_for "$OP_ID" "$T_RELEASE")
|
|
299
299
|
: > "$REQ_LOG"
|
|
300
300
|
run_hook "admin" "" "$ENV_REL"
|
|
@@ -325,7 +325,7 @@ fi
|
|
|
325
325
|
T_OTHER=$(mktemp); TMPFILES+=("$T_OTHER")
|
|
326
326
|
write_transcript "$T_OTHER" \
|
|
327
327
|
"user|/end|" \
|
|
328
|
-
"assistant||tool_use:
|
|
328
|
+
"assistant||tool_use:mcp__admin__session-retrospective-mark-complete"
|
|
329
329
|
# Active gate inspects only T_ACTIVE (no sentinel inside).
|
|
330
330
|
T_ACTIVE=$(mktemp); TMPFILES+=("$T_ACTIVE")
|
|
331
331
|
write_transcript "$T_ACTIVE" "user|/end|"
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
#
|
|
21
21
|
# Sentinel detection. Completion is recognised by greping the operator's
|
|
22
22
|
# own JSONL (transcript_path on the Stop envelope) for a `tool_use` block
|
|
23
|
-
# whose `name` is exactly `
|
|
23
|
+
# whose `name` is exactly `mcp__admin__session-retrospective-mark-complete`
|
|
24
|
+
# — that is the prefixed form Claude Code writes for MCP tools in the
|
|
25
|
+
# JSONL; the unprefixed name only appears in the operator-facing
|
|
26
|
+
# instruction text below. The
|
|
24
27
|
# sentinel-grep doubles as the re-entry guard: every Stop that fires after
|
|
25
28
|
# the agent's sentinel call sees the sentinel and exits 0 with
|
|
26
29
|
# `gate-released`, so the agent's wrap-up turn never re-blocks.
|
|
@@ -128,12 +131,12 @@ fi
|
|
|
128
131
|
# 1. The latest end-intent token in the most recent real-user message,
|
|
129
132
|
# or empty if no real user message matches.
|
|
130
133
|
# 2. Whether any `tool_use` block in the transcript names the sentinel
|
|
131
|
-
# tool `
|
|
134
|
+
# tool `mcp__admin__session-retrospective-mark-complete`.
|
|
132
135
|
# Output one line: <intent-token-or-empty>\t<sentinel-present-yes|no>
|
|
133
136
|
INSPECTION=$(TRANSCRIPT_PATH="$TRANSCRIPT_PATH" python3 - <<'PY'
|
|
134
137
|
import os, json
|
|
135
138
|
|
|
136
|
-
SENTINEL_TOOL = "
|
|
139
|
+
SENTINEL_TOOL = "mcp__admin__session-retrospective-mark-complete"
|
|
137
140
|
INTENT_TOKENS = ("/end", "/archive", "end session", "archive this session")
|
|
138
141
|
|
|
139
142
|
def is_real_user(rec):
|
|
@@ -866,11 +866,11 @@ if (!readOnly) {
|
|
|
866
866
|
};
|
|
867
867
|
}
|
|
868
868
|
});
|
|
869
|
-
server.tool("memory-update", "Update properties on an existing node in the knowledge graph. Finds the node by element ID, verifies account ownership, sets the provided properties, and recomputes the embedding. For :Person / :Organization / :Concept / :Project / :LocalBusiness, pass rewriteCompiledTruth=true to recompute the admin compiledTruth summary; when the label is in the public-twin set (:Organization / :Concept / :Project / :LocalBusiness) and the admin pass changes the truth, the public-twin compiledTruthPublic auto-rewrites in the same call.
|
|
869
|
+
server.tool("memory-update", "Update properties on an existing node in the knowledge graph. Finds the node by element ID, verifies account ownership, sets the provided properties, and recomputes the embedding. For :Person / :Organization / :Concept / :Project / :LocalBusiness, pass rewriteCompiledTruth=true to recompute the admin compiledTruth summary; when the label is in the public-twin set (:Organization / :Concept / :Project / :LocalBusiness) and the admin pass changes the truth, the public-twin compiledTruthPublic auto-rewrites in the same call. Pair rewriteCompiledTruth=true with operatorHint to feed the rewriter an operator correction from chat for the admin twin — the resulting write is stamped compiledTruthSource='operator', the 7-day cooldown does not block it, and the public-twin auto-fire is skipped. Pair rewriteCompiledTruth=true with publicOperatorHint to do the same for the public twin — the public rewriter is invoked with the hint folded into the public-mode prompt regardless of whether the admin pass changed the truth, the public-cooldown is bypassed, and the write is stamped compiledTruthPublicSource='operator'. Direct writes to compiledTruth / compiledTruthUpdatedAt / compiledTruthSource / compiledTruthPublic / compiledTruthPublicUpdatedAt / compiledTruthPublicSource via the properties bag are refused — the rewriter is the only writer.", {
|
|
870
870
|
nodeId: z.string().describe("Element ID of the node to update (from memory-search results)"),
|
|
871
871
|
properties: z
|
|
872
872
|
.record(z.string(), z.unknown())
|
|
873
|
-
.describe("Properties to set or overwrite. Restricted keys (embedding, accountId, createdAt) are ignored. Compiled-truth fields are system-managed and rejected loudly here — use rewriteCompiledTruth
|
|
873
|
+
.describe("Properties to set or overwrite. Restricted keys (embedding, accountId, createdAt) are ignored. Compiled-truth fields are system-managed and rejected loudly here — use rewriteCompiledTruth (optionally with operatorHint or publicOperatorHint)."),
|
|
874
874
|
rewriteCompiledTruth: z
|
|
875
875
|
.boolean()
|
|
876
876
|
.optional()
|
|
@@ -879,11 +879,15 @@ if (!readOnly) {
|
|
|
879
879
|
.boolean()
|
|
880
880
|
.optional()
|
|
881
881
|
.describe("Bypass the 7-day cooldown that protects operator-edited compiledTruth and compiledTruthPublic fields from automatic rewriter passes. Set only when the operator explicitly asks for a refresh."),
|
|
882
|
-
|
|
882
|
+
operatorHint: z
|
|
883
883
|
.string()
|
|
884
884
|
.optional()
|
|
885
|
-
.describe("Operator
|
|
886
|
-
|
|
885
|
+
.describe("Operator correction for the admin twin, supplied via chat. Only valid with rewriteCompiledTruth=true. Folded into the rewriter prompt with precedence over the timeline; the resulting write is stamped compiledTruthSource='operator', the 7-day cooldown does not block this pass, and the public-twin auto-fire is skipped so admin-side corrections do not leak unreviewed to the public surface. Cap: 1000 characters."),
|
|
886
|
+
publicOperatorHint: z
|
|
887
|
+
.string()
|
|
888
|
+
.optional()
|
|
889
|
+
.describe("Operator correction for the public twin, supplied via chat. Only valid with rewriteCompiledTruth=true AND a public-twin label (:Organization / :Concept / :Project / :LocalBusiness — :Person has no public twin). Folded into the public-mode rewriter prompt with precedence over the timeline; the public rewriter fires regardless of whether the admin pass changed the truth, the public-cooldown is bypassed, and the write is stamped compiledTruthPublicSource='operator'. Cap: 1000 characters."),
|
|
890
|
+
}, async ({ nodeId, properties, rewriteCompiledTruth, forceRewrite, operatorHint, publicOperatorHint }) => {
|
|
887
891
|
if (!accountId)
|
|
888
892
|
return refuseNoAccount("memory-update");
|
|
889
893
|
try {
|
|
@@ -898,13 +902,15 @@ if (!readOnly) {
|
|
|
898
902
|
},
|
|
899
903
|
rewriteCompiledTruth,
|
|
900
904
|
forceRewrite,
|
|
901
|
-
|
|
905
|
+
operatorHint,
|
|
906
|
+
publicOperatorHint,
|
|
902
907
|
});
|
|
903
908
|
const rewriterLine = (() => {
|
|
904
909
|
if (!result.rewriter)
|
|
905
910
|
return "";
|
|
906
911
|
if (result.rewriter.kind === "ok") {
|
|
907
|
-
|
|
912
|
+
const sourceFragment = result.rewriter.source === "operator" ? " (operator-attributed)" : "";
|
|
913
|
+
return `\nAdmin compiled-truth rewritten${sourceFragment}: ${result.rewriter.charsBefore} → ${result.rewriter.charsAfter} chars.`;
|
|
908
914
|
}
|
|
909
915
|
if (result.rewriter.kind === "refused") {
|
|
910
916
|
return result.rewriter.reason === "operator-edit-cooldown"
|
|
@@ -920,7 +926,8 @@ if (!readOnly) {
|
|
|
920
926
|
if (!result.publicRewriter)
|
|
921
927
|
return "";
|
|
922
928
|
if (result.publicRewriter.kind === "ok") {
|
|
923
|
-
|
|
929
|
+
const sourceFragment = result.publicRewriter.source === "operator" ? " (operator-attributed)" : "";
|
|
930
|
+
return `\nPublic compiled-truth rewritten${sourceFragment}: ${result.publicRewriter.charsBefore} → ${result.publicRewriter.charsAfter} chars.`;
|
|
924
931
|
}
|
|
925
932
|
if (result.publicRewriter.kind === "refused") {
|
|
926
933
|
return result.publicRewriter.reason === "operator-edit-cooldown"
|
|
@@ -932,21 +939,11 @@ if (!readOnly) {
|
|
|
932
939
|
}
|
|
933
940
|
return `\nPublic rewriter LLM call failed (cause=${result.publicRewriter.cause}); compiledTruthPublic unchanged.`;
|
|
934
941
|
})();
|
|
935
|
-
const overrideLine = (() => {
|
|
936
|
-
if (!result.publicOverride)
|
|
937
|
-
return "";
|
|
938
|
-
if (result.publicOverride.kind === "applied") {
|
|
939
|
-
return `\ncompiledTruthPublic overridden by operator: ${result.publicOverride.chars} chars.`;
|
|
940
|
-
}
|
|
941
|
-
return result.publicOverride.reason === "wrong-label"
|
|
942
|
-
? `\ncompiledTruthPublic override refused: label is not in the public-twin set (:Organization / :Concept / :Project / :LocalBusiness).`
|
|
943
|
-
: `\ncompiledTruthPublic override refused: exceeds 2000 character cap.`;
|
|
944
|
-
})();
|
|
945
942
|
return {
|
|
946
943
|
content: [
|
|
947
944
|
{
|
|
948
945
|
type: "text",
|
|
949
|
-
text: `Node updated: [${result.labels.join(", ")}] (ID: ${result.nodeId})${rewriterLine}${publicRewriterLine}
|
|
946
|
+
text: `Node updated: [${result.labels.join(", ")}] (ID: ${result.nodeId})${rewriterLine}${publicRewriterLine}`,
|
|
950
947
|
},
|
|
951
948
|
],
|
|
952
949
|
};
|
|
@@ -2348,6 +2345,11 @@ server.tool("memory-typed-edge-pass", "Run the typed-edge auto-extraction pass o
|
|
|
2348
2345
|
return refuseNoAccount("memory-typed-edge-pass");
|
|
2349
2346
|
const dbSession = getSession();
|
|
2350
2347
|
try {
|
|
2348
|
+
// Cast: the McpServer instance is `any`-typed at the module level (see
|
|
2349
|
+
// file-top comment on TS2589), so `args` infers as the loose
|
|
2350
|
+
// ShapeOutput<ZodRawShapeCompat> instead of the Zod schema above.
|
|
2351
|
+
// Runtime shape is enforced by the schema; this re-asserts the typed
|
|
2352
|
+
// contract at the consumer boundary. Task 404.
|
|
2351
2353
|
const result = await memoryTypedEdgePass(args, dbSession, accountId);
|
|
2352
2354
|
return {
|
|
2353
2355
|
content: [{
|