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 +5 -0
- package/index.js +5 -1
- package/package.json +1 -1
- package/scripts/codex/cozeloop_hook.py +155 -5
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
|
@@ -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
|
-
|
|
1051
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|