@rubytech/create-maxy-code 0.1.66 → 0.1.68
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/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/turn-completed-graph-write.test.sh +251 -94
- package/payload/platform/plugins/admin/hooks/turn-completed-graph-write.sh +119 -71
- package/payload/platform/plugins/docs/references/admin-session.md +54 -11
- package/payload/platform/plugins/docs/references/deployment.md +2 -2
- package/payload/platform/plugins/docs/references/internals.md +2 -0
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-not-applicable.test.js +3 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-not-applicable.test.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js +55 -55
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts +4 -4
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +3 -0
- 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 +73 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +18 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts +42 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js +330 -0
- package/payload/platform/services/claude-session-manager/dist/jsonl-tail.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-tools-probe.d.ts +36 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-tools-probe.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-tools-probe.js +131 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-tools-probe.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +53 -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 +134 -6
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-store.d.ts +9 -0
- package/payload/platform/services/claude-session-manager/dist/session-store.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/session-store.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +8 -5
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +35 -16
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
- package/payload/platform/templates/agents/admin/IDENTITY.md +8 -0
- package/payload/platform/templates/agents/admin/SOUL.md +1 -1
- package/payload/platform/templates/specialists/agents/database-operator.md +35 -10
- package/payload/premium-plugins/real-agent/plugins/brochures/skills/property-brochure/SKILL.md +36 -5
- package/payload/server/{chunk-S6PQA6Q4.js → chunk-HZ77KMTV.js} +1 -1
- package/payload/server/{chunk-NPZKH5EC.js → chunk-ITQ55PCQ.js} +1 -1
- package/payload/server/{chunk-X3Y6ENKA.js → chunk-VGMVEYX7.js} +35 -61
- package/payload/server/{cloudflare-task-tracker-TL6R364J.js → cloudflare-task-tracker-PZBX7X3D.js} +2 -2
- package/payload/server/maxy-edge.js +2 -2
- package/payload/server/public/assets/{admin-ymReS1mN.js → admin-BPRjc4z9.js} +3 -3
- package/payload/server/public/assets/{architectureDiagram-Q4EWVU46-DgfgldJT.js → architectureDiagram-Q4EWVU46-Asw7tNG5.js} +1 -1
- package/payload/server/public/assets/{blockDiagram-DXYQGD6D-agu8GDzA.js → blockDiagram-DXYQGD6D-Blvay4_b.js} +1 -1
- package/payload/server/public/assets/{brand-BWCtpG7_.js → brand-B8JhGW9e.js} +1 -1
- package/payload/server/public/assets/{brand-BDASZqMG.css → brand-CwHvVF8s.css} +1 -1
- package/payload/server/public/assets/{c4Diagram-AHTNJAMY-CmNsUmAe.js → c4Diagram-AHTNJAMY-C2st6jZP.js} +1 -1
- package/payload/server/public/assets/channel-DTh_uFbz.js +1 -0
- package/payload/server/public/assets/{chunk-336JU56O-D9Uts7k9.js → chunk-336JU56O-Cp7B2yh1.js} +2 -2
- package/payload/server/public/assets/{chunk-426QAEUC-D4ABqXMn.js → chunk-426QAEUC-B0x3RXvj.js} +1 -1
- package/payload/server/public/assets/{chunk-4TB4RGXK-D1KE4OAX.js → chunk-4TB4RGXK-B0yM0pgq.js} +1 -1
- package/payload/server/public/assets/{chunk-5FUZZQ4R-CAh0GXe2.js → chunk-5FUZZQ4R-CqGhfArO.js} +1 -1
- package/payload/server/public/assets/{chunk-5PVQY5BW-DBHa6HOv.js → chunk-5PVQY5BW-jSeD3Yrj.js} +1 -1
- package/payload/server/public/assets/{chunk-EDXVE4YY-BDC-6mL3.js → chunk-EDXVE4YY-C7HuvG2r.js} +1 -1
- package/payload/server/public/assets/{chunk-ENJZ2VHE-CH3uROkW.js → chunk-ENJZ2VHE-DzCrge6H.js} +1 -1
- package/payload/server/public/assets/{chunk-ICPOFSXX-BzZf0nAw.js → chunk-ICPOFSXX-BPvCh0Yc.js} +1 -1
- package/payload/server/public/assets/{chunk-OYMX7WX6-BrLHrI6O.js → chunk-OYMX7WX6-DamxwrTn.js} +1 -1
- package/payload/server/public/assets/{chunk-U2HBQHQK-Bw4UkLe_.js → chunk-U2HBQHQK-DDiVd0qV.js} +1 -1
- package/payload/server/public/assets/{chunk-X2U36JSP-23xgdk05.js → chunk-X2U36JSP-CDof681v.js} +1 -1
- package/payload/server/public/assets/{chunk-YZCP3GAM-BA8iGgRa.js → chunk-YZCP3GAM-DYzaYjrN.js} +1 -1
- package/payload/server/public/assets/{chunk-ZZ45TVLE-B-ol4EgR.js → chunk-ZZ45TVLE-j0bcHHzU.js} +1 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-Dsupydrn.js +1 -0
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-DrwAXnBf.js +1 -0
- package/payload/server/public/assets/clone-COx5mf-H.js +1 -0
- package/payload/server/public/assets/{dagre--kXszEIb.js → dagre-C8nbdG6y.js} +1 -1
- package/payload/server/public/assets/{dagre-KV5264BT-amN1uU2d.js → dagre-KV5264BT-1RnGN_fy.js} +1 -1
- package/payload/server/public/assets/data-DOI26n9p.js +1 -0
- package/payload/server/public/assets/{device-url-actions-CPoz5fqz.js → device-url-actions-C_Keyr0L.js} +1 -1
- package/payload/server/public/assets/{diagram-5BDNPKRD-Cwizs9tP.js → diagram-5BDNPKRD-C6d3Rrui.js} +1 -1
- package/payload/server/public/assets/{diagram-G4DWMVQ6-BpSu5Uao.js → diagram-G4DWMVQ6-aD4Dik01.js} +1 -1
- package/payload/server/public/assets/{diagram-MMDJMWI5-DiArtkto.js → diagram-MMDJMWI5-CAub49YE.js} +1 -1
- package/payload/server/public/assets/{diagram-TYMM5635-_tygK58Y.js → diagram-TYMM5635-CAKnAfEr.js} +1 -1
- package/payload/server/public/assets/{erDiagram-SMLLAGMA-CBV6LWYA.js → erDiagram-SMLLAGMA-BrqOworC.js} +1 -1
- package/payload/server/public/assets/{flowDiagram-DWJPFMVM-CHZFrtOU.js → flowDiagram-DWJPFMVM-DV4eU-T2.js} +1 -1
- package/payload/server/public/assets/{ganttDiagram-T4ZO3ILL-B3vKq5ov.js → ganttDiagram-T4ZO3ILL-DayXUN42.js} +1 -1
- package/payload/server/public/assets/{gitGraphDiagram-UUTBAWPF-dnJPjbtj.js → gitGraphDiagram-UUTBAWPF-CK1xV1OV.js} +1 -1
- package/payload/server/public/assets/graph-DP9P7TKZ.js +1 -0
- package/payload/server/public/assets/graph-labels-BoLyECfJ.js +1 -0
- package/payload/server/public/assets/{graphlib-CNXtnega.js → graphlib-CkF9o1XT.js} +1 -1
- package/payload/server/public/assets/{infoDiagram-42DDH7IO-D8DYwVAF.js → infoDiagram-42DDH7IO-CiepM2H6.js} +1 -1
- package/payload/server/public/assets/{ishikawaDiagram-UXIWVN3A-CfEVY2Ud.js → ishikawaDiagram-UXIWVN3A-DwNkdQ6z.js} +1 -1
- package/payload/server/public/assets/{journeyDiagram-VCZTEJTY-FmoBq7Kh.js → journeyDiagram-VCZTEJTY-BlDLMfu_.js} +1 -1
- package/payload/server/public/assets/{kanban-definition-6JOO6SKY-B-Q9qk99.js → kanban-definition-6JOO6SKY-ayZTc0Oj.js} +1 -1
- package/payload/server/public/assets/{line-DZwbJENP.js → line-LhPUiuH-.js} +1 -1
- package/payload/server/public/assets/{mermaid-parser.core-C3ahKykw.js → mermaid-parser.core-DyZYsQv_.js} +1 -1
- package/payload/server/public/assets/{mermaid.core-B8OFZIE8.js → mermaid.core-CauxgRdz.js} +3 -3
- package/payload/server/public/assets/{mindmap-definition-QFDTVHPH-CqhkfbWa.js → mindmap-definition-QFDTVHPH-CQzcaJuh.js} +1 -1
- package/payload/server/public/assets/{page-D-OLjort.js → page-BGv81dGN.js} +1 -1
- package/payload/server/public/assets/{page-DFFt9_Q0.js → page-DAw3-U_s.js} +2 -2
- package/payload/server/public/assets/{pieDiagram-DEJITSTG-CJqQ6u3-.js → pieDiagram-DEJITSTG-BPhAeTm9.js} +1 -1
- package/payload/server/public/assets/{public-rWe9ADCS.js → public-CGI24wrL.js} +5 -5
- package/payload/server/public/assets/{quadrantDiagram-34T5L4WZ-BtEqOXQM.js → quadrantDiagram-34T5L4WZ-CxrzL3ZF.js} +1 -1
- package/payload/server/public/assets/{requirementDiagram-MS252O5E-DPHWUtE6.js → requirementDiagram-MS252O5E-ZBSC4fGX.js} +1 -1
- package/payload/server/public/assets/{sankeyDiagram-XADWPNL6-FBmc3wHT.js → sankeyDiagram-XADWPNL6-BTDBEisC.js} +1 -1
- package/payload/server/public/assets/{sequenceDiagram-FGHM5R23-BMSd2-1M.js → sequenceDiagram-FGHM5R23-Bv7fA5_J.js} +1 -1
- package/payload/server/public/assets/{stateDiagram-FHFEXIEX-DqFbU-LT.js → stateDiagram-FHFEXIEX-D07lcZrk.js} +1 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-Cr1Qet-f.js +1 -0
- package/payload/server/public/assets/{timeline-definition-GMOUNBTQ-DsfvHVK8.js → timeline-definition-GMOUNBTQ-DyeyUqKa.js} +1 -1
- package/payload/server/public/assets/{vennDiagram-DHZGUBPP-kHU05mqC.js → vennDiagram-DHZGUBPP-B9FqDmwS.js} +1 -1
- package/payload/server/public/assets/{wardleyDiagram-NUSXRM2D-C696Mez_.js → wardleyDiagram-NUSXRM2D-4-5uCIAD.js} +1 -1
- package/payload/server/public/assets/{xychartDiagram-5P7HB3ND-yht-JyXJ.js → xychartDiagram-5P7HB3ND-WdkP5T_y.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +5 -5
- package/payload/server/public/index.html +7 -7
- package/payload/server/public/public.html +4 -4
- package/payload/server/server.js +20 -6
- package/payload/server/public/assets/channel-DXGOX55q.js +0 -1
- package/payload/server/public/assets/classDiagram-6PBFFD2Q-DTuuDxpK.js +0 -1
- package/payload/server/public/assets/classDiagram-v2-HSJHXN6E-CIw8RU0q.js +0 -1
- package/payload/server/public/assets/clone-O4ZyuvYl.js +0 -1
- package/payload/server/public/assets/data-D6U2e85B.js +0 -1
- package/payload/server/public/assets/graph-C274PN27.js +0 -1
- package/payload/server/public/assets/graph-labels-DQFzd1FY.js +0 -1
- package/payload/server/public/assets/stateDiagram-v2-QKLJ7IA2-CXTnsHGq.js +0 -1
package/package.json
CHANGED
|
@@ -129,7 +129,7 @@ Tools are available via the `admin` MCP server.
|
|
|
129
129
|
- `hooks/pre-tool-use.sh` — enforces admin agent write boundaries
|
|
130
130
|
- `hooks/playwright-file-guard.sh` — rewrites file:// URLs to a backgrounded loopback http.server before Playwright sees them
|
|
131
131
|
- `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.
|
|
132
|
-
- `hooks/turn-completed-graph-write.sh` — 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
|
|
132
|
+
- `hooks/turn-completed-graph-write.sh` — 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 conforming to the database-operator agent file's `## Input shape`. 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.
|
|
133
133
|
|
|
134
134
|
## Session identifiers (Task 135)
|
|
135
135
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Task 129/131/165/175 regression test for the Stop hook.
|
|
2
|
+
# Task 129/131/165/175/177 regression test for the Stop hook.
|
|
3
3
|
#
|
|
4
4
|
# Contract:
|
|
5
5
|
# - Every log emission goes through `POST /api/admin/log-ingest`. Hook
|
|
@@ -11,11 +11,14 @@
|
|
|
11
11
|
# `conversationBytes=<n>` and exactly ONE POST to
|
|
12
12
|
# `/api/admin/claude-sessions`. The body carries the operator
|
|
13
13
|
# session id as `adminSessionId` and an `initialMessage` whose value
|
|
14
|
-
# is a JSON-stringified object with EXACTLY the
|
|
15
|
-
# the
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# `
|
|
14
|
+
# is a JSON-stringified object with EXACTLY the four keys declared by
|
|
15
|
+
# the Task 177 envelope: turns, conversationId, accountId, occurredAt.
|
|
16
|
+
# `turns` is the chronologically-ordered list of conversation turns.
|
|
17
|
+
# No top-level operatorMessage. No top-level assistantReply.
|
|
18
|
+
# No leading instruction prose. No `Conversation id:` prose.
|
|
19
|
+
# - The hook emits one `[turn-recorder] envelope ...` log line before
|
|
20
|
+
# `spawn-request`, naming turnsCount / userTurns / assistantTurns /
|
|
21
|
+
# toolCallTurns.
|
|
19
22
|
# - The hook never writes the legacy `spawn-with-input` / `fired` stderr
|
|
20
23
|
# lines.
|
|
21
24
|
#
|
|
@@ -119,14 +122,12 @@ for raw in sys.stdin:
|
|
|
119
122
|
' || true
|
|
120
123
|
}
|
|
121
124
|
|
|
122
|
-
# Common envelope: a valid Stop-hook stdin shape with a real UUID + a
|
|
123
|
-
# transcript file the hook can read. Transcript has one user turn and
|
|
124
|
-
# one assistant turn.
|
|
125
125
|
start_listener
|
|
126
|
+
# Simple two-turn transcript with timestamps. One user, one assistant text.
|
|
126
127
|
TRANSCRIPT=$(mktemp); TMPFILES+=("$TRANSCRIPT")
|
|
127
128
|
{
|
|
128
|
-
printf '{"type":"user","message":{"role":"user","content":"hello operator"}}\n'
|
|
129
|
-
printf '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"hello, assistant reply"}]}}\n'
|
|
129
|
+
printf '{"type":"user","timestamp":"2026-05-19T21:27:00.000Z","message":{"role":"user","content":"hello operator"}}\n'
|
|
130
|
+
printf '{"type":"assistant","timestamp":"2026-05-19T21:27:01.000Z","message":{"id":"msg_001","role":"assistant","content":[{"type":"text","text":"hello, assistant reply"}]}}\n'
|
|
130
131
|
} > "$TRANSCRIPT"
|
|
131
132
|
ENVELOPE=$(python3 -c '
|
|
132
133
|
import json, sys
|
|
@@ -181,19 +182,33 @@ else
|
|
|
181
182
|
fail "case-4 expected trigger-skipped missing-transcript, got: $(ingest_lines)"
|
|
182
183
|
fi
|
|
183
184
|
|
|
184
|
-
# --- Case 5: happy path
|
|
185
|
+
# --- Case 5 (Task 177): ordered-turns happy path -----------------------
|
|
186
|
+
# Transcript with four turns: user-A, asst-A-reply, user-B, asst-B-reply.
|
|
187
|
+
# Envelope `turns` array must carry exactly that order, oldest first.
|
|
188
|
+
# Envelope shape: { turns, conversationId, accountId, occurredAt } — no
|
|
189
|
+
# top-level operatorMessage / assistantReply.
|
|
190
|
+
ORDERED_TRANSCRIPT=$(mktemp); TMPFILES+=("$ORDERED_TRANSCRIPT")
|
|
191
|
+
{
|
|
192
|
+
printf '{"type":"user","timestamp":"2026-05-19T21:27:42.258Z","message":{"role":"user","content":"New Real Agent session"}}\n'
|
|
193
|
+
printf '{"type":"assistant","timestamp":"2026-05-19T21:27:45.158Z","message":{"id":"msg_a","role":"assistant","content":[{"type":"text","text":"Session noted. What would you like to work on?"}]}}\n'
|
|
194
|
+
printf '{"type":"user","timestamp":"2026-05-19T21:27:57.314Z","message":{"role":"user","content":"you tell me"}}\n'
|
|
195
|
+
printf '{"type":"assistant","timestamp":"2026-05-19T21:28:00.847Z","message":{"id":"msg_b","role":"assistant","content":[{"type":"text","text":"One question to anchor the next move: what is your name?"}]}}\n'
|
|
196
|
+
} > "$ORDERED_TRANSCRIPT"
|
|
197
|
+
ORDERED_ENVELOPE=$(python3 -c '
|
|
198
|
+
import json, sys
|
|
199
|
+
print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
|
|
200
|
+
' "$OP_ID" "$ORDERED_TRANSCRIPT")
|
|
185
201
|
: > "$REQ_LOG"
|
|
186
|
-
run_hook "admin" "" "$
|
|
202
|
+
run_hook "admin" "" "$ORDERED_ENVELOPE"
|
|
187
203
|
[[ "$HOOK_RC" -eq 0 ]] || fail "case-5 rc=$HOOK_RC stderr=$HOOK_STDERR"
|
|
188
204
|
[[ -z "$HOOK_STDERR" ]] || fail "case-5 stderr must be empty, got: $HOOK_STDERR"
|
|
189
205
|
|
|
190
|
-
# 5a. exactly ONE `trigger` log-ingest line carrying the operator session id
|
|
191
|
-
# and `conversationBytes=<n>`.
|
|
206
|
+
# 5a. exactly ONE `trigger` log-ingest line carrying the operator session id.
|
|
192
207
|
TRIGGER_COUNT=$(ingest_lines | grep -cE "^trigger sessionId=${OP_ID} turnIndex=0 transcriptBytes=[0-9]+ conversationBytes=[0-9]+$" || true)
|
|
193
208
|
if [[ "$TRIGGER_COUNT" -eq 1 ]]; then
|
|
194
|
-
pass "case-5a exactly one trigger line
|
|
209
|
+
pass "case-5a exactly one trigger line via log-ingest"
|
|
195
210
|
else
|
|
196
|
-
fail "case-5a expected exactly 1 trigger line
|
|
211
|
+
fail "case-5a expected exactly 1 trigger line, got $TRIGGER_COUNT (lines: $(ingest_lines))"
|
|
197
212
|
fi
|
|
198
213
|
|
|
199
214
|
# 5b. exactly ONE POST to /api/admin/claude-sessions
|
|
@@ -204,10 +219,10 @@ else
|
|
|
204
219
|
fail "case-5b expected exactly 1 recorder-spawn POST, got $RECORDER_LINES"
|
|
205
220
|
fi
|
|
206
221
|
|
|
207
|
-
# 5c.
|
|
208
|
-
#
|
|
209
|
-
#
|
|
210
|
-
#
|
|
222
|
+
# 5c. Envelope has exactly four keys (turns, conversationId, accountId,
|
|
223
|
+
# occurredAt). `turns` carries 4 entries in record order; each has
|
|
224
|
+
# role/text/ts. No `toolCalls` on turns where there were no tool_use
|
|
225
|
+
# blocks (omitted-when-empty rule).
|
|
211
226
|
RECORDER_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
|
|
212
227
|
BODY_OK=$(printf '%s' "$RECORDER_BODY" | python3 -c '
|
|
213
228
|
import sys, json, re
|
|
@@ -216,36 +231,43 @@ acct = sys.argv[2]
|
|
|
216
231
|
try:
|
|
217
232
|
outer = json.load(sys.stdin)
|
|
218
233
|
msg_str = outer.get("initialMessage")
|
|
219
|
-
# initialMessage must be a string carrying a JSON-stringified object.
|
|
220
234
|
if not isinstance(msg_str, str):
|
|
221
235
|
print("no:initialMessage-not-string:"+repr(type(msg_str).__name__))
|
|
222
236
|
sys.exit(0)
|
|
223
237
|
msg = json.loads(msg_str)
|
|
224
238
|
keys = set(msg.keys())
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
"
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
"
|
|
241
|
-
"
|
|
242
|
-
|
|
243
|
-
|
|
239
|
+
turns = msg.get("turns") or []
|
|
240
|
+
conds = []
|
|
241
|
+
conds.append(("envelope-keys", keys == {"turns", "conversationId", "accountId", "occurredAt"}))
|
|
242
|
+
conds.append(("turns-len", len(turns) == 4))
|
|
243
|
+
conds.append(("conversationId", msg.get("conversationId") == op))
|
|
244
|
+
conds.append(("accountId", msg.get("accountId") == acct))
|
|
245
|
+
conds.append(("occurredAt-shape", bool(re.match(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z$", msg.get("occurredAt") or ""))))
|
|
246
|
+
conds.append(("no-operatorMessage", "operatorMessage" not in keys))
|
|
247
|
+
conds.append(("no-assistantReply", "assistantReply" not in keys))
|
|
248
|
+
if len(turns) >= 4:
|
|
249
|
+
conds.append(("t0-role", turns[0].get("role") == "user"))
|
|
250
|
+
conds.append(("t0-text", turns[0].get("text") == "New Real Agent session"))
|
|
251
|
+
conds.append(("t0-ts", turns[0].get("ts") == "2026-05-19T21:27:42.258Z"))
|
|
252
|
+
conds.append(("t0-no-toolCalls", "toolCalls" not in turns[0]))
|
|
253
|
+
conds.append(("t1-role", turns[1].get("role") == "assistant"))
|
|
254
|
+
conds.append(("t1-text", turns[1].get("text") == "Session noted. What would you like to work on?"))
|
|
255
|
+
conds.append(("t1-ts", turns[1].get("ts") == "2026-05-19T21:27:45.158Z"))
|
|
256
|
+
conds.append(("t1-no-toolCalls", "toolCalls" not in turns[1]))
|
|
257
|
+
conds.append(("t2-role", turns[2].get("role") == "user"))
|
|
258
|
+
conds.append(("t2-text", turns[2].get("text") == "you tell me"))
|
|
259
|
+
conds.append(("t3-role", turns[3].get("role") == "assistant"))
|
|
260
|
+
conds.append(("t3-text", turns[3].get("text") == "One question to anchor the next move: what is your name?"))
|
|
261
|
+
conds.append(("outer-specialist", outer.get("specialist") == "database-operator"))
|
|
262
|
+
conds.append(("outer-model", outer.get("model") == "haiku"))
|
|
263
|
+
conds.append(("outer-adminSessionId", outer.get("adminSessionId") == op))
|
|
264
|
+
failed = [name for name, ok in conds if not ok]
|
|
265
|
+
print("yes" if not failed else "no:"+repr(failed)+"|turns="+repr(turns))
|
|
244
266
|
except Exception as e:
|
|
245
267
|
print("parse-fail:"+str(e))
|
|
246
268
|
' "$OP_ID" "$ACCT_ID" 2>/dev/null)
|
|
247
269
|
if [[ "$BODY_OK" == "yes" ]]; then
|
|
248
|
-
pass "case-5c
|
|
270
|
+
pass "case-5c envelope is { turns, conversationId, accountId, occurredAt }; turns is the 4-entry ordered list"
|
|
249
271
|
else
|
|
250
272
|
fail "case-5c envelope shape wrong ($BODY_OK): $RECORDER_BODY"
|
|
251
273
|
fi
|
|
@@ -258,51 +280,95 @@ else
|
|
|
258
280
|
fail "case-5d expected 1 spawn-request, got $SPAWN_REQ_COUNT (lines: $(ingest_lines))"
|
|
259
281
|
fi
|
|
260
282
|
|
|
261
|
-
# 5e.
|
|
283
|
+
# 5e. Task 177 observability — exactly ONE `[turn-recorder] envelope ...`
|
|
284
|
+
# line emitted before `spawn-request`. The line carries turnsCount,
|
|
285
|
+
# userTurns, assistantTurns, toolCallTurns.
|
|
286
|
+
ENVELOPE_LINE_COUNT=$(ingest_lines | grep -cE "^envelope sessionId=${OP_ID} turnsCount=4 userTurns=2 assistantTurns=2 toolCallTurns=0$" || true)
|
|
287
|
+
if [[ "$ENVELOPE_LINE_COUNT" -eq 1 ]]; then
|
|
288
|
+
pass "case-5e envelope log line emitted with correct counts"
|
|
289
|
+
else
|
|
290
|
+
fail "case-5e expected exactly 1 envelope line w/ turnsCount=4 userTurns=2 assistantTurns=2 toolCallTurns=0, got $ENVELOPE_LINE_COUNT (lines: $(ingest_lines))"
|
|
291
|
+
fi
|
|
292
|
+
|
|
293
|
+
# 5f. envelope line precedes spawn-request line.
|
|
294
|
+
ALL_LINES=$(ingest_lines)
|
|
295
|
+
ENV_POS=$(printf '%s\n' "$ALL_LINES" | grep -nE "^envelope sessionId=${OP_ID}" | head -1 | cut -d: -f1)
|
|
296
|
+
SPAWN_POS=$(printf '%s\n' "$ALL_LINES" | grep -nE "^spawn-request sessionId=${OP_ID}" | head -1 | cut -d: -f1)
|
|
297
|
+
if [[ -n "$ENV_POS" && -n "$SPAWN_POS" && "$ENV_POS" -lt "$SPAWN_POS" ]]; then
|
|
298
|
+
pass "case-5f envelope line precedes spawn-request"
|
|
299
|
+
else
|
|
300
|
+
fail "case-5f envelope line must precede spawn-request (env=$ENV_POS spawn=$SPAWN_POS)"
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
# 5g. no legacy stderr emissions
|
|
262
304
|
if echo "$HOOK_STDERR" | grep -qE 'spawn-with-input|spawn-with-stdin|turn-completed-graph-write fired'; then
|
|
263
|
-
fail "case-
|
|
305
|
+
fail "case-5g legacy stderr line emitted: $HOOK_STDERR"
|
|
264
306
|
else
|
|
265
|
-
pass "case-
|
|
307
|
+
pass "case-5g no legacy stderr emissions"
|
|
266
308
|
fi
|
|
267
309
|
|
|
268
|
-
# --- Case 6 (Task
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
310
|
+
# --- Case 6 (Task 177): multi-block assistant collapse -----------------
|
|
311
|
+
# Two assistant JSONL records sharing `message.id`: first is thinking-only,
|
|
312
|
+
# second carries the text. Walker collapses both into ONE `turns` entry
|
|
313
|
+
# whose text is the text-record's text. Thinking records contribute no
|
|
314
|
+
# text and do not establish the turn; `ts` is the first CONTRIBUTING
|
|
315
|
+
# record's ts (here, the text record's ts).
|
|
316
|
+
MULTI_TRANSCRIPT=$(mktemp); TMPFILES+=("$MULTI_TRANSCRIPT")
|
|
317
|
+
{
|
|
318
|
+
printf '{"type":"user","timestamp":"2026-05-19T21:30:00.000Z","message":{"role":"user","content":"think then answer"}}\n'
|
|
319
|
+
printf '{"type":"assistant","timestamp":"2026-05-19T21:30:01.000Z","message":{"id":"msg_multi","role":"assistant","content":[{"type":"thinking","thinking":"considering..."}]}}\n'
|
|
320
|
+
printf '{"type":"assistant","timestamp":"2026-05-19T21:30:02.000Z","message":{"id":"msg_multi","role":"assistant","content":[{"type":"text","text":"answer is 42"}]}}\n'
|
|
321
|
+
} > "$MULTI_TRANSCRIPT"
|
|
322
|
+
MULTI_ENVELOPE=$(python3 -c '
|
|
274
323
|
import json, sys
|
|
275
324
|
print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
|
|
276
|
-
' "$OP_ID" "$
|
|
325
|
+
' "$OP_ID" "$MULTI_TRANSCRIPT")
|
|
277
326
|
: > "$REQ_LOG"
|
|
278
|
-
run_hook "admin" "" "$
|
|
279
|
-
[[ "$HOOK_RC" -eq 0 ]] || fail "case-6 rc=$HOOK_RC"
|
|
280
|
-
|
|
281
|
-
|
|
327
|
+
run_hook "admin" "" "$MULTI_ENVELOPE"
|
|
328
|
+
[[ "$HOOK_RC" -eq 0 ]] || fail "case-6 rc=$HOOK_RC stderr=$HOOK_STDERR"
|
|
329
|
+
MULTI_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
|
|
330
|
+
MULTI_OK=$(printf '%s' "$MULTI_BODY" | python3 -c '
|
|
331
|
+
import sys, json
|
|
332
|
+
try:
|
|
333
|
+
outer = json.load(sys.stdin)
|
|
334
|
+
msg = json.loads(outer.get("initialMessage"))
|
|
335
|
+
turns = msg.get("turns") or []
|
|
336
|
+
conds = []
|
|
337
|
+
conds.append(("turns-len-2", len(turns) == 2))
|
|
338
|
+
if len(turns) >= 2:
|
|
339
|
+
conds.append(("t0-user", turns[0].get("role") == "user" and turns[0].get("text") == "think then answer"))
|
|
340
|
+
conds.append(("t1-assistant", turns[1].get("role") == "assistant"))
|
|
341
|
+
conds.append(("t1-text", turns[1].get("text") == "answer is 42"))
|
|
342
|
+
conds.append(("t1-ts-text-record", turns[1].get("ts") == "2026-05-19T21:30:02.000Z"))
|
|
343
|
+
conds.append(("t1-no-toolCalls", "toolCalls" not in turns[1]))
|
|
344
|
+
failed = [name for name, ok in conds if not ok]
|
|
345
|
+
print("yes" if not failed else "no:"+repr(failed)+"|turns="+repr(turns))
|
|
346
|
+
except Exception as e:
|
|
347
|
+
print("parse-fail:"+str(e))
|
|
348
|
+
' 2>/dev/null)
|
|
349
|
+
if [[ "$MULTI_OK" == "yes" ]]; then
|
|
350
|
+
pass "case-6 multi-block assistant collapses to one turn entry; ts is the first record's ts"
|
|
282
351
|
else
|
|
283
|
-
fail "case-6
|
|
284
|
-
fi
|
|
285
|
-
# And no recorder-spawn POST is made.
|
|
286
|
-
if grep -qE '^/api/admin/claude-sessions ' "$REQ_LOG"; then
|
|
287
|
-
fail "case-6 recorder-spawn must NOT be called when conversation-empty"
|
|
352
|
+
fail "case-6 multi-block collapse wrong ($MULTI_OK): $MULTI_BODY"
|
|
288
353
|
fi
|
|
289
354
|
|
|
290
|
-
# --- Case 7 (Task
|
|
291
|
-
#
|
|
292
|
-
#
|
|
355
|
+
# --- Case 7 (Task 177): tool-call pairing on owning assistant turn -----
|
|
356
|
+
# Assistant tool_use followed by user tool_result. Both attach as ONE
|
|
357
|
+
# `toolCalls` entry on the assistant turn that owned the tool_use.
|
|
358
|
+
# The tool_result-only user record does NOT create a separate user turn.
|
|
293
359
|
TOOL_TRANSCRIPT=$(mktemp); TMPFILES+=("$TOOL_TRANSCRIPT")
|
|
294
360
|
python3 - "$TOOL_TRANSCRIPT" <<'PY'
|
|
295
361
|
import json, sys
|
|
296
362
|
out = sys.argv[1]
|
|
297
363
|
recs = [
|
|
298
|
-
{"type":"user","message":{"role":"user","content":"please write a node"}},
|
|
299
|
-
{"type":"assistant","message":{"role":"assistant","content":[
|
|
364
|
+
{"type":"user","timestamp":"2026-05-19T21:32:00.000Z","message":{"role":"user","content":"please write a node"}},
|
|
365
|
+
{"type":"assistant","timestamp":"2026-05-19T21:32:01.000Z","message":{"id":"msg_tu","role":"assistant","content":[
|
|
300
366
|
{"type":"tool_use","id":"toolu_001","name":"mcp__memory__memory-write","input":{"nodeType":"LocalBusiness","name":"Smalleys","nested":{"k":1}}}
|
|
301
367
|
]}},
|
|
302
|
-
{"type":"user","message":{"role":"user","content":[
|
|
368
|
+
{"type":"user","timestamp":"2026-05-19T21:32:02.000Z","message":{"role":"user","content":[
|
|
303
369
|
{"type":"tool_result","tool_use_id":"toolu_001","content":"ok: wrote LocalBusiness"}
|
|
304
370
|
]}},
|
|
305
|
-
{"type":"assistant","message":{"role":"assistant","content":[
|
|
371
|
+
{"type":"assistant","timestamp":"2026-05-19T21:32:03.000Z","message":{"id":"msg_done","role":"assistant","content":[
|
|
306
372
|
{"type":"text","text":"done"}
|
|
307
373
|
]}},
|
|
308
374
|
]
|
|
@@ -322,29 +388,117 @@ import sys, json
|
|
|
322
388
|
try:
|
|
323
389
|
outer = json.load(sys.stdin)
|
|
324
390
|
msg = json.loads(outer.get("initialMessage"))
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
conds
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
391
|
+
turns = msg.get("turns") or []
|
|
392
|
+
conds = []
|
|
393
|
+
# Expected 3 turns: user "please write a node", assistant tool_use (text=""),
|
|
394
|
+
# assistant "done". The user tool_result record carries no separate user turn.
|
|
395
|
+
conds.append(("turns-len-3", len(turns) == 3))
|
|
396
|
+
if len(turns) >= 3:
|
|
397
|
+
conds.append(("t0", turns[0].get("role") == "user" and turns[0].get("text") == "please write a node"))
|
|
398
|
+
conds.append(("t1-role", turns[1].get("role") == "assistant"))
|
|
399
|
+
conds.append(("t1-text-empty", turns[1].get("text") == ""))
|
|
400
|
+
conds.append(("t1-toolCalls-len", len(turns[1].get("toolCalls") or []) == 1))
|
|
401
|
+
tc = (turns[1].get("toolCalls") or [{}])[0]
|
|
402
|
+
conds.append(("tc-tool", tc.get("tool") == "mcp__memory__memory-write"))
|
|
403
|
+
conds.append(("tc-input", tc.get("input") == {"nodeType":"LocalBusiness","name":"Smalleys","nested":{"k":1}}))
|
|
404
|
+
conds.append(("tc-output", tc.get("output") == "ok: wrote LocalBusiness"))
|
|
405
|
+
conds.append(("t2", turns[2].get("role") == "assistant" and turns[2].get("text") == "done"))
|
|
406
|
+
conds.append(("t2-no-toolCalls", "toolCalls" not in turns[2]))
|
|
407
|
+
failed = [name for name, ok in conds if not ok]
|
|
408
|
+
print("yes" if not failed else "no:"+repr(failed)+"|turns="+repr(turns))
|
|
337
409
|
except Exception as e:
|
|
338
410
|
print("parse-fail:"+str(e))
|
|
339
411
|
' 2>/dev/null)
|
|
340
412
|
if [[ "$TOOL_OK" == "yes" ]]; then
|
|
341
|
-
pass "case-7 toolCalls
|
|
413
|
+
pass "case-7 tool_use + tool_result land as one toolCalls entry on owning assistant turn; no extra user turn"
|
|
342
414
|
else
|
|
343
|
-
fail "case-7
|
|
415
|
+
fail "case-7 tool pairing wrong ($TOOL_OK): $TOOL_BODY"
|
|
344
416
|
fi
|
|
345
417
|
|
|
346
|
-
#
|
|
347
|
-
|
|
418
|
+
# Envelope log line for case-7: turnsCount=3, userTurns=1, assistantTurns=2, toolCallTurns=1
|
|
419
|
+
ENV_LINE_7=$(ingest_lines | grep -cE "^envelope sessionId=${OP_ID} turnsCount=3 userTurns=1 assistantTurns=2 toolCallTurns=1$" || true)
|
|
420
|
+
if [[ "$ENV_LINE_7" -eq 1 ]]; then
|
|
421
|
+
pass "case-7 envelope log line counts tool-call turn correctly"
|
|
422
|
+
else
|
|
423
|
+
fail "case-7 expected envelope line w/ turnsCount=3 userTurns=1 assistantTurns=2 toolCallTurns=1, got $ENV_LINE_7 (lines: $(ingest_lines))"
|
|
424
|
+
fi
|
|
425
|
+
|
|
426
|
+
# --- Case 8 (Task 177): long-conversation 100 turns ------------------
|
|
427
|
+
# Transcript with 100 turns (50 user + 50 assistant, interleaved) →
|
|
428
|
+
# turns.length === 100. No drop, no truncation, no env-knob override.
|
|
429
|
+
LONG_TRANSCRIPT=$(mktemp); TMPFILES+=("$LONG_TRANSCRIPT")
|
|
430
|
+
python3 - "$LONG_TRANSCRIPT" <<'PY'
|
|
431
|
+
import json, sys
|
|
432
|
+
out = sys.argv[1]
|
|
433
|
+
with open(out,"w") as f:
|
|
434
|
+
for i in range(50):
|
|
435
|
+
ts_u = f"2026-05-19T21:{(i*2)//60:02d}:{(i*2)%60:02d}.000Z"
|
|
436
|
+
ts_a = f"2026-05-19T21:{(i*2+1)//60:02d}:{(i*2+1)%60:02d}.000Z"
|
|
437
|
+
f.write(json.dumps({"type":"user","timestamp":ts_u,"message":{"role":"user","content":f"user msg {i}"}})+"\n")
|
|
438
|
+
f.write(json.dumps({"type":"assistant","timestamp":ts_a,"message":{"id":f"msg_{i}","role":"assistant","content":[{"type":"text","text":f"asst reply {i}"}]}})+"\n")
|
|
439
|
+
PY
|
|
440
|
+
LONG_ENVELOPE=$(python3 -c '
|
|
441
|
+
import json, sys
|
|
442
|
+
print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
|
|
443
|
+
' "$OP_ID" "$LONG_TRANSCRIPT")
|
|
444
|
+
: > "$REQ_LOG"
|
|
445
|
+
run_hook "admin" "" "$LONG_ENVELOPE"
|
|
446
|
+
[[ "$HOOK_RC" -eq 0 ]] || fail "case-8 rc=$HOOK_RC stderr=$HOOK_STDERR"
|
|
447
|
+
LONG_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
|
|
448
|
+
LONG_OK=$(printf '%s' "$LONG_BODY" | python3 -c '
|
|
449
|
+
import sys, json
|
|
450
|
+
try:
|
|
451
|
+
outer = json.load(sys.stdin)
|
|
452
|
+
msg = json.loads(outer.get("initialMessage"))
|
|
453
|
+
turns = msg.get("turns") or []
|
|
454
|
+
conds = []
|
|
455
|
+
conds.append(("turns-len-100", len(turns) == 100))
|
|
456
|
+
if len(turns) == 100:
|
|
457
|
+
conds.append(("first-user-0", turns[0].get("role") == "user" and turns[0].get("text") == "user msg 0"))
|
|
458
|
+
conds.append(("last-asst-49", turns[99].get("role") == "assistant" and turns[99].get("text") == "asst reply 49"))
|
|
459
|
+
# ts strictly non-decreasing
|
|
460
|
+
tss = [t.get("ts","") for t in turns]
|
|
461
|
+
conds.append(("ts-ordered", tss == sorted(tss)))
|
|
462
|
+
failed = [name for name, ok in conds if not ok]
|
|
463
|
+
print("yes" if not failed else "no:"+repr(failed)+"|len="+str(len(turns)))
|
|
464
|
+
except Exception as e:
|
|
465
|
+
print("parse-fail:"+str(e))
|
|
466
|
+
' 2>/dev/null)
|
|
467
|
+
if [[ "$LONG_OK" == "yes" ]]; then
|
|
468
|
+
pass "case-8 100-turn transcript → turns.length === 100, oldest first, no truncation"
|
|
469
|
+
else
|
|
470
|
+
fail "case-8 long-conversation wrong ($LONG_OK)"
|
|
471
|
+
fi
|
|
472
|
+
|
|
473
|
+
# --- Case 9 (Task 177): empty-skip ------------------------------------
|
|
474
|
+
# Transcript with no text content and no tool_use → turns array is empty
|
|
475
|
+
# → trigger-skipped reason=conversation-empty, no spawn POST.
|
|
476
|
+
EMPTY_TRANSCRIPT=$(mktemp); TMPFILES+=("$EMPTY_TRANSCRIPT")
|
|
477
|
+
{
|
|
478
|
+
printf '{"type":"system","summary":"noop"}\n'
|
|
479
|
+
printf '{"type":"assistant","timestamp":"2026-05-19T21:40:00.000Z","message":{"id":"msg_t","role":"assistant","content":[{"type":"thinking","thinking":"silent"}]}}\n'
|
|
480
|
+
} > "$EMPTY_TRANSCRIPT"
|
|
481
|
+
EMPTY_ENVELOPE=$(python3 -c '
|
|
482
|
+
import json, sys
|
|
483
|
+
print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
|
|
484
|
+
' "$OP_ID" "$EMPTY_TRANSCRIPT")
|
|
485
|
+
: > "$REQ_LOG"
|
|
486
|
+
run_hook "admin" "" "$EMPTY_ENVELOPE"
|
|
487
|
+
[[ "$HOOK_RC" -eq 0 ]] || fail "case-9 rc=$HOOK_RC"
|
|
488
|
+
if ingest_lines | grep -qE "^trigger-skipped sessionId=${OP_ID} reason=conversation-empty$"; then
|
|
489
|
+
pass "case-9 thinking-only transcript → trigger-skipped reason=conversation-empty"
|
|
490
|
+
else
|
|
491
|
+
fail "case-9 expected trigger-skipped conversation-empty, got: $(ingest_lines)"
|
|
492
|
+
fi
|
|
493
|
+
if grep -qE '^/api/admin/claude-sessions ' "$REQ_LOG"; then
|
|
494
|
+
fail "case-9 recorder-spawn must NOT be called when conversation-empty"
|
|
495
|
+
else
|
|
496
|
+
pass "case-9 no recorder-spawn POST"
|
|
497
|
+
fi
|
|
498
|
+
|
|
499
|
+
# --- Case 10 (Task 177): escaping round-trip --------------------------
|
|
500
|
+
# Quotes, backslashes, newlines, non-ASCII all round-trip cleanly through
|
|
501
|
+
# the JSON envelope's `turns[*].text` field.
|
|
348
502
|
ESC_TRANSCRIPT=$(mktemp); TMPFILES+=("$ESC_TRANSCRIPT")
|
|
349
503
|
python3 - "$ESC_TRANSCRIPT" <<'PY'
|
|
350
504
|
import json, sys
|
|
@@ -352,8 +506,8 @@ out = sys.argv[1]
|
|
|
352
506
|
op_msg = 'hello "world"\\nline2\nlast — café 🦊'
|
|
353
507
|
asst_msg = 'reply with backslash \\\\ and newline\nhere'
|
|
354
508
|
recs = [
|
|
355
|
-
{"type":"user","message":{"role":"user","content":op_msg}},
|
|
356
|
-
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":asst_msg}]}},
|
|
509
|
+
{"type":"user","timestamp":"2026-05-19T21:50:00.000Z","message":{"role":"user","content":op_msg}},
|
|
510
|
+
{"type":"assistant","timestamp":"2026-05-19T21:50:01.000Z","message":{"id":"msg_esc","role":"assistant","content":[{"type":"text","text":asst_msg}]}},
|
|
357
511
|
]
|
|
358
512
|
with open(out,"w") as f:
|
|
359
513
|
for r in recs: f.write(json.dumps(r)+"\n")
|
|
@@ -364,7 +518,7 @@ print(json.dumps({"session_id": sys.argv[1], "transcript_path": sys.argv[2]}))
|
|
|
364
518
|
' "$OP_ID" "$ESC_TRANSCRIPT")
|
|
365
519
|
: > "$REQ_LOG"
|
|
366
520
|
run_hook "admin" "" "$ESC_ENVELOPE"
|
|
367
|
-
[[ "$HOOK_RC" -eq 0 ]] || fail "case-
|
|
521
|
+
[[ "$HOOK_RC" -eq 0 ]] || fail "case-10 rc=$HOOK_RC stderr=$HOOK_STDERR"
|
|
368
522
|
ESC_BODY=$(grep -E '^/api/admin/claude-sessions ' "$REQ_LOG" | head -1 | cut -f2-)
|
|
369
523
|
ESC_OK=$(printf '%s' "$ESC_BODY" | python3 -c '
|
|
370
524
|
import sys, json
|
|
@@ -373,18 +527,21 @@ EXPECTED_ASST = "reply with backslash \\\\ and newline\nhere"
|
|
|
373
527
|
try:
|
|
374
528
|
outer = json.load(sys.stdin)
|
|
375
529
|
msg = json.loads(outer.get("initialMessage"))
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
)
|
|
380
|
-
|
|
530
|
+
turns = msg.get("turns") or []
|
|
531
|
+
conds = []
|
|
532
|
+
conds.append(("len-2", len(turns) == 2))
|
|
533
|
+
if len(turns) == 2:
|
|
534
|
+
conds.append(("op-text", turns[0].get("text") == EXPECTED_OP))
|
|
535
|
+
conds.append(("asst-text", turns[1].get("text") == EXPECTED_ASST))
|
|
536
|
+
failed = [name for name, ok in conds if not ok]
|
|
537
|
+
print("yes" if not failed else "no:"+repr(failed)+"|turns="+repr(turns))
|
|
381
538
|
except Exception as e:
|
|
382
539
|
print("parse-fail:"+str(e))
|
|
383
540
|
' 2>/dev/null)
|
|
384
541
|
if [[ "$ESC_OK" == "yes" ]]; then
|
|
385
|
-
pass "case-
|
|
542
|
+
pass "case-10 escaping: quotes/backslashes/newlines/non-ASCII round-trip cleanly through turns[*].text"
|
|
386
543
|
else
|
|
387
|
-
fail "case-
|
|
544
|
+
fail "case-10 escaping wrong ($ESC_OK): $ESC_BODY"
|
|
388
545
|
fi
|
|
389
546
|
|
|
390
547
|
# --- Summary ------------------------------------------------------------
|