prizmkit 1.1.145 → 1.1.146

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.
@@ -994,6 +994,328 @@ def test_progress_parser_does_not_mark_failed_pi_agent_end_as_success():
994
994
  assert "provider request failed" in data["errors"]
995
995
 
996
996
 
997
+ def test_progress_parser_recovers_from_retrying_pi_attempt_before_final_success():
998
+ module = _load_progress_parser_module()
999
+ tracker = module.ProgressTracker()
1000
+ tracker.process_event({"type": "session", "version": 3, "id": "pi-session", "cwd": "/tmp/project"})
1001
+ tracker.process_event({"type": "agent_start"})
1002
+ tracker.process_event({
1003
+ "type": "message_end",
1004
+ "message": {
1005
+ "role": "assistant",
1006
+ "content": [],
1007
+ "stopReason": "error",
1008
+ "errorMessage": "Error Code internal_server_error: unexpected EOF",
1009
+ },
1010
+ })
1011
+ tracker.process_event({"type": "agent_end", "willRetry": True, "messages": []})
1012
+ tracker.process_event({
1013
+ "type": "auto_retry_start",
1014
+ "attempt": 1,
1015
+ "delayMs": 2000,
1016
+ "errorMessage": "Error Code internal_server_error: unexpected EOF",
1017
+ })
1018
+ tracker.process_event({"type": "agent_start"})
1019
+ tracker.process_event({"type": "auto_retry_end", "attempt": 1})
1020
+ tracker.process_event({
1021
+ "type": "message_end",
1022
+ "message": {
1023
+ "role": "assistant",
1024
+ "content": [{"type": "text", "text": "WORKFLOW_COMPLETED"}],
1025
+ "stopReason": "stop",
1026
+ },
1027
+ })
1028
+ tracker.process_event({
1029
+ "type": "agent_end",
1030
+ "willRetry": False,
1031
+ "messages": [{
1032
+ "role": "assistant",
1033
+ "content": [{"type": "text", "text": "WORKFLOW_COMPLETED"}],
1034
+ "stopReason": "stop",
1035
+ }],
1036
+ })
1037
+
1038
+ data = tracker.to_dict()
1039
+
1040
+ assert data["result_subtype"] == "success"
1041
+ assert data["last_result_is_error"] is False
1042
+ assert data["stop_reason"] == "stop"
1043
+ assert data["fatal_error_code"] == ""
1044
+ assert data["terminal_success_at"]
1045
+ assert any("unexpected EOF" in str(error) for error in data["errors"])
1046
+
1047
+
1048
+ def test_progress_parser_keeps_final_error_after_internal_retry_exhausts():
1049
+ module = _load_progress_parser_module()
1050
+ tracker = module.ProgressTracker()
1051
+ tracker.process_event({"type": "session", "id": "pi-session"})
1052
+ tracker.process_event({"type": "agent_start"})
1053
+ tracker.process_event({
1054
+ "type": "message_end",
1055
+ "message": {"role": "assistant", "content": [], "stopReason": "error", "errorMessage": "temporary error"},
1056
+ })
1057
+ tracker.process_event({"type": "agent_end", "willRetry": True, "messages": []})
1058
+ tracker.process_event({"type": "auto_retry_start", "attempt": 1, "errorMessage": "temporary error"})
1059
+ tracker.process_event({"type": "agent_start"})
1060
+ tracker.process_event({
1061
+ "type": "message_end",
1062
+ "message": {"role": "assistant", "content": [], "stopReason": "error", "errorMessage": "final error"},
1063
+ })
1064
+ tracker.process_event({
1065
+ "type": "agent_end",
1066
+ "willRetry": False,
1067
+ "messages": [{"role": "assistant", "content": [], "stopReason": "error", "errorMessage": "final error"}],
1068
+ })
1069
+
1070
+ data = tracker.to_dict()
1071
+
1072
+ assert data["result_subtype"] == "error"
1073
+ assert data["last_result_is_error"] is True
1074
+ assert data["stop_reason"] == "error"
1075
+ assert data["terminal_success_at"] == ""
1076
+ assert "final error" in data["terminal_result_text"]
1077
+
1078
+
1079
+ def test_progress_parser_clears_retrying_fatal_marker_on_new_pi_attempt():
1080
+ module = _load_progress_parser_module()
1081
+ tracker = module.ProgressTracker()
1082
+ tracker.process_event({"type": "session", "id": "pi-session"})
1083
+ tracker.process_event({"type": "agent_start"})
1084
+ tracker.process_event({
1085
+ "type": "message_end",
1086
+ "message": {
1087
+ "role": "assistant",
1088
+ "content": [],
1089
+ "stopReason": "error",
1090
+ "errorMessage": "API Error: Your input exceeds the context window",
1091
+ },
1092
+ })
1093
+ tracker.process_event({"type": "agent_end", "willRetry": True, "messages": []})
1094
+ failed_attempt = tracker.to_dict()
1095
+
1096
+ tracker.process_event({"type": "agent_start"})
1097
+ next_attempt = tracker.to_dict()
1098
+
1099
+ assert failed_attempt["fatal_error_code"] == "context_overflow"
1100
+ assert failed_attempt["last_result_is_error"] is True
1101
+ assert next_attempt["fatal_error_code"] == ""
1102
+ assert next_attempt["result_subtype"] == ""
1103
+ assert next_attempt["last_result_is_error"] is False
1104
+ assert any("context window" in str(error) for error in next_attempt["errors"])
1105
+ assert next_attempt["is_active"] is True
1106
+
1107
+
1108
+ def test_semantic_log_normalizer_compacts_pi_cumulative_stream():
1109
+ module = _load_progress_parser_module()
1110
+ normalizer = module.SemanticLogNormalizer()
1111
+ events = [
1112
+ {"type": "session", "id": "session-1"},
1113
+ {"type": "message_start", "message": {"role": "user"}},
1114
+ {"type": "message_end", "message": {"role": "user", "content": [{"type": "text", "text": "SECRET PROMPT"}]}},
1115
+ {"type": "turn_start"},
1116
+ {"type": "message_update", "assistantMessageEvent": {"type": "thinking_start", "contentIndex": 0}},
1117
+ {"type": "message_update", "assistantMessageEvent": {"type": "thinking_delta", "contentIndex": 0, "delta": "Inspecting"}},
1118
+ {"type": "message_update", "assistantMessageEvent": {"type": "thinking_delta", "contentIndex": 0, "delta": " state"}},
1119
+ {"type": "message_update", "assistantMessageEvent": {"type": "thinking_end", "contentIndex": 0, "content": "Inspecting state"}},
1120
+ {"type": "tool_execution_start", "toolCallId": "call-1", "toolName": "bash", "args": {"command": "printf ABC"}},
1121
+ {"type": "tool_execution_update", "toolCallId": "call-1", "toolName": "bash", "partialResult": {"content": [{"type": "text", "text": "A"}]}},
1122
+ {"type": "tool_execution_update", "toolCallId": "call-1", "toolName": "bash", "partialResult": {"content": [{"type": "text", "text": "AB"}]}},
1123
+ {"type": "tool_execution_update", "toolCallId": "call-1", "toolName": "bash", "partialResult": {"content": [{"type": "text", "text": "ABC"}]}},
1124
+ {"type": "tool_execution_end", "toolCallId": "call-1", "toolName": "bash", "result": {"content": [{"type": "text", "text": "ABC"}]}, "isError": False},
1125
+ {"type": "message_start", "message": {"role": "toolResult"}},
1126
+ {"type": "message_end", "message": {"role": "toolResult", "toolCallId": "call-1", "toolName": "bash", "content": [{"type": "text", "text": "ABC"}]}},
1127
+ {"type": "message_update", "assistantMessageEvent": {"type": "text_start", "contentIndex": 1}},
1128
+ {"type": "message_update", "assistantMessageEvent": {"type": "text_delta", "contentIndex": 1, "delta": "Done"}},
1129
+ {"type": "message_update", "assistantMessageEvent": {"type": "text_end", "contentIndex": 1, "content": "Done"}},
1130
+ {"type": "message_end", "message": {"role": "assistant", "content": [{"type": "text", "text": "Done"}], "stopReason": "stop"}},
1131
+ {"type": "turn_end", "message": {"role": "assistant", "content": [{"type": "text", "text": "Done"}], "stopReason": "stop"}},
1132
+ {"type": "agent_end", "willRetry": False, "messages": [{"role": "assistant", "content": [{"type": "text", "text": "Done"}], "stopReason": "stop"}]},
1133
+ {"type": "agent_settled"},
1134
+ ]
1135
+
1136
+ rendered = "".join(normalizer.process_event(event) for event in events) + normalizer.finish()
1137
+
1138
+ assert rendered.count("[THINK]") == 1
1139
+ assert "[THINK] Inspecting state\n" in rendered
1140
+ assert rendered.count("[ACTION]") == 1
1141
+ assert "bash" in rendered and "printf ABC" in rendered
1142
+ assert rendered.count("[OBSERVATION]") == 1
1143
+ assert "[OBSERVATION] ABC\n" in rendered
1144
+ assert rendered.count("[FINAL]") == 1
1145
+ assert "[FINAL] Done\n" in rendered
1146
+ assert rendered.count("[SESSION_END]") == 1
1147
+ assert "message_update" not in rendered
1148
+ assert "SECRET PROMPT" not in rendered
1149
+
1150
+
1151
+ def test_semantic_log_normalizer_ignores_user_prompt_but_keeps_nested_tool_results():
1152
+ module = _load_progress_parser_module()
1153
+ normalizer = module.SemanticLogNormalizer()
1154
+ rendered = normalizer.process_event({
1155
+ "type": "user",
1156
+ "message": {
1157
+ "content": [
1158
+ {"type": "text", "text": "SECRET PROMPT"},
1159
+ {"type": "tool_result", "tool_use_id": "tool-1", "content": "tool output"},
1160
+ ]
1161
+ },
1162
+ })
1163
+ rendered += normalizer.finish()
1164
+
1165
+ assert "SECRET PROMPT" not in rendered
1166
+ assert "[OBSERVATION] tool output" in rendered
1167
+
1168
+
1169
+ def test_semantic_log_normalizer_preserves_repeated_plain_and_tool_output_records():
1170
+ module = _load_progress_parser_module()
1171
+ normalizer = module.SemanticLogNormalizer()
1172
+ rendered = normalizer.process_text("same\n")
1173
+ rendered += normalizer.process_text("same\n")
1174
+ rendered += normalizer.process_event({"type": "function_call_result", "id": "one", "output": "same result"})
1175
+ rendered += normalizer.process_event({"type": "function_call_result", "id": "two", "output": "same result"})
1176
+ rendered += normalizer.finish()
1177
+
1178
+ assert rendered.count("[OUTPUT] same") == 2
1179
+ assert rendered.count("[OBSERVATION] same result") == 2
1180
+
1181
+
1182
+ def test_semantic_log_normalizer_emits_terminal_metadata_once():
1183
+ module = _load_progress_parser_module()
1184
+ normalizer = module.SemanticLogNormalizer()
1185
+ rendered = normalizer.process_event({"type": "turn.completed"})
1186
+ rendered += normalizer.process_event({"type": "result", "subtype": "success", "result": "Done"})
1187
+ rendered += normalizer.finish()
1188
+
1189
+ assert rendered.count("[SESSION_END]") == 1
1190
+ assert "[FINAL] Done" in rendered
1191
+
1192
+
1193
+ def test_semantic_log_normalizer_preserves_identical_text_from_distinct_turns():
1194
+ module = _load_progress_parser_module()
1195
+ normalizer = module.SemanticLogNormalizer()
1196
+ events = [
1197
+ {"type": "turn_start"},
1198
+ {"type": "message_end", "message": {"role": "assistant", "content": [{"type": "text", "text": "Done"}], "stopReason": "stop"}},
1199
+ {"type": "turn_start"},
1200
+ {"type": "message_end", "message": {"role": "assistant", "content": [{"type": "text", "text": "Done"}], "stopReason": "stop"}},
1201
+ ]
1202
+
1203
+ rendered = "".join(normalizer.process_event(event) for event in events) + normalizer.finish()
1204
+
1205
+ assert rendered.count("[FINAL] Done") == 2
1206
+
1207
+
1208
+ def test_semantic_log_normalizer_size_tracks_unique_cumulative_content():
1209
+ module = _load_progress_parser_module()
1210
+ normalizer = module.SemanticLogNormalizer()
1211
+ chunks = []
1212
+ payload = ""
1213
+ chunks.append(normalizer.process_event({"type": "tool_execution_start", "toolCallId": "call-1", "toolName": "read", "args": {"path": "sample"}}))
1214
+ for index in range(1, 501):
1215
+ payload += chr(64 + ((index - 1) % 26) + 1)
1216
+ chunks.append(normalizer.process_event({
1217
+ "type": "tool_execution_update",
1218
+ "toolCallId": "call-1",
1219
+ "toolName": "read",
1220
+ "partialResult": {"content": [{"type": "text", "text": payload}]},
1221
+ }))
1222
+ assert sum(len(value) for value in normalizer._snapshots.values()) == len(payload)
1223
+ assert not any(stream_id.startswith("tool:call-1:output") for stream_id in normalizer._stream_buffers)
1224
+ chunks.append(normalizer.process_event({
1225
+ "type": "tool_execution_end",
1226
+ "toolCallId": "call-1",
1227
+ "toolName": "read",
1228
+ "result": {"content": [{"type": "text", "text": payload}]},
1229
+ "isError": False,
1230
+ }))
1231
+ rendered = "".join(chunks) + normalizer.finish()
1232
+
1233
+ assert rendered.count(payload) == 1
1234
+ assert len(rendered.encode("utf-8")) <= len(payload.encode("utf-8")) + 256
1235
+
1236
+
1237
+ @pytest.mark.parametrize(
1238
+ ("events", "expected_labels"),
1239
+ [
1240
+ (
1241
+ [
1242
+ {"type": "assistant", "message": {"content": [{"type": "text", "text": "Answer"}, {"type": "tool_use", "id": "tool-1", "name": "Read", "input": {"path": "a"}}]}},
1243
+ {"type": "user", "message": {"content": [{"type": "tool_result", "tool_use_id": "tool-1", "content": "result"}]}},
1244
+ {"type": "result", "subtype": "success", "stop_reason": "end_turn", "result": "Answer"},
1245
+ ],
1246
+ {"[FINAL]", "[ACTION]", "[OBSERVATION]", "[SESSION_END]"},
1247
+ ),
1248
+ (
1249
+ [
1250
+ {"type": "message", "sessionId": "cb-1", "role": "assistant", "content": [{"type": "text", "text": "Answer"}]},
1251
+ {"type": "function_call", "sessionId": "cb-1", "id": "call-1", "name": "read", "arguments": "{\"path\":\"a\"}"},
1252
+ {"type": "function_call_result", "sessionId": "cb-1", "id": "call-1", "output": "result"},
1253
+ {"type": "result", "subtype": "success", "result": "Answer"},
1254
+ ],
1255
+ {"[FINAL]", "[ACTION]", "[OBSERVATION]", "[SESSION_END]"},
1256
+ ),
1257
+ (
1258
+ [
1259
+ {"type": "turn.started"},
1260
+ {"type": "item.completed", "item": {"id": "reason-1", "type": "reasoning", "text": "Think"}},
1261
+ {"type": "item.started", "item": {"id": "cmd-1", "type": "command_execution", "command": "printf result"}},
1262
+ {"type": "item.completed", "item": {"id": "cmd-1", "type": "command_execution", "command": "printf result", "aggregated_output": "result", "status": "completed"}},
1263
+ {"type": "item.completed", "item": {"id": "answer-1", "type": "agent_message", "text": "Answer"}},
1264
+ {"type": "turn.completed", "usage": {"input_tokens": 1}},
1265
+ ],
1266
+ {"[THINK]", "[FINAL]", "[ACTION]", "[OBSERVATION]", "[SESSION_END]"},
1267
+ ),
1268
+ ],
1269
+ )
1270
+ def test_semantic_log_normalizer_has_cross_format_contract(events, expected_labels):
1271
+ module = _load_progress_parser_module()
1272
+ normalizer = module.SemanticLogNormalizer()
1273
+
1274
+ rendered = "".join(normalizer.process_event(event) for event in events) + normalizer.finish()
1275
+
1276
+ for label in expected_labels:
1277
+ assert label in rendered
1278
+ assert not any(json.dumps(event, ensure_ascii=False) in rendered for event in events)
1279
+
1280
+
1281
+ def test_live_smoke_harness_reports_capability_skips_without_running(tmp_path, monkeypatch, capsys):
1282
+ import importlib.util
1283
+
1284
+ script = PIPELINE_ROOT / "tests" / "live_ai_cli_log_smoke.py"
1285
+ spec = importlib.util.spec_from_file_location("live_ai_cli_log_smoke_test", script)
1286
+ module = importlib.util.module_from_spec(spec)
1287
+ assert spec.loader is not None
1288
+ sys.modules[spec.name] = module
1289
+ try:
1290
+ spec.loader.exec_module(module)
1291
+ finally:
1292
+ sys.modules.pop(spec.name, None)
1293
+
1294
+ fake_command = tmp_path / "fake-cli"
1295
+ fake_command.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
1296
+ fake_command.chmod(0o755)
1297
+ profile = module.SmokeProfile("example", str(fake_command), "custom")
1298
+ monkeypatch.setattr(module, "discover_profiles", lambda: ([profile], [module.SmokeResult("missing", "skipped", "executable not found")]))
1299
+ monkeypatch.delenv("PRIZMKIT_RUN_LIVE_AI_SMOKE", raising=False)
1300
+
1301
+ code = module.main([])
1302
+ payload = json.loads(capsys.readouterr().out)
1303
+
1304
+ assert code == 0
1305
+ assert {item["name"]: item["status"] for item in payload["results"]} == {
1306
+ "example": "skipped",
1307
+ "missing": "skipped",
1308
+ }
1309
+ reasons = {item["name"]: item["reason"] for item in payload["results"]}
1310
+ assert "not authorized" in reasons["example"]
1311
+ assert "executable not found" in reasons["missing"]
1312
+ assert "Do not read, create, edit, or delete project files" in module.smoke_prompt()
1313
+ assert module._looks_capability_blocked("login required") is True
1314
+ assert module._looks_capability_blocked("network is unreachable") is True
1315
+ assert module._looks_capability_blocked("model not found") is True
1316
+ assert module._looks_capability_blocked("ordinary tool failure") is False
1317
+
1318
+
997
1319
  def test_progress_parser_normalizes_claude_subagents_without_double_counting(tmp_path):
