@rubytech/create-maxy-code 0.1.44 → 0.1.47
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/dist/index.js +8 -16
- package/package.json +1 -1
- package/payload/platform/lib/admins-write/dist/index.d.ts +0 -1
- package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admins-write/dist/index.js +2 -5
- package/payload/platform/lib/admins-write/dist/index.js.map +1 -1
- package/payload/platform/lib/admins-write/src/index.ts +2 -8
- package/payload/platform/lib/graph-mcp/dist/index.js +1 -4
- package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts +7 -6
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js +7 -6
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js.map +1 -1
- package/payload/platform/lib/graph-mcp/src/index.ts +1 -4
- package/payload/platform/lib/graph-mcp/src/schema-cache.ts +7 -6
- package/payload/platform/lib/graph-search/dist/index.d.ts +5 -5
- package/payload/platform/lib/graph-search/dist/index.js +4 -4
- package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +6 -7
- package/payload/platform/lib/graph-search/src/index.ts +5 -5
- package/payload/platform/lib/graph-trash/dist/index.d.ts +8 -12
- package/payload/platform/lib/graph-trash/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-trash/dist/index.js +8 -12
- package/payload/platform/lib/graph-trash/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-trash/src/index.ts +8 -12
- package/payload/platform/lib/graph-write/dist/index.d.ts +8 -9
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +9 -10
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-write/src/index.ts +9 -10
- package/payload/platform/lib/mcp-eager/dist/index.d.ts +4 -4
- package/payload/platform/lib/mcp-eager/dist/index.js +4 -4
- package/payload/platform/lib/mcp-eager/src/index.ts +4 -4
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +138 -18
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +31 -67
- package/payload/platform/plugins/cloudflare/scripts/__tests__/tunnel-ingress.test.ts +2 -2
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +7 -9
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +10 -12
- package/payload/platform/plugins/cloudflare/scripts/tunnel-ingress.ts +3 -3
- package/payload/platform/plugins/docs/references/deployment.md +0 -14
- package/payload/platform/plugins/docs/references/platform.md +6 -4
- package/payload/platform/plugins/docs/references/plugins-guide.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +5 -5
- package/payload/platform/plugins/loop/PLUGIN.md +67 -12
- package/payload/platform/plugins/loop/mcp/dist/index.js +42 -16
- package/payload/platform/plugins/loop/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.d.ts +12 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.js +22 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/auto-responder.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts +0 -7
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.js +4 -38
- package/payload/platform/plugins/loop/mcp/dist/tools/marketing-enquiry.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.d.ts +33 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.js +19 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-intent.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.d.ts +0 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.d.ts.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.js +1 -7
- package/payload/platform/plugins/loop/mcp/dist/tools/property-listed.js.map +1 -1
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.d.ts +9 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.d.ts.map +1 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.js +19 -0
- package/payload/platform/plugins/loop/mcp/dist/tools/property-sold.js.map +1 -0
- package/payload/platform/plugins/loop/mcp/src/__tests__/new-tool-shape.test.ts +99 -0
- package/payload/platform/plugins/loop/mcp/src/__tests__/swagger-write-coverage.test.ts +12 -11
- package/payload/platform/plugins/loop/mcp/src/index.ts +68 -17
- package/payload/platform/plugins/loop/mcp/src/tools/auto-responder.ts +80 -0
- package/payload/platform/plugins/loop/mcp/src/tools/marketing-enquiry.ts +14 -100
- package/payload/platform/plugins/loop/mcp/src/tools/property-intent.ts +84 -0
- package/payload/platform/plugins/loop/mcp/src/tools/property-listed.ts +1 -8
- package/payload/platform/plugins/loop/mcp/src/tools/property-sold.ts +67 -0
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js +4 -3
- package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/llm-classifier.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js +7 -12
- package/payload/platform/plugins/memory/mcp/dist/lib/llm-classifier.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-not-applicable.test.js +2 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js +4 -4
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/session-name.d.ts.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/session-name.js +8 -9
- package/payload/platform/plugins/tasks/mcp/dist/tools/session-name.js.map +1 -1
- package/payload/platform/plugins/waitlist/mcp/dist/tools/waitlist-heal.d.ts +2 -2
- package/payload/platform/plugins/waitlist/mcp/dist/tools/waitlist-heal.js +2 -2
- package/payload/platform/scripts/admin-persist-audit.ts +4 -10
- package/payload/platform/scripts/conversation-id-allowlist.txt +0 -1
- package/payload/platform/scripts/lib/resolve-account-dir.sh +0 -2
- package/payload/platform/scripts/resume-tunnel.sh +2 -9
- package/payload/platform/scripts/seed-neo4j.sh +26 -104
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +67 -5
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- 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 +13 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +12 -12
- package/payload/platform/templates/agents/admin/SOUL.md +23 -1
- package/payload/premium-plugins/real-agent/agents/buyer-enquiry/SOUL.md +10 -0
- package/payload/premium-plugins/real-agent/agents/compliance.md +1 -1
- package/payload/premium-plugins/real-agent/agents/negotiator.md +1 -1
- package/payload/premium-plugins/real-agent/agents/valuer.md +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/PLUGIN.md +67 -12
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js +42 -16
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/index.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.d.ts +12 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.js +22 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/auto-responder.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts +0 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.js +4 -38
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/marketing-enquiry.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.d.ts +33 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.js +19 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-intent.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.d.ts +0 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.d.ts.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.js +1 -7
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-listed.js.map +1 -1
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.d.ts +9 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.d.ts.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.js +19 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/dist/tools/property-sold.js.map +1 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/__tests__/new-tool-shape.test.ts +99 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/__tests__/swagger-write-coverage.test.ts +12 -11
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/index.ts +68 -17
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/auto-responder.ts +80 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/marketing-enquiry.ts +14 -100
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-intent.ts +84 -0
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-listed.ts +1 -8
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-sold.ts +67 -0
- package/payload/server/public/assets/admin-SyUHjvNs.js +216 -0
- package/payload/server/public/assets/{data-CU9uH2JJ.js → data-DwQ7ZEyy.js} +1 -1
- package/payload/server/public/assets/{graph-BI7yVwmT.js → graph-CwZkD6GX.js} +1 -1
- package/payload/server/public/assets/graph-labels-CVkmOHhw.js +1 -0
- package/payload/server/public/assets/page-CQ7ELqGs.js +1 -0
- package/payload/server/public/assets/page-SOCqJGlV.js +50 -0
- 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 +9 -46
- package/payload/platform/plugins/docs/references/migration-guide.md +0 -90
- package/payload/platform/plugins/loop/mcp/src/tools/property-request.ts +0 -42
- package/payload/platform/scripts/component-knowledgedoc-backfill.ts +0 -214
- package/payload/platform/scripts/migrate-import.sh +0 -437
- package/payload/platform/scripts/taskmaster-export.sh +0 -388
- package/payload/premium-plugins/real-agent/plugins/loop/mcp/src/tools/property-request.ts +0 -42
- package/payload/server/public/assets/admin-GOZURixP.js +0 -216
- package/payload/server/public/assets/graph-labels-bI0uSt_-.js +0 -1
- package/payload/server/public/assets/page-BaRMnC96.js +0 -1
- package/payload/server/public/assets/page-DKdUMAhI.js +0 -50
|
@@ -1,27 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Stop hook — fires on every completed admin-agent turn and dispatches one
|
|
3
|
-
#
|
|
4
|
-
# the only writer to the Neo4j graph; the admin agent
|
|
5
|
-
# operator's request.
|
|
3
|
+
# headless database-operator session against the conversation transcript.
|
|
4
|
+
# The hidden recorder is the only writer to the Neo4j graph; the admin agent
|
|
5
|
+
# stays focused on the operator's request.
|
|
6
6
|
#
|
|
7
|
-
# Pipeline (Task
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
# the recorder PTY; the MAXY_HIDDEN_SPAWN gate exits early so no
|
|
18
|
-
# recursion. The recorder's PTY exits naturally; its Stop is then
|
|
19
|
-
# driven by POST /<sessionId>/stop from the same hook invocation
|
|
20
|
-
# that posted /input.
|
|
7
|
+
# Pipeline (Task 114 redesign — replaces Task 106's `--print` + stdin-EOT
|
|
8
|
+
# shape, which is incompatible with the OAuth subscription the manager runs
|
|
9
|
+
# on):
|
|
10
|
+
# 1. /spawn with `hidden=true, specialist=database-operator` — the manager
|
|
11
|
+
# produces a structurally normal admin specialist session (same argv,
|
|
12
|
+
# same OAuth path) and returns its sessionId.
|
|
13
|
+
# 2. POST /<sessionId>/input with `<instruction>\n\nConversation transcript:\n<filtered JSON>`.
|
|
14
|
+
# 3. The manager's recorder-auto-archive subscriber stops the PTY as soon
|
|
15
|
+
# as the recorder's JSONL contains an assistant message with
|
|
16
|
+
# `stop_reason === "end_turn"`. The hook does not need to follow up.
|
|
21
17
|
#
|
|
22
18
|
# Gating:
|
|
23
19
|
# - MAXY_SESSION_ROLE must equal "admin"
|
|
24
|
-
# - MAXY_HIDDEN_SPAWN must equal "0" (
|
|
20
|
+
# - MAXY_HIDDEN_SPAWN must equal "0" (the recorder PTY itself runs with
|
|
21
|
+
# MAXY_HIDDEN_SPAWN=1; the guard prevents accidental recursion).
|
|
25
22
|
#
|
|
26
23
|
# Input: Claude Code's Stop hook stdin shape
|
|
27
24
|
# { "session_id": "<intrinsic>", "transcript_path": "<jsonl path>", ... }
|
|
@@ -37,32 +34,6 @@ if [ "${MAXY_SESSION_ROLE:-}" != "admin" ]; then
|
|
|
37
34
|
exit 0
|
|
38
35
|
fi
|
|
39
36
|
if [ "${MAXY_HIDDEN_SPAWN:-0}" = "1" ]; then
|
|
40
|
-
# Recorder PTY's own Stop hook. Read its session_id and ask the manager
|
|
41
|
-
# to SIGTERM the recorder so it does not idle in the active recents list
|
|
42
|
-
# after its single turn completes. Keep the JSONL — it's the audit trail.
|
|
43
|
-
if [ -t 0 ]; then
|
|
44
|
-
exit 0
|
|
45
|
-
fi
|
|
46
|
-
REC_INPUT=$(cat)
|
|
47
|
-
if [ -z "$REC_INPUT" ]; then
|
|
48
|
-
exit 0
|
|
49
|
-
fi
|
|
50
|
-
REC_SESSION_ID=$(printf '%s' "$REC_INPUT" | python3 -c '
|
|
51
|
-
import sys, json
|
|
52
|
-
try:
|
|
53
|
-
d = json.load(sys.stdin)
|
|
54
|
-
print(d.get("session_id", "") or "")
|
|
55
|
-
except Exception:
|
|
56
|
-
print("")
|
|
57
|
-
' 2>/dev/null)
|
|
58
|
-
if [ -z "$REC_SESSION_ID" ]; then
|
|
59
|
-
exit 0
|
|
60
|
-
fi
|
|
61
|
-
REC_PORT="${CLAUDE_SESSION_MANAGER_PORT:-19400}"
|
|
62
|
-
curl -sS -X POST \
|
|
63
|
-
--max-time 5 \
|
|
64
|
-
"http://127.0.0.1:${REC_PORT}/${REC_SESSION_ID}/stop" >/dev/null 2>&1
|
|
65
|
-
echo "[recorder] self-stop sessionId=${REC_SESSION_ID:0:8}" >&2
|
|
66
37
|
exit 0
|
|
67
38
|
fi
|
|
68
39
|
|
|
@@ -111,9 +82,6 @@ def keep(rec):
|
|
|
111
82
|
return False
|
|
112
83
|
|
|
113
84
|
path = sys.argv[1]
|
|
114
|
-
# Read whole file; admin transcripts are bounded by Claude Code's own
|
|
115
|
-
# compaction so the read cost is reasonable. Take the last 200 records
|
|
116
|
-
# from the kept set so a long transcript doesn't blow the prompt budget.
|
|
117
85
|
out = []
|
|
118
86
|
try:
|
|
119
87
|
with open(path, 'r', encoding='utf-8') as f:
|
|
@@ -135,10 +103,8 @@ FILTERED_COUNT=$(printf '%s' "$FILTERED_TAIL" | python3 -c 'import sys,json; d=j
|
|
|
135
103
|
MANAGER_PORT="${CLAUDE_SESSION_MANAGER_PORT:-19400}"
|
|
136
104
|
MANAGER_BASE="http://127.0.0.1:${MANAGER_PORT}"
|
|
137
105
|
|
|
138
|
-
# Spawn the recorder PTY. Manager blocks on its watcher's PID-file event
|
|
139
|
-
# and returns the recorder's intrinsic sessionId in the response body.
|
|
140
106
|
SPAWN_BODY=$(python3 -c '
|
|
141
|
-
import json
|
|
107
|
+
import json
|
|
142
108
|
body = {
|
|
143
109
|
"senderId": "turn-recorder",
|
|
144
110
|
"role": "admin",
|
|
@@ -147,7 +113,7 @@ body = {
|
|
|
147
113
|
"specialist": "database-operator",
|
|
148
114
|
}
|
|
149
115
|
print(json.dumps(body))
|
|
150
|
-
')
|
|
116
|
+
' 2>/dev/null)
|
|
151
117
|
|
|
152
118
|
SPAWN_RES=$(curl -sS -X POST \
|
|
153
119
|
-H 'Content-Type: application/json' \
|
|
@@ -169,33 +135,31 @@ if [ -z "$RECORDER_SESSION_ID" ]; then
|
|
|
169
135
|
exit 0
|
|
170
136
|
fi
|
|
171
137
|
|
|
172
|
-
# Compose the input
|
|
138
|
+
# Compose the input prompt and POST it to /<sessionId>/input. The instruction
|
|
139
|
+
# string is fixed (Task 114). The transcript is appended as a JSON array so
|
|
140
|
+
# the recorder reads it as a single user turn.
|
|
173
141
|
INPUT_BODY=$(python3 -c '
|
|
174
142
|
import sys, json
|
|
175
143
|
sid = sys.argv[1]
|
|
176
144
|
tail_json = sys.argv[2]
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
"
|
|
180
|
-
"
|
|
181
|
-
f"Conversation
|
|
145
|
+
instruction = "update the graph with any missing information or intent that can be derived from this conversation"
|
|
146
|
+
text = (
|
|
147
|
+
f"{instruction}\n\n"
|
|
148
|
+
f"Conversation id: {sid}\n"
|
|
149
|
+
f"Conversation transcript:\n{tail_json}\n"
|
|
182
150
|
)
|
|
183
|
-
print(json.dumps({"text":
|
|
151
|
+
print(json.dumps({"text": text}))
|
|
184
152
|
' "$ADMIN_SESSION_ID" "$FILTERED_TAIL" 2>/dev/null)
|
|
185
153
|
|
|
186
|
-
|
|
187
|
-
exit 0
|
|
188
|
-
fi
|
|
154
|
+
INPUT_BYTES=$(printf '%s' "$INPUT_BODY" | python3 -c 'import sys,json; b=json.load(sys.stdin); print(len(b.get("text","").encode("utf-8")))' 2>/dev/null || echo 0)
|
|
189
155
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
curl -sS -X POST \
|
|
156
|
+
INPUT_HTTP=$(curl -sS -o /tmp/turn-recorder-input.out -w '%{http_code}' -X POST \
|
|
193
157
|
-H 'Content-Type: application/json' \
|
|
194
|
-
--max-time
|
|
158
|
+
--max-time 10 \
|
|
195
159
|
--data "$INPUT_BODY" \
|
|
196
|
-
"${MANAGER_BASE}/${RECORDER_SESSION_ID}/input" >/dev/null
|
|
160
|
+
"${MANAGER_BASE}/${RECORDER_SESSION_ID}/input" 2>/dev/null || echo 000)
|
|
197
161
|
|
|
198
|
-
echo "[recorder] input
|
|
162
|
+
echo "[turn-recorder] spawn-with-input sessionId=${RECORDER_SESSION_ID:0:8} bytes=${INPUT_BYTES} filtered-from-records=${FILTERED_COUNT} input-http=${INPUT_HTTP}" >&2
|
|
199
163
|
|
|
200
164
|
END_MS=$(python3 -c 'import time; print(int(time.time()*1000))')
|
|
201
165
|
ELAPSED=$((END_MS - START_MS))
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
// renderConfigYml
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
|
|
26
|
-
test("renderConfigYml: HTTPS-only matches
|
|
26
|
+
test("renderConfigYml: HTTPS-only matches expected shape byte-for-byte", () => {
|
|
27
27
|
const spec: IngressSpec = {
|
|
28
28
|
tunnelId: "abc-123",
|
|
29
29
|
credentialsPath: "/home/admin/.maxy/cloudflared/abc-123.json",
|
|
@@ -99,7 +99,7 @@ test("renderConfigYml: empty string hostnames are treated as absent", () => {
|
|
|
99
99
|
// renderTunnelState
|
|
100
100
|
// ---------------------------------------------------------------------------
|
|
101
101
|
|
|
102
|
-
test("renderTunnelState:
|
|
102
|
+
test("renderTunnelState: base fields preserved when ssh/smb absent", () => {
|
|
103
103
|
const state: TunnelState = {
|
|
104
104
|
tunnelId: "abc",
|
|
105
105
|
tunnelName: "maxy",
|
|
@@ -55,11 +55,10 @@ import { fileURLToPath } from "node:url";
|
|
|
55
55
|
// source-of-truth contract:
|
|
56
56
|
// Runtime: brand.json `cdpPort` at `${MAXY_PLATFORM_ROOT}/config/brand.json`
|
|
57
57
|
// is authoritative. Wrapper exports MAXY_PLATFORM_ROOT and BRAND. Missing
|
|
58
|
-
// env / file / field → loud-fail with one of three named reasons.
|
|
59
|
-
//
|
|
60
|
-
// `cdp-unreachable
|
|
61
|
-
//
|
|
62
|
-
// never falls back silently.
|
|
58
|
+
// env / file / field → loud-fail with one of three named reasons. A
|
|
59
|
+
// silent CDP-port default would make every non-default brand fail
|
|
60
|
+
// `cdp-unreachable`; runtime config never falls back silently
|
|
61
|
+
// (NEO4J_URI sets the same precedent).
|
|
63
62
|
//
|
|
64
63
|
// Test overrides: when BOTH `LIST_CF_DOMAINS_CDP_HOST` and
|
|
65
64
|
// `LIST_CF_DOMAINS_CDP_PORT` are set, they win over brand.json. The
|
|
@@ -531,10 +530,9 @@ async function dumpHtml(
|
|
|
531
530
|
// truncated mid-document. The false positive (outerHTML exactly at the
|
|
532
531
|
// ceiling) is a rounding coincidence on the order of 1-in-5M and is
|
|
533
532
|
// acceptable — a loud truncation signal with a ~10^-7 false-positive
|
|
534
|
-
// rate is strictly better than silent truncation
|
|
535
|
-
//
|
|
536
|
-
//
|
|
537
|
-
// can re-scrape with a larger cap or `outerHTML` unsliced.
|
|
533
|
+
// rate is strictly better than silent truncation. A downstream
|
|
534
|
+
// investigator seeing `truncated=true` can re-scrape with a larger
|
|
535
|
+
// cap or `outerHTML` unsliced.
|
|
538
536
|
const truncated = htmlStr.length >= OUTER_HTML_CAPTURE_MAX_CHARS;
|
|
539
537
|
return { path: dumpPath, truncated };
|
|
540
538
|
} catch (err) {
|
|
@@ -358,8 +358,8 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
358
358
|
SPAWN_STDERR="$(tr '\n' ' ' < "${CHROMIUM_SPAWN_ERR}" | head -c 300 || echo unavailable)"
|
|
359
359
|
rm -f "${CHROMIUM_SPAWN_ERR}"
|
|
360
360
|
# Loud-fail rather than fire-and-forget fallback: a systemd-run failure
|
|
361
|
-
#
|
|
362
|
-
#
|
|
361
|
+
# leaves no teardown handle. Operator should see the bus-not-running
|
|
362
|
+
# / linger-not-enabled cause.
|
|
363
363
|
phase_line setup-tunnel step=browser-spawn result=error \
|
|
364
364
|
reason=systemd-run-failed exit="${SPAWN_RC}" stderr="${SPAWN_STDERR}" \
|
|
365
365
|
unit="${CHROMIUM_UNIT}"
|
|
@@ -456,15 +456,13 @@ fi
|
|
|
456
456
|
# --------------------------------------------------------------------------
|
|
457
457
|
# Step 2+3: Resolve the tunnel identity from operator input.
|
|
458
458
|
#
|
|
459
|
-
#
|
|
460
|
-
# -
|
|
461
|
-
#
|
|
462
|
-
#
|
|
463
|
-
#
|
|
464
|
-
# said "Done. tunnel=maxy-maxytest" while the operator's `maxytest` hostname
|
|
465
|
-
# kept resolving via a stale orphan.
|
|
459
|
+
# The local hostname has no authority over which tunnel the operator's
|
|
460
|
+
# logged-in Cloudflare account holds. Deriving TUNNEL_NAME locally (e.g.
|
|
461
|
+
# "${BRAND}-$(hostname -s)") and reusing-or-creating it produces a new
|
|
462
|
+
# tunnel when the device is renamed while existing CNAMEs continue to
|
|
463
|
+
# point at the old one.
|
|
466
464
|
#
|
|
467
|
-
#
|
|
465
|
+
# Contract: the form (rendered via /api/admin/cloudflare/tunnels list) is
|
|
468
466
|
# the source of truth. The endpoint passes exactly one of:
|
|
469
467
|
# TUNNEL_ID — operator selected an existing tunnel from the list
|
|
470
468
|
# TUNNEL_NAME — operator typed a name to create a new tunnel
|
|
@@ -505,8 +503,8 @@ if [ -n "${TUNNEL_ID:-}" ]; then
|
|
|
505
503
|
TUNNEL_SOURCE="operator-selected"
|
|
506
504
|
else
|
|
507
505
|
# Operator-create branch. Refuse to silently reuse an existing tunnel of
|
|
508
|
-
# the same name —
|
|
509
|
-
# The operator should have picked it from the list.
|
|
506
|
+
# the same name — silent reuse hides which tunnel CNAMEs actually point
|
|
507
|
+
# at. The operator should have picked it from the list.
|
|
510
508
|
EXISTING_ID="$(cloudflared --origincert "${CFG_DIR}/cert.pem" tunnel list --output json 2>/dev/null \
|
|
511
509
|
| jq -r --arg N "${TUNNEL_NAME}" '.[]? | select(.name == $N) | .id' | head -1)"
|
|
512
510
|
if [ -n "${EXISTING_ID}" ] && [ "${EXISTING_ID}" != "null" ]; then
|
|
@@ -111,9 +111,9 @@ export function renderConfigYml(spec: IngressSpec): string {
|
|
|
111
111
|
|
|
112
112
|
/**
|
|
113
113
|
* Render the tunnel.state JSON, preserving SSH/SMB hostnames as additive
|
|
114
|
-
* fields so re-runs can rehydrate them. The shape
|
|
115
|
-
*
|
|
116
|
-
*
|
|
114
|
+
* fields so re-runs can rehydrate them. The base shape (tunnelId,
|
|
115
|
+
* tunnelName, domain, configPath, credentialsPath) is a superset that
|
|
116
|
+
* consumers without ssh/smb knowledge continue to read correctly.
|
|
117
117
|
*/
|
|
118
118
|
export function renderTunnelState(state: TunnelState): string {
|
|
119
119
|
const out: Record<string, string> = {
|
|
@@ -163,24 +163,10 @@ Each installed brand runs two per-brand `--user` systemd units (earlier platform
|
|
|
163
163
|
- `{hostname}.service` — the admin + public HTTP server on `127.0.0.1:19201` (public port + 1). Restarted by the upgrade flow; short downtime is expected during steps 8→11 of an upgrade. An earlier fix: the unit carries two port env vars — `PORT=<public>` (canonical public port, read by the upgrade detector) and `MAXY_UI_INTERNAL_PORT=<public+1>` (the port maxy-ui actually binds).
|
|
164
164
|
- `{hostname}-edge.service` — the always-on public listener on the configured port (default 19200). Reverse-proxies HTTP to the main brand service and handles `/websockify` (VNC) WebSocket upgrades locally. An earlier fix: also hosts `/api/admin/actions/*` and `/api/admin/version*` — the Software Update modal's own routes — so the log stream survives the brand service's restart window. Does NOT restart during an upgrade — the browser WebSocket stays connected by construction.
|
|
165
165
|
|
|
166
|
-
**Port-drift recovery.** Devices upgraded between Tasks 647 and 666 may have drifted +1 on every upgrade because the pre-Task-666 installer wrote `Environment=PORT=<internal>` into `{hostname}.service` and the upgrade reader correctly treated `PORT=` as public. The first post-Task-666 install detects this (comparing maxy's PORT against the edge's EDGE_PORT) and emits a one-shot loud log: `[port-recovery] detected drift maxy=<X> edge=<Y> — pinning at <Y>`. Subsequent upgrades are silent. If your Cloudflare tunnel was pointing at a drifted port, the ingress `config.yml` still needs a one-time manual fix: `sed -i 's|localhost:<old>|localhost:<current>|' ~/.{configDir}/cloudflared/config.yml && cloudflared tunnel ingress validate`. {{productName}} never rewrites cloudflared config programmatically.
|
|
167
|
-
|
|
168
166
|
Upgrade and Cloudflare setup run as detached actions: `systemd-run --user` transient units per invocation with stdout+stderr persisted to `~/.maxy/logs/actions/<actionId>.log` and streamed to the UI via SSE. No boot-time service file exists for these.
|
|
169
167
|
|
|
170
168
|
If an action looks stuck, read `~/.maxy/logs/actions/<actionId>.log` directly for the full output, or `journalctl --user --identifier=maxy-action-<actionId>` for systemd's record.
|
|
171
169
|
|
|
172
|
-
**Pre-Task-662 / pre-Task-664 upgrade** — devices that ran an installer have legacy shared `maxy-edge.service` / `maxy-ttyd.service` units; devices that ran have per-brand `{hostname}-ttyd.service` units plus a pinned `/usr/local/bin/ttyd` binary. Neither is removed automatically — do this cleanup once per device before re-running any installer:
|
|
173
|
-
|
|
174
|
-
```bash
|
|
175
|
-
systemctl --user stop maxy-edge maxy-ttyd realagent-ttyd 2>/dev/null || true
|
|
176
|
-
systemctl --user disable maxy-edge maxy-ttyd realagent-ttyd 2>/dev/null || true
|
|
177
|
-
rm -f ~/.config/systemd/user/maxy-edge.service \
|
|
178
|
-
~/.config/systemd/user/maxy-ttyd.service \
|
|
179
|
-
~/.config/systemd/user/realagent-ttyd.service
|
|
180
|
-
sudo rm -f /usr/local/bin/ttyd
|
|
181
|
-
systemctl --user daemon-reload
|
|
182
|
-
```
|
|
183
|
-
|
|
184
170
|
## Linux laptops: snap-confined Chromium replacement
|
|
185
171
|
|
|
186
172
|
On Ubuntu 24.04 (Noble) the system Chromium binary at `/usr/bin/chromium` is a symlink into the snap. Snap's AppArmor profile denies writes to hidden directories under your home folder, so the per-brand Chromium profile at `~/.{brand}/chromium-profile/` is unwritable and the VNC browser never starts. Pi installs (Debian Bookworm) are unaffected because Bookworm ships a real `.deb` chromium.
|
|
@@ -52,7 +52,7 @@ Roles are installed during setup and listed when {{productName}} introduces itse
|
|
|
52
52
|
|
|
53
53
|
{{productName}} maintains a graph database (Neo4j) of everything you've told it. People, conversations, preferences, and context are stored as connected nodes. When you ask {{productName}} something, it searches this graph to retrieve relevant context before responding.
|
|
54
54
|
|
|
55
|
-
**The recording loop.** A background recorder watches every completed admin-agent turn. Once your turn finishes and the reply is on screen, a Stop hook fires the `database-operator` specialist as a
|
|
55
|
+
**The recording loop.** A background recorder watches every completed admin-agent turn. Once your turn finishes and the reply is on screen, a Stop hook fires the `database-operator` specialist as a single-shot headless Claude Code session: the filtered transcript is written to the recorder's stdin and stdin is closed, so claude processes one turn, writes through the wrapped graph writers, and exits. The row archives the moment claude exits — no long-lived hidden PTY. The admin agent itself does not write to the graph. The recorder runs on a cheap fast model so the recording adds nothing to your perceived latency, and its session never appears in the sidebar or anywhere on the operator UI. To disable it for an account, remove the `Stop` hook entry from that account's `~/.<brand>/.claude/settings.json`; there is no toggle.
|
|
56
56
|
|
|
57
57
|
The memory graph is stored on your Pi. It never leaves your network.
|
|
58
58
|
|
|
@@ -70,22 +70,24 @@ There is no dashboard, no settings panel, no menus. Everything is done through c
|
|
|
70
70
|
|
|
71
71
|
The chat input auto-grows as you type — it expands to fit your message and shrinks back when you delete text. You can also drag the resize handle above the input to set a custom height.
|
|
72
72
|
|
|
73
|
-
The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (the database-operator recorder, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with Maxy and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The chat and artefact divider is drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Open JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
|
|
73
|
+
The admin interface is a three-pane layout: a sidebar on the left with navigation (Sessions, People, Agents, Projects, Tasks, Artefacts) and your recent conversations; the chat in the middle; and an artefact pane on the right that opens when you select a document, click a project, or open Browser, Data, or Graph from the menu, holding the surface side-by-side with the conversation so the chat stays live while you work in it. At the very top of the sidebar — above the nav rows — a borderless row holds two controls: a "+ New session" button on the left that spawns a fresh Claude Code session, and a Mode trigger on the right showing the current permission mode (Ask, Accept edits, Plan, or Auto). The sidebar's vertical order is: new-session strip first, then the nav (Sessions, People, Agents, Projects, Tasks, Artefacts), then the sessions list, then the footer. Both controls render as plain text-plus-icon affordances with no surrounding rectangle. The "+ New session" button is a text-width hit target — its clickable area is exactly the icon plus label, not the whole row — and shows no hover fill; the only hover feedback is the pointer cursor. The Mode trigger is pushed flush to the right edge of the row. Clicking the Mode trigger opens a popover downward from the row whose header reads "Mode" and lists the four permission modes with the current selection check-marked. The sidebar's nav rows swap the list view in place: Sessions shows recent conversations, Projects shows your active work projects, and Artefacts lists every KnowledgeDocument plus this account's agent templates (your admin agent's IDENTITY, SOUL, and KNOWLEDGE files plus one entry per enabled specialist). Each recent session row carries a three-state indicator: three pulsing dots when the session is busy (currently processing a turn), a solid sage dot when it is idle (live PTY waiting for input), and a hollow ring when it is archived (PTY exited, JSONL on disk for audit). The list itself splits into three views via a segmented control above the rows: **Active** shows every live session, **Archived** shows every JSONL on disk whose PTY has exited, and **All** shows both. The view choice persists across reloads. An "Include subagents" toggle inside the Active view surfaces specialist spawns (the database-operator recorder, premium-plugin agents, anything spawned with a `--agent` flag) which are hidden by default so the list reflects what you started directly. Each row also carries a small uppercase badge — `admin` for operator-driven sessions, the specialist name (for example `db-op`) for background work — so the source of any row is unambiguous at a glance. The People, Agents, and Tasks rows are graph shortcuts: clicking each opens the artefact-pane Graph filtered to every Person, every public Agent, or every Task in your account respectively, with no side-list, because the graph itself is the result. Public agents become first-class graph entities the moment you create them, with edges to their IDENTITY/SOUL/KNOWLEDGE files, edges to every knowledge document they have access to, and edges from every conversation they have handled, so a single Agents click reveals the whole shape of who knows what and who has been talking to whom. Click an artefact row to open the document. KnowledgeDocuments and your admin agent's templates are editable: type in the document and changes save automatically; specialist agent templates are read-only because they ship with Maxy and your edits would be overwritten on the next install. PDF artefacts render inline so you can read them without leaving the pane. If your browser doesn't have a built-in PDF viewer, a Download button appears instead. Artefacts that have no readable file backing them (orphan rows, files removed from disk, unsupported content types) show a one-line banner explaining the skip instead of opening to a blank pane. Click a project row to open the Graph view focused on that project's neighbourhood; clicking a second project swaps the focus rather than stacking on top. The sidebar's right edge is drag-resizable on every admin page (Sessions root, Graph, and Data): drag the handle to widen or narrow the sidebar, and your chosen width is remembered across reloads and shared across all three pages. The drag handle is mounted by each AdminShell consumer rather than by AdminShell itself, so any new admin route must include `<SidebarSplitter />` as a direct child of its `<AdminShell>` to pick up the shared width. The chat and artefact divider is also drag-resizable: drag the line between the columns to make either side wider; double-click it to reset to half of the available width (viewport minus sidebar), clamped to the chat and artefact min-width floors. Your chosen width is remembered across reloads. On wider screens (>1280px) all three panes are visible. The sidebar narrows at 1280px, the artefact pane hides at 1080px (Browser, Data, and Graph then open as full-window pages instead), and the sidebar collapses to a 56px icon rail at 820px. On every viewport the chat header reads left to right as a triptych: a dedicated sidebar toggle (the panel-right icon, which swaps to panel-right-open when the sidebar is showing), the brand mark next to the title in the centre, and the menu burger on the right. This header toggle is the sole sidebar-toggle button; the sidebar itself no longer carries a duplicate. Tap the sidebar toggle to show or hide the sidebar: on phones (<720px) it slides the drawer in or out, on wider screens it collapses or expands the sidebar column. The brand mark in the centre is decorative; clicks go through the dedicated toggle so the affordance is unambiguous. The drawer animation only fires on tap (220ms slide in or out); resizing your window across the 720px boundary snaps the layout without animation, so you never see a half-open flash. At ≤640px the session metadata pane stacks each label above its value instead of the desktop two-column grid, and the row of action buttons (Open in new tab / Open JSONL / Rename / Pin / Archive / End or Purge) collapses behind a single Actions trigger that opens a popover upward from the foot of the pane. Breakpoint summary: >1280px = full sidebar + chat + artefact pane (drag-resizable divider); 1280px→1080px = sidebar narrows; 1080px→820px = artefact pane hides (Browser/Data/Graph open as full-window pages instead); 820px→720px = sidebar collapses to 56px icon rail; ≤720px = sidebar becomes off-canvas drawer (vertical stack of nav, recents list, foot, the same shape as the desktop sidebar, just on top of the chat instead of beside it).
|
|
74
74
|
|
|
75
75
|
Page titles are brand-aware: the browser tab shows your product name (e.g. `Real Agent` instead of `Maxy`) on every shell — chat, graph, and data — so a non-default brand never leaks the default name in tab strips or browser history.
|
|
76
76
|
|
|
77
|
-
**Session lifecycle and reconcile model.** The sidebar Sessions list is driven by a single Server-Sent Events feed at `/api/admin/claude-sessions/events`. The session manager watches the two directories Claude Code writes (`${CLAUDE_CONFIG_DIR}/sessions/<pid>.json` for live state, `${CLAUDE_CONFIG_DIR}/projects/<slug>/<sid>.jsonl` for transcripts) and emits `row-created`, `row-updated`, `row-archived`, or `row-removed` deltas to every connected browser tab. On connect the manager replays the current row index so a freshly-opened tab catches up without polling, then streams deltas as files change on disk. Two open tabs see the same list within ~300ms of any spawn, status flip, or exit; no refresh button required for state to be current. The legacy `/list` fetch and `useAdminSessions` hook stay mounted to serve the ConversationsModal and the post-action reconcile path in `session-actions`, but the sidebar's visible rows come from the row store, not from `/list`. Each EventSource open emits `[admin-events] client-connected ip=<…> seeded-rows=<n>` server-side and `[admin-ui] session-row-store connected events-received=<n>` in the browser console; transport drops log `[admin-ui] session-row-store reconnect trigger=<auto|manual> attempt=<n> delay-ms=<n>` until the EventSource reattaches. The small dot at the right edge of the Active/Archived/All segmented control is the live-updates indicator: sage when the SSE feed is connected, grey when the feed has dropped. The grey state is an actionable button — clicking it cancels any pending backoff and re-opens the feed immediately, with the click logged as `trigger=manual` so manual retries are distinguishable from automatic ones in the console.
|
|
77
|
+
**Session lifecycle and reconcile model.** The sidebar Sessions list is driven by a single Server-Sent Events feed at `/api/admin/claude-sessions/events`. The session manager watches the two directories Claude Code writes (`${CLAUDE_CONFIG_DIR}/sessions/<pid>.json` for live state, `${CLAUDE_CONFIG_DIR}/projects/<slug>/<sid>.jsonl` for transcripts) and emits `row-created`, `row-updated`, `row-archived`, or `row-removed` deltas to every connected browser tab. Three real delete shapes map to deltas — there is no fourth: PID file gone with JSONL surviving demotes the row to `row-archived`; PID file gone with no JSONL ever written (the per-turn recorder case) emits `row-removed` against the unindexed sessionId; a JSONL deletion against an already-unindexed row also emits `row-removed`. The recorder branch is what reconciles transient hidden spawns — without it, ghost rows persist after the recorder exits. On connect the manager replays the current row index so a freshly-opened tab catches up without polling, then streams deltas as files change on disk. Two open tabs see the same list within ~300ms of any spawn, status flip, or exit; no refresh button required for state to be current. The legacy `/list` fetch and `useAdminSessions` hook stay mounted to serve the ConversationsModal and the post-action reconcile path in `session-actions`, but the sidebar's visible rows come from the row store, not from `/list`. Each EventSource open emits `[admin-events] client-connected ip=<…> seeded-rows=<n>` server-side and `[admin-ui] session-row-store connected events-received=<n>` in the browser console; transport drops log `[admin-ui] session-row-store reconnect trigger=<auto|manual> attempt=<n> delay-ms=<n>` until the EventSource reattaches. The small dot at the right edge of the Active/Archived/All segmented control is the live-updates indicator: sage when the SSE feed is connected, grey when the feed has dropped. The grey state is an actionable button — clicking it cancels any pending backoff and re-opens the feed immediately, with the click logged as `trigger=manual` so manual retries are distinguishable from automatic ones in the console. The refresh icon at the top of the Sessions list is the operator-recoverable reconcile path against any SSE gap: it fetches `/api/admin/claude-sessions` and passes the authoritative id set to the row store, which evicts any indexed row that the server no longer reports. SSE replay only re-asserts currently-indexed rows and never emits `row-removed` for a row that vanished while disconnected, so without this manual surface a stale row can persist until the operator reloads the tab. Each click logs `[admin-ui] session-row-store reconcile evicted=<n> kept=<n>` when at least one row is evicted, and is silent otherwise.
|
|
78
78
|
|
|
79
79
|
The row feed sits behind `requireAdminSession` like every other admin route, so the URL must carry `?session_key=<cacheKey>` — `EventSource` cannot send custom headers, so the query string is the only viable transport. Every admin URL (fetch and EventSource alike) routes through the shared `appendAdminSessionKey(url, cacheKey)` helper exported from `app/lib/useAdminFetch.ts`, which is the single source of truth for the convention; no caller constructs the query string by hand. On a 4xx rejection the browser-side store probes the same URL once per reconnect (suppressed after a successful `open`, capped at one fetch per attempt) and logs `[admin-ui] session-row-store sse-error status=<n> code=<code> attempt=<n>`. The `code` field uses the closed `AdminSessionRejectCode` taxonomy (`session-missing | session-not-registered | session-expired-age | grant-expired`, plus a default `unknown` bucket) that mirrors the server-side rejection emitted by `requireAdminSession`, so a single grep correlates client and server timelines on the same code.
|
|
80
80
|
|
|
81
81
|
The trade-off is a longer-lived connection per tab: the manager's per-process subscriber count rises with open tabs, and the SSE channel must survive proxy idle timeouts. The manager emits a 25-second keep-alive comment line on every connection (ignored by EventSource consumers, refreshes the proxy clock) and the browser-side store force-closes-and-reconnects on transport errors with exponential backoff capped at 30s.
|
|
82
82
|
|
|
83
|
-
**Spawn lifecycle: PID-file driven.** Clicking "+ New session" spawns the PTY and waits for Claude Code's PID file to appear at `${CLAUDE_CONFIG_DIR}/sessions/<pid>.json`. The PID file lands at process init (for `entrypoint: cli` spawns) and carries the intrinsic `sessionId`, `bridgeSessionId`, `agent`, and `status` directly. The manager's filesystem watcher reports the create event; the spawn response includes the canonical `sessionId` from that file. URL capture still runs in parallel to populate the operator-facing iframe URL, but it no longer gates readiness. The JSONL transcript is written on the first operator turn (true on 2.1.143 and 2.1.128); the watcher fires a separate event for that, and `/list`, `/meta`, `/log` resolve any of four ids — `sessionId`, `bridgeSessionId`, `bridgeSuffix`, or numeric `pid` — to the same row.
|
|
83
|
+
**Spawn lifecycle: PID-file driven.** Clicking "+ New session" spawns the PTY and waits for Claude Code's PID file to appear at `${CLAUDE_CONFIG_DIR}/sessions/<pid>.json`. The PID file lands at process init (for `entrypoint: cli` spawns) and carries the intrinsic `sessionId`, `bridgeSessionId`, `agent`, and `status` directly. The manager's filesystem watcher reports the create event; the spawn response includes the canonical `sessionId` from that file. URL capture still runs in parallel to populate the operator-facing iframe URL, but it no longer gates readiness. The JSONL transcript is written on the first operator turn (true on 2.1.143 and 2.1.128); the watcher fires a separate event for that, and `/list`, `/meta`, `/log` resolve any of four ids — `sessionId`, `bridgeSessionId`, `bridgeSuffix`, or numeric `pid` — to the same row. Every fresh spawn also injects a first user turn so the agent has the stimulus the CLI needs to emit its first reply: the Sidebar's POST body carries `initialMessage` set to the current ISO-8601 UTC timestamp (e.g. `2026-05-18T13:15:45.398Z`), computed at click-time on the client. The server forwards it as `POST /<id>/input { text }` immediately after the spawn returns. The timestamp is the minimum-information opener — it has no instructional meaning, so the agent reads it as "begin", not as a directive to do something. Resume flows are unaffected (the prior transcript is the stimulus). Out of scope here: the *content* of the agent's reply, which is owned by the orchestrator's greeting rules.
|
|
84
84
|
|
|
85
85
|
**Stop vs. delete.** `POST /<id>/stop` sends SIGTERM, leaves the JSONL on disk for audit, and is idempotent against an already-dead row. `DELETE /<id>` removes the JSONL + sidecar + per-session subdir and returns 409 if the PTY is still alive (stop first). Any unknown id returns 404; nothing returns a silent 204 against an id the manager does not know.
|
|
86
86
|
|
|
87
87
|
The metadata pane subscribes to the same /list projection. When an operator clicks End on an alive row, the DELETE returns 200 and the post-mutation refetch decides what happens next: a session that wrote a JSONL surfaces as a dehydrated `status: 'ended'` row (the pane swaps `End session` for `Purge JSONL` plus `Resume`), and a session that never wrote a JSONL (`Turns: 0`) leaves the list entirely (the pane shows a `Session ended without a transcript. Close this pane.` banner with a Close button and no destructive action). The manager's `/list` and `/meta` are the only authorities on post-End state; the client does not pre-empt either response with an optimistic mutation.
|
|
88
88
|
|
|
89
|
+
**Admin URL hygiene: `?sessionId=<id>` is retained only while `/meta` returns 200.** The shell hydrates `selectedSessionId` from the query-string on mount so a banner-click redirect can re-open a session. The first `/meta 404` (the session has been deleted out from under the slug) strips the query-string via `history.replaceState`, clears the selection, and emits `[admin-ui] stale-session-slug-stripped sessionId=<8-prefix> trigger=meta-404`. A reload from the dead URL therefore starts at base instead of re-resolving a 404.
|
|
90
|
+
|
|
89
91
|
The Data search panel ranks results by combining vector similarity with keyword (BM25) matching. Each row shows a one-line score breakdown — `vector 0.NN · bm25 0.NN · combined 0.NN` — so you can tell whether a row surfaced because of meaning, exact-keyword match, or both. A bm25 column of `0.00` across every row means your search term wasn't in the keyword index, so ranking fell back to pure vector similarity (this can produce surprising results — the breakdown tells you when to interpret with caution). Above the result list, a chip row shows the unique types in your current results — click one to filter, click again to clear. Click any row to jump straight to that node's neighbourhood in the Graph; from the artefact pane the graph opens alongside chat, from the standalone Data page it opens in place.
|
|
90
92
|
|
|
91
93
|
## Software Update and Cloudflare Setup
|
|
@@ -73,7 +73,7 @@ Brand decides which premium plugins ship. Maxy installs ship only `platform/plug
|
|
|
73
73
|
| Plugin | Type | What it does | Public agent |
|
|
74
74
|
|--------|------|-------------|-------------|
|
|
75
75
|
| `teaching` | Skills | Interactive tutoring, lesson planning, and study pack generation from your knowledge base | Yes — all 3 skills serve students and parents |
|
|
76
|
-
| `real-agent` | Bundle (10 sub-plugins) | UK estate agency skills — sales, listings, vendor management, buyer management, lead generation, coaching, business operations, teaching, Loop CRM, and property brochures. 3 specialist roles (negotiator, valuer, compliance) | 4 sub-plugins (estate-sales, buyers, estate-coaching, estate-teaching) |
|
|
76
|
+
| `real-agent` | Bundle (10 sub-plugins) | UK estate agency skills — sales, listings, vendor management, buyer management, lead generation, coaching, business operations, teaching, Loop CRM (five value pillars: auto-respond, viewing lifecycle, pipeline mining, listings prospecting, maintenance & preferences), and property brochures. 3 specialist roles (negotiator, valuer, compliance) | 4 sub-plugins (estate-sales, buyers, estate-coaching, estate-teaching) |
|
|
77
77
|
| `writer-craft` | Skills + Agent | Manuscript review and writing craft — story architecture, reader engagement, prose craft, editorial practice, and multi-level review | No — writing craft serves the author |
|
|
78
78
|
|
|
79
79
|
**How it works:** Every boot {{productName}} delivers the brand's premium plugins from staging into `platform/plugins/` and stamps `enabledPlugins` against what is actually on disk. No conversation needed — the brand's full set is active from the first turn after install. Updates and reinstalls re-deliver from staging.
|
|
@@ -178,7 +178,7 @@ If the initial Cloudflare login fails during setup, {{productName}} will fall ba
|
|
|
178
178
|
|
|
179
179
|
**Continuous `err-class=econnrefused-coldstart` for >30 s past the last `[edge] listening` line** indicates the upstream never binds — the upgrade or boot has stalled. Recover via `sudo systemctl --user status maxy.service` and check the action runner log per the next section. Permanent-failure UI escalation (turning the holding page into an error after N seconds) is intentionally deferred.
|
|
180
180
|
|
|
181
|
-
**The literal string `maxy-ui` should never appear in `edge.log` or in any user-visible 502 body**, regardless of brand. If it does, the edge is running
|
|
181
|
+
**The literal string `maxy-ui` should never appear in `edge.log` or in any user-visible 502 body**, regardless of brand. If it does, the edge is running stale code — re-bundle and re-publish.
|
|
182
182
|
|
|
183
183
|
**Verifying the holding page locally:** `curl -sS -H 'Accept: text/html' http://127.0.0.1:<EDGE_PORT>/` while `maxy.service` is stopped should return HTML containing the brand `productName`. The `Accept: text/html` header is required — non-html clients (default `curl`, `fetch`, XHR) get the branded plain-text 502 instead, so the holding page's own `/api/health` polls don't break themselves during cold-start.
|
|
184
184
|
|
|
@@ -262,7 +262,7 @@ Failure modes:
|
|
|
262
262
|
|
|
263
263
|
## Software Update click shows an error instead of opening the terminal
|
|
264
264
|
|
|
265
|
-
> **
|
|
265
|
+
> **First-line diagnostic for the byte-stream xterm.js terminal surface:** `sudo systemctl --user status maxy-ttyd` plus `sudo grep 'ttyd-proxy' ~/.maxy/logs/edge-boot.log | tail -20`. Failure mode signals: `ttyd-ws-upgrade accepted` with no `ttyd-proxy-open` → `maxy-ttyd.service` is down; `ttyd-proxy-open` with no `ttyd-proxy-chunk dir=upstream→client` → ttyd/tmux is not attaching a PTY.
|
|
266
266
|
|
|
267
267
|
**Symptom:** You clicked **Upgrade** in the Software Update modal, but instead of the VNC terminal overlay appearing, the modal shows a red error row like:
|
|
268
268
|
|
|
@@ -337,7 +337,7 @@ sudo grep 'started.*reason=upgrade' ~/.maxy/logs/terminal-launch.log | tail -3
|
|
|
337
337
|
# Expected shape: started pid=<N> display=:99 cmd="/usr/bin/xterm... -e bash -c 'npx -y @rubytech/create-maxy@latest; exec bash'" transport=vnc windowPresent=true reason=upgrade
|
|
338
338
|
```
|
|
339
339
|
|
|
340
|
-
If the `cmd=` field does not contain `-e bash -c`, re-run the installer — the vnc.sh on the device is
|
|
340
|
+
If the `cmd=` field does not contain `-e bash -c`, re-run the installer — the vnc.sh on the device is stale. If the command IS logged correctly but nothing is running, open the VNC overlay and type `history | tail` inside the shell — if the npx line is there, it ran and exited (check `~/.maxy/logs/install-*.log` for the exit status).
|
|
341
341
|
|
|
342
342
|
---
|
|
343
343
|
|
|
@@ -533,9 +533,9 @@ sudo systemctl --user start maxy-ui
|
|
|
533
533
|
|
|
534
534
|
## Admin DevTools console floods with `onboarding-banner-mount` or `sessions-poll` lines
|
|
535
535
|
|
|
536
|
-
**
|
|
536
|
+
**Regression symptom.** Open DevTools on the admin shell at `/` with `onboardingComplete=false`, leave the page idle for a minute, then scroll back through the console. Thousands of `[admin-ui] onboarding-banner-mount onboardingComplete=false` lines (one per AdminShell render, ~40/min driven by the 3s sessions poll) with no per-tick poll telemetry indicates the banner-mount log has regressed back into the render body.
|
|
537
537
|
|
|
538
|
-
**Steady-state invariants at
|
|
538
|
+
**Steady-state invariants at `/`:**
|
|
539
539
|
|
|
540
540
|
- `grep -c '\[admin-ui\] onboarding-banner-mount' ~/.maxy/logs/admin-ui-console.log` equals page-load count plus onboarding-flip count, not the render count. Sustained climb at idle means the banner mount log regressed back into the render body (fix).
|
|
541
541
|
- `grep -c '\[admin-ui\] sessions-poll' ~/.maxy/logs/admin-ui-console.log` over a 60-minute idle window equals zero. The hook no longer installs a `setInterval`; every `sessions-poll` line is operator-triggered (initial mount, refresh button, post-mutation refetch). One or more lines during operator idle means `setInterval` was reinstated.
|
|
@@ -18,7 +18,13 @@ tools:
|
|
|
18
18
|
publicAllowlist: false
|
|
19
19
|
- name: loop-property-listed
|
|
20
20
|
publicAllowlist: false
|
|
21
|
-
- name: loop-property-
|
|
21
|
+
- name: loop-property-sold
|
|
22
|
+
publicAllowlist: false
|
|
23
|
+
- name: loop-property-viewing
|
|
24
|
+
publicAllowlist: false
|
|
25
|
+
- name: loop-property-callback
|
|
26
|
+
publicAllowlist: false
|
|
27
|
+
- name: loop-property-information
|
|
22
28
|
publicAllowlist: false
|
|
23
29
|
- name: loop-viewing-search
|
|
24
30
|
publicAllowlist: false
|
|
@@ -44,6 +50,8 @@ tools:
|
|
|
44
50
|
publicAllowlist: false
|
|
45
51
|
- name: loop-marketing-enquiry
|
|
46
52
|
publicAllowlist: false
|
|
53
|
+
- name: loop-auto-responder
|
|
54
|
+
publicAllowlist: false
|
|
47
55
|
- name: loop-customer-preferences
|
|
48
56
|
publicAllowlist: false
|
|
49
57
|
- name: loop-supplier
|
|
@@ -112,20 +120,67 @@ Never include API key values in responses. Identify teams by name only. When the
|
|
|
112
120
|
|
|
113
121
|
## When to use these tools
|
|
114
122
|
|
|
115
|
-
|
|
123
|
+
Loop covers five value pillars with API surface. Each pillar has both read and write triggers — use reads to surface what's already in Loop unprompted, use writes when the operator's intent is to record a decision. See `.docs/loop-value-extraction.md` for the verbatim Loop value claims and swagger endpoint citations behind each pillar.
|
|
124
|
+
|
|
125
|
+
### Pillar 1 — Auto-respond to portal enquiries (Loop's stated 40% efficiency lever)
|
|
126
|
+
|
|
127
|
+
**Read triggers** — operator says "what enquiries came in", "any new enquiries", "did we get anything from Rightmove":
|
|
128
|
+
- `loop-auto-responder` (action=get) — fetch enquiry questions + applicant details
|
|
129
|
+
|
|
130
|
+
**Write triggers** — operator says "answer the enquiry", "send the auto-responder reply", "refer it to <team>":
|
|
131
|
+
- `loop-auto-responder` (action=answers) — submit applicant answers
|
|
132
|
+
- `loop-auto-responder` (action=details) — update enquiry record
|
|
133
|
+
- `loop-auto-responder` (action=refer) — refer enquiry to another team
|
|
134
|
+
- `loop-marketing-enquiry` — submit a seller-side enquiry
|
|
135
|
+
|
|
136
|
+
### Pillar 2 — Viewing lifecycle (book → feedback → notes)
|
|
137
|
+
|
|
138
|
+
**Read triggers** — "what viewings are coming up", "any viewings without feedback yet", "did we hear back from <name>":
|
|
139
|
+
- `loop-viewing-search`, `loop-viewing-detail`, `loop-feedback-get`
|
|
140
|
+
|
|
141
|
+
**Write triggers** — "book a viewing", "the viewer said X", "the seller wants Y", "add a note":
|
|
142
|
+
- `loop-viewing-create` — book a viewing
|
|
143
|
+
- `loop-viewing-update` — record buyer/renter feedback, seller/landlord feedback, or a note
|
|
144
|
+
- `loop-feedback-submit` — explicit feedback submission against a viewing
|
|
145
|
+
|
|
146
|
+
### Pillar 3 — Pipeline mining (unconverted enquiries, stale matches)
|
|
147
|
+
|
|
148
|
+
**Read triggers** — "who haven't we followed up", "any matches we haven't actioned", "find people looking in <area>", "show me buyers":
|
|
149
|
+
- `loop-people-search`, `loop-people-detail`
|
|
150
|
+
- `loop-marketing-match`, `loop-marketing-match-batch`
|
|
151
|
+
|
|
152
|
+
**Write triggers** — "request a viewing for the match", "ask them to call", "send them more info":
|
|
153
|
+
- `loop-marketing-match-request`
|
|
154
|
+
|
|
155
|
+
### Pillar 4 — Listings prospecting (canvassing + competitive sweep)
|
|
156
|
+
|
|
157
|
+
**Read triggers** — "what have we sold on <street>", "what's listed on Rightmove", "give me canvassing material for <postcode>":
|
|
158
|
+
- `loop-property-sold` — exchanged/completed properties by channel (canvassing source)
|
|
159
|
+
- `loop-property-listed` — currently active listings by channel
|
|
160
|
+
- `loop-property-search`, `loop-property-detail`
|
|
161
|
+
|
|
162
|
+
**Write triggers** — "request a viewing", "ask them to call back", "request a brochure":
|
|
163
|
+
- `loop-property-viewing` — viewing request against a specific property
|
|
164
|
+
- `loop-property-callback` — callback request
|
|
165
|
+
- `loop-property-information` — information/brochure request
|
|
166
|
+
|
|
167
|
+
### Pillar 5 — Maintenance closeout and customer preferences
|
|
168
|
+
|
|
169
|
+
**Read triggers** — "what does <person> want", "any open maintenance jobs", "any quotes outstanding":
|
|
170
|
+
- `loop-customer-preferences` (action=read)
|
|
171
|
+
- `loop-supplier` (action=maintenance-jobs, maintenance-quotes, board-jobs)
|
|
172
|
+
|
|
173
|
+
**Write triggers** — "save what they're looking for", "mark the job complete", "submit a quote":
|
|
174
|
+
- `loop-customer-preferences` (action=write)
|
|
175
|
+
- `loop-supplier` (action=maintenance-complete, board-complete, maintenance-submit-quote)
|
|
176
|
+
|
|
177
|
+
### Team-key prerequisite
|
|
116
178
|
|
|
117
|
-
|
|
118
|
-
- "submit", "send", "log", "record" → `loop-feedback-submit`, `loop-marketing-enquiry`, `loop-property-request`
|
|
119
|
-
- "create", "book", "schedule", "set up" a viewing → `loop-viewing-create`
|
|
120
|
-
- "add a note", "leave feedback", "record what they said" → `loop-viewing-update`
|
|
121
|
-
- "request a callback", "request more info", "ask for a viewing" → `loop-property-request` or `loop-marketing-match-request`
|
|
122
|
-
- "update preferences", "save what they're looking for" → `loop-customer-preferences` (action=write)
|
|
123
|
-
- "mark complete", "close out", "finished the job" → `loop-supplier` (action=maintenance-complete or board-complete)
|
|
124
|
-
- "submit a quote" → `loop-supplier` (action=maintenance-submit-quote)
|
|
179
|
+
Every Loop tool routes through one or more team keys. If `loop-key-list` returns nothing for the account, surface the registration prompt before attempting any other Loop call — every downstream tool will fail otherwise. If a call returns `Team "X" does not have <group> permission`, the key was scoped to fewer than 8 endpoint groups; re-register with the full permissions array (`["properties","people","viewings","feedback","team","marketing","customer","supplier"]`).
|
|
125
180
|
|
|
126
|
-
|
|
181
|
+
### What Loop markets without an API surface (do not promise these)
|
|
127
182
|
|
|
128
|
-
|
|
183
|
+
Loop's marketing pages also list Tasks/Reminders, full Diary beyond `team/availability`, Market Data (distressed properties, instructed sellers), Inspections & Certificates, and Client Accounts. Loop's public V2 API does not expose these — Real Agent cannot do them. See `.docs/loop-api-gaps.md` for the full inventory.
|
|
129
184
|
|
|
130
185
|
## Cross-cutting skills hosted here
|
|
131
186
|
|