prizmkit 1.1.144 → 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.
@@ -963,6 +963,357 @@ def test_progress_parser_normalizes_pi_json_events():
963
963
  assert {item["name"]: item["count"] for item in data["tool_calls"]}["edit"] == 1
964
964
  assert data["current_phase"] == "implement"
965
965
  assert data["is_active"] is False
966
+ assert data["result_subtype"] == "success"
967
+ assert data["terminal_success_at"]
968
+
969
+
970
+ def test_progress_parser_does_not_mark_failed_pi_agent_end_as_success():
971
+ module = _load_progress_parser_module()
972
+ tracker = module.ProgressTracker()
973
+ tracker.process_event({"type": "session", "version": 3, "id": "pi-session", "cwd": "/tmp/project"})
974
+ tracker.process_event({"type": "agent_start"})
975
+ tracker.process_event({
976
+ "type": "message_end",
977
+ "message": {
978
+ "role": "assistant",
979
+ "content": [],
980
+ "stopReason": "error",
981
+ "errorMessage": "provider request failed",
982
+ },
983
+ })
984
+ tracker.process_event({"type": "agent_end", "messages": []})
985
+
986
+ data = tracker.to_dict()
987
+
988
+ assert data["event_format"] == "pi-json"
989
+ assert data["is_active"] is False
990
+ assert data["result_subtype"] == "error"
991
+ assert data["stop_reason"] == "error"
992
+ assert data["last_result_is_error"] is True
993
+ assert data["terminal_success_at"] == ""
994
+ assert "provider request failed" in data["errors"]
995
+
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
966
1317
 
967
1318
 
968
1319
  def test_progress_parser_normalizes_claude_subagents_without_double_counting(tmp_path):
@@ -1061,10 +1412,14 @@ def test_progress_parser_normalizes_codex_collab_metadata_with_missing_fields():
1061
1412
  },
1062
1413
  })
1063
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}})
1064
1416
 
1065
1417
  data = tracker.to_dict()
1066
1418
 
1067
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
1068
1423
  assert data["child_thread_ids"] == ["thread-a", "thread-b"]
1069
1424
  assert data["subagent_spawn_count"] == 2
1070
1425
  assert data["active_subagent_count"] == 1
@@ -1155,6 +1510,135 @@ def test_session_dual_writes_progress_and_recovers_backup(tmp_path):
1155
1510
  assert second_log.read_text(encoding="utf-8") == "larger recovered log"
1156
1511
 
1157
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
+
1158
1642
  def test_custom_session_launch_writes_existing_logs_and_progress(tmp_path):
1159
1643
  from prizmkit_runtime.launch_profiles import HeadlessLaunchProfile
1160
1644
  from prizmkit_runtime.sessions import AISessionConfig, AISessionLauncher
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.144",
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.144",
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": {