998
1320
  module = _load_progress_parser_module()
999
1321
  tracker = module.ProgressTracker()
@@ -1090,10 +1412,14 @@ def test_progress_parser_normalizes_codex_collab_metadata_with_missing_fields():
1090
1412
  },
1091
1413
  })
1092
1414
  tracker.process_event({"type": "item.completed", "item": {"type": "collab_tool_call", "tool": "delegate"}})
1415
+ tracker.process_event({"type": "turn.completed", "usage": {"input_tokens": 1}})
1093
1416
 
1094
1417
  data = tracker.to_dict()
1095
1418
 
1096
1419
  assert data["event_format"] == "codex-json"
1420
+ assert data["result_subtype"] == "success"
1421
+ assert data["stop_reason"] == "completed"
1422
+ assert data["is_active"] is False
1097
1423
  assert data["child_thread_ids"] == ["thread-a", "thread-b"]
1098
1424
  assert data["subagent_spawn_count"] == 2
1099
1425
  assert data["active_subagent_count"] == 1
@@ -1184,6 +1510,135 @@ def test_session_dual_writes_progress_and_recovers_backup(tmp_path):
1184
1510
  assert second_log.read_text(encoding="utf-8") == "larger recovered log"
1185
1511
 
1186
1512
 
1513
+ def test_session_reader_does_not_retain_or_reread_raw_history():
1514
+ import inspect
1515
+ import prizmkit_runtime.sessions as sessions
1516
+
1517
+ reader_source = inspect.getsource(sessions._reader_thread)
1518
+ run_source = inspect.getsource(sessions.AISessionLauncher.run)
1519
+
1520
+ assert "lines.append" not in reader_source
1521
+ assert "output_queue" not in reader_source
1522
+ assert "session_log.read_text" not in run_source
1523
+ assert "splitlines()" not in run_source
1524
+ assert "maxsize=1" in run_source
1525
+
1526
+
1527
+ def test_session_reader_contains_parser_failures_and_continues_draining(tmp_path, monkeypatch):
1528
+ import io
1529
+ import queue
1530
+ import prizmkit_runtime.sessions as sessions
1531
+
1532
+ class FailingTracker:
1533
+ def __init__(self, _session_log):
1534
+ pass
1535
+
1536
+ def process_event(self, _event):
1537
+ raise ValueError("unsupported shape")
1538
+
1539
+ def to_dict(self):
1540
+ return {"event_type_counts": {}, "errors": []}
1541
+
1542
+ class FailingNormalizer:
1543
+ def process_event(self, _event):
1544
+ raise ValueError("unsupported shape")
1545
+
1546
+ def process_text(self, text):
1547
+ return f"[OUTPUT] {text.strip()}\n"
1548
+
1549
+ def finish(self):
1550
+ return "[SESSION_END] status=stream_closed\n"
1551
+
1552
+ monkeypatch.setattr(sessions, "_load_stream_parser_classes", lambda: (FailingTracker, FailingNormalizer))
1553
+ session_log = tmp_path / "session.log"
1554
+ backup_log = tmp_path / "backup.log"
1555
+ session_log.write_text("", encoding="utf-8")
1556
+ backup_log.write_text("", encoding="utf-8")
1557
+ result_queue = queue.Queue(maxsize=1)
1558
+ pipe = io.StringIO('{"type":"result","subtype":"success","result":"done"}\nplain\n')
1559
+
1560
+ sessions._reader_thread(pipe, session_log, backup_log, result_queue)
1561
+
1562
+ text = session_log.read_text(encoding="utf-8")
1563
+ assert "[ERROR] progress parser rejected event=result: ValueError" in text
1564
+ assert "[ERROR] semantic log adapter rejected event=result: ValueError" in text
1565
+ assert "[OUTPUT] done" in text
1566
+ assert "[OUTPUT] plain" in text
1567
+ assert "[SESSION_END] status=stream_closed" in text
1568
+ assert backup_log.read_text(encoding="utf-8") == text
1569
+ assert result_queue.get_nowait()["event_type_counts"] == {}
1570
+
1571
+
1572
+ def test_session_persists_normalized_stream_without_raw_history(tmp_path, monkeypatch):
1573
+ import prizmkit_runtime.sessions as sessions
1574
+
1575
+ fake_cli = tmp_path / "pi_like_cli.py"
1576
+ fake_cli.write_text(
1577
+ "#!/usr/bin/env python3\n"
1578
+ "import json, sys\n"
1579
+ "_prompt = sys.stdin.read()\n"
1580
+ "events = [\n"
1581
+ " {'type':'session','id':'s1'},\n"
1582
+ " {'type':'agent_start'},\n"
1583
+ " {'type':'turn_start'},\n"
1584
+ " {'type':'message_update','assistantMessageEvent':{'type':'thinking_start','contentIndex':0}},\n"
1585
+ " {'type':'message_update','assistantMessageEvent':{'type':'thinking_delta','contentIndex':0,'delta':'Think'}},\n"
1586
+ " {'type':'message_update','assistantMessageEvent':{'type':'thinking_end','contentIndex':0,'content':'Think'}},\n"
1587
+ " {'type':'tool_execution_start','toolCallId':'c1','toolName':'bash','args':{'command':'printf ABC'}},\n"
1588
+ " {'type':'tool_execution_update','toolCallId':'c1','toolName':'bash','partialResult':{'content':[{'type':'text','text':'A'}]}},\n"
1589
+ " {'type':'tool_execution_update','toolCallId':'c1','toolName':'bash','partialResult':{'content':[{'type':'text','text':'ABC'}]}},\n"
1590
+ " {'type':'tool_execution_end','toolCallId':'c1','toolName':'bash','result':{'content':[{'type':'text','text':'ABC'}]},'isError':False},\n"
1591
+ " {'type':'message_update','assistantMessageEvent':{'type':'text_start','contentIndex':1}},\n"
1592
+ " {'type':'message_update','assistantMessageEvent':{'type':'text_delta','contentIndex':1,'delta':'Done'}},\n"
1593
+ " {'type':'message_update','assistantMessageEvent':{'type':'text_end','contentIndex':1,'content':'Done'}},\n"
1594
+ " {'type':'message_end','message':{'role':'assistant','content':[{'type':'text','text':'Done'}],'stopReason':'stop'}},\n"
1595
+ " {'type':'agent_end','willRetry':False,'messages':[{'role':'assistant','content':[{'type':'text','text':'Done'}],'stopReason':'stop'}]},\n"
1596
+ "]\n"
1597
+ "for event in events: print(json.dumps(event), flush=True)\n",
1598
+ encoding="utf-8",
1599
+ )
1600
+ fake_cli.chmod(0o755)
1601
+ prompt = tmp_path / "prompt.md"
1602
+ prompt.write_text("hello", encoding="utf-8")
1603
+ session_log = tmp_path / "state" / "sessions" / "sess-normalized" / "logs" / "session.log"
1604
+ backup_log = tmp_path / "home" / ".prizmkit" / "session-backups" / "sess-normalized.log"
1605
+ progress = session_log.with_name("progress.json")
1606
+ captured_backup = {}
1607
+ original_recover = sessions.recover_session_log
1608
+
1609
+ def capture_recovery(primary, backup, min_size=1024):
1610
+ captured_backup["text"] = backup.read_text(encoding="utf-8")
1611
+ return original_recover(primary, backup, min_size)
1612
+
1613
+ monkeypatch.setattr(sessions, "recover_session_log", capture_recovery)
1614
+ result = sessions.AISessionLauncher(
1615
+ sessions.AISessionConfig(
1616
+ cli_command=str(fake_cli),
1617
+ platform="pi",
1618
+ model=None,
1619
+ prompt_path=prompt,
1620
+ cwd=tmp_path,
1621
+ log_path=session_log,
1622
+ backup_log_path=backup_log,
1623
+ progress_path=progress,
1624
+ )
1625
+ ).run()
1626
+
1627
+ text = session_log.read_text(encoding="utf-8")
1628
+ assert result.exit_code == 0
1629
+ assert result.progress_summary.result_subtype == "success"
1630
+ assert "[THINK] Think" in text
1631
+ assert "[ACTION] bash" in text
1632
+ assert "[OBSERVATION] ABC" in text
1633
+ assert "[FINAL] Done" in text
1634
+ assert "[SESSION_END] status=success" in text
1635
+ assert text.count("ABC") == 2 # one action argument and one observation
1636
+ assert '"type": "message_update"' not in text
1637
+ assert '"type":"message_update"' not in text
1638
+ assert captured_backup["text"] == text
1639
+ assert not backup_log.exists()
1640
+
1641
+
1187
1642
  def test_custom_session_launch_writes_existing_logs_and_progress(tmp_path):
1188
1643
  from prizmkit_runtime.launch_profiles import HeadlessLaunchProfile
1189
1644
  from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.145",
2
+ "version": "1.1.146",
3
3
  "skills": {
4
4
  "prizmkit": {
5
5
  "description": "Framework introduction and navigation for the formal single-requirement lifecycle, project initialization, Prizm docs, and independent deployment.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prizmkit",
3
- "version": "1.1.145",
3
+ "version": "1.1.146",
4
4
  "description": "Create a new PrizmKit-powered project with clean initialization — no framework dev files, just what you need.",
5
5
  "type": "module",
6
6
  "bin": {