coze_lab 0.1.15 → 0.1.17

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/README.md CHANGED
@@ -61,6 +61,11 @@ For cloud Codex with `--cloud --agent-id=<agentId>`, Codex hooks are written to
61
61
  does not already exist, so callers do not need to pass `--codex-home` for the
62
62
  standard coze-bridge layout.
63
63
 
64
+ Codex hook diagnostics are appended to `hooks/cozeloop.log` under the same
65
+ Codex home. For cloud Codex, check
66
+ `~/.coze/agents/<agentId>/codex-home/hooks/cozeloop.log`. If that file is not
67
+ created after a new Codex turn, Codex did not load or execute the hook.
68
+
64
69
  ## Token lifecycle
65
70
 
66
71
  OAuth tokens are stored in `~/.cozeloop/credentials.json` (mode 600).
package/index.js CHANGED
@@ -4554,6 +4554,7 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
4554
4554
  const hooksDir = path.join(home, 'hooks');
4555
4555
  const hookScript = path.join(hooksDir, 'cozeloop_hook.py');
4556
4556
  const envFile = path.join(hooksDir, 'cozeloop.env');
4557
+ const logFile = path.join(hooksDir, 'cozeloop.log');
4557
4558
  const hooksJson = path.join(home, 'hooks.json');
4558
4559
 
4559
4560
  // 1. Write Python hook scripts (trace + refresh)
@@ -4571,6 +4572,8 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
4571
4572
  if (!cloud) {
4572
4573
  envLines.push(`COZELOOP_API_TOKEN=${token}`);
4573
4574
  }
4575
+ envLines.push(`CODEX_HOME=${home}`);
4576
+ envLines.push(`COZELOOP_HOOK_LOG=${logFile}`);
4574
4577
  envLines.push('TRACE_TO_COZELOOP=true');
4575
4578
  // PPE 泳道:cozeloop SDK 读这两个环境变量,自动注入 x-tt-env / x-use-ppe header
4576
4579
  envLines.push(`x_tt_env=${PPE_TT_ENV}`);
@@ -4615,7 +4618,7 @@ function writeCodexHook(token, workspaceId, pythonCmd, codexHome, cloud) {
4615
4618
  ok(`Hook registered in ${hooksJson}`);
4616
4619
  warn('Codex hook trust: 首次启动 Codex 会提示 "Hooks need review"。在该提示按 t(Trust all and continue),或在会话内运行 /hooks 后按 t,即可一次性信任全部 hook 启用 trace 上报。');
4617
4620
 
4618
- return { hookScript, envFile, hooksJson };
4621
+ return { hookScript, envFile, hooksJson, logFile };
4619
4622
  }
4620
4623
 
