@rubytech/create-realagent-code 0.1.115 → 0.1.117
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/neo4j/schema.cypher +73 -0
- package/payload/platform/plugins/admin/PLUGIN.md +2 -0
- package/payload/platform/plugins/admin/hooks/signal-detector-stop.sh +309 -0
- package/payload/platform/plugins/docs/PLUGIN.md +1 -0
- package/payload/platform/plugins/docs/references/admin-session.md +1 -1
- package/payload/platform/plugins/docs/references/deployment.md +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +7 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +32 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js +62 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/schema-cypher-drift.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-signals-recent.d.ts +35 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-signals-recent.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-signals-recent.js +73 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-signals-recent.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/vitest.config.ts +2 -0
- package/payload/platform/scripts/seed-neo4j.sh +2 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +12 -0
- package/payload/platform/templates/specialists/agents/signal-detector.md +129 -0
- package/payload/server/public/assets/{admin-DOxHsV7g.js → admin-D6IfAzYY.js} +1 -1
- package/payload/server/public/assets/{data-NMvGx6jm.js → data-BGbQyufe.js} +1 -1
- package/payload/server/public/assets/{graph-BkwTCV2Y.js → graph-D-1lRTeL.js} +1 -1
- package/payload/server/public/assets/{graph-labels-h39S93Xo.js → graph-labels-BRXJHNYE.js} +1 -1
- package/payload/server/public/assets/{page-B0iMmY6t.js → page-B4oirCvn.js} +1 -1
- package/payload/server/public/assets/{page-zQ79YCEt.js → page-FmJ7PIYx.js} +2 -2
- package/payload/server/public/data.html +3 -3
- package/payload/server/public/graph.html +3 -3
- package/payload/server/public/index.html +4 -4
- package/payload/server/server.js +14 -0
package/package.json
CHANGED
|
@@ -1100,3 +1100,76 @@ FOR (h:CloudflareHostname) REQUIRE (h.accountId, h.hostnameValue) IS UNIQUE;
|
|
|
1100
1100
|
|
|
1101
1101
|
CREATE INDEX cloudflare_hostname_account IF NOT EXISTS
|
|
1102
1102
|
FOR (h:CloudflareHostname) ON (h.accountId);
|
|
1103
|
+
|
|
1104
|
+
// ----------------------------------------------------------
|
|
1105
|
+
// Estate-agent ontology (Task 358) — Listing-centric model
|
|
1106
|
+
// documented at platform/plugins/memory/references/schema-estate-agent.md.
|
|
1107
|
+
//
|
|
1108
|
+
// These declarations exist primarily so the memory-write validator's
|
|
1109
|
+
// parseLabelsFromSchemaCypher() registers each label on a fresh deploy
|
|
1110
|
+
// where no node of the label has yet landed — without them, the first
|
|
1111
|
+
// :Listing write from the listing-curator agent is rejected as "unknown
|
|
1112
|
+
// label" against an empty db.labels() set.
|
|
1113
|
+
//
|
|
1114
|
+
// Idempotency contracts mirror schema-estate-agent.md:
|
|
1115
|
+
// :Listing / :Viewing / :Offer — (accountId, sourceSystem, sourceId)
|
|
1116
|
+
// :Property — (accountId, slug)
|
|
1117
|
+
// :ImageObject — (accountId, listingSlug, url) for the
|
|
1118
|
+
// estate-agent variant, (accountId, name,
|
|
1119
|
+
// contentUrl) for the brand-asset variant.
|
|
1120
|
+
// Because the two writers use different
|
|
1121
|
+
// natural keys, only an accountId index is
|
|
1122
|
+
// declared at the schema level — uniqueness
|
|
1123
|
+
// is enforced at write time by the curator
|
|
1124
|
+
// / brand-asset writer's MERGE clause.
|
|
1125
|
+
// ----------------------------------------------------------
|
|
1126
|
+
|
|
1127
|
+
CREATE CONSTRAINT listing_account_source_unique IF NOT EXISTS
|
|
1128
|
+
FOR (l:Listing) REQUIRE (l.accountId, l.sourceSystem, l.sourceId) IS UNIQUE;
|
|
1129
|
+
|
|
1130
|
+
CREATE INDEX listing_account IF NOT EXISTS
|
|
1131
|
+
FOR (l:Listing) ON (l.accountId);
|
|
1132
|
+
|
|
1133
|
+
CREATE INDEX listing_status IF NOT EXISTS
|
|
1134
|
+
FOR (l:Listing) ON (l.status);
|
|
1135
|
+
|
|
1136
|
+
CREATE INDEX listing_type IF NOT EXISTS
|
|
1137
|
+
FOR (l:Listing) ON (l.listingType);
|
|
1138
|
+
|
|
1139
|
+
CREATE VECTOR INDEX listing_embedding IF NOT EXISTS
|
|
1140
|
+
FOR (l:Listing) ON (l.embedding)
|
|
1141
|
+
OPTIONS {
|
|
1142
|
+
indexConfig: {
|
|
1143
|
+
`vector.dimensions`: 768,
|
|
1144
|
+
`vector.similarity_function`: 'cosine'
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1148
|
+
CREATE CONSTRAINT property_account_slug_unique IF NOT EXISTS
|
|
1149
|
+
FOR (p:Property) REQUIRE (p.accountId, p.slug) IS UNIQUE;
|
|
1150
|
+
|
|
1151
|
+
CREATE INDEX property_account IF NOT EXISTS
|
|
1152
|
+
FOR (p:Property) ON (p.accountId);
|
|
1153
|
+
|
|
1154
|
+
CREATE CONSTRAINT viewing_account_source_unique IF NOT EXISTS
|
|
1155
|
+
FOR (v:Viewing) REQUIRE (v.accountId, v.sourceSystem, v.sourceId) IS UNIQUE;
|
|
1156
|
+
|
|
1157
|
+
CREATE INDEX viewing_account IF NOT EXISTS
|
|
1158
|
+
FOR (v:Viewing) ON (v.accountId);
|
|
1159
|
+
|
|
1160
|
+
CREATE CONSTRAINT offer_account_source_unique IF NOT EXISTS
|
|
1161
|
+
FOR (o:Offer) REQUIRE (o.accountId, o.sourceSystem, o.sourceId) IS UNIQUE;
|
|
1162
|
+
|
|
1163
|
+
CREATE INDEX offer_account IF NOT EXISTS
|
|
1164
|
+
FOR (o:Offer) ON (o.accountId);
|
|
1165
|
+
|
|
1166
|
+
CREATE INDEX image_object_account IF NOT EXISTS
|
|
1167
|
+
FOR (i:ImageObject) ON (i.accountId);
|
|
1168
|
+
|
|
1169
|
+
// :PostalAddress (schema:PostalAddress) — shared address nodes documented in
|
|
1170
|
+
// schema-base.md. Address rows are deliberately accountId-free (one address
|
|
1171
|
+
// can be shared across customers / orders / listings); writers MERGE on the
|
|
1172
|
+
// formatted address triple. Declared here so a fresh deploy registers the
|
|
1173
|
+
// label before the first :PostalAddress write lands.
|
|
1174
|
+
CREATE INDEX postal_address_postcode IF NOT EXISTS
|
|
1175
|
+
FOR (a:PostalAddress) ON (a.postalCode);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: admin
|
|
3
|
+
surface: platform
|
|
3
4
|
description: "Platform administration plugin. Provides system-status, public-hostname (deterministic Cloudflare public-URL resolver, single call returning the operator's canonical hostname so agents never guess property names on :CloudflareHostname nodes), brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, skill-load (one-call resolve+read for SKILL.md by skill name, the canonical primitive for loading a named skill; plugin-read remains the reader for references/* and PLUGIN.md), store-skill (deterministic write counterpart to plugin-read; persists operator-authored skills as plugin files under the active account), session-reset, session-resume, file-attach, wifi, action-approval tools (action-pending, action-approve, action-reject, action-edit), and session-retrospective-mark-complete (the deterministic sentinel the session-end Stop hook reads via JSONL grep to release the retrospective gate; admin calls it once per session after running the three-pass retrospective) for managing the Maxy platform."
|
|
4
5
|
tools:
|
|
5
6
|
- name: system-status
|
|
@@ -136,6 +137,7 @@ Tools are available via the `admin` MCP server.
|
|
|
136
137
|
- `hooks/webfetch-preflight.mjs` — short-circuits WebFetch on JS-SPA shells with a structured `WEBFETCH_CANNOT_READ_JS_SPA` error so the agent surfaces a loud failure to the owner instead of paying the 60s extraction timeout. Fail-open on any internal error.
|
|
137
138
|
- `hooks/askuserquestion-investigate-gate.sh` — PreToolUse matcher=`AskUserQuestion`. Blocks the question (exit 2) when no read-only investigation tool has fired since the latest real user turn in the session JSONL. The structural fix for the failure class where the agent fabricates a menu before evidence-gathering (session `c085ec2c-46fb-4b73-8865-68cf85866ea8` 2026-05-22 — "change remote access password" → invented options "Admin PIN / Cloudflare tunnel / WiFi password" with zero prior tool_use; post-correction the agent immediately fired `remote-auth-status` → `ToolSearch` → `remote-auth-set-password`, proving it knew the moves). **Allowlist** (exact, with trailing `__<tool>` suffix-match for namespaced `mcp__plugin_<plugin>_<server>__<tool>` aliases): `ToolSearch`, `Grep`, `Glob`, `Read`, `LS`, `NotebookRead`, `Bash`, `WebFetch`, `WebSearch`, plus the read-only admin / memory MCP tools (`*-status`, `*-list`, `*-read`, `skill-find`, `memory-find-candidates`, `profile-read`, `conversation-list`, `memory-list-attachments`, `memory-read-attachment`). **Block message:** `Blocked: AskUserQuestion requires at least one investigation tool (ToolSearch, Grep, Read, *-list, *-status, *-read, skill-find, ...) earlier in this turn. Search the operator's literal phrase first.` **Log line** (stderr, one per call): `[ask-gate] decision=<allow|block> sessionId=<id8> seen=<csv|-> reason=<allowlist-hit|no-investigation|fail-open-no-transcript|fail-open-parse-error>`. **Fail-open** on missing transcript or parse error — nudges, never bricks the UI.
|
|
138
139
|
- `hooks/session-end-retrospective.sh` — **Stop hook.** Fires on every assistant end_turn but exits 0 with `trigger-skipped reason=<role-not-admin|is-specialist|empty-stdin|missing-transcript|no-intent-match>` on every path EXCEPT when the operator's latest real-user message carries an end-intent token (`/end`, `/archive`, `end session`, `archive this session`, case-insensitive, must match the whole message or a standalone line — not embedded in prose). On that path the hook blocks (exit 2 + structured retrospective instruction on stderr per Stop-hook contract) and emits `[session-retrospective] gate-blocked sessionId=<id> reason=sentinel-absent` until the admin agent calls the `session-retrospective-mark-complete` MCP sentinel tool. Completion is recognised by greping the operator's own JSONL (`transcript_path` on the Stop envelope) for a `tool_use` block whose `name` equals that exact string — never by parsing prose ([[feedback_no_stdout_parsing_for_control_flow]], [[feedback_doctrine_paragraph_is_not_a_gate]]). The sentinel-grep doubles as the re-entry guard: every Stop after the sentinel call sees it and exits 0 with `gate-released sessionId=<id>`. Recursion guard: any session whose `MAXY_SPECIALIST` env is set (specialist subagent dispatched via Task tool) skips the gate. The Sidebar archive button POSTs `/:sessionId/archive` directly to the manager which kills the PTY — no Stop fires after that, so clicking archive is the operator's current escape hatch (opt-out remains deferred per the task spec). All emits go through `POST /api/admin/log-ingest`; the only stderr writer is the gate-blocked path (the instruction block the agent reads). The retrospective itself runs as one or more additional turns inside the operator's existing admin session — Task-tool delegation to `database-operator` is the existing IDENTITY.md "Recording" route, not a parallel admin session ([[feedback_deterministic_means_remove_llm]]).
|
|
140
|
+
- `hooks/signal-detector-stop.sh` — **Stop hook.** Task 303 — gbrain port. Fires on every admin-agent end_turn alongside `session-end-retrospective.sh` (both registered on the Stop matcher). Inspects the operator's JSONL transcript and decides whether to dispatch the `signal-detector` specialist for background brain-compounding. Throttle gates: ≥3 new real-user turns since the most recent in-transcript Task call with `subagent_type=signal-detector` (overridable via `MAXY_SIGNAL_DETECTOR_MIN_NEW_TURNS`) AND ≥30 000 ms since the latest real-user message's `timestamp` (overridable via `MAXY_SIGNAL_DETECTOR_MIN_IDLE_MS`). Below either threshold → exit 0 with `trigger-skipped reason=below-{turn,idle}-threshold`. Recursion guard mirrors `session-end-retrospective.sh`: any session with `MAXY_SPECIALIST` set on the PTY env is skipped, plus the in-transcript Task-call grep means the very next Stop after the agent's dispatch turn sees the call and exits 0 (`reason=already-dispatched`). Gate-blocked path emits exit 2 + stderr directive `[system: signal-detector-stop-hook]` per Stop-hook contract; the admin IDENTITY.md "Stop-hook system directives" section binds the agent's response shape (perform one `Task` dispatch, emit a single-space ack, stop). First-fire bootstrap: idempotently copies the template from `$MAXY_PLATFORM_ROOT/templates/specialists/agents/signal-detector.md` into `$ACCOUNT_DIR/specialists/agents/` and appends the AGENTS.md routing line so the specialist is available platform-core without onboarding-flow surgery. All emits go through `POST /api/admin/log-ingest` under tag `signal-detector-hook`. Operator-channel doctrine: by design, only the admin agent's stop fires this hook (public/visitor channels never do — high-value signal flows through the operator, not the visitor).
|
|
139
141
|
- `hooks/turn-completed-graph-write.sh` — **Dormant since Task 214** — the Stop-hook registration is no longer written to account settings.json; admin delegates graph writes to `database-operator` via the Task tool. The script, envelope walker, loopback `/api/admin/claude-sessions` 127.0.0.1 bypass, `[turn-recorder]` emitters, recorder-auto-archive subscriber, and `initialMessage` envelope spec are preserved as reusable infrastructure for any future autonomous post-turn flow. Historical contract (still describes the dormant path): Stop hook fired once per completed admin-agent turn. Gates on `MAXY_SESSION_ROLE=admin` + `MAXY_SPECIALIST!=database-operator` so it never recurses into the recorder PTY or fires on public sessions. **Task 147** — the recorder is spawned via the same route Sidebar uses. ONE POST to `POST /api/admin/claude-sessions`, body carries `{specialist:'database-operator', model:'haiku', initialMessage:<json-envelope>, adminSessionId:<op>}` — no synthetic `senderId: 'turn-recorder'` marker. The Hono wrapper bypasses cookie auth on this exact method+path when the request originates from `127.0.0.1` (same trust boundary the claude-session-manager itself relies on), looks up the operator's senderId from the manager's `/<adminSessionId>/meta`, and forwards a Sidebar-shape spawn body. The `/recorder-spawn` sibling route is gone. The hook reads its UI port from `MAXY_UI_INTERNAL_PORT` (stamped on the manager systemd unit) — no fallback; absence emits `[turn-recorder] spawn-failed reason=missing-env env=MAXY_UI_INTERNAL_PORT` to stderr instead of silently 19199-ing. **Task 177** — `initialMessage` is a JSON-stringified envelope; the schema lives in [`platform/plugins/docs/references/admin-session.md`](../docs/references/admin-session.md) under "`initialMessage` JSON envelope (Task 177)". Top-level keys exactly: `turns`, `conversationId`, `accountId`, `occurredAt`. `turns` is the full conversation transcript, oldest first, newest last, with each entry `{ role: "user"|"assistant", text, ts, toolCalls? }`. No windowing, no truncation. Multi-record assistant messages collapse on `message.id`. `tool_use` and matching `tool_result` blocks attach as a single `toolCalls` entry on the owning assistant turn; the user record carrying only the `tool_result` does not create a separate user turn. No leading instruction prose. `toolCalls[].input` and `toolCalls[].output` are native JSON values, never re-stringified. (Replaces Task 175's `(operatorMessage, assistantReply)` pair, which asserted a temporal Q→A relationship the walker never enforced.) One observability line `[turn-recorder] envelope sessionId=<op> turnsCount=<n> userTurns=<n> assistantTurns=<n> toolCallTurns=<n>` precedes `spawn-request`. The envelope rides on the `/spawn` body as a trailing positional argv to `claude`, so the database-operator session's JSONL first `role=user` line is the JSON object verbatim. No separate `POST /:id/input` call, no bracketed-paste, no keystroke injection. The recorder-auto-archive subscriber stops the recorder PTY as soon as its JSONL contains an assistant message with `stop_reason === "end_turn"`. **Task 129** — every emit goes through `POST /api/admin/log-ingest` so the lines land in `server.log` keyed by the operator session id. The chain is `trigger` → `spawn-request` → `spawn-success` → `input-delivered` → `tool-call` × N → `tool-result` × N → `write-complete` → `auto-archive`; each gated-off path emits one `trigger-skipped reason=<role-not-admin|is-recorder|empty-stdin|missing-transcript|conversation-empty>` line. Failure modes — `spawn-failed`, `tool-surface-missing`, `input-failed`, `write-empty`, `auto-archive reason=stale-recorder` — each emit one named line; absence is itself a defect.
|
|
140
142
|
|
|
141
143
|
## Session identifiers (Task 135)
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Stop hook — gbrain signal-detector dispatch. Task 303.
|
|
3
|
+
#
|
|
4
|
+
# Fires on every admin-agent assistant end_turn. Inspects the operator's
|
|
5
|
+
# JSONL transcript and decides whether to invoke the `signal-detector`
|
|
6
|
+
# specialist to extract entity mentions and original-thinking candidates
|
|
7
|
+
# from the conversation slice since the last dispatch.
|
|
8
|
+
#
|
|
9
|
+
# Trigger logic (must clear both gates to fire):
|
|
10
|
+
# - >= MIN_NEW_TURNS new real-user messages since the most recent
|
|
11
|
+
# in-transcript Task dispatch with subagent_type='signal-detector'
|
|
12
|
+
# (default 3 — overridable via MAXY_SIGNAL_DETECTOR_MIN_NEW_TURNS).
|
|
13
|
+
# - >= MIN_IDLE_MS milliseconds since the latest real-user message's
|
|
14
|
+
# `timestamp` (default 30_000 — overridable via
|
|
15
|
+
# MAXY_SIGNAL_DETECTOR_MIN_IDLE_MS). Captures "operator paused, not
|
|
16
|
+
# mid-thought".
|
|
17
|
+
#
|
|
18
|
+
# Recursion guard. The dispatch resolves through a Task tool call carrying
|
|
19
|
+
# `subagent_type=signal-detector`; once such a call appears in the transcript
|
|
20
|
+
# after the latest real-user message, the gate has already fired for this
|
|
21
|
+
# turn and must NOT re-fire. Same shape as
|
|
22
|
+
# session-end-retrospective.sh's sentinel grep
|
|
23
|
+
# ([[feedback_no_stdout_parsing_for_control_flow]] /
|
|
24
|
+
# [[feedback_doctrine_paragraph_is_not_a_gate]]) — control flow gates on a
|
|
25
|
+
# structural tool_use, never on prose.
|
|
26
|
+
#
|
|
27
|
+
# Specialist auto-install on first fire. The template ships at
|
|
28
|
+
# `$MAXY_TEMPLATES_DIR/specialists/agents/signal-detector.md` (or
|
|
29
|
+
# `$ACCOUNT_DIR/../templates/...` via PLATFORM_ROOT). On the first fire
|
|
30
|
+
# that decides to dispatch, the hook idempotently copies the template into
|
|
31
|
+
# `$ACCOUNT_DIR/specialists/agents/` and appends the AGENTS.md routing line
|
|
32
|
+
# if absent — so signal-detector is platform-core (no operator install
|
|
33
|
+
# step) without surgery on the onboarding flow.
|
|
34
|
+
#
|
|
35
|
+
# Gating (emits via `/api/admin/log-ingest`; stderr stays silent on every
|
|
36
|
+
# skip path; the gate-blocked path is the only stderr emitter, by Stop-hook
|
|
37
|
+
# contract — exit 2 + stderr is how the agent receives the dispatch
|
|
38
|
+
# directive):
|
|
39
|
+
# - MAXY_SESSION_ROLE must equal "admin" → reason=role-not-admin
|
|
40
|
+
# - MAXY_SPECIALIST must be empty → reason=is-specialist
|
|
41
|
+
# - Stop-hook stdin must be non-empty → reason=empty-stdin
|
|
42
|
+
# - transcript_path must exist on disk → reason=missing-transcript
|
|
43
|
+
# - new real-user turns since last fire < MIN_NEW → reason=below-turn-threshold
|
|
44
|
+
# - idle_ms < MIN_IDLE_MS → reason=below-idle-threshold
|
|
45
|
+
# - latest real-user message older than the latest → reason=already-dispatched
|
|
46
|
+
# signal-detector dispatch
|
|
47
|
+
#
|
|
48
|
+
# Blocks:
|
|
49
|
+
# - gate-blocked sessionId=<id> reason=dispatching newTurns=<n> idleMs=<n>
|
|
50
|
+
#
|
|
51
|
+
# Input: Claude Code's Stop hook stdin shape
|
|
52
|
+
# { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>", ... }
|
|
53
|
+
|
|
54
|
+
set -uo pipefail
|
|
55
|
+
|
|
56
|
+
MIN_NEW_TURNS="${MAXY_SIGNAL_DETECTOR_MIN_NEW_TURNS:-3}"
|
|
57
|
+
MIN_IDLE_MS="${MAXY_SIGNAL_DETECTOR_MIN_IDLE_MS:-30000}"
|
|
58
|
+
|
|
59
|
+
UI_PORT="${MAXY_UI_INTERNAL_PORT:-}"
|
|
60
|
+
if [ -z "$UI_PORT" ]; then
|
|
61
|
+
echo "[signal-detector-hook] trigger-skipped sessionId=unknown reason=missing-env env=MAXY_UI_INTERNAL_PORT" >&2
|
|
62
|
+
exit 0
|
|
63
|
+
fi
|
|
64
|
+
UI_BASE="http://127.0.0.1:${UI_PORT}"
|
|
65
|
+
LOG_INGEST_URL="${UI_BASE}/api/admin/log-ingest"
|
|
66
|
+
|
|
67
|
+
emit_log() {
|
|
68
|
+
local line="$1"
|
|
69
|
+
curl -sS -o /dev/null -X POST \
|
|
70
|
+
-H 'Content-Type: application/json' \
|
|
71
|
+
--max-time 2 \
|
|
72
|
+
--data "$(python3 -c '
|
|
73
|
+
import sys, json
|
|
74
|
+
print(json.dumps({"tag": "signal-detector-hook", "level": "info", "line": sys.argv[1]}))
|
|
75
|
+
' "$line")" \
|
|
76
|
+
"$LOG_INGEST_URL" 2>/dev/null || true
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
INPUT=""
|
|
80
|
+
if [ -t 0 ]; then
|
|
81
|
+
INPUT=""
|
|
82
|
+
else
|
|
83
|
+
INPUT=$(cat)
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
PARSED=$(printf '%s' "$INPUT" | python3 -c '
|
|
87
|
+
import sys, json
|
|
88
|
+
try:
|
|
89
|
+
d = json.load(sys.stdin)
|
|
90
|
+
sid = d.get("session_id", "") or ""
|
|
91
|
+
tpath = d.get("transcript_path", "") or ""
|
|
92
|
+
print(f"{sid}\t{tpath}")
|
|
93
|
+
except Exception:
|
|
94
|
+
print("\t")
|
|
95
|
+
' 2>/dev/null)
|
|
96
|
+
SESSION_ID="${PARSED%% *}"
|
|
97
|
+
TRANSCRIPT_PATH="${PARSED#* }"
|
|
98
|
+
SESSION_ID_REPORTED="${SESSION_ID:-unknown}"
|
|
99
|
+
|
|
100
|
+
if [ "${MAXY_SESSION_ROLE:-}" != "admin" ]; then
|
|
101
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=role-not-admin"
|
|
102
|
+
exit 0
|
|
103
|
+
fi
|
|
104
|
+
# Recursion guard. A specialist subagent (database-operator, signal-detector
|
|
105
|
+
# itself, etc.) stamps `MAXY_SPECIALIST` on its PTY env. Skip — the hook
|
|
106
|
+
# only fires on the operator's own admin session, never on a subagent's
|
|
107
|
+
# own stop.
|
|
108
|
+
if [ -n "${MAXY_SPECIALIST:-}" ]; then
|
|
109
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=is-specialist specialist=${MAXY_SPECIALIST}"
|
|
110
|
+
exit 0
|
|
111
|
+
fi
|
|
112
|
+
if [ -z "$INPUT" ]; then
|
|
113
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=empty-stdin"
|
|
114
|
+
exit 0
|
|
115
|
+
fi
|
|
116
|
+
if [ -z "$SESSION_ID" ] || [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
|
|
117
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID_REPORTED} reason=missing-transcript"
|
|
118
|
+
exit 0
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
# Single Python pass over the transcript. Returns three tab-separated
|
|
122
|
+
# fields:
|
|
123
|
+
# 1. count of real-user messages whose timestamp is more recent than the
|
|
124
|
+
# latest in-transcript Task dispatch with subagent_type=signal-detector
|
|
125
|
+
# (or, when no such dispatch exists yet, the total real-user count)
|
|
126
|
+
# 2. idle_ms — milliseconds between the latest real-user message's
|
|
127
|
+
# timestamp and now (hook fire time)
|
|
128
|
+
# 3. excerpt — the latest real-user message's text, truncated to 2000
|
|
129
|
+
# chars and base64-encoded so newlines and quotes survive the shell
|
|
130
|
+
# round-trip
|
|
131
|
+
INSPECTION=$(TRANSCRIPT_PATH="$TRANSCRIPT_PATH" python3 - <<'PY'
|
|
132
|
+
import os, json, base64
|
|
133
|
+
from datetime import datetime, timezone
|
|
134
|
+
|
|
135
|
+
def is_real_user(rec):
|
|
136
|
+
if not isinstance(rec, dict) or rec.get("type") != "user":
|
|
137
|
+
return False
|
|
138
|
+
msg = rec.get("message")
|
|
139
|
+
if not isinstance(msg, dict) or msg.get("role") != "user":
|
|
140
|
+
return False
|
|
141
|
+
content = msg.get("content")
|
|
142
|
+
if isinstance(content, str):
|
|
143
|
+
return True
|
|
144
|
+
if isinstance(content, list):
|
|
145
|
+
for b in content:
|
|
146
|
+
if isinstance(b, dict) and b.get("type") != "tool_result":
|
|
147
|
+
return True
|
|
148
|
+
return False
|
|
149
|
+
return True
|
|
150
|
+
|
|
151
|
+
def extract_user_text(rec):
|
|
152
|
+
msg = rec.get("message", {}) or {}
|
|
153
|
+
content = msg.get("content")
|
|
154
|
+
if isinstance(content, str):
|
|
155
|
+
return content
|
|
156
|
+
if isinstance(content, list):
|
|
157
|
+
out = []
|
|
158
|
+
for b in content:
|
|
159
|
+
if isinstance(b, dict) and b.get("type") == "text":
|
|
160
|
+
t = b.get("text")
|
|
161
|
+
if isinstance(t, str):
|
|
162
|
+
out.append(t)
|
|
163
|
+
return "".join(out)
|
|
164
|
+
return ""
|
|
165
|
+
|
|
166
|
+
def latest_signal_detector_dispatch_index(records):
|
|
167
|
+
"""Return the index of the most recent assistant tool_use of `Task`
|
|
168
|
+
whose `subagent_type` is `signal-detector`, or -1 if none."""
|
|
169
|
+
for i in range(len(records) - 1, -1, -1):
|
|
170
|
+
rec = records[i]
|
|
171
|
+
if not isinstance(rec, dict) or rec.get("type") != "assistant":
|
|
172
|
+
continue
|
|
173
|
+
msg = rec.get("message", {}) or {}
|
|
174
|
+
if msg.get("role") != "assistant":
|
|
175
|
+
continue
|
|
176
|
+
content = msg.get("content")
|
|
177
|
+
if not isinstance(content, list):
|
|
178
|
+
continue
|
|
179
|
+
for b in content:
|
|
180
|
+
if not isinstance(b, dict) or b.get("type") != "tool_use":
|
|
181
|
+
continue
|
|
182
|
+
if b.get("name") != "Task":
|
|
183
|
+
continue
|
|
184
|
+
inp = b.get("input", {})
|
|
185
|
+
if isinstance(inp, dict) and inp.get("subagent_type") == "signal-detector":
|
|
186
|
+
return i
|
|
187
|
+
return -1
|
|
188
|
+
|
|
189
|
+
path = os.environ["TRANSCRIPT_PATH"]
|
|
190
|
+
records = []
|
|
191
|
+
try:
|
|
192
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
193
|
+
for raw in f:
|
|
194
|
+
raw = raw.strip()
|
|
195
|
+
if not raw:
|
|
196
|
+
continue
|
|
197
|
+
try:
|
|
198
|
+
records.append(json.loads(raw))
|
|
199
|
+
except Exception:
|
|
200
|
+
continue
|
|
201
|
+
except Exception:
|
|
202
|
+
# Fail-open — print empty fields, the bash side will treat as missing.
|
|
203
|
+
print("0\t0\t")
|
|
204
|
+
raise SystemExit(0)
|
|
205
|
+
|
|
206
|
+
last_dispatch_idx = latest_signal_detector_dispatch_index(records)
|
|
207
|
+
|
|
208
|
+
# Count real-user messages after last_dispatch_idx (exclusive); when
|
|
209
|
+
# last_dispatch_idx is -1, count from the start.
|
|
210
|
+
new_user_count = 0
|
|
211
|
+
latest_user_ts = None
|
|
212
|
+
latest_user_text = ""
|
|
213
|
+
start = last_dispatch_idx + 1
|
|
214
|
+
for i in range(start, len(records)):
|
|
215
|
+
rec = records[i]
|
|
216
|
+
if is_real_user(rec):
|
|
217
|
+
new_user_count += 1
|
|
218
|
+
ts = rec.get("timestamp") or ""
|
|
219
|
+
if isinstance(ts, str) and ts:
|
|
220
|
+
latest_user_ts = ts
|
|
221
|
+
latest_user_text = extract_user_text(rec).strip()
|
|
222
|
+
|
|
223
|
+
if latest_user_ts is None:
|
|
224
|
+
print(f"{new_user_count}\t0\t")
|
|
225
|
+
raise SystemExit(0)
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
# JSONL timestamps from Claude Code are ISO 8601 with Z suffix.
|
|
229
|
+
parsed = datetime.fromisoformat(latest_user_ts.replace("Z", "+00:00"))
|
|
230
|
+
now = datetime.now(timezone.utc)
|
|
231
|
+
idle_ms = max(0, int((now - parsed).total_seconds() * 1000))
|
|
232
|
+
except Exception:
|
|
233
|
+
idle_ms = 0
|
|
234
|
+
|
|
235
|
+
excerpt = latest_user_text[:2000]
|
|
236
|
+
b64 = base64.b64encode(excerpt.encode("utf-8")).decode("ascii")
|
|
237
|
+
print(f"{new_user_count}\t{idle_ms}\t{b64}")
|
|
238
|
+
PY
|
|
239
|
+
)
|
|
240
|
+
NEW_TURNS="${INSPECTION%% *}"
|
|
241
|
+
REMAINING="${INSPECTION#* }"
|
|
242
|
+
IDLE_MS="${REMAINING%% *}"
|
|
243
|
+
EXCERPT_B64="${REMAINING#* }"
|
|
244
|
+
|
|
245
|
+
# Throttle gates.
|
|
246
|
+
if [ "$NEW_TURNS" -eq 0 ] 2>/dev/null; then
|
|
247
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID} reason=already-dispatched newTurns=0"
|
|
248
|
+
exit 0
|
|
249
|
+
fi
|
|
250
|
+
if [ "$NEW_TURNS" -lt "$MIN_NEW_TURNS" ] 2>/dev/null; then
|
|
251
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID} reason=below-turn-threshold newTurns=${NEW_TURNS} threshold=${MIN_NEW_TURNS}"
|
|
252
|
+
exit 0
|
|
253
|
+
fi
|
|
254
|
+
if [ "$IDLE_MS" -lt "$MIN_IDLE_MS" ] 2>/dev/null; then
|
|
255
|
+
emit_log "trigger-skipped sessionId=${SESSION_ID} reason=below-idle-threshold idleMs=${IDLE_MS} threshold=${MIN_IDLE_MS}"
|
|
256
|
+
exit 0
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
# Specialist auto-install. The template lives under
|
|
260
|
+
# `$PLATFORM_ROOT/templates/specialists/agents/signal-detector.md`. The
|
|
261
|
+
# installer / seeder copies templates to per-account dirs at onboarding;
|
|
262
|
+
# this fallback heals an account that pre-dates the template's existence
|
|
263
|
+
# without forcing the operator to re-run onboarding.
|
|
264
|
+
PLATFORM_ROOT_FALLBACK="${MAXY_PLATFORM_ROOT:-}"
|
|
265
|
+
ACCOUNT_DIR_LOCAL="${ACCOUNT_DIR:-}"
|
|
266
|
+
SPECIALIST_TARGET="${ACCOUNT_DIR_LOCAL}/specialists/agents/signal-detector.md"
|
|
267
|
+
if [ -n "$ACCOUNT_DIR_LOCAL" ] && [ ! -f "$SPECIALIST_TARGET" ] && [ -n "$PLATFORM_ROOT_FALLBACK" ]; then
|
|
268
|
+
TEMPLATE_SRC="${PLATFORM_ROOT_FALLBACK}/templates/specialists/agents/signal-detector.md"
|
|
269
|
+
if [ -f "$TEMPLATE_SRC" ]; then
|
|
270
|
+
mkdir -p "$(dirname "$SPECIALIST_TARGET")" 2>/dev/null
|
|
271
|
+
cp "$TEMPLATE_SRC" "$SPECIALIST_TARGET" 2>/dev/null
|
|
272
|
+
AGENTS_MD="${ACCOUNT_DIR_LOCAL}/agents/admin/AGENTS.md"
|
|
273
|
+
if [ -f "$AGENTS_MD" ]; then
|
|
274
|
+
if ! grep -q "specialists:signal-detector" "$AGENTS_MD" 2>/dev/null; then
|
|
275
|
+
printf '%s\n' "- **specialists:signal-detector**: Background brain-compounder dispatched only by the admin-agent Stop hook (\`platform/plugins/admin/hooks/signal-detector-stop.sh\`). Never invoke manually." >> "$AGENTS_MD"
|
|
276
|
+
fi
|
|
277
|
+
fi
|
|
278
|
+
emit_log "bootstrap-installed sessionId=${SESSION_ID} target=${SPECIALIST_TARGET}"
|
|
279
|
+
fi
|
|
280
|
+
fi
|
|
281
|
+
|
|
282
|
+
EXCERPT_PREVIEW=$(printf '%s' "$EXCERPT_B64" | base64 --decode 2>/dev/null | head -c 200 | tr '\n' ' ')
|
|
283
|
+
emit_log "gate-blocked sessionId=${SESSION_ID} reason=dispatching newTurns=${NEW_TURNS} idleMs=${IDLE_MS} preview=$(printf '%s' "$EXCERPT_PREVIEW" | sed 's/[\"]//g' | head -c 80)"
|
|
284
|
+
|
|
285
|
+
# Emit the dispatch directive on stderr per Stop-hook contract. The admin
|
|
286
|
+
# IDENTITY.md "Stop-hook system directives" section names this token prefix
|
|
287
|
+
# and the binding response shape — agent must perform the single Agent
|
|
288
|
+
# dispatch and emit an empty acknowledgement on return, nothing else.
|
|
289
|
+
EXCERPT_DECODED=$(printf '%s' "$EXCERPT_B64" | base64 --decode 2>/dev/null)
|
|
290
|
+
|
|
291
|
+
cat >&2 <<DIRECTIVE
|
|
292
|
+
[system: signal-detector-stop-hook]
|
|
293
|
+
Dispatch the signal-detector specialist NOW using the Task tool. Do not respond to the operator; this is a background brain-compounding task and the operator does not see this turn. After the specialist returns its one-line summary, emit a single space character and stop.
|
|
294
|
+
|
|
295
|
+
Agent dispatch parameters:
|
|
296
|
+
subagent_type: signal-detector
|
|
297
|
+
prompt: |
|
|
298
|
+
accountId: ${ACCOUNT_ID:-<unset>}
|
|
299
|
+
sessionId: ${SESSION_ID}
|
|
300
|
+
newUserTurnsSinceLastRun: ${NEW_TURNS}
|
|
301
|
+
idleMsBeforeFire: ${IDLE_MS}
|
|
302
|
+
|
|
303
|
+
The operator's latest turn (truncated to 2000 chars):
|
|
304
|
+
${EXCERPT_DECODED}
|
|
305
|
+
|
|
306
|
+
Resolve the operator's most recent :Message in this conversation via memory-search (filter by conversationId from your MCP server env), then write :MENTIONS edges and :Idea nodes per your specialist body. Return one line in the documented output shape.
|
|
307
|
+
DIRECTIVE
|
|
308
|
+
|
|
309
|
+
exit 2
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: docs
|
|
3
|
+
surface: platform
|
|
3
4
|
description: "Comprehensive user guide and platform documentation for Maxy. Load references when users ask how-to, setup, or troubleshooting questions. Also contains platform architecture references for admin use."
|
|
4
5
|
metadata: {"platform":{"always":true,"embed":["public","admin"],"pluginKey":"docs"}}
|
|
5
6
|
---
|
|
@@ -79,7 +79,7 @@ PreToolUse hook `platform/plugins/admin/hooks/askuserquestion-investigate-gate.s
|
|
|
79
79
|
|
|
80
80
|
## Spawn-with-`initialMessage` wrapper
|
|
81
81
|
|
|
82
|
-
The Hono route `POST /api/admin/claude-sessions` at [`platform/ui/server/routes/admin/claude-sessions.ts`](../../../ui/server/routes/admin/claude-sessions.ts) is the single canonical entry point for any caller — UI click handler or programmatic — that needs to open an admin PTY session with a first user prompt. Direct calls to the session-manager's `/spawn` are not allowed; the wrapper owns the per-spawn enrichment (`aboutOwner`, `dormantPlugins`, `activePlugins`, `specialistDomains`, `tunnelUrl`) and the `senderId` resolution from the admin session cookie. The session manager binds to `127.0.0.1` only so the wrapper is the sole authorised caller; any ad-hoc spawn that bypasses it duplicates this contract and is a bug.
|
|
82
|
+
The Hono route `POST /api/admin/claude-sessions` at [`platform/ui/server/routes/admin/claude-sessions.ts`](../../../ui/server/routes/admin/claude-sessions.ts) is the single canonical entry point for any caller — UI click handler or programmatic — that needs to open an admin PTY session with a first user prompt. Direct calls to the session-manager's `/spawn` are not allowed; the wrapper owns the per-spawn enrichment (`aboutOwner`, `dormantPlugins`, `activePlugins`, `specialistDomains`, `tunnelUrl`) and the `senderId` resolution from the admin session cookie. The session manager binds to `127.0.0.1` only so the wrapper is the sole authorised caller; any ad-hoc spawn that bypasses it duplicates this contract and is a bug. `dormantPlugins` is `installed − enabledPlugins` with one exclusion: PLUGIN.md frontmatter `surface: platform` (admin, docs) marks platform-shell plugins that ship with every install and are never opt-in features, so they never surface in the `<dormant-plugins>` nudge.
|
|
83
83
|
|
|
84
84
|
**Body schema.** `{channel?, permissionMode?, initialMessage?, specialist?, model?}` for the cookie path (Sidebar — `senderId` resolved from the admin session cookie, `hidden: false`). `permissionMode` accepts one of five values matching Claude Code's CLI: `'default'`, `'acceptEdits'`, `'plan'`, `'auto'`, `'bypassPermissions'`. The Stop-hook loopback caller adds `adminSessionId` and uses three overrides: `specialist: 'database-operator'`, `model: 'haiku'`, plus `initialMessage`. The wrapper rejects nothing structurally on the cookie path — string fields are coerced or defaulted (`channel` → `'browser'`, `permissionMode` → undefined, `initialMessage` → null if empty/whitespace). The loopback path refuses with `400 admin-session-id-required` if `adminSessionId` is missing or non-UUID, and `404 admin-session-not-found` if the manager has no StoredSession for it.
|
|
85
85
|
|
|
@@ -85,7 +85,7 @@ Every PTY spawn injects an `--append-system-prompt` block composed of these sent
|
|
|
85
85
|
| `<about-owner>` | upstream `loadUserProfile` + `formatProfileSummary` (UI process) | sentinel renders the prose body with `NOTHING (operator-data source unavailable: \`<reason>\`)` on line one; spawn proceeds |
|
|
86
86
|
| `<specialist-domains>` | `<accountDir>/specialists/agents/*.md` frontmatter (UI process) | sentinel omitted when set is empty |
|
|
87
87
|
| `<plugin-manifest>` | enabled plugins' PLUGIN.md frontmatter + skill SKILL.md frontmatter (UI process) | sentinel omitted when set is empty |
|
|
88
|
-
| `<dormant-plugins>` | installed-minus-enabled set from `platform/plugins/` and `account.json` | sentinel omitted when set is empty |
|
|
88
|
+
| `<dormant-plugins>` | installed-minus-enabled set from `platform/plugins/` and `account.json`, excluding plugins whose PLUGIN.md frontmatter sets `surface: platform` (platform-shell — ships with every install, never opt-in). Absent `surface` field defaults to `feature` (dormant-eligible). | sentinel omitted when set is empty |
|
|
89
89
|
|
|
90
90
|
Diagnostic line per spawn (`~/.<brand>/logs/server.log`):
|
|
91
91
|
|
|
@@ -77,6 +77,9 @@ tools:
|
|
|
77
77
|
- name: profile-delete
|
|
78
78
|
publicAllowlist: false
|
|
79
79
|
adminAllowlist: true
|
|
80
|
+
- name: memory-signals-recent
|
|
81
|
+
publicAllowlist: false
|
|
82
|
+
adminAllowlist: true
|
|
80
83
|
- name: graph-prune-denylist-add
|
|
81
84
|
publicAllowlist: false
|
|
82
85
|
adminAllowlist: true
|
|
@@ -162,6 +165,10 @@ Graph hygiene is **agent-directed, case by case** — no autonomous rule engine,
|
|
|
162
165
|
|
|
163
166
|
**Why soft-delete.** On 2026-04-20 an autonomous orphan-delete rule wiped 19 nodes in one `graph-prune-run` call; properties were unrecoverable (Neo4j Community has no PITR, no APOC). The cron, the `graph-prune-run` tool, and the rule engine have been removed. An earlier fix added the `:Trashed` label primitive so every soft-delete caller (`memory-delete`, `contact-delete`, the admin `/graph` page's trash button) runs the same shared `trashNode` helper — relationships preserved, unique keys snapshotted into `_trashedKeys`, audit lines `[trash:marked] by=<surface>` for cross-cut provenance.
|
|
164
167
|
|
|
168
|
+
## Signal-detector diagnostic — `memory-signals-recent`
|
|
169
|
+
|
|
170
|
+
Read-only window into the writes the `signal-detector` specialist has produced under this account. Returns recent `:MENTIONS` edges (`:Message-[:MENTIONS]->Entity`) and `:Idea` nodes interleaved by `observedAt`. Default window is 24 hours; pass `windowHours` (max 720) and `limit` (max 200) to scan a longer arc. Use this when the operator asks "what has the brain captured today?", "show me recent mentions of X", or "what ideas did I leave on the table this week?". The signal-detector specialist itself is dispatched only by the admin-agent Stop hook at `platform/plugins/admin/hooks/signal-detector-stop.sh` — never by operator intent and never via `<specialist-domains>` routing. Task 303.
|
|
171
|
+
|
|
165
172
|
## Conversational Memory
|
|
166
173
|
|
|
167
174
|
The owner's profile and preferences accumulate organically from conversation — never from questionnaires. Run `skill-load skillName=conversational-memory` for full guidance on when to observe preferences, how to handle remember/forget requests, and how to answer "what do you know about me?" transparently with confidence scores and evidence trail.
|
|
@@ -31,6 +31,7 @@ import { memoryReadAttachment } from "./tools/memory-read-attachment.js";
|
|
|
31
31
|
import { memoryEditAttachment } from "./tools/memory-edit-attachment.js";
|
|
32
32
|
import { memoryRenameAttachment } from "./tools/memory-rename-attachment.js";
|
|
33
33
|
import { profileRead } from "./tools/profile-read.js";
|
|
34
|
+
import { memorySignalsRecent } from "./tools/memory-signals-recent.js";
|
|
34
35
|
import { profileUpdate } from "./tools/profile-update.js";
|
|
35
36
|
import { profileDelete } from "./tools/profile-delete.js";
|
|
36
37
|
import { graphPruneDenylistAdd } from "./tools/graph-prune-denylist-add.js";
|
|
@@ -1678,6 +1679,37 @@ if (!readOnly) {
|
|
|
1678
1679
|
};
|
|
1679
1680
|
}
|
|
1680
1681
|
});
|
|
1682
|
+
// memory-signals-recent: read-only diagnostic for the signal-detector
|
|
1683
|
+
// specialist's writes. Returns recent :MENTIONS edges
|
|
1684
|
+
// (:Message-[:MENTIONS]->Entity) and :Idea nodes filtered by
|
|
1685
|
+
// r.createdByAgent='signal-detector'. The admin agent uses this to
|
|
1686
|
+
// answer "what has the brain captured today?" without touching cypher.
|
|
1687
|
+
// The signal-detector specialist itself is dispatched only by the
|
|
1688
|
+
// admin-agent Stop hook — never by operator intent. Task 303.
|
|
1689
|
+
server.tool("memory-signals-recent", "Read the most recent signal-detector writes for this account: entity mentions extracted from operator messages, and original-thinking :Idea nodes. " +
|
|
1690
|
+
"Default window is the last 24 hours. " +
|
|
1691
|
+
"Use this when the operator asks 'what has the brain captured today?', 'show me recent mentions of X', or 'what ideas did I leave on the table this week?'.", {
|
|
1692
|
+
windowHours: z.number().int().optional().describe("Hours back from now. Default 24. Capped at 720 (30 days)."),
|
|
1693
|
+
limit: z.number().int().optional().describe("Max rows returned. Default 50, max 200."),
|
|
1694
|
+
}, async ({ windowHours, limit }) => {
|
|
1695
|
+
try {
|
|
1696
|
+
if (!accountId)
|
|
1697
|
+
return refuseNoAccount("memory-signals-recent");
|
|
1698
|
+
const result = await memorySignalsRecent({ accountId, windowHours, limit });
|
|
1699
|
+
return {
|
|
1700
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
catch (err) {
|
|
1704
|
+
return {
|
|
1705
|
+
content: [{
|
|
1706
|
+
type: "text",
|
|
1707
|
+
text: `memory-signals-recent failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1708
|
+
}],
|
|
1709
|
+
isError: true,
|
|
1710
|
+
};
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1681
1713
|
// -------------------------------------------------------------------------
|
|
1682
1714
|
// Deny-list maintenance + conversation-memory expunge.
|
|
1683
1715
|
//
|