4621
4624
  function resolveCodexHome(args) {
@@ -5325,6 +5328,7 @@ async function main() {
5325
5328
  summaryLines.push(`Hook script: ${written.hookScript || '~/.codex/hooks/cozeloop_hook.py'}`);
5326
5329
  summaryLines.push(`Config: ${written.hooksJson || '~/.codex/hooks.json'}`);
5327
5330
  summaryLines.push(`Credentials: ${written.envFile || '~/.codex/hooks/cozeloop.env'}`);
5331
+ if (written.logFile) summaryLines.push(`Hook log: ${written.logFile}`);
5328
5332
  } else {
5329
5333
  summaryLines.push(`Config: ~/.openclaw/openclaw.json`);
5330
5334
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coze_lab",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Configure local AI agents (Claude Code, Codex, OpenClaw) to report traces to CozeLoop",
5
5
  "keywords": [
6
6
  "cozeloop",
@@ -130,12 +130,15 @@ def _make_finish_event_processor():
130
130
  def _processor(info):
131
131
  try:
132
132
  if not getattr(info, "is_event_fail", False):
133
+ hook_log("upload success")
133
134
  return
134
135
  detail = getattr(info, "detail_msg", "") or ""
135
136
  logid = _extract_logid(detail)
136
137
  if logid:
138
+ hook_log(f"upload failed logid={logid} detail={detail[:500]}")
137
139
  print(f"[CozeLoop] 上报失败 logid={logid} (可用 bytedcli log get-logid-log {logid} 排查)", file=sys.stderr)
138
140
  else:
141
+ hook_log(f"upload failed detail={detail[:500]}")
139
142
  print(f"[CozeLoop] 上报失败: {detail[:300]}", file=sys.stderr)
140
143
  except Exception:
141
144
  pass
@@ -255,8 +258,27 @@ else:
255
258
  DEBUG = os.environ.get("CC_COZELOOP_DEBUG", "").lower() == "true"
256
259
 
257
260
 
261
+ def _log_file_path() -> str:
262
+ return os.environ.get("COZELOOP_HOOK_LOG", "").strip()
263
+
264
+
265
+ def hook_log(message: str):
266
+ """Append one diagnostic line to the hook log, if configured."""
267
+ log_path = _log_file_path()
268
+ if not log_path:
269
+ return
270
+ try:
271
+ p = Path(log_path).expanduser()
272
+ p.parent.mkdir(parents=True, exist_ok=True)
273
+ with p.open("a", encoding="utf-8") as f:
274
+ f.write(f"{datetime.now().isoformat()} {message}\n")
275
+ except Exception:
276
+ pass
277
+
278
+
258
279
  def debug_log(message: str):
259
280
  """Print debug message if debug mode is enabled."""
281
+ hook_log(f"DEBUG {message}")
260
282
  if DEBUG:
261
283
  print(f"[COZELOOP_HOOK_DEBUG] {datetime.now().isoformat()} - {message}", file=sys.stderr)
262
284
 
@@ -344,6 +366,101 @@ def read_rollout_messages(transcript_path: str, start_line: int = 0) -> List[Dic
344
366
  return entries
345
367
 
346
368
 
369
+ def _add_unique_path(paths: List[Path], p: Optional[Path]):
370
+ if not p:
371
+ return
372
+ try:
373
+ resolved = p.expanduser().resolve()
374
+ except Exception:
375
+ return
376
+ if resolved not in paths:
377
+ paths.append(resolved)
378
+
379
+
380
+ def _candidate_codex_homes() -> List[Path]:
381
+ homes: List[Path] = []
382
+ _add_unique_path(homes, Path(os.environ["CODEX_HOME"]) if os.environ.get("CODEX_HOME") else None)
383
+
384
+ log_path = _log_file_path()
385
+ if log_path:
386
+ try:
387
+ log_parent = Path(log_path).expanduser().resolve().parent
388
+ if log_parent.name == "hooks":
389
+ _add_unique_path(homes, log_parent.parent)
390
+ except Exception:
391
+ pass
392
+
393
+ _add_unique_path(homes, Path.home() / ".codex")
394
+
395
+ agents_root = Path.home() / ".coze" / "agents"
396
+ try:
397
+ if agents_root.exists():
398
+ for child in agents_root.iterdir():
399
+ _add_unique_path(homes, child / "codex-home")
400
+ except Exception:
401
+ pass
402
+
403
+ return homes
404
+
405
+
406
+ def _latest_file(paths: List[Path]) -> Optional[Path]:
407
+ existing = []
408
+ for p in paths:
409
+ try:
410
+ if p.is_file():
411
+ existing.append(p)
412
+ except Exception:
413
+ pass
414
+ if not existing:
415
+ return None
416
+ return max(existing, key=lambda p: p.stat().st_mtime)
417
+
418
+
419
+ def find_latest_transcript() -> Optional[str]:
420
+ """Best-effort fallback when Codex does not pass hook stdin."""
421
+ candidates: List[Path] = []
422
+ for codex_home in _candidate_codex_homes():
423
+ sessions_dir = codex_home / "sessions"
424
+ if not sessions_dir.exists():
425
+ continue
426
+ try:
427
+ candidates.extend(sessions_dir.rglob("rollout-*.jsonl"))
428
+ except Exception as e:
429
+ hook_log(f"fallback scan failed dir={sessions_dir} error={repr(e)}")
430
+
431
+ latest = _latest_file(candidates)
432
+ if latest:
433
+ return str(latest)
434
+
435
+ for codex_home in _candidate_codex_homes():
436
+ sessions_dir = codex_home / "sessions"
437
+ if not sessions_dir.exists():
438
+ continue
439
+ try:
440
+ candidates.extend(sessions_dir.rglob("*.jsonl"))
441
+ except Exception:
442
+ pass
443
+
444
+ latest = _latest_file(candidates)
445
+ return str(latest) if latest else None
446
+
447
+
448
+ def recover_hook_input(reason: str) -> Optional[Dict[str, Any]]:
449
+ transcript_path = find_latest_transcript()
450
+ if not transcript_path:
451
+ hook_log(f"fallback failed reason={reason} no transcript found")
452
+ print(f"[CozeLoop] Hook input missing ({reason}); no Codex transcript found.", file=sys.stderr)
453
+ return None
454
+ hook_log(f"fallback transcript reason={reason} path={transcript_path}")
455
+ print(f"[CozeLoop] Hook input missing ({reason}); fallback transcript: {transcript_path}", file=sys.stderr)
456
+ return {
457
+ "hook_event_name": "Stop",
458
+ "session_id": "",
459
+ "transcript_path": transcript_path,
460
+ "input_fallback": reason,
461
+ }
462
+
463
+
347
464
  def parse_session_meta(entries: List[Dict[str, Any]]) -> Dict[str, Any]:
348
465
  """Extract session identity from session_meta entry."""
349
466
  result = {
@@ -690,12 +807,15 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
690
807
  token = get_fresh_token()
691
808
  if token:
692
809
  os.environ["COZELOOP_API_TOKEN"] = token
810
+ hook_log(f"token resolved prefix={token[:12]}...")
693
811
  print(f"[CozeLoop] Token 获取成功 ({token[:12]}...)", file=sys.stderr)
694
812
  else:
813
+ hook_log("token missing")
695
814
  print("[CozeLoop] 警告: 未找到有效 Token,上报可能失败", file=sys.stderr)
696
815
  creds = _load_credentials()
697
816
  workspace_id = (creds or {}).get("workspace_id") or os.environ.get("COZELOOP_WORKSPACE_ID", "") or _DEFAULT_WORKSPACE_ID
698
817
  os.environ["COZELOOP_WORKSPACE_ID"] = workspace_id
818
+ hook_log(f"workspace_id={workspace_id}")
699
819
  client_kwargs = {
700
820
  "ultra_large_report": True,
701
821
  "upload_timeout": 120,
@@ -1018,13 +1138,16 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
1018
1138
  debug_log(f"Error processing turn {i}: {e}")
1019
1139
  continue
1020
1140
 
1141
+ hook_log(f"processed turns={len(turns)} session_id={session_id}")
1021
1142
  debug_log(f"Successfully processed {len(turns)} turn(s) for session {session_id}")
1022
1143
 
1023
1144
  except Exception as e:
1145
+ hook_log(f"send exception={repr(e)}")
1024
1146
  debug_log(f"An error occurred while sending traces to CozeLoop: {e}")
1025
1147
  return None
1026
1148
  finally:
1027
1149
  client.close()
1150
+ hook_log("client closed")
1028
1151
  debug_log("CozeLoop client closed.")
1029
1152
 
1030
1153
  return ctx
@@ -1035,10 +1158,12 @@ def send_turns_to_cozeloop(turns: List[Dict[str, Any]], session_id: str, model_n
1035
1158
  def main():
1036
1159
  """Main entry point for the Codex CozeLoop hook."""
1037
1160
  print("[CozeLoop] Hook triggered (Codex).", file=sys.stderr)
1161
+ hook_log("hook triggered")
1038
1162
  debug_log("Codex CozeLoop hook started.")
1039
1163
 
1040
1164
  # Check if tracing is enabled
1041
1165
  if os.environ.get("TRACE_TO_COZELOOP", "").lower() == "false":
1166
+ hook_log("skip trace disabled")
1042
1167
  debug_log("TRACE_TO_COZELOOP is set to 'false', skipping")
1043
1168
  return
1044
1169
 
@@ -1046,24 +1171,41 @@ def main():
1046
1171
  try:
1047
1172
  raw_input = sys.stdin.read().strip()
1048
1173
  if not raw_input:
1174
+ hook_log("stdin empty, trying fallback")
1049
1175
  debug_log("No input received from stdin")
1050
- return
1051
- hook_input = json.loads(raw_input)
1176
+ hook_input = recover_hook_input("empty_stdin")
1177
+ if not hook_input:
1178
+ return
1179
+ else:
1180
+ hook_input = json.loads(raw_input)
1052
1181
  except Exception as e:
1182
+ hook_log(f"stdin parse error={repr(e)}, trying fallback")
1053
1183
  debug_log(f"Error reading hook input from stdin: {e}")
1054
- return
1184
+ hook_input = recover_hook_input("stdin_parse_error")
1185
+ if not hook_input:
1186
+ return
1055
1187
 
1056
1188
  debug_log(f"Hook input: {json.dumps(hook_input, ensure_ascii=False)}")
1057
1189
 
1058
1190
  # Get transcript path
1059
1191
  transcript_path = hook_input.get("transcript_path")
1060
1192
  if not transcript_path:
1193
+ hook_log("missing transcript_path, trying fallback")
1061
1194
  debug_log("No transcript_path in hook input")
1062
- return
1195
+ recovered = recover_hook_input("missing_transcript_path")
1196
+ if not recovered:
1197
+ return
1198
+ hook_input = recovered
1199
+ transcript_path = hook_input.get("transcript_path")
1063
1200
 
1064
1201
  if not os.path.exists(transcript_path):
1202
+ hook_log(f"transcript not found path={transcript_path}, trying fallback")
1065
1203
  debug_log(f"Transcript file not found: {transcript_path}")
1066
- return
1204
+ recovered = recover_hook_input("transcript_not_found")
1205
+ if not recovered:
1206
+ return
1207
+ hook_input = recovered
1208
+ transcript_path = hook_input.get("transcript_path")
1067
1209
 
1068
1210
  # Load state
1069
1211
  state_file = get_state_file_path(transcript_path)
@@ -1073,9 +1215,11 @@ def main():
1073
1215
  entries = read_rollout_messages(transcript_path, state["last_processed_line"])
1074
1216
 
1075
1217
  if not entries:
1218
+ hook_log(f"skip no new entries transcript={transcript_path}")
1076
1219
  debug_log("No new entries to process")
1077
1220
  return
1078
1221
 
1222
+ hook_log(f"read entries={len(entries)} from_line={state['last_processed_line']} transcript={transcript_path}")
1079
1223
  debug_log(f"Read {len(entries)} new entries from line {state['last_processed_line']}")
1080
1224
 
1081
1225
  # Parse session identity
@@ -1125,6 +1269,7 @@ def main():
1125
1269
  last_line = max(e.get("_line_number", 0) for e in entries) + 1
1126
1270
  state["last_processed_line"] = last_line
1127
1271
  save_state(state_file, state)
1272
+ hook_log(f"subagent saved session_id={session_id} turns={len(turns[-1:])} last_line={last_line}")
1128
1273
  debug_log("Subagent data saved, hook completed")
1129
1274
  return
1130
1275
 
@@ -1135,6 +1280,7 @@ def main():
1135
1280
  for t in turns
1136
1281
  )
1137
1282
  if not has_coze_ctx:
1283
+ hook_log(f"skip no coze-context turns={len(turns)} session_id={session_id}")
1138
1284
  debug_log("No coze-context found in any turn, skipping upload.")
1139
1285
  return
1140
1286
  history_context = state.get("conversation_history", [])
@@ -1147,12 +1293,16 @@ def main():
1147
1293
  state["last_processed_line"] = last_line
1148
1294
  state["conversation_history"] = updated_history
1149
1295
  save_state(state_file, state)
1296
+ hook_log(f"state advanced last_line={last_line} session_id={session_id}")
1150
1297
  debug_log(f"State updated, last processed line: {last_line}")
1151
1298
  else:
1299
+ hook_log(f"send failed state not advanced session_id={session_id}")
1152
1300
  debug_log("Send failed, state not advanced")
1153
1301
  else:
1302
+ hook_log(f"skip no turns session_id={session_id}")
1154
1303
  debug_log("No turns to send")
1155
1304
 
1305
+ hook_log("hook completed")
1156
1306
  debug_log("Codex CozeLoop hook completed.")
1157
1307
 
1158
